feat(ppt): add data review step between parsing and generation
Add a mandatory data verification step so users can inspect and edit LLM-extracted data before generating the PPT. - New PptDataReview.vue: auto-validation on mount, key metrics cards, editable benefit/withdrawal tables, inline error highlighting - New PUT /session/<id>/extractions API endpoint for saving user edits - Extend PptPage flow from 4 steps to 5 (upload→parse→review→generate→result) - Update ppt-api.ts with updateExtractions() method - Update PptParsing button text to match new flow Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1debd7df39
commit
63c7e87ea7
@ -450,6 +450,61 @@ def validate_extraction(session_id):
|
||||
})
|
||||
|
||||
|
||||
# ─── 更新提取数据 ─────────────────────────────────────────
|
||||
|
||||
@ppt_bp.route("/session/<session_id>/extractions", methods=["PUT"])
|
||||
@jwt_required
|
||||
def update_extractions(session_id):
|
||||
"""保存用户修改后的提取数据。"""
|
||||
user_id = str(getattr(request, "user_id", "guest"))
|
||||
session = _get_session(session_id, user_id)
|
||||
if not session:
|
||||
return error(ErrorCode.NOT_FOUND, "会话不存在")
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
extractions = data.get("extractions")
|
||||
if not isinstance(extractions, list):
|
||||
return error(ErrorCode.PARAM_ERROR, "extractions 必须是数组")
|
||||
|
||||
# 合并更新:只更新 data 字段,保留 pdfPath/status 等元信息
|
||||
existing = json.loads(session.extractions_json) if session.extractions_json else []
|
||||
existing_map = {e["pdfName"]: e for e in existing}
|
||||
|
||||
for ext in extractions:
|
||||
pdf_name = ext.get("pdfName")
|
||||
if not pdf_name or pdf_name not in existing_map:
|
||||
continue
|
||||
# 更新数据字段
|
||||
if "data" in ext:
|
||||
existing_map[pdf_name]["data"] = ext["data"]
|
||||
if "productName" in ext:
|
||||
existing_map[pdf_name]["productName"] = ext["productName"]
|
||||
if "planType" in ext:
|
||||
existing_map[pdf_name]["planType"] = ext["planType"]
|
||||
# 重新计算行数
|
||||
d = existing_map[pdf_name].get("data")
|
||||
if d:
|
||||
rows = d.get("benefit_illustration") or d.get("benefitRows") or []
|
||||
existing_map[pdf_name]["yearCount"] = len(rows)
|
||||
|
||||
updated = list(existing_map.values())
|
||||
session.extractions_json = json.dumps(updated, ensure_ascii=False)
|
||||
session.status = "parsed" # 回到 parsed 状态,需要重新生成
|
||||
_save_session(session)
|
||||
|
||||
return success({
|
||||
"sessionId": session_id,
|
||||
"status": "updated",
|
||||
"extractions": [{
|
||||
"pdfName": e["pdfName"],
|
||||
"planType": e["planType"],
|
||||
"status": e["status"],
|
||||
"productName": e["productName"],
|
||||
"yearCount": e["yearCount"],
|
||||
} for e in updated],
|
||||
})
|
||||
|
||||
|
||||
# ─── 公司知识库匹配 ───────────────────────────────────────
|
||||
|
||||
@ppt_bp.route("/company-kb/match", methods=["POST"])
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
<el-steps :active="currentStep" finish-status="success" align-center class="ppt-steps">
|
||||
<el-step title="上传" description="选择PDF计划书" />
|
||||
<el-step title="解析" description="AI智能识别" />
|
||||
<el-step title="校验" description="查看并确认数据" />
|
||||
<el-step title="生成" description="选择风格并生成" />
|
||||
<el-step title="结果" description="下载PPT" />
|
||||
</el-steps>
|
||||
@ -20,16 +21,22 @@
|
||||
@parsed="onParsed"
|
||||
@back="currentStep = 0"
|
||||
/>
|
||||
<PptGenerate
|
||||
<PptDataReview
|
||||
v-else-if="currentStep === 2"
|
||||
:session-id="sessionId"
|
||||
@generated="onGenerated"
|
||||
@confirmed="onDataConfirmed"
|
||||
@back="currentStep = 1"
|
||||
/>
|
||||
<PptResult
|
||||
<PptGenerate
|
||||
v-else-if="currentStep === 3"
|
||||
:session-id="sessionId"
|
||||
@regenerate="currentStep = 2"
|
||||
@generated="onGenerated"
|
||||
@back="currentStep = 2"
|
||||
/>
|
||||
<PptResult
|
||||
v-else-if="currentStep === 4"
|
||||
:session-id="sessionId"
|
||||
@regenerate="currentStep = 3"
|
||||
@new-session="resetAll"
|
||||
/>
|
||||
</div>
|
||||
@ -40,6 +47,7 @@
|
||||
import { ref } from 'vue'
|
||||
import PptUpload from './components/ppt/PptUpload.vue'
|
||||
import PptParsing from './components/ppt/PptParsing.vue'
|
||||
import PptDataReview from './components/ppt/PptDataReview.vue'
|
||||
import PptGenerate from './components/ppt/PptGenerate.vue'
|
||||
import PptResult from './components/ppt/PptResult.vue'
|
||||
|
||||
@ -55,10 +63,14 @@ function onParsed() {
|
||||
currentStep.value = 2
|
||||
}
|
||||
|
||||
function onGenerated() {
|
||||
function onDataConfirmed() {
|
||||
currentStep.value = 3
|
||||
}
|
||||
|
||||
function onGenerated() {
|
||||
currentStep.value = 4
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
currentStep.value = 0
|
||||
sessionId.value = ''
|
||||
|
||||
497
frontend/src/pages/components/ppt/PptDataReview.vue
Normal file
497
frontend/src/pages/components/ppt/PptDataReview.vue
Normal file
@ -0,0 +1,497 @@
|
||||
<template>
|
||||
<div class="ppt-data-review">
|
||||
<el-card shadow="never" class="review-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>数据校验</span>
|
||||
<el-tag v-if="validationStatus === 'pass'" type="success" size="small" class="header-tag">全部通过</el-tag>
|
||||
<el-tag v-else-if="validationStatus === 'warn'" type="warning" size="small" class="header-tag">有警告</el-tag>
|
||||
<el-tag v-else-if="validationStatus === 'error'" type="danger" size="small" class="header-tag">有错误</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<el-icon class="is-loading" :size="32"><Loading /></el-icon>
|
||||
<p>正在校验数据...</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 验证问题列表 -->
|
||||
<div v-if="issues.length" class="issues-section">
|
||||
<div
|
||||
v-for="(issue, idx) in issues"
|
||||
:key="idx"
|
||||
class="issue-item"
|
||||
:class="issue.severity"
|
||||
>
|
||||
<el-icon v-if="issue.severity === 'error'" color="#f56c6c"><CircleCloseFilled /></el-icon>
|
||||
<el-icon v-else color="#e6a23c"><WarningFilled /></el-icon>
|
||||
<span class="issue-field">{{ issue.field }}</span>
|
||||
<span class="issue-msg">{{ issue.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 逐个产品展示 -->
|
||||
<div v-for="(ext, idx) in extractions" :key="idx" class="extraction-block">
|
||||
<div class="extraction-header">
|
||||
<el-tag :type="typeTagMap[ext.planType] || 'info'" size="small">
|
||||
{{ typeNameMap[ext.planType] || ext.planType }}
|
||||
</el-tag>
|
||||
<span class="product-name">{{ ext.productName }}</span>
|
||||
<el-tag v-if="ext.status === 'success' || ext.status === 'cached'" type="success" size="small">解析成功</el-tag>
|
||||
<el-tag v-else type="danger" size="small">解析失败</el-tag>
|
||||
</div>
|
||||
|
||||
<template v-if="ext.data && (ext.status === 'success' || ext.status === 'cached')">
|
||||
<!-- 关键指标卡 -->
|
||||
<div class="metrics-row">
|
||||
<div class="metric-card" v-for="m in getKeyMetrics(ext)" :key="m.label">
|
||||
<div class="metric-value">{{ m.value }}</div>
|
||||
<div class="metric-label">{{ m.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息(可编辑) -->
|
||||
<el-collapse>
|
||||
<el-collapse-item title="基本信息(可编辑)" name="basic">
|
||||
<el-form label-width="120px" size="small" class="basic-form">
|
||||
<el-form-item label="产品名称">
|
||||
<el-input v-model="ext.data.product_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品类型">
|
||||
<el-select v-model="ext.data.product_type" style="width: 100%">
|
||||
<el-option label="储蓄险" value="savings" />
|
||||
<el-option label="重疾险" value="ci" />
|
||||
<el-option label="IUL" value="iul" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="被保人年龄">
|
||||
<el-input-number v-model="ext.data.insured.age" :min="0" :max="120" />
|
||||
</el-form-item>
|
||||
<el-form-item label="被保人性别">
|
||||
<el-select v-model="ext.data.insured.gender" style="width: 100%">
|
||||
<el-option label="男" value="male" />
|
||||
<el-option label="女" value="female" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="年缴保费">
|
||||
<el-input-number v-model="ext.data.policy.annual_premium" :min="0" :step="1000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="缴费年期">
|
||||
<el-input-number v-model="ext.data.policy.premium_payment_period" :min="1" :max="50" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="ext.data.policy.sum_insured" label="保额">
|
||||
<el-input-number v-model="ext.data.policy.sum_insured" :min="0" :step="10000" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-collapse-item>
|
||||
|
||||
<!-- 利益演示表(可编辑) -->
|
||||
<el-collapse-item :title="`利益演示表(${ext.yearCount} 行,可编辑)`" name="benefit">
|
||||
<div class="table-wrapper">
|
||||
<el-table
|
||||
:data="ext.data.benefit_illustration || []"
|
||||
stripe
|
||||
border
|
||||
size="small"
|
||||
max-height="400"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="policy_year" label="保单年度" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.policy_year" :min="1" :max="100" size="small" controls-position="right" style="width: 70px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="age" label="年龄" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.age" :min="0" :max="130" size="small" controls-position="right" style="width: 60px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_premium_paid" label="累计保费" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.total_premium_paid" :min="0" :step="1000" size="small" controls-position="right" style="width: 100px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="guaranteed_cash_value" label="保证现金价值" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.guaranteed_cash_value" :min="0" :step="1000" size="small" controls-position="right" style="width: 110px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="hasField(ext.data.benefit_illustration, 'reversionary_bonus')" prop="reversionary_bonus" label="归原红利" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.reversionary_bonus" :min="0" :step="1000" size="small" controls-position="right" style="width: 100px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="hasField(ext.data.benefit_illustration, 'terminal_dividend')" prop="terminal_dividend" label="终期红利" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.terminal_dividend" :min="0" :step="1000" size="small" controls-position="right" style="width: 100px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_surrender_value" label="退保总值" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.total_surrender_value" :min="0" :step="1000" size="small" controls-position="right" style="width: 100px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="hasField(ext.data.benefit_illustration, 'death_benefit')" prop="death_benefit" label="身故赔偿" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.death_benefit" :min="0" :step="1000" size="small" controls-position="right" style="width: 100px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<!-- 退保提取表(可编辑,如果有) -->
|
||||
<el-collapse-item
|
||||
v-if="ext.data.withdrawal_illustration && ext.data.withdrawal_illustration.length"
|
||||
:title="`退保提取表(${ext.data.withdrawal_illustration.length} 行,可编辑)`"
|
||||
name="withdrawal"
|
||||
>
|
||||
<div class="table-wrapper">
|
||||
<el-table
|
||||
:data="ext.data.withdrawal_illustration"
|
||||
stripe
|
||||
border
|
||||
size="small"
|
||||
max-height="300"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="policy_year" label="保单年度" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.policy_year" :min="1" :max="100" size="small" controls-position="right" style="width: 70px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="withdrawal_amount" label="提取金额" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.withdrawal_amount" :min="0" :step="1000" size="small" controls-position="right" style="width: 100px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="hasField(ext.data.withdrawal_illustration, 'cumulative_withdrawal')" prop="cumulative_withdrawal" label="累计提取" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.cumulative_withdrawal" :min="0" :step="1000" size="small" controls-position="right" style="width: 100px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remaining_surrender_value" label="剩余退保价值" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.remaining_surrender_value" :min="0" :step="1000" size="small" controls-position="right" style="width: 110px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
|
||||
<el-empty v-else-if="ext.status === 'error'" :description="ext.error || '解析失败'" />
|
||||
</div>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="review-actions">
|
||||
<el-button @click="$emit('back')">返回上传</el-button>
|
||||
<el-button v-if="isDirty" @click="handleSave" :loading="saving">
|
||||
保存修改
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="validationStatus === 'error'"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确认数据,生成 PPT
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
DataAnalysis, Loading, CircleCloseFilled, WarningFilled,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { pptApi } from '@/utils/ppt-api'
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirmed: []
|
||||
back: []
|
||||
}>()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const extractions = ref<any[]>([])
|
||||
const issues = ref<Array<{ field: string; severity: string; message: string }>>([])
|
||||
const originalJson = ref('')
|
||||
|
||||
const typeTagMap: Record<string, string> = {
|
||||
savings: 'success',
|
||||
ci: 'warning',
|
||||
iul: 'info',
|
||||
}
|
||||
const typeNameMap: Record<string, string> = {
|
||||
savings: '储蓄险',
|
||||
ci: '重疾险',
|
||||
iul: 'IUL',
|
||||
}
|
||||
|
||||
const validationStatus = computed(() => {
|
||||
if (issues.value.some(i => i.severity === 'error')) return 'error'
|
||||
if (issues.value.some(i => i.severity === 'warn')) return 'warn'
|
||||
return 'pass'
|
||||
})
|
||||
|
||||
const isDirty = computed(() => {
|
||||
return JSON.stringify(extractions.value) !== originalJson.value
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 并行获取会话数据和验证结果
|
||||
const [sessionRes, validateRes]: any[] = await Promise.all([
|
||||
pptApi.getSession(props.sessionId),
|
||||
pptApi.validate(props.sessionId),
|
||||
])
|
||||
|
||||
// 解析会话中的完整提取数据
|
||||
const sessionData = sessionRes?.data
|
||||
if (sessionData?.extractions_json) {
|
||||
extractions.value = JSON.parse(sessionData.extractions_json)
|
||||
} else if (sessionData?.extractions) {
|
||||
extractions.value = sessionData.extractions
|
||||
}
|
||||
|
||||
// 确保 data 中的子对象存在
|
||||
for (const ext of extractions.value) {
|
||||
if (ext.data) {
|
||||
ext.data.insured = ext.data.insured || {}
|
||||
ext.data.policy = ext.data.policy || {}
|
||||
}
|
||||
}
|
||||
|
||||
originalJson.value = JSON.stringify(extractions.value)
|
||||
|
||||
// 验证结果
|
||||
const validateData = validateRes?.data
|
||||
issues.value = validateData?.issues || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error('加载数据失败: ' + (e?.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function getKeyMetrics(ext: any) {
|
||||
const d = ext.data
|
||||
if (!d) return []
|
||||
|
||||
const policy = d.policy || {}
|
||||
const insured = d.insured || {}
|
||||
const rows = d.benefit_illustration || []
|
||||
|
||||
const metrics = [
|
||||
{ label: '被保人年龄', value: insured.age ? `${insured.age}岁` : '-' },
|
||||
{ label: '年缴保费', value: policy.annual_premium ? formatNum(policy.annual_premium) : '-' },
|
||||
{ label: '缴费年期', value: policy.premium_payment_period ? `${policy.premium_payment_period}年` : '-' },
|
||||
]
|
||||
|
||||
// 计算回本年份
|
||||
const annualPremium = Number(policy.annual_premium) || 0
|
||||
const payYears = Number(policy.premium_payment_period) || 0
|
||||
if (annualPremium > 0 && payYears > 0 && rows.length > 0) {
|
||||
const totalInvest = annualPremium * payYears
|
||||
const breakevenRow = rows.find((r: any) => (Number(r.total_surrender_value) || 0) >= totalInvest)
|
||||
metrics.push({ label: '总投入', value: formatNum(totalInvest) })
|
||||
metrics.push({ label: '回本年份', value: breakevenRow ? `第${breakevenRow.policy_year}年` : '未回本' })
|
||||
|
||||
// 第20年倍数
|
||||
const row20 = rows.find((r: any) => r.policy_year === 20)
|
||||
if (row20) {
|
||||
const sv = Number(row20.total_surrender_value) || 0
|
||||
const multiple = totalInvest > 0 ? (sv / totalInvest).toFixed(2) : '-'
|
||||
metrics.push({ label: '20年倍数', value: `${multiple}x` })
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.length > 0) {
|
||||
metrics.push({ label: '数据行数', value: `${rows.length}行` })
|
||||
}
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
function formatNum(n: number) {
|
||||
return Number(n).toLocaleString('en-US')
|
||||
}
|
||||
|
||||
function hasField(rows: any[], field: string): boolean {
|
||||
if (!rows || !rows.length) return false
|
||||
return rows.some(r => r[field] !== undefined && r[field] !== null)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
await pptApi.updateExtractions(props.sessionId, extractions.value)
|
||||
originalJson.value = JSON.stringify(extractions.value)
|
||||
ElMessage.success('数据已保存')
|
||||
|
||||
// 重新验证
|
||||
const validateRes: any = await pptApi.validate(props.sessionId)
|
||||
issues.value = validateRes?.data?.issues || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error('保存失败: ' + (e?.message || '未知错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (isDirty.value) {
|
||||
// 有未保存的修改,先保存再跳转
|
||||
handleSave().then(() => emit('confirmed'))
|
||||
} else {
|
||||
emit('confirmed')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ppt-data-review {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.review-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-tag {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 60px 0;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 验证问题 */
|
||||
.issues-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background: #fdf6ec;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #faecd8;
|
||||
}
|
||||
|
||||
.issue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.issue-item.error {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.issue-item .issue-field {
|
||||
font-weight: 600;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.issue-item .issue-msg {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 产品块 */
|
||||
.extraction-block {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.extraction-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 指标卡 */
|
||||
.metrics-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* 基本信息表单 */
|
||||
.basic-form {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/* 底部操作 */
|
||||
.review-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
</style>
|
||||
@ -35,7 +35,7 @@
|
||||
>
|
||||
<template #extra>
|
||||
<el-button @click="$emit('back')">返回上传</el-button>
|
||||
<el-button v-if="hasSuccess" type="primary" @click="$emit('parsed')">下一步:生成 PPT</el-button>
|
||||
<el-button v-if="hasSuccess" type="primary" @click="$emit('parsed')">下一步:校验数据</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
|
||||
|
||||
@ -81,6 +81,11 @@ export const pptApi = {
|
||||
return api.get(`/ppt/validate/${sessionId}`)
|
||||
},
|
||||
|
||||
/** 保存用户修改后的提取数据 */
|
||||
updateExtractions(sessionId: string, extractions: any[]) {
|
||||
return api.put(`/ppt/session/${sessionId}/extractions`, { extractions })
|
||||
},
|
||||
|
||||
/** 获取渲染选项 */
|
||||
getRenderOptions() {
|
||||
return api.get('/ppt/render-options')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user