修复海报生成过程的BUG
This commit is contained in:
parent
573210f9c3
commit
38a25fbe4a
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,6 +13,8 @@ deploy/
|
||||
api_storage
|
||||
api_storage_backup
|
||||
|
||||
data_bak
|
||||
|
||||
|
||||
dify-main/docker
|
||||
|
||||
|
||||
@ -211,8 +211,7 @@ def build_scenario_slides(products: list[dict], scenario: str) -> list[dict]:
|
||||
page_types = ["cover", "company", "narrative", "chart"]
|
||||
if len(products) > 1:
|
||||
page_types.append("compare")
|
||||
kinds = {str(p.get("kind") or "").lower() for p in products}
|
||||
if len(kinds) > 1:
|
||||
if scenario != SCENARIO_GENERIC_COMPARE:
|
||||
page_types.append("synergy")
|
||||
page_types.extend(["table", "conclusion", "closing"])
|
||||
return [{"pageType": page_type} for page_type in page_types]
|
||||
|
||||
@ -1297,31 +1297,90 @@ def add_slide_alignment_table(prs, deck, colors, meta):
|
||||
)
|
||||
|
||||
|
||||
def _compare_card_body(product, comparison_product):
|
||||
"""按险种生成对比卡片正文。CI/IUL 使用与储蓄险不同的指标。"""
|
||||
kind = str(product.get("kind") or "savings").lower()
|
||||
policy = product.get("policy") or {}
|
||||
ap = policy.get("annualPremium", 0)
|
||||
|
||||
if kind == "ci":
|
||||
si = policy.get("sumInsured") or 0
|
||||
br = product.get("benefitRows") or []
|
||||
period = int(br[-1].get("policyYear", 0)) if br else 0
|
||||
return (f"年缴 ${money(ap)}\n"
|
||||
f"基本保额 ${money(si)}\n"
|
||||
f"保障期 {period} 年")
|
||||
|
||||
if kind == "iul":
|
||||
# 优先使用比较契约中已计算的年份数据
|
||||
yv = (comparison_product or {}).get("yearValues") or {}
|
||||
g10 = None
|
||||
ng10 = None
|
||||
for yr_key in ("10", "20", "30"):
|
||||
vals = yv.get(yr_key)
|
||||
if vals:
|
||||
g10 = vals.get("guaranteedValue")
|
||||
ng10 = vals.get("totalValue")
|
||||
break
|
||||
if g10 is None:
|
||||
br = product.get("benefitRows") or []
|
||||
row10 = next((r for r in br if int(r.get("policyYear", 0)) == 10), None)
|
||||
if row10:
|
||||
g10 = row10.get("guaranteedCashValue", 0)
|
||||
ng10 = row10.get("totalSurrenderValue", 0)
|
||||
return (f"年缴 ${money(ap)}\n"
|
||||
f"第10年保证 ${money(g10 or 0)}\n"
|
||||
f"第10年非保证 ${money(ng10 or 0)}")
|
||||
|
||||
# 储蓄险(默认)
|
||||
s = _product_summary(product)
|
||||
return (f"年缴 ${money(s['annualPremium'])}\n"
|
||||
f"总投入 ${money(s['totalPremium'])}\n"
|
||||
f"回本约第 {s['paybackYear'] or '?'} 年")
|
||||
|
||||
|
||||
def add_slide_compare(prs, deck, colors, meta):
|
||||
"""对比页:多产品对比,支持 1~10 份。"""
|
||||
"""对比页:多产品对比,支持 1~10 份。按险种显示不同指标。"""
|
||||
slide = add_blank_slide(prs)
|
||||
add_bg(slide, colors)
|
||||
|
||||
products = deck.get("products", [])
|
||||
comparison = deck.get("comparison") or {}
|
||||
comp_products = comparison.get("products") or []
|
||||
title = meta.get("title", "产品对比")
|
||||
add_title(slide, title, meta.get("narrativeHint", ""), colors=colors)
|
||||
|
||||
if len(products) < 2:
|
||||
# 单产品时显示保证 vs 非保证对比
|
||||
product = products[0] if products else {}
|
||||
kind = str(product.get("kind") or "savings").lower()
|
||||
s = _product_summary(product)
|
||||
add_fact_card(slide, Inches(0.8), Inches(1.45), Inches(5.45), Inches(2.5),
|
||||
"保证部分", f"保证现金价值\n回本约第 {s['paybackYear'] or '?'} 年\n确定性高",
|
||||
fill_color=colors["accent_light"], colors=colors)
|
||||
add_fact_card(slide, Inches(6.95), Inches(1.45), Inches(5.45), Inches(2.5),
|
||||
"非保证部分", f"归原红利 + 终期分红\n长期弹性空间大\n取决于公司投资表现",
|
||||
fill_color=colors["good_light"], colors=colors)
|
||||
if kind == "ci":
|
||||
add_fact_card(slide, Inches(0.8), Inches(1.45), Inches(5.45), Inches(2.5),
|
||||
"重疾保障", f"基本保额 ${money(s.get('annualPremium', 0) * 20)}\n保障期内确诊即赔\n多次赔付视条款而定",
|
||||
fill_color=colors["accent_light"], colors=colors)
|
||||
add_fact_card(slide, Inches(6.95), Inches(1.45), Inches(5.45), Inches(2.5),
|
||||
"早期重疾", f"原位癌、早期危疾等\n赔付比例通常 20%~25%\n不终止主保单",
|
||||
fill_color=colors["good_light"], colors=colors)
|
||||
elif kind == "iul":
|
||||
add_fact_card(slide, Inches(0.8), Inches(1.45), Inches(5.45), Inches(2.5),
|
||||
"保证部分", f"保证现金价值\n最低身故保障\n不受投资表现影响",
|
||||
fill_color=colors["accent_light"], colors=colors)
|
||||
add_fact_card(slide, Inches(6.95), Inches(1.45), Inches(5.45), Inches(2.5),
|
||||
"非保证部分", f"指数账户收益\n演示利率仅供参考\n实际取决于市场表现",
|
||||
fill_color=colors["good_light"], colors=colors)
|
||||
else:
|
||||
add_fact_card(slide, Inches(0.8), Inches(1.45), Inches(5.45), Inches(2.5),
|
||||
"保证部分", f"保证现金价值\n回本约第 {s['paybackYear'] or '?'} 年\n确定性高",
|
||||
fill_color=colors["accent_light"], colors=colors)
|
||||
add_fact_card(slide, Inches(6.95), Inches(1.45), Inches(5.45), Inches(2.5),
|
||||
"非保证部分", f"归原红利 + 终期分红\n长期弹性空间大\n取决于公司投资表现",
|
||||
fill_color=colors["good_light"], colors=colors)
|
||||
add_paragraphs(slide, Inches(0.85), Inches(4.5), Inches(11.4), Inches(1.5),
|
||||
["保证部分是底线,非保证部分是弹性。两者合计才是长期回报的完整图景。"],
|
||||
size=15, bullet=True, colors=colors)
|
||||
elif len(products) <= 4:
|
||||
# 2~4 产品:卡片网格布局
|
||||
card_w = Inches(5.45) if len(products) == 2 else Inches(5.45)
|
||||
# 2~4 产品:卡片网格,按险种显示不同指标
|
||||
card_h = Inches(2.5) if len(products) <= 2 else Inches(2.0)
|
||||
fill_colors = [colors["accent_light"], colors["good_light"],
|
||||
colors["accent_light"], colors["good_light"]]
|
||||
@ -1332,40 +1391,59 @@ def add_slide_compare(prs, deck, colors, meta):
|
||||
(Inches(6.95), Inches(3.7)),
|
||||
]
|
||||
for i, product in enumerate(products):
|
||||
s = _product_summary(product)
|
||||
cp = comp_products[i] if i < len(comp_products) else None
|
||||
body = _compare_card_body(product, cp)
|
||||
x, y = positions[i]
|
||||
add_fact_card(slide, x, y, card_w, card_h,
|
||||
s["productName"] or f"产品 {i + 1}",
|
||||
f"年缴 ${money(s['annualPremium'])}\n"
|
||||
f"总投入 ${money(s['totalPremium'])}\n"
|
||||
f"回本约第 {s['paybackYear'] or '?'} 年",
|
||||
name = product.get("productName", "") or f"产品 {i + 1}"
|
||||
kind_label = _kind_label(product.get("kind", "savings"))
|
||||
add_fact_card(slide, x, y, Inches(5.45), card_h,
|
||||
f"{name}({kind_label})", body,
|
||||
fill_color=fill_colors[i % len(fill_colors)], colors=colors)
|
||||
bottom_y = Inches(6.0) if len(products) <= 2 else Inches(6.0)
|
||||
add_paragraphs(slide, Inches(0.85), bottom_y, Inches(11.4), Inches(1.0),
|
||||
["对比重点在相同保单年度下的保证价值、总退保价值和回本时间。"],
|
||||
size=15, bullet=True, colors=colors)
|
||||
else:
|
||||
# 5+ 产品:表格布局
|
||||
from pptx.util import Emu
|
||||
# 5+ 产品:表格布局,按险种调整列
|
||||
kinds = {str(p.get("kind") or "savings").lower() for p in products}
|
||||
if len(kinds) == 1 and "ci" in kinds:
|
||||
headers = ["产品", "年缴保费", "基本保额", "保障期", "缴费年期"]
|
||||
elif len(kinds) == 1 and "iul" in kinds:
|
||||
headers = ["产品", "年缴保费", "第10年保证", "第10年非保证", "缴费年期"]
|
||||
else:
|
||||
headers = ["产品", "险种", "年缴保费", "回本年", "期末倍数"]
|
||||
rows = len(products) + 1
|
||||
cols = 5
|
||||
cols = len(headers)
|
||||
table_left, table_top = Inches(0.6), Inches(1.5)
|
||||
table_w, table_h = Inches(12.0), Inches(0.4 * rows + 0.2)
|
||||
table_shape = slide.shapes.add_table(rows, cols, table_left, table_top, table_w, table_h)
|
||||
table = table_shape.table
|
||||
headers = ["产品", "年缴保费", "总投入", "回本年", "期末倍数"]
|
||||
for c, h in enumerate(headers):
|
||||
table.cell(0, c).text = h
|
||||
_style_cell(table.cell(0, c), bold=True, bg=colors.get("accent", RGBColor(59, 122, 87)))
|
||||
for r, product in enumerate(products, 1):
|
||||
kind = str(product.get("kind") or "savings").lower()
|
||||
policy = product.get("policy") or {}
|
||||
s = _product_summary(product)
|
||||
values = [
|
||||
s["productName"] or f"产品 {r}",
|
||||
f"${money(s['annualPremium'])}",
|
||||
f"${money(s['totalPremium'])}",
|
||||
str(s["paybackYear"] or "-"),
|
||||
s["multiple"],
|
||||
]
|
||||
name = product.get("productName", "") or f"产品 {r}"
|
||||
if len(kinds) == 1 and "ci" in kinds:
|
||||
br = product.get("benefitRows") or []
|
||||
period = int(br[-1].get("policyYear", 0)) if br else 0
|
||||
values = [name, f"${money(policy.get('annualPremium', 0))}",
|
||||
f"${money(policy.get('sumInsured', 0))}",
|
||||
f"{period} 年", f"{policy.get('payYears', '-')} 年"]
|
||||
elif len(kinds) == 1 and "iul" in kinds:
|
||||
cp = comp_products[r - 1] if (r - 1) < len(comp_products) else None
|
||||
yv = (cp or {}).get("yearValues") or {}
|
||||
v10 = yv.get("10") or {}
|
||||
values = [name, f"${money(policy.get('annualPremium', 0))}",
|
||||
f"${money(v10.get('guaranteedValue', 0))}",
|
||||
f"${money(v10.get('totalValue', 0))}",
|
||||
f"{policy.get('payYears', '-')} 年"]
|
||||
else:
|
||||
values = [name, _kind_label(kind),
|
||||
f"${money(s['annualPremium'])}",
|
||||
str(s["paybackYear"] or "-"), s["multiple"]]
|
||||
for c, v in enumerate(values):
|
||||
table.cell(r, c).text = v
|
||||
_style_cell(table.cell(r, c), bold=(c == 0))
|
||||
|
||||
@ -134,7 +134,12 @@ export function usePosterWorkspace() {
|
||||
async function restore(): Promise<boolean> {
|
||||
const urlRecordId = route.params.recordId as string
|
||||
if (urlRecordId) {
|
||||
recordId.value = Number(urlRecordId)
|
||||
const nextRecordId = Number(urlRecordId)
|
||||
if (!Number.isFinite(nextRecordId)) return false
|
||||
if (recordId.value !== nextRecordId) {
|
||||
recordId.value = nextRecordId
|
||||
draft.value = createEmptyDraft()
|
||||
}
|
||||
}
|
||||
|
||||
// 先尝试 localStorage 快恢复
|
||||
@ -147,9 +152,8 @@ export function usePosterWorkspace() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await api.get(`/poster/records/${recordId.value}`)
|
||||
const data = res?.data ?? res
|
||||
if (data?.code === 0 && data?.data) {
|
||||
const record = data.data
|
||||
const record = res?.data ?? res
|
||||
if (record?.id) {
|
||||
// 用后端数据填充缺失字段
|
||||
if (!draft.value.productId && record.productId) {
|
||||
draft.value.productId = record.productId
|
||||
@ -166,9 +170,18 @@ export function usePosterWorkspace() {
|
||||
if (record.copyContent) {
|
||||
draft.value.copyContent = record.copyContent
|
||||
}
|
||||
if (record.aiRawContent) {
|
||||
draft.value.aiRawContent = record.aiRawContent
|
||||
}
|
||||
if (record.extraData?.useMaskedData !== undefined) {
|
||||
draft.value.useMaskedData = record.extraData.useMaskedData
|
||||
}
|
||||
draft.value.taskRecordId = record.id
|
||||
draft.value.taskStatus = record.taskStatus === 'pending'
|
||||
? 'queued'
|
||||
: (record.taskStatus || 'idle')
|
||||
draft.value.taskProgress = record.taskProgress ?? 0
|
||||
draft.value.taskError = record.taskError || null
|
||||
restored.value = true
|
||||
return true
|
||||
}
|
||||
|
||||
@ -62,9 +62,8 @@ export function usePptWorkspace() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await api.get(`/ppt/session/${urlSessionId}`)
|
||||
const data = res?.data ?? res
|
||||
if (data?.code === 0 && data?.data) {
|
||||
const session = data.data
|
||||
const session = res?.data ?? res
|
||||
if (session?.id) {
|
||||
sessionId.value = session.id
|
||||
serverStatus.value = session.status
|
||||
parseProgress.value = session.parse_progress || 0
|
||||
|
||||
@ -84,14 +84,14 @@
|
||||
|
||||
<!-- 任务抽屉 -->
|
||||
<el-drawer v-model="showTasksDrawer" title="任务中心" size="400px">
|
||||
<TasksPage artifact-type="poster" embedded />
|
||||
<TasksPage artifact-type="poster" embedded @open-workspace="showTasksDrawer = false" />
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { EditPen } from '@element-plus/icons-vue'
|
||||
import { usePosterWorkspace } from '@/composables/usePosterWorkspace'
|
||||
@ -105,6 +105,7 @@ import PosterActionBar from '@/components/poster/workspace/PosterActionBar.vue'
|
||||
import TasksPage from './TasksPage.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const ws = usePosterWorkspace()
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
@ -315,10 +316,54 @@ async function onReset() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 生命周期 ─────────────────────────────
|
||||
onMounted(async () => {
|
||||
// ── 工作区恢复 ───────────────────────────
|
||||
async function restoreWorkspace() {
|
||||
await ws.restore()
|
||||
})
|
||||
// 恢复任务真实状态;已完成任务同时重建浏览器内的图片预览 URL
|
||||
const d = ws.draft.value
|
||||
if (d.taskRecordId || ws.recordId.value) {
|
||||
const pollId = d.taskRecordId || ws.recordId.value!
|
||||
try {
|
||||
const res: any = await posterApi.getRecord(pollId)
|
||||
const record = res?.data ?? res
|
||||
const realStatus = record?.taskStatus || record?.task_status
|
||||
d.taskStatus = realStatus === 'pending' ? 'queued' : (realStatus || 'idle')
|
||||
d.taskProgress = record?.taskProgress ?? record?.task_progress ?? 0
|
||||
d.taskError = record?.taskError || record?.task_error || null
|
||||
|
||||
if (realStatus === 'done') {
|
||||
try {
|
||||
const blob = await posterApi.downloadPoster(pollId)
|
||||
d.posterUrl = URL.createObjectURL(blob)
|
||||
} catch {
|
||||
d.taskStatus = 'failed'
|
||||
d.taskError = '海报图片加载失败,请刷新页面重试'
|
||||
}
|
||||
} else if (realStatus !== 'failed') {
|
||||
startPolling(pollId)
|
||||
}
|
||||
} catch {
|
||||
d.taskStatus = 'failed'
|
||||
d.taskError = '任务内容恢复失败,请稍后重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 生命周期 ─────────────────────────────
|
||||
onMounted(restoreWorkspace)
|
||||
|
||||
watch(
|
||||
() => route.params.recordId,
|
||||
async (nextRecordId, previousRecordId) => {
|
||||
if (nextRecordId && nextRecordId !== previousRecordId) {
|
||||
stopPolling()
|
||||
if (ws.draft.value.posterUrl) {
|
||||
URL.revokeObjectURL(ws.draft.value.posterUrl)
|
||||
}
|
||||
await restoreWorkspace()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user