Compare commits

..

No commits in common. "52beb9b2642d1aa10016d08932efb843cb4ef62b" and "11ae08dc96160d613c81d46bbba13865cb71bff4" have entirely different histories.

5 changed files with 215 additions and 304 deletions

View File

@ -448,16 +448,6 @@ export default {
) )
} }
// 注册时间筛选(日期范围)
if (params.created_at && Array.isArray(params.created_at) && params.created_at.length === 2) {
const [startTime, endTime] = params.created_at
filteredUsers = filteredUsers.filter(user => {
if (!user.created_at) return false
const userTime = new Date(user.created_at).getTime()
return userTime >= startTime && userTime <= endTime
})
}
// 分页处理 // 分页处理
const page = Number(params.page) || 1 const page = Number(params.page) || 1
const pageSize = Number(params.page_size) || 10 const pageSize = Number(params.page_size) || 10

View File

@ -135,7 +135,6 @@ const modalTitle = props.mode === 'invoice' ? '开票' : '查看发票'
v-model:value="formData.email" v-model:value="formData.email"
placeholder="请输入邮箱地址" placeholder="请输入邮箱地址"
clearable clearable
:disabled="mode === 'view'"
/> />
</NFormItem> </NFormItem>
@ -146,7 +145,6 @@ const modalTitle = props.mode === 'invoice' ? '开票' : '查看发票'
placeholder="请输入文案内容" placeholder="请输入文案内容"
:rows="4" :rows="4"
clearable clearable
:disabled="mode === 'view'"
/> />
</NFormItem> </NFormItem>
@ -160,7 +158,6 @@ const modalTitle = props.mode === 'invoice' ? '开票' : '查看发票'
:before-upload="beforeUpload" :before-upload="beforeUpload"
@change="handleUploadChange" @change="handleUploadChange"
@remove="handleRemove" @remove="handleRemove"
:disabled="mode === 'view'"
> >
<div class="upload-trigger"> <div class="upload-trigger">
<div class="upload-icon">+</div> <div class="upload-icon">+</div>
@ -169,17 +166,20 @@ const modalTitle = props.mode === 'invoice' ? '开票' : '查看发票'
</div> </div>
</NFormItem> </NFormItem>
<NFormItem>
<div style="width: 100%; text-align: left; padding-left: 100px">
<NText depth="3" style="font-size: 12px; color: #ff9800">
ps最多上传两张
</NText>
</div>
</NFormItem>
</NForm> </NForm>
<template #footer> <template #footer>
<div v-if="mode !== 'view'" style="display: flex; justify-content: flex-end; gap: 12px"> <div style="display: flex; justify-content: flex-end; gap: 12px">
<NButton @click="handleClose">取消</NButton> <NButton @click="handleClose">取消</NButton>
<NButton type="primary" @click="handleConfirm">确定</NButton> <NButton type="primary" @click="handleConfirm">确定</NButton>
</div> </div>
<div v-else style="display: flex; justify-content: flex-end; gap: 12px">
<NButton @click="handleClose">关闭</NButton>
</div>
</template> </template>
</NModal> </NModal>
</template> </template>
@ -192,6 +192,8 @@ const modalTitle = props.mode === 'invoice' ? '开票' : '查看发票'
.upload-trigger { .upload-trigger {
width: 100px; width: 100px;
height: 100px; height: 100px;
border: 2px dashed #d9d9d9;
border-radius: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -199,6 +201,10 @@ const modalTitle = props.mode === 'invoice' ? '开票' : '查看发票'
transition: all 0.3s; transition: all 0.3s;
} }
.upload-trigger:hover {
border-color: #40a9ff;
}
.upload-icon { .upload-icon {
font-size: 32px; font-size: 32px;
color: #d9d9d9; color: #d9d9d9;

View File

@ -9,7 +9,6 @@ import {
NSwitch, NSwitch,
NTag, NTag,
NPopconfirm, NPopconfirm,
NDatePicker,
} from 'naive-ui' } from 'naive-ui'
import CommonPage from '@/components/page/CommonPage.vue' import CommonPage from '@/components/page/CommonPage.vue'
@ -70,21 +69,21 @@ const columns = [
{ {
title: 'ID', title: 'ID',
key: 'id', key: 'id',
width: 100, width: 80,
align: 'center', align: 'center',
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
}, },
{ {
title: '手机号', title: '手机号',
key: 'phone', key: 'phone',
width: 140, width: 120,
align: 'center', align: 'center',
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
}, },
{ {
title: '微信号', title: '微信号',
key: 'wechat', key: 'wechat',
width: 140, width: 120,
align: 'center', align: 'center',
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
render(row) { render(row) {
@ -95,7 +94,7 @@ const columns = [
title: '注册时间', title: '注册时间',
key: 'created_at', key: 'created_at',
align: 'center', align: 'center',
width: 180, width: 160,
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
render(row) { render(row) {
return row.created_at ? formatDate(row.created_at) : '-' return row.created_at ? formatDate(row.created_at) : '-'
@ -117,7 +116,7 @@ const columns = [
width: 120, width: 120,
align: 'center', align: 'center',
render(row) { render(row) {
return row.remaining_count !== undefined ? row.remaining_count : 0 return row.remaining_count || 0
}, },
}, },
{ {
@ -249,36 +248,24 @@ const validateForm = {
:get-data="api.getAppUserList" :get-data="api.getAppUserList"
> >
<template #queryBar> <template #queryBar>
<QueryBarItem label="手机号" :label-width="60"> <QueryBarItem label="手机号" :label-width="50">
<NInput <NInput
v-model:value="queryItems.phone" v-model:value="queryItems.phone"
clearable clearable
type="text" type="text"
placeholder="请输入手机号" placeholder="请输入手机号"
style="width: 200px"
@keypress.enter="$table?.handleSearch()" @keypress.enter="$table?.handleSearch()"
/> />
</QueryBarItem> </QueryBarItem>
<QueryBarItem label="微信号" :label-width="60"> <QueryBarItem label="微信号" :label-width="50">
<NInput <NInput
v-model:value="queryItems.wechat" v-model:value="queryItems.wechat"
clearable clearable
type="text" type="text"
placeholder="请输入微信号" placeholder="请输入微信号"
style="width: 200px"
@keypress.enter="$table?.handleSearch()" @keypress.enter="$table?.handleSearch()"
/> />
</QueryBarItem> </QueryBarItem>
<QueryBarItem label="注册时间" :label-width="70">
<NDatePicker
v-model:value="queryItems.created_at"
type="daterange"
clearable
placeholder="请选择注册时间"
style="width: 280px"
@update:value="$table?.handleSearch()"
/>
</QueryBarItem>
</template> </template>
</CrudTable> </CrudTable>

View File

@ -265,102 +265,35 @@ const handleCertificateConfirm = (data) => {
</NTabPane> </NTabPane>
<NTabPane name="flow" tab="计算流程"> <NTabPane name="flow" tab="计算流程">
<NSpin :show="loading"> <NSpin :show="loading">
<div class="calc-flow-container"> <div class="calc-summary">
<!-- 左侧详细计算流程 --> <div class="calc-card">
<div class="calc-flow-left"> <p>模型估值A·B法</p>
<div class="calc-formula-header"> <strong>{{ formatAmount(detailData?.model_value_b) }}</strong>
最终估值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 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> </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> </div>
<div v-else class="calc-empty">暂无计算流程数据</div>
</NSpin> </NSpin>
</NTabPane> </NTabPane>
</NTabs> </NTabs>
@ -523,141 +456,75 @@ const handleCertificateConfirm = (data) => {
gap: 4px; gap: 4px;
} }
/* 计算流程容器 */ .calc-summary {
.calc-flow-container { display: grid;
display: flex; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 24px; gap: 16px;
min-height: 600px;
}
/* 左侧详细流程 */
.calc-flow-left {
flex: 1;
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
overflow-y: auto;
max-height: 800px;
}
.calc-formula-header {
font-size: 18px;
font-weight: 600;
color: #1d2129;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #e5e7eb;
}
.calc-section {
margin-bottom: 24px;
}
.calc-section-title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 16px;
line-height: 1.6;
}
.calc-subsection {
margin-left: 20px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.calc-subsection-title { .calc-card {
font-size: 15px; 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; font-weight: 600;
color: #374151;
margin-bottom: 12px;
line-height: 1.6;
} }
.calc-item { .step-body {
margin-left: 20px; flex: 1;
margin-bottom: 16px;
} }
.calc-item-label { .step-title {
font-size: 14px; margin: 0 0 6px;
color: #4b5563; font-weight: 600;
margin-bottom: 8px;
line-height: 1.6;
} }
.calc-detail { .step-desc {
margin-left: 20px; margin: 0;
margin-top: 8px; color: #666;
}
.calc-detail-item {
font-size: 13px; font-size: 13px;
color: #6b7280;
margin-bottom: 4px;
line-height: 1.6;
} }
/* 右侧大纲 */ .step-value {
.calc-flow-right {
width: 320px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px;
overflow-y: auto;
max-height: 800px;
}
.calc-outline-title {
font-size: 16px;
font-weight: 600; font-weight: 600;
color: #1d2129; color: #1d2129;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #e5e7eb;
}
.calc-outline {
font-size: 13px;
}
.outline-section {
margin-bottom: 16px;
}
.outline-title {
font-size: 14px;
font-weight: 600;
color: #1d2129;
margin-bottom: 8px;
}
.outline-subsection {
margin-left: 12px;
margin-bottom: 12px;
}
.outline-subtitle {
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
}
.outline-item {
font-size: 12px;
color: #4b5563;
margin-bottom: 4px;
margin-left: 12px;
}
.outline-detail {
font-size: 11px;
color: #6b7280;
margin-left: 24px;
margin-bottom: 2px;
line-height: 1.5;
} }
.calc-empty { .calc-empty {

View File

@ -6,8 +6,7 @@ import {
NButton, NButton,
NUpload, NUpload,
NText, NText,
NImage, NImage
NImageGroup
} from 'naive-ui' } from 'naive-ui'
// //
// import { DownloadIcon } from '@vicons/tabler' // import { DownloadIcon } from '@vicons/tabler'
@ -143,32 +142,13 @@ const handleDownloadReport = () => {
// //
const handlePreview = (file) => { const handlePreview = (file) => {
// // PDF
if (!file.type?.startsWith('image/')) { if (file.type?.startsWith('image/') || file.type === 'application/pdf') {
$message.info('此文件类型不支持预览,请下载查看') window.open(file.url || '', '_blank')
return false } else {
console.log('此文件类型不支持预览')
// TODO:
} }
// true NUpload 使
return true
}
//
const handleDownload = (file) => {
const link = document.createElement('a')
link.href = file.url || ''
link.download = file.name || 'download'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
//
const customRequest = ({ file, onFinish, onError }) => {
//
//
setTimeout(() => {
onFinish()
}, 1000)
} }
const modalTitle = computed(() => { const modalTitle = computed(() => {
@ -188,17 +168,18 @@ const isUploadMode = computed(() => props.mode === 'upload')
style="width: 700px" style="width: 700px"
@update:show="handleClose" @update:show="handleClose"
> >
<!-- 上传/查看模式 --> <!-- 上传模式 -->
<div class="certificate-content"> <div v-if="isUploadMode" class="certificate-content">
<!-- 报告上传部分 --> <!-- 报告上传部分 -->
<div class="upload-section"> <div class="upload-section">
<div class="section-header">
<div class="section-title">报告:</div> <div class="section-title">报告:</div>
<div class="upload-content">
<div class="download-section">
<NButton text type="primary" @click="handleDownloadReport"> <NButton text type="primary" @click="handleDownloadReport">
点击下载原版报告 点击下载原版报告
</NButton> </NButton>
</div> </div>
<div class="upload-content">
<NUpload <NUpload
v-model:file-list="reportFileList" v-model:file-list="reportFileList"
:max="5" :max="5"
@ -206,11 +187,6 @@ const isUploadMode = computed(() => props.mode === 'upload')
:before-upload="beforeUpload" :before-upload="beforeUpload"
@change="handleReportUploadChange" @change="handleReportUploadChange"
@remove="handleRemove" @remove="handleRemove"
:custom-request="customRequest"
:disabled="!isUploadMode"
show-preview-button
show-download-button
@download="handleDownload"
> >
</NUpload> </NUpload>
</div> </div>
@ -227,17 +203,64 @@ const isUploadMode = computed(() => props.mode === 'upload')
:before-upload="beforeUpload" :before-upload="beforeUpload"
@change="handleCertificateUploadChange" @change="handleCertificateUploadChange"
@remove="handleRemove" @remove="handleRemove"
:custom-request="customRequest"
:disabled="!isUploadMode"
show-preview-button
show-download-button
@download="handleDownload"
> >
</NUpload> </NUpload>
</div> </div>
</div> </div>
</div> </div>
<!-- 查看模式 -->
<div v-else class="certificate-content">
<!-- 报告查看部分 -->
<div class="view-section">
<div class="section-title">报告:</div>
<div class="view-content">
<div class="download-area">
<NButton text type="primary" @click="handleDownloadReport">
<!-- 临时移除图标 -->
<!-- <template #icon>
<NIcon :component="DownloadIcon" />
</template> -->
点击下载原版报告
</NButton>
</div>
<div v-if="reportFileList.length > 0" class="file-info">
<NText>{{ reportFileList[0]?.name }} 下载</NText>
</div>
</div>
</div>
<!-- 证书查看部分 -->
<div class="view-section">
<div class="section-title">证书:</div>
<div class="view-content">
<div v-if="certificateFileList.length === 0" class="empty-state">
<NText depth="3">暂无证书文件</NText>
</div>
<div v-else class="certificate-display">
<div
v-for="file in certificateFileList"
:key="file.id"
class="certificate-image"
@click="handlePreview(file)"
>
<NImage
v-if="file.type?.startsWith('image/')"
:src="file.url"
width="120"
height="120"
objectFit="cover"
preview-disabled
/>
<div v-else class="file-icon">
{{ file.name?.split('.').pop()?.toUpperCase() || 'FILE' }}
</div>
</div>
</div>
</div>
</div>
</div>
<template #footer> <template #footer>
<div class="modal-footer"> <div class="modal-footer">
@ -270,18 +293,11 @@ const isUploadMode = computed(() => props.mode === 'upload')
margin-bottom: 20px; margin-bottom: 20px;
} }
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.section-title { .section-title {
font-size: 14px; font-size: 14px;
color: #666; color: #666;
margin-bottom: 12px;
font-weight: normal; font-weight: normal;
margin: 0;
} }
.upload-content { .upload-content {
@ -389,6 +405,44 @@ const isUploadMode = computed(() => props.mode === 'upload')
display: inline-block; display: inline-block;
} }
.certificate-display {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.certificate-image {
cursor: pointer;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e8e8e8;
transition: all 0.3s;
}
.certificate-image:hover {
border-color: #1890ff;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
}
.empty-state {
padding: 40px 20px;
text-align: center;
color: #999;
font-size: 14px;
}
.file-icon {
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
color: #666;
font-size: 24px;
font-weight: 600;
border-radius: 8px;
}
/* 底部按钮 */ /* 底部按钮 */
.modal-footer { .modal-footer {
@ -420,5 +474,12 @@ const isUploadMode = computed(() => props.mode === 'upload')
width: 95vw !important; width: 95vw !important;
} }
.certificate-display {
justify-content: center;
}
.certificate-image {
flex: 0 0 auto;
}
} }
</style> </style>