feat: 添加评估报告下载功能,包括下载页面、生成工具和模板,并更新了路由和权限守卫。

This commit is contained in:
Wei_佳 2025-11-26 12:25:49 +08:00
parent 4b945339d0
commit 5332324b10
9 changed files with 445 additions and 6 deletions

View File

@ -19,11 +19,15 @@
"@zclzone/eslint-config": "^0.0.4",
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"docxtemplater": "^3.67.5",
"dotenv": "^16.3.1",
"eslint": "^8.46.0",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"naive-ui": "^2.34.4",
"pinia": "^2.1.6",
"pizzip": "^3.2.0",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.65.1",
"typescript": "^5.1.6",

110
web/pnpm-lock.yaml generated
View File

@ -29,12 +29,21 @@ importers:
dayjs:
specifier: ^1.11.9
version: 1.11.12
docxtemplater:
specifier: ^3.67.5
version: 3.67.5
dotenv:
specifier: ^16.3.1
version: 16.4.5
eslint:
specifier: ^8.46.0
version: 8.57.0
file-saver:
specifier: ^2.0.5
version: 2.0.5
jszip:
specifier: ^3.10.1
version: 3.10.1
lodash-es:
specifier: ^4.17.21
version: 4.17.21
@ -44,6 +53,9 @@ importers:
pinia:
specifier: ^2.1.6
version: 2.2.0(typescript@5.5.4)(vue@3.4.34(typescript@5.5.4))
pizzip:
specifier: ^3.2.0
version: 3.2.0
rollup-plugin-visualizer:
specifier: ^5.9.2
version: 5.12.0(rollup@3.29.4)
@ -581,6 +593,10 @@ packages:
'@vueuse/shared@10.11.0':
resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==}
'@xmldom/xmldom@0.9.8':
resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==}
engines: {node: '>=14.6'}
'@zclzone/eslint-config@0.0.4':
resolution: {integrity: sha512-dDDHsLc0qEt/tczC1nRU5d+2LCOPwwKohw5Wlq4A1mTFgTQaFoSDmP/j9XnAbjCYfxbGUeEat0221WwwVbPhuw==}
@ -810,6 +826,9 @@ packages:
resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==}
engines: {node: '>=0.10.0'}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
@ -941,6 +960,10 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
docxtemplater@3.67.5:
resolution: {integrity: sha512-Jnh9rdMf5sDmrfONs3nVDhZwVFxLNdP3RVHkqLQYA6eZLkWA+kx5aYy0cVmEqJcVIaFYf5JJEFdClD7fn6ZUng==}
engines: {node: '>=0.10'}
dom-serializer@0.2.2:
resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
@ -1159,6 +1182,9 @@ packages:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
@ -1363,6 +1389,9 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immutable@4.3.7:
resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==}
@ -1575,6 +1604,9 @@ packages:
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@ -1601,6 +1633,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
loader-utils@1.4.2:
resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==}
engines: {node: '>=4.0.0'}
@ -1803,6 +1838,12 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
@ -1864,6 +1905,9 @@ packages:
typescript:
optional: true
pizzip@3.2.0:
resolution: {integrity: sha512-X4NPNICxCfIK8VYhF6wbksn81vTiziyLbvKuORVAmolvnUzl1A1xmz9DAWKxPRq9lZg84pJOOAMq3OE61bD8IQ==}
pkg-types@1.1.3:
resolution: {integrity: sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==}
@ -1922,6 +1966,9 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@ -1936,6 +1983,9 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@ -2018,6 +2068,9 @@ packages:
resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==}
engines: {node: '>=0.4'}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@ -2056,6 +2109,9 @@ packages:
resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
engines: {node: '>=0.10.0'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -2149,6 +2205,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@ -3067,6 +3126,8 @@ snapshots:
- '@vue/composition-api'
- vue
'@xmldom/xmldom@0.9.8': {}
'@zclzone/eslint-config@0.0.4':
dependencies:
eslint: 8.57.0
@ -3315,6 +3376,8 @@ snapshots:
copy-descriptor@0.1.1: {}
core-util-is@1.0.3: {}
cors@2.8.5:
dependencies:
object-assign: 4.1.1
@ -3442,6 +3505,10 @@ snapshots:
dependencies:
esutils: 2.0.3
docxtemplater@3.67.5:
dependencies:
'@xmldom/xmldom': 0.9.8
dom-serializer@0.2.2:
dependencies:
domelementtype: 2.3.0
@ -3779,6 +3846,8 @@ snapshots:
dependencies:
flat-cache: 3.2.0
file-saver@2.0.5: {}
filelist@1.0.4:
dependencies:
minimatch: 5.1.6
@ -3988,6 +4057,8 @@ snapshots:
image-size@0.5.5: {}
immediate@3.0.6: {}
immutable@4.3.7: {}
import-fresh@3.3.0:
@ -4176,6 +4247,13 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@ -4199,6 +4277,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lie@3.3.0:
dependencies:
immediate: 3.0.6
loader-utils@1.4.2:
dependencies:
big.js: 5.2.2
@ -4444,6 +4526,10 @@ snapshots:
dependencies:
p-limit: 3.1.0
pako@1.0.11: {}
pako@2.1.0: {}
param-case@3.0.4:
dependencies:
dot-case: 3.0.4
@ -4488,6 +4574,10 @@ snapshots:
optionalDependencies:
typescript: 5.5.4
pizzip@3.2.0:
dependencies:
pako: 2.1.0
pkg-types@1.1.3:
dependencies:
confbox: 0.1.7
@ -4551,6 +4641,8 @@ snapshots:
prettier@2.8.8: {}
process-nextick-args@2.0.1: {}
proxy-from-env@1.1.0: {}
punycode@2.3.1: {}
@ -4562,6 +4654,16 @@ snapshots:
queue-microtask@1.2.3: {}
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
@ -4636,6 +4738,8 @@ snapshots:
has-symbols: 1.0.3
isarray: 2.0.5
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
safe-regex-test@1.0.3:
@ -4683,6 +4787,8 @@ snapshots:
is-plain-object: 2.0.4
split-string: 3.1.0
setimmediate@1.0.5: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@ -4790,6 +4896,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.0.0
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1

Binary file not shown.

View File

@ -7,7 +7,7 @@ export function createAuthGuard(router) {
/** 没有token的情况 */
if (isNullOrWhitespace(token)) {
if (WHITE_LIST.includes(to.path)) return true
if (WHITE_LIST.includes(to.path) || to.path.startsWith('/report/download/')) return true
return { path: 'login', query: { ...to.query, redirect: to.path } }
}

View File

@ -107,6 +107,15 @@ export const basicRoutes = [
component: () => import('@/views/error-page/404.vue'),
isHidden: true,
},
{
name: 'ReportDownload',
path: '/report/download/:id',
component: () => import('@/views/report/download/index.vue'),
isHidden: true,
meta: {
title: '下载报告',
},
},
{
name: 'Login',
path: '/login',

131
web/src/utils/report.js Normal file
View File

@ -0,0 +1,131 @@
import PizZip from 'pizzip'
import Docxtemplater from 'docxtemplater'
import { saveAs } from 'file-saver'
import {
formatNumberValue,
formatThreeYearIncome,
formatAgeDistribution,
formatHistoricalEvidence,
formatPlatformAccounts,
formatPriceRange
} from '@/views/valuation/audit/utils'
export const generateReport = async (detailData) => {
try {
// Load the template
const response = await fetch('/report_template.docx')
if (!response.ok) {
throw new Error('Failed to load report template')
}
const content = await response.arrayBuffer()
const zip = new PizZip(content)
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
delimiters: { start: '[', end: ']' },
})
// Helper to get safe number
const getNum = (val) => (val || val === 0 ? Number(val) : '-')
const getWan = (val) => (val || val === 0 ? formatNumberValue(val) : '-')
// Prepare data
const data = {
...detailData,
// Summary
B: getWan(detailData.model_value_b),
B1: getWan(detailData.economic_value_b1),
B2: getWan(detailData.cultural_value_b2),
B3: getNum(detailData.risk_coefficient_b3),
C: getWan(detailData.market_value_c),
DPR: getNum(detailData.pledge_rate),
// Basic Info
asset_name: detailData.asset_name || '-',
institution: detailData.institution || '-',
credit_code: detailData.credit_code || detailData.credit_code_or_id || '-',
industry: detailData.industry || '-',
business_heritage_intro: detailData.business_heritage_intro || detailData.biz_intro || '-',
// Finance
annual_revenue: getWan(detailData.annual_revenue),
rd_investment: getWan(detailData.rd_investment),
three_year_income: Array.isArray(detailData.three_year_income) ? detailData.three_year_income.join(',') : '-',
funding_status: detailData.funding_status || '-',
// Tech / Non-heritage attributes
heritage_level: detailData.heritage_level || detailData.heritage_asset_level || '-',
inheritor_age_count_50: detailData.inheritor_age_count?.[0] || 0,
inheritor_age_count_50_70: detailData.inheritor_age_count?.[1] || 0,
inheritor_age_count_70: detailData.inheritor_age_count?.[2] || 0,
historical_evidence_artifacts: detailData.historical_evidence?.artifacts || 0,
historical_evidence_literature: detailData.historical_evidence?.ancient_literature || 0,
historical_evidence_testimony: detailData.historical_evidence?.inheritor_testimony || 0,
historical_evidence_research: detailData.historical_evidence?.modern_research || 0,
offline_activities: getNum(detailData.offline_activities ?? detailData.offline_teaching_count),
online_clicks: getWan(detailData.online_clicks), // Assuming this field exists or needs mapping
// Market
platform_accounts_bilibili: detailData.platform_accounts?.bilibili?.account || '-',
platform_accounts_douyin: detailData.platform_accounts?.douyin?.account || '-',
price_fluctuation_min: detailData.price_fluctuation?.[0] || '-',
price_fluctuation_max: detailData.price_fluctuation?.[1] || '-',
monthly_transaction: detailData.monthly_transaction || detailData.monthly_transaction_amount || '-',
circulation: detailData.circulation || detailData.scarcity_level || '-',
// Detailed Parameters (Assuming these keys exist in detailData or calculation_result)
// Economic Value B1
B11: getWan(detailData.basic_value_b11),
F: getWan(detailData.financial_value_p),
L: getNum(detailData.legal_strength_l),
D: getNum(detailData.development_potential_d),
B12: getNum(detailData.traffic_factor_b12),
search_index_ratio: getNum(detailData.search_index_ratio),
S3: getNum(detailData.social_media_spread_s3),
B13: getNum(detailData.policy_multiplier_b13),
policy_fit_score: getNum(detailData.policy_fit_score),
// Cultural Value B2
B21: getNum(detailData.living_inheritance_b21),
inheritor_level_score: getNum(detailData.inheritor_level_score),
teaching_frequency_score: getNum(detailData.teaching_frequency_score),
cooperation_depth_score: getNum(detailData.cooperation_depth_score),
B22: getNum(detailData.pattern_entropy_b22),
SC: getNum(detailData.structure_complexity_sc),
H: getNum(detailData.info_entropy_h),
HI: getNum(detailData.history_inheritance_hi),
// Risk
risk_market: getNum(detailData.risk_market),
risk_legal: getNum(detailData.risk_legal),
risk_inheritance: getNum(detailData.risk_inheritance),
// Market Verification
C1: getWan(detailData.market_bidding_c1),
C2: getNum(detailData.heat_coefficient_c2),
C3: getNum(detailData.scarcity_multiplier_c3),
C4: getNum(detailData.time_decay_c4),
// Current Date
current_date: new Date().toLocaleDateString('zh-CN'),
}
doc.render(data)
const out = doc.getZip().generate({
type: 'blob',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
})
saveAs(out, `${detailData.asset_name || '评估报告'}.docx`)
} catch (error) {
console.error('Report generation failed:', error)
throw error
}
}

View File

@ -0,0 +1,178 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { NCard, NButton, NResult, NSpin, useMessage } from 'naive-ui'
import api from '@/api'
import { generateReport } from '@/utils/report'
import TheIcon from '@/components/icon/TheIcon.vue'
const route = useRoute()
const message = useMessage()
const loading = ref(false)
const error = ref(null)
const detailData = ref(null)
const downloaded = ref(false)
const fetchDetail = async () => {
const id = route.params.id || route.query.id
if (!id) {
error.value = '参数错误缺少ID'
return
}
loading.value = true
try {
const { data } = await api.getValuationById({ valuation_id: Number(id) })
detailData.value = data
} catch (err) {
error.value = err.message || '获取数据失败'
} finally {
loading.value = false
}
}
const handleDownload = async () => {
if (!detailData.value) return
try {
loading.value = true
await generateReport(detailData.value)
message.success('下载成功')
downloaded.value = true
} catch (err) {
console.error(err)
message.error('下载失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDetail()
})
</script>
<template>
<div class="download-page">
<div class="content-wrapper">
<NSpin :show="loading">
<NCard v-if="!error && detailData" class="download-card">
<template #header>
<div class="card-header">
<TheIcon icon="mdi:file-document-outline" :size="32" color="#18a058" />
<span>评估报告下载</span>
</div>
</template>
<div class="info-section">
<div class="info-item">
<span class="label">资产名称</span>
<span class="value">{{ detailData.asset_name }}</span>
</div>
<div class="info-item">
<span class="label">评估结果</span>
<span class="value">¥{{ detailData.final_value_ab?.toLocaleString() || '-' }}</span>
</div>
<div class="info-item">
<span class="label">生成时间</span>
<span class="value">{{ new Date().toLocaleDateString() }}</span>
</div>
</div>
<div class="action-section">
<NButton
type="primary"
size="large"
:loading="loading"
@click="handleDownload"
class="download-btn"
>
<template #icon>
<TheIcon icon="mdi:download" />
</template>
下载 Word 报告
</NButton>
</div>
</NCard>
<NResult
v-else-if="error"
status="error"
title="无法下载"
:description="error"
>
<template #footer>
<NButton @click="fetchDetail">重试</NButton>
</template>
</NResult>
</NSpin>
</div>
</div>
</template>
<style scoped>
.download-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f2f5;
padding: 20px;
}
.content-wrapper {
width: 100%;
max-width: 480px;
}
.download-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
}
.info-section {
margin: 24px 0;
padding: 16px;
background-color: #f9fafb;
border-radius: 8px;
}
.info-item {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 14px;
}
.info-item:last-child {
margin-bottom: 0;
}
.label {
color: #6b7280;
}
.value {
font-weight: 500;
color: #111827;
}
.action-section {
display: flex;
justify-content: center;
margin-top: 24px;
}
.download-btn {
width: 100%;
}
</style>

View File

@ -327,7 +327,8 @@ const handleViewCertificate = () => {
certificateData.value = {
reportFiles: formatFiles(props.detailData?.report_url),
certificateFiles: formatFiles(props.detailData?.certificate_url)
certificateFiles: formatFiles(props.detailData?.certificate_url),
detailData: props.detailData
}
certificateModalVisible.value = true
}

View File

@ -14,6 +14,7 @@ import {
// import { DownloadIcon } from '@vicons/tabler'
import { getToken } from '@/utils/auth/token'
import { generateReport } from '@/utils/report'
const props = defineProps({
visible: {
@ -162,10 +163,15 @@ const isUploadMode = computed(() => props.mode === 'upload')
//
const handleDownloadReport = () => {
//
console.log('下载原版报告')
// TODO:
const handleDownloadReport = async () => {
try {
message.loading('正在生成报告...')
await generateReport(props.certificateData.detailData)
message.success('报告生成并下载成功')
} catch (error) {
console.error(error)
message.error('报告生成失败')
}
}
//