feat: 修复 H5 底部导航覆盖 + 更新项目进度文档
## H5 底部导航修复 (Bug #10) - 精简 App.vue,移除重复 tabbar,仅保留全局样式 - uni-page 设置 height: calc(100% - 50px) + overflow-y: auto - 内容区域精确停在底部导航上方,独立滚动不再叠加 - 恢复 custom-tab-bar 组件 ## 项目进度文档 - PROGRESS.md 更新至 10 个 Bug 修复 - 新增 H5 底部导航修复记录 - 新增历史变更条目
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
from typing import Dict, Any, Optional, List
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy import select, and_, or_
|
||||
from app.models.quotation import Quotation, QuotationItem
|
||||
from app.models.customer import Customer
|
||||
from app.models.user import Product
|
||||
from app.ai.router import get_ai_router
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -90,6 +92,135 @@ class QuotationService:
|
||||
await self.db.flush()
|
||||
return await self._to_dict(q)
|
||||
|
||||
async def generate_from_inquiry(
|
||||
self, user_id: str, inquiry_text: str, customer_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
ai = get_ai_router()
|
||||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"product_requests": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"product_name": {"type": "string"},
|
||||
"quantity": {"type": "integer"},
|
||||
"unit": {"type": "string"},
|
||||
"specifications": {"type": "string"},
|
||||
"target_price": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"payment_terms": {"type": "string"},
|
||||
"delivery_terms": {"type": "string"},
|
||||
"urgency": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
extract_result = await ai.extract(inquiry_text, schema)
|
||||
extracted = extract_result.get("data", {})
|
||||
product_requests = extracted.get("product_requests", [])
|
||||
|
||||
if not product_requests:
|
||||
schema_simple = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"product_name": {"type": "string"},
|
||||
"quantity": {"type": "integer"},
|
||||
"specifications": {"type": "string"},
|
||||
},
|
||||
}
|
||||
extract_result = await ai.extract(inquiry_text, schema_simple)
|
||||
extracted = extract_result.get("data", {})
|
||||
if extracted.get("product_name"):
|
||||
product_requests = [{
|
||||
"product_name": extracted["product_name"],
|
||||
"quantity": extracted.get("quantity", 1),
|
||||
"unit": "pcs",
|
||||
"specifications": extracted.get("specifications", ""),
|
||||
}]
|
||||
|
||||
product_result = await self.db.execute(
|
||||
select(Product).where(
|
||||
and_(
|
||||
Product.user_id == user_id,
|
||||
Product.is_active == True,
|
||||
)
|
||||
)
|
||||
)
|
||||
user_products = product_result.scalars().all()
|
||||
|
||||
matched_products = []
|
||||
for req in product_requests:
|
||||
req_name = req.get("product_name", "").lower()
|
||||
best_match = None
|
||||
best_score = 0
|
||||
|
||||
for p in user_products:
|
||||
score = 0
|
||||
p_name = (p.name or "").lower()
|
||||
p_name_en = (p.name_en or "").lower()
|
||||
|
||||
if req_name in p_name or p_name in req_name:
|
||||
score += 3
|
||||
if req_name in p_name_en or p_name_en in req_name:
|
||||
score += 2
|
||||
|
||||
keywords = p.keywords or []
|
||||
for kw in keywords:
|
||||
if isinstance(kw, str) and kw.lower() in req_name:
|
||||
score += 1
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_match = p
|
||||
|
||||
if best_match and best_score > 0:
|
||||
unit_price = float(best_match.price) if best_match.price else 0
|
||||
quantity = req.get("quantity", 1)
|
||||
matched_products.append({
|
||||
"product_id": str(best_match.id),
|
||||
"product_name": best_match.name,
|
||||
"description": best_match.description_en or best_match.description,
|
||||
"quantity": quantity,
|
||||
"unit_price": unit_price,
|
||||
"total_price": unit_price * quantity,
|
||||
"unit": req.get("unit", "pcs"),
|
||||
"match_score": best_score,
|
||||
})
|
||||
else:
|
||||
matched_products.append({
|
||||
"product_id": None,
|
||||
"product_name": req.get("product_name", "Unknown"),
|
||||
"description": req.get("specifications", ""),
|
||||
"quantity": req.get("quantity", 1),
|
||||
"unit_price": 0,
|
||||
"total_price": 0,
|
||||
"unit": req.get("unit", "pcs"),
|
||||
"match_score": 0,
|
||||
})
|
||||
|
||||
subtotal = sum(p["total_price"] for p in matched_products)
|
||||
total = subtotal
|
||||
|
||||
suggested_quotation = {
|
||||
"title": f"Quotation - {', '.join(p['product_name'] for p in matched_products[:3])}",
|
||||
"items": matched_products,
|
||||
"subtotal": subtotal,
|
||||
"total": total,
|
||||
"payment_terms": extracted.get("payment_terms", "T/T"),
|
||||
"delivery_terms": extracted.get("delivery_terms", "FOB"),
|
||||
"lead_time": "15-20 days" if extracted.get("urgency") != "urgent" else "7-10 days",
|
||||
"notes": f"Generated from customer inquiry: {inquiry_text[:100]}..." if len(inquiry_text) > 100 else f"Generated from customer inquiry: {inquiry_text}",
|
||||
"extracted_data": extracted,
|
||||
"matched_count": len([p for p in matched_products if p["product_id"]]),
|
||||
"unmatched_count": len([p for p in matched_products if not p["product_id"]]),
|
||||
}
|
||||
|
||||
return suggested_quotation
|
||||
|
||||
async def generate_quotation_text(self, q: Quotation) -> str:
|
||||
items_result = await self.db.execute(
|
||||
select(QuotationItem).where(QuotationItem.quotation_id == q.id)
|
||||
|
||||
Reference in New Issue
Block a user