回复提交
This commit is contained in:
parent
0bb9f76080
commit
0b69978812
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
target: PPT and poster generation UI
|
||||||
|
total_score: 22
|
||||||
|
max_score: 40
|
||||||
|
na_heuristics:
|
||||||
|
p0_count: 0
|
||||||
|
p1_count: 3
|
||||||
|
timestamp: 2026-07-27T05-55-39Z
|
||||||
|
slug: ages-pptpage-vue-frontend-src-pages-posterpage-vue
|
||||||
|
---
|
||||||
|
Method: dual-agent (A: 019fa220-a919-70c1-84a3-41f7a2882aa1 · B: 019fa220-d017-71c2-9c00-e288c7c960e2)
|
||||||
|
|
||||||
|
Design Health Score: 22/40. 可用,但需要明显设计提升。
|
||||||
|
|
||||||
|
Nielsen scores: 状态可见性 3;现实语言 3;用户控制 2;一致性 3;错误预防 2;识别优于记忆 3;效率 1;美学极简 2;错误恢复 2;帮助文档 1。
|
||||||
|
|
||||||
|
Design Specificity Verdict: 中等偏低。PPT 的三类计划书上传、数据校验、利益演示表编辑有保险业务特征,但视觉和交互主要是 Element Plus 默认卡片、步骤条、表格、按钮。海报页更像通用生成器,缺少保险销售场景、合规安心感、成品素材感。
|
||||||
|
|
||||||
|
Deterministic scan: 相关 PPT/海报页面和组件 detector findings 为 0。全量 frontend/src 只有 1 条无关 warning:TokenUsageCard.vue:483 side-tab accent border,不属于本次目标范围。
|
||||||
|
|
||||||
|
Overall Impression: 基础流程能成立,但界面不像一个能让销售放心交付给客户的内容生产工具。最大机会是把流程从“表单和表格集合”重塑成“交付材料工作台”:先给风险摘要、关键确认和成品预览,再把高级编辑放到次级层级。
|
||||||
|
|
||||||
|
What's Working:
|
||||||
|
1. PPT 和海报都使用线性步骤,用户知道自己在流程中的位置。
|
||||||
|
2. 上传、解析、生成、下载都有基础状态反馈。
|
||||||
|
3. PPT 校验确实围绕保险计划书的核心数据,而不是泛泛文件上传器。
|
||||||
|
|
||||||
|
Priority Issues:
|
||||||
|
[P1] PPT 数据校验页像后台数据表,不像销售辅助工具。PptDataReview.vue 同时展示错误、产品块、指标、基础表单、利益演示表、退保提取表和底部操作,信息密度过高。修复方向:风险摘要优先,核心客户信息和关键指标置顶,原始明细按需展开。
|
||||||
|
|
||||||
|
[P1] 海报缺少强成品预览感。PosterStepTemplate.vue 在选择模板和文案时不能看到真实海报结构,PosterStepPreview.vue 到最后才生成结果。修复方向:模板卡使用真实比例预览,展示标题区、卖点区、CTA/二维码区;尺寸选择前置。
|
||||||
|
|
||||||
|
[P1] 高频销售用户效率不足。没有最近模板、默认公司、复制上次配置、历史入口或快速生成路径。修复方向:增加常用配置、最近生成、复用上次材料。
|
||||||
|
|
||||||
|
[P2] 帮助与合规信号不足。IUL、利益演示、退保提取、AI 文案等高风险内容缺少字段解释和人工确认提示。修复方向:关键字段旁添加简短解释,AI 文案和最终下载前加入合规检查提示。
|
||||||
|
|
||||||
|
[P2] 移动端可完成但不舒服。顶部长步骤条、复杂表格、右侧/底部散落按钮都不适合企微 H5。修复方向:移动端使用第 n/m 步摘要,底部固定主按钮,复杂表格改为逐条编辑卡片。
|
||||||
|
|
||||||
|
Persona Red Flags:
|
||||||
|
Jordan 首次使用者:不理解 IUL、利益演示表、退保提取表,不知道哪些字段必须核对。
|
||||||
|
Alex 高频销售用户:每天重复生成时缺少快捷路径,会觉得慢。
|
||||||
|
Sam 辅助技术用户:产品卡和模板卡是 div 点击,存在键盘和屏幕阅读器可达性风险。
|
||||||
|
Casey 移动用户:表格编辑和模板选择在手机上压力大,关键操作不在稳定底部区域。
|
||||||
|
|
||||||
|
Minor Observations:
|
||||||
|
PPT 和海报最大宽度、按钮位置、步骤密度不一致。
|
||||||
|
PPT 结果页只有会话信息和下载,缺少缩略图、页结构、质量确认。
|
||||||
|
海报尺寸选择太晚,前面选模板时无法预期构图比例。
|
||||||
|
后台配置页偏数据库表单,模板/模型管理缺少预览、校验、解释和风险提示。
|
||||||
|
|
||||||
|
Questions:
|
||||||
|
1. 60 秒内生成客户版材料时,哪些配置应默认化?
|
||||||
|
2. 校验页能否先回答“是否可信、哪里需人工确认”,再暴露完整表格?
|
||||||
|
3. 海报模板是在选视觉风格,还是选朋友圈、私聊、讲座邀约、产品对比等销售场景?
|
||||||
@ -568,10 +568,7 @@ class PptAdminService:
|
|||||||
settings = SystemSetting.query.all()
|
settings = SystemSetting.query.all()
|
||||||
data = {}
|
data = {}
|
||||||
for s in settings:
|
for s in settings:
|
||||||
if any(kw in s.key.lower() for kw in self._SENSITIVE_KEYS):
|
data[s.key] = s.value
|
||||||
data[s.key] = self._mask_value(s.key, s.value)
|
|
||||||
else:
|
|
||||||
data[s.key] = s.value
|
|
||||||
return {
|
return {
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"data": data,
|
"data": data,
|
||||||
|
|||||||
@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|||||||
def migrate():
|
def migrate():
|
||||||
"""执行迁移。"""
|
"""执行迁移。"""
|
||||||
from insurance.db.compat import db
|
from insurance.db.compat import db
|
||||||
dialect = db.session.bind.dialect.name
|
dialect = db.engine.dialect.name
|
||||||
|
|
||||||
_fix_slides_config_json(db)
|
_fix_slides_config_json(db)
|
||||||
_add_poster_record_fields(db, dialect)
|
_add_poster_record_fields(db, dialect)
|
||||||
@ -25,7 +25,7 @@ def migrate():
|
|||||||
|
|
||||||
def _column_exists(db, table_name: str, column_name: str) -> bool:
|
def _column_exists(db, table_name: str, column_name: str) -> bool:
|
||||||
"""检查列是否存在。"""
|
"""检查列是否存在。"""
|
||||||
dialect = db.session.bind.dialect.name
|
dialect = db.engine.dialect.name
|
||||||
if dialect == "postgresql":
|
if dialect == "postgresql":
|
||||||
result = db.session.execute(text(
|
result = db.session.execute(text(
|
||||||
"SELECT COUNT(*) FROM information_schema.columns "
|
"SELECT COUNT(*) FROM information_schema.columns "
|
||||||
|
|||||||
156
api/insurance/ppt/comparison.py
Normal file
156
api/insurance/ppt/comparison.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
"""多份保险计划书的确定性对比计算。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from insurance.ppt.irr import compute_irr_ma, ia_irr_cap
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_COMPARISON_YEARS = [5, 10, 20, 30]
|
||||||
|
MAX_COMPARISON_PRODUCTS = 4
|
||||||
|
|
||||||
|
|
||||||
|
def build_comparison_contract(
|
||||||
|
products: list[dict],
|
||||||
|
mode: str = "single",
|
||||||
|
comparison_years: Iterable[int] | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""将归一化产品转换为渲染器可直接使用的对比契约。"""
|
||||||
|
normalized_mode = (mode or "single").lower()
|
||||||
|
if normalized_mode not in {"single", "compare", "portfolio"}:
|
||||||
|
raise ValueError("generationMode 必须是 single、compare 或 portfolio")
|
||||||
|
|
||||||
|
if normalized_mode == "single":
|
||||||
|
return {"mode": "single", "products": [], "years": [], "warnings": []}
|
||||||
|
|
||||||
|
if len(products) < 2:
|
||||||
|
raise ValueError("产品对比或组合方案至少需要 2 份有效计划书")
|
||||||
|
if len(products) > MAX_COMPARISON_PRODUCTS:
|
||||||
|
raise ValueError(f"一次最多对比 {MAX_COMPARISON_PRODUCTS} 份计划书")
|
||||||
|
|
||||||
|
years = _normalize_years(comparison_years)
|
||||||
|
kinds = {str(p.get("kind") or "").lower() for p in products}
|
||||||
|
currencies = {
|
||||||
|
str((p.get("policy") or {}).get("currency") or "").upper()
|
||||||
|
for p in products
|
||||||
|
if (p.get("policy") or {}).get("currency")
|
||||||
|
}
|
||||||
|
warnings: list[str] = []
|
||||||
|
|
||||||
|
if normalized_mode == "compare":
|
||||||
|
if len(kinds) != 1:
|
||||||
|
raise ValueError("不同险种不能直接横向排名,请选择“组合方案”模式")
|
||||||
|
if len(currencies) > 1:
|
||||||
|
raise ValueError("不同币种不能直接比较,请上传相同币种的计划书")
|
||||||
|
elif len(currencies) > 1:
|
||||||
|
warnings.append("组合中包含不同币种,金额仅分别展示,不计算合计")
|
||||||
|
|
||||||
|
insured_signatures = {
|
||||||
|
(
|
||||||
|
(p.get("insured") or {}).get("age"),
|
||||||
|
str((p.get("insured") or {}).get("gender") or "").lower(),
|
||||||
|
)
|
||||||
|
for p in products
|
||||||
|
}
|
||||||
|
if len(insured_signatures) > 1:
|
||||||
|
warnings.append("计划书的受保人年龄或性别不一致,比较结论需谨慎使用")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"mode": normalized_mode,
|
||||||
|
"years": years,
|
||||||
|
"currency": next(iter(currencies), ""),
|
||||||
|
"productKinds": sorted(kinds),
|
||||||
|
"products": [_build_product_metrics(product, years) for product in products],
|
||||||
|
"warnings": warnings,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_years(years: Iterable[int] | None) -> list[int]:
|
||||||
|
values = []
|
||||||
|
for value in years or DEFAULT_COMPARISON_YEARS:
|
||||||
|
try:
|
||||||
|
year = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if 1 <= year <= 100 and year not in values:
|
||||||
|
values.append(year)
|
||||||
|
return sorted(values) or list(DEFAULT_COMPARISON_YEARS)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_product_metrics(product: dict, years: list[int]) -> dict:
|
||||||
|
policy = product.get("policy") or {}
|
||||||
|
rows = product.get("benefitRows") or []
|
||||||
|
annual_premium = _number(policy.get("annualPremium"))
|
||||||
|
pay_years = int(_number(policy.get("payYears")))
|
||||||
|
total_premium = _number(
|
||||||
|
policy.get("contractualTotalPremium")
|
||||||
|
or policy.get("totalPremium")
|
||||||
|
or annual_premium * pay_years
|
||||||
|
)
|
||||||
|
currency = str(policy.get("currency") or "").upper()
|
||||||
|
|
||||||
|
year_values = {}
|
||||||
|
for year in years:
|
||||||
|
row = _find_year(rows, year)
|
||||||
|
if not row:
|
||||||
|
year_values[str(year)] = None
|
||||||
|
continue
|
||||||
|
total_value = _number(row.get("totalSurrenderValue"))
|
||||||
|
guaranteed_value = _number(row.get("guaranteedCashValue"))
|
||||||
|
death_benefit = _number(
|
||||||
|
row.get("nonGuaranteedDeathBenefit")
|
||||||
|
or row.get("deathBenefit")
|
||||||
|
or row.get("guaranteedDeathBenefit")
|
||||||
|
)
|
||||||
|
irr = compute_irr_ma(
|
||||||
|
annual_premium,
|
||||||
|
pay_years,
|
||||||
|
total_value,
|
||||||
|
year,
|
||||||
|
ia_irr_cap(currency or "HKD"),
|
||||||
|
)
|
||||||
|
year_values[str(year)] = {
|
||||||
|
"guaranteedValue": guaranteed_value,
|
||||||
|
"totalValue": total_value,
|
||||||
|
"deathBenefit": death_benefit,
|
||||||
|
"irr": round(irr * 100, 2) if irr is not None else None,
|
||||||
|
"sourcePage": row.get("sourcePage"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"fileId": product.get("fileId", ""),
|
||||||
|
"pdfName": product.get("pdfName", ""),
|
||||||
|
"companyId": product.get("companyId", ""),
|
||||||
|
"productName": product.get("productName", ""),
|
||||||
|
"kind": product.get("kind", "savings"),
|
||||||
|
"currency": currency,
|
||||||
|
"annualPremium": annual_premium,
|
||||||
|
"payYears": pay_years,
|
||||||
|
"totalPremium": total_premium,
|
||||||
|
"sumInsured": _number(policy.get("sumInsured")),
|
||||||
|
"breakevenYear": _find_breakeven_year(rows),
|
||||||
|
"yearValues": year_values,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _find_year(rows: list[dict], year: int) -> dict | None:
|
||||||
|
for row in rows:
|
||||||
|
if int(_number(row.get("policyYear"))) == year:
|
||||||
|
return row
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _find_breakeven_year(rows: list[dict]) -> int | None:
|
||||||
|
for row in rows:
|
||||||
|
premium = _number(row.get("totalPremiumPaid"))
|
||||||
|
value = _number(row.get("totalSurrenderValue"))
|
||||||
|
if premium > 0 and value >= premium:
|
||||||
|
return int(_number(row.get("policyYear")))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _number(value) -> float:
|
||||||
|
try:
|
||||||
|
return float(value or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
Loading…
Reference in New Issue
Block a user