refactor: 重构审核列表页面,优化详情页面和弹窗交互

This commit is contained in:
Wei_佳 2025-11-13 18:47:53 +08:00
parent ad4bd8145c
commit b63306890d
5 changed files with 1011 additions and 389 deletions

View File

@ -1,5 +1,281 @@
import { request } from '@/utils' import { request } from '@/utils'
const baseValuationDetail = {
valuation_result: 1180000,
created_at: '2024-11-10T09:30:00Z',
reviewed_at: null,
status: 'pending',
admin_notes: null,
asset_name: '蜀绣传承精品',
institution: '天府非遗文化发展有限公司',
industry: '文化创意',
annual_revenue: 980000,
rd_investment: 165000,
three_year_income: [890000, 975000, 1180000],
funding_status: '国家资助',
inheritor_level: '市级传承人',
inheritor_age_count: [4, 6, 2],
inheritor_certificates: [
'https://dummyimage.com/120x80/edf2ff/409eff&text=证书A1',
'https://dummyimage.com/120x80/fef6f0/f0a020&text=证书A2',
],
heritage_level: '国家级非遗',
heritage_asset_level: '一级保护',
patent_application_no: '1111111,2222222',
historical_evidence: {
artifacts: 1,
ancient_literature: 2,
inheritor_testimony: 0,
modern_research: 3,
},
patent_certificates: ['https://dummyimage.com/120x80/e8f5e9/34a853&text=专利1'],
pattern_images: ['https://dummyimage.com/120x80/f3e8ff/9c27b0&text=纹样1'],
application_maturity: '推广阶段',
application_coverage: '全国覆盖',
cooperation_depth: '品牌联名',
offline_activities: 4,
platform_accounts: {
bilibili: { account: 'B站@蜀绣', likes: 1260, comments: 320, shares: 188 },
},
sales_volume: 5200,
link_views: 86500,
circulation: '500-1000份',
last_market_activity: '近3个月',
monthly_transaction: '50-100万元',
price_fluctuation: [1200, 3200],
model_value_b: 1250000,
market_value_c: 1180000,
final_value_ab: 1220000,
dynamic_pledge_rate: 0.62,
calculation_result: {
flow: [
{
title: '基础估值',
description: '基于近三年收益与研发投入的模型估值',
value: '¥1,250,000',
},
{
title: '市场对标',
description: '结合同类资产市场成交价修正',
value: '¥1,180,000',
},
{
title: '综合校准',
description: '叠加ESG、政策匹配度得出最终估值',
value: '¥1,220,000',
},
],
},
}
const valuationRecords = [
{
id: 2001,
phone: '13800138001',
wechat: 'zhangsan_wx',
},
{
id: 2002,
phone: '13800138002',
wechat: 'lisi2024',
valuation_result: 880000,
created_at: '2024-11-09T14:20:00Z',
reviewed_at: '2024-11-09T16:45:00Z',
status: 'approved',
admin_notes: '评估结果合理,已通过审核',
asset_name: '景泰蓝掐丝珐琅',
institution: '京华非遗研究院',
application_maturity: '成熟期',
application_coverage: '华北地区',
cooperation_depth: '科技载体',
platform_accounts: {
douyin: { account: '抖音@景泰蓝工坊', likes: 2350, comments: 610, shares: 302 },
},
price_fluctuation: [980, 2680],
calculation_result: {
flow: [
{
title: '基础估值',
description: '模型估算品牌溢价后得出结果',
value: '¥900,000',
},
{
title: '市场对标',
description: '对比近六个月文博拍卖价格',
value: '¥860,000',
},
{
title: '综合校准',
description: '结合政策扶持与线上声量校准',
value: '¥880,000',
},
],
},
},
{
id: 2003,
phone: '13800138003',
wechat: 'wangwu_user',
valuation_result: 2100000,
created_at: '2024-11-08T16:45:00Z',
reviewed_at: '2024-11-08T18:30:00Z',
status: 'approved',
admin_notes: '评估价格偏高,但审核通过',
asset_name: '苗绣银饰',
institution: '黔锦民族文化有限公司',
industry: '民族工艺',
funding_status: '地方配套资金',
inheritor_level: '国家级代表性传承人',
inheritor_age_count: [2, 3, 1],
application_coverage: '西南片区',
platform_accounts: {
kuaishou: { account: '快手@苗绣手作', likes: 1800, comments: 420, shares: 210 },
},
price_fluctuation: [2600, 5200],
},
{
id: 2004,
phone: '13800138004',
wechat: 'zhaoliu_vip',
valuation_result: 560000,
created_at: '2024-11-07T11:15:00Z',
status: 'pending',
asset_name: '景德镇青花',
institution: '景尚文化传播有限公司',
industry: '陶瓷制造',
funding_status: '社会资本',
platform_accounts: {
bilibili: { account: 'B站@青花研习社', likes: 860, comments: 146, shares: 98 },
},
price_fluctuation: [560, 1200],
},
{
id: 2005,
phone: '13800138005',
wechat: 'sunqi888',
valuation_result: 1680000,
created_at: '2024-11-06T08:30:00Z',
reviewed_at: '2024-11-06T10:15:00Z',
status: 'approved',
admin_notes: '评估数据完整,审核通过',
asset_name: '藏医药香丸',
institution: '高原本草研究中心',
industry: '中医药',
application_coverage: '西藏及周边',
cooperation_depth: '国家外交礼品',
platform_accounts: {
douyin: { account: '抖音@藏医手作', likes: 3120, comments: 815, shares: 356 },
},
price_fluctuation: [3200, 7600],
},
{
id: 2006,
phone: '13800138006',
wechat: 'zhouba2024',
valuation_result: 950000,
created_at: '2024-11-05T13:20:00Z',
status: 'pending',
asset_name: '苏绣屏风',
institution: '苏澜绣坊',
funding_status: '企业自筹',
platform_accounts: {
bilibili: { account: 'B站@苏绣博物馆', likes: 980, comments: 240, shares: 130 },
},
price_fluctuation: [1500, 3600],
},
{
id: 2007,
phone: '13800138007',
wechat: 'wujiu_user',
valuation_result: 3200000,
created_at: '2024-11-04T15:45:00Z',
reviewed_at: '2024-11-04T17:20:00Z',
status: 'approved',
admin_notes: '高价值资产,评估结果准确',
asset_name: '宋锦织造',
institution: '苏州织造研究所',
funding_status: '国家重点补贴',
inheritor_age_count: [6, 8, 4],
application_maturity: '成熟期',
cooperation_depth: '科技载体',
platform_accounts: {
douyin: { account: '抖音@宋锦织造', likes: 4800, comments: 1020, shares: 520 },
},
},
{
id: 2008,
phone: '13800138008',
wechat: 'zhengshi_vip',
valuation_result: 750000,
created_at: '2024-11-03T10:10:00Z',
reviewed_at: '2024-11-03T12:00:00Z',
status: 'approved',
admin_notes: '评估流程规范,结果可信',
asset_name: '黄梅挑花',
institution: '徽楚非遗中心',
cooperation_depth: '品牌联名',
price_fluctuation: [980, 1800],
},
{
id: 2009,
phone: '13800138009',
wechat: 'chenjun2024',
valuation_result: 1890000,
created_at: '2024-11-02T14:30:00Z',
status: 'pending',
asset_name: '黎锦织造',
institution: '海南黎锦工坊',
funding_status: '国家资助',
application_coverage: '华南地区',
},
{
id: 2010,
phone: '13800138010',
wechat: 'liuxia_user',
valuation_result: 430000,
created_at: '2024-11-01T11:45:00Z',
reviewed_at: '2024-11-01T13:30:00Z',
status: 'approved',
admin_notes: '低价值资产,评估合理',
asset_name: '大漆工艺',
institution: '榫卯器物社',
funding_status: '地方专项',
cooperation_depth: '品牌联名',
platform_accounts: {
bilibili: { account: 'B站@大漆工坊', likes: 420, comments: 75, shares: 33 },
},
},
{
id: 2011,
phone: '13800138011',
wechat: 'zhaolei2024',
valuation_result: 2100000,
created_at: '2024-10-31T09:20:00Z',
reviewed_at: '2024-10-31T11:00:00Z',
status: 'approved',
admin_notes: '评估报告详细,数据支撑充分',
asset_name: '龙泉青瓷',
institution: '浙瓷非遗研究院',
cooperation_depth: '科技载体',
},
{
id: 2012,
phone: '13800138012',
wechat: 'sunmei_vip',
valuation_result: 680000,
created_at: '2024-10-30T16:15:00Z',
status: 'pending',
asset_name: '侗锦织造',
institution: '黔东南侗锦合作社',
funding_status: '社会资本',
},
]
const mockValuationDetails = valuationRecords.map((record) => ({
...baseValuationDetail,
...record,
}))
export default { export default {
login: (data) => request.post('/base/access_token', data, { noNeedToken: true }), login: (data) => request.post('/base/access_token', data, { noNeedToken: true }),
getUserInfo: () => request.get('/base/userinfo'), getUserInfo: () => request.get('/base/userinfo'),
@ -127,8 +403,8 @@ export default {
} }
// 分页处理 // 分页处理
const page = params.page || 1 const page = Number(params.page) || 1
const pageSize = params.page_size || 10 const pageSize = Number(params.page_size) || 10
const startIndex = (page - 1) * pageSize const startIndex = (page - 1) * pageSize
const endIndex = startIndex + pageSize const endIndex = startIndex + pageSize
const paginatedUsers = filteredUsers.slice(startIndex, endIndex) const paginatedUsers = filteredUsers.slice(startIndex, endIndex)
@ -374,132 +650,8 @@ export default {
sendInvoice: (data = {}) => request.post('/invoice/send', data), sendInvoice: (data = {}) => request.post('/invoice/send', data),
// valuation (估值评估) // valuation (估值评估)
getValuationList: (params = {}) => { getValuationList: (params = {}) => {
// Mock 数据
const mockValuations = [
{
id: 2001,
phone: '13800138001',
wechat: 'zhangsan_wx',
valuation_result: 1250000.00,
created_at: '2024-11-10T09:30:00Z',
reviewed_at: null,
status: 'pending',
admin_notes: null
},
{
id: 2002,
phone: '13800138002',
wechat: 'lisi2024',
valuation_result: 880000.00,
created_at: '2024-11-09T14:20:00Z',
reviewed_at: '2024-11-09T16:45:00Z',
status: 'approved',
admin_notes: '评估结果合理,已通过审核'
},
{
id: 2003,
phone: '13800138003',
wechat: 'wangwu_user',
valuation_result: 2100000.00,
created_at: '2024-11-08T16:45:00Z',
reviewed_at: '2024-11-08T18:30:00Z',
status: 'approved',
admin_notes: '评估价格偏高,但审核通过'
},
{
id: 2004,
phone: '13800138004',
wechat: 'zhaoliu_vip',
valuation_result: 560000.00,
created_at: '2024-11-07T11:15:00Z',
reviewed_at: null,
status: 'pending',
admin_notes: null
},
{
id: 2005,
phone: '13800138005',
wechat: 'sunqi888',
valuation_result: 1680000.00,
created_at: '2024-11-06T08:30:00Z',
reviewed_at: '2024-11-06T10:15:00Z',
status: 'approved',
admin_notes: '评估数据完整,审核通过'
},
{
id: 2006,
phone: '13800138006',
wechat: 'zhouba2024',
valuation_result: 950000.00,
created_at: '2024-11-05T13:20:00Z',
reviewed_at: null,
status: 'pending',
admin_notes: null
},
{
id: 2007,
phone: '13800138007',
wechat: 'wujiu_user',
valuation_result: 3200000.00,
created_at: '2024-11-04T15:45:00Z',
reviewed_at: '2024-11-04T17:20:00Z',
status: 'approved',
admin_notes: '高价值资产,评估结果准确'
},
{
id: 2008,
phone: '13800138008',
wechat: 'zhengshi_vip',
valuation_result: 750000.00,
created_at: '2024-11-03T10:10:00Z',
reviewed_at: '2024-11-03T12:00:00Z',
status: 'approved',
admin_notes: '评估流程规范,结果可信'
},
{
id: 2009,
phone: '13800138009',
wechat: 'chenjun2024',
valuation_result: 1890000.00,
created_at: '2024-11-02T14:30:00Z',
reviewed_at: null,
status: 'pending',
admin_notes: null
},
{
id: 2010,
phone: '13800138010',
wechat: 'liuxia_user',
valuation_result: 430000.00,
created_at: '2024-11-01T11:45:00Z',
reviewed_at: '2024-11-01T13:30:00Z',
status: 'approved',
admin_notes: '低价值资产,评估合理'
},
{
id: 2011,
phone: '13800138011',
wechat: 'zhaolei2024',
valuation_result: 2100000.00,
created_at: '2024-10-31T09:20:00Z',
reviewed_at: '2024-10-31T11:00:00Z',
status: 'approved',
admin_notes: '评估报告详细,数据支撑充分'
},
{
id: 2012,
phone: '13800138012',
wechat: 'sunmei_vip',
valuation_result: 680000.00,
created_at: '2024-10-30T16:15:00Z',
reviewed_at: null,
status: 'pending',
admin_notes: null
}
]
// 模拟分页和搜索 // 模拟分页和搜索
let filteredValuations = [...mockValuations] let filteredValuations = [...mockValuationDetails]
// 手机号搜索 // 手机号搜索
if (params.phone) { if (params.phone) {
@ -536,7 +688,16 @@ export default {
const pageSize = params.page_size || 10 const pageSize = params.page_size || 10
const startIndex = (page - 1) * pageSize const startIndex = (page - 1) * pageSize
const endIndex = startIndex + pageSize const endIndex = startIndex + pageSize
const paginatedValuations = filteredValuations.slice(startIndex, endIndex) const paginatedValuations = filteredValuations.slice(startIndex, endIndex).map((item) => ({
id: item.id,
phone: item.phone,
wechat: item.wechat,
valuation_result: item.valuation_result,
created_at: item.created_at,
reviewed_at: item.reviewed_at,
status: item.status,
admin_notes: item.admin_notes,
}))
// 返回 Promise 模拟异步请求 // 返回 Promise 模拟异步请求
return new Promise((resolve) => { return new Promise((resolve) => {
@ -550,7 +711,19 @@ export default {
}, 300) // 模拟网络延迟 }, 300) // 模拟网络延迟
}) })
}, },
getValuationById: (params = {}) => request.get(`/valuation/${params.valuation_id}`), getValuationById: (params = {}) => {
const id = Number(params.valuation_id || params.id)
return new Promise((resolve, reject) => {
setTimeout(() => {
const detail = mockValuationDetails.find((item) => item.id === id)
if (detail) {
resolve({ data: detail })
} else {
reject({ code: 404, message: '未找到估值详情' })
}
}, 200)
})
},
createValuation: (data = {}) => request.post('/valuation', data), createValuation: (data = {}) => request.post('/valuation', data),
updateValuation: (data = {}) => request.put(`/valuation/${data.id}`, data), updateValuation: (data = {}) => request.put(`/valuation/${data.id}`, data),
deleteValuation: (params = {}) => request.delete(`/valuation/${params.valuation_id}`), deleteValuation: (params = {}) => request.delete(`/valuation/${params.valuation_id}`),

View File

@ -0,0 +1,422 @@
<script setup>
import { computed, ref, watch } from 'vue'
import {
NButton,
NTag,
NTabs,
NTabPane,
NSpin,
NImage,
NImageGroup,
} from 'naive-ui'
import { formatDate } from '@/utils'
import TheIcon from '@/components/icon/TheIcon.vue'
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 activeDetailTab = ref('audit')
watch(
() => props.detailData?.id,
() => {
activeDetailTab.value = 'audit'
}
)
const detailSections = computed(() => {
const detail = props.detailData
if (!detail) return []
return [
{
key: 'basic',
title: '基础信息',
columns: [
{ label: '资产名称', type: 'text', value: detail.asset_name || '-' },
{ label: '所属机构', type: 'text', value: detail.institution || '-' },
{ label: '所属行业', type: 'text', value: detail.industry || '-' },
],
},
{
key: 'finance',
title: '财务状况',
columns: [
{ label: '近12个月机构营收/万元', type: 'text', value: formatNumberValue(detail.annual_revenue) },
{ label: '近12个月机构研发投入/万元', type: 'text', value: formatNumberValue(detail.rd_investment) },
{ label: '近三年机构收益/万元', type: 'list', value: formatThreeYearIncome(detail.three_year_income) },
{ label: '资产受资助情况', type: 'text', value: detail.funding_status || '-' },
],
},
{
key: 'tech',
title: '非遗等级与技术',
columns: [
{ label: '非遗传承人等级', type: 'text', value: detail.inheritor_level || '-' },
{ label: '非遗传承人年龄水平及数量', type: 'list', value: formatAgeDistribution(detail.inheritor_age_count) },
{ label: '非遗传承人等级证书', type: 'images', value: detail.inheritor_certificates || [] },
{ label: '非遗等级', type: 'text', value: detail.heritage_level || '-' },
{ label: '非遗资产所用专利的申请号', type: 'text', value: detail.patent_application_no || '-' },
{ label: '非遗资产历史证明证据及数量', type: 'list', value: formatHistoricalEvidence(detail.historical_evidence) },
{
label: '非遗资产所用专利/纹样图片',
type: 'images',
value: [...(detail.patent_certificates || []), ...(detail.pattern_images || [])],
},
],
},
{
key: 'promotion',
title: '非遗应用与推广',
columns: [
{ 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) },
{ label: '线上相关宣传账号信息', type: 'list', value: formatPlatformAccounts(detail.platform_accounts) },
],
},
{
key: 'products',
title: '非遗资产衍生商品信息',
columns: [
{ 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) },
],
},
]
})
const calcFlow = computed(() => props.detailData?.calculation_result?.flow || [])
</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 v-if="mode === 'approve' && detailData?.status === 'pending'" class="detail-actions">
<NButton tertiary type="error" @click="emit('reject')">
<TheIcon icon="mdi:close-circle-outline" :size="16" class="mr-4" />
拒绝
</NButton>
<NButton type="primary" @click="emit('approve')">
<TheIcon icon="mdi:check-circle-outline" :size="16" class="mr-4" />
通过
</NButton>
</div>
</div>
<NTabs v-model:value="activeDetailTab">
<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>
<div class="table-wrapper">
<table class="info-table">
<thead>
<tr>
<th class="first-col">字段名</th>
<th v-for="column in section.columns" :key="column.label">
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="first-col">用户输入</td>
<td v-for="column in section.columns" :key="column.label">
<div v-if="column.type === 'list'" class="cell-multi">
<template v-if="column.value && column.value.length">
<span v-for="(item, idx) in column.value" :key="idx">{{ item }}</span>
</template>
<span v-else>-</span>
</div>
<div v-else-if="column.type === 'images'">
<template v-if="column.value && column.value.length">
<NImageGroup>
<NImage
v-for="(img, idx) in column.value"
:key="idx"
width="72"
height="48"
:src="img"
object-fit="cover"
/>
</NImageGroup>
</template>
<span v-else>-</span>
</div>
<span v-else>{{ column.value || '-' }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</NSpin>
</NTabPane>
<NTabPane name="flow" tab="计算流程">
<NSpin :show="loading">
<div class="calc-summary">
<div class="calc-card">
<p>模型估值A·B法</p>
<strong>{{ formatAmount(detailData?.model_value_b) }}</strong>
</div>
<div class="calc-card">
<p>市场对标估值</p>
<strong>{{ formatAmount(detailData?.market_value_c) }}</strong>
</div>
<div class="calc-card">
<p>综合校准估值</p>
<strong>{{ formatAmount(detailData?.final_value_ab) }}</strong>
</div>
<div class="calc-card">
<p>动态质押率</p>
<strong>{{ formatPercent(detailData?.dynamic_pledge_rate) }}</strong>
</div>
</div>
<div v-if="calcFlow.length" class="calc-flow">
<div v-for="(step, index) in calcFlow" :key="index" class="calc-step">
<div class="step-index">{{ index + 1 }}</div>
<div class="step-body">
<p class="step-title">{{ step.title }}</p>
<p class="step-desc">{{ step.description }}</p>
</div>
<div class="step-value">{{ step.value }}</div>
</div>
</div>
<div v-else class="calc-empty">暂无计算流程数据</div>
</NSpin>
</NTabPane>
</NTabs>
</div>
</template>
<style scoped>
.audit-detail {
background: #fff;
border-radius: 12px;
padding: 24px;
}
.detail-header {
display: flex;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.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;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
margin-bottom: 12px;
}
.section-title .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #409eff;
}
.table-wrapper {
overflow-x: auto;
}
.info-table {
width: 100%;
border-collapse: collapse;
background: #f9fafe;
table-layout: fixed;
}
.info-table th,
.info-table td {
border: 1px solid #e5e6eb;
padding: 12px;
text-align: left;
min-width: 140px;
word-break: break-word;
}
.info-table .first-col {
width: 120px;
text-align: center;
background: #f1f2f5;
font-weight: 600;
}
.info-table th:not(.first-col),
.info-table td:not(.first-col) {
width: 180px;
}
.cell-multi {
display: flex;
flex-direction: column;
gap: 4px;
}
.calc-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.calc-card {
padding: 16px;
border: 1px solid #eef0f6;
border-radius: 10px;
background: #fdfdff;
}
.calc-card p {
margin: 0 0 8px;
color: #888;
}
.calc-card strong {
font-size: 18px;
color: #1d2129;
}
.calc-flow {
display: flex;
flex-direction: column;
gap: 12px;
}
.calc-step {
display: flex;
gap: 16px;
align-items: center;
padding: 16px;
border-radius: 10px;
border: 1px dashed #dce1f0;
}
.step-index {
width: 32px;
height: 32px;
border-radius: 50%;
background: #eef4ff;
color: #3b82f6;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.step-body {
flex: 1;
}
.step-title {
margin: 0 0 6px;
font-weight: 600;
}
.step-desc {
margin: 0;
color: #666;
font-size: 13px;
}
.step-value {
font-weight: 600;
color: #1d2129;
}
.calc-empty {
text-align: center;
color: #999;
padding: 40px 0;
}
</style>

View File

@ -0,0 +1,12 @@
export const STATUS_OPTIONS = [
{ label: '全部', value: '' },
{ label: '待审核', value: 'pending' },
{ label: '已完成', value: 'approved' },
]
export const STATUS_MAP = {
pending: { type: 'warning', text: '待审核' },
approved: { type: 'success', text: '已完成' },
}
export const getStatusConfig = (status) => STATUS_MAP[status] || { type: 'default', text: '未知' }

View File

@ -1,16 +1,14 @@
<script setup> <script setup>
import { h, onMounted, ref, resolveDirective, withDirectives } from 'vue' import { h, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { import {
NButton, NButton,
NForm, NForm,
NFormItem, NFormItem,
NInput, NInput,
NTag, NTag,
NPopconfirm,
NSelect, NSelect,
NDatePicker, NDatePicker,
NInputNumber,
NSpace,
} from 'naive-ui' } from 'naive-ui'
import CommonPage from '@/components/page/CommonPage.vue' import CommonPage from '@/components/page/CommonPage.vue'
@ -19,87 +17,28 @@ import CrudModal from '@/components/table/CrudModal.vue'
import CrudTable from '@/components/table/CrudTable.vue' import CrudTable from '@/components/table/CrudTable.vue'
import { formatDate, renderIcon } from '@/utils' import { formatDate, renderIcon } from '@/utils'
import { useCRUD } from '@/composables'
import api from '@/api' import api from '@/api'
import TheIcon from '@/components/icon/TheIcon.vue' import TheIcon from '@/components/icon/TheIcon.vue'
import AuditDetail from './components/AuditDetail.vue'
import { STATUS_OPTIONS, getStatusConfig } from './constants'
import { formatAmount } from './utils'
defineOptions({ name: '审核列表' }) defineOptions({ name: '审核列表' })
const router = useRouter()
const route = useRoute()
const $table = ref(null) const $table = ref(null)
const queryItems = ref({}) const queryItems = ref({})
const vPermission = resolveDirective('permission')
// //
const statusOptions = [
{ label: '全部', value: '' },
{ label: '待审核', value: 'pending' },
{ label: '已完成', value: 'approved' },
]
const {
modalVisible,
modalTitle,
modalAction,
modalLoading,
handleSave,
modalForm,
modalFormRef,
handleEdit,
handleDelete,
handleAdd,
} = useCRUD({
name: '估值评估',
initForm: {},
doCreate: api.createValuation,
doUpdate: api.updateValuation,
doDelete: api.deleteValuation,
refresh: () => $table.value?.handleSearch(),
})
//
const approvalModalVisible = ref(false)
const approvalModalTitle = ref('')
const approvalForm = ref({
valuation_id: null,
admin_notes: '',
action: '', // 'approve' or 'reject'
})
const approvalFormRef = ref(null)
//
const contentModalVisible = ref(false)
const contentForm = ref({
content: '',
})
const contentFormRef = ref(null)
onMounted(() => {
$table.value?.handleSearch()
})
//
const renderStatus = (status) => { const renderStatus = (status) => {
const statusMap = { const config = getStatusConfig(status)
pending: { type: 'warning', text: '待审核' },
approved: { type: 'success', text: '已完成' },
}
const config = statusMap[status] || { type: 'default', text: '未知' }
return h(NTag, { type: config.type }, { default: () => config.text }) return h(NTag, { type: config.type }, { default: () => config.text })
} }
// //
const formatAmount = (amount) => {
if (!amount) return '-'
return `¥${Number(amount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
const columns = [ const columns = [
{ { title: '编号', key: 'id', width: 80, align: 'center' },
title: '编号',
key: 'id',
width: 80,
align: 'center',
},
{ {
title: '手机号', title: '手机号',
key: 'phone', key: 'phone',
@ -163,20 +102,21 @@ const columns = [
{ {
size: 'small', size: 'small',
type: 'primary', type: 'primary',
onClick: () => handleApprove(row), onClick: () => goToDetail(row, 'approve'),
}, },
{ {
default: () => '审核', default: () => '审核',
icon: renderIcon('mdi:check-circle-outline', { size: 16 }), icon: renderIcon('mdi:check-circle-outline', { size: 16 }),
} }
) )
} else if (row.status === 'approved') { }
if (row.status === 'approved') {
return h( return h(
NButton, NButton,
{ {
size: 'small', size: 'small',
type: 'info', type: 'info',
onClick: () => handleView(row), onClick: () => goToDetail(row, 'view'),
}, },
{ {
default: () => '查看', default: () => '查看',
@ -189,215 +129,227 @@ const columns = [
}, },
] ]
// //
function handleApprove(row) { const isDetailView = ref(false)
approvalForm.value = { const detailLoading = ref(false)
valuation_id: row.id, const detailData = ref(null)
admin_notes: '', const detailMode = ref('view')
action: 'approve',
//
const approvalModalVisible = ref(false)
const approvalModalTitle = ref('审核估值评估')
const approvalForm = ref({
valuation_id: null,
admin_notes: '',
action: 'approve',
})
const approvalFormRef = ref(null)
//
const contentModalVisible = ref(false)
const contentForm = ref({
content: '',
})
const contentFormRef = ref(null)
onMounted(() => {
$table.value?.handleSearch()
})
//
//
function goToDetail(row, mode = 'view') {
const nextQuery = { ...route.query, detailId: String(row.id), mode }
router.push({ query: nextQuery })
}
function resetDetailState() {
isDetailView.value = false
detailLoading.value = false
detailData.value = null
detailMode.value = 'view'
}
async function loadDetail(detailId, mode = 'view') {
const parsedId = Number(detailId)
if (!parsedId) {
resetDetailState()
return
} }
approvalModalTitle.value = '审核估值评估' detailMode.value = mode === 'approve' ? 'approve' : 'view'
isDetailView.value = true
detailLoading.value = true
try {
const { data } = await api.getValuationById({ valuation_id: parsedId })
detailData.value = data
approvalForm.value.valuation_id = parsedId
} catch (error) {
$message?.error(error?.message || '获取详情失败')
backToList()
} finally {
detailLoading.value = false
}
}
function backToList() {
const nextQuery = { ...route.query }
delete nextQuery.detailId
delete nextQuery.mode
router.replace({ query: nextQuery })
$table.value?.handleSearch()
}
function openApprovalModal(action) {
approvalForm.value.action = action
approvalForm.value.admin_notes = ''
approvalModalTitle.value = action === 'approve' ? '通过审核' : '拒绝审核'
approvalModalVisible.value = true approvalModalVisible.value = true
} }
// //
function handleView(row) {
console.log('查看详情', row)
}
//
function handleAddContent() { function handleAddContent() {
contentForm.value = { contentForm.value = { content: '' }
content: '',
}
contentModalVisible.value = true contentModalVisible.value = true
} }
//
function handleContentSave() { function handleContentSave() {
contentFormRef.value?.validate((errors) => { contentFormRef.value?.validate((errors) => {
if (!errors) { if (!errors) {
console.log('保存文案设置', contentForm.value)
// API
contentModalVisible.value = false contentModalVisible.value = false
$message.success('文案上传并通知成功') $message?.success('文案上传并通知成功')
} }
}) })
} }
// //
async function handleApprovalSubmit(action) { async function handleApprovalSubmit() {
try { try {
approvalForm.value.action = action
await approvalFormRef.value?.validate() await approvalFormRef.value?.validate()
const action = approvalForm.value.action
const apiCall = action === 'approve' ? api.approveValuation : api.rejectValuation const apiCall = action === 'approve' ? api.approveValuation : api.rejectValuation
await apiCall({ await apiCall({
valuation_id: approvalForm.value.valuation_id, valuation_id: approvalForm.value.valuation_id,
admin_notes: approvalForm.value.admin_notes, admin_notes: approvalForm.value.admin_notes,
}) })
$message?.success(action === 'approve' ? '审核通过成功' : '审核拒绝成功')
$message.success(action === 'approve' ? '审核通过成功' : '审核拒绝成功')
approvalModalVisible.value = false approvalModalVisible.value = false
$table.value?.handleSearch() backToList()
} catch (error) { } catch (error) {
if (error?.message) { if (error?.message) {
$message.error(error.message) $message?.error(error.message)
} }
} }
} }
const approvalRules = { const approvalRules = {
admin_notes: [ admin_notes: [
{ { required: true, message: '请输入审核备注', trigger: ['input', 'blur'] },
required: true,
message: '请输入审核备注',
trigger: ['input', 'blur'],
},
], ],
} }
const contentRules = { const contentRules = {
content: [ content: [
{ { required: true, message: '请输入文案内容', trigger: ['input', 'blur'] },
required: true,
message: '请输入文案内容',
trigger: ['input', 'blur'],
},
], ],
} }
watch(
() => [route.query.detailId, route.query.mode],
([detailId, mode]) => {
if (detailId) {
loadDetail(detailId, mode)
} else {
resetDetailState()
}
},
{ immediate: true }
)
</script> </script>
<template> <template>
<CommonPage show-footer title="审核列表"> <CommonPage show-footer title="审核列表">
<!-- 表格 --> <template v-if="!isDetailView">
<CrudTable <CrudTable
ref="$table" ref="$table"
v-model:query-items="queryItems" v-model:query-items="queryItems"
:columns="columns" :columns="columns"
:get-data="api.getValuationList" :get-data="api.getValuationList"
>
<template #queryBar>
<QueryBarItem label="手机号" :label-width="80">
<NInput
v-model:value="queryItems.phone"
clearable
type="text"
placeholder="请输入手机号"
style="width: 200px"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="微信号" :label-width="80">
<NInput
v-model:value="queryItems.wechat"
clearable
type="text"
placeholder="请输入微信号"
style="width: 200px"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="提交时间" :label-width="80">
<NDatePicker
v-model:value="queryItems.created_at"
type="daterange"
clearable
placeholder="请选择提交时间"
style="width: 280px"
@update:value="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="审核时间" :label-width="80">
<NDatePicker
v-model:value="queryItems.reviewed_at"
type="daterange"
clearable
placeholder="请选择审核时间"
style="width: 280px"
@update:value="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="状态" :label-width="80">
<NSelect
v-model:value="queryItems.status"
:options="statusOptions"
placeholder="请选择状态"
clearable
style="width: 200px"
@update:value="$table?.handleSearch()"
/>
</QueryBarItem>
</template>
<template #action>
<NButton type="primary" @click="handleAddContent">
<TheIcon icon="mdi:plus" :size="18" class="mr-5" />
新增文案设置
</NButton>
</template>
</CrudTable>
<!-- 查看详情弹窗 -->
<CrudModal
v-model:visible="modalVisible"
:title="modalTitle"
:loading="modalLoading"
:show-footer="false"
>
<NForm
ref="modalFormRef"
label-placement="left"
label-align="left"
:label-width="120"
:model="modalForm"
> >
<NFormItem label="编号"> <template #queryBar>
<span>{{ modalForm.id }}</span> <QueryBarItem label="手机号" :label-width="80">
</NFormItem> <NInput
<NFormItem label="手机号"> v-model:value="queryItems.phone"
<span>{{ modalForm.phone }}</span> clearable
</NFormItem> type="text"
<NFormItem label="微信号"> placeholder="请输入手机号"
<span>{{ modalForm.wechat }}</span> style="width: 200px"
</NFormItem> @keypress.enter="$table?.handleSearch()"
<NFormItem label="评估结果"> />
<span>{{ formatAmount(modalForm.valuation_result) }}</span> </QueryBarItem>
</NFormItem> <QueryBarItem label="微信号" :label-width="80">
<NFormItem label="资产名称"> <NInput
<span>{{ modalForm.asset_name || '-' }}</span> v-model:value="queryItems.wechat"
</NFormItem> clearable
<NFormItem label="所属机构"> type="text"
<span>{{ modalForm.institution || '-' }}</span> placeholder="请输入微信号"
</NFormItem> style="width: 200px"
<NFormItem label="所属行业"> @keypress.enter="$table?.handleSearch()"
<span>{{ modalForm.industry || '-' }}</span> />
</NFormItem> </QueryBarItem>
<NFormItem label="非遗等级"> <QueryBarItem label="提交时间" :label-width="80">
<span>{{ modalForm.heritage_level || '-' }}</span> <NDatePicker
</NFormItem> v-model:value="queryItems.created_at"
<NFormItem label="提交时间"> type="daterange"
<span>{{ formatDate(modalForm.created_at) }}</span> clearable
</NFormItem> placeholder="请选择提交时间"
<NFormItem label="审核时间"> style="width: 280px"
<span>{{ modalForm.reviewed_at ? formatDate(modalForm.reviewed_at) : '-' }}</span> @update:value="$table?.handleSearch()"
</NFormItem> />
<NFormItem label="状态"> </QueryBarItem>
{{ renderStatus(modalForm.status) }} <QueryBarItem label="审核时间" :label-width="80">
</NFormItem> <NDatePicker
<NFormItem label="管理员备注"> v-model:value="queryItems.reviewed_at"
<span>{{ modalForm.admin_notes || '-' }}</span> type="daterange"
</NFormItem> clearable
</NForm> placeholder="请选择审核时间"
</CrudModal> style="width: 280px"
@update:value="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="状态" :label-width="80">
<NSelect
v-model:value="queryItems.status"
:options="STATUS_OPTIONS"
placeholder="请选择状态"
clearable
style="width: 200px"
@update:value="$table?.handleSearch()"
/>
</QueryBarItem>
</template>
<template #action>
<NButton type="primary" @click="handleAddContent">
<TheIcon icon="mdi:plus" :size="18" class="mr-5" />
新增文案设置
</NButton>
</template>
</CrudTable>
</template>
<template v-else>
<AuditDetail
:detail-data="detailData"
:loading="detailLoading"
:mode="detailMode"
@back="backToList"
@approve="openApprovalModal('approve')"
@reject="openApprovalModal('reject')"
/>
</template>
<!-- 审核弹窗 --> <!-- 审核弹窗 -->
<CrudModal <CrudModal v-model:visible="approvalModalVisible" :title="approvalModalTitle" :show-footer="false">
v-model:visible="approvalModalVisible"
:title="approvalModalTitle"
:show-footer="false"
>
<NForm <NForm
ref="approvalFormRef" ref="approvalFormRef"
label-placement="left" label-placement="left"
@ -415,31 +367,18 @@ const contentRules = {
/> />
</NFormItem> </NFormItem>
<NFormItem> <NFormItem>
<NSpace> <div class="modal-actions">
<NButton type="success" @click="handleApprovalSubmit('approve')">
<template #icon>
<TheIcon icon="mdi:check-circle-outline" :size="16" />
</template>
通过
</NButton>
<NButton type="error" @click="handleApprovalSubmit('reject')">
<template #icon>
<TheIcon icon="mdi:close-circle-outline" :size="16" />
</template>
拒绝
</NButton>
<NButton @click="approvalModalVisible = false">取消</NButton> <NButton @click="approvalModalVisible = false">取消</NButton>
</NSpace> <NButton type="primary" @click="handleApprovalSubmit">
{{ approvalForm.action === 'approve' ? '确认通过' : '确认拒绝' }}
</NButton>
</div>
</NFormItem> </NFormItem>
</NForm> </NForm>
</CrudModal> </CrudModal>
<!-- 文案设置弹窗 --> <!-- 文案设置弹窗 -->
<CrudModal <CrudModal v-model:visible="contentModalVisible" title="新增文案设置" :show-footer="false">
v-model:visible="contentModalVisible"
title="新增文案设置"
:show-footer="false"
>
<NForm <NForm
ref="contentFormRef" ref="contentFormRef"
label-placement="left" label-placement="left"
@ -457,14 +396,21 @@ const contentRules = {
/> />
</NFormItem> </NFormItem>
<NFormItem> <NFormItem>
<div style="display: flex; justify-content: flex-end; gap: 12px; width: 100%"> <div class="modal-actions">
<NButton @click="contentModalVisible = false">取消</NButton> <NButton @click="contentModalVisible = false">取消</NButton>
<NButton type="primary" @click="handleContentSave"> <NButton type="primary" @click="handleContentSave">上传并通知</NButton>
上传并通知
</NButton>
</div> </div>
</NFormItem> </NFormItem>
</NForm> </NForm>
</CrudModal> </CrudModal>
</CommonPage> </CommonPage>
</template> </template>
<style scoped>
.modal-actions {
width: 100%;
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@ -0,0 +1,69 @@
const platformLabelMap = {
bilibili: 'B站账号',
douyin: '抖音账号',
kuaishou: '快手账号',
qita: '其他账号',
}
export const formatAmount = (amount) => {
if (!amount && amount !== 0) return '-'
return `¥${Number(amount).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`
}
export const formatNumberValue = (value, decimals = 0) => {
if (value === null || value === undefined || value === '') return '-'
if (typeof value === 'number') {
return Number(value).toLocaleString('zh-CN', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
})
}
return value
}
export const formatPercent = (value) => {
if (value === null || value === undefined || Number.isNaN(Number(value))) return '-'
return `${(Number(value) * 100).toFixed(2)}%`
}
export const formatThreeYearIncome = (list = []) => {
if (!Array.isArray(list) || !list.length) return ['暂无数据']
return list.map((item, index) => `${index + 1}年:${formatNumberValue(item)}`)
}
export const formatAgeDistribution = (list = []) => {
return [
{ label: '≤50岁', value: list?.[0] },
{ label: '50-70岁', value: list?.[1] },
{ label: '≥70岁', value: list?.[2] },
].map((bucket) => `${bucket.label}${formatNumberValue(bucket.value)}`)
}
export const formatHistoricalEvidence = (evidence = {}) => {
const mapping = [
{ key: 'artifacts', label: '出土实物' },
{ key: 'ancient_literature', label: '古代文献' },
{ key: 'inheritor_testimony', label: '传承人佐证' },
{ key: 'modern_research', label: '现代研究' },
]
return mapping.map(({ key, label }) => `${label}${formatNumberValue(evidence?.[key])}`)
}
export const formatPlatformAccounts = (accounts = {}) => {
const list = Object.entries(accounts || {}).map(([platform, info]) => {
const label = platformLabelMap[platform] || platform
if (!info) return `${label}-`
return `${label}${info.account || '-'}(赞${formatNumberValue(info.likes)} / 评${formatNumberValue(
info.comments
)} / 转${formatNumberValue(info.shares)}`
})
return list.length ? list : ['暂无账号信息']
}
export const formatPriceRange = (range = []) => {
if (!Array.isArray(range) || range.length < 2) return '-'
return `${formatAmount(range[0])} - ${formatAmount(range[1])}`
}