feat: 管理端与用户端批量更新(订单/邀请双码/tabBar/审核与分销等)
- 管理端:用户详情、企业看板双小程序码、超管设置与用户列表等 - 后端:订单接口、邀请码企业版+个人版、分析与测试相关调整 - 微信小程序/抖音:自定义 tabBar 居中与拍摄钮上移、相机页与结果页、订单页、支付与统计 - 新增 faceResultDetail 工具与 mp 分析迁移脚本 Made-with: Cursor
This commit is contained in:
@@ -9,142 +9,132 @@
|
||||
append-to-body
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<div v-loading="loading" class="ud-wrap">
|
||||
<!-- 侧栏 ud-sidebar 与主区 ud-main-pane 之间间距由 .ud-layout 的 gap 控制 -->
|
||||
<div v-loading="loading" class="ud-layout">
|
||||
<template v-if="user">
|
||||
<aside class="ud-side">
|
||||
<div class="ud-avatar-block">
|
||||
<img v-if="user.avatar" :src="user.avatar" class="ud-avatar-img" referrerpolicy="no-referrer" />
|
||||
<div v-else class="ud-avatar-letter">{{ avatarLetter }}</div>
|
||||
<div class="ud-name">{{ user.username || '未命名用户' }}</div>
|
||||
<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-meta">
|
||||
<div class="ud-meta-row">
|
||||
<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-meta-row" v-if="user.phone">
|
||||
<div class="ud-profile-meta__row" v-if="user.phone">
|
||||
<el-icon><Phone /></el-icon>
|
||||
<span>{{ user.phone }}</span>
|
||||
</div>
|
||||
<div class="ud-meta-row">
|
||||
<div class="ud-profile-meta__row">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
<span>{{ formatDate(user.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ud-stat-icons">
|
||||
<div class="ud-quick-stats">
|
||||
<el-tooltip content="测试次数" placement="top">
|
||||
<div class="ud-stat-ic"><el-icon><DataAnalysis /></el-icon>{{ user.testCount ?? 0 }}</div>
|
||||
<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-stat-ic"><el-icon><Aim /></el-icon>{{ shortOrDash(user.mbtiType) }}</div>
|
||||
<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-stat-ic"><el-icon><TrendCharts /></el-icon>{{ shortOrDash(user.pdpType, 4) }}</div>
|
||||
<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-stat-ic"><el-icon><PieChart /></el-icon>{{ shortOrDash(user.discType, 3) }}</div>
|
||||
<div class="ud-quick-stats__tile"><el-icon><PieChart /></el-icon>{{ shortOrDash(user.discType, 3) }}</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="ud-tags" v-if="profileTags.length">
|
||||
<div class="ud-tags-title">维度标签</div>
|
||||
<el-tag v-for="t in profileTags" :key="t" size="small" class="ud-tag">{{ t }}</el-tag>
|
||||
<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">
|
||||
<main class="ud-main-pane">
|
||||
<el-tabs v-model="udTab" class="ud-tabs">
|
||||
<el-tab-pane label="分析结果" name="analysis">
|
||||
<div class="ud-scroll">
|
||||
<div class="ud-radar-row">
|
||||
<div class="ud-radar-cell">
|
||||
<div class="ud-radar-title"><el-icon><Aim /></el-icon> MBTI</div>
|
||||
<VChart v-if="mbtiRadarOption" class="ud-chart" :option="mbtiRadarOption" autoresize />
|
||||
<div v-else class="ud-chart-empty">暂无 MBTI 测评</div>
|
||||
<div v-if="mbtiSummary" class="ud-mini-type">{{ mbtiSummary }}</div>
|
||||
<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-radar-cell">
|
||||
<div class="ud-radar-title"><el-icon><TrendCharts /></el-icon> PDP</div>
|
||||
<VChart v-if="pdpRadarOption" class="ud-chart" :option="pdpRadarOption" autoresize />
|
||||
<div v-else class="ud-chart-empty">暂无 PDP 测评</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-radar-cell">
|
||||
<div class="ud-radar-title"><el-icon><PieChart /></el-icon> DISC</div>
|
||||
<VChart v-if="discRadarOption" class="ud-chart" :option="discRadarOption" autoresize />
|
||||
<div v-else class="ud-chart-empty">暂无 DISC 测评</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-row2">
|
||||
<div class="ud-card">
|
||||
<div class="ud-card-h"><el-icon><Star /></el-icon> 盖洛普优势</div>
|
||||
<div v-if="gallupList.length" class="ud-gallup">
|
||||
<div v-for="(g, i) in gallupList" :key="i" class="ud-gallup-item">
|
||||
<span class="ud-gi-n">{{ i + 1 }}</span>
|
||||
<span class="ud-gi-t">{{ g }}</span>
|
||||
<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-muted">暂无盖洛普数据(深度报告解锁后可见)</div>
|
||||
<div v-else class="ud-empty-hint">暂无盖洛普数据(深度报告解锁后可见)</div>
|
||||
</div>
|
||||
<div class="ud-card ud-grow">
|
||||
<div class="ud-card-h"><el-icon><OfficeBuilding /></el-icon> 岗位匹配参考</div>
|
||||
<div class="ud-roles">
|
||||
<div v-for="r in roleFitList" :key="r.name" class="ud-role">
|
||||
<span class="ud-role-n">{{ r.name }}</span>
|
||||
<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-role-p">{{ r.pct }}%</span>
|
||||
<span class="ud-rolefit-list__percent">{{ r.pct }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showEnterpriseMatch" class="ud-card ud-match-card">
|
||||
<div class="ud-card-h">
|
||||
<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-hint">按登记企业测评池与您维度同质度排序</span>
|
||||
<span class="ud-panel-card__hint">按登记企业测评池与您维度同质度排序</span>
|
||||
</div>
|
||||
<div v-if="matchingEnterprises.length" class="ud-match-list">
|
||||
<div v-for="ent in matchingEnterprises" :key="ent.id" class="ud-match-row">
|
||||
<div class="ud-m-info">
|
||||
<div class="ud-m-name">{{ ent.name }}</div>
|
||||
<div class="ud-m-sub">{{ ent.matchTypeLabel }} · 匹配度 {{ ent.matchScore }}%</div>
|
||||
<div class="ud-m-reason">{{ ent.matchReason }}</div>
|
||||
<div v-if="ent.contactName || ent.contactPhone" class="ud-m-contact">
|
||||
<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-m-phone">{{ ent.contactPhone }}</span>
|
||||
<span v-if="ent.contactPhone" class="ud-enterprise-match__phone">{{ ent.contactPhone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ud-m-actions">
|
||||
<el-button
|
||||
v-if="ent.contactPhone"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openDial(ent.contactPhone)"
|
||||
>
|
||||
<el-icon><Phone /></el-icon>
|
||||
</el-button>
|
||||
<el-button v-if="ent.contactPhone" size="small" @click="copyText(ent.contactPhone, '电话已复制')">
|
||||
复制电话
|
||||
</el-button>
|
||||
<el-button v-if="ent.contactEmail" size="small" @click="openMail(ent.contactEmail)">
|
||||
邮件
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="ud-muted">暂无企业数据</div>
|
||||
<div v-else class="ud-empty-hint">暂无企业数据</div>
|
||||
</div>
|
||||
<div v-else class="ud-muted ud-enterprise-hint">
|
||||
<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-tb">
|
||||
<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-ticon" :class="testIconClass(row.testType)"><component :is="testIcon(row.testType)" /></el-icon>
|
||||
<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">
|
||||
@@ -172,8 +162,8 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-else class="ud-muted">暂无测试记录</div>
|
||||
<div class="ud-pager" v-if="testTableData.length > testPageSize">
|
||||
<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"
|
||||
@@ -185,17 +175,17 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="人像相册" name="photos">
|
||||
<div v-if="facePhotos.length" class="ud-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-ph"
|
||||
class="ud-photo-gallery__thumb"
|
||||
:preview-src-list="facePhotos"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="ud-muted">暂无人脸分析照片</div>
|
||||
<div v-else class="ud-empty-hint">暂无人脸分析照片</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</main>
|
||||
@@ -277,6 +267,13 @@ const avatarLetter = computed(() => {
|
||||
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))
|
||||
@@ -601,18 +598,16 @@ function openMail(email: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.ud-wrap {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.ud-grid {
|
||||
/* 整行布局:侧栏与主区之间保留视觉空隙(与设计稿一致约 16~24px) */
|
||||
.ud-layout {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
min-height: 200px;
|
||||
max-height: 78vh;
|
||||
}
|
||||
|
||||
.ud-side {
|
||||
.ud-sidebar {
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(180deg, #faf5ff 0%, #fff 40%);
|
||||
@@ -621,19 +616,19 @@ function openMail(email: string) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ud-avatar-block {
|
||||
.ud-profile-avatar {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ud-avatar-img {
|
||||
.ud-profile-avatar__img {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.ud-avatar-letter {
|
||||
.ud-profile-avatar__letter {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
@@ -646,14 +641,14 @@ function openMail(email: string) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ud-name {
|
||||
.ud-profile-name {
|
||||
margin-top: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.ud-meta-row {
|
||||
.ud-profile-meta__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@@ -666,14 +661,14 @@ function openMail(email: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.ud-stat-icons {
|
||||
.ud-quick-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ud-stat-ic {
|
||||
.ud-quick-stats__tile {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
@@ -690,21 +685,21 @@ function openMail(email: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.ud-tags {
|
||||
.ud-dimension-tags {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.ud-tags-title {
|
||||
.ud-dimension-tags__title {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.ud-tag {
|
||||
.ud-dimension-tags__item {
|
||||
margin: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.ud-main {
|
||||
.ud-main-pane {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 1px solid #f3f4f6;
|
||||
@@ -730,26 +725,26 @@ function openMail(email: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.ud-scroll {
|
||||
.ud-main-scroll {
|
||||
max-height: calc(78vh - 120px);
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.ud-radar-row {
|
||||
.ud-chart-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ud-radar-cell {
|
||||
.ud-chart-panel {
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 6px 4px 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ud-radar-title {
|
||||
.ud-chart-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -764,12 +759,12 @@ function openMail(email: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.ud-chart {
|
||||
.ud-chart-echart {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ud-chart-empty {
|
||||
.ud-chart-placeholder {
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -778,19 +773,19 @@ function openMail(email: string) {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.ud-mini-type {
|
||||
.ud-chart-footnote {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
padding: 0 4px 4px;
|
||||
}
|
||||
|
||||
.ud-row2 {
|
||||
.ud-dual-cards {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ud-card {
|
||||
.ud-panel-card {
|
||||
background: #fff;
|
||||
border: 1px solid #f3f4f6;
|
||||
border-radius: 8px;
|
||||
@@ -799,11 +794,11 @@ function openMail(email: string) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ud-grow {
|
||||
.ud-panel-card--wide {
|
||||
flex: 1.2;
|
||||
}
|
||||
|
||||
.ud-card-h {
|
||||
.ud-panel-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@@ -817,14 +812,14 @@ function openMail(email: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.ud-hint {
|
||||
.ud-panel-card__hint {
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.ud-gallup-item {
|
||||
.ud-gallup-list__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -832,7 +827,7 @@ function openMail(email: string) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.ud-gi-n {
|
||||
.ud-gallup-list__index {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
@@ -845,13 +840,13 @@ function openMail(email: string) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ud-roles {
|
||||
.ud-rolefit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ud-role {
|
||||
.ud-rolefit-list__row {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr 36px;
|
||||
gap: 8px;
|
||||
@@ -859,30 +854,30 @@ function openMail(email: string) {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.ud-role-n {
|
||||
.ud-rolefit-list__name {
|
||||
color: #4b5563;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ud-role-p {
|
||||
.ud-rolefit-list__percent {
|
||||
text-align: right;
|
||||
color: #7c3aed;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ud-match-card {
|
||||
.ud-enterprise-match {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ud-match-list {
|
||||
.ud-enterprise-match__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ud-match-row {
|
||||
.ud-enterprise-match__item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
@@ -892,26 +887,26 @@ function openMail(email: string) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.ud-m-name {
|
||||
.ud-enterprise-match__name {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.ud-m-sub {
|
||||
.ud-enterprise-match__meta {
|
||||
font-size: 11px;
|
||||
color: #7c3aed;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.ud-m-reason {
|
||||
.ud-enterprise-match__desc {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ud-m-contact {
|
||||
.ud-enterprise-match__contact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
@@ -924,34 +919,34 @@ function openMail(email: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.ud-m-phone {
|
||||
.ud-enterprise-match__phone {
|
||||
font-family: ui-monospace, monospace;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.ud-m-actions {
|
||||
.ud-enterprise-match__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ud-muted {
|
||||
.ud-empty-hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.ud-enterprise-hint {
|
||||
.ud-enterprise-match__hint {
|
||||
margin-top: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ud-test-tb {
|
||||
.ud-test-records-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ud-ticon {
|
||||
.ud-test-type-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
.tic-mbti {
|
||||
@@ -970,33 +965,33 @@ function openMail(email: string) {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.ud-pager {
|
||||
.ud-test-records-pagination {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ud-photos {
|
||||
.ud-photo-gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ud-ph {
|
||||
.ud-photo-gallery__thumb {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.ud-grid {
|
||||
.ud-layout {
|
||||
flex-direction: column;
|
||||
max-height: none;
|
||||
}
|
||||
.ud-side {
|
||||
.ud-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
.ud-radar-row {
|
||||
.ud-chart-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
138
admin/src/utils/faceResultDetail.ts
Normal file
138
admin/src/utils/faceResultDetail.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 人脸 / AI 分析结果:与微信小程序 pages/index/result 展示字段对齐,供后台「测试详情」使用
|
||||
*/
|
||||
|
||||
export interface FaceFeatureItem {
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface FaceBoneIceSummary {
|
||||
elementType?: string
|
||||
boneFleshRelation?: string
|
||||
}
|
||||
|
||||
export interface FaceDetailView {
|
||||
photos: string[]
|
||||
mbti: { type?: string; title?: string } | null
|
||||
disc: { primary?: string; secondary?: string } | null
|
||||
pdp: { primary?: string; secondary?: string } | null
|
||||
overview: string
|
||||
personalitySummary: string
|
||||
/** 字符串版面相(与小程序 faceAnalysisText) */
|
||||
faceAnalysisText: string
|
||||
/** 结构化面相:面部特征列表 */
|
||||
faceFeatures: FaceFeatureItem[]
|
||||
/** 字符串版骨相正文 */
|
||||
boneAnalysisText: string
|
||||
/** 《冰鉴》结构化摘要(小程序 boneFormSummary) */
|
||||
boneIceSummary: FaceBoneIceSummary | null
|
||||
advantages: string[]
|
||||
gallupTop3: string[]
|
||||
relationship: string
|
||||
careerDevelopment: string
|
||||
familyParenting: string
|
||||
partnerCofounder: string
|
||||
portrait: Record<string, any> | null
|
||||
hrView: Record<string, any> | null
|
||||
bossView: Record<string, any> | null
|
||||
resumeHighlights: string
|
||||
careers: string[]
|
||||
}
|
||||
|
||||
function parseMaybeJsonObject(raw: unknown): Record<string, any> | null {
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
return raw as Record<string, any>
|
||||
}
|
||||
if (typeof raw === 'string' && raw.trim().startsWith('{')) {
|
||||
try {
|
||||
const o = JSON.parse(raw) as unknown
|
||||
return o && typeof o === 'object' && !Array.isArray(o) ? (o as Record<string, any>) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function extractFaceFeatures(raw: unknown): FaceFeatureItem[] {
|
||||
const o = parseMaybeJsonObject(raw)
|
||||
if (!o) return []
|
||||
const fe = o.facialFeatures
|
||||
if (!Array.isArray(fe)) return []
|
||||
return fe
|
||||
.map((item: any) => ({
|
||||
label: String(item?.label ?? ''),
|
||||
description: String(item?.description ?? '')
|
||||
}))
|
||||
.filter((f) => f.label || f.description)
|
||||
}
|
||||
|
||||
function extractBoneIceSummary(raw: unknown): FaceBoneIceSummary | null {
|
||||
const o = parseMaybeJsonObject(raw)
|
||||
const sum = o?.boneFormSummary
|
||||
if (sum && typeof sum === 'object') {
|
||||
return {
|
||||
elementType: sum.elementType != null ? String(sum.elementType) : undefined,
|
||||
boneFleshRelation: sum.boneFleshRelation != null ? String(sum.boneFleshRelation) : undefined
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 从 AI 结果 JSON 构建后台展示模型(兼容字符串 / 结构化 / JSON 字符串) */
|
||||
export function buildFaceDetailFromParsed(parsed: Record<string, any> | null | undefined): FaceDetailView | null {
|
||||
if (!parsed || typeof parsed !== 'object') return null
|
||||
|
||||
const faceRaw = parsed.faceAnalysis
|
||||
const boneRaw = parsed.boneAnalysis
|
||||
|
||||
const faceFeatures = extractFaceFeatures(faceRaw)
|
||||
let faceAnalysisText = ''
|
||||
if (typeof faceRaw === 'string') {
|
||||
const tryObj = parseMaybeJsonObject(faceRaw)
|
||||
if (tryObj && Array.isArray(tryObj.facialFeatures) && tryObj.facialFeatures.length) {
|
||||
faceAnalysisText = ''
|
||||
} else {
|
||||
faceAnalysisText = faceRaw.trim()
|
||||
}
|
||||
}
|
||||
|
||||
const boneIceSummary = extractBoneIceSummary(boneRaw)
|
||||
let boneAnalysisText = ''
|
||||
if (typeof boneRaw === 'string') {
|
||||
const tryObj = parseMaybeJsonObject(boneRaw)
|
||||
if (tryObj?.boneFormSummary && typeof tryObj.boneFormSummary === 'object') {
|
||||
boneAnalysisText = ''
|
||||
} else {
|
||||
boneAnalysisText = boneRaw.trim()
|
||||
}
|
||||
}
|
||||
|
||||
const strArr = (v: unknown): string[] =>
|
||||
Array.isArray(v) ? (v as unknown[]).map((x) => String(x)).filter(Boolean) : []
|
||||
|
||||
return {
|
||||
photos: Array.isArray(parsed.photoUrls) ? (parsed.photoUrls as string[]) : [],
|
||||
mbti: parsed.mbti && typeof parsed.mbti === 'object' ? parsed.mbti : null,
|
||||
disc: parsed.disc && typeof parsed.disc === 'object' ? parsed.disc : null,
|
||||
pdp: parsed.pdp && typeof parsed.pdp === 'object' ? parsed.pdp : null,
|
||||
overview: String(parsed.overview ?? ''),
|
||||
personalitySummary: String(parsed.personalitySummary ?? ''),
|
||||
faceAnalysisText,
|
||||
faceFeatures,
|
||||
boneAnalysisText,
|
||||
boneIceSummary,
|
||||
advantages: strArr(parsed.advantages),
|
||||
gallupTop3: strArr(parsed.gallupTop3),
|
||||
relationship: String(parsed.relationship ?? ''),
|
||||
careerDevelopment: String(parsed.careerDevelopment ?? ''),
|
||||
familyParenting: String(parsed.familyParenting ?? ''),
|
||||
partnerCofounder: String(parsed.partnerCofounder ?? ''),
|
||||
portrait: parsed.portrait && typeof parsed.portrait === 'object' ? parsed.portrait : null,
|
||||
hrView: parsed.hrView && typeof parsed.hrView === 'object' ? parsed.hrView : null,
|
||||
bossView: parsed.bossView && typeof parsed.bossView === 'object' ? parsed.bossView : null,
|
||||
resumeHighlights: String(parsed.resumeHighlights ?? ''),
|
||||
careers: strArr(parsed.careers)
|
||||
}
|
||||
}
|
||||
@@ -110,15 +110,24 @@
|
||||
<div class="panel-head row">
|
||||
<div>
|
||||
<h2 class="panel-title">邀请小程序码</h2>
|
||||
<p class="panel-desc">员工 / 客户扫码进入企业测评</p>
|
||||
<p class="panel-desc">企业版扫码进企业测评;个人版扫码进个人首页</p>
|
||||
</div>
|
||||
<el-button size="small" type="primary" @click="loadInviteQrcode" :loading="inviteLoading">
|
||||
{{ inviteQrcode ? '刷新' : '生成' }}
|
||||
{{ inviteQrcodeEnterprise || inviteQrcodePersonal ? '刷新' : '生成' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="invite-body">
|
||||
<img v-if="inviteQrcode" :src="inviteQrcode" alt="邀请码" class="invite-img" />
|
||||
<span v-else class="invite-placeholder">点击生成</span>
|
||||
<template v-if="inviteQrcodeEnterprise || inviteQrcodePersonal">
|
||||
<div v-if="inviteQrcodeEnterprise" class="invite-card">
|
||||
<span class="invite-label">企业版</span>
|
||||
<img :src="inviteQrcodeEnterprise" alt="企业版邀请码" class="invite-img" />
|
||||
</div>
|
||||
<div v-if="inviteQrcodePersonal" class="invite-card">
|
||||
<span class="invite-label">个人版</span>
|
||||
<img :src="inviteQrcodePersonal" alt="个人版邀请码" class="invite-img" />
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="invite-placeholder">点击生成(将同时生成两枚码)</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -189,7 +198,8 @@ const faceSubtypeHints = ref<{
|
||||
|
||||
const loading = ref(false)
|
||||
const inviteLoading = ref(false)
|
||||
const inviteQrcode = ref<string>('')
|
||||
const inviteQrcodeEnterprise = ref<string>('')
|
||||
const inviteQrcodePersonal = ref<string>('')
|
||||
|
||||
/** 侧栏表格最大高度:单屏内滚动,不撑开整页 */
|
||||
const tableMaxH = 220
|
||||
@@ -388,9 +398,13 @@ const loadInviteQrcode = async () => {
|
||||
inviteLoading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/admin/invite/qrcode')
|
||||
const qrcode = res?.data?.qrcode
|
||||
if (qrcode && typeof qrcode === 'string') {
|
||||
inviteQrcode.value = qrcode
|
||||
const d = res?.data
|
||||
const ent = d?.enterprise?.qrcode ?? d?.qrcode
|
||||
const per = d?.personal?.qrcode
|
||||
if (typeof ent === 'string' && ent) inviteQrcodeEnterprise.value = ent
|
||||
if (typeof per === 'string' && per) inviteQrcodePersonal.value = per
|
||||
if (inviteQrcodeEnterprise.value || inviteQrcodePersonal.value) {
|
||||
ElMessage.success('已生成企业版与个人版小程序码')
|
||||
} else {
|
||||
ElMessage.error(res?.message || res?.msg || '生成失败,请确认企业绑定')
|
||||
}
|
||||
@@ -807,11 +821,26 @@ const loadInviteQrcode = async () => {
|
||||
|
||||
.invite-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
.invite-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.invite-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.invite-img {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
|
||||
@@ -35,7 +35,16 @@
|
||||
<el-table-column label="用户信息" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info-cell">
|
||||
<div class="user-avatar">{{ (row.username || '?')[0] }}</div>
|
||||
<el-avatar
|
||||
:size="36"
|
||||
shape="circle"
|
||||
:src="displayAvatar(row) || undefined"
|
||||
fit="cover"
|
||||
class="user-avatar user-el-avatar"
|
||||
:style="!displayAvatar(row) ? { backgroundColor: avatarBgColor(row) } : {}"
|
||||
>
|
||||
<span class="user-avatar-letter-fallback">{{ avatarLetter(row) }}</span>
|
||||
</el-avatar>
|
||||
<div class="user-details">
|
||||
<div class="username">{{ row.username || '未设置昵称' }}</div>
|
||||
<div class="openid-line">{{ row.openid || '—' }}</div>
|
||||
@@ -328,41 +337,169 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="face-right">
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.mbti">
|
||||
<!-- 第一组:核心性格标签 (MBTI, DISC, PDP) -->
|
||||
<div class="test-detail-row compact-row">
|
||||
<div class="test-detail-block flex-1" v-if="faceDetail.mbti">
|
||||
<h4 class="detail-subtitle">AI 识别 MBTI</h4>
|
||||
<p class="test-desc">
|
||||
<strong>{{ faceDetail.mbti.type }}</strong>
|
||||
<span v-if="faceDetail.mbti.title"> · {{ faceDetail.mbti.title }}</span>
|
||||
<strong class="text-primary">{{ faceDetail.mbti.type }}</strong>
|
||||
<span v-if="faceDetail.mbti.title" class="text-secondary"> · {{ faceDetail.mbti.title }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.disc">
|
||||
<div class="test-detail-block flex-1" v-if="faceDetail.disc">
|
||||
<h4 class="detail-subtitle">AI 识别 DISC</h4>
|
||||
<p class="test-desc">
|
||||
主类型:<strong>{{ faceDetail.disc.primary }}</strong>
|
||||
<span v-if="faceDetail.disc.secondary">,次类型:{{ faceDetail.disc.secondary }}</span>
|
||||
主类型:<strong class="text-primary">{{ faceDetail.disc.primary }}</strong>
|
||||
<span v-if="faceDetail.disc.secondary" class="text-secondary">,次:{{ faceDetail.disc.secondary }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.pdp">
|
||||
<div class="test-detail-block flex-1" v-if="faceDetail.pdp">
|
||||
<h4 class="detail-subtitle">AI 识别 PDP</h4>
|
||||
<p class="test-desc">
|
||||
主类型:<strong>{{ faceDetail.pdp.primary }}</strong>
|
||||
<span v-if="faceDetail.pdp.secondary">,次类型:{{ faceDetail.pdp.secondary }}</span>
|
||||
主类型:<strong class="text-primary">{{ faceDetail.pdp.primary }}</strong>
|
||||
<span v-if="faceDetail.pdp.secondary" class="text-secondary">,次:{{ faceDetail.pdp.secondary }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二组:核心优势 (主要优势, 盖洛普) -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.advantages?.length">
|
||||
<h4 class="detail-subtitle">主要优势</h4>
|
||||
<ul class="tag-list">
|
||||
<li v-for="item in faceDetail.advantages" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.gallupTop3?.length">
|
||||
<h4 class="detail-subtitle">盖洛普前三大优势</h4>
|
||||
<ol class="face-gallup-list">
|
||||
<li v-for="(g, gi) in faceDetail.gallupTop3" :key="gi">{{ g }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三组:综述 -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.overview">
|
||||
<h4 class="detail-subtitle">整体气质概览</h4>
|
||||
<p class="test-desc">{{ faceDetail.overview }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.boneAnalysis">
|
||||
<h4 class="detail-subtitle">骨相特征</h4>
|
||||
<p class="test-desc">{{ faceDetail.boneAnalysis }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.personalitySummary">
|
||||
<h4 class="detail-subtitle">性格总结</h4>
|
||||
<p class="test-desc">{{ faceDetail.personalitySummary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第四组:面相与骨相 -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.faceFeatures?.length">
|
||||
<h4 class="detail-subtitle">面部特征分析</h4>
|
||||
<ul class="face-feature-list">
|
||||
<li v-for="(f, fi) in faceDetail.faceFeatures" :key="fi">
|
||||
<strong v-if="f.label">{{ f.label }}:</strong>
|
||||
<span>{{ f.description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-else-if="faceDetail.faceAnalysisText">
|
||||
<h4 class="detail-subtitle">面相分析</h4>
|
||||
<p class="test-desc">{{ faceDetail.faceAnalysisText }}</p>
|
||||
</div>
|
||||
|
||||
<div class="test-detail-block" v-if="faceDetail.boneIceSummary && (faceDetail.boneIceSummary.elementType || faceDetail.boneIceSummary.boneFleshRelation)">
|
||||
<h4 class="detail-subtitle">骨相分析(《冰鉴》)</h4>
|
||||
<p v-if="faceDetail.boneIceSummary.elementType" class="test-desc">
|
||||
<strong>五行形相:</strong>{{ faceDetail.boneIceSummary.elementType }}
|
||||
</p>
|
||||
<p v-if="faceDetail.boneIceSummary.boneFleshRelation" class="test-desc">
|
||||
<strong>骨肉关系:</strong>{{ faceDetail.boneIceSummary.boneFleshRelation }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.boneAnalysisText">
|
||||
<h4 class="detail-subtitle">骨相特征</h4>
|
||||
<p class="test-desc">{{ faceDetail.boneAnalysisText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第五组:人际与发展 -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.relationship">
|
||||
<h4 class="detail-subtitle">人际关系与团队合作</h4>
|
||||
<p class="test-desc">{{ faceDetail.relationship }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.careers?.length">
|
||||
<h4 class="detail-subtitle">职业方向参考</h4>
|
||||
<ul class="tag-list tag-list-info">
|
||||
<li v-for="c in faceDetail.careers" :key="c">{{ c }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第六组:生活场景 -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.careerDevelopment">
|
||||
<h4 class="detail-subtitle">职业发展方向</h4>
|
||||
<p class="test-desc">{{ faceDetail.careerDevelopment }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.familyParenting">
|
||||
<h4 class="detail-subtitle">家庭亲子关系</h4>
|
||||
<p class="test-desc">{{ faceDetail.familyParenting }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.partnerCofounder">
|
||||
<h4 class="detail-subtitle">寻找合伙人</h4>
|
||||
<p class="test-desc">{{ faceDetail.partnerCofounder }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第七组:深度职场洞察 (Portrait, Boss, HR, Resume) -->
|
||||
<div class="test-detail-grid" v-if="faceDetail.portrait || faceDetail.bossView || faceDetail.hrView || faceDetail.resumeHighlights">
|
||||
<template v-if="faceDetail.portrait">
|
||||
<div class="test-detail-block" v-if="faceDetail.portrait.coreStrengths?.length">
|
||||
<h4 class="detail-subtitle">深度·核心优势</h4>
|
||||
<ul class="tag-list">
|
||||
<li v-for="s in faceDetail.portrait.coreStrengths" :key="s">{{ s }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.portrait.coreRisks?.length">
|
||||
<h4 class="detail-subtitle">深度·潜在风险</h4>
|
||||
<ul class="tag-list tag-list-warning">
|
||||
<li v-for="r in faceDetail.portrait.coreRisks" :key="r">{{ r }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.portrait.workStyle">
|
||||
<h4 class="detail-subtitle">深度·工作风格</h4>
|
||||
<p class="test-desc">{{ faceDetail.portrait.workStyle }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="test-detail-block" v-if="faceDetail.resumeHighlights">
|
||||
<h4 class="detail-subtitle">简历要点</h4>
|
||||
<p class="test-desc">{{ faceDetail.resumeHighlights }}</p>
|
||||
</div>
|
||||
<template v-if="faceDetail.bossView">
|
||||
<div class="test-detail-block" v-if="faceDetail.bossView.headline">
|
||||
<h4 class="detail-subtitle">老板视角·一句话</h4>
|
||||
<p class="test-desc">{{ faceDetail.bossView.headline }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.bossView.costInsight">
|
||||
<h4 class="detail-subtitle">老板视角·成本洞察</h4>
|
||||
<p class="test-desc">{{ faceDetail.bossView.costInsight }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="faceDetail.hrView">
|
||||
<div class="test-detail-block" v-if="faceDetail.hrView.roleRecommend?.bestFit?.length">
|
||||
<h4 class="detail-subtitle">HR·推荐岗位</h4>
|
||||
<ul class="tag-list tag-list-info">
|
||||
<li v-for="r in faceDetail.hrView.roleRecommend.bestFit" :key="r">{{ r }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.hrView.roleRecommend?.notSuitable?.length">
|
||||
<h4 class="detail-subtitle">HR·不适合场景</h4>
|
||||
<ul class="tag-list tag-list-warning">
|
||||
<li v-for="r in faceDetail.hrView.roleRecommend.notSuitable" :key="r">{{ r }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -491,7 +628,10 @@
|
||||
<div class="resume-content">{{ currentTestDescription }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentTestType !== 'resume' && currentTestDescription">
|
||||
<div
|
||||
class="detail-section"
|
||||
v-if="currentTestType !== 'resume' && currentTestType !== 'face' && currentTestType !== 'ai' && currentTestDescription"
|
||||
>
|
||||
<h4 class="detail-subtitle">补充说明</h4>
|
||||
<p class="test-desc">{{ currentTestDescription }}</p>
|
||||
</div>
|
||||
@@ -505,6 +645,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 { buildFaceDetailFromParsed } from '@/utils/faceResultDetail'
|
||||
import UserDetailDialog from '@/components/UserDetailDialog.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -553,6 +694,24 @@ async function loadUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
function displayAvatar(row: Record<string, any>) {
|
||||
const a = row?.avatar ?? row?.avatarUrl ?? ''
|
||||
return typeof a === 'string' ? a.trim() : ''
|
||||
}
|
||||
|
||||
function avatarLetter(row: { username?: string; nickname?: string }) {
|
||||
const name = (row?.username || row?.nickname || '?').trim()
|
||||
return (name.charAt(0) || '?').toUpperCase()
|
||||
}
|
||||
|
||||
const AVATAR_PALETTE = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#0ea5e9', '#3b82f6', '#eab308']
|
||||
function avatarBgColor(row: { username?: string; nickname?: string }) {
|
||||
const name = (row?.username || row?.nickname || '?').trim()
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) hash += name.charCodeAt(i)
|
||||
return AVATAR_PALETTE[Math.abs(hash) % AVATAR_PALETTE.length]
|
||||
}
|
||||
|
||||
function formatPhone(phone: string) {
|
||||
if (!phone) return '-'
|
||||
if (phone.length === 11) return phone.substring(0, 3) + '****' + phone.substring(7)
|
||||
@@ -691,15 +850,7 @@ const pdpDetail = computed(() => {
|
||||
const faceDetail = computed(() => {
|
||||
const parsed = currentTestParsed.value
|
||||
if (!parsed || (currentTestType.value !== 'face' && currentTestType.value !== 'ai')) return null
|
||||
return {
|
||||
mbti: parsed.mbti || null,
|
||||
disc: parsed.disc || null,
|
||||
pdp: parsed.pdp || null,
|
||||
overview: parsed.overview ?? '',
|
||||
boneAnalysis: parsed.boneAnalysis ?? '',
|
||||
personalitySummary: parsed.personalitySummary ?? '',
|
||||
photos: Array.isArray(parsed.photoUrls) ? parsed.photoUrls : []
|
||||
}
|
||||
return buildFaceDetailFromParsed(parsed)
|
||||
})
|
||||
|
||||
const resumeDetail = computed(() => {
|
||||
@@ -735,7 +886,15 @@ function extractTestDescription(parsed: any, testType: string): string {
|
||||
}
|
||||
|
||||
if (t === 'face' || t === 'ai') {
|
||||
return String(parsed.overview ?? parsed.personalitySummary ?? '')
|
||||
const parts = [
|
||||
parsed.relationship,
|
||||
parsed.faceAnalysis && typeof parsed.faceAnalysis === 'string' ? parsed.faceAnalysis : '',
|
||||
parsed.boneAnalysis && typeof parsed.boneAnalysis === 'string' ? parsed.boneAnalysis : ''
|
||||
]
|
||||
.map((x) => String(x || '').trim())
|
||||
.filter(Boolean)
|
||||
const extra = parts.length ? `\n\n${parts.join('\n\n')}` : ''
|
||||
return String(parsed.overview ?? parsed.personalitySummary ?? '') + extra
|
||||
}
|
||||
|
||||
if (t === 'resume') {
|
||||
@@ -936,18 +1095,14 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.user-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: #7c3aed;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.user-avatar.user-el-avatar {
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-avatar-letter-fallback {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
@@ -1313,6 +1468,9 @@ onMounted(() => {
|
||||
.face-left {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.face-photos {
|
||||
@@ -1332,6 +1490,53 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.test-detail-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.compact-row .test-detail-block {
|
||||
margin-bottom: 0;
|
||||
padding: 12px;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #4f46e5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #6b7280;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.face-gallup-list {
|
||||
margin: 6px 0 0 18px;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.face-feature-list {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
line-height: 1.65;
|
||||
|
||||
li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.test-desc {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
>
|
||||
<template #title>数据库归并(可选)</template>
|
||||
<p class="migrate-text">
|
||||
若需把库里的「无 enterpriseId」记录<strong>永久写入</strong>到「存客宝」企业,可点下方按钮(先预览条数,再二次确认)。仅统计展示时不必执行,接口已把个人池并入存客宝卡片。
|
||||
将把无企业归属的用户写入「存客宝」、补齐其 <code>test_results</code> 的企业字段,并把 <code>user_profile</code> 中 personal 行与该企业 enterprise 行<strong>合并归档</strong>(计数相加后删除 personal)。先预览条数,再二次确认。
|
||||
</p>
|
||||
<el-button type="primary" plain size="small" :loading="migrateLoading" @click="runOrphanMigrate">
|
||||
预览并归并到存客宝
|
||||
@@ -72,10 +72,15 @@ async function runOrphanMigrate() {
|
||||
})
|
||||
const d = res.data ?? res
|
||||
const name = d.enterpriseName || '存客宝'
|
||||
const tr = d.testResultsRows ?? 0
|
||||
const wu = d.wechatUsersRows ?? 0
|
||||
const tr = d.testResultsRows ?? 0
|
||||
const pr = d.userProfilePersonalRows ?? 0
|
||||
if (wu === 0 && tr === 0 && pr === 0) {
|
||||
ElMessage.info((d.hint as string) || '当前无需归并')
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(
|
||||
`将把无企业归属的数据写入「${name}」:测试记录约 ${tr} 条,小程序用户表约 ${wu} 行。此操作会修改数据库,是否继续?`,
|
||||
`即将写入「${name}」:小程序用户约 ${wu} 人;待补全企业的测试记录约 ${tr} 条;待合并的 personal 画像行约 ${pr} 条。将修改 wechat_users、test_results、user_profile,是否继续?`,
|
||||
'确认归并',
|
||||
{ type: 'warning', confirmButtonText: '写入', cancelButtonText: '取消' }
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -136,7 +136,7 @@
|
||||
<el-icon class="header-icon"><Setting /></el-icon>
|
||||
<span>系统基础配置</span>
|
||||
</div>
|
||||
<p class="header-description">管理网站名称、维护模式等基础设置</p>
|
||||
<p class="header-description">管理网站名称等基础设置</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="card-content">
|
||||
@@ -255,16 +255,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开关项 -->
|
||||
<div class="switch-section">
|
||||
<div class="switch-item">
|
||||
<div class="switch-info">
|
||||
<p class="switch-title">维护模式</p>
|
||||
<p class="switch-desc">开启后前台将显示维护页面</p>
|
||||
</div>
|
||||
<el-switch v-model="systemConfig.maintenanceMode" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
|
||||
@@ -102,19 +102,16 @@
|
||||
<el-table-column label="用户信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info-cell">
|
||||
<img
|
||||
v-if="row.avatar"
|
||||
:src="row.avatar"
|
||||
class="user-avatar user-avatar-img"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="user-avatar user-avatar-letter"
|
||||
:style="{ backgroundColor: avatarBgColor(row) }"
|
||||
<el-avatar
|
||||
:size="36"
|
||||
shape="circle"
|
||||
:src="displayAvatar(row) || undefined"
|
||||
fit="cover"
|
||||
class="user-avatar user-el-avatar"
|
||||
:style="!displayAvatar(row) ? { backgroundColor: avatarBgColor(row) } : {}"
|
||||
>
|
||||
{{ avatarLetter(row) }}
|
||||
</div>
|
||||
<span class="user-avatar-letter-fallback">{{ avatarLetter(row) }}</span>
|
||||
</el-avatar>
|
||||
<div class="user-details">
|
||||
<div class="username">{{ row.username || '未设置昵称' }}</div>
|
||||
<div class="openid-line">{{ row.openid || '—' }}</div>
|
||||
@@ -427,41 +424,169 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="face-right">
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.mbti">
|
||||
<!-- 第一组:核心性格标签 (MBTI, DISC, PDP) -->
|
||||
<div class="test-detail-row compact-row">
|
||||
<div class="test-detail-block flex-1" v-if="faceDetail.mbti">
|
||||
<h4 class="detail-subtitle">AI 识别 MBTI</h4>
|
||||
<p class="test-desc">
|
||||
<strong>{{ faceDetail.mbti.type }}</strong>
|
||||
<span v-if="faceDetail.mbti.title"> · {{ faceDetail.mbti.title }}</span>
|
||||
<strong class="text-primary">{{ faceDetail.mbti.type }}</strong>
|
||||
<span v-if="faceDetail.mbti.title" class="text-secondary"> · {{ faceDetail.mbti.title }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.disc">
|
||||
<div class="test-detail-block flex-1" v-if="faceDetail.disc">
|
||||
<h4 class="detail-subtitle">AI 识别 DISC</h4>
|
||||
<p class="test-desc">
|
||||
主类型:<strong>{{ faceDetail.disc.primary }}</strong>
|
||||
<span v-if="faceDetail.disc.secondary">,次类型:{{ faceDetail.disc.secondary }}</span>
|
||||
主类型:<strong class="text-primary">{{ faceDetail.disc.primary }}</strong>
|
||||
<span v-if="faceDetail.disc.secondary" class="text-secondary">,次:{{ faceDetail.disc.secondary }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.pdp">
|
||||
<div class="test-detail-block flex-1" v-if="faceDetail.pdp">
|
||||
<h4 class="detail-subtitle">AI 识别 PDP</h4>
|
||||
<p class="test-desc">
|
||||
主类型:<strong>{{ faceDetail.pdp.primary }}</strong>
|
||||
<span v-if="faceDetail.pdp.secondary">,次类型:{{ faceDetail.pdp.secondary }}</span>
|
||||
主类型:<strong class="text-primary">{{ faceDetail.pdp.primary }}</strong>
|
||||
<span v-if="faceDetail.pdp.secondary" class="text-secondary">,次:{{ faceDetail.pdp.secondary }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二组:核心优势 (主要优势, 盖洛普) -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.advantages?.length">
|
||||
<h4 class="detail-subtitle">主要优势</h4>
|
||||
<ul class="tag-list">
|
||||
<li v-for="item in faceDetail.advantages" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.gallupTop3?.length">
|
||||
<h4 class="detail-subtitle">盖洛普前三大优势</h4>
|
||||
<ol class="face-gallup-list">
|
||||
<li v-for="(g, gi) in faceDetail.gallupTop3" :key="gi">{{ g }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三组:综述 -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.overview">
|
||||
<h4 class="detail-subtitle">整体气质概览</h4>
|
||||
<p class="test-desc">{{ faceDetail.overview }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.boneAnalysis">
|
||||
<h4 class="detail-subtitle">骨相特征</h4>
|
||||
<p class="test-desc">{{ faceDetail.boneAnalysis }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.personalitySummary">
|
||||
<h4 class="detail-subtitle">性格总结</h4>
|
||||
<p class="test-desc">{{ faceDetail.personalitySummary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第四组:面相与骨相 -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.faceFeatures?.length">
|
||||
<h4 class="detail-subtitle">面部特征分析</h4>
|
||||
<ul class="face-feature-list">
|
||||
<li v-for="(f, fi) in faceDetail.faceFeatures" :key="fi">
|
||||
<strong v-if="f.label">{{ f.label }}:</strong>
|
||||
<span>{{ f.description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-else-if="faceDetail.faceAnalysisText">
|
||||
<h4 class="detail-subtitle">面相分析</h4>
|
||||
<p class="test-desc">{{ faceDetail.faceAnalysisText }}</p>
|
||||
</div>
|
||||
|
||||
<div class="test-detail-block" v-if="faceDetail.boneIceSummary && (faceDetail.boneIceSummary.elementType || faceDetail.boneIceSummary.boneFleshRelation)">
|
||||
<h4 class="detail-subtitle">骨相分析(《冰鉴》)</h4>
|
||||
<p v-if="faceDetail.boneIceSummary.elementType" class="test-desc">
|
||||
<strong>五行形相:</strong>{{ faceDetail.boneIceSummary.elementType }}
|
||||
</p>
|
||||
<p v-if="faceDetail.boneIceSummary.boneFleshRelation" class="test-desc">
|
||||
<strong>骨肉关系:</strong>{{ faceDetail.boneIceSummary.boneFleshRelation }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.boneAnalysisText">
|
||||
<h4 class="detail-subtitle">骨相特征</h4>
|
||||
<p class="test-desc">{{ faceDetail.boneAnalysisText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第五组:人际与发展 -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.relationship">
|
||||
<h4 class="detail-subtitle">人际关系与团队合作</h4>
|
||||
<p class="test-desc">{{ faceDetail.relationship }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.careers?.length">
|
||||
<h4 class="detail-subtitle">职业方向参考</h4>
|
||||
<ul class="tag-list tag-list-info">
|
||||
<li v-for="c in faceDetail.careers" :key="c">{{ c }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第六组:生活场景 -->
|
||||
<div class="test-detail-grid">
|
||||
<div class="test-detail-block" v-if="faceDetail.careerDevelopment">
|
||||
<h4 class="detail-subtitle">职业发展方向</h4>
|
||||
<p class="test-desc">{{ faceDetail.careerDevelopment }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.familyParenting">
|
||||
<h4 class="detail-subtitle">家庭亲子关系</h4>
|
||||
<p class="test-desc">{{ faceDetail.familyParenting }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.partnerCofounder">
|
||||
<h4 class="detail-subtitle">寻找合伙人</h4>
|
||||
<p class="test-desc">{{ faceDetail.partnerCofounder }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第七组:深度职场洞察 (Portrait, Boss, HR, Resume) -->
|
||||
<div class="test-detail-grid" v-if="faceDetail.portrait || faceDetail.bossView || faceDetail.hrView || faceDetail.resumeHighlights">
|
||||
<template v-if="faceDetail.portrait">
|
||||
<div class="test-detail-block" v-if="faceDetail.portrait.coreStrengths?.length">
|
||||
<h4 class="detail-subtitle">深度·核心优势</h4>
|
||||
<ul class="tag-list">
|
||||
<li v-for="s in faceDetail.portrait.coreStrengths" :key="s">{{ s }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.portrait.coreRisks?.length">
|
||||
<h4 class="detail-subtitle">深度·潜在风险</h4>
|
||||
<ul class="tag-list tag-list-warning">
|
||||
<li v-for="r in faceDetail.portrait.coreRisks" :key="r">{{ r }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.portrait.workStyle">
|
||||
<h4 class="detail-subtitle">深度·工作风格</h4>
|
||||
<p class="test-desc">{{ faceDetail.portrait.workStyle }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="test-detail-block" v-if="faceDetail.resumeHighlights">
|
||||
<h4 class="detail-subtitle">简历要点</h4>
|
||||
<p class="test-desc">{{ faceDetail.resumeHighlights }}</p>
|
||||
</div>
|
||||
<template v-if="faceDetail.bossView">
|
||||
<div class="test-detail-block" v-if="faceDetail.bossView.headline">
|
||||
<h4 class="detail-subtitle">老板视角·一句话</h4>
|
||||
<p class="test-desc">{{ faceDetail.bossView.headline }}</p>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.bossView.costInsight">
|
||||
<h4 class="detail-subtitle">老板视角·成本洞察</h4>
|
||||
<p class="test-desc">{{ faceDetail.bossView.costInsight }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="faceDetail.hrView">
|
||||
<div class="test-detail-block" v-if="faceDetail.hrView.roleRecommend?.bestFit?.length">
|
||||
<h4 class="detail-subtitle">HR·推荐岗位</h4>
|
||||
<ul class="tag-list tag-list-info">
|
||||
<li v-for="r in faceDetail.hrView.roleRecommend.bestFit" :key="r">{{ r }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-detail-block" v-if="faceDetail.hrView.roleRecommend?.notSuitable?.length">
|
||||
<h4 class="detail-subtitle">HR·不适合场景</h4>
|
||||
<ul class="tag-list tag-list-warning">
|
||||
<li v-for="r in faceDetail.hrView.roleRecommend.notSuitable" :key="r">{{ r }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -582,8 +707,11 @@
|
||||
<div class="resume-content">{{ currentTestDescription }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 通用补充说明 -->
|
||||
<div class="detail-section" v-if="currentTestType !== 'resume' && currentTestDescription">
|
||||
<!-- 通用补充说明(人脸/AI 已在上方分块展示,避免重复堆砌) -->
|
||||
<div
|
||||
class="detail-section"
|
||||
v-if="currentTestType !== 'resume' && currentTestType !== 'face' && currentTestType !== 'ai' && currentTestDescription"
|
||||
>
|
||||
<h4 class="detail-subtitle">补充说明</h4>
|
||||
<p class="test-desc">{{ currentTestDescription }}</p>
|
||||
</div>
|
||||
@@ -597,6 +725,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 { buildFaceDetailFromParsed } from '@/utils/faceResultDetail'
|
||||
import UserDetailDialog from '@/components/UserDetailDialog.vue'
|
||||
|
||||
withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
|
||||
@@ -709,6 +838,12 @@ function formatPhone(phone: string) {
|
||||
return phone
|
||||
}
|
||||
|
||||
/** 列表头像 URL(兼容 avatar / avatarUrl,避免字段名或空白异常导致整列不显示) */
|
||||
function displayAvatar(row: Record<string, any>) {
|
||||
const a = row?.avatar ?? row?.avatarUrl ?? ''
|
||||
return typeof a === 'string' ? a.trim() : ''
|
||||
}
|
||||
|
||||
/** 文字头像:根据昵称取首字 */
|
||||
function avatarLetter(row: { username?: string; nickname?: string }) {
|
||||
const name = (row?.username || row?.nickname || '?').trim()
|
||||
@@ -856,15 +991,7 @@ const pdpDetail = computed(() => {
|
||||
const faceDetail = computed(() => {
|
||||
const parsed = currentTestParsed.value
|
||||
if (!parsed || (currentTestType.value !== 'face' && currentTestType.value !== 'ai')) return null
|
||||
return {
|
||||
mbti: parsed.mbti || null,
|
||||
disc: parsed.disc || null,
|
||||
pdp: parsed.pdp || null,
|
||||
overview: parsed.overview ?? '',
|
||||
boneAnalysis: parsed.boneAnalysis ?? '',
|
||||
personalitySummary: parsed.personalitySummary ?? '',
|
||||
photos: Array.isArray(parsed.photoUrls) ? parsed.photoUrls : []
|
||||
}
|
||||
return buildFaceDetailFromParsed(parsed)
|
||||
})
|
||||
|
||||
const resumeDetail = computed(() => {
|
||||
@@ -899,7 +1026,13 @@ function extractTestDescription(parsed: any, testType: string): string {
|
||||
}
|
||||
|
||||
if (t === 'face' || t === 'ai') {
|
||||
return String(parsed.overview ?? parsed.personalitySummary ?? '')
|
||||
const parts = [
|
||||
parsed.relationship,
|
||||
parsed.faceAnalysis && typeof parsed.faceAnalysis === 'string' ? parsed.faceAnalysis : '',
|
||||
parsed.boneAnalysis && typeof parsed.boneAnalysis === 'string' ? parsed.boneAnalysis : ''
|
||||
].map((x) => String(x || '').trim()).filter(Boolean)
|
||||
const extra = parts.length ? `\n\n${parts.join('\n\n')}` : ''
|
||||
return String(parsed.overview ?? parsed.personalitySummary ?? '') + extra
|
||||
}
|
||||
|
||||
if (t === 'resume') {
|
||||
@@ -1361,6 +1494,16 @@ onMounted(() => {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-el-avatar {
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-avatar-letter-fallback {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
.username {
|
||||
font-weight: 600;
|
||||
@@ -1744,6 +1887,9 @@ onMounted(() => {
|
||||
.face-left {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.face-photos {
|
||||
@@ -1763,6 +1909,53 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.test-detail-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.compact-row .test-detail-block {
|
||||
margin-bottom: 0 !important;
|
||||
padding: 12px;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #4f46e5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #6b7280;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.face-gallup-list {
|
||||
margin: 6px 0 0 18px;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.face-feature-list {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
line-height: 1.65;
|
||||
|
||||
li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.test-desc {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
|
||||
Reference in New Issue
Block a user