722 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { computed, ref, watch, h } from 'vue'
import {
NButton,
NTag,
NTabs,
NTabPane,
NSpin,
NImage,
NImageGroup,
NDataTable,
} from 'naive-ui'
import { formatDate } from '@/utils'
import TheIcon from '@/components/icon/TheIcon.vue'
import CertificateModal from './CertificateModal.vue'
import api from '@/api'
import { useMessage } from 'naive-ui'
import { getStatusConfig } from '../constants'
import {
formatAgeDistribution,
formatAmount,
formatHistoricalEvidence,
formatPercent,
formatPlatformAccounts,
formatPriceRange,
formatThreeYearIncome,
formatNumberValue,
} from '../utils'
const props = defineProps({
loading: { type: Boolean, default: false },
detailData: { type: Object, default: null },
mode: { type: String, default: 'view' },
})
const emit = defineEmits(['back', 'approve', 'reject'])
const $message = useMessage()
const activeDetailTab = ref('audit')
// 证书弹窗相关状态
const certificateModalVisible = ref(false)
const certificateModalMode = ref('upload') // 'upload' 或 'view'
const certificateData = ref({})
watch(
() => props.detailData?.id,
() => {
activeDetailTab.value = 'audit'
}
)
const detailSections = computed(() => {
const detail = props.detailData
if (!detail) return []
const sections = [
{
key: 'basic',
title: '基础信息',
fields: [
{ label: '资产名称', type: 'text', value: detail.asset_name || '-' },
{ label: '所属机构/权利人', type: 'text', value: detail.institution || '-' },
{ label: '统一社会信用代码/身份证号', type: 'text', value: detail.credit_code || detail.credit_code_or_id || '-' },
{ label: '所属行业', type: 'text', value: detail.industry || '-' },
{ label: '业务/传承介绍', type: 'text', value: detail.business_heritage_intro || detail.biz_intro || '-' },
],
},
{
key: 'finance',
title: '财务状况',
fields: [
{ label: '近12个月机构营收/万元', type: 'text', value: formatNumberValue(detail.annual_revenue) },
{ label: '近12个月机构研发投入/万元', type: 'text', value: formatNumberValue(detail.rd_investment) },
{ label: '近三年机构收益/万元', type: 'text', value: formatThreeYearIncome(detail.three_year_income) },
// { label: '近三年机构收益/万元', type: 'list', value: formatThreeYearIncome(detail.three_year_income) },
{ label: '资产受资助情况', type: 'text', value: detail.funding_status || '-' },
],
},
{
key: 'tech',
title: '非遗等级与技术',
fields: [
{ label: '非遗传承人等级', type: 'text', value: detail.inheritor_level || '-' },
{
label: '非遗传承人年龄水平及数量',
type: 'text',
// type: 'list',
value: formatAgeDistribution(detail.inheritor_age_count || detail.inheritor_ages),
},
{ label: '非遗传承人等级证书', type: 'images', value: detail.inheritor_certificates || [] },
{ label: '非遗等级', type: 'text', value: detail.heritage_level || detail.heritage_asset_level || '-' },
{ label: '非遗资产所用专利的申请号', type: 'text', value: detail.patent_application_no || '-' },
{ label: '非遗资产历史证明证据及数量', type: 'text', value: formatHistoricalEvidence(detail.historical_evidence) },
// { label: '非遗资产历史证明证据及数量', type: 'list', value: formatHistoricalEvidence(detail.historical_evidence) },
{
label: '非遗资产所用专利/纹样图片',
type: 'images',
value: [...(detail.patent_certificates || []), ...(detail.pattern_images || [])],
},
],
},
{
key: 'promotion',
title: '非遗应用与推广',
fields: [
{ label: '非遗资产应用成熟度', type: 'text', value: detail.application_maturity || detail.implementation_stage || '-' },
{ label: '非遗资产应用覆盖范围', type: 'text', value: detail.application_coverage || detail.coverage_area || '-' },
{ label: '非遗资产跨界合作深度', type: 'text', value: detail.cooperation_depth || detail.collaboration_type || '-' },
{
label: '近12个月线下相关宣讲活动次数',
type: 'text',
value: formatNumberValue(detail.offline_activities ?? detail.offline_teaching_count),
},
{ label: '线上相关宣传账号信息', type: 'text', value: formatPlatformAccounts(detail.platform_accounts) },
// { label: '线上相关宣传账号信息', type: 'list', value: formatPlatformAccounts(detail.platform_accounts) },
],
},
{
key: 'products',
title: '非遗资产衍生商品信息',
fields: [
{ label: '代表产品近12个月销售数量', type: 'text', value: formatNumberValue(detail.sales_volume) },
{ label: '商品链接浏览量', type: 'text', value: formatNumberValue(detail.link_views) },
{ label: '发行量', type: 'text', value: detail.circulation || detail.scarcity_level || '-' },
{ label: '最近一次市场活动时间', type: 'text', value: detail.last_market_activity || detail.market_activity_time || '-' },
{ label: '月交易额水平', type: 'text', value: detail.monthly_transaction || detail.monthly_transaction_amount || '-' },
{ label: '近30天价格区间', type: 'text', value: formatPriceRange(detail.price_fluctuation) },
],
},
]
// 为每个 section 生成 NDataTable 需要的 columns 和 data
return sections.map(section => {
const columns = [
{
title: '字段名',
key: 'fieldName',
width: 120,
align: 'center',
fixed: 'left',
},
...section.fields.map(field => ({
title: field.label,
key: field.label,
width: 200,
ellipsis: {
tooltip: true,
},
render: (row) => {
const fieldData = row[field.label]
if (!fieldData) return '-'
if (fieldData.type === 'list') {
if (fieldData.value && fieldData.value.length) {
return h('div', { class: 'cell-multi' },
fieldData.value.map(item => h('span', item))
)
}
return '-'
} else if (fieldData.type === 'images') {
if (fieldData.value && fieldData.value.length) {
return h(NImageGroup, {}, () =>
fieldData.value.map(img =>
h(NImage, {
src: img,
width: 72,
height: 48,
objectFit: 'cover',
style: 'margin-right: 8px;'
})
)
)
}
return '-'
} else {
return fieldData.value || '-'
}
}
})),
]
const data = [
{
fieldName: '用户输入',
...section.fields.reduce((acc, field) => {
acc[field.label] = field
return acc
}, {}),
},
]
return {
...section,
columns,
data,
}
})
})
const calcFlow = computed(() => props.detailData?.calculation_result?.flow || [])
const mockFlowHtml = ref(`
<div class="calc-flow-container">
<!-- 左侧:详细计算流程 -->
<div class="calc-flow-left">
<div class="calc-formula-header">
最终估值A12000=模型估值B12*0.7+市场估值C33*0.3
</div>
<div class="calc-section">
<div class="calc-section-title">一、模型估值B12=经济价值B143*0.7+文化价值B255*0.3*风险调整系数B343</div>
<div class="calc-subsection">
<div class="calc-subsection-title">1、经济价值B143=基础价值B1173*1+流量因子B1212*政策驱动B1345</div>
<div class="calc-item">
<div class="calc-item-label">(1) 基础价值B1173=财务价值P2000*0.45+0.05*行业系数I0.8+法律强度L0.3*0.35+0.05*行业系数I0.8+发展潜力D0.5*0.2</div>
<div class="calc-detail">
<div class="calc-detail-item">• 财务价值P2000=[3年内均收益10000*1+增长率23%*5]/5</div>
<div class="calc-detail-item">• 法律强度L12=专利分*12*0.4+商标分*12*0.3+版权分*12*0.3</div>
<div class="calc-detail-item">• 发展潜力D=专利分*0.5+ESG分*0.2+创新投入比*0.3</div>
</div>
</div>
<div class="calc-item">
<div class="calc-item-label">(2) 流量因子B1212</div>
<div class="calc-detail">
<div class="calc-detail-item">• 近 30 天搜索指数M1</div>
<div class="calc-detail-item">• 行业均值S2</div>
<div class="calc-detail-item">• 社交媒体传播度S3</div>
</div>
</div>
<div class="calc-item">
<div class="calc-item-label">(3) 政策驱动B13</div>
<div class="calc-detail">
<div class="calc-detail-item">• 政策契合度P</div>
</div>
</div>
</div>
<div class="calc-subsection">
<div class="calc-subsection-title">2、文化价值B2</div>
<div class="calc-item">
<div class="calc-item-label">(1) 活态传承系数B21</div>
<div class="calc-detail">
<div class="calc-detail-item">• 传承人等级系数</div>
<div class="calc-detail-item">• 教学传播频次</div>
<div class="calc-detail-item">• 跨界合作深度</div>
</div>
</div>
<div class="calc-item">
<div class="calc-item-label">(2) 纹样基因值B22</div>
<div class="calc-detail">
<div class="calc-detail-item">• 纹样复杂度SC</div>
<div class="calc-detail-item">• 归一化稀缺H</div>
<div class="calc-detail-item">• 历史承载度HI</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧:计算流程大纲 -->
<div class="calc-flow-right">
<div class="calc-outline-title">最终估值A</div>
<div class="calc-outline">
<div class="outline-section">
<div class="outline-title">一、模型估值B</div>
<div class="outline-subsection">
<div class="outline-subtitle">1、经济价值B1</div>
<div class="outline-item">(1) 基础价值B11</div>
<div class="outline-detail">• 基础价值B1173=财务价值P2000*0.45+0.05*行业系数I0.8+法律强度L0.3*0.35+0.05*行业系数I0.8+发展潜力D0.5*0.2</div>
<div class="outline-item">(2) 流量因子B12</div>
<div class="outline-detail">• 近 30 天搜索指数M1</div>
<div class="outline-detail">• 行业均值S2</div>
<div class="outline-detail">• 社交媒体传播度S3</div>
<div class="outline-item">(3) 政策驱动B13</div>
<div class="outline-detail">• 政策契合度P</div>
</div>
<div class="outline-subsection">
<div class="outline-subtitle">2、文化价值B2</div>
<div class="outline-item">(1) 活态传承系数B21</div>
<div class="outline-detail">• 传承人等级系数</div>
<div class="outline-detail">• 教学传播频次</div>
<div class="outline-detail">• 跨界合作深度</div>
<div class="outline-item">(2) 纹样基因值B22</div>
<div class="outline-detail">• 纹样复杂度SC</div>
<div class="outline-detail">• 归一化稀缺H</div>
<div class="outline-detail">• 历史承载度HI</div>
</div>
</div>
</div>
</div>
</div>
`)
// 证书相关功能
const handleUploadCertificate = () => {
certificateModalMode.value = 'upload'
certificateData.value = {}
certificateModalVisible.value = true
}
const handleViewCertificate = () => {
certificateModalMode.value = 'view'
const formatFiles = (urlData) => {
if (!urlData) return []
// Handle string (single or comma-separated)
const urls = typeof urlData === 'string' ? urlData.split(',') : (Array.isArray(urlData) ? urlData : [])
return urls.filter(u => u).map((url, index) => ({
id: String(index),
name: url.substring(url.lastIndexOf('/') + 1) || 'unknown',
status: 'finished',
url: url
}))
}
certificateData.value = {
reportFiles: formatFiles(props.detailData?.report_url),
certificateFiles: formatFiles(props.detailData?.certificate_url)
}
certificateModalVisible.value = true
}
const handleCertificateConfirm = async (data) => {
console.log('证书数据:', data)
try {
const certificateUrl = data.certificateFiles?.map(f => f.url).filter(Boolean) || []
const reportUrl = data.reportFiles?.map(f => f.url).filter(Boolean) || []
// 现在改为只能上传 1 张
const payload = {
...props.detailData,
certificate_url: certificateUrl?.[0],
report_url: reportUrl?.[0],
status: 'success'
}
await api.updateValuation(payload)
$message.success('上传并通知成功')
certificateModalVisible.value = false
emit('back') // 或者 emit('refresh') 取决于需求,这里假设返回列表或刷新
} catch (error) {
console.error('更新失败:', error)
$message.error('操作失败')
}
}
</script>
<template>
<div class="audit-detail">
<div class="detail-header">
<div>
<button type="button" class="back-btn" @click="emit('back')">
<TheIcon icon="mdi:arrow-left" :size="16" class="mr-4" />
返回审核列表
</button>
<!-- <div class="detail-title">
<h2>{{ detailData?.asset_name || '审核详情' }}</h2>
<NTag size="small" :type="getStatusConfig(detailData?.status).type">
{{ getStatusConfig(detailData?.status).text }}
</NTag>
</div>
<p class="detail-meta">
<span>手机号{{ detailData?.phone || '-' }}</span>
<span>微信号{{ detailData?.wechat || '-' }}</span>
<span>提交时间{{ formatDate(detailData?.created_at) }}</span>
<span>审核时间{{ detailData?.reviewed_at ? formatDate(detailData?.reviewed_at) : '-' }}</span>
</p> -->
</div>
</div>
<NTabs
v-model:value="activeDetailTab"
type="line"
size="large"
class="audit-tabs"
>
<NTabPane name="audit" tab="审核信息">
<NSpin :show="loading">
<div v-for="section in detailSections" :key="section.key" class="detail-section">
<div class="section-title">
<span class="dot" />
<span>{{ section.title }}</span>
</div>
<NDataTable
:columns="section.columns"
:data="section.data"
:bordered="true"
:single-line="false"
:scroll-x="section.fields.length * 200 + 120"
>
<template #empty>
<span>暂无数据</span>
</template>
</NDataTable>
</div>
</NSpin>
</NTabPane>
<NTabPane name="flow" tab="计算流程">
<NSpin :show="loading">
<div v-html="mockFlowHtml"></div>
</NSpin>
</NTabPane>
</NTabs>
<!-- 证书按钮 -->
<div class="certificate-actions">
<NButton
v-if="mode === 'approve'"
type="primary"
@click="handleUploadCertificate"
>
<TheIcon icon="mdi:upload" :size="16" class="mr-4" />
上传证书
</NButton>
<NButton
v-else
type="info"
@click="handleViewCertificate"
>
<TheIcon icon="mdi:eye" :size="16" class="mr-4" />
查看证书
</NButton>
</div>
<!-- 证书弹窗 -->
<CertificateModal
v-model:visible="certificateModalVisible"
:mode="certificateModalMode"
:certificate-data="certificateData"
@confirm="handleCertificateConfirm"
/>
</div>
</template>
<style scoped>
.audit-detail {
background: #fff;
border-radius: 12px;
padding: 24px;
.certificate-actions {
margin-top: 20px;
width: 100%;
display: flex;
justify-content: flex-end;
z-index: 100;
}
}
.detail-header {
display: flex;
justify-content: space-between;
gap: 16px;
margin: -16px 0px 10px ;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0;
border: none;
background: transparent;
color: #409eff;
cursor: pointer;
}
.detail-title {
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0;
}
.detail-title h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.detail-meta {
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 16px;
color: #666;
font-size: 13px;
}
.detail-actions {
display: flex;
gap: 12px;
align-items: center;
}
.detail-section {
margin-bottom: 24px;
}
.audit-tabs :deep(.n-tabs-nav) {
margin-bottom: 16px;
}
.audit-tabs :deep(.n-tabs-tab) {
font-size: 16px;
font-weight: 500;
color: #1d2129;
padding: 0 24px;
}
.audit-tabs :deep(.n-tabs-tab.n-tabs-tab--active) {
color: #ff6f3b;
}
.audit-tabs :deep(.n-tabs-nav__line) {
background-color: #ff6f3b;
height: 3px;
border-radius: 999px;
}
.section-title {
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
margin-bottom: 12px;
font-size: 18px;
color: #1d2129;
}
.section-title .dot {
width: 10px;
height: 18px;
border-radius: 4px;
background: #3b82f6;
}
.detail-section :deep(.n-data-table) {
background: #f9fafe;
}
.detail-section :deep(.n-data-table-th) {
background: #f1f2f5;
font-weight: 600;
}
.detail-section :deep(.n-data-table-th:first-child) {
background: #f1f2f5;
text-align: center;
}
.detail-section :deep(.n-data-table-td:first-child) {
background: #f1f2f5;
text-align: center;
font-weight: 600;
}
.cell-multi {
display: flex;
flex-direction: column;
gap: 4px;
}
/* 计算流程容器 */
:deep(.calc-flow-container) {
display: flex;
gap: 24px;
min-height: 600px;
}
/* 左侧详细流程 */
:deep(.calc-flow-left) {
flex: 1;
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
overflow-y: auto;
max-height: 800px;
}
:deep(.calc-formula-header) {
font-size: 18px;
font-weight: 600;
color: #1d2129;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #e5e7eb;
}
:deep(.calc-section) {
margin-bottom: 24px;
}
:deep(.calc-section-title) {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 16px;
line-height: 1.6;
}
:deep(.calc-subsection) {
margin-left: 20px;
margin-bottom: 20px;
}
:deep(.calc-subsection-title) {
font-size: 15px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
line-height: 1.6;
}
:deep(.calc-item) {
margin-left: 20px;
margin-bottom: 16px;
}
:deep(.calc-item-label) {
font-size: 14px;
color: #4b5563;
margin-bottom: 8px;
line-height: 1.6;
}
:deep(.calc-detail) {
margin-left: 20px;
margin-top: 8px;
}
:deep(.calc-detail-item) {
font-size: 13px;
color: #6b7280;
margin-bottom: 4px;
line-height: 1.6;
}
/* 右侧大纲 */
:deep(.calc-flow-right) {
width: 320px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px;
overflow-y: auto;
max-height: 800px;
}
:deep(.calc-outline-title) {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #e5e7eb;
}
:deep(.calc-outline) {
font-size: 13px;
}
:deep(.outline-section) {
margin-bottom: 16px;
}
:deep(.outline-title) {
font-size: 14px;
font-weight: 600;
color: #1d2129;
margin-bottom: 8px;
}
:deep(.outline-subsection) {
margin-left: 12px;
margin-bottom: 12px;
}
:deep(.outline-subtitle) {
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
}
:deep(.outline-item) {
font-size: 12px;
color: #4b5563;
margin-bottom: 4px;
margin-left: 12px;
}
:deep(.outline-detail) {
font-size: 11px;
color: #6b7280;
margin-left: 24px;
margin-bottom: 2px;
line-height: 1.5;
}
.calc-empty {
text-align: center;
color: #999;
padding: 40px 0;
}
</style>