diff --git a/admin/src/components/UserDetailDialog.vue b/admin/src/components/UserDetailDialog.vue
index 72b3e91..8a67b2a 100644
--- a/admin/src/components/UserDetailDialog.vue
+++ b/admin/src/components/UserDetailDialog.vue
@@ -178,6 +178,8 @@
fit="cover"
class="ud-photo-gallery__thumb"
:preview-src-list="facePhotos"
+ :initial-index="idx"
+ preview-teleported
/>
暂无人脸分析照片
@@ -218,6 +220,8 @@ import {
RadarComponent
} from 'echarts/components'
import VChart from 'vue-echarts'
+import { discTopTwoLabel, discCompactLabel } from '@/utils/discDisplay'
+import { buildFaceDetailFromParsed } from '@/utils/faceResultDetail'
use([CanvasRenderer, RadarChart, GridComponent, TooltipComponent, LegendComponent, RadarComponent])
@@ -327,6 +331,8 @@ function extractTestSummary(test: any): string {
const type = (test?.testType || '').toLowerCase()
if (type === 'mbti') return String(data.mbtiType ?? data.type ?? data.result ?? '')
if (type === 'disc') {
+ const two = discTopTwoLabel(data)
+ if (two) return two
const desc = data.description?.type
if (typeof desc === 'string' && desc) return desc
if (data.dominantType) return String(data.dominantType) + '型'
@@ -452,7 +458,7 @@ const PDP_LABELS: Record = {
Tiger: '虎',
Peacock: '孔雀',
Owl: '猫头鹰',
- Koala: '考拉',
+ Koala: '无尾熊',
Chameleon: '变色龙'
}
@@ -464,7 +470,7 @@ const pdpRadarOption = computed(() => {
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))
+ const idx = ['老虎', '孔雀', '猫头鹰', '无尾熊', '考拉', '变色龙'].findIndex(x => dom.includes(x))
if (idx < 0) return null
const v2 = [15, 15, 15, 15, 15]
v2[idx] = 55
@@ -500,10 +506,10 @@ const discRadarOption = computed(() => {
color: ['#2563eb'],
radar: {
indicator: [
- { name: 'D', max: 100 },
- { name: 'I', max: 100 },
- { name: 'S', max: 100 },
- { name: 'C', max: 100 }
+ { name: discCompactLabel('D'), max: 100 },
+ { name: discCompactLabel('I'), max: 100 },
+ { name: discCompactLabel('S'), max: 100 },
+ { name: discCompactLabel('C'), max: 100 }
],
radius: 58,
axisName: { fontSize: 11, color: '#6b7280' }
@@ -535,8 +541,8 @@ const roleFitList = computed(() => {
const facePhotos = computed(() => {
const f = latestFace.value
- const urls = f?.photoUrls
- return Array.isArray(urls) ? urls : []
+ const detail = f ? buildFaceDetailFromParsed(f) : null
+ return detail?.photos?.length ? detail.photos : []
})
function testIcon(testType: string) {
diff --git a/admin/src/utils/discDisplay.ts b/admin/src/utils/discDisplay.ts
new file mode 100644
index 0000000..f8a7bb3
--- /dev/null
+++ b/admin/src/utils/discDisplay.ts
@@ -0,0 +1,128 @@
+/**
+ * 管理后台 DISC 展示:双字母摘要与「力量 / 活跃 / 和平 / 完美」称谓。
+ * 逻辑对齐 miniprogram/utils/resultFormat.js 与 api PdpDiscResultText::discResolveTwoLetters。
+ */
+
+export const DISC_STYLE_NAMES: Record = {
+ D: '力量',
+ I: '活跃',
+ S: '和平',
+ C: '完美'
+}
+
+const ALLOW = new Set(['D', 'I', 'S', 'C'])
+
+function discOrderedFromScores(scores: Record | null | undefined): [string, string] {
+ if (!scores || typeof scores !== 'object') return ['', '']
+ const pairs: [string, number][] = []
+ for (const k of Object.keys(scores)) {
+ const u = String(k).trim().toUpperCase().charAt(0)
+ if (!ALLOW.has(u)) continue
+ pairs.push([u, Number(scores[k]) || 0])
+ }
+ pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
+ return [pairs[0]?.[0] ?? '', pairs[1]?.[0] ?? '']
+}
+
+export function discPrimaryLetter(data: any): string {
+ if (!data || typeof data !== 'object') return ''
+ const dType = data.description?.type
+ if (typeof dType === 'string' && dType) {
+ const noXing = String(dType).trim().replace(/型$/, '')
+ if (noXing.length === 1) {
+ const u = noXing.toUpperCase()
+ if (ALLOW.has(u)) return u
+ }
+ }
+ if (data.dominantType) {
+ const u = String(data.dominantType).trim().toUpperCase().charAt(0)
+ if (ALLOW.has(u)) return u
+ }
+ if (data.disc) {
+ const noXing = String(data.disc).trim().replace(/型$/, '')
+ if (noXing.length === 1) {
+ const u = noXing.toUpperCase()
+ if (ALLOW.has(u)) return u
+ }
+ }
+ return ''
+}
+
+export function discResolveTwoLetters(data: any): [string, string] {
+ let f = discPrimaryLetter(data)
+ let s = ''
+ if (data.secondaryType) {
+ const u = String(data.secondaryType).trim().toUpperCase().charAt(0)
+ if (ALLOW.has(u)) s = u
+ }
+ let a0 = ''
+ let b = ''
+ if (data.scores && typeof data.scores === 'object') {
+ const ord = discOrderedFromScores(data.scores as Record)
+ a0 = ord[0] || ''
+ b = ord[1] || ''
+ }
+ if (!a0 && !b && data.percentages && typeof data.percentages === 'object') {
+ const ord = discOrderedFromScores(data.percentages as Record)
+ a0 = ord[0] || ''
+ b = ord[1] || ''
+ }
+ if (!f && a0) f = a0
+ if (!s || s === f) {
+ if (b && b !== f) s = b
+ else s = ''
+ }
+ return [f, s]
+}
+
+export function discNormalizeLegacyDualType(desc: unknown): string {
+ if (typeof desc !== 'string' || !desc) return ''
+ const t = desc.replace(/\s+/g, '').replace(/\uFF0B/g, '+')
+ let m = t.match(/^([DISC])型\+([DISC])型$/i)
+ if (m) return m[1].toUpperCase() + '+' + m[2].toUpperCase() + '型'
+ m = t.match(/^([DISC])型$/i)
+ if (m) return m[1].toUpperCase() + '型'
+ return ''
+}
+
+export function discTopTwoLabel(data: any): string {
+ if (!data || typeof data !== 'object') return ''
+ const [fL, sL] = discResolveTwoLetters(data)
+ if (fL || sL) {
+ if (!fL) return sL ? sL + '型' : ''
+ if (!sL || sL === fL) return fL + '型'
+ return fL + '+' + sL + '型'
+ }
+ return discNormalizeLegacyDualType(data.description?.type)
+}
+
+/** 副标题:单型「完美」,双型「完美 · 力量」 */
+export function discStyleSubtitle(data: any): string {
+ if (!data || typeof data !== 'object') return ''
+ const [f, s] = discResolveTwoLetters(data)
+ const nf = f ? DISC_STYLE_NAMES[f] ?? '' : ''
+ const ns = s ? DISC_STYLE_NAMES[s] ?? '' : ''
+ if (!nf && !ns) return ''
+ if (!ns || s === f) return nf
+ return `${nf} · ${ns}`
+}
+
+/** 柱状图左侧:D(力量) */
+export function discDimensionLabel(letter: string): string {
+ const u = String(letter).trim().toUpperCase().charAt(0)
+ const name = DISC_STYLE_NAMES[u]
+ return name ? `${u}(${name})` : u || letter
+}
+
+/** 仅风格名,用于与字母分开展示 */
+export function discStyleName(letter: string): string {
+ const u = String(letter).trim().toUpperCase().charAt(0)
+ return DISC_STYLE_NAMES[u] ?? ''
+}
+
+/** 雷达轴等紧凑单行:D·力量 */
+export function discCompactLabel(letter: string): string {
+ const u = String(letter).trim().toUpperCase().charAt(0)
+ const n = DISC_STYLE_NAMES[u]
+ return n ? `${u}·${n}` : u || letter
+}
diff --git a/admin/src/utils/faceResultDetail.ts b/admin/src/utils/faceResultDetail.ts
index 190ae9a..37bee20 100644
--- a/admin/src/utils/faceResultDetail.ts
+++ b/admin/src/utils/faceResultDetail.ts
@@ -2,6 +2,8 @@
* 人脸 / AI 分析结果:与微信小程序 pages/index/result 展示字段对齐,供后台「测试详情」使用
*/
+import { discStyleName } from './discDisplay'
+
export interface FaceFeatureItem {
label: string
description: string
@@ -40,26 +42,72 @@ export interface FaceDetailView {
careers: string[]
}
-function collectPhotoUrls(parsed: Record): string[] {
- const out: string[] = []
+function normalizeImageUrl(s: string): string | null {
+ let t = s.trim()
+ if (!t) return null
+ if (t.startsWith('//')) t = 'https:' + t
+ if (t.startsWith('http://') || t.startsWith('https://')) return t
+ return null
+}
+
+function collectPhotoUrlsFromObject(obj: Record | null | undefined, out: string[]): void {
+ if (!obj || typeof obj !== 'object') return
+
const pushUrl = (s: string) => {
- const t = s.trim()
- if (t && (t.startsWith('http://') || t.startsWith('https://'))) out.push(t)
+ const n = normalizeImageUrl(s)
+ if (n) out.push(n)
}
+
const pushArr = (v: unknown) => {
+ if (v == null) return
+ if (typeof v === 'string') {
+ const t = v.trim()
+ if (t.startsWith('[')) {
+ try {
+ const arr = JSON.parse(t) as unknown
+ pushArr(arr)
+ } catch {
+ /* ignore */
+ }
+ }
+ return
+ }
if (!Array.isArray(v)) return
for (const x of v) {
if (typeof x === 'string') pushUrl(x)
+ else if (x && typeof x === 'object' && !Array.isArray(x)) {
+ const o = x as Record
+ const u = o.url ?? o.src ?? o.imageUrl
+ if (typeof u === 'string') pushUrl(u)
+ }
}
}
- pushArr(parsed.photoUrls)
- pushArr(parsed.photos)
- pushArr(parsed.imageUrls)
+
+ pushArr(obj.photoUrls)
+ pushArr(obj.photos)
+ pushArr(obj.imageUrls)
for (const k of ['photoUrl', 'imageUrl', 'faceImageUrl'] as const) {
- const u = parsed[k]
+ const u = obj[k]
if (typeof u === 'string') pushUrl(u)
}
- return [...new Set(out)]
+}
+
+function collectPhotoUrls(parsed: Record): string[] {
+ const out: string[] = []
+ collectPhotoUrlsFromObject(parsed, out)
+ const inner = parsed?.result
+ if (inner && typeof inner === 'object' && !Array.isArray(inner)) {
+ collectPhotoUrlsFromObject(inner as Record, out)
+ }
+ // 保序去重(仅去掉完全相同的 URL,不误删带不同签名的地址)
+ const seen = new Set()
+ const deduped: string[] = []
+ for (const u of out) {
+ if (seen.has(u)) continue
+ seen.add(u)
+ deduped.push(u)
+ }
+ return deduped
}
function isPlaceholderTitle(s: string): boolean {
@@ -203,3 +251,22 @@ export function buildFaceDetailFromParsed(parsed: Record | null | u
careers: strArr(parsed.careers)
}
}
+
+/** 面相报告 DISC:S → S(和平) */
+export function faceDiscDisplayLabel(raw: string | undefined | null): string {
+ if (raw == null || raw === '') return ''
+ const s = String(raw).trim()
+ const noXing = s.replace(/型$/u, '').trim()
+ const ch = noXing.charAt(0).toUpperCase()
+ if (ch && ['D', 'I', 'S', 'C'].includes(ch)) {
+ const cn = discStyleName(ch)
+ if (cn) return `${ch}(${cn})`
+ }
+ return s
+}
+
+/** 面相报告 PDP:考拉 → 无尾熊(与其它端展示一致) */
+export function facePdpDisplayLabel(raw: string | undefined | null): string {
+ if (raw == null || raw === '') return ''
+ return String(raw).trim().replace(/考拉/g, '无尾熊')
+}
diff --git a/admin/src/views/admin/Settings.vue b/admin/src/views/admin/Settings.vue
index 94112df..ab2ef3c 100644
--- a/admin/src/views/admin/Settings.vue
+++ b/admin/src/views/admin/Settings.vue
@@ -47,6 +47,61 @@
+
+