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:
Binary file not shown.
@@ -1,6 +1,5 @@
|
||||
from typing import Dict, Any, Optional
|
||||
import json
|
||||
from anthropic import AsyncAnthropic
|
||||
from app.ai.base import AIProvider
|
||||
|
||||
|
||||
@@ -19,6 +18,13 @@ SYSTEM_PROMPTS = {
|
||||
|
||||
class ClaudeProvider(AIProvider):
|
||||
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.model = model
|
||||
self._name = f"claude-sonnet"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Dict, Any, Optional
|
||||
import json
|
||||
from openai import AsyncOpenAI
|
||||
from app.ai.base import AIProvider
|
||||
|
||||
|
||||
@@ -20,6 +19,13 @@ SYSTEM_PROMPTS = {
|
||||
|
||||
class OpenAIProvider(AIProvider):
|
||||
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}
|
||||
if base_url:
|
||||
kwargs["base_url"] = base_url
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Dict, Any, Optional
|
||||
import json
|
||||
from openai import AsyncOpenAI
|
||||
from app.ai.base import AIProvider
|
||||
|
||||
|
||||
@@ -18,6 +17,10 @@ SYSTEM_PROMPTS = {
|
||||
class SparkProvider(AIProvider):
|
||||
def __init__(self, api_key: str, model: str = "astron-code-latest", base_url: str = None):
|
||||
from app.config import settings
|
||||
try:
|
||||
from openai import AsyncOpenAI
|
||||
except ImportError:
|
||||
raise ImportError("openai>=1.0 is required for SparkProvider")
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=api_key,
|
||||
base_url=base_url or settings.IFLYTEK_API_BASE,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
from app.database import get_db
|
||||
from app.services.admin import AdminService
|
||||
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")
|
||||
async def get_dashboard(
|
||||
_: dict = Depends(require_admin),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AdminService(db)
|
||||
return await service.get_dashboard()
|
||||
@@ -28,7 +27,7 @@ async def list_users(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
_: dict = Depends(require_admin),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AdminService(db)
|
||||
return await service.list_users(page, size)
|
||||
@@ -39,7 +38,7 @@ async def update_user_tier(
|
||||
target_user_id: str,
|
||||
data: dict,
|
||||
_: dict = Depends(require_admin),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AdminService(db)
|
||||
tier = data.get("tier")
|
||||
@@ -55,7 +54,7 @@ async def update_user_tier(
|
||||
async def toggle_user_active(
|
||||
target_user_id: str,
|
||||
_: dict = Depends(require_admin),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AdminService(db)
|
||||
success = await service.toggle_user_active(target_user_id)
|
||||
@@ -66,7 +65,7 @@ async def toggle_user_active(
|
||||
|
||||
@router.get("/health")
|
||||
async def system_health(
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AdminService(db)
|
||||
return await service.get_system_health()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
from app.database import get_db
|
||||
from app.services.analytics import AnalyticsService
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
@@ -11,7 +10,7 @@ router = APIRouter()
|
||||
@router.get("/customers")
|
||||
async def customer_analytics(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AnalyticsService(db)
|
||||
return await service.get_customer_stats(user_id)
|
||||
@@ -20,7 +19,7 @@ async def customer_analytics(
|
||||
@router.get("/translations")
|
||||
async def translation_analytics(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AnalyticsService(db)
|
||||
return await service.get_translation_stats(user_id)
|
||||
@@ -29,7 +28,7 @@ async def translation_analytics(
|
||||
@router.get("/quotations")
|
||||
async def quotation_analytics(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AnalyticsService(db)
|
||||
return await service.get_quotation_stats(user_id)
|
||||
@@ -38,7 +37,7 @@ async def quotation_analytics(
|
||||
@router.get("/messages")
|
||||
async def message_analytics(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AnalyticsService(db)
|
||||
return await service.get_message_stats(user_id)
|
||||
@@ -47,7 +46,7 @@ async def message_analytics(
|
||||
@router.get("/overview")
|
||||
async def overview(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AnalyticsService(db)
|
||||
customers = await service.get_customer_stats(user_id)
|
||||
@@ -67,7 +66,7 @@ async def overview(
|
||||
@router.get("/marketing")
|
||||
async def marketing_analytics(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = AnalyticsService(db)
|
||||
return await service.get_marketing_stats(user_id)
|
||||
|
||||
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Header
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
@@ -31,7 +31,7 @@ class RefreshRequest(BaseModel):
|
||||
|
||||
|
||||
@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))
|
||||
if existing.scalar_one_or_none():
|
||||
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)
|
||||
async def login(
|
||||
form: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
form: OAuth2PasswordRequestForm = Depends(),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(User).where(User.phone == form.username))
|
||||
user = result.scalar_one_or_none()
|
||||
@@ -128,7 +128,7 @@ async def refresh(data: RefreshRequest):
|
||||
@router.get("/me")
|
||||
async def get_me(
|
||||
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 "):
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
@@ -178,8 +178,17 @@ class WeChatLoginRequest(BaseModel):
|
||||
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")
|
||||
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
|
||||
|
||||
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(
|
||||
data: SettingsUpdate,
|
||||
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 "):
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Response
|
||||
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.services.customer import CustomerService
|
||||
from app.services.customer_health import CustomerHealthService
|
||||
@@ -18,7 +18,7 @@ async def list_customers(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerService(db)
|
||||
return await service.list_customers(user_id, status, page, size)
|
||||
@@ -28,7 +28,7 @@ async def list_customers(
|
||||
async def get_silent(
|
||||
days: int = Query(3, ge=1),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerService(db)
|
||||
customers = await service.get_silent_customers(user_id, days)
|
||||
@@ -43,7 +43,7 @@ async def get_silent(
|
||||
async def get_customer(
|
||||
customer_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerService(db)
|
||||
customer = await service.get_customer(user_id, customer_id)
|
||||
@@ -56,7 +56,7 @@ async def get_customer(
|
||||
async def create_customer(
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerService(db)
|
||||
customer = await service.create_customer(user_id, data)
|
||||
@@ -68,7 +68,7 @@ async def update_customer(
|
||||
customer_id: str,
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerService(db)
|
||||
customer = await service.update_customer(user_id, customer_id, data)
|
||||
@@ -81,7 +81,7 @@ async def update_customer(
|
||||
async def delete_customer(
|
||||
customer_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerService(db)
|
||||
deleted = await service.delete_customer(user_id, customer_id)
|
||||
@@ -94,7 +94,7 @@ async def delete_customer(
|
||||
async def import_customers(
|
||||
file: UploadFile = File(...),
|
||||
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
|
||||
|
||||
@@ -135,7 +135,7 @@ async def import_customers(
|
||||
async def export_customers(
|
||||
status: Optional[str] = None,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerService(db)
|
||||
result = await service.list_customers(user_id, status, 1, 9999)
|
||||
@@ -151,7 +151,7 @@ async def export_customers(
|
||||
@router.get("/health-overview")
|
||||
async def get_health_overview(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerHealthService(db)
|
||||
return await service.get_health_overview(user_id)
|
||||
@@ -160,7 +160,7 @@ async def get_health_overview(
|
||||
@router.get("/health-scores")
|
||||
async def get_all_health_scores(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerHealthService(db)
|
||||
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(
|
||||
customer_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerHealthService(db)
|
||||
health = await service.get_customer_health(user_id, customer_id)
|
||||
@@ -185,7 +185,7 @@ async def get_conversation(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(50, ge=1, le=200),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = CustomerService(db)
|
||||
return await service.get_conversation(user_id, customer_id, page, size)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
from pydantic import BaseModel
|
||||
from app.database import get_db
|
||||
from app.models.feedback import Feedback
|
||||
@@ -19,7 +18,7 @@ class FeedbackRequest(BaseModel):
|
||||
async def submit_feedback(
|
||||
data: FeedbackRequest,
|
||||
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():
|
||||
raise HTTPException(status_code=400, detail="Content is required")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
from app.database import get_db
|
||||
from app.services.followup_engine import FollowupEngine
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
@@ -11,7 +11,7 @@ router = APIRouter()
|
||||
@router.get("/strategies")
|
||||
async def list_strategies(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
engine = FollowupEngine(db)
|
||||
await engine.ensure_default_strategies()
|
||||
@@ -23,7 +23,7 @@ async def get_pending_followups(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
engine = FollowupEngine(db)
|
||||
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),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
engine = FollowupEngine(db)
|
||||
return await engine.get_followup_logs(user_id, page, size)
|
||||
@@ -44,7 +44,7 @@ async def get_followup_logs(
|
||||
async def mark_followup_sent(
|
||||
log_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
engine = FollowupEngine(db)
|
||||
success = await engine.mark_sent(user_id, log_id)
|
||||
@@ -58,7 +58,7 @@ async def edit_and_send_followup(
|
||||
log_id: str,
|
||||
body: dict,
|
||||
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", "")
|
||||
if not edited_text:
|
||||
@@ -73,7 +73,7 @@ async def edit_and_send_followup(
|
||||
@router.get("/stats")
|
||||
async def get_followup_stats(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
engine = FollowupEngine(db)
|
||||
return await engine.get_stats(user_id)
|
||||
@@ -82,7 +82,7 @@ async def get_followup_stats(
|
||||
@router.post("/scan")
|
||||
async def trigger_followup_scan(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
engine = FollowupEngine(db)
|
||||
result = await engine.scan_and_followup()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
from app.database import get_db
|
||||
from app.services.preference import UserPreferenceService
|
||||
from app.services.marketing_effect import MarketingEffectService
|
||||
@@ -13,7 +12,7 @@ router = APIRouter()
|
||||
async def record_selection(
|
||||
data: dict,
|
||||
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")
|
||||
selected_index = data.get("selected_index")
|
||||
@@ -30,7 +29,7 @@ async def record_selection(
|
||||
async def record_edit(
|
||||
data: dict,
|
||||
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")
|
||||
edited_text = data.get("edited_text")
|
||||
@@ -46,7 +45,7 @@ async def record_edit(
|
||||
@router.post("/analyze")
|
||||
async def analyze_preferences(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = UserPreferenceService(db)
|
||||
preferences = await service.analyze_preferences(user_id)
|
||||
@@ -56,7 +55,7 @@ async def analyze_preferences(
|
||||
@router.get("/preferences")
|
||||
async def get_preferences(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = UserPreferenceService(db)
|
||||
return await service.get_analysis(user_id)
|
||||
@@ -66,7 +65,7 @@ async def get_preferences(
|
||||
async def track_marketing_effect(
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = MarketingEffectService(db)
|
||||
result = await service.track_event(
|
||||
@@ -87,7 +86,7 @@ async def get_marketing_effects(
|
||||
page: int = 1,
|
||||
size: int = 20,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = MarketingEffectService(db)
|
||||
return await service.get_effects(user_id, page, size)
|
||||
@@ -96,7 +95,7 @@ async def get_marketing_effects(
|
||||
@router.get("/marketing-effects/stats")
|
||||
async def get_marketing_effect_stats(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = MarketingEffectService(db)
|
||||
return await service.get_stats(user_id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from typing import Optional, Annotated
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
@@ -43,7 +43,7 @@ class CompetitorRequest(BaseModel):
|
||||
async def generate_marketing(
|
||||
data: MarketingRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = MarketingService()
|
||||
pref_service = UserPreferenceService(db)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
from app.database import get_db
|
||||
from app.services.notification import NotificationService
|
||||
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),
|
||||
unread_only: bool = Query(False),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = NotificationService(db)
|
||||
return await service.list_notifications(user_id, page, size, unread_only)
|
||||
@@ -23,7 +23,7 @@ async def list_notifications(
|
||||
@router.get("/unread-count")
|
||||
async def unread_count(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = NotificationService(db)
|
||||
count = await service.get_unread_count(user_id)
|
||||
@@ -34,7 +34,7 @@ async def unread_count(
|
||||
async def mark_read(
|
||||
notification_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = NotificationService(db)
|
||||
success = await service.mark_read(user_id, notification_id)
|
||||
@@ -46,7 +46,7 @@ async def mark_read(
|
||||
@router.post("/read-all")
|
||||
async def mark_all_read(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = NotificationService(db)
|
||||
count = await service.mark_all_read(user_id)
|
||||
@@ -57,7 +57,7 @@ async def mark_all_read(
|
||||
async def delete_notification(
|
||||
notification_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = NotificationService(db)
|
||||
success = await service.delete_notification(user_id, notification_id)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
from pydantic import BaseModel
|
||||
from app.database import get_db
|
||||
from app.services.onboarding import OnboardingService
|
||||
@@ -19,7 +18,7 @@ class OnboardingRequest(BaseModel):
|
||||
@router.get("/status")
|
||||
async def get_status(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = OnboardingService(db)
|
||||
return await service.check_status(user_id)
|
||||
@@ -29,7 +28,7 @@ async def get_status(
|
||||
async def create_first_product(
|
||||
data: OnboardingRequest,
|
||||
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():
|
||||
raise HTTPException(status_code=400, detail="Product name is required")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
from pydantic import BaseModel
|
||||
from app.database import get_db
|
||||
from app.services.payment import PaymentService
|
||||
@@ -27,7 +26,7 @@ async def get_plans():
|
||||
@router.get("/subscription")
|
||||
async def get_subscription(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = PaymentService(db)
|
||||
return await svc.get_current_subscription(user_id)
|
||||
@@ -37,7 +36,7 @@ async def get_subscription(
|
||||
async def create_order(
|
||||
data: CreateOrderRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = PaymentService(db)
|
||||
try:
|
||||
@@ -49,7 +48,7 @@ async def create_order(
|
||||
@router.post("/callback")
|
||||
async def payment_callback(
|
||||
data: PaymentCallbackRequest,
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = PaymentService(db)
|
||||
success = await svc.handle_payment_callback(data.payment_id, data.success)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
from app.database import get_db
|
||||
from app.services.product import ProductService
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
@@ -44,7 +44,7 @@ async def list_products(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ProductService(db)
|
||||
return await service.list_products(user_id, category, page, size)
|
||||
@@ -54,7 +54,7 @@ async def list_products(
|
||||
async def get_product(
|
||||
product_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ProductService(db)
|
||||
product = await service.get_product(user_id, product_id)
|
||||
@@ -67,7 +67,7 @@ async def get_product(
|
||||
async def create_product(
|
||||
data: ProductCreate,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ProductService(db)
|
||||
product = await service.create_product(user_id, data.dict())
|
||||
@@ -79,7 +79,7 @@ async def update_product(
|
||||
product_id: str,
|
||||
data: ProductUpdate,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ProductService(db)
|
||||
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(
|
||||
product_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ProductService(db)
|
||||
deleted = await service.delete_product(user_id, product_id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from app.database import get_db
|
||||
from app.services.push import PushService
|
||||
@@ -20,7 +20,7 @@ class DeviceRegisterRequest(BaseModel):
|
||||
async def register_device(
|
||||
data: DeviceRegisterRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = PushService(db)
|
||||
device = await service.register_device(
|
||||
@@ -41,7 +41,7 @@ async def register_device(
|
||||
async def unregister_device(
|
||||
data: dict,
|
||||
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")
|
||||
if not client_id:
|
||||
@@ -56,7 +56,7 @@ async def unregister_device(
|
||||
@router.get("/devices")
|
||||
async def list_devices(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = PushService(db)
|
||||
devices = await service.get_user_devices(user_id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from app.database import get_db
|
||||
from app.services.quotation import QuotationService
|
||||
@@ -23,7 +23,7 @@ class InquiryRequest(BaseModel):
|
||||
async def generate_from_inquiry(
|
||||
data: InquiryRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = QuotationService(db)
|
||||
result = await service.generate_from_inquiry(
|
||||
@@ -38,7 +38,7 @@ async def generate_from_inquiry(
|
||||
async def create_quotation(
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = QuotationService(db)
|
||||
try:
|
||||
@@ -53,7 +53,7 @@ async def list_quotations(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = QuotationService(db)
|
||||
return await service.list_quotations(user_id, page, size)
|
||||
@@ -63,7 +63,7 @@ async def list_quotations(
|
||||
async def get_quotation(
|
||||
quotation_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = QuotationService(db)
|
||||
quotation = await service.get_quotation(user_id, quotation_id)
|
||||
@@ -77,7 +77,7 @@ async def update_quotation_status(
|
||||
quotation_id: str,
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = QuotationService(db)
|
||||
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")
|
||||
async def export_quotations(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = QuotationService(db)
|
||||
result = await service.list_quotations(user_id, 1, 9999)
|
||||
@@ -106,7 +106,7 @@ async def export_quotations(
|
||||
async def export_quotation_pdf(
|
||||
quotation_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = QuotationService(db)
|
||||
quotation = await service.get_quotation(user_id, quotation_id)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
from app.database import get_db
|
||||
from app.services.silent_pattern import SilentPatternService
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
@@ -11,7 +10,7 @@ router = APIRouter()
|
||||
@router.get("/risk-analysis")
|
||||
async def get_silent_risk_analysis(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = SilentPatternService(db)
|
||||
risks = await service.analyze_silent_risk(user_id)
|
||||
@@ -27,7 +26,7 @@ async def get_silent_risk_analysis(
|
||||
async def get_followup_suggestions(
|
||||
customer_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = SilentPatternService(db)
|
||||
suggestions = await service.get_suggestions(user_id, customer_id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from app.database import get_db
|
||||
from app.services.team import TeamService
|
||||
@@ -26,7 +26,7 @@ class UpdateRoleRequest(BaseModel):
|
||||
async def create_team(
|
||||
data: CreateTeamRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = TeamService(db)
|
||||
try:
|
||||
@@ -39,7 +39,7 @@ async def create_team(
|
||||
@router.get("")
|
||||
async def list_teams(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = TeamService(db)
|
||||
return {"teams": await service.list_user_teams(user_id)}
|
||||
@@ -49,7 +49,7 @@ async def list_teams(
|
||||
async def get_team(
|
||||
team_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = TeamService(db)
|
||||
team = await service.get_team(team_id, user_id)
|
||||
@@ -63,7 +63,7 @@ async def invite_member(
|
||||
team_id: str,
|
||||
data: InviteRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = TeamService(db)
|
||||
try:
|
||||
@@ -78,7 +78,7 @@ async def remove_member(
|
||||
team_id: str,
|
||||
member_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = TeamService(db)
|
||||
success = await service.remove_member(team_id, user_id, member_id)
|
||||
@@ -91,7 +91,7 @@ async def remove_member(
|
||||
async def leave_team(
|
||||
team_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = TeamService(db)
|
||||
success = await service.leave_team(team_id, user_id)
|
||||
@@ -106,7 +106,7 @@ async def update_member_role(
|
||||
member_id: str,
|
||||
data: UpdateRoleRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = TeamService(db)
|
||||
if data.role not in ("admin", "member", "viewer"):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
from app.database import get_db
|
||||
from app.services.corpus_trainer import CorpusTrainer
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
@@ -10,7 +9,7 @@ router = APIRouter()
|
||||
|
||||
@router.post("/corpus/run")
|
||||
async def run_corpus_training(
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
trainer = CorpusTrainer(db)
|
||||
result = await trainer.run_pipeline()
|
||||
@@ -20,7 +19,7 @@ async def run_corpus_training(
|
||||
@router.post("/corpus/embeddings")
|
||||
async def compute_embeddings(
|
||||
batch_size: int = 50,
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
trainer = CorpusTrainer(db)
|
||||
result = await trainer.compute_embeddings(batch_size)
|
||||
@@ -29,7 +28,7 @@ async def compute_embeddings(
|
||||
|
||||
@router.get("/corpus/stats")
|
||||
async def corpus_stats(
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
trainer = CorpusTrainer(db)
|
||||
return await trainer.get_stats()
|
||||
@@ -37,7 +36,7 @@ async def corpus_stats(
|
||||
|
||||
@router.post("/corpus/deduplicate")
|
||||
async def deduplicate_corpus(
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
trainer = CorpusTrainer(db)
|
||||
result = await trainer.deduplicate()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
@@ -51,7 +51,7 @@ async def translate_text(
|
||||
async def generate_reply(
|
||||
data: ReplyRequest,
|
||||
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_context = await pref_service.get_preference_context(user_id, "reply")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Request, HTTPException, Depends, Header
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from app.database import get_db
|
||||
from app.services.whatsapp import WhatsAppService
|
||||
@@ -30,7 +30,7 @@ async def verify_webhook(
|
||||
async def handle_webhook(
|
||||
request: Request,
|
||||
x_hub_signature_256: Optional[str] = Header(None),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = WhatsAppService()
|
||||
body = await request.body()
|
||||
@@ -80,7 +80,7 @@ class SendMessageRequest(BaseModel):
|
||||
async def send_message(
|
||||
data: SendMessageRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = WhatsAppService()
|
||||
|
||||
|
||||
@@ -796,7 +796,7 @@ const deleteCustomer = async (id) => {
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 40rpx;
|
||||
bottom: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
<text class="username">{{ userInfo?.username || '用户' }}</text>
|
||||
<text class="tier">{{ userInfo?.tier === 'pro' ? 'Pro' : '免费版' }}</text>
|
||||
</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>
|
||||
<button class="login-btn" @click="goToLogin">登录</button>
|
||||
</view>
|
||||
@@ -58,8 +62,11 @@
|
||||
<view class="try-result" v-if="tryResult">
|
||||
<view class="result-header">
|
||||
<text class="result-label">翻译结果</text>
|
||||
<view class="result-actions">
|
||||
<text class="result-play" @click="playTryResult">朗读</text>
|
||||
<text class="result-copy" @click="copyTryResult">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="result-content">
|
||||
<text class="result-text">{{ tryResult }}</text>
|
||||
</view>
|
||||
@@ -92,39 +99,6 @@
|
||||
<view class="empty" v-else>暂无待跟进客户</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-title">
|
||||
<text>待跟进提醒</text>
|
||||
@@ -138,7 +112,7 @@
|
||||
</view>
|
||||
|
||||
<view class="more-section">
|
||||
<view class="section-title">更多功能</view>
|
||||
<view class="section-title">功能矩阵</view>
|
||||
<view class="more-grid">
|
||||
<view class="more-item" @click="hasLogin ? goToPage('/pages/product/product') : goToLogin()">
|
||||
<text class="more-icon">📦</text>
|
||||
@@ -221,15 +195,39 @@
|
||||
<text class="ob-skip" @click="finishOnboarding" v-if="onboardingStep === 1">跳过,以后再说</text>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
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 token = uni.getStorageSync('token')
|
||||
const isGuest = uni.getStorageSync('isGuest')
|
||||
@@ -257,6 +255,10 @@ const tryExtracted = ref('')
|
||||
const tryLoading = ref(false)
|
||||
|
||||
onShow(() => {
|
||||
announcementTimer = setInterval(() => {
|
||||
currentAnnouncement.value = (currentAnnouncement.value + 1) % announcements.length
|
||||
}, 4000)
|
||||
|
||||
const token = uni.getStorageSync('token')
|
||||
const isGuest = uni.getStorageSync('isGuest')
|
||||
if (token && !isGuest) {
|
||||
@@ -272,6 +274,10 @@ onShow(() => {
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (announcementTimer) clearInterval(announcementTimer)
|
||||
})
|
||||
|
||||
const checkOnboarding = async () => {
|
||||
if (uni.getStorageSync('onboarded')) return
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -460,15 +495,39 @@ const copyTryResult = () => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.guest-info {
|
||||
.header-left {
|
||||
display: flex;
|
||||
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 {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
@@ -654,11 +713,21 @@ const copyTryResult = () => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.result-copy {
|
||||
font-size: 24rpx;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.result-play {
|
||||
font-size: 24rpx;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.result-content, .extracted-content {
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
@@ -759,57 +828,6 @@ const copyTryResult = () => {
|
||||
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 {
|
||||
background: #fff;
|
||||
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-primary { background: #1890ff; color: #fff; }
|
||||
.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>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<view class="login-container">
|
||||
<view class="silent-loading" v-if="silentLoading">
|
||||
<text class="silent-loading-text">正在自动登录...</text>
|
||||
</view>
|
||||
|
||||
<view class="welcome-section">
|
||||
<text class="logo">TradeMate</text>
|
||||
<text class="subtitle">外贸小助手</text>
|
||||
@@ -110,7 +114,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { authApi } from '@/utils/api.js'
|
||||
|
||||
const phone = ref('')
|
||||
@@ -118,14 +122,74 @@ const password = ref('')
|
||||
const username = ref('')
|
||||
const isRegister = ref(false)
|
||||
const loading = ref(false)
|
||||
const silentLoading = ref(true)
|
||||
const error = ref('')
|
||||
const showForm = 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
|
||||
// 微信小程序:静默登录
|
||||
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
|
||||
|
||||
// #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 = () => {
|
||||
isRegister.value = !isRegister.value
|
||||
error.value = ''
|
||||
@@ -178,15 +242,7 @@ const handleWechatLogin = () => {
|
||||
success: async (loginRes) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await authApi.wechatLogin(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)
|
||||
await doWechatLogin(loginRes.code)
|
||||
} catch (err) {
|
||||
error.value = err.message || '微信登录失败'
|
||||
} finally {
|
||||
@@ -438,6 +494,24 @@ const goToQuickTry = async () => {
|
||||
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 {
|
||||
text-align: center;
|
||||
margin-top: 40rpx;
|
||||
|
||||
@@ -4,28 +4,28 @@
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'copy' }"
|
||||
@click="activeTab = 'copy'; activeTab === 'copy' && loadStats()"
|
||||
@click="switchTab('copy')"
|
||||
>
|
||||
开发信
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'whatsapp' }"
|
||||
@click="activeTab = 'whatsapp'; activeTab === 'whatsapp' && loadStats()"
|
||||
@click="switchTab('whatsapp')"
|
||||
>
|
||||
WhatsApp话术
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'product' }"
|
||||
@click="activeTab = 'product'; activeTab === 'product' && loadStats()"
|
||||
@click="switchTab('product')"
|
||||
>
|
||||
产品描述
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'keywords' }"
|
||||
@click="activeTab = 'keywords'"
|
||||
@click="switchTab('keywords')"
|
||||
>
|
||||
关键词
|
||||
</view>
|
||||
@@ -49,19 +49,19 @@
|
||||
<view class="form-section">
|
||||
<view class="form-item">
|
||||
<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 class="form-item">
|
||||
<text class="form-label">产品描述</text>
|
||||
<textarea class="form-textarea" v-model="formData.description" placeholder="描述产品的特点、规格、优势..." />
|
||||
<text class="form-label">{{ tabConfig[activeTab].descLabel }}</text>
|
||||
<textarea class="form-textarea" v-model="formData.description" :placeholder="tabConfig[activeTab].descPlaceholder" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-item" v-if="tabConfig[activeTab].showTarget">
|
||||
<text class="form-label">目标市场</text>
|
||||
<picker :range="targetMarkets" @change="onTargetChange">
|
||||
<view class="picker-value">{{ formData.target || '选择目标市场' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-item" v-if="tabConfig[activeTab].showStyle">
|
||||
<text class="form-label">文案风格</text>
|
||||
<view class="style-options">
|
||||
<view
|
||||
@@ -88,34 +88,30 @@
|
||||
</view>
|
||||
</view>
|
||||
<button class="generate-btn" @click="generateContent" :disabled="loading">
|
||||
{{ loading ? '生成中...' : '生成文案' }}
|
||||
{{ loading ? '生成中...' : tabConfig[activeTab].btnText }}
|
||||
</button>
|
||||
</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">
|
||||
<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="export-btn" @click="exportCsv">导出CSV</text>
|
||||
<text class="export-btn" @click="exportCsv" v-if="activeTab !== 'keywords'">导出CSV</text>
|
||||
</view>
|
||||
<view class="results-list">
|
||||
<view class="result-item" v-for="(item, index) in results" :key="index">
|
||||
</view>
|
||||
<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>
|
||||
<view class="result-actions">
|
||||
<text class="copy-btn" @click="copyText(item)">复制</text>
|
||||
<text class="send-btn" @click="sendToWhatsapp(item)">发送</text>
|
||||
<text class="competitor-btn" @click="runCompetitorAnalysis">竞品分析</text>
|
||||
<text class="send-btn" @click="sendToWhatsapp(item)" v-if="activeTab !== 'product'">发送</text>
|
||||
<text class="competitor-btn" @click="runCompetitorAnalysis" v-if="activeTab === 'copy'">竞品分析</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<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)">
|
||||
<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)">
|
||||
{{ kw }}
|
||||
</view>
|
||||
</view>
|
||||
@@ -131,20 +127,70 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" v-if="!loading && results.length === 0 && activeTab !== 'keywords'">
|
||||
<text>输入产品信息,点击生成文案</text>
|
||||
<view class="empty" v-if="!loading && (!resultsMap[activeTab] || resultsMap[activeTab].length === 0)">
|
||||
<text>{{ tabConfig[activeTab].emptyHint }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
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 loading = ref(false)
|
||||
const results = ref([])
|
||||
const keywords = ref([])
|
||||
const resultsMap = reactive({ copy: [], whatsapp: [], product: [], keywords: [] })
|
||||
const competitorResult = ref(null)
|
||||
const stats = ref(null)
|
||||
|
||||
@@ -167,6 +213,11 @@ const onTargetChange = (e) => {
|
||||
formData.value.target = targetMarkets.value[e.detail.value]
|
||||
}
|
||||
|
||||
const switchTab = (tab) => {
|
||||
activeTab.value = tab
|
||||
loadStats()
|
||||
}
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res = await interactionApi.getMarketingEffectStats()
|
||||
@@ -183,24 +234,26 @@ const generateContent = async () => {
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
const tab = activeTab.value
|
||||
|
||||
try {
|
||||
if (activeTab.value === 'keywords') {
|
||||
if (tab === 'keywords') {
|
||||
const res = await marketingApi.getKeywords(
|
||||
formData.value.product_name,
|
||||
formData.value.description,
|
||||
''
|
||||
)
|
||||
keywords.value = res.keywords || []
|
||||
resultsMap[tab] = res.keywords || []
|
||||
} else {
|
||||
const cfg = tabConfig[tab]
|
||||
const res = await marketingApi.generate(
|
||||
formData.value.product_name,
|
||||
formData.value.description,
|
||||
'',
|
||||
cfg.category,
|
||||
formData.value.target,
|
||||
formData.value.style
|
||||
)
|
||||
results.value = res.results || []
|
||||
resultsMap[tab] = res.results || []
|
||||
loadStats()
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -222,9 +275,10 @@ const copyText = (text) => {
|
||||
}
|
||||
|
||||
const exportCsv = () => {
|
||||
if (results.value.length === 0) return
|
||||
const items = resultsMap[activeTab.value]
|
||||
if (!items || items.length === 0) return
|
||||
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 url = URL.createObjectURL(blob)
|
||||
uni.downloadFile({
|
||||
@@ -414,6 +468,11 @@ const runCompetitorAnalysis = async () => {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.results-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
font-size: 24rpx;
|
||||
color: #1890ff;
|
||||
@@ -422,7 +481,6 @@ const runCompetitorAnalysis = async () => {
|
||||
.export-btn {
|
||||
font-size: 24rpx;
|
||||
color: #52c41a;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
@@ -470,19 +528,6 @@ const runCompetitorAnalysis = async () => {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -503,7 +503,7 @@ const exportPdf = (item) => {
|
||||
.export-csv-btn {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 160rpx;
|
||||
bottom: calc(100px + 100rpx + 24rpx);
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: #722ed1;
|
||||
@@ -523,7 +523,7 @@ const exportPdf = (item) => {
|
||||
.add-btn {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 40rpx;
|
||||
bottom: 100px;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: #1890ff;
|
||||
|
||||
@@ -114,13 +114,14 @@ const inputText = ref('')
|
||||
const result = ref('')
|
||||
const suggestions = ref([])
|
||||
const loading = ref(false)
|
||||
const targetIndex = ref(1)
|
||||
const targetIndex = ref(0)
|
||||
const keyboardHeight = ref(0)
|
||||
const rating = ref(0)
|
||||
const extractedInfo = ref(null)
|
||||
const preferences = ref(null)
|
||||
|
||||
const targetLangs = ref([
|
||||
{ code: 'auto', name: '自动检测' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'zh', name: '中文' },
|
||||
{ code: 'es', name: 'Español' },
|
||||
@@ -143,9 +144,14 @@ const handleTranslate = async () => {
|
||||
|
||||
try {
|
||||
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(
|
||||
inputText.value,
|
||||
targetLangs[targetIndex.value].code
|
||||
targetLang
|
||||
)
|
||||
result.value = res.translated
|
||||
loadPreferences()
|
||||
|
||||
@@ -61,6 +61,7 @@ export const authApi = {
|
||||
register: (phone, password, username) => request('/auth/register', 'POST', { phone, password, username }),
|
||||
getUserInfo: () => request('/auth/me'),
|
||||
wechatLogin: (code) => request('/auth/wechat-login', 'POST', { code }),
|
||||
wechatConfig: () => request('/auth/wechat/config'),
|
||||
guestLogin: () => requestWithoutAuth('/auth/login/guest', 'POST'),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user