fix: 仪表盘与用户管理、分析/测试接口、双端结果页与个人中心展示调整
- admin:Dashboard/Users 与超管 Users 联动后端统计 - api:Dashboard、Analyze、Test、超管 AppUser 逻辑更新 - 微信/抖音小程序:result、profile 页面与样式 - 新增 admin 公共组件与需求文档截图 Made-with: Cursor
This commit is contained in:
1003
admin/src/components/UserDetailDialog.vue
Normal file
1003
admin/src/components/UserDetailDialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,11 +19,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dash-catalog" v-if="testCatalog.length">
|
||||
<div
|
||||
v-for="(row, i) in catalogRows"
|
||||
:key="row.key"
|
||||
class="catalog-card"
|
||||
:style="{ animationDelay: `${120 + i * 40}ms` }"
|
||||
>
|
||||
<div :class="['catalog-icon', row.tone]">
|
||||
<el-icon><component :is="row.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="catalog-body">
|
||||
<div class="catalog-label">{{ row.label }}</div>
|
||||
<div class="catalog-metrics">
|
||||
<span><em>{{ row.records }}</em> 人次</span>
|
||||
<span class="sep">·</span>
|
||||
<span><em>{{ row.uniqueUsers }}</em> 人</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dash-main">
|
||||
<section class="panel panel-chart">
|
||||
<div class="panel-head">
|
||||
<h2 class="panel-title">近 14 日测试趋势</h2>
|
||||
<p class="panel-desc">人脸 · MBTI · PDP · DISC 完成量</p>
|
||||
<p class="panel-desc">人脸 · MBTI · PDP · DISC 完成量;下方为性格结果分布(与本企业测评数据一致)</p>
|
||||
</div>
|
||||
<div v-if="hasDistribution" class="distr-band">
|
||||
<div v-for="block in distributionBlocks" :key="block.key" class="distr-col">
|
||||
<div class="distr-col-head">
|
||||
<el-icon class="distr-head-ic"><component :is="block.icon" /></el-icon>
|
||||
<span>{{ block.title }}</span>
|
||||
</div>
|
||||
<div class="distr-list">
|
||||
<div v-for="it in block.items" :key="block.key + it.label" class="distr-row-line">
|
||||
<span class="distr-lab" :title="it.label">{{ it.label }}</span>
|
||||
<div class="distr-bar-track">
|
||||
<div
|
||||
class="distr-bar-fill"
|
||||
:class="block.barClass"
|
||||
:style="{ width: barWidthPct(block.max, it.count) }"
|
||||
/>
|
||||
</div>
|
||||
<span class="distr-num">{{ it.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<VChart v-if="testTrends.length" class="trend-chart" :option="chartOption" autoresize />
|
||||
@@ -87,7 +129,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { User, Document, TrendCharts } from '@element-plus/icons-vue'
|
||||
import {
|
||||
User,
|
||||
Document,
|
||||
TrendCharts,
|
||||
Camera,
|
||||
Reading,
|
||||
Histogram,
|
||||
Medal
|
||||
} from '@element-plus/icons-vue'
|
||||
import { request } from '@/utils/request'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { use } from 'echarts/core'
|
||||
@@ -125,6 +175,18 @@ const testTrends = ref<
|
||||
Array<{ date: string; face: number; mbti: number; pdp: number; disc: number; total: number }>
|
||||
>([])
|
||||
const topTestUsers = ref<TopUserRow[]>([])
|
||||
const testCatalog = ref<
|
||||
Array<{ key: string; label: string; records: number; uniqueUsers: number }>
|
||||
>([])
|
||||
const distributionMbti = ref<Array<{ label: string; count: number }>>([])
|
||||
const distributionDisc = ref<Array<{ label: string; count: number }>>([])
|
||||
const distributionPdp = ref<Array<{ label: string; count: number }>>([])
|
||||
const faceSubtypeHints = ref<{
|
||||
mbti: Array<{ label: string; count: number }>
|
||||
disc: Array<{ label: string; count: number }>
|
||||
pdp: Array<{ label: string; count: number }>
|
||||
}>({ mbti: [], disc: [], pdp: [] })
|
||||
|
||||
const loading = ref(false)
|
||||
const inviteLoading = ref(false)
|
||||
const inviteQrcode = ref<string>('')
|
||||
@@ -139,6 +201,69 @@ const kpiCards = computed(() => [
|
||||
{ key: 'p', label: '待审核', value: stats.pendingReviews, icon: User, tone: 'orange' }
|
||||
])
|
||||
|
||||
const catalogIconMap: Record<string, { icon: typeof Camera; tone: string }> = {
|
||||
face: { icon: Camera, tone: 'teal' },
|
||||
mbti: { icon: Reading, tone: 'blue' },
|
||||
disc: { icon: Histogram, tone: 'indigo' },
|
||||
pdp: { icon: Medal, tone: 'amber' }
|
||||
}
|
||||
|
||||
const catalogRows = computed(() =>
|
||||
testCatalog.value.map(row => {
|
||||
const m = catalogIconMap[row.key] || { icon: Document, tone: 'blue' }
|
||||
return {
|
||||
...row,
|
||||
icon: m.icon,
|
||||
tone: m.tone,
|
||||
records: row.records ?? 0,
|
||||
uniqueUsers: row.uniqueUsers ?? 0
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
function sliceItems(items: Array<{ label: string; count: number }>, n: number) {
|
||||
return (items || []).slice(0, n)
|
||||
}
|
||||
|
||||
const distributionBlocks = computed(() => {
|
||||
const mbti = sliceItems(distributionMbti.value, 6)
|
||||
const disc = sliceItems(distributionDisc.value, 6)
|
||||
const pdp = sliceItems(distributionPdp.value, 6)
|
||||
const faceMerged: Array<{ label: string; count: number }> = []
|
||||
const fh = faceSubtypeHints.value || { mbti: [], disc: [], pdp: [] }
|
||||
const pushPref = (prefix: string, arr: Array<{ label: string; count: number }>, max: number) => {
|
||||
let k = 0
|
||||
for (const it of arr || []) {
|
||||
if (k >= max) break
|
||||
faceMerged.push({ label: `${prefix}${it.label}`, count: it.count })
|
||||
k++
|
||||
}
|
||||
}
|
||||
pushPref('面·MBTI ', fh.mbti, 2)
|
||||
pushPref('面·DISC ', fh.disc, 2)
|
||||
pushPref('面·PDP ', fh.pdp, 2)
|
||||
|
||||
const blocks = [
|
||||
{ key: 'mbti', title: 'MBTI(答题)', items: mbti, icon: Reading, barClass: 'bar-mbti' },
|
||||
{ key: 'disc', title: 'DISC(答题)', items: disc, icon: Histogram, barClass: 'bar-disc' },
|
||||
{ key: 'pdp', title: 'PDP(答题)', items: pdp, icon: Medal, barClass: 'bar-pdp' },
|
||||
{ key: 'face', title: '面相推测', items: faceMerged, icon: Camera, barClass: 'bar-face' }
|
||||
]
|
||||
return blocks.map(b => ({
|
||||
...b,
|
||||
max: Math.max(1, ...b.items.map(i => i.count))
|
||||
}))
|
||||
})
|
||||
|
||||
const hasDistribution = computed(() =>
|
||||
distributionBlocks.value.some(b => b.items.length > 0)
|
||||
)
|
||||
|
||||
function barWidthPct(max: number, count: number) {
|
||||
if (!max || !count) return '0%'
|
||||
return `${Math.round((count / max) * 100)}%`
|
||||
}
|
||||
|
||||
const chartOption = computed(() => {
|
||||
const dates = testTrends.value.map(d => d.date.slice(5))
|
||||
return {
|
||||
@@ -226,6 +351,25 @@ const loadData = async () => {
|
||||
stats.pendingReviews = response.data.pendingReviews || 0
|
||||
testTrends.value = response.data.testTrends || []
|
||||
topTestUsers.value = Array.isArray(response.data.topTestUsers) ? response.data.topTestUsers : []
|
||||
testCatalog.value = Array.isArray(response.data.testCatalog) ? response.data.testCatalog : []
|
||||
distributionMbti.value = Array.isArray(response.data.distributionMbti)
|
||||
? response.data.distributionMbti
|
||||
: []
|
||||
distributionDisc.value = Array.isArray(response.data.distributionDisc)
|
||||
? response.data.distributionDisc
|
||||
: []
|
||||
distributionPdp.value = Array.isArray(response.data.distributionPdp)
|
||||
? response.data.distributionPdp
|
||||
: []
|
||||
const fh = response.data.faceSubtypeHints
|
||||
faceSubtypeHints.value =
|
||||
fh && typeof fh === 'object'
|
||||
? {
|
||||
mbti: Array.isArray(fh.mbti) ? fh.mbti : [],
|
||||
disc: Array.isArray(fh.disc) ? fh.disc : [],
|
||||
pdp: Array.isArray(fh.pdp) ? fh.pdp : []
|
||||
}
|
||||
: { mbti: [], disc: [], pdp: [] }
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载数据失败:', error)
|
||||
@@ -307,7 +451,94 @@ const loadInviteQrcode = async () => {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dash-catalog {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.catalog-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
animation: dashFadeUp 0.45s ease-out both;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 17px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.teal {
|
||||
background: #f0fdfa;
|
||||
color: #0d9488;
|
||||
}
|
||||
|
||||
&.blue {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
&.indigo {
|
||||
background: #eef2ff;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
&.amber {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.catalog-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.catalog-metrics {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.sep {
|
||||
margin: 0 4px;
|
||||
color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@@ -396,6 +627,97 @@ const loadInviteQrcode = async () => {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.distr-band {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.distr-col {
|
||||
min-width: 0;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.distr-col-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.distr-head-ic {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.distr-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 108px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.distr-row-line {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(36px, 42%) 22px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.distr-lab {
|
||||
color: #4b5563;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.distr-bar-track {
|
||||
height: 5px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.distr-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.35s ease;
|
||||
|
||||
&.bar-mbti {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
&.bar-disc {
|
||||
background: #6366f1;
|
||||
}
|
||||
|
||||
&.bar-pdp {
|
||||
background: #f97316;
|
||||
}
|
||||
|
||||
&.bar-face {
|
||||
background: #14b8a6;
|
||||
}
|
||||
}
|
||||
|
||||
.distr-num {
|
||||
text-align: right;
|
||||
color: #6b7280;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.panel-side {
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
@@ -435,7 +757,7 @@ const loadInviteQrcode = async () => {
|
||||
.trend-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 160px;
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.panel-empty {
|
||||
@@ -524,5 +846,13 @@ const loadInviteQrcode = async () => {
|
||||
.dash-kpis {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.dash-catalog {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.distr-band {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -164,80 +164,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户详情对话框:测试记录 -->
|
||||
<el-dialog
|
||||
<UserDetailDialog
|
||||
v-model="showDetailDialog"
|
||||
title="测试记录"
|
||||
width="55%"
|
||||
destroy-on-close
|
||||
v-loading="detailLoading"
|
||||
>
|
||||
<template v-if="detailUser">
|
||||
<div class="detail-section" v-if="testTableData.length">
|
||||
<el-table :data="paginatedTests" size="small" max-height="500" class="test-table">
|
||||
<el-table-column prop="createdAt" label="时间" width="140">
|
||||
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="testType" label="类型" width="120">
|
||||
<template #default="{ row }">{{ formatTestType(row.testType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="testScope" label="版本" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="row.testScope === 'enterprise' ? 'primary' : 'info'"
|
||||
>
|
||||
{{ row.testScope === 'enterprise' ? '企业版' : '个人版' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="summary" label="结果摘要" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="需付费" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.requiresPayment ? 'warning' : 'info'">
|
||||
{{ row.requiresPayment ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已付费" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.isPaid ? 'success' : 'info'">
|
||||
{{ row.isPaid ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="付款金额" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="payment-cell">
|
||||
{{ row.isPaid ? formatAmount(row.paidAmount) : '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="付款时间" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">
|
||||
{{ row.isPaid ? formatDate(row.paidAt) : '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleViewTest(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="test-pagination" v-if="testTableData.length > testPageSize">
|
||||
<el-pagination
|
||||
v-model:current-page="testPage"
|
||||
:page-size="testPageSize"
|
||||
:total="testTableData.length"
|
||||
layout="prev, pager, next, total"
|
||||
@current-change="handleTestPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
:user="detailUser"
|
||||
:loading="detailLoading"
|
||||
:show-enterprise-match="false"
|
||||
@view-test="handleViewTest"
|
||||
/>
|
||||
|
||||
<!-- 单次测试详情对话框 -->
|
||||
<el-dialog
|
||||
@@ -572,6 +505,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { Download, Search, View } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { request } from '@/utils/request'
|
||||
import UserDetailDialog from '@/components/UserDetailDialog.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
@@ -583,8 +517,6 @@ const searchTerm = ref('')
|
||||
const showDetailDialog = ref(false)
|
||||
const detailUser = ref<Record<string, any> | null>(null)
|
||||
|
||||
const testPage = ref(1)
|
||||
const testPageSize = 10
|
||||
const showTestDetailDialog = ref(false)
|
||||
const currentTest = ref<any | null>(null)
|
||||
|
||||
@@ -594,18 +526,6 @@ const rawTests = computed<any[]>(() => {
|
||||
return (u.testList || []) as any[]
|
||||
})
|
||||
|
||||
const testTableData = computed(() =>
|
||||
rawTests.value.map(t => ({
|
||||
...t,
|
||||
summary: extractTestSummary(t)
|
||||
}))
|
||||
)
|
||||
|
||||
const paginatedTests = computed(() => {
|
||||
const start = (testPage.value - 1) * testPageSize
|
||||
return testTableData.value.slice(start, start + testPageSize)
|
||||
})
|
||||
|
||||
const currentTestType = computed(() => (currentTest.value?.testType || '').toLowerCase())
|
||||
|
||||
const users = ref<any[]>([])
|
||||
@@ -833,10 +753,6 @@ function handlePageChange() {
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
function handleTestPageChange(page: number) {
|
||||
testPage.value = page
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
ElMessage.info('导出功能开发中')
|
||||
}
|
||||
@@ -874,7 +790,6 @@ async function loadDetailUser(userId: number) {
|
||||
}
|
||||
|
||||
async function handleView(row: any) {
|
||||
testPage.value = 1
|
||||
detailUser.value = null
|
||||
showDetailDialog.value = true
|
||||
|
||||
@@ -892,7 +807,6 @@ function handleViewTest(row: any) {
|
||||
|
||||
async function handleClickTestTag(row: any, testType: string) {
|
||||
// 仅加载用户测试记录,弹出单次测试详情对话框并显示加载动画
|
||||
testPage.value = 1
|
||||
currentTest.value = null
|
||||
showTestDetailDialog.value = true
|
||||
testDetailLoading.value = true
|
||||
|
||||
@@ -262,70 +262,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户详情对话框 -->
|
||||
<el-dialog
|
||||
<UserDetailDialog
|
||||
v-model="showDetailDialog"
|
||||
title="测试记录"
|
||||
width="55%"
|
||||
destroy-on-close
|
||||
v-loading="detailLoading"
|
||||
>
|
||||
<template v-if="detailUser">
|
||||
<div class="detail-section" v-if="testTableData.length">
|
||||
<el-table :data="paginatedTests" size="small" max-height="500" class="test-table">
|
||||
<el-table-column prop="createdAt" label="时间" width="140">
|
||||
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="testType" label="类型" width="120">
|
||||
<template #default="{ row }">{{ formatTestType(row.testType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="summary" label="结果摘要" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="需付费" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.requiresPayment ? 'warning' : 'info'">
|
||||
{{ row.requiresPayment ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已付费" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.isPaid ? 'success' : 'info'">
|
||||
{{ row.isPaid ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="付款金额" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="payment-cell">
|
||||
{{ row.isPaid ? formatAmount(row.paidAmount) : '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="付款时间" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">
|
||||
{{ row.isPaid ? formatDate(row.paidAt) : '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleViewTest(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="test-pagination" v-if="testTableData.length > testPageSize">
|
||||
<el-pagination
|
||||
v-model:current-page="testPage"
|
||||
:page-size="testPageSize"
|
||||
:total="testTableData.length"
|
||||
layout="prev, pager, next, total"
|
||||
@current-change="handleTestPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
:user="detailUser"
|
||||
:loading="detailLoading"
|
||||
:show-enterprise-match="true"
|
||||
@view-test="handleViewTest"
|
||||
/>
|
||||
|
||||
<!-- 单次测试详情对话框 -->
|
||||
<el-dialog
|
||||
@@ -654,6 +597,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { Download, Search, View, UserFilled, User, OfficeBuilding, DataLine } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { request } from '@/utils/request'
|
||||
import UserDetailDialog from '@/components/UserDetailDialog.vue'
|
||||
|
||||
withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
|
||||
|
||||
@@ -670,8 +614,6 @@ const selectedMbti = ref('')
|
||||
const showDetailDialog = ref(false)
|
||||
const detailUser = ref<Record<string, any> | null>(null)
|
||||
|
||||
const testPage = ref(1)
|
||||
const testPageSize = 10
|
||||
const showTestDetailDialog = ref(false)
|
||||
const currentTest = ref<any | null>(null)
|
||||
|
||||
@@ -682,18 +624,6 @@ const rawTests = computed<any[]>(() => {
|
||||
return (u.testList || []) as any[]
|
||||
})
|
||||
|
||||
const testTableData = computed(() =>
|
||||
rawTests.value.map(t => ({
|
||||
...t,
|
||||
summary: extractTestSummary(t)
|
||||
}))
|
||||
)
|
||||
|
||||
const paginatedTests = computed(() => {
|
||||
const start = (testPage.value - 1) * testPageSize
|
||||
return testTableData.value.slice(start, start + testPageSize)
|
||||
})
|
||||
|
||||
const currentTestType = computed(() => (currentTest.value?.testType || '').toLowerCase())
|
||||
|
||||
const userCards = ref<Array<{ type: string; name: string; total: number; active: number; tested: number; enterpriseId?: number }>>([
|
||||
@@ -988,10 +918,6 @@ function handlePageChange() {
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
function handleTestPageChange(page: number) {
|
||||
testPage.value = page
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
ElMessage.info('导出功能开发中')
|
||||
}
|
||||
@@ -1042,7 +968,6 @@ async function loadDetailUser(userId: number) {
|
||||
}
|
||||
|
||||
async function handleView(row: any) {
|
||||
testPage.value = 1
|
||||
detailUser.value = null
|
||||
showDetailDialog.value = true
|
||||
|
||||
@@ -1060,7 +985,6 @@ function handleViewTest(row: any) {
|
||||
|
||||
async function handleClickTestTag(row: any, testType: string) {
|
||||
// 仅加载用户测试记录,弹出单次测试详情对话框并显示加载动画
|
||||
testPage.value = 1
|
||||
currentTest.value = null
|
||||
showTestDetailDialog.value = true
|
||||
testDetailLoading.value = true
|
||||
|
||||
Reference in New Issue
Block a user