report-result.vue 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. <template>
  2. <view class="container">
  3. <!-- ==================== Hero 头部 ==================== -->
  4. <view class="hero">
  5. <view class="hero-bg-circle hero-bg-circle-1"></view>
  6. <view class="hero-bg-circle hero-bg-circle-2"></view>
  7. <view class="hero-top">
  8. <text class="hero-name">{{ childName }}</text>
  9. <view class="hero-dim-row">
  10. <text class="hero-dim-badge">{{ dimensionLabel }}</text>
  11. <text class="hero-level-badge" v-if="level">等级 {{ level }}</text>
  12. <text class="hero-date">{{ dateText }}</text>
  13. </view>
  14. </view>
  15. <!-- 总分区 -->
  16. <view class="hero-score-wrap" v-if="totalScore !== null">
  17. <view class="hero-score-col">
  18. <text class="hero-score-num">{{ totalScore }}</text>
  19. <text class="hero-score-label">{{ percentile !== null ? '总得分' : '综合得分' }}</text>
  20. </view>
  21. <template v-if="percentile !== null">
  22. <view class="hero-score-divider"></view>
  23. <view class="hero-score-col">
  24. <text class="hero-score-num hero-score-num-pct">{{ percentile }}<text class="hero-score-pct-unit">%</text></text>
  25. <text class="hero-score-label">百分位</text>
  26. </view>
  27. </template>
  28. </view>
  29. <!-- 百分位进度条:位置标记 -->
  30. <view class="hero-pct-bar-wrap" v-if="percentile !== null">
  31. <view class="hero-pct-track">
  32. <view class="hero-pct-fill" :style="'width:' + percentile + '%'"></view>
  33. <view class="hero-pct-marker" :style="'left:' + percentile + '%'"></view>
  34. </view>
  35. <view class="hero-pct-foot">
  36. <text class="hero-pct-foot-text">超过 {{ percentile }}% 的同龄人</text>
  37. </view>
  38. </view>
  39. </view>
  40. <view class="body">
  41. <view class="loading-wrap" v-if="loading">
  42. <text class="loading-text">加载中...</text>
  43. </view>
  44. <template v-else-if="hasContent">
  45. <!-- ==================== 核心能力(认知/综合能力类,百分位制) ==================== -->
  46. <view class="card" v-if="abilityBlock">
  47. <view class="card-title-row">
  48. <view class="card-title-bar"></view>
  49. <text class="card-title">{{ abilityBlock.title }}</text>
  50. <text class="card-title-sub" v-if="!showAbilityRadar">百分位制,50 为同龄平均</text>
  51. </view>
  52. <!-- 认知能力:3 项及以上数值 → 雷达图 -->
  53. <view class="radar-wrap" v-if="showAbilityRadar">
  54. <RadarChart
  55. :dimensions="abilityRadarDims"
  56. :scores="abilityRadarScores"
  57. :population-scores="abilityRadarPopulation"
  58. fillColor="#6366F1"
  59. gridColor="#EEF0F6"
  60. labelColor="#1F2937"
  61. :width="600"
  62. :height="600"
  63. :child-name="childName" />
  64. </view>
  65. <view v-else>
  66. <view class="metric-row" v-for="(m, mi) in abilityMetrics" :key="mi">
  67. <view class="metric-head">
  68. <text class="metric-name">{{ m.name }}</text>
  69. <view class="metric-value-wrap">
  70. <text class="metric-warn-badge" v-if="m.warn">需关注</text>
  71. <text class="metric-value" :class="m.warn ? 'metric-value-warn' : ''">{{ m.value }}<text class="metric-unit">%</text></text>
  72. </view>
  73. </view>
  74. <view class="metric-track">
  75. <view class="metric-fill" :class="m.warn ? 'metric-fill-warn' : 'metric-fill-main'"
  76. :style="'width:' + m.pct + '%'"></view>
  77. <view class="metric-ref-line" style="left:50%"></view>
  78. </view>
  79. </view>
  80. </view>
  81. </view>
  82. <!-- ==================== 大五人格(1-5 分制,5 项 → 雷达图) ==================== -->
  83. <view class="card" v-if="bigFiveBlock">
  84. <view class="card-title-row">
  85. <view class="card-title-bar"></view>
  86. <text class="card-title">{{ bigFiveBlock.title }}</text>
  87. <text class="card-title-sub">1 ~ 5 分,虚线为同龄平均</text>
  88. </view>
  89. <view class="radar-wrap" v-if="showBigFiveRadar">
  90. <RadarChart
  91. :dimensions="bigFiveRadarDims"
  92. :scores="bigFiveRadarScores"
  93. :population-scores="bigFiveRadarPopulation"
  94. fillColor="#8B5CF6"
  95. gridColor="#F0ECFA"
  96. labelColor="#5B21B6"
  97. :width="600"
  98. :height="600"
  99. :child-name="childName" />
  100. </view>
  101. <view v-else>
  102. <view class="bf-row" v-for="(m, mi) in bigFiveMetrics" :key="mi">
  103. <view class="bf-head">
  104. <text class="bf-name">{{ m.name }}</text>
  105. <text class="bf-value">{{ m.value }}</text>
  106. </view>
  107. <view class="bf-track">
  108. <view class="bf-fill" :class="m.tone" :style="'width:' + m.pct + '%'"></view>
  109. <view class="bf-avg-line"></view>
  110. </view>
  111. <text class="bf-hint" v-if="m.hint">{{ m.hint }}</text>
  112. </view>
  113. </view>
  114. </view>
  115. <!-- ==================== 情绪状态(4 分测验,0-15 分制) ==================== -->
  116. <view class="card" v-if="emotionBlock">
  117. <view class="card-title-row">
  118. <view class="card-title-bar"></view>
  119. <text class="card-title">{{ emotionBlock.title }}</text>
  120. </view>
  121. <view class="emo-summary" v-if="emotionSummary.total || emotionSummary.avg">
  122. <text class="emo-summary-text">情绪不稳定总分 {{ emotionSummary.total || '-' }} 分,同龄平均 {{ emotionSummary.avg || '-' }} 分</text>
  123. </view>
  124. <view class="emo-row" v-for="(m, mi) in emotionMetrics" :key="mi">
  125. <view class="emo-chip">
  126. <text class="emo-name">{{ m.name }}</text>
  127. <text class="emo-value">{{ m.value }}</text>
  128. </view>
  129. <view class="emo-track">
  130. <view class="emo-fill" :style="'width:' + m.pct + '%'"></view>
  131. </view>
  132. </view>
  133. </view>
  134. <!-- ==================== 体质健康(身高/体重/BMI + 生活习惯) ==================== -->
  135. <view class="card" v-if="bodyBaseMetrics.length > 0 || bodyHabitMetrics.length > 0">
  136. <view class="card-title-row">
  137. <view class="card-title-bar"></view>
  138. <text class="card-title">体质健康</text>
  139. </view>
  140. <view class="info-grid" v-if="bodyBaseMetrics.length > 0">
  141. <view class="info-cell" v-for="(m, mi) in bodyBaseMetrics" :key="mi">
  142. <text class="info-value">{{ m.value }}<text class="info-unit" v-if="m.unit">{{ m.unit }}</text></text>
  143. <text class="info-name">{{ m.name }}</text>
  144. </view>
  145. </view>
  146. <view class="habit-row" v-for="(m, mi) in bodyHabitMetrics" :key="mi">
  147. <text class="habit-name">{{ m.name }}</text>
  148. <text class="habit-value">{{ m.value }}</text>
  149. </view>
  150. </view>
  151. <!-- ==================== 通用指标块(自我概念/自驱力等,量纲自适应) ==================== -->
  152. <view class="card" v-for="(blk, bi) in genericBlocks" :key="getBlockKey(bi)">
  153. <view class="card-title-row">
  154. <view class="card-title-bar"></view>
  155. <text class="card-title">{{ blk.title }}</text>
  156. </view>
  157. <view class="metric-row" v-for="(m, mi) in blk.metrics" :key="mi">
  158. <!-- 非数值指标(如依恋类型)只展示名称与取值,不画条 -->
  159. <view class="metric-plain-row" v-if="m.nonNumeric">
  160. <text class="metric-name">{{ m.name }}</text>
  161. <text class="metric-plain-value">{{ m.value }}</text>
  162. </view>
  163. <template v-else>
  164. <view class="metric-head">
  165. <text class="metric-name">{{ m.name }}</text>
  166. <view class="metric-value-wrap">
  167. <text class="metric-warn-badge" v-if="m.warn">需关注</text>
  168. <text class="metric-value" :class="m.warn ? 'metric-value-warn' : ''">{{ m.value }}</text>
  169. </view>
  170. </view>
  171. <view class="metric-track">
  172. <view class="metric-fill" :class="m.warn ? 'metric-fill-warn' : 'metric-fill-main'"
  173. :style="'width:' + m.pct + '%'"></view>
  174. </view>
  175. </template>
  176. </view>
  177. </view>
  178. <!-- ==================== 基本信息(身高体重等,过滤噪音) ==================== -->
  179. <view class="card" v-if="otherMetrics.length > 0">
  180. <view class="card-title-row">
  181. <view class="card-title-bar"></view>
  182. <text class="card-title">基本信息</text>
  183. </view>
  184. <view class="info-grid">
  185. <view class="info-cell" v-for="(m, mi) in otherMetrics" :key="mi">
  186. <text class="info-value">{{ m.value }}<text class="info-unit" v-if="m.unit">{{ m.unit }}</text></text>
  187. <text class="info-name">{{ m.name }}</text>
  188. </view>
  189. </view>
  190. </view>
  191. <!-- ==================== 成长建议(长文本折叠) ==================== -->
  192. <view class="card" v-if="adviceText">
  193. <view class="card-title-row">
  194. <view class="card-title-bar card-title-bar-gold"></view>
  195. <text class="card-title">{{ adviceTitle }}</text>
  196. </view>
  197. <text class="advice-content" :class="!adviceExpanded ? 'advice-clamped' : ''">{{ adviceText }}</text>
  198. <view class="advice-toggle" @click="adviceExpanded = !adviceExpanded" v-if="adviceText.length > 160">
  199. <text class="advice-toggle-text">{{ adviceExpanded ? '收起 ▲' : '展开全部 ▼' }}</text>
  200. </view>
  201. </view>
  202. <!-- ==================== 报告评语 ==================== -->
  203. <view class="card" v-if="summaryText">
  204. <view class="card-title-row">
  205. <view class="card-title-bar"></view>
  206. <text class="card-title">报告评语</text>
  207. </view>
  208. <text class="advice-content">{{ summaryText }}</text>
  209. </view>
  210. <view class="foot-note">
  211. <text class="foot-note-text">本报告由 DAN 测评系统生成,结果仅供成长参考</text>
  212. </view>
  213. </template>
  214. <view class="empty-wrap" v-else>
  215. <text class="empty-text">暂无数据</text>
  216. </view>
  217. </view>
  218. <view class="bottom-spacer"></view>
  219. </view>
  220. </template>
  221. <script>
  222. import { getDanReportDetail } from '../../utils/api.js'
  223. import RadarChart from '../../components/RadarChart.vue'
  224. // 噪音指标过滤(报告头/OCR 带出的非测评数据,对历史数据同样生效)
  225. var NOISE_NAME_KEYS = ['联系电话', '报告编码', '测试时间', '档案', '具体情况', '页', '公众号',
  226. '出生', '年份', '编码', '编号', '手机', '身份证', '总均分', '邮箱']
  227. // 百分位制 category(0-100)
  228. var PERCENT_CATEGORIES = ['overall', 'cognitive']
  229. export default {
  230. components: { RadarChart },
  231. data() {
  232. return {
  233. reportId: null,
  234. loading: true,
  235. childName: '',
  236. dateText: '',
  237. dimension: '',
  238. reportType: '',
  239. level: '',
  240. totalScore: null,
  241. percentile: null,
  242. blocks: [],
  243. catMap: {},
  244. adviceExpanded: false,
  245. adviceSourceTitle: '',
  246. adviceSourceText: '',
  247. summarySourceText: ''
  248. }
  249. },
  250. computed: {
  251. hasContent() {
  252. return this.blocks.length > 0
  253. },
  254. dimensionLabel() {
  255. var map = {
  256. mind: '心维度',
  257. wisdom: '智维度',
  258. cognition: '认知维度',
  259. learning: '学习能力',
  260. behavior: '行为教养',
  261. career: '职业发展',
  262. campus: '校园标准',
  263. adolescent: '青春期挑战'
  264. }
  265. var base = map[this.dimension] || ''
  266. if (this.reportType) base += ' · ' + this.reportType
  267. return base || 'DAN 测评'
  268. },
  269. // 核心能力块(优先"认知能力",其次含"能力"的块)
  270. abilityBlock() {
  271. var fallback = null
  272. for (var i = 0; i < this.blocks.length; i++) {
  273. var b = this.blocks[i]
  274. if (b.type !== 'indicator' || !b.items || !b.items.length) continue
  275. var t = String(b.title || '')
  276. if (t.indexOf('认知') >= 0) return b
  277. if (t.indexOf('能力') >= 0 && !fallback) fallback = b
  278. }
  279. return fallback
  280. },
  281. abilityMetrics() {
  282. if (!this.abilityBlock) return []
  283. var self = this
  284. return (this.abilityBlock.items || []).filter(function(it) {
  285. return !self.isNoise(it.name)
  286. }).map(function(it) {
  287. var v = parseFloat(it.value)
  288. var pct = isNaN(v) ? 0 : self.clampPct(v)
  289. return {
  290. name: String(it.name).replace('百分位', ''),
  291. value: it.value,
  292. pct: pct,
  293. warn: !isNaN(v) && v < 50
  294. }
  295. })
  296. },
  297. // 认知能力雷达图模式:标题含「认知」且数值项 >= 3
  298. showAbilityRadar() {
  299. var b = this.abilityBlock
  300. if (!b) return false
  301. var t = String(b.title || '')
  302. return t.indexOf('认知') >= 0 && this.abilityMetrics.length >= 3
  303. },
  304. abilityRadarDims() {
  305. return this.abilityMetrics.map(function(m) {
  306. return { label: m.name + ' ' + m.value + '%', key: m.name }
  307. })
  308. },
  309. abilityRadarScores() {
  310. return this.abilityMetrics.map(function(m) {
  311. var v = parseFloat(m.value)
  312. return isNaN(v) ? 0 : Math.min(100, Math.max(0, Math.round(v)))
  313. })
  314. },
  315. // 同龄平均位置:50 百分位
  316. abilityRadarPopulation() {
  317. var arr = []
  318. for (var i = 0; i < this.abilityMetrics.length; i++) { arr.push(50) }
  319. return arr
  320. },
  321. // 大五人格块
  322. bigFiveBlock() {
  323. for (var i = 0; i < this.blocks.length; i++) {
  324. var b = this.blocks[i]
  325. if (b.type === 'indicator' && b.title && String(b.title).indexOf('人格') >= 0) {
  326. return b
  327. }
  328. }
  329. return null
  330. },
  331. bigFiveMetrics() {
  332. if (!this.bigFiveBlock) return []
  333. return (this.bigFiveBlock.items || []).map(function(it) {
  334. var v = parseFloat(it.value)
  335. var pct = isNaN(v) ? 0 : Math.min(100, Math.max(0, v / 5 * 100))
  336. var tone = 'bf-neutral'
  337. var hint = ''
  338. if (!isNaN(v)) {
  339. if (v >= 4) { tone = 'bf-high'; hint = '突出优势' }
  340. else if (v <= 2.5) { tone = 'bf-low'; hint = '低于同龄平均' }
  341. }
  342. return { name: it.name, value: it.value, pct: Math.round(pct), tone: tone, hint: hint }
  343. })
  344. },
  345. // 大五人格雷达图模式:5 个维度齐全时用雷达图(1-5 分制换算成 0-100)
  346. showBigFiveRadar() {
  347. return this.bigFiveMetrics.length >= 3
  348. },
  349. bigFiveRadarDims() {
  350. return this.bigFiveMetrics.map(function(m) {
  351. return { label: m.name + ' ' + m.value, key: m.name }
  352. })
  353. },
  354. bigFiveRadarScores() {
  355. return this.bigFiveMetrics.map(function(m) {
  356. var v = parseFloat(m.value)
  357. return isNaN(v) ? 0 : Math.round(v / 5 * 100)
  358. })
  359. },
  360. // 同龄平均位置:3 分(1-5 分制)
  361. bigFiveRadarPopulation() {
  362. var arr = []
  363. for (var i = 0; i < this.bigFiveMetrics.length; i++) { arr.push(60) }
  364. return arr
  365. },
  366. // 情绪状态块
  367. emotionBlock() {
  368. for (var i = 0; i < this.blocks.length; i++) {
  369. var b = this.blocks[i]
  370. if (b.type === 'indicator' && b.title && String(b.title).indexOf('情绪') >= 0) {
  371. return b
  372. }
  373. }
  374. return null
  375. },
  376. emotionMetrics() {
  377. if (!this.emotionBlock) return []
  378. var self = this
  379. return (this.emotionBlock.items || []).filter(function(it) {
  380. if (self.isNoise(it.name)) return false
  381. var n = String(it.name)
  382. return n.indexOf('总得分') < 0 && n.indexOf('平均分') < 0
  383. }).map(function(it) {
  384. var v = parseFloat(it.value)
  385. // 分测验为 0-15 分制(PDF 标尺 1/10/15)
  386. var pct = isNaN(v) ? 0 : Math.min(100, Math.max(0, v / 15 * 100))
  387. return { name: it.name, value: it.value, pct: Math.round(pct) }
  388. })
  389. },
  390. // 情绪稳定性总分 / 同龄平均分(单独成行展示,不与分测验混在同一量纲)
  391. emotionSummary() {
  392. var out = { total: '', avg: '' }
  393. if (!this.emotionBlock) return out
  394. var items = this.emotionBlock.items || []
  395. for (var i = 0; i < items.length; i++) {
  396. var n = String(items[i].name)
  397. if (n.indexOf('总得分') >= 0) out.total = items[i].value
  398. else if (n.indexOf('平均分') >= 0) out.avg = items[i].value
  399. }
  400. return out
  401. },
  402. // 体质健康块(身高/体重/BMI + 饮食/睡眠/运动习惯)
  403. bodyBlock() {
  404. for (var i = 0; i < this.blocks.length; i++) {
  405. var b = this.blocks[i]
  406. if (b.type !== 'indicator' || !b.items || !b.items.length) continue
  407. var t = String(b.title || '')
  408. if (t.indexOf('体质') >= 0 || t.indexOf('健康') >= 0 || t.indexOf('身体') >= 0) {
  409. return b
  410. }
  411. }
  412. return null
  413. },
  414. bodyBlockItems() {
  415. var self = this
  416. if (!this.bodyBlock) return []
  417. return (this.bodyBlock.items || []).filter(function(it) {
  418. return !self.isNoise(it.name)
  419. })
  420. },
  421. // 体质卡片上部:数值型指标(身高/体重/BMI)
  422. bodyBaseMetrics() {
  423. return this.bodyBlockItems.filter(function(it) {
  424. var n = String(it.name)
  425. return n.indexOf('身高') >= 0 || n.indexOf('体重') >= 0 || n.indexOf('BMI') >= 0
  426. }).map(function(it) {
  427. var name = String(it.name)
  428. var unit = ''
  429. if (name.indexOf('身高') >= 0) unit = 'cm'
  430. else if (name.indexOf('体重') >= 0) unit = 'kg'
  431. return { name: name, value: it.value, unit: unit }
  432. })
  433. },
  434. // 体质卡片下部:等级与生活习惯(文本/量纲不一,用列表行展示)
  435. bodyHabitMetrics() {
  436. return this.bodyBlockItems.filter(function(it) {
  437. var n = String(it.name)
  438. return n.indexOf('身高') < 0 && n.indexOf('体重') < 0 && n.indexOf('BMI') < 0
  439. })
  440. },
  441. // 其他指标("其他指标"块,过滤噪音,提取身高体重等)
  442. otherMetrics() {
  443. for (var i = 0; i < this.blocks.length; i++) {
  444. var b = this.blocks[i]
  445. if (b.type === 'indicator' && b.title && String(b.title).indexOf('其他') >= 0) {
  446. var self = this
  447. return (b.items || []).filter(function(it) {
  448. return !self.isNoise(it.name)
  449. }).map(function(it) {
  450. var unit = ''
  451. var name = String(it.name)
  452. if (name.indexOf('身高') >= 0) { unit = 'cm' }
  453. if (name.indexOf('体重') >= 0) { unit = 'kg' }
  454. return { name: name, value: it.value, unit: unit }
  455. })
  456. }
  457. }
  458. return []
  459. },
  460. // 通用指标块(自我概念/自驱力/社会关系等未专项渲染的 indicator)
  461. genericBlocks() {
  462. var taken = []
  463. if (this.abilityBlock) taken.push(this.abilityBlock)
  464. if (this.bigFiveBlock) taken.push(this.bigFiveBlock)
  465. if (this.emotionBlock) taken.push(this.emotionBlock)
  466. if (this.bodyBlock) taken.push(this.bodyBlock)
  467. var self = this
  468. return this.blocks.filter(function(b) {
  469. if (b.type !== 'indicator') return false
  470. if (taken.indexOf(b) >= 0) return false
  471. if (b.title && String(b.title).indexOf('其他') >= 0) return false
  472. return b.items && b.items.length > 0
  473. }).map(function(b) {
  474. var title = String(b.title || '')
  475. return {
  476. title: b.title,
  477. metrics: (b.items || []).filter(function(it) {
  478. return !self.isNoise(it.name)
  479. }).map(function(it) {
  480. return self.toMetric(it, title)
  481. })
  482. }
  483. }).filter(function(b) { return b.metrics.length > 0 })
  484. },
  485. adviceTitle() {
  486. return this.adviceSourceTitle || '成长建议'
  487. },
  488. adviceText() {
  489. return this.adviceSourceText || ''
  490. },
  491. summaryText() {
  492. return this.summarySourceText || ''
  493. }
  494. },
  495. onLoad(options) {
  496. this.reportId = options.reportId ? parseInt(options.reportId) : null
  497. if (this.reportId) this.loadData()
  498. },
  499. methods: {
  500. getBlockKey(bi) {
  501. return 'g' + bi
  502. },
  503. isNoise(name) {
  504. if (!name) return true
  505. for (var i = 0; i < NOISE_NAME_KEYS.length; i++) {
  506. if (String(name).indexOf(NOISE_NAME_KEYS[i]) >= 0) return true
  507. }
  508. return false
  509. },
  510. clampPct(v) {
  511. return Math.round(Math.min(100, Math.max(0, v)))
  512. },
  513. // 依据 category / 名称推断量纲上限
  514. resolveMax(name, category) {
  515. if (name && String(name).indexOf('百分位') >= 0) return 100
  516. if (category === 'bigFive') return 5
  517. if (category === 'social') return 100
  518. if (PERCENT_CATEGORIES.indexOf(category) >= 0) return 100
  519. return 10
  520. },
  521. // blockTitle:调用方(通用指标块)可显式指定量纲,如社会关系传 100
  522. toMetric(it, blockTitle) {
  523. var v = parseFloat(it.value)
  524. if (isNaN(v)) {
  525. // 非数值(如依恋类型「安全型/安全型/安全型」)不画条,仅展示取值
  526. return { name: it.name, value: it.value, pct: 0, warn: false, nonNumeric: true }
  527. }
  528. var cat = this.catMap[it.name] || ''
  529. var max = null
  530. if (blockTitle) {
  531. var t = String(blockTitle)
  532. if (t.indexOf('社会') >= 0 || t.indexOf('关系') >= 0) max = 100
  533. }
  534. if (max === null) max = this.resolveMax(it.name, cat)
  535. var pct = Math.min(100, Math.max(0, v / max * 100))
  536. // 仅百分位制指标(A2 认知等)低于 50 才告警;人格/自我概念等非能力项不告警
  537. var warn = (max === 100) && v < 50
  538. return { name: it.name, value: it.value, pct: Math.round(pct), warn: warn, nonNumeric: false }
  539. },
  540. loadData() {
  541. var self = this
  542. this.loading = true
  543. getDanReportDetail(this.reportId).then(function(res) {
  544. self.loading = false
  545. if (res.code !== 200 || !res.data) {
  546. uni.showToast({ title: res.message || '加载失败', icon: 'none' })
  547. return
  548. }
  549. var data = res.data
  550. var result = data.result || {}
  551. self.childName = result.childName || data.childName || ''
  552. var dateRaw = result.assessmentDate || data.confirmedAt || ''
  553. self.dateText = dateRaw ? String(dateRaw).substring(0, 10) : ''
  554. self.dimension = data.dimension || ''
  555. self.reportType = data.reportType || ''
  556. self.level = result.danLevel || ''
  557. self.blocks = (data.blocks || []).filter(function(b) {
  558. return b && ((b.items && b.items.length) || b.content)
  559. })
  560. // 从 structuredAnalysis 构建 名称→category 映射(blocks item 已丢 category)
  561. var catMap = {}
  562. if (result.structuredAnalysis) {
  563. try {
  564. var arr = JSON.parse(result.structuredAnalysis)
  565. for (var ai = 0; ai < arr.length; ai++) {
  566. if (arr[ai] && arr[ai].name) catMap[arr[ai].name] = arr[ai].category || ''
  567. }
  568. } catch (e) {}
  569. }
  570. self.catMap = catMap
  571. // 提取总分/百分位(score 块)
  572. for (var i = 0; i < self.blocks.length; i++) {
  573. var b = self.blocks[i]
  574. if (b.type === 'score' && b.items) {
  575. for (var j = 0; j < b.items.length; j++) {
  576. var it = b.items[j]
  577. var label = String(it.label)
  578. if (label.indexOf('总') >= 0) {
  579. var tv = parseFloat(it.value)
  580. if (!isNaN(tv)) self.totalScore = tv
  581. } else if (label.indexOf('百分位') >= 0) {
  582. var pv = parseFloat(it.value)
  583. if (!isNaN(pv)) self.percentile = Math.min(100, Math.max(0, pv))
  584. }
  585. }
  586. }
  587. }
  588. // 兜底:无 score 块(如 B4 智维度)时,用 result.overallScore 作综合得分
  589. if (self.totalScore === null && result.overallScore !== null && result.overallScore !== undefined) {
  590. var ov = parseFloat(result.overallScore)
  591. if (!isNaN(ov)) self.totalScore = ov
  592. }
  593. // text 块:成长建议优先(非空 content),评语单独
  594. for (var k = 0; k < self.blocks.length; k++) {
  595. var tb = self.blocks[k]
  596. if (tb.type === 'text' && tb.content) {
  597. if (String(tb.title).indexOf('建议') >= 0) {
  598. self.adviceSourceTitle = tb.title
  599. self.adviceSourceText = tb.content
  600. } else {
  601. self.summarySourceText = tb.content
  602. }
  603. }
  604. }
  605. // 兜底:无 text 建议块时用 result.growthSuggestions
  606. if (!self.adviceSourceText && result.growthSuggestions) {
  607. self.adviceSourceText = result.growthSuggestions
  608. }
  609. }).catch(function(e) {
  610. self.loading = false
  611. uni.showToast({ title: '加载异常', icon: 'none' })
  612. })
  613. }
  614. }
  615. }
  616. </script>
  617. <style scoped>
  618. .container { min-height: 100vh; background: #F3F2FA; padding-bottom: 40rpx; }
  619. /* ==================== Hero ==================== */
  620. .hero {
  621. position: relative;
  622. background: linear-gradient(160deg, #4F46E5 0%, #7C3AED 55%, #8B5CF6 100%);
  623. padding: 48rpx 36rpx 44rpx;
  624. overflow: hidden;
  625. border-bottom-left-radius: 36rpx;
  626. border-bottom-right-radius: 36rpx;
  627. }
  628. .hero-bg-circle { position: absolute; border-radius: 50%; background: rgba(255,255,255,0.06); }
  629. .hero-bg-circle-1 { width: 380rpx; height: 380rpx; right: -120rpx; top: -140rpx; }
  630. .hero-bg-circle-2 { width: 220rpx; height: 220rpx; left: -80rpx; bottom: -60rpx; }
  631. .hero-top { position: relative; z-index: 1; }
  632. .hero-name { font-size: 44rpx; font-weight: 700; color: #FFFFFF; display: block; }
  633. .hero-dim-row { display: flex; align-items: center; margin-top: 14rpx; flex-wrap: wrap; }
  634. .hero-dim-badge {
  635. font-size: 22rpx; color: #FFFFFF;
  636. background: rgba(255,255,255,0.22);
  637. border-radius: 24rpx; padding: 6rpx 18rpx;
  638. margin-right: 14rpx;
  639. }
  640. .hero-level-badge {
  641. font-size: 22rpx; color: #7C2D12; font-weight: 600;
  642. background: linear-gradient(135deg, #FDE68A, #FBBF24);
  643. border-radius: 24rpx; padding: 6rpx 18rpx;
  644. margin-right: 14rpx;
  645. }
  646. .hero-date { font-size: 24rpx; color: rgba(255,255,255,0.75); }
  647. .hero-score-wrap {
  648. position: relative; z-index: 1;
  649. display: flex; align-items: center; justify-content: center;
  650. margin-top: 36rpx;
  651. }
  652. .hero-score-col { flex: 1; display: flex; flex-direction: column; align-items: center; }
  653. .hero-score-divider { width: 1rpx; height: 72rpx; background: rgba(255,255,255,0.25); }
  654. .hero-score-num { font-size: 88rpx; font-weight: 700; color: #FFFFFF; line-height: 1; }
  655. .hero-score-num-pct { font-size: 72rpx; }
  656. .hero-score-pct-unit { font-size: 32rpx; font-weight: 400; margin-left: 2rpx; }
  657. .hero-score-label { font-size: 24rpx; color: rgba(255,255,255,0.75); margin-top: 12rpx; }
  658. .hero-pct-bar-wrap { position: relative; z-index: 1; margin-top: 32rpx; }
  659. .hero-pct-track {
  660. position: relative; height: 14rpx; border-radius: 8rpx;
  661. background: rgba(255,255,255,0.18);
  662. }
  663. .hero-pct-fill {
  664. height: 100%; border-radius: 8rpx;
  665. background: linear-gradient(90deg, #A5F3FC, #67E8F9, #FDE68A);
  666. }
  667. .hero-pct-marker {
  668. position: absolute; top: -6rpx; width: 4rpx; height: 26rpx;
  669. background: #FFFFFF; border-radius: 2rpx; transform: translateX(-2rpx);
  670. }
  671. .hero-pct-foot { margin-top: 12rpx; text-align: center; }
  672. .hero-pct-foot-text { font-size: 22rpx; color: rgba(255,255,255,0.85); }
  673. /* ==================== 通用卡片 ==================== */
  674. .body { padding: 28rpx 24rpx 0; }
  675. .card {
  676. background: #FFFFFF;
  677. border-radius: 20rpx;
  678. padding: 28rpx 26rpx;
  679. margin-bottom: 22rpx;
  680. box-shadow: 0 4rpx 16rpx rgba(99,102,241,0.06);
  681. }
  682. .card-title-row { display: flex; align-items: baseline; margin-bottom: 24rpx; }
  683. .card-title-bar {
  684. width: 8rpx; height: 30rpx; border-radius: 4rpx;
  685. background: linear-gradient(180deg, #6366F1, #8B5CF6);
  686. margin-right: 12rpx; align-self: center;
  687. }
  688. .card-title-bar-gold { background: linear-gradient(180deg, #F59E0B, #F97316); }
  689. .card-title { font-size: 30rpx; font-weight: 600; color: #1F2937; }
  690. .card-title-sub { font-size: 22rpx; color: #9CA3AF; margin-left: 14rpx; }
  691. /* ==================== 指标条 ==================== */
  692. .metric-row { margin-bottom: 26rpx; }
  693. .metric-row:last-child { margin-bottom: 4rpx; }
  694. .metric-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
  695. .metric-name { font-size: 26rpx; color: #374151; font-weight: 500; }
  696. .metric-value-wrap { display: flex; align-items: center; }
  697. .metric-value { font-size: 30rpx; font-weight: 700; color: #4F46E5; }
  698. .metric-value-warn { color: #EA580C; }
  699. .metric-unit { font-size: 20rpx; font-weight: 400; }
  700. .metric-warn-badge {
  701. font-size: 20rpx; color: #EA580C;
  702. background: #FFF7ED; border: 1rpx solid #FDBA74;
  703. border-radius: 16rpx; padding: 2rpx 12rpx; margin-right: 10rpx;
  704. }
  705. .metric-track {
  706. position: relative; height: 16rpx; border-radius: 8rpx; background: #EEF0F6;
  707. }
  708. .metric-fill { height: 100%; border-radius: 8rpx; }
  709. .metric-fill-main { background: linear-gradient(90deg, #818CF8, #6366F1); }
  710. .metric-fill-warn { background: linear-gradient(90deg, #FDBA74, #EA580C); }
  711. .metric-ref-line {
  712. position: absolute; top: -4rpx; width: 2rpx; height: 24rpx;
  713. background: #C7C9D9; border-radius: 1rpx;
  714. }
  715. /* 非数值指标行(名称 + 取值,无进度条) */
  716. .metric-plain-row {
  717. display: flex; justify-content: space-between; align-items: center;
  718. padding: 10rpx 0;
  719. }
  720. .metric-plain-value { font-size: 26rpx; color: #4F46E5; font-weight: 600; }
  721. /* ==================== 大五人格 ==================== */
  722. .bf-row { margin-bottom: 24rpx; }
  723. .bf-row:last-child { margin-bottom: 4rpx; }
  724. .bf-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10rpx; }
  725. .bf-name { font-size: 26rpx; color: #374151; font-weight: 500; }
  726. .bf-value { font-size: 30rpx; font-weight: 700; color: #7C3AED; }
  727. .bf-track { position: relative; height: 14rpx; border-radius: 7rpx; background: #F0ECFA; }
  728. .bf-fill { height: 100%; border-radius: 7rpx; }
  729. .bf-neutral { background: linear-gradient(90deg, #C4B5FD, #8B5CF6); }
  730. .bf-high { background: linear-gradient(90deg, #6EE7B7, #10B981); }
  731. .bf-low { background: linear-gradient(90deg, #FCD34D, #F59E0B); }
  732. .bf-avg-line {
  733. position: absolute; top: -5rpx; left: 60%; width: 2rpx; height: 24rpx;
  734. background: #9CA3AF; border-radius: 1rpx;
  735. }
  736. .bf-hint { display: block; font-size: 20rpx; color: #9CA3AF; margin-top: 6rpx; }
  737. /* ==================== 情绪状态 ==================== */
  738. .emo-row { display: flex; align-items: center; margin-bottom: 20rpx; }
  739. .emo-row:last-child { margin-bottom: 4rpx; }
  740. .emo-chip {
  741. display: flex; align-items: baseline;
  742. background: #F5F3FF; border-radius: 14rpx;
  743. padding: 10rpx 18rpx; margin-right: 18rpx; flex-shrink: 0;
  744. }
  745. .emo-name { font-size: 24rpx; color: #6D28D9; margin-right: 12rpx; }
  746. .emo-value { font-size: 28rpx; font-weight: 700; color: #7C3AED; }
  747. .emo-track { flex: 1; height: 12rpx; border-radius: 6rpx; background: #F0ECFA; overflow: hidden; }
  748. .emo-fill { height: 100%; border-radius: 6rpx; background: linear-gradient(90deg, #A78BFA, #7C3AED); }
  749. /* ==================== 雷达图容器 ==================== */
  750. .radar-wrap {
  751. display: flex;
  752. flex-direction: column;
  753. align-items: center;
  754. margin-bottom: 8rpx;
  755. }
  756. /* ==================== 情绪总分行 ==================== */
  757. .emo-summary {
  758. background: #F5F3FF;
  759. border-radius: 14rpx;
  760. padding: 16rpx 20rpx;
  761. margin-bottom: 20rpx;
  762. }
  763. .emo-summary-text { font-size: 24rpx; color: #6D28D9; }
  764. /* ==================== 体质健康 ==================== */
  765. .habit-row {
  766. display: flex;
  767. justify-content: space-between;
  768. align-items: center;
  769. padding: 14rpx 0;
  770. border-bottom: 1rpx solid #F1F2F7;
  771. }
  772. .habit-row:last-child { border-bottom: none; }
  773. .habit-name { font-size: 26rpx; color: #374151; }
  774. .habit-value { font-size: 26rpx; color: #4F46E5; font-weight: 600; }
  775. /* ==================== 基本信息 ==================== */
  776. .info-grid { display: flex; flex-wrap: wrap; }
  777. .info-cell {
  778. width: 33.33%; display: flex; flex-direction: column; align-items: center;
  779. margin-bottom: 16rpx;
  780. }
  781. .info-value { font-size: 36rpx; font-weight: 700; color: #1F2937; }
  782. .info-unit { font-size: 20rpx; font-weight: 400; color: #9CA3AF; margin-left: 4rpx; }
  783. .info-name { font-size: 22rpx; color: #9CA3AF; margin-top: 6rpx; }
  784. /* ==================== 成长建议/评语 ==================== */
  785. .advice-content {
  786. font-size: 26rpx; color: #4B5563; line-height: 1.8;
  787. white-space: pre-wrap; word-break: break-all;
  788. }
  789. .advice-clamped {
  790. max-height: 320rpx; overflow: hidden;
  791. -webkit-mask-image: linear-gradient(180deg, #000 60%, transparent 100%);
  792. mask-image: linear-gradient(180deg, #000 60%, transparent 100%);
  793. }
  794. .advice-toggle { text-align: center; margin-top: 16rpx; }
  795. .advice-toggle-text { font-size: 24rpx; color: #6366F1; font-weight: 500; }
  796. .foot-note { text-align: center; padding: 18rpx 0 8rpx; }
  797. .foot-note-text { font-size: 20rpx; color: #B0B3C3; }
  798. /* ==================== 其他 ==================== */
  799. .loading-wrap { display: flex; justify-content: center; padding: 140rpx 0; }
  800. .loading-text { font-size: 28rpx; color: #999; }
  801. .empty-wrap { display: flex; justify-content: center; padding: 140rpx 0; }
  802. .empty-text { font-size: 28rpx; color: #999; }
  803. .bottom-spacer { height: 60rpx; }
  804. </style>