feat: silent wechat login, marketing tab optimization, admin page foundation

- Add silent WeChat login for MP/browser environments
- Fix Python 3.6 compatibility (remove typing.Annotated usage)
- Marketing page: tab-based content generation with category support
- Translate page: add auto-detect language default
- Homepage: add TTS playback, announcement ticker, remove redundant quick-actions
- Fix FAB button overlap with custom tabbar on customers/quotation pages
- Make openai/anthropic imports lazy for Python 3.6 compat
This commit is contained in:
TradeMate Dev
2026-05-14 00:30:48 +08:00
parent f70dd24c7d
commit 23a31f7c00
30 changed files with 485 additions and 269 deletions
BIN
View File
Binary file not shown.
+7 -1
View File
@@ -1,6 +1,5 @@
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import json import json
from anthropic import AsyncAnthropic
from app.ai.base import AIProvider from app.ai.base import AIProvider
@@ -19,6 +18,13 @@ SYSTEM_PROMPTS = {
class ClaudeProvider(AIProvider): class ClaudeProvider(AIProvider):
def __init__(self, api_key: str, model: str = "claude-sonnet-4-20250514"): def __init__(self, api_key: str, model: str = "claude-sonnet-4-20250514"):
try:
from anthropic import AsyncAnthropic
except ImportError:
raise ImportError(
"anthropic SDK is required for ClaudeProvider. "
"Install it with: pip install anthropic"
)
self.client = AsyncAnthropic(api_key=api_key) self.client = AsyncAnthropic(api_key=api_key)
self.model = model self.model = model
self._name = f"claude-sonnet" self._name = f"claude-sonnet"
+7 -1
View File
@@ -1,6 +1,5 @@
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import json import json
from openai import AsyncOpenAI
from app.ai.base import AIProvider from app.ai.base import AIProvider
@@ -20,6 +19,13 @@ SYSTEM_PROMPTS = {
class OpenAIProvider(AIProvider): class OpenAIProvider(AIProvider):
def __init__(self, api_key: str, model: str = "gpt-4o", base_url: Optional[str] = None): def __init__(self, api_key: str, model: str = "gpt-4o", base_url: Optional[str] = None):
try:
from openai import AsyncOpenAI
except ImportError:
raise ImportError(
"openai>=1.0 is required for OpenAIProvider. "
"Install it with: pip install 'openai>=1.0'"
)
kwargs = {"api_key": api_key} kwargs = {"api_key": api_key}
if base_url: if base_url:
kwargs["base_url"] = base_url kwargs["base_url"] = base_url
+4 -1
View File
@@ -1,6 +1,5 @@
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import json import json
from openai import AsyncOpenAI
from app.ai.base import AIProvider from app.ai.base import AIProvider
@@ -18,6 +17,10 @@ SYSTEM_PROMPTS = {
class SparkProvider(AIProvider): class SparkProvider(AIProvider):
def __init__(self, api_key: str, model: str = "astron-code-latest", base_url: str = None): def __init__(self, api_key: str, model: str = "astron-code-latest", base_url: str = None):
from app.config import settings from app.config import settings
try:
from openai import AsyncOpenAI
except ImportError:
raise ImportError("openai>=1.0 is required for SparkProvider")
self.client = AsyncOpenAI( self.client = AsyncOpenAI(
api_key=api_key, api_key=api_key,
base_url=base_url or settings.IFLYTEK_API_BASE, base_url=base_url or settings.IFLYTEK_API_BASE,
+5 -6
View File
@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from app.database import get_db from app.database import get_db
from app.services.admin import AdminService from app.services.admin import AdminService
from app.api.v1.deps import get_current_user from app.api.v1.deps import get_current_user
@@ -17,7 +16,7 @@ async def require_admin(current_user: dict = Depends(get_current_user)) -> dict:
@router.get("/dashboard") @router.get("/dashboard")
async def get_dashboard( async def get_dashboard(
_: dict = Depends(require_admin), _: dict = Depends(require_admin),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AdminService(db) service = AdminService(db)
return await service.get_dashboard() return await service.get_dashboard()
@@ -28,7 +27,7 @@ async def list_users(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
_: dict = Depends(require_admin), _: dict = Depends(require_admin),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AdminService(db) service = AdminService(db)
return await service.list_users(page, size) return await service.list_users(page, size)
@@ -39,7 +38,7 @@ async def update_user_tier(
target_user_id: str, target_user_id: str,
data: dict, data: dict,
_: dict = Depends(require_admin), _: dict = Depends(require_admin),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AdminService(db) service = AdminService(db)
tier = data.get("tier") tier = data.get("tier")
@@ -55,7 +54,7 @@ async def update_user_tier(
async def toggle_user_active( async def toggle_user_active(
target_user_id: str, target_user_id: str,
_: dict = Depends(require_admin), _: dict = Depends(require_admin),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AdminService(db) service = AdminService(db)
success = await service.toggle_user_active(target_user_id) success = await service.toggle_user_active(target_user_id)
@@ -66,7 +65,7 @@ async def toggle_user_active(
@router.get("/health") @router.get("/health")
async def system_health( async def system_health(
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AdminService(db) service = AdminService(db)
return await service.get_system_health() return await service.get_system_health()
+6 -7
View File
@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from app.database import get_db from app.database import get_db
from app.services.analytics import AnalyticsService from app.services.analytics import AnalyticsService
from app.api.v1.deps import get_current_user_id from app.api.v1.deps import get_current_user_id
@@ -11,7 +10,7 @@ router = APIRouter()
@router.get("/customers") @router.get("/customers")
async def customer_analytics( async def customer_analytics(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AnalyticsService(db) service = AnalyticsService(db)
return await service.get_customer_stats(user_id) return await service.get_customer_stats(user_id)
@@ -20,7 +19,7 @@ async def customer_analytics(
@router.get("/translations") @router.get("/translations")
async def translation_analytics( async def translation_analytics(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AnalyticsService(db) service = AnalyticsService(db)
return await service.get_translation_stats(user_id) return await service.get_translation_stats(user_id)
@@ -29,7 +28,7 @@ async def translation_analytics(
@router.get("/quotations") @router.get("/quotations")
async def quotation_analytics( async def quotation_analytics(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AnalyticsService(db) service = AnalyticsService(db)
return await service.get_quotation_stats(user_id) return await service.get_quotation_stats(user_id)
@@ -38,7 +37,7 @@ async def quotation_analytics(
@router.get("/messages") @router.get("/messages")
async def message_analytics( async def message_analytics(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AnalyticsService(db) service = AnalyticsService(db)
return await service.get_message_stats(user_id) return await service.get_message_stats(user_id)
@@ -47,7 +46,7 @@ async def message_analytics(
@router.get("/overview") @router.get("/overview")
async def overview( async def overview(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AnalyticsService(db) service = AnalyticsService(db)
customers = await service.get_customer_stats(user_id) customers = await service.get_customer_stats(user_id)
@@ -67,7 +66,7 @@ async def overview(
@router.get("/marketing") @router.get("/marketing")
async def marketing_analytics( async def marketing_analytics(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = AnalyticsService(db) service = AnalyticsService(db)
return await service.get_marketing_stats(user_id) return await service.get_marketing_stats(user_id)
+16 -7
View File
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Header
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from typing import Annotated, Optional from typing import Optional
import uuid import uuid
from app.database import get_db from app.database import get_db
from app.models.user import User from app.models.user import User
@@ -31,7 +31,7 @@ class RefreshRequest(BaseModel):
@router.post("/register") @router.post("/register")
async def register(data: RegisterRequest, db: Annotated[AsyncSession, Depends(get_db)] = None): async def register(data: RegisterRequest, db: AsyncSession = Depends(get_db)):
existing = await db.execute(select(User).where(User.phone == data.phone)) existing = await db.execute(select(User).where(User.phone == data.phone))
if existing.scalar_one_or_none(): if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Phone already registered") raise HTTPException(status_code=400, detail="Phone already registered")
@@ -56,8 +56,8 @@ async def register(data: RegisterRequest, db: Annotated[AsyncSession, Depends(ge
@router.post("/login", response_model=LoginResponse) @router.post("/login", response_model=LoginResponse)
async def login( async def login(
form: Annotated[OAuth2PasswordRequestForm, Depends()], form: OAuth2PasswordRequestForm = Depends(),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
result = await db.execute(select(User).where(User.phone == form.username)) result = await db.execute(select(User).where(User.phone == form.username))
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
@@ -128,7 +128,7 @@ async def refresh(data: RefreshRequest):
@router.get("/me") @router.get("/me")
async def get_me( async def get_me(
authorization: Optional[str] = Header(None, alias="Authorization"), authorization: Optional[str] = Header(None, alias="Authorization"),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
if not authorization or not authorization.startswith("Bearer "): if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing token") raise HTTPException(status_code=401, detail="Missing token")
@@ -178,8 +178,17 @@ class WeChatLoginRequest(BaseModel):
iv: str = "" iv: str = ""
@router.get("/wechat/config")
async def wechat_config():
from app.config import settings
return {
"available": bool(settings.WECHAT_APP_ID and settings.WECHAT_APP_SECRET),
"app_id": settings.WECHAT_APP_ID or "",
}
@router.post("/wechat-login") @router.post("/wechat-login")
async def wechat_login(data: WeChatLoginRequest, db: Annotated[AsyncSession, Depends(get_db)] = None): async def wechat_login(data: WeChatLoginRequest, db: AsyncSession = Depends(get_db)):
from app.services.wechat import wechat_service from app.services.wechat import wechat_service
session = await wechat_service.code2session(data.code) session = await wechat_service.code2session(data.code)
@@ -216,7 +225,7 @@ async def wechat_login(data: WeChatLoginRequest, db: Annotated[AsyncSession, Dep
async def update_settings( async def update_settings(
data: SettingsUpdate, data: SettingsUpdate,
authorization: Optional[str] = Header(None, alias="Authorization"), authorization: Optional[str] = Header(None, alias="Authorization"),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
if not authorization or not authorization.startswith("Bearer "): if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing token") raise HTTPException(status_code=401, detail="Missing token")
+13 -13
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Response from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated, Optional, List from typing import Optional, List
from app.database import get_db from app.database import get_db
from app.services.customer import CustomerService from app.services.customer import CustomerService
from app.services.customer_health import CustomerHealthService from app.services.customer_health import CustomerHealthService
@@ -18,7 +18,7 @@ async def list_customers(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerService(db) service = CustomerService(db)
return await service.list_customers(user_id, status, page, size) return await service.list_customers(user_id, status, page, size)
@@ -28,7 +28,7 @@ async def list_customers(
async def get_silent( async def get_silent(
days: int = Query(3, ge=1), days: int = Query(3, ge=1),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerService(db) service = CustomerService(db)
customers = await service.get_silent_customers(user_id, days) customers = await service.get_silent_customers(user_id, days)
@@ -43,7 +43,7 @@ async def get_silent(
async def get_customer( async def get_customer(
customer_id: str, customer_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerService(db) service = CustomerService(db)
customer = await service.get_customer(user_id, customer_id) customer = await service.get_customer(user_id, customer_id)
@@ -56,7 +56,7 @@ async def get_customer(
async def create_customer( async def create_customer(
data: dict, data: dict,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerService(db) service = CustomerService(db)
customer = await service.create_customer(user_id, data) customer = await service.create_customer(user_id, data)
@@ -68,7 +68,7 @@ async def update_customer(
customer_id: str, customer_id: str,
data: dict, data: dict,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerService(db) service = CustomerService(db)
customer = await service.update_customer(user_id, customer_id, data) customer = await service.update_customer(user_id, customer_id, data)
@@ -81,7 +81,7 @@ async def update_customer(
async def delete_customer( async def delete_customer(
customer_id: str, customer_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerService(db) service = CustomerService(db)
deleted = await service.delete_customer(user_id, customer_id) deleted = await service.delete_customer(user_id, customer_id)
@@ -94,7 +94,7 @@ async def delete_customer(
async def import_customers( async def import_customers(
file: UploadFile = File(...), file: UploadFile = File(...),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
from app.workers.tasks import process_customer_import from app.workers.tasks import process_customer_import
@@ -135,7 +135,7 @@ async def import_customers(
async def export_customers( async def export_customers(
status: Optional[str] = None, status: Optional[str] = None,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerService(db) service = CustomerService(db)
result = await service.list_customers(user_id, status, 1, 9999) result = await service.list_customers(user_id, status, 1, 9999)
@@ -151,7 +151,7 @@ async def export_customers(
@router.get("/health-overview") @router.get("/health-overview")
async def get_health_overview( async def get_health_overview(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerHealthService(db) service = CustomerHealthService(db)
return await service.get_health_overview(user_id) return await service.get_health_overview(user_id)
@@ -160,7 +160,7 @@ async def get_health_overview(
@router.get("/health-scores") @router.get("/health-scores")
async def get_all_health_scores( async def get_all_health_scores(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerHealthService(db) service = CustomerHealthService(db)
return {"items": await service.get_all_health_scores(user_id)} return {"items": await service.get_all_health_scores(user_id)}
@@ -170,7 +170,7 @@ async def get_all_health_scores(
async def get_customer_health( async def get_customer_health(
customer_id: str, customer_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerHealthService(db) service = CustomerHealthService(db)
health = await service.get_customer_health(user_id, customer_id) health = await service.get_customer_health(user_id, customer_id)
@@ -185,7 +185,7 @@ async def get_conversation(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(50, ge=1, le=200), size: int = Query(50, ge=1, le=200),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = CustomerService(db) service = CustomerService(db)
return await service.get_conversation(user_id, customer_id, page, size) return await service.get_conversation(user_id, customer_id, page, size)
+1 -2
View File
@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from pydantic import BaseModel from pydantic import BaseModel
from app.database import get_db from app.database import get_db
from app.models.feedback import Feedback from app.models.feedback import Feedback
@@ -19,7 +18,7 @@ class FeedbackRequest(BaseModel):
async def submit_feedback( async def submit_feedback(
data: FeedbackRequest, data: FeedbackRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
if not data.content.strip(): if not data.content.strip():
raise HTTPException(status_code=400, detail="Content is required") raise HTTPException(status_code=400, detail="Content is required")
+8 -8
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated, Optional from typing import Optional
from app.database import get_db from app.database import get_db
from app.services.followup_engine import FollowupEngine from app.services.followup_engine import FollowupEngine
from app.api.v1.deps import get_current_user_id from app.api.v1.deps import get_current_user_id
@@ -11,7 +11,7 @@ router = APIRouter()
@router.get("/strategies") @router.get("/strategies")
async def list_strategies( async def list_strategies(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
engine = FollowupEngine(db) engine = FollowupEngine(db)
await engine.ensure_default_strategies() await engine.ensure_default_strategies()
@@ -23,7 +23,7 @@ async def get_pending_followups(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
engine = FollowupEngine(db) engine = FollowupEngine(db)
return await engine.get_pending_followups(user_id, page, size) return await engine.get_pending_followups(user_id, page, size)
@@ -34,7 +34,7 @@ async def get_followup_logs(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
engine = FollowupEngine(db) engine = FollowupEngine(db)
return await engine.get_followup_logs(user_id, page, size) return await engine.get_followup_logs(user_id, page, size)
@@ -44,7 +44,7 @@ async def get_followup_logs(
async def mark_followup_sent( async def mark_followup_sent(
log_id: str, log_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
engine = FollowupEngine(db) engine = FollowupEngine(db)
success = await engine.mark_sent(user_id, log_id) success = await engine.mark_sent(user_id, log_id)
@@ -58,7 +58,7 @@ async def edit_and_send_followup(
log_id: str, log_id: str,
body: dict, body: dict,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
edited_text = body.get("edited_text", "") edited_text = body.get("edited_text", "")
if not edited_text: if not edited_text:
@@ -73,7 +73,7 @@ async def edit_and_send_followup(
@router.get("/stats") @router.get("/stats")
async def get_followup_stats( async def get_followup_stats(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
engine = FollowupEngine(db) engine = FollowupEngine(db)
return await engine.get_stats(user_id) return await engine.get_stats(user_id)
@@ -82,7 +82,7 @@ async def get_followup_stats(
@router.post("/scan") @router.post("/scan")
async def trigger_followup_scan( async def trigger_followup_scan(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
engine = FollowupEngine(db) engine = FollowupEngine(db)
result = await engine.scan_and_followup() result = await engine.scan_and_followup()
+7 -8
View File
@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from app.database import get_db from app.database import get_db
from app.services.preference import UserPreferenceService from app.services.preference import UserPreferenceService
from app.services.marketing_effect import MarketingEffectService from app.services.marketing_effect import MarketingEffectService
@@ -13,7 +12,7 @@ router = APIRouter()
async def record_selection( async def record_selection(
data: dict, data: dict,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
message_id = data.get("message_id") message_id = data.get("message_id")
selected_index = data.get("selected_index") selected_index = data.get("selected_index")
@@ -30,7 +29,7 @@ async def record_selection(
async def record_edit( async def record_edit(
data: dict, data: dict,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
message_id = data.get("message_id") message_id = data.get("message_id")
edited_text = data.get("edited_text") edited_text = data.get("edited_text")
@@ -46,7 +45,7 @@ async def record_edit(
@router.post("/analyze") @router.post("/analyze")
async def analyze_preferences( async def analyze_preferences(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = UserPreferenceService(db) service = UserPreferenceService(db)
preferences = await service.analyze_preferences(user_id) preferences = await service.analyze_preferences(user_id)
@@ -56,7 +55,7 @@ async def analyze_preferences(
@router.get("/preferences") @router.get("/preferences")
async def get_preferences( async def get_preferences(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = UserPreferenceService(db) service = UserPreferenceService(db)
return await service.get_analysis(user_id) return await service.get_analysis(user_id)
@@ -66,7 +65,7 @@ async def get_preferences(
async def track_marketing_effect( async def track_marketing_effect(
data: dict, data: dict,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = MarketingEffectService(db) service = MarketingEffectService(db)
result = await service.track_event( result = await service.track_event(
@@ -87,7 +86,7 @@ async def get_marketing_effects(
page: int = 1, page: int = 1,
size: int = 20, size: int = 20,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = MarketingEffectService(db) service = MarketingEffectService(db)
return await service.get_effects(user_id, page, size) return await service.get_effects(user_id, page, size)
@@ -96,7 +95,7 @@ async def get_marketing_effects(
@router.get("/marketing-effects/stats") @router.get("/marketing-effects/stats")
async def get_marketing_effect_stats( async def get_marketing_effect_stats(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = MarketingEffectService(db) service = MarketingEffectService(db)
return await service.get_stats(user_id) return await service.get_stats(user_id)
+2 -2
View File
@@ -1,5 +1,5 @@
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from typing import Optional, Annotated from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
@@ -43,7 +43,7 @@ class CompetitorRequest(BaseModel):
async def generate_marketing( async def generate_marketing(
data: MarketingRequest, data: MarketingRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = MarketingService() service = MarketingService()
pref_service = UserPreferenceService(db) pref_service = UserPreferenceService(db)
+6 -6
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated, Optional from typing import Optional
from app.database import get_db from app.database import get_db
from app.services.notification import NotificationService from app.services.notification import NotificationService
from app.api.v1.deps import get_current_user_id from app.api.v1.deps import get_current_user_id
@@ -14,7 +14,7 @@ async def list_notifications(
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
unread_only: bool = Query(False), unread_only: bool = Query(False),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = NotificationService(db) service = NotificationService(db)
return await service.list_notifications(user_id, page, size, unread_only) return await service.list_notifications(user_id, page, size, unread_only)
@@ -23,7 +23,7 @@ async def list_notifications(
@router.get("/unread-count") @router.get("/unread-count")
async def unread_count( async def unread_count(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = NotificationService(db) service = NotificationService(db)
count = await service.get_unread_count(user_id) count = await service.get_unread_count(user_id)
@@ -34,7 +34,7 @@ async def unread_count(
async def mark_read( async def mark_read(
notification_id: str, notification_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = NotificationService(db) service = NotificationService(db)
success = await service.mark_read(user_id, notification_id) success = await service.mark_read(user_id, notification_id)
@@ -46,7 +46,7 @@ async def mark_read(
@router.post("/read-all") @router.post("/read-all")
async def mark_all_read( async def mark_all_read(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = NotificationService(db) service = NotificationService(db)
count = await service.mark_all_read(user_id) count = await service.mark_all_read(user_id)
@@ -57,7 +57,7 @@ async def mark_all_read(
async def delete_notification( async def delete_notification(
notification_id: str, notification_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = NotificationService(db) service = NotificationService(db)
success = await service.delete_notification(user_id, notification_id) success = await service.delete_notification(user_id, notification_id)
+2 -3
View File
@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from pydantic import BaseModel from pydantic import BaseModel
from app.database import get_db from app.database import get_db
from app.services.onboarding import OnboardingService from app.services.onboarding import OnboardingService
@@ -19,7 +18,7 @@ class OnboardingRequest(BaseModel):
@router.get("/status") @router.get("/status")
async def get_status( async def get_status(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = OnboardingService(db) service = OnboardingService(db)
return await service.check_status(user_id) return await service.check_status(user_id)
@@ -29,7 +28,7 @@ async def get_status(
async def create_first_product( async def create_first_product(
data: OnboardingRequest, data: OnboardingRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
if not data.name.strip(): if not data.name.strip():
raise HTTPException(status_code=400, detail="Product name is required") raise HTTPException(status_code=400, detail="Product name is required")
+3 -4
View File
@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from pydantic import BaseModel from pydantic import BaseModel
from app.database import get_db from app.database import get_db
from app.services.payment import PaymentService from app.services.payment import PaymentService
@@ -27,7 +26,7 @@ async def get_plans():
@router.get("/subscription") @router.get("/subscription")
async def get_subscription( async def get_subscription(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
svc = PaymentService(db) svc = PaymentService(db)
return await svc.get_current_subscription(user_id) return await svc.get_current_subscription(user_id)
@@ -37,7 +36,7 @@ async def get_subscription(
async def create_order( async def create_order(
data: CreateOrderRequest, data: CreateOrderRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
svc = PaymentService(db) svc = PaymentService(db)
try: try:
@@ -49,7 +48,7 @@ async def create_order(
@router.post("/callback") @router.post("/callback")
async def payment_callback( async def payment_callback(
data: PaymentCallbackRequest, data: PaymentCallbackRequest,
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
svc = PaymentService(db) svc = PaymentService(db)
success = await svc.handle_payment_callback(data.payment_id, data.success) success = await svc.handle_payment_callback(data.payment_id, data.success)
+6 -6
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated, Optional from typing import Optional
from app.database import get_db from app.database import get_db
from app.services.product import ProductService from app.services.product import ProductService
from app.api.v1.deps import get_current_user_id from app.api.v1.deps import get_current_user_id
@@ -44,7 +44,7 @@ async def list_products(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = ProductService(db) service = ProductService(db)
return await service.list_products(user_id, category, page, size) return await service.list_products(user_id, category, page, size)
@@ -54,7 +54,7 @@ async def list_products(
async def get_product( async def get_product(
product_id: str, product_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = ProductService(db) service = ProductService(db)
product = await service.get_product(user_id, product_id) product = await service.get_product(user_id, product_id)
@@ -67,7 +67,7 @@ async def get_product(
async def create_product( async def create_product(
data: ProductCreate, data: ProductCreate,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = ProductService(db) service = ProductService(db)
product = await service.create_product(user_id, data.dict()) product = await service.create_product(user_id, data.dict())
@@ -79,7 +79,7 @@ async def update_product(
product_id: str, product_id: str,
data: ProductUpdate, data: ProductUpdate,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = ProductService(db) service = ProductService(db)
product = await service.update_product(user_id, product_id, data.dict(exclude_unset=True)) product = await service.update_product(user_id, product_id, data.dict(exclude_unset=True))
@@ -92,7 +92,7 @@ async def update_product(
async def delete_product( async def delete_product(
product_id: str, product_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = ProductService(db) service = ProductService(db)
deleted = await service.delete_product(user_id, product_id) deleted = await service.delete_product(user_id, product_id)
+4 -4
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated, Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from app.database import get_db from app.database import get_db
from app.services.push import PushService from app.services.push import PushService
@@ -20,7 +20,7 @@ class DeviceRegisterRequest(BaseModel):
async def register_device( async def register_device(
data: DeviceRegisterRequest, data: DeviceRegisterRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = PushService(db) service = PushService(db)
device = await service.register_device( device = await service.register_device(
@@ -41,7 +41,7 @@ async def register_device(
async def unregister_device( async def unregister_device(
data: dict, data: dict,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
client_id = data.get("client_id") client_id = data.get("client_id")
if not client_id: if not client_id:
@@ -56,7 +56,7 @@ async def unregister_device(
@router.get("/devices") @router.get("/devices")
async def list_devices( async def list_devices(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = PushService(db) service = PushService(db)
devices = await service.get_user_devices(user_id) devices = await service.get_user_devices(user_id)
+8 -8
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Response from fastapi import APIRouter, Depends, HTTPException, Query, Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated, Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from app.database import get_db from app.database import get_db
from app.services.quotation import QuotationService from app.services.quotation import QuotationService
@@ -23,7 +23,7 @@ class InquiryRequest(BaseModel):
async def generate_from_inquiry( async def generate_from_inquiry(
data: InquiryRequest, data: InquiryRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = QuotationService(db) service = QuotationService(db)
result = await service.generate_from_inquiry( result = await service.generate_from_inquiry(
@@ -38,7 +38,7 @@ async def generate_from_inquiry(
async def create_quotation( async def create_quotation(
data: dict, data: dict,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = QuotationService(db) service = QuotationService(db)
try: try:
@@ -53,7 +53,7 @@ async def list_quotations(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = QuotationService(db) service = QuotationService(db)
return await service.list_quotations(user_id, page, size) return await service.list_quotations(user_id, page, size)
@@ -63,7 +63,7 @@ async def list_quotations(
async def get_quotation( async def get_quotation(
quotation_id: str, quotation_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = QuotationService(db) service = QuotationService(db)
quotation = await service.get_quotation(user_id, quotation_id) quotation = await service.get_quotation(user_id, quotation_id)
@@ -77,7 +77,7 @@ async def update_quotation_status(
quotation_id: str, quotation_id: str,
data: dict, data: dict,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = QuotationService(db) service = QuotationService(db)
quotation = await service.update_status(user_id, quotation_id, data.get("status", "draft")) quotation = await service.update_status(user_id, quotation_id, data.get("status", "draft"))
@@ -89,7 +89,7 @@ async def update_quotation_status(
@router.get("/export/csv") @router.get("/export/csv")
async def export_quotations( async def export_quotations(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = QuotationService(db) service = QuotationService(db)
result = await service.list_quotations(user_id, 1, 9999) result = await service.list_quotations(user_id, 1, 9999)
@@ -106,7 +106,7 @@ async def export_quotations(
async def export_quotation_pdf( async def export_quotation_pdf(
quotation_id: str, quotation_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = QuotationService(db) service = QuotationService(db)
quotation = await service.get_quotation(user_id, quotation_id) quotation = await service.get_quotation(user_id, quotation_id)
+2 -3
View File
@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from app.database import get_db from app.database import get_db
from app.services.silent_pattern import SilentPatternService from app.services.silent_pattern import SilentPatternService
from app.api.v1.deps import get_current_user_id from app.api.v1.deps import get_current_user_id
@@ -11,7 +10,7 @@ router = APIRouter()
@router.get("/risk-analysis") @router.get("/risk-analysis")
async def get_silent_risk_analysis( async def get_silent_risk_analysis(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = SilentPatternService(db) service = SilentPatternService(db)
risks = await service.analyze_silent_risk(user_id) risks = await service.analyze_silent_risk(user_id)
@@ -27,7 +26,7 @@ async def get_silent_risk_analysis(
async def get_followup_suggestions( async def get_followup_suggestions(
customer_id: str, customer_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = SilentPatternService(db) service = SilentPatternService(db)
suggestions = await service.get_suggestions(user_id, customer_id) suggestions = await service.get_suggestions(user_id, customer_id)
+8 -8
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated, Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from app.database import get_db from app.database import get_db
from app.services.team import TeamService from app.services.team import TeamService
@@ -26,7 +26,7 @@ class UpdateRoleRequest(BaseModel):
async def create_team( async def create_team(
data: CreateTeamRequest, data: CreateTeamRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = TeamService(db) service = TeamService(db)
try: try:
@@ -39,7 +39,7 @@ async def create_team(
@router.get("") @router.get("")
async def list_teams( async def list_teams(
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = TeamService(db) service = TeamService(db)
return {"teams": await service.list_user_teams(user_id)} return {"teams": await service.list_user_teams(user_id)}
@@ -49,7 +49,7 @@ async def list_teams(
async def get_team( async def get_team(
team_id: str, team_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = TeamService(db) service = TeamService(db)
team = await service.get_team(team_id, user_id) team = await service.get_team(team_id, user_id)
@@ -63,7 +63,7 @@ async def invite_member(
team_id: str, team_id: str,
data: InviteRequest, data: InviteRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = TeamService(db) service = TeamService(db)
try: try:
@@ -78,7 +78,7 @@ async def remove_member(
team_id: str, team_id: str,
member_id: str, member_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = TeamService(db) service = TeamService(db)
success = await service.remove_member(team_id, user_id, member_id) success = await service.remove_member(team_id, user_id, member_id)
@@ -91,7 +91,7 @@ async def remove_member(
async def leave_team( async def leave_team(
team_id: str, team_id: str,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = TeamService(db) service = TeamService(db)
success = await service.leave_team(team_id, user_id) success = await service.leave_team(team_id, user_id)
@@ -106,7 +106,7 @@ async def update_member_role(
member_id: str, member_id: str,
data: UpdateRoleRequest, data: UpdateRoleRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
service = TeamService(db) service = TeamService(db)
if data.role not in ("admin", "member", "viewer"): if data.role not in ("admin", "member", "viewer"):
+4 -5
View File
@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from app.database import get_db from app.database import get_db
from app.services.corpus_trainer import CorpusTrainer from app.services.corpus_trainer import CorpusTrainer
from app.api.v1.deps import get_current_user_id from app.api.v1.deps import get_current_user_id
@@ -10,7 +9,7 @@ router = APIRouter()
@router.post("/corpus/run") @router.post("/corpus/run")
async def run_corpus_training( async def run_corpus_training(
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
trainer = CorpusTrainer(db) trainer = CorpusTrainer(db)
result = await trainer.run_pipeline() result = await trainer.run_pipeline()
@@ -20,7 +19,7 @@ async def run_corpus_training(
@router.post("/corpus/embeddings") @router.post("/corpus/embeddings")
async def compute_embeddings( async def compute_embeddings(
batch_size: int = 50, batch_size: int = 50,
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
trainer = CorpusTrainer(db) trainer = CorpusTrainer(db)
result = await trainer.compute_embeddings(batch_size) result = await trainer.compute_embeddings(batch_size)
@@ -29,7 +28,7 @@ async def compute_embeddings(
@router.get("/corpus/stats") @router.get("/corpus/stats")
async def corpus_stats( async def corpus_stats(
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
trainer = CorpusTrainer(db) trainer = CorpusTrainer(db)
return await trainer.get_stats() return await trainer.get_stats()
@@ -37,7 +36,7 @@ async def corpus_stats(
@router.post("/corpus/deduplicate") @router.post("/corpus/deduplicate")
async def deduplicate_corpus( async def deduplicate_corpus(
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
trainer = CorpusTrainer(db) trainer = CorpusTrainer(db)
result = await trainer.deduplicate() result = await trainer.deduplicate()
+2 -2
View File
@@ -1,5 +1,5 @@
from fastapi import APIRouter, HTTPException, Response, Depends from fastapi import APIRouter, HTTPException, Response, Depends
from typing import Optional, Dict, Any, Annotated from typing import Optional, Dict, Any
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
@@ -51,7 +51,7 @@ async def translate_text(
async def generate_reply( async def generate_reply(
data: ReplyRequest, data: ReplyRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
pref_service = UserPreferenceService(db) pref_service = UserPreferenceService(db)
pref_context = await pref_service.get_preference_context(user_id, "reply") pref_context = await pref_service.get_preference_context(user_id, "reply")
+3 -3
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, HTTPException, Depends, Header from fastapi import APIRouter, Request, HTTPException, Depends, Header
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_ from sqlalchemy import select, and_
from typing import Annotated, Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from app.database import get_db from app.database import get_db
from app.services.whatsapp import WhatsAppService from app.services.whatsapp import WhatsAppService
@@ -30,7 +30,7 @@ async def verify_webhook(
async def handle_webhook( async def handle_webhook(
request: Request, request: Request,
x_hub_signature_256: Optional[str] = Header(None), x_hub_signature_256: Optional[str] = Header(None),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
svc = WhatsAppService() svc = WhatsAppService()
body = await request.body() body = await request.body()
@@ -80,7 +80,7 @@ class SendMessageRequest(BaseModel):
async def send_message( async def send_message(
data: SendMessageRequest, data: SendMessageRequest,
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: Annotated[AsyncSession, Depends(get_db)] = None, db: AsyncSession = Depends(get_db),
): ):
svc = WhatsAppService() svc = WhatsAppService()
+1 -1
View File
@@ -796,7 +796,7 @@ const deleteCustomer = async (id) => {
.bottom-actions { .bottom-actions {
position: fixed; position: fixed;
right: 40rpx; right: 40rpx;
bottom: 40rpx; bottom: 100px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24rpx; gap: 24rpx;
+165 -91
View File
@@ -5,7 +5,11 @@
<text class="username">{{ userInfo?.username || '用户' }}</text> <text class="username">{{ userInfo?.username || '用户' }}</text>
<text class="tier">{{ userInfo?.tier === 'pro' ? 'Pro' : '免费版' }}</text> <text class="tier">{{ userInfo?.tier === 'pro' ? 'Pro' : '免费版' }}</text>
</view> </view>
<view class="guest-info" v-else> <view class="header-left" v-if="!hasLogin" @click="showAnnouncement = true">
<text class="announcement-icon">📢</text>
<text class="announcement-ticker">{{ announcements[currentAnnouncement] }}</text>
</view>
<view class="header-right" v-if="!hasLogin">
<text class="guest-label">👋 游客模式</text> <text class="guest-label">👋 游客模式</text>
<button class="login-btn" @click="goToLogin">登录</button> <button class="login-btn" @click="goToLogin">登录</button>
</view> </view>
@@ -58,8 +62,11 @@
<view class="try-result" v-if="tryResult"> <view class="try-result" v-if="tryResult">
<view class="result-header"> <view class="result-header">
<text class="result-label">翻译结果</text> <text class="result-label">翻译结果</text>
<view class="result-actions">
<text class="result-play" @click="playTryResult">朗读</text>
<text class="result-copy" @click="copyTryResult">复制</text> <text class="result-copy" @click="copyTryResult">复制</text>
</view> </view>
</view>
<view class="result-content"> <view class="result-content">
<text class="result-text">{{ tryResult }}</text> <text class="result-text">{{ tryResult }}</text>
</view> </view>
@@ -92,39 +99,6 @@
<view class="empty" v-else>暂无待跟进客户</view> <view class="empty" v-else>暂无待跟进客户</view>
</view> </view>
<view class="quick-actions">
<view class="action-item" @click="hasLogin ? goToPage('/pages/product/product') : goToLogin()">
<view class="action-icon-wrap" style="background:#e6f7ff">
<text class="action-icon-text" style="color:#1890ff"></text>
</view>
<text class="action-label">产品库</text>
</view>
<view class="action-item" @click="hasLogin ? goToPage('/pages/followup/followup') : goToLogin()">
<view class="action-icon-wrap" style="background:#f0f0ff">
<text class="action-icon-text" style="color:#667eea"></text>
</view>
<text class="action-label">
<text>跟进</text>
<text class="action-badge" v-if="hasLogin && followupStats.pending > 0">{{ followupStats.pending > 99 ? '99+' : followupStats.pending }}</text>
</text>
</view>
<view class="action-item" @click="hasLogin ? goToPage('/pages/analytics/analytics') : goToLogin()">
<view class="action-icon-wrap" style="background:#f6ffed">
<text class="action-icon-text" style="color:#52c41a"></text>
</view>
<text class="action-label">数据</text>
</view>
<view class="action-item" @click="hasLogin ? goToPage('/pages/notification/notification') : goToLogin()">
<view class="action-icon-wrap" style="background:#fff7e6">
<text class="action-icon-text" style="color:#fa8c16"></text>
</view>
<text class="action-label">
<text>通知</text>
<text class="action-badge" v-if="hasLogin && unreadCount > 0">{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
</text>
</view>
</view>
<view class="section" v-if="hasLogin && followupStats.pending > 0"> <view class="section" v-if="hasLogin && followupStats.pending > 0">
<view class="section-title"> <view class="section-title">
<text>待跟进提醒</text> <text>待跟进提醒</text>
@@ -138,7 +112,7 @@
</view> </view>
<view class="more-section"> <view class="more-section">
<view class="section-title">更多功能</view> <view class="section-title">功能矩阵</view>
<view class="more-grid"> <view class="more-grid">
<view class="more-item" @click="hasLogin ? goToPage('/pages/product/product') : goToLogin()"> <view class="more-item" @click="hasLogin ? goToPage('/pages/product/product') : goToLogin()">
<text class="more-icon">📦</text> <text class="more-icon">📦</text>
@@ -221,15 +195,39 @@
<text class="ob-skip" @click="finishOnboarding" v-if="onboardingStep === 1">跳过以后再说</text> <text class="ob-skip" @click="finishOnboarding" v-if="onboardingStep === 1">跳过以后再说</text>
</view> </view>
</view> </view>
<view class="modal-overlay" v-if="showAnnouncement" @click="showAnnouncement = false">
<view class="announcement-modal" @click.stop>
<text class="announcement-title">📢 系统公告</text>
<view class="announcement-body">
<text class="announcement-line">欢迎使用外贸小助手</text>
<text class="announcement-line">登录后可解锁以下功能</text>
<text class="announcement-item"> 客户管理 管理客户信息与跟进记录</text>
<text class="announcement-item"> 报价单 快速生成并导出专业报价</text>
<text class="announcement-item"> 数据分析 查看业务统计与趋势</text>
<text class="announcement-item"> 营销素材 AI 生成营销文案与关键词</text>
<text class="announcement-line" style="margin-top: 20rpx">现在登录体验全部功能 🚀</text>
</view>
<button class="announcement-btn" @click="showAnnouncement = false; goToLogin()">去登录</button>
</view>
</view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed, onUnmounted } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app'
import { authApi, customerApi, analyticsApi, onboardingApi, notificationApi, followupApi, translateApi } from '@/utils/api.js' import { authApi, customerApi, analyticsApi, onboardingApi, notificationApi, followupApi, translateApi, BASE_URL } from '@/utils/api.js'
const showAnnouncement = ref(false)
const currentAnnouncement = ref(0)
const announcements = [
'全新 AI 翻译引擎上线,支持多语言商务翻译',
'登录后免费使用客户管理与报价单功能',
'每日数据看板上线,实时掌握业务动态',
]
let announcementTimer = null
const userInfo = ref(null)
const hasLogin = computed(() => { const hasLogin = computed(() => {
const token = uni.getStorageSync('token') const token = uni.getStorageSync('token')
const isGuest = uni.getStorageSync('isGuest') const isGuest = uni.getStorageSync('isGuest')
@@ -257,6 +255,10 @@ const tryExtracted = ref('')
const tryLoading = ref(false) const tryLoading = ref(false)
onShow(() => { onShow(() => {
announcementTimer = setInterval(() => {
currentAnnouncement.value = (currentAnnouncement.value + 1) % announcements.length
}, 4000)
const token = uni.getStorageSync('token') const token = uni.getStorageSync('token')
const isGuest = uni.getStorageSync('isGuest') const isGuest = uni.getStorageSync('isGuest')
if (token && !isGuest) { if (token && !isGuest) {
@@ -272,6 +274,10 @@ onShow(() => {
} }
}) })
onUnmounted(() => {
if (announcementTimer) clearInterval(announcementTimer)
})
const checkOnboarding = async () => { const checkOnboarding = async () => {
if (uni.getStorageSync('onboarded')) return if (uni.getStorageSync('onboarded')) return
try { try {
@@ -435,6 +441,35 @@ const copyTryResult = () => {
}, },
}) })
} }
const playTryResult = () => {
if (!tryResult.value || !tryText.value) return
const hasChinese = /[\u4e00-\u9fa5]/.test(tryText.value)
const lang = hasChinese ? 'en' : 'zh'
const token = uni.getStorageSync('token')
const url = `${BASE_URL}/translate/tts?text=${encodeURIComponent(tryResult.value)}&lang=${lang}`
uni.showLoading({ title: '语音生成中...' })
uni.downloadFile({
url,
header: token ? { Authorization: `Bearer ${token}` } : {},
success: (res) => {
uni.hideLoading()
if (res.statusCode === 200) {
const audioCtx = uni.createInnerAudioContext()
audioCtx.src = res.tempFilePath
audioCtx.play()
audioCtx.onEnded(() => audioCtx.destroy())
} else {
uni.showToast({ title: '语音生成失败', icon: 'none' })
}
},
fail: () => {
uni.hideLoading()
uni.showToast({ title: '语音生成失败', icon: 'none' })
},
})
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -460,15 +495,39 @@ const copyTryResult = () => {
align-items: center; align-items: center;
} }
.guest-info { .header-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16rpx; gap: 12rpx;
flex: 1;
cursor: pointer;
overflow: hidden;
}
.announcement-icon {
font-size: 32rpx;
flex-shrink: 0;
}
.announcement-ticker {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-right {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
} }
.guest-label { .guest-label {
font-size: 28rpx; font-size: 28rpx;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
} }
.login-btn { .login-btn {
@@ -654,11 +713,21 @@ const copyTryResult = () => {
font-weight: 500; font-weight: 500;
} }
.result-actions {
display: flex;
gap: 16rpx;
}
.result-copy { .result-copy {
font-size: 24rpx; font-size: 24rpx;
color: #1890ff; color: #1890ff;
} }
.result-play {
font-size: 24rpx;
color: #52c41a;
}
.result-content, .extracted-content { .result-content, .extracted-content {
background: #fff; background: #fff;
border-radius: 8rpx; border-radius: 8rpx;
@@ -759,57 +828,6 @@ const copyTryResult = () => {
margin-top: 12rpx; margin-top: 12rpx;
} }
.quick-actions {
display: flex;
background: #fff;
border-radius: 16rpx;
padding: 20rpx 16rpx;
margin-bottom: 20rpx;
justify-content: space-around;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
cursor: pointer;
}
.action-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.action-icon-text {
font-size: 30rpx;
font-weight: bold;
}
.action-label {
font-size: 22rpx;
color: #666;
display: flex;
align-items: center;
gap: 4rpx;
}
.action-badge {
background: #ff4d4f;
color: #fff;
font-size: 18rpx;
min-width: 28rpx;
height: 28rpx;
border-radius: 14rpx;
text-align: center;
line-height: 28rpx;
padding: 0 6rpx;
}
.more-section { .more-section {
background: #fff; background: #fff;
border-radius: 16rpx; border-radius: 16rpx;
@@ -887,4 +905,60 @@ const copyTryResult = () => {
.ob-btn { width: 100%; height: 88rpx; border-radius: 12rpx; font-size: 30rpx; border: none; display: flex; align-items: center; justify-content: center; } .ob-btn { width: 100%; height: 88rpx; border-radius: 12rpx; font-size: 30rpx; border: none; display: flex; align-items: center; justify-content: center; }
.ob-btn-primary { background: #1890ff; color: #fff; } .ob-btn-primary { background: #1890ff; color: #fff; }
.ob-skip { display: block; text-align: center; margin-top: 24rpx; font-size: 24rpx; color: #999; } .ob-skip { display: block; text-align: center; margin-top: 24rpx; font-size: 24rpx; color: #999; }
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex; align-items: center; justify-content: center;
z-index: 999; padding: 40rpx;
}
.announcement-modal {
background: #fff;
border-radius: 24rpx;
padding: 48rpx 40rpx;
width: 100%;
max-width: 560rpx;
}
.announcement-title {
font-size: 34rpx;
font-weight: 700;
color: #333;
display: block;
text-align: center;
margin-bottom: 32rpx;
}
.announcement-body {
margin-bottom: 32rpx;
}
.announcement-line {
font-size: 26rpx;
color: #555;
display: block;
line-height: 1.6;
margin-bottom: 8rpx;
}
.announcement-item {
font-size: 24rpx;
color: #666;
display: block;
line-height: 1.8;
padding-left: 20rpx;
}
.announcement-btn {
width: 100%;
height: 88rpx;
background: #1890ff;
color: #fff;
border-radius: 12rpx;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
</style> </style>
+84 -10
View File
@@ -1,5 +1,9 @@
<template> <template>
<view class="login-container"> <view class="login-container">
<view class="silent-loading" v-if="silentLoading">
<text class="silent-loading-text">正在自动登录...</text>
</view>
<view class="welcome-section"> <view class="welcome-section">
<text class="logo">TradeMate</text> <text class="logo">TradeMate</text>
<text class="subtitle">外贸小助手</text> <text class="subtitle">外贸小助手</text>
@@ -110,7 +114,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { authApi } from '@/utils/api.js' import { authApi } from '@/utils/api.js'
const phone = ref('') const phone = ref('')
@@ -118,14 +122,74 @@ const password = ref('')
const username = ref('') const username = ref('')
const isRegister = ref(false) const isRegister = ref(false)
const loading = ref(false) const loading = ref(false)
const silentLoading = ref(true)
const error = ref('') const error = ref('')
const showForm = ref(false) const showForm = ref(false)
const isWechatAvailable = ref(false) const isWechatAvailable = ref(false)
const doWechatLogin = async (code) => {
const res = await authApi.wechatLogin(code)
uni.setStorageSync('token', res.access_token)
uni.setStorageSync('userInfo', res.user)
uni.setStorageSync('hasLogin', true)
uni.setStorageSync('isGuest', false)
uni.switchTab({ url: '/pages/index/index' })
}
onMounted(async () => {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
// 微信小程序:静默登录
isWechatAvailable.value = true isWechatAvailable.value = true
try {
const loginRes = await new Promise((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject })
})
await doWechatLogin(loginRes.code)
} catch (_) {
silentLoading.value = false
}
// #endif // #endif
// #ifdef H5
// H5 微信内置浏览器:OAuth 静默登录
const isWechatBrowser = /MicroMessenger/i.test(navigator.userAgent)
if (isWechatBrowser) {
try {
const cfg = await authApi.wechatConfig()
if (cfg.available && cfg.app_id) {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
if (code) {
// OAuth 回调,携带 code
await doWechatLogin(code)
// 清除 URL 中的 code 参数
window.history.replaceState({}, '', window.location.pathname)
return
} else {
// 跳转微信 OAuth 授权
const redirectUri = encodeURIComponent(window.location.href.split('?')[0])
window.location.href =
`https://open.weixin.qq.com/connect/oauth2/authorize` +
`?appid=${cfg.app_id}` +
`&redirect_uri=${redirectUri}` +
`&response_type=code` +
`&scope=snsapi_base` +
`&state=STATE#wechat_redirect`
return
}
}
} catch (_) {}
}
silentLoading.value = false
// #endif
// #ifndef MP-WEIXIN
if (!/MicroMessenger/i.test(navigator.userAgent)) {
silentLoading.value = false
}
// #endif
})
const toggleMode = () => { const toggleMode = () => {
isRegister.value = !isRegister.value isRegister.value = !isRegister.value
error.value = '' error.value = ''
@@ -178,15 +242,7 @@ const handleWechatLogin = () => {
success: async (loginRes) => { success: async (loginRes) => {
try { try {
loading.value = true loading.value = true
const res = await authApi.wechatLogin(loginRes.code) await doWechatLogin(loginRes.code)
uni.setStorageSync('token', res.access_token)
uni.setStorageSync('userInfo', res.user)
uni.setStorageSync('hasLogin', true)
uni.setStorageSync('isGuest', false)
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
} catch (err) { } catch (err) {
error.value = err.message || '微信登录失败' error.value = err.message || '微信登录失败'
} finally { } finally {
@@ -438,6 +494,24 @@ const goToQuickTry = async () => {
margin-right: 12rpx; margin-right: 12rpx;
} }
.silent-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.silent-loading-text {
font-size: 28rpx;
color: #999;
}
.footer { .footer {
text-align: center; text-align: center;
margin-top: 40rpx; margin-top: 40rpx;
+95 -50
View File
@@ -4,28 +4,28 @@
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'copy' }" :class="{ active: activeTab === 'copy' }"
@click="activeTab = 'copy'; activeTab === 'copy' && loadStats()" @click="switchTab('copy')"
> >
开发信 开发信
</view> </view>
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'whatsapp' }" :class="{ active: activeTab === 'whatsapp' }"
@click="activeTab = 'whatsapp'; activeTab === 'whatsapp' && loadStats()" @click="switchTab('whatsapp')"
> >
WhatsApp话术 WhatsApp话术
</view> </view>
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'product' }" :class="{ active: activeTab === 'product' }"
@click="activeTab = 'product'; activeTab === 'product' && loadStats()" @click="switchTab('product')"
> >
产品描述 产品描述
</view> </view>
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'keywords' }" :class="{ active: activeTab === 'keywords' }"
@click="activeTab = 'keywords'" @click="switchTab('keywords')"
> >
关键词 关键词
</view> </view>
@@ -49,19 +49,19 @@
<view class="form-section"> <view class="form-section">
<view class="form-item"> <view class="form-item">
<text class="form-label">产品名称</text> <text class="form-label">产品名称</text>
<input class="form-input" v-model="formData.product_name" placeholder="如: 户外折叠椅" /> <input class="form-input" v-model="formData.product_name" :placeholder="tabConfig[activeTab].namePlaceholder" />
</view> </view>
<view class="form-item"> <view class="form-item">
<text class="form-label">产品描述</text> <text class="form-label">{{ tabConfig[activeTab].descLabel }}</text>
<textarea class="form-textarea" v-model="formData.description" placeholder="描述产品的特点、规格、优势..." /> <textarea class="form-textarea" v-model="formData.description" :placeholder="tabConfig[activeTab].descPlaceholder" />
</view> </view>
<view class="form-item"> <view class="form-item" v-if="tabConfig[activeTab].showTarget">
<text class="form-label">目标市场</text> <text class="form-label">目标市场</text>
<picker :range="targetMarkets" @change="onTargetChange"> <picker :range="targetMarkets" @change="onTargetChange">
<view class="picker-value">{{ formData.target || '选择目标市场' }}</view> <view class="picker-value">{{ formData.target || '选择目标市场' }}</view>
</picker> </picker>
</view> </view>
<view class="form-item"> <view class="form-item" v-if="tabConfig[activeTab].showStyle">
<text class="form-label">文案风格</text> <text class="form-label">文案风格</text>
<view class="style-options"> <view class="style-options">
<view <view
@@ -88,34 +88,30 @@
</view> </view>
</view> </view>
<button class="generate-btn" @click="generateContent" :disabled="loading"> <button class="generate-btn" @click="generateContent" :disabled="loading">
{{ loading ? '生成中...' : '生成文案' }} {{ loading ? '生成中...' : tabConfig[activeTab].btnText }}
</button> </button>
</view> </view>
<view class="results-section" v-if="results.length > 0 && activeTab !== 'keywords'"> <view class="results-section" v-if="resultsMap[activeTab] && resultsMap[activeTab].length > 0">
<view class="results-header"> <view class="results-header">
<text class="results-title">生成的文案</text> <text class="results-title">{{ tabConfig[activeTab].resultTitle }}</text>
<view class="results-actions">
<text class="refresh-btn" @click="generateContent">换一批</text> <text class="refresh-btn" @click="generateContent">换一批</text>
<text class="export-btn" @click="exportCsv">导出CSV</text> <text class="export-btn" @click="exportCsv" v-if="activeTab !== 'keywords'">导出CSV</text>
</view> </view>
<view class="results-list"> </view>
<view class="result-item" v-for="(item, index) in results" :key="index"> <view class="results-list" v-if="activeTab !== 'keywords'">
<view class="result-item" v-for="(item, index) in resultsMap[activeTab]" :key="index">
<text class="result-text">{{ item }}</text> <text class="result-text">{{ item }}</text>
<view class="result-actions"> <view class="result-actions">
<text class="copy-btn" @click="copyText(item)">复制</text> <text class="copy-btn" @click="copyText(item)">复制</text>
<text class="send-btn" @click="sendToWhatsapp(item)">发送</text> <text class="send-btn" @click="sendToWhatsapp(item)" v-if="activeTab !== 'product'">发送</text>
<text class="competitor-btn" @click="runCompetitorAnalysis">竞品分析</text> <text class="competitor-btn" @click="runCompetitorAnalysis" v-if="activeTab === 'copy'">竞品分析</text>
</view> </view>
</view> </view>
</view> </view>
</view> <view class="keywords-list" v-if="activeTab === 'keywords' && resultsMap.keywords && resultsMap.keywords.length > 0">
<view class="keyword-tag" v-for="(kw, idx) in resultsMap.keywords" :key="idx" @click="copyText(kw)">
<view class="history-section" v-if="activeTab === 'keywords' && keywords.length > 0">
<view class="history-header">
<text class="history-title">关键词建议</text>
</view>
<view class="keywords-list">
<view class="keyword-tag" v-for="(kw, idx) in keywords" :key="idx" @click="copyText(kw)">
{{ kw }} {{ kw }}
</view> </view>
</view> </view>
@@ -131,20 +127,70 @@
</view> </view>
</view> </view>
<view class="empty" v-if="!loading && results.length === 0 && activeTab !== 'keywords'"> <view class="empty" v-if="!loading && (!resultsMap[activeTab] || resultsMap[activeTab].length === 0)">
<text>输入产品信息点击生成文案</text> <text>{{ tabConfig[activeTab].emptyHint }}</text>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, reactive } from 'vue'
import { marketingApi, interactionApi } from '@/utils/api.js' import { marketingApi, interactionApi } from '@/utils/api.js'
const tabConfig = {
copy: {
label: '开发信',
category: 'sales_letter',
namePlaceholder: '如: 户外折叠椅',
descLabel: '产品描述',
descPlaceholder: '描述产品的特点、规格、优势...',
btnText: '生成开发信',
resultTitle: '生成的开发信',
emptyHint: '输入产品信息,点击生成开发信',
showTarget: true,
showStyle: true,
},
whatsapp: {
label: 'WhatsApp话术',
category: 'whatsapp',
namePlaceholder: '如: 户外折叠椅',
descLabel: '产品及沟通场景',
descPlaceholder: '描述产品特点,以及和客户沟通的具体场景...',
btnText: '生成话术',
resultTitle: '生成的WhatsApp话术',
emptyHint: '输入产品信息,点击生成话术',
showTarget: true,
showStyle: true,
},
product: {
label: '产品描述',
category: 'product_description',
namePlaceholder: '如: 户外折叠椅',
descLabel: '产品详细规格',
descPlaceholder: '描述产品的材质、尺寸、承重、颜色、包装等规格...',
btnText: '生成描述',
resultTitle: '生成的产品描述',
emptyHint: '输入产品信息,点击生成描述',
showTarget: false,
showStyle: false,
},
keywords: {
label: '关键词',
category: '',
namePlaceholder: '如: 户外折叠椅',
descLabel: '产品卖点',
descPlaceholder: '描述产品的核心卖点和目标客户群体...',
btnText: '生成关键词',
resultTitle: '关键词建议',
emptyHint: '输入产品信息,点击生成关键词',
showTarget: false,
showStyle: false,
},
}
const activeTab = ref('copy') const activeTab = ref('copy')
const loading = ref(false) const loading = ref(false)
const results = ref([]) const resultsMap = reactive({ copy: [], whatsapp: [], product: [], keywords: [] })
const keywords = ref([])
const competitorResult = ref(null) const competitorResult = ref(null)
const stats = ref(null) const stats = ref(null)
@@ -167,6 +213,11 @@ const onTargetChange = (e) => {
formData.value.target = targetMarkets.value[e.detail.value] formData.value.target = targetMarkets.value[e.detail.value]
} }
const switchTab = (tab) => {
activeTab.value = tab
loadStats()
}
const loadStats = async () => { const loadStats = async () => {
try { try {
const res = await interactionApi.getMarketingEffectStats() const res = await interactionApi.getMarketingEffectStats()
@@ -183,24 +234,26 @@ const generateContent = async () => {
} }
loading.value = true loading.value = true
const tab = activeTab.value
try { try {
if (activeTab.value === 'keywords') { if (tab === 'keywords') {
const res = await marketingApi.getKeywords( const res = await marketingApi.getKeywords(
formData.value.product_name, formData.value.product_name,
formData.value.description, formData.value.description,
'' ''
) )
keywords.value = res.keywords || [] resultsMap[tab] = res.keywords || []
} else { } else {
const cfg = tabConfig[tab]
const res = await marketingApi.generate( const res = await marketingApi.generate(
formData.value.product_name, formData.value.product_name,
formData.value.description, formData.value.description,
'', cfg.category,
formData.value.target, formData.value.target,
formData.value.style formData.value.style
) )
results.value = res.results || [] resultsMap[tab] = res.results || []
loadStats() loadStats()
} }
} catch (err) { } catch (err) {
@@ -222,9 +275,10 @@ const copyText = (text) => {
} }
const exportCsv = () => { const exportCsv = () => {
if (results.value.length === 0) return const items = resultsMap[activeTab.value]
if (!items || items.length === 0) return
let csv = 'Content\n' let csv = 'Content\n'
results.value.forEach(r => { csv += `"${r.replace(/"/g, '""')}"\n` }) items.forEach(r => { csv += `"${r.replace(/"/g, '""')}"\n` })
const blob = new Blob([csv], { type: 'text/csv' }) const blob = new Blob([csv], { type: 'text/csv' })
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
uni.downloadFile({ uni.downloadFile({
@@ -414,6 +468,11 @@ const runCompetitorAnalysis = async () => {
font-weight: 600; font-weight: 600;
} }
.results-actions {
display: flex;
gap: 16rpx;
}
.refresh-btn { .refresh-btn {
font-size: 24rpx; font-size: 24rpx;
color: #1890ff; color: #1890ff;
@@ -422,7 +481,6 @@ const runCompetitorAnalysis = async () => {
.export-btn { .export-btn {
font-size: 24rpx; font-size: 24rpx;
color: #52c41a; color: #52c41a;
margin-left: 16rpx;
} }
.results-list { .results-list {
@@ -470,19 +528,6 @@ const runCompetitorAnalysis = async () => {
color: #722ed1; color: #722ed1;
} }
.history-section {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
}
.history-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 20rpx;
display: block;
}
.keywords-list { .keywords-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
+2 -2
View File
@@ -503,7 +503,7 @@ const exportPdf = (item) => {
.export-csv-btn { .export-csv-btn {
position: fixed; position: fixed;
right: 40rpx; right: 40rpx;
bottom: 160rpx; bottom: calc(100px + 100rpx + 24rpx);
width: 100rpx; width: 100rpx;
height: 100rpx; height: 100rpx;
background: #722ed1; background: #722ed1;
@@ -523,7 +523,7 @@ const exportPdf = (item) => {
.add-btn { .add-btn {
position: fixed; position: fixed;
right: 40rpx; right: 40rpx;
bottom: 40rpx; bottom: 100px;
width: 100rpx; width: 100rpx;
height: 100rpx; height: 100rpx;
background: #1890ff; background: #1890ff;
+8 -2
View File
@@ -114,13 +114,14 @@ const inputText = ref('')
const result = ref('') const result = ref('')
const suggestions = ref([]) const suggestions = ref([])
const loading = ref(false) const loading = ref(false)
const targetIndex = ref(1) const targetIndex = ref(0)
const keyboardHeight = ref(0) const keyboardHeight = ref(0)
const rating = ref(0) const rating = ref(0)
const extractedInfo = ref(null) const extractedInfo = ref(null)
const preferences = ref(null) const preferences = ref(null)
const targetLangs = ref([ const targetLangs = ref([
{ code: 'auto', name: '自动检测' },
{ code: 'en', name: 'English' }, { code: 'en', name: 'English' },
{ code: 'zh', name: '中文' }, { code: 'zh', name: '中文' },
{ code: 'es', name: 'Español' }, { code: 'es', name: 'Español' },
@@ -143,9 +144,14 @@ const handleTranslate = async () => {
try { try {
if (mode.value === 'translate') { if (mode.value === 'translate') {
let targetLang = targetLangs[targetIndex.value].code
if (targetLang === 'auto') {
const hasChinese = /[\u4e00-\u9fa5]/.test(inputText.value)
targetLang = hasChinese ? 'en' : 'zh'
}
const res = await translateApi.translate( const res = await translateApi.translate(
inputText.value, inputText.value,
targetLangs[targetIndex.value].code targetLang
) )
result.value = res.translated result.value = res.translated
loadPreferences() loadPreferences()
+1
View File
@@ -61,6 +61,7 @@ export const authApi = {
register: (phone, password, username) => request('/auth/register', 'POST', { phone, password, username }), register: (phone, password, username) => request('/auth/register', 'POST', { phone, password, username }),
getUserInfo: () => request('/auth/me'), getUserInfo: () => request('/auth/me'),
wechatLogin: (code) => request('/auth/wechat-login', 'POST', { code }), wechatLogin: (code) => request('/auth/wechat-login', 'POST', { code }),
wechatConfig: () => request('/auth/wechat/config'),
guestLogin: () => requestWithoutAuth('/auth/login/guest', 'POST'), guestLogin: () => requestWithoutAuth('/auth/login/guest', 'POST'),
} }