- 管理端:用户详情、企业看板双小程序码、超管设置与用户列表等 - 后端:订单接口、邀请码企业版+个人版、分析与测试相关调整 - 微信小程序/抖音:自定义 tabBar 居中与拍摄钮上移、相机页与结果页、订单页、支付与统计 - 新增 faceResultDetail 工具与 mp 分析迁移脚本 Made-with: Cursor
999 lines
27 KiB
Vue
999 lines
27 KiB
Vue
<template>
|
||
<el-dialog
|
||
:model-value="modelValue"
|
||
class="user-detail-dialog"
|
||
title="用户详情"
|
||
width="min(1180px, 96vw)"
|
||
top="3vh"
|
||
destroy-on-close
|
||
append-to-body
|
||
@update:model-value="$emit('update:modelValue', $event)"
|
||
>
|
||
<!-- 侧栏 ud-sidebar 与主区 ud-main-pane 之间间距由 .ud-layout 的 gap 控制 -->
|
||
<div v-loading="loading" class="ud-layout">
|
||
<template v-if="user">
|
||
<aside class="ud-sidebar">
|
||
<div class="ud-profile-avatar">
|
||
<img
|
||
v-if="detailAvatarUrl"
|
||
:src="detailAvatarUrl"
|
||
class="ud-profile-avatar__img"
|
||
referrerpolicy="no-referrer"
|
||
/>
|
||
<div v-else class="ud-profile-avatar__letter">{{ avatarLetter }}</div>
|
||
<div class="ud-profile-name">{{ user.username || '未命名用户' }}</div>
|
||
</div>
|
||
<div class="ud-profile-meta">
|
||
<div class="ud-profile-meta__row">
|
||
<el-icon><Key /></el-icon>
|
||
<span>ID {{ user.id }}</span>
|
||
</div>
|
||
<div class="ud-profile-meta__row" v-if="user.phone">
|
||
<el-icon><Phone /></el-icon>
|
||
<span>{{ user.phone }}</span>
|
||
</div>
|
||
<div class="ud-profile-meta__row">
|
||
<el-icon><Calendar /></el-icon>
|
||
<span>{{ formatDate(user.createdAt) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="ud-quick-stats">
|
||
<el-tooltip content="测试次数" placement="top">
|
||
<div class="ud-quick-stats__tile"><el-icon><DataAnalysis /></el-icon>{{ user.testCount ?? 0 }}</div>
|
||
</el-tooltip>
|
||
<el-tooltip content="MBTI" placement="top">
|
||
<div class="ud-quick-stats__tile"><el-icon><Aim /></el-icon>{{ shortOrDash(user.mbtiType) }}</div>
|
||
</el-tooltip>
|
||
<el-tooltip content="PDP" placement="top">
|
||
<div class="ud-quick-stats__tile"><el-icon><TrendCharts /></el-icon>{{ shortOrDash(user.pdpType, 4) }}</div>
|
||
</el-tooltip>
|
||
<el-tooltip content="DISC" placement="top">
|
||
<div class="ud-quick-stats__tile"><el-icon><PieChart /></el-icon>{{ shortOrDash(user.discType, 3) }}</div>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="ud-dimension-tags" v-if="profileTags.length">
|
||
<div class="ud-dimension-tags__title">维度标签</div>
|
||
<el-tag v-for="t in profileTags" :key="t" size="small" class="ud-dimension-tags__item">{{ t }}</el-tag>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="ud-main-pane">
|
||
<el-tabs v-model="udTab" class="ud-tabs">
|
||
<el-tab-pane label="分析结果" name="analysis">
|
||
<div class="ud-main-scroll">
|
||
<div class="ud-chart-row">
|
||
<div class="ud-chart-panel">
|
||
<div class="ud-chart-heading"><el-icon><Aim /></el-icon> MBTI</div>
|
||
<VChart v-if="mbtiRadarOption" class="ud-chart-echart" :option="mbtiRadarOption" autoresize />
|
||
<div v-else class="ud-chart-placeholder">暂无 MBTI 测评</div>
|
||
<div v-if="mbtiSummary" class="ud-chart-footnote">{{ mbtiSummary }}</div>
|
||
</div>
|
||
<div class="ud-chart-panel">
|
||
<div class="ud-chart-heading"><el-icon><TrendCharts /></el-icon> PDP</div>
|
||
<VChart v-if="pdpRadarOption" class="ud-chart-echart" :option="pdpRadarOption" autoresize />
|
||
<div v-else class="ud-chart-placeholder">暂无 PDP 测评</div>
|
||
</div>
|
||
<div class="ud-chart-panel">
|
||
<div class="ud-chart-heading"><el-icon><PieChart /></el-icon> DISC</div>
|
||
<VChart v-if="discRadarOption" class="ud-chart-echart" :option="discRadarOption" autoresize />
|
||
<div v-else class="ud-chart-placeholder">暂无 DISC 测评</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ud-dual-cards">
|
||
<div class="ud-panel-card">
|
||
<div class="ud-panel-card__head"><el-icon><Star /></el-icon> 盖洛普优势</div>
|
||
<div v-if="gallupList.length" class="ud-gallup-list">
|
||
<div v-for="(g, idx) in gallupList" :key="idx" class="ud-gallup-list__item">
|
||
<span class="ud-gallup-list__index">{{ Number(idx) + 1 }}</span>
|
||
<span class="ud-gallup-list__text">{{ g }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-else class="ud-empty-hint">暂无盖洛普数据(深度报告解锁后可见)</div>
|
||
</div>
|
||
<div class="ud-panel-card ud-panel-card--wide">
|
||
<div class="ud-panel-card__head"><el-icon><OfficeBuilding /></el-icon> 岗位匹配参考</div>
|
||
<div class="ud-rolefit-list">
|
||
<div v-for="r in roleFitList" :key="r.name" class="ud-rolefit-list__row">
|
||
<span class="ud-rolefit-list__name">{{ r.name }}</span>
|
||
<el-progress :percentage="r.pct" :stroke-width="6" :show-text="false" />
|
||
<span class="ud-rolefit-list__percent">{{ r.pct }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="showEnterpriseMatch" class="ud-panel-card ud-enterprise-match">
|
||
<div class="ud-panel-card__head">
|
||
<el-icon><Connection /></el-icon> 推荐匹配企业
|
||
<span class="ud-panel-card__hint">按登记企业测评池与您维度同质度排序</span>
|
||
</div>
|
||
<div v-if="matchingEnterprises.length" class="ud-enterprise-match__list">
|
||
<div v-for="ent in matchingEnterprises" :key="ent.id" class="ud-enterprise-match__item">
|
||
<div class="ud-enterprise-match__body">
|
||
<div class="ud-enterprise-match__name">{{ ent.name }}</div>
|
||
<div class="ud-enterprise-match__meta">{{ ent.matchTypeLabel }} · 匹配度 {{ ent.matchScore }}%</div>
|
||
<div class="ud-enterprise-match__desc">{{ ent.matchReason }}</div>
|
||
<div v-if="ent.contactName || ent.contactPhone" class="ud-enterprise-match__contact">
|
||
<el-icon><User /></el-icon>
|
||
{{ ent.contactName || '负责人' }}
|
||
<span v-if="ent.contactPhone" class="ud-enterprise-match__phone">{{ ent.contactPhone }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="ud-empty-hint">暂无企业数据</div>
|
||
</div>
|
||
<div v-else class="ud-empty-hint ud-enterprise-match__hint">
|
||
跨企业人才匹配与负责人直连请在「超级管理后台 → 用户总览」查看。
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="测试记录" name="tests">
|
||
<el-table v-if="testTableData.length" :data="paginatedTests" size="small" max-height="360" class="ud-test-records-table">
|
||
<el-table-column width="52" align="center">
|
||
<template #default="{ row }">
|
||
<el-icon class="ud-test-type-icon" :class="testIconClass(row.testType)"><component :is="testIcon(row.testType)" /></el-icon>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createdAt" label="时间" width="108">
|
||
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="testType" label="类型" width="100">
|
||
<template #default="{ row }">{{ formatTestType(row.testType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="hasTestScopeCol" prop="testScope" label="版本" width="78" 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="120" show-overflow-tooltip />
|
||
<el-table-column label="付费" width="120" 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="72" align="center">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" size="small" @click="$emit('view-test', row)">详情</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div v-else class="ud-empty-hint">暂无测试记录</div>
|
||
<div class="ud-test-records-pagination" v-if="testTableData.length > testPageSize">
|
||
<el-pagination
|
||
v-model:current-page="testPage"
|
||
:page-size="testPageSize"
|
||
:total="testTableData.length"
|
||
layout="prev, pager, next, total"
|
||
small
|
||
/>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="人像相册" name="photos">
|
||
<div v-if="facePhotos.length" class="ud-photo-gallery">
|
||
<el-image
|
||
v-for="(url, idx) in facePhotos"
|
||
:key="url + idx"
|
||
:src="url"
|
||
fit="cover"
|
||
class="ud-photo-gallery__thumb"
|
||
:preview-src-list="facePhotos"
|
||
/>
|
||
</div>
|
||
<div v-else class="ud-empty-hint">暂无人脸分析照片</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</main>
|
||
</template>
|
||
</div>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import {
|
||
Aim,
|
||
Calendar,
|
||
Connection,
|
||
DataAnalysis,
|
||
DataLine,
|
||
Document,
|
||
Key,
|
||
OfficeBuilding,
|
||
Phone,
|
||
PieChart,
|
||
Picture,
|
||
Star,
|
||
TrendCharts,
|
||
User
|
||
} from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { use } from 'echarts/core'
|
||
import { CanvasRenderer } from 'echarts/renderers'
|
||
import { RadarChart } from 'echarts/charts'
|
||
import {
|
||
GridComponent,
|
||
TooltipComponent,
|
||
LegendComponent,
|
||
RadarComponent
|
||
} from 'echarts/components'
|
||
import VChart from 'vue-echarts'
|
||
|
||
use([CanvasRenderer, RadarChart, GridComponent, TooltipComponent, LegendComponent, RadarComponent])
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
modelValue: boolean
|
||
user: Record<string, any> | null
|
||
loading?: boolean
|
||
showEnterpriseMatch?: boolean
|
||
}>(),
|
||
{ loading: false, showEnterpriseMatch: false }
|
||
)
|
||
|
||
defineEmits<{
|
||
'update:modelValue': [v: boolean]
|
||
'view-test': [row: any]
|
||
}>()
|
||
|
||
const udTab = ref('analysis')
|
||
const testPage = ref(1)
|
||
const testPageSize = 8
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
v => {
|
||
if (v) {
|
||
udTab.value = 'analysis'
|
||
testPage.value = 1
|
||
}
|
||
}
|
||
)
|
||
|
||
watch(
|
||
() => props.user?.id,
|
||
() => {
|
||
testPage.value = 1
|
||
}
|
||
)
|
||
|
||
const avatarLetter = computed(() => {
|
||
const n = (props.user?.username || props.user?.nickname || '?').trim()
|
||
return (n.charAt(0) || '?').toUpperCase()
|
||
})
|
||
|
||
const detailAvatarUrl = computed(() => {
|
||
const u = props.user as Record<string, any> | null | undefined
|
||
if (!u) return ''
|
||
const a = u.avatar ?? u.avatarUrl ?? ''
|
||
return typeof a === 'string' ? a.trim() : ''
|
||
})
|
||
|
||
const rawTests = computed(() => (props.user?.testList || []) as any[])
|
||
|
||
const hasTestScopeCol = computed(() => rawTests.value.some(t => t.testScope != null))
|
||
|
||
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 matchingEnterprises = computed(() => {
|
||
const m = props.user?.matchingEnterprises
|
||
return Array.isArray(m) ? m : []
|
||
})
|
||
|
||
function shortOrDash(s: string | undefined, max = 6) {
|
||
if (!s) return '—'
|
||
return s.length > max ? s.slice(0, max) + '…' : s
|
||
}
|
||
|
||
function formatDate(date: number | string | null | undefined) {
|
||
if (date == null) return '-'
|
||
if (typeof date === 'number') {
|
||
const d = new Date(date * 1000)
|
||
return (
|
||
d.getFullYear() +
|
||
'-' +
|
||
String(d.getMonth() + 1).padStart(2, '0') +
|
||
'-' +
|
||
String(d.getDate()).padStart(2, '0')
|
||
)
|
||
}
|
||
return String(date)
|
||
}
|
||
|
||
function formatTestType(testType: string) {
|
||
const t = (testType || '').toLowerCase()
|
||
if (!t) return '-'
|
||
if (t === 'mbti') return 'MBTI'
|
||
if (t === 'disc') return 'DISC'
|
||
if (t === 'pdp') return 'PDP'
|
||
if (t === 'face' || t === 'ai') return '人脸'
|
||
if (t === 'resume') return '简历'
|
||
return testType
|
||
}
|
||
|
||
function extractTestSummary(test: any): string {
|
||
const raw = test?.result
|
||
if (typeof raw !== 'string' || !raw) return ''
|
||
let data: any
|
||
try {
|
||
data = JSON.parse(raw)
|
||
} catch {
|
||
return raw
|
||
}
|
||
if (!data || typeof data !== 'object') return raw
|
||
const type = (test?.testType || '').toLowerCase()
|
||
if (type === 'mbti') return String(data.mbtiType ?? data.type ?? data.result ?? '')
|
||
if (type === 'disc') {
|
||
const desc = data.description?.type
|
||
if (typeof desc === 'string' && desc) return desc
|
||
if (data.dominantType) return String(data.dominantType) + '型'
|
||
return String(data.disc ?? '')
|
||
}
|
||
if (type === 'pdp') {
|
||
const desc = data.description?.type
|
||
if (typeof desc === 'string' && desc) return desc
|
||
if (data.dominantType) return String(data.dominantType)
|
||
return String(data.pdp ?? '')
|
||
}
|
||
if (type === 'face' || type === 'ai') return '人脸分析'
|
||
if (type === 'resume') {
|
||
const c = String(data.content ?? '')
|
||
return c ? c.substring(0, 24).replace(/\n/g, ' ') + (c.length > 24 ? '…' : '') : '简历'
|
||
}
|
||
return String(data.type ?? data.result ?? '')
|
||
}
|
||
|
||
function parseTestResult(test: any): any {
|
||
const raw = test?.result
|
||
if (typeof raw !== 'string' || !raw) return null
|
||
try {
|
||
const data = JSON.parse(raw)
|
||
return data && typeof data === 'object' ? data : null
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
function latestTest(type: string) {
|
||
const t = type.toLowerCase()
|
||
for (const r of rawTests.value) {
|
||
if ((r.testType || '').toLowerCase() === t) return r
|
||
}
|
||
return null
|
||
}
|
||
|
||
const latestMbti = computed(() => parseTestResult(latestTest('mbti')))
|
||
const latestPdp = computed(() => parseTestResult(latestTest('pdp')))
|
||
const latestDisc = computed(() => parseTestResult(latestTest('disc')))
|
||
const latestFace = computed(() => parseTestResult(latestTest('face')) || parseTestResult(latestTest('ai')))
|
||
const latestResume = computed(() => parseTestResult(latestTest('resume')))
|
||
|
||
const mbtiSummary = computed(() => {
|
||
const p = latestMbti.value
|
||
if (!p) return ''
|
||
const type = String(p.mbtiType ?? p.description?.type ?? p.type ?? '')
|
||
const name = String(p.description?.name ?? '')
|
||
return [type, name].filter(Boolean).join(' · ')
|
||
})
|
||
|
||
const profileTags = computed(() => {
|
||
const u = props.user || {}
|
||
const tags: string[] = []
|
||
if (u.mbtiType) tags.push('MBTI-' + u.mbtiType)
|
||
if (u.pdpType) tags.push('PDP-' + u.pdpType)
|
||
if (u.discType) tags.push('DISC-' + u.discType)
|
||
if (u.faceMbtiType) tags.push('面相MBTI')
|
||
return tags
|
||
})
|
||
|
||
const gallupList = computed(() => {
|
||
const face = latestFace.value
|
||
if (face?.gallupTop3 && Array.isArray(face.gallupTop3)) return face.gallupTop3.slice(0, 5)
|
||
const res = latestResume.value
|
||
if (res?.gallupTop3 && Array.isArray(res.gallupTop3)) return res.gallupTop3.slice(0, 5)
|
||
const m = latestMbti.value
|
||
const st = m?.description?.strengths
|
||
if (Array.isArray(st) && st.length) return st.slice(0, 3)
|
||
return []
|
||
})
|
||
|
||
function mbtiLetterScores(typeStr: string): number[] {
|
||
const order = [
|
||
['E', 'I'],
|
||
['S', 'N'],
|
||
['T', 'F'],
|
||
['J', 'P']
|
||
]
|
||
const t = typeStr.toUpperCase().replace(/[^A-Z]/g, '')
|
||
if (t.length < 4) return [50, 50, 50, 50]
|
||
const out: number[] = []
|
||
for (let i = 0; i < 4; i++) {
|
||
const ch = t[i]
|
||
const [a, b] = order[i]
|
||
out.push(ch === a ? 78 : ch === b ? 72 : 55)
|
||
}
|
||
return out
|
||
}
|
||
|
||
const mbtiRadarOption = computed(() => {
|
||
const p = latestMbti.value
|
||
const typeStr = String(p?.mbtiType ?? p?.description?.type ?? p?.type ?? props.user?.mbtiType ?? '')
|
||
if (!typeStr || typeStr.length < 2) return null
|
||
const vals = mbtiLetterScores(typeStr)
|
||
return {
|
||
color: ['#7c3aed'],
|
||
radar: {
|
||
indicator: [
|
||
{ name: 'E 能量', max: 100 },
|
||
{ name: 'N 信息', max: 100 },
|
||
{ name: 'T 决策', max: 100 },
|
||
{ name: 'J 生活方式', max: 100 }
|
||
],
|
||
radius: 58,
|
||
splitNumber: 4,
|
||
axisName: { fontSize: 10, color: '#6b7280' }
|
||
},
|
||
series: [
|
||
{
|
||
type: 'radar',
|
||
data: [{ value: vals, name: 'MBTI' }],
|
||
areaStyle: { opacity: 0.12 }
|
||
}
|
||
],
|
||
tooltip: { trigger: 'item' }
|
||
}
|
||
})
|
||
|
||
const PDP_KEYS = ['Tiger', 'Peacock', 'Owl', 'Koala', 'Chameleon'] as const
|
||
const PDP_LABELS: Record<string, string> = {
|
||
Tiger: '虎',
|
||
Peacock: '孔雀',
|
||
Owl: '猫头鹰',
|
||
Koala: '考拉',
|
||
Chameleon: '变色龙'
|
||
}
|
||
|
||
const pdpRadarOption = computed(() => {
|
||
const p = latestPdp.value
|
||
if (!p) return null
|
||
const pct = p.percentages || {}
|
||
const vals = PDP_KEYS.map(k => Number(pct[k] ?? pct[k.toLowerCase()] ?? 0) || 0)
|
||
if (vals.every(v => v === 0)) {
|
||
const dom = String(p.dominantType ?? p.description?.type ?? '')
|
||
if (!dom) return null
|
||
const idx = ['老虎', '孔雀', '猫头鹰', '考拉', '变色龙'].findIndex(x => dom.includes(x))
|
||
if (idx < 0) return null
|
||
const v2 = [15, 15, 15, 15, 15]
|
||
v2[idx] = 55
|
||
return buildPdpChart(v2)
|
||
}
|
||
return buildPdpChart(vals)
|
||
})
|
||
|
||
function buildPdpChart(values: number[]) {
|
||
const indicators = PDP_KEYS.map(k => ({ name: PDP_LABELS[k] || k, max: 100 }))
|
||
return {
|
||
color: ['#d97706'],
|
||
radar: {
|
||
indicator: indicators,
|
||
radius: 58,
|
||
axisName: { fontSize: 10, color: '#6b7280' }
|
||
},
|
||
series: [{ type: 'radar', data: [{ value: values, name: 'PDP' }], areaStyle: { opacity: 0.1 } }],
|
||
tooltip: { trigger: 'item' }
|
||
}
|
||
}
|
||
|
||
const discRadarOption = computed(() => {
|
||
const p = latestDisc.value
|
||
if (!p) return null
|
||
const pct = p.percentages || {}
|
||
const d = Number(pct.D ?? pct.d ?? 0)
|
||
const i = Number(pct.I ?? pct.i ?? 0)
|
||
const s = Number(pct.S ?? pct.s ?? 0)
|
||
const c = Number(pct.C ?? pct.c ?? 0)
|
||
if (d + i + s + c < 1) return null
|
||
return {
|
||
color: ['#2563eb'],
|
||
radar: {
|
||
indicator: [
|
||
{ name: 'D', max: 100 },
|
||
{ name: 'I', max: 100 },
|
||
{ name: 'S', max: 100 },
|
||
{ name: 'C', max: 100 }
|
||
],
|
||
radius: 58,
|
||
axisName: { fontSize: 11, color: '#6b7280' }
|
||
},
|
||
series: [{ type: 'radar', data: [{ value: [d, i, s, c], name: 'DISC' }], areaStyle: { opacity: 0.1 } }],
|
||
tooltip: { trigger: 'item' }
|
||
}
|
||
})
|
||
|
||
const roleFitList = computed(() => {
|
||
const res = latestResume.value
|
||
const fromResume = res?.hrView?.roleRecommend?.bestFit
|
||
let names: string[] = []
|
||
if (Array.isArray(fromResume) && fromResume.length) {
|
||
names = fromResume.slice(0, 6)
|
||
} else {
|
||
const careers = latestMbti.value?.description?.careers
|
||
if (Array.isArray(careers) && careers.length) {
|
||
names = careers.slice(0, 6)
|
||
} else {
|
||
names = ['增长', '用户运营', '内容运营', '产品', '研发', '设计']
|
||
}
|
||
}
|
||
return names.map((name, i) => ({
|
||
name,
|
||
pct: 42 + ((i * 17 + (props.user?.id || 0)) % 37)
|
||
}))
|
||
})
|
||
|
||
const facePhotos = computed(() => {
|
||
const f = latestFace.value
|
||
const urls = f?.photoUrls
|
||
return Array.isArray(urls) ? urls : []
|
||
})
|
||
|
||
function testIcon(testType: string) {
|
||
const t = (testType || '').toLowerCase()
|
||
if (t === 'mbti') return Aim
|
||
if (t === 'pdp') return TrendCharts
|
||
if (t === 'disc') return PieChart
|
||
if (t === 'face' || t === 'ai') return Picture
|
||
if (t === 'resume') return Document
|
||
return DataLine
|
||
}
|
||
|
||
function testIconClass(testType: string) {
|
||
const t = (testType || '').toLowerCase()
|
||
if (t === 'mbti') return 'tic-mbti'
|
||
if (t === 'pdp') return 'tic-pdp'
|
||
if (t === 'disc') return 'tic-disc'
|
||
if (t === 'face' || t === 'ai') return 'tic-face'
|
||
return 'tic-other'
|
||
}
|
||
|
||
function copyText(text: string, msg: string) {
|
||
if (!text) return
|
||
void navigator.clipboard.writeText(text).then(
|
||
() => ElMessage.success(msg),
|
||
() => ElMessage.error('复制失败')
|
||
)
|
||
}
|
||
|
||
function openDial(phone: string) {
|
||
if (!phone) return
|
||
window.location.href = 'tel:' + phone
|
||
}
|
||
|
||
function openMail(email: string) {
|
||
if (!email) return
|
||
window.location.href = 'mailto:' + email
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.user-detail-dialog {
|
||
:deep(.el-dialog__body) {
|
||
padding: 8px 16px 16px;
|
||
max-height: 86vh;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
|
||
/* 整行布局:侧栏与主区之间保留视觉空隙(与设计稿一致约 16~24px) */
|
||
.ud-layout {
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 20px;
|
||
min-height: 200px;
|
||
max-height: 78vh;
|
||
}
|
||
|
||
.ud-sidebar {
|
||
width: 200px;
|
||
flex-shrink: 0;
|
||
background: linear-gradient(180deg, #faf5ff 0%, #fff 40%);
|
||
border: 1px solid #ede9fe;
|
||
border-radius: 10px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.ud-profile-avatar {
|
||
text-align: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.ud-profile-avatar__img {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.ud-profile-avatar__letter {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 50%;
|
||
background: #7c3aed;
|
||
color: #fff;
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ud-profile-name {
|
||
margin-top: 6px;
|
||
font-weight: 700;
|
||
font-size: 14px;
|
||
color: #111827;
|
||
}
|
||
|
||
.ud-profile-meta__row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: #4b5563;
|
||
margin-bottom: 6px;
|
||
|
||
.el-icon {
|
||
color: #a855f7;
|
||
}
|
||
}
|
||
|
||
.ud-quick-stats {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 6px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.ud-quick-stats__tile {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 6px 8px;
|
||
font-size: 11px;
|
||
color: #374151;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
border: 1px solid #f3e8ff;
|
||
|
||
.el-icon {
|
||
color: #7c3aed;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.ud-dimension-tags {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.ud-dimension-tags__title {
|
||
font-size: 11px;
|
||
color: #9ca3af;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.ud-dimension-tags__item {
|
||
margin: 0 4px 4px 0;
|
||
}
|
||
|
||
.ud-main-pane {
|
||
flex: 1;
|
||
min-width: 0;
|
||
border: 1px solid #f3f4f6;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.ud-tabs {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
|
||
:deep(.el-tabs__content) {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.el-tab-pane) {
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.ud-main-scroll {
|
||
max-height: calc(78vh - 120px);
|
||
overflow-y: auto;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.ud-chart-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.ud-chart-panel {
|
||
background: #fafafa;
|
||
border-radius: 8px;
|
||
padding: 6px 4px 4px;
|
||
text-align: center;
|
||
}
|
||
|
||
.ud-chart-heading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
margin-bottom: 2px;
|
||
|
||
.el-icon {
|
||
color: #7c3aed;
|
||
}
|
||
}
|
||
|
||
.ud-chart-echart {
|
||
height: 150px;
|
||
width: 100%;
|
||
}
|
||
|
||
.ud-chart-placeholder {
|
||
height: 120px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.ud-chart-footnote {
|
||
font-size: 11px;
|
||
color: #6b7280;
|
||
padding: 0 4px 4px;
|
||
}
|
||
|
||
.ud-dual-cards {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.ud-panel-card {
|
||
background: #fff;
|
||
border: 1px solid #f3f4f6;
|
||
border-radius: 8px;
|
||
padding: 8px 10px;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.ud-panel-card--wide {
|
||
flex: 1.2;
|
||
}
|
||
|
||
.ud-panel-card__head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
margin-bottom: 8px;
|
||
|
||
.el-icon {
|
||
color: #7c3aed;
|
||
}
|
||
}
|
||
|
||
.ud-panel-card__hint {
|
||
font-weight: 400;
|
||
font-size: 11px;
|
||
color: #9ca3af;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.ud-gallup-list__item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.ud-gallup-list__index {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 4px;
|
||
background: #ede9fe;
|
||
color: #5b21b6;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ud-rolefit-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.ud-rolefit-list__row {
|
||
display: grid;
|
||
grid-template-columns: 72px 1fr 36px;
|
||
gap: 8px;
|
||
align-items: center;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.ud-rolefit-list__name {
|
||
color: #4b5563;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ud-rolefit-list__percent {
|
||
text-align: right;
|
||
color: #7c3aed;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ud-enterprise-match {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.ud-enterprise-match__list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.ud-enterprise-match__item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
align-items: flex-start;
|
||
padding: 8px;
|
||
background: #fafafa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.ud-enterprise-match__name {
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
color: #111827;
|
||
}
|
||
|
||
.ud-enterprise-match__meta {
|
||
font-size: 11px;
|
||
color: #7c3aed;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.ud-enterprise-match__desc {
|
||
font-size: 11px;
|
||
color: #6b7280;
|
||
margin-top: 4px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.ud-enterprise-match__contact {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: #374151;
|
||
|
||
.el-icon {
|
||
color: #9ca3af;
|
||
}
|
||
}
|
||
|
||
.ud-enterprise-match__phone {
|
||
font-family: ui-monospace, monospace;
|
||
color: #111827;
|
||
}
|
||
|
||
.ud-enterprise-match__actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.ud-empty-hint {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.ud-enterprise-match__hint {
|
||
margin-top: 8px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.ud-test-records-table {
|
||
width: 100%;
|
||
}
|
||
|
||
.ud-test-type-icon {
|
||
font-size: 18px;
|
||
}
|
||
.tic-mbti {
|
||
color: #4f46e5;
|
||
}
|
||
.tic-pdp {
|
||
color: #d97706;
|
||
}
|
||
.tic-disc {
|
||
color: #2563eb;
|
||
}
|
||
.tic-face {
|
||
color: #059669;
|
||
}
|
||
.tic-other {
|
||
color: #6b7280;
|
||
}
|
||
|
||
.ud-test-records-pagination {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ud-photo-gallery {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.ud-photo-gallery__thumb {
|
||
width: 100px;
|
||
height: 100px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.ud-layout {
|
||
flex-direction: column;
|
||
max-height: none;
|
||
}
|
||
.ud-sidebar {
|
||
width: 100%;
|
||
}
|
||
.ud-chart-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|