diff --git a/admin/src/components/UserDetailDialog.vue b/admin/src/components/UserDetailDialog.vue index 8a67b2a..25ba7d3 100644 --- a/admin/src/components/UserDetailDialog.vue +++ b/admin/src/components/UserDetailDialog.vue @@ -45,6 +45,11 @@
{{ shortOrDash(user.discType, 16) }}
+ +
+ {{ shortOrDash(user.sbtiType, 18) }} +
+
维度标签
@@ -73,6 +78,26 @@
暂无 DISC 测评
+
+
SBTI
+ +
+ {{ sbtiPanelSummary || '—' }} + (无 15 维 rawScores/levels,无法绘图) +
+
暂无 SBTI 测评
+
+ {{ sbtiPanelSummary }} +
+
@@ -200,6 +225,7 @@ import { DataAnalysis, DataLine, Document, + Grid, Key, OfficeBuilding, Phone, @@ -222,6 +248,7 @@ import { import VChart from 'vue-echarts' import { discTopTwoLabel, discCompactLabel } from '@/utils/discDisplay' import { buildFaceDetailFromParsed } from '@/utils/faceResultDetail' +import { SBTI_RADAR_DIMENSION_ORDER, buildSbtiRadarValues, formatSbtiSummary } from '@/utils/sbtiDisplay' use([CanvasRenderer, RadarChart, GridComponent, TooltipComponent, LegendComponent, RadarComponent]) @@ -315,6 +342,7 @@ function formatTestType(testType: string) { if (t === 'pdp') return 'PDP' if (t === 'face' || t === 'ai') return '人脸' if (t === 'resume') return '简历' + if (t === 'sbti') return 'SBTI' return testType } @@ -349,6 +377,10 @@ function extractTestSummary(test: any): string { const c = String(data.content ?? '') return c ? c.substring(0, 24).replace(/\n/g, ' ') + (c.length > 24 ? '…' : '') : '简历' } + if (type === 'sbti') { + const line = formatSbtiSummary(data) + if (line) return line + } return String(data.type ?? data.result ?? '') } @@ -374,6 +406,33 @@ function latestTest(type: string) { const latestMbti = computed(() => parseTestResult(latestTest('mbti'))) const latestPdp = computed(() => parseTestResult(latestTest('pdp'))) const latestDisc = computed(() => parseTestResult(latestTest('disc'))) +const latestSbti = computed(() => parseTestResult(latestTest('sbti'))) +const sbtiPanelSummary = computed(() => formatSbtiSummary(latestSbti.value)) + +const sbtiRadarOption = computed(() => { + const p = latestSbti.value + if (!p) return null + const values = buildSbtiRadarValues(p) + if (!values?.length) return null + const indicator = SBTI_RADAR_DIMENSION_ORDER.map(code => ({ name: code, max: 100 })) + return { + color: ['#5a7268'], + radar: { + indicator, + radius: 58, + splitNumber: 4, + axisName: { fontSize: 9, color: '#5a7268' } + }, + series: [ + { + type: 'radar', + data: [{ value: values, name: 'SBTI' }], + areaStyle: { opacity: 0.14 } + } + ], + tooltip: { trigger: 'item' } + } +}) const latestFace = computed(() => parseTestResult(latestTest('face')) || parseTestResult(latestTest('ai'))) const latestResume = computed(() => parseTestResult(latestTest('resume'))) @@ -391,6 +450,7 @@ const profileTags = computed(() => { 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.sbtiType) tags.push('SBTI-' + u.sbtiType) if (u.faceMbtiType) tags.push('面相MBTI') return tags }) @@ -550,6 +610,7 @@ function testIcon(testType: string) { if (t === 'mbti') return Aim if (t === 'pdp') return TrendCharts if (t === 'disc') return PieChart + if (t === 'sbti') return Grid if (t === 'face' || t === 'ai') return Picture if (t === 'resume') return Document return DataLine @@ -560,6 +621,7 @@ function testIconClass(testType: string) { if (t === 'mbti') return 'tic-mbti' if (t === 'pdp') return 'tic-pdp' if (t === 'disc') return 'tic-disc' + if (t === 'sbti') return 'tic-sbti' if (t === 'face' || t === 'ai') return 'tic-face' return 'tic-other' } @@ -679,6 +741,15 @@ function openMail(email: string) { } } +.ud-quick-stats__tile--sbti { + border-color: #dbe8e0; + background: #f2f7f3; + + .el-icon { + color: #5a7268; + } +} + .ud-dimension-tags { margin-top: 12px; } @@ -727,10 +798,16 @@ function openMail(email: string) { .ud-chart-row { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } +@media (max-width: 1200px) { + .ud-chart-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + .ud-chart-panel { background: #fafafa; border-radius: 8px; @@ -758,6 +835,11 @@ function openMail(email: string) { width: 100%; } +/* 15 维 SBTI 雷达略增高,轴标签较多 */ +.ud-chart-echart--sbti { + height: 200px; +} + .ud-chart-placeholder { height: 120px; display: flex; @@ -767,6 +849,40 @@ function openMail(email: string) { color: #9ca3af; } +.ud-chart-placeholder--sbti-fallback { + flex-direction: column; +} + +.ud-chart-panel--sbti { + background: #f2f7f3; + border: 1px solid #e3ebe6; +} + +.ud-chart-heading--sbti .el-icon { + color: #5a7268 !important; +} + +.ud-sbti-placeholder-text { + font-size: 11px; + color: #5a7268; + line-height: 1.4; + padding: 8px; + text-align: center; + word-break: break-all; +} + +.ud-sbti-radar-missing { + display: block; + font-size: 10px; + color: #9ca3af; + margin-top: 6px; + font-weight: 400; +} + +.ud-chart-footnote--sbti { + color: #5a7268; +} + .ud-chart-footnote { font-size: 11px; color: #6b7280; @@ -952,6 +1068,9 @@ function openMail(email: string) { .tic-disc { color: #2563eb; } +.tic-sbti { + color: #5a7268; +} .tic-face { color: #059669; } diff --git a/admin/src/utils/sbtiDisplay.ts b/admin/src/utils/sbtiDisplay.ts new file mode 100644 index 0000000..fdf61e8 --- /dev/null +++ b/admin/src/utils/sbtiDisplay.ts @@ -0,0 +1,119 @@ +/** + * SBTI 展示与类型图 URL(与小程序 miniprogram/utils/sbtiData.js TYPE_IMAGES 一致) + */ +export const SBTI_TYPE_IMAGES: Record = { + IMSB: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/IMSB.png', + BOSS: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/BOSS.png', + MUM: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/MUM.png', + FAKE: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/FAKE.png', + 'Dior-s': 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/Dior-s.jpg', + DEAD: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/DEAD.png', + ZZZZ: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/ZZZZ.png', + GOGO: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/GOGO.png', + FUCK: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/FUCK.png', + CTRL: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/CTRL.png', + HHHH: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/HHHH.png', + SEXY: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/SEXY.png', + OJBK: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/OJBK.png', + 'JOKE-R': 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/JOKE-R.jpg', + POOR: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/POOR.png', + 'OH-NO': 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/OH-NO.png', + MONK: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/MONK.png', + SHIT: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/SHIT.png', + 'THAN-K': 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/THAN-K.png', + MALO: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/MALO.png', + 'ATM-er': 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/ATM-er.png', + 'THIN-K': 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/THIN-K.png', + SOLO: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/SOLO.png', + 'LOVE-R': 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/LOVE-R.png', + 'WOC!': 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/WOC.png', + DRUNK: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/DRUNK.png', + IMFW: 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/SBTI/SBTI/IMFW.png' +} + +export function getSbtiCode(data: Record | null | undefined): string { + if (!data || typeof data !== 'object') return '' + const direct = String(data.sbtiType ?? '').trim() + if (direct) return direct + const ft = data.finalType + if (ft && typeof ft === 'object' && ft.code != null && String(ft.code).trim() !== '') { + return String(ft.code).trim() + } + return String(data.code ?? '').trim() +} + +export function getSbtiCn(data: Record | null | undefined): string { + if (!data || typeof data !== 'object') return '' + const cn = String(data.sbtiCn ?? '').trim() + if (cn) return cn + const ft = data.finalType + if (ft && typeof ft === 'object' && ft.cn != null && String(ft.cn).trim() !== '') { + return String(ft.cn).trim() + } + return '' +} + +/** 列表摘要、弹窗标题:CODE(中文名) */ +export function formatSbtiSummary(data: Record | null | undefined): string { + const code = getSbtiCode(data) + const cn = getSbtiCn(data) + if (code && cn) return `${code}(${cn})` + if (code) return code + if (cn) return cn + return '' +} + +export function sbtiTypeImageUrl(code: string): string { + if (!code) return '' + return SBTI_TYPE_IMAGES[code] || '' +} + +/** 与小程序 `sbtiData.dimensionOrder` 一致,用于雷达图 15 轴 */ +export const SBTI_RADAR_DIMENSION_ORDER = [ + 'S1', + 'S2', + 'S3', + 'E1', + 'E2', + 'E3', + 'A1', + 'A2', + 'A3', + 'Ac1', + 'Ac2', + 'Ac3', + 'So1', + 'So2', + 'So3' +] as const + +/** + * 从 SBTI 结果 JSON 生成 15 维雷达数值(0~100)。 + * 优先 `rawScores`(每维两题分和约 2~6);否则用 `levels` 的 L/M/H 映射。 + */ +export function buildSbtiRadarValues(parsed: Record | null | undefined): number[] | null { + if (!parsed || typeof parsed !== 'object') return null + const raw = parsed.rawScores as Record | undefined + const levels = parsed.levels as Record | undefined + const vals: number[] = [] + for (const dim of SBTI_RADAR_DIMENSION_ORDER) { + let v: number | null = null + if (raw && raw[dim] != null && raw[dim] !== '') { + const s = Number(raw[dim]) + if (Number.isFinite(s)) { + v = Math.round(((s - 2) / 4) * 100) + v = Math.max(0, Math.min(100, v)) + } + } + if (v == null && levels && levels[dim] != null && levels[dim] !== '') { + const lv = String(levels[dim]).toUpperCase() + if (lv === 'L') v = 34 + else if (lv === 'M') v = 67 + else if (lv === 'H') v = 100 + else v = 50 + } + if (v == null) return null + vals.push(v) + } + return vals +} diff --git a/admin/src/views/admin/Pricing.vue b/admin/src/views/admin/Pricing.vue index d6d28ee..195b6cb 100644 --- a/admin/src/views/admin/Pricing.vue +++ b/admin/src/views/admin/Pricing.vue @@ -42,6 +42,10 @@
+
+ + +
@@ -75,6 +79,10 @@
+
+ + +
@@ -107,8 +115,8 @@ const tabs = [ ] const activeTab = ref('personal') -const personal = reactive({ face: 0, mbti: 0, disc: 0, pdp: 0 }) -const enterprise = reactive({ face: 0, mbti: 0, disc: 0, pdp: 0 }) +const personal = reactive({ face: 0, mbti: 0, disc: 0, pdp: 0, sbti: 0 }) +const enterprise = reactive({ face: 0, mbti: 0, disc: 0, pdp: 0, sbti: 0 }) const loading = ref(false) const isUsingSuperAdminPersonalConfig = ref(false) @@ -161,7 +169,8 @@ const saveEnterprise = async () => { face: enterprise.face, mbti: enterprise.mbti, disc: enterprise.disc, - pdp: enterprise.pdp + pdp: enterprise.pdp, + sbti: enterprise.sbti } }) if (response.code === 200) { diff --git a/admin/src/views/admin/Questions.vue b/admin/src/views/admin/Questions.vue index d5c913f..7a8260b 100644 --- a/admin/src/views/admin/Questions.vue +++ b/admin/src/views/admin/Questions.vue @@ -3,7 +3,7 @@