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:
TradeMate Dev
2026-05-12 20:24:42 +08:00
parent 69e164dcae
commit 7b62c2f8b4
125 changed files with 19725 additions and 728 deletions
+229
View File
@@ -0,0 +1,229 @@
from typing import Optional, Dict, Any, List
from datetime import datetime
import os
import logging
logger = logging.getLogger(__name__)
try:
from weasyprint import HTML
HAS_WEASYPRINT = True
except ImportError:
HAS_WEASYPRINT = False
logger.warning("weasyprint not installed, PDF generation disabled")
QUOTATION_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
@page {{
size: A4;
margin: 2cm;
}}
body {{
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 12pt;
color: #333;
line-height: 1.6;
}}
.header {{
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #1890ff;
padding-bottom: 20px;
}}
.header h1 {{
font-size: 24pt;
color: #1890ff;
margin: 0;
}}
.header .number {{
font-size: 14pt;
color: #666;
}}
.info-grid {{
display: flex;
justify-content: space-between;
margin-bottom: 30px;
}}
.info-block {{
width: 48%;
}}
.info-block h3 {{
font-size: 11pt;
color: #1890ff;
margin-bottom: 8px;
border-bottom: 1px solid #e8e8e8;
padding-bottom: 4px;
}}
.info-block p {{
margin: 4px 0;
font-size: 10pt;
}}
table {{
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}}
th {{
background: #1890ff;
color: white;
padding: 10px 8px;
text-align: left;
font-size: 10pt;
}}
td {{
padding: 8px;
border-bottom: 1px solid #e8e8e8;
font-size: 10pt;
}}
.amount-row td {{
text-align: right;
padding: 4px 8px;
border: none;
}}
.total-row td {{
font-weight: bold;
font-size: 12pt;
border-top: 2px solid #333;
}}
.terms {{
margin-top: 30px;
padding-top: 15px;
border-top: 1px solid #e8e8e8;
}}
.terms h3 {{
font-size: 11pt;
color: #1890ff;
}}
.terms p {{
font-size: 9pt;
color: #666;
margin: 4px 0;
}}
.footer {{
text-align: center;
margin-top: 40px;
font-size: 9pt;
color: #999;
}}
</style>
</head>
<body>
<div class="header">
<h1>QUOTATION</h1>
<p class="number">#{quotation_number}</p>
</div>
<div class="info-grid">
<div class="info-block">
<h3>Bill To</h3>
<p>{customer_name}</p>
<p>{customer_company}</p>
<p>{customer_country}</p>
</div>
<div class="info-block">
<h3>Quote Details</h3>
<p>Date: {date}</p>
<p>Valid Until: {valid_until}</p>
<p>Currency: {currency}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>Item</th>
<th>Description</th>
<th>Qty</th>
<th>Unit</th>
<th>Unit Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{items_rows}
</tbody>
</table>
<table>
<tr class="amount-row"><td colspan="4"></td><td>Subtotal:</td><td>{subtotal}</td></tr>
<tr class="amount-row"><td colspan="4"></td><td>Discount:</td><td>-{discount}</td></tr>
<tr class="amount-row"><td colspan="4"></td><td>Shipping:</td><td>{shipping}</td></tr>
<tr class="total-row"><td colspan="4"></td><td>TOTAL:</td><td>{total}</td></tr>
</table>
<div class="terms">
<h3>Terms & Conditions</h3>
<p>Payment Terms: {payment_terms}</p>
<p>Delivery Terms: {delivery_terms}</p>
<p>Lead Time: {lead_time}</p>
{notes_html}
</div>
<div class="footer">
<p>Generated by TradeMate - {generated_at}</p>
</div>
</body>
</html>
"""
class PDFGenerator:
@staticmethod
def generate_quotation(data: Dict[str, Any]) -> Optional[bytes]:
if not HAS_WEASYPRINT:
return None
items = data.get("items", [])
items_rows = ""
for i, item in enumerate(items, 1):
items_rows += (
f"<tr>"
f"<td>{item.get('product_name', '')}</td>"
f"<td>{item.get('description', '') or ''}</td>"
f"<td>{item.get('quantity', 0)}</td>"
f"<td>{item.get('unit', 'pcs')}</td>"
f"<td>{item.get('unit_price', 0):.2f}</td>"
f"<td>{item.get('total_price', 0):.2f}</td>"
f"</tr>"
)
cur = data.get("currency", "USD")
subtotal = f"{cur} {data.get('subtotal', 0):.2f}"
discount = f"{cur} {data.get('discount', 0):.2f}" if data.get("discount") else f"{cur} 0.00"
shipping = f"{cur} {data.get('shipping', 0):.2f}" if data.get("shipping") else f"{cur} 0.00"
total = f"{cur} {data.get('total', 0):.2f}"
notes_html = ""
if data.get("notes"):
notes_html = f"<p>Notes: {data['notes']}</p>"
html = QUOTATION_TEMPLATE.format(
quotation_number=data.get("quotation_number", "N/A"),
customer_name=data.get("customer_name", ""),
customer_company=data.get("customer_company", "") or "",
customer_country=data.get("customer_country", "") or "",
date=data.get("date", datetime.utcnow().strftime("%Y-%m-%d")),
valid_until=data.get("valid_until", "N/A"),
currency=cur,
items_rows=items_rows,
subtotal=subtotal,
discount=discount,
shipping=shipping,
total=total,
payment_terms=data.get("payment_terms", "N/A"),
delivery_terms=data.get("delivery_terms", "N/A"),
lead_time=data.get("lead_time", "N/A"),
notes_html=notes_html,
generated_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"),
)
pdf = HTML(string=html).write_pdf()
return pdf
pdf_generator = PDFGenerator()