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) }}
+
+
+
+
SBTI
+
+
+ {{ sbtiPanelSummary || '—' }}
+ (无 15 维 rawScores/levels,无法绘图)
+
+
暂无 SBTI 测评
+
+
@@ -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 @@