index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. <template>
  2. <view class="wisdom-container">
  3. <!-- Tabbar 切换过渡页 -->
  4. <tab-transition v-if="showTabTransition" dimCode="wisdom" ref="tabTransition" @close="showTabTransition = false" />
  5. <!-- 1. 品牌头部 -->
  6. <PageBanner theme="wisdom"
  7. tagline="学以启智 · 慧及未来"
  8. quote="知者不惑,仁者不忧" />
  9. <!-- 品牌定位语(登录前显示) -->
  10. <view class="brand-strap" v-if="!isLoggedIn">
  11. <text class="brand-strap-text">提升全家认知能力与学习效率</text>
  12. </view>
  13. <!-- 2. 认知雷达图(始终显示,无数据时仅显示人群平均值) -->
  14. <view class="section">
  15. <view class="section-header">
  16. <text class="section-title">认知雷达</text>
  17. <text class="avg-badge" v-if="!cognitiveReport">同龄平均值 · 完成测评后显示个人数据</text>
  18. <text class="section-more" v-if="isLoggedIn" @click="goCognitiveReport">查看详情 ›</text>
  19. </view>
  20. <RadarChart
  21. :dimensions="cognitiveDimensions"
  22. :scores="cognitiveScores"
  23. :avg-scores="cognitiveAvgScores"
  24. :childName="activeChildName"
  25. fillColor="#6366F1"
  26. gridColor="#E0E7FF"
  27. labelColor="#3730A3" />
  28. </view>
  29. <!-- 家庭成员选择条 -->
  30. <FamilyMemberStrip
  31. v-if="isLoggedIn"
  32. :members="familyMembersVisible"
  33. :selectedMemberId="selectedMemberId"
  34. @select="onFamilyMemberSelect"
  35. @removed="loadFamilyMembersVisible" />
  36. <!-- 游客引导语 -->
  37. <view class="monthly-guidance" v-if="!isLoggedIn">
  38. <text class="guidance-icon">⚡</text>
  39. <text class="guidance-text">{{ monthlyGuidance }}</text>
  40. </view>
  41. <!-- 4. 五维能量条(登录后) -->
  42. <FamilyEnergyBar
  43. v-if="isLoggedIn"
  44. dimensionCode="wisdom"
  45. :sandboxData="sandboxData"
  46. :dualDimension="dualDimension" />
  47. <!-- ===== DAN 报告(登录后) ===== -->
  48. <view class="section" v-if="isLoggedIn">
  49. <view class="section-header">
  50. <text class="section-title">&#x1F4CA; DAN 报告</text>
  51. <text class="section-more" @click="goDanUpload">上传 +</text>
  52. </view>
  53. <view class="dan-card" v-for="report in danReports" :key="report.id" @click="viewDanReport(report)">
  54. <view class="dan-card-header">
  55. <text class="dan-report-date">{{ formatDate(report.assessmentDate) }}</text>
  56. <text :class="['dan-status', report.draftStatus === 'confirmed' ? 'confirmed' : 'pending']">
  57. {{ report.draftStatus === 'confirmed' ? '已确认' : '待确认' }}
  58. </text>
  59. </view>
  60. <text class="dan-report-desc">DAN B4 智维度测评报告</text>
  61. </view>
  62. <view class="dan-empty" v-if="danReports.length === 0">
  63. <text class="dan-empty-text">暂无 DAN 报告,点击上方「上传 +」上传测评报告</text>
  64. </view>
  65. </view>
  66. <!-- 6. 功能入口 -->
  67. <view class="func-section" v-if="sectionVisible('func_entries')">
  68. <view class="func-grid">
  69. <view class="func-item" v-for="item in funcList" :key="item.label" @click="onFuncClick(item)">
  70. <view class="func-icon-wrap">
  71. <text class="func-icon">{{ item.icon }}</text>
  72. </view>
  73. <text class="func-label">{{ item.label }}</text>
  74. </view>
  75. </view>
  76. </view>
  77. <!-- 11. 快捷操作按钮 -->
  78. <view class="section quick-actions" v-if="isLoggedIn">
  79. <view class="action-btn" @click="goCognitiveReport">
  80. <text class="action-icon">📊</text>
  81. <text class="action-text">认知报告</text>
  82. </view>
  83. <view class="action-btn" @click="goTrainingHub">
  84. <text class="action-icon">🧠</text>
  85. <text class="action-text">认知训练</text>
  86. </view>
  87. <view class="action-btn" @click="goAssessment">
  88. <text class="action-icon">📋</text>
  89. <text class="action-text">测评预约</text>
  90. </view>
  91. <view class="action-btn" @click="goDanUpload">
  92. <text class="action-icon">📄</text>
  93. <text class="action-text">上传报告</text>
  94. </view>
  95. </view>
  96. <!-- 智维度介绍卡片(登录后显示) -->
  97. <DimensionIntroCard dimCode="wisdom" v-if="isLoggedIn" learn-more-url="/pages/wisdom-detail/dimension-intro" />
  98. <!-- 底部占位 -->
  99. <view class="bottom-spacer"></view>
  100. <AIFloatingAvatar />
  101. </view>
  102. </template>
  103. <script>
  104. import nav from '../../utils/nav'
  105. import TabTransition from '../../components/tab-transition.vue'
  106. import PageBanner from '../../components/PageBanner.vue'
  107. import RadarChart from '../../components/RadarChart.vue'
  108. import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
  109. import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
  110. import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
  111. import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
  112. import { getVisibleSections, getChildren, getFamilyEnergySandbox, getAssessmentLatestResult, getDanReportByChild } from '../../utils/api.js'
  113. import { parseDate } from '../../utils/format.js'
  114. export default {
  115. components: { TabTransition, PageBanner, RadarChart, FamilyEnergyBar, AIFloatingAvatar, FamilyMemberStrip, DimensionIntroCard },
  116. data() {
  117. return {
  118. isLoggedIn: false,
  119. role: null,
  120. showTabTransition: true,
  121. activeChildId: null,
  122. activeChildName: '',
  123. userName: '',
  124. children: [],
  125. familyMembersVisible: [],
  126. selectedMemberId: null,
  127. sandboxData: null,
  128. dualDimension: null,
  129. visibleSections: [],
  130. cognitiveReport: null,
  131. articles: [],
  132. /** Mock 平均值(7维认知):感知/专注/记忆/逻辑/空间/加工速度/成长型思维 */
  133. cognitiveAvgScores: [70, 65, 72, 68, 66, 71, 70],
  134. danReports: [],
  135. funcList: [
  136. { icon: '\u{1F4CA}', label: '认知报告', needLogin: true, page: '/pages/wisdom-detail/cognitive-report' },
  137. { icon: '\u{1F9E0}', label: '训练中心', needLogin: true, page: '/pages/wisdom-detail/training-hub' },
  138. { icon: '\u{1F3AF}', label: '测评预约', needLogin: false, page: '/pages/assessment/apply' },
  139. { icon: '\u{1F4D6}', label: '阅读天地', needLogin: false, page: '/pages/mind-extra/articles' }
  140. ]
  141. }
  142. },
  143. computed: {
  144. wisdomArticles: function() {
  145. if (!this.articles || this.articles.length === 0) return []
  146. var result = []
  147. for (var i = 0; i < this.articles.length; i++) {
  148. var a = this.articles[i]
  149. if (a.relatedDimensions && a.relatedDimensions.indexOf('wisdom') !== -1) {
  150. result.push(a)
  151. }
  152. }
  153. if (result.length === 0) return this.articles.slice(0, 5)
  154. return result.slice(0, 5)
  155. },
  156. cognitiveDimensions: function() {
  157. return [
  158. { label: '感知能力', key: 'perceptionScore' },
  159. { label: '专注力', key: 'focusScore' },
  160. { label: '记忆力', key: 'memoryScore' },
  161. { label: '逻辑思维', key: 'logicScore' },
  162. { label: '空间思维', key: 'spatialScore' },
  163. { label: '加工速度', key: 'processingSpeedScore' },
  164. { label: '成长型思维', key: 'wisdomGrowthMindset' }
  165. ]
  166. },
  167. cognitiveScores: function() {
  168. if (!this.cognitiveReport) return []
  169. var dims = this.cognitiveDimensions
  170. var scores = []
  171. // 解析 structuredAnalysis 中的成长型思维
  172. var growthMindset = 0
  173. if (this.cognitiveReport.structuredAnalysis) {
  174. try {
  175. var analysis = JSON.parse(this.cognitiveReport.structuredAnalysis)
  176. if (analysis && analysis.extra && analysis.extra.growthMindset) {
  177. var gm = analysis.extra.growthMindset
  178. var vals = Object.values(gm)
  179. if (vals.length > 0) growthMindset = parseInt(vals[0]) || 0
  180. }
  181. } catch (e) { /* ignore parse error */ }
  182. }
  183. for (var i = 0; i < dims.length; i++) {
  184. var key = dims[i].key
  185. if (key === 'wisdomGrowthMindset') {
  186. scores.push(growthMindset)
  187. } else {
  188. scores.push(this.cognitiveReport[key] || 0)
  189. }
  190. }
  191. return scores
  192. },
  193. /** 每月引导语 */
  194. monthlyGuidance: function() {
  195. var month = new Date().getMonth()
  196. var messages = [
  197. '一月:新年新目标,制定学习计划',
  198. '二月:春节收获知识,拓展认知边界',
  199. '三月:植树节到,播种智慧种子',
  200. '四月:清明踏青,激发创造力',
  201. '五月:劳动节后,专注能力提升',
  202. '六月:考试季,科学备考策略',
  203. '七月:暑期黄金期,查漏补缺',
  204. '八月:开学准备,提前规划学习',
  205. '九月:新学期,设定学习目标',
  206. '十月:国庆假期,阅读充电',
  207. '十一月:年终冲刺,巩固知识',
  208. '十二月:年度总结,规划来年学习'
  209. ]
  210. return messages[month] || messages[0]
  211. }
  212. },
  213. onShow() {
  214. this.showTabTransition = true
  215. var token = uni.getStorageSync('token')
  216. this.isLoggedIn = !!token
  217. if (this.isLoggedIn) {
  218. this.role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
  219. this.userName = uni.getStorageSync('nickname') || uni.getStorageSync('userName') || '家长'
  220. }
  221. this.loadSectionConfig()
  222. if (this.isLoggedIn) {
  223. this.loadChildren()
  224. this.loadFamilyMembersVisible()
  225. this.loadCognitiveReport()
  226. this.loadDanReports()
  227. }
  228. // 游客也能浏览文章
  229. this._watchPageReady()
  230. },
  231. methods: {
  232. onFamilyMemberSelect: function(member) {
  233. this.selectedMemberId = member.id
  234. },
  235. loadSectionConfig: function() {
  236. var baseSections = ['func_entries', 'recommended_reading']
  237. if (!uni.getStorageSync('token')) {
  238. this.visibleSections = baseSections
  239. return
  240. }
  241. var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
  242. var self = this
  243. getVisibleSections({ pageKey: 'wisdom', role: role }).then(function(res) {
  244. if (res.code === 200 && res.data) {
  245. var keys = res.data.map(function(s) { return s.sectionKey })
  246. var merged = baseSections.slice()
  247. keys.forEach(function(k) { if (merged.indexOf(k) === -1) merged.push(k) })
  248. self.visibleSections = merged
  249. }
  250. }).catch(function(e) {
  251. self.visibleSections = ['func_entries', 'recommended_reading']
  252. })
  253. },
  254. loadChildren: function() {
  255. var self = this
  256. getChildren().then(function(res) {
  257. if (res.code === 200 && res.data) {
  258. self.children = res.data
  259. if (self.children.length > 0) {
  260. var activeId = uni.getStorageSync('currentChildId')
  261. if (activeId) {
  262. for (var i = 0; i < self.children.length; i++) {
  263. if (self.children[i].id == activeId) {
  264. self.activeChildId = activeId
  265. self.activeChildName = self.children[i].nickname || self.children[i].name || ''
  266. break
  267. }
  268. }
  269. }
  270. // Fallback: 首次进入或未选孩子时,默认选第一个孩子以加载认知数据(仅本地状态,不触发全局切换)
  271. if (!self.activeChildId && self.children.length > 0) {
  272. self.activeChildId = self.children[0].id
  273. self.activeChildName = self.children[0].nickname || self.children[0].name || ''
  274. }
  275. }
  276. // Load cognitive report if activeChildId is available
  277. if (self.activeChildId) {
  278. self.loadCognitiveReport()
  279. }
  280. }
  281. }).catch(function(e) {})
  282. },
  283. loadFamilyMembersVisible: function() {
  284. var self = this
  285. getFamilyEnergySandbox().then(function(res) {
  286. if (res && res.data) {
  287. self.sandboxData = res.data.sandboxData || null
  288. self.dualDimension = res.data.dualDimension || null
  289. if (res.data.members) {
  290. self.familyMembersVisible = res.data.members
  291. }
  292. }
  293. }).catch(function(e) {})
  294. },
  295. loadCognitiveReport: function() {
  296. var self = this
  297. var memberId = this.activeChildId
  298. if (!memberId) return
  299. getAssessmentLatestResult(memberId).then(function(res) {
  300. if (res.code === 200 && res.data) {
  301. self.cognitiveReport = res.data
  302. }
  303. }).catch(function(e) {})
  304. },
  305. onChildChanged: function(memberId) {
  306. this.activeChildId = memberId
  307. // FIX: 同步提交到 Vuex store
  308. this.$store.commit('switchToChild', memberId)
  309. for (var i = 0; i < this.children.length; i++) {
  310. if (this.children[i].id == memberId) {
  311. this.activeChildName = this.children[i].nickname || this.children[i].name || ''
  312. break
  313. }
  314. }
  315. this.loadFamilyMembersVisible()
  316. this.loadCognitiveReport()
  317. this.loadDanReports()
  318. },
  319. sectionVisible: function(key) {
  320. return this.visibleSections.length === 0 || this.visibleSections.indexOf(key) !== -1
  321. },
  322. onFuncClick: function(item) {
  323. if (item.needLogin && !this.isLoggedIn) {
  324. uni.showModal({
  325. title: '提示',
  326. content: '请先登录后使用此功能',
  327. success: function(res) { if (res.confirm) uni.navigateTo({ url: '/pages/login/login' }) }
  328. })
  329. return
  330. }
  331. if (item.page) {
  332. nav.goPage(item.page)
  333. }
  334. },
  335. scrollToSection: function(section) {},
  336. goCognitiveReport: function() {
  337. uni.navigateTo({ url: '/pages/wisdom-detail/cognitive-report' })
  338. },
  339. goTrainingHub: function() {
  340. uni.navigateTo({ url: '/pages/wisdom-detail/training-hub' })
  341. },
  342. goAssessment: function() {
  343. uni.navigateTo({ url: '/pages/assessment/apply' })
  344. },
  345. _watchPageReady() {
  346. var self = this
  347. var unwatch = this.$watch(function() {
  348. if (self.isLoggedIn) {
  349. return !!(self.cognitiveReport || self.sandboxData)
  350. } }, function(val) {
  351. if (val) {
  352. if (unwatch) unwatch()
  353. if (self.$refs.tabTransition) self.$refs.tabTransition.markReady()
  354. }
  355. })
  356. },
  357. // ===== DAN 报告 =====
  358. loadDanReports: function() {
  359. var self = this
  360. var memberId = self.activeChildId
  361. if (!memberId) return
  362. getDanReportByChild(memberId, 'wisdom').then(function(res) {
  363. if (res.code === 200 && res.data) {
  364. self.danReports = res.data
  365. }
  366. }).catch(function() {})
  367. },
  368. goDanUpload: function() {
  369. uni.navigateTo({ url: '/pages/dan-assessment/report-upload' })
  370. },
  371. viewDanReport: function(report) {
  372. var reportId = report.sourceReportId || report.id
  373. uni.navigateTo({ url: '/pages/health/report-detail?reportType=dan&reportId=' + reportId })
  374. },
  375. formatDate: function(dateStr) {
  376. if (!dateStr) return ''
  377. var d = parseDate(dateStr)
  378. if (!d) return ''
  379. return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
  380. }
  381. }
  382. }
  383. </script>
  384. <style scoped>
  385. .wisdom-container {
  386. min-height: 100vh;
  387. background: #f5f7fa;
  388. padding-bottom: 120rpx;
  389. }
  390. .func-section {
  391. margin: 20rpx 20rpx 0;
  392. }
  393. .func-grid {
  394. background: #fff;
  395. border-radius: 24rpx;
  396. padding: 30rpx 16rpx;
  397. display: flex;
  398. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
  399. }
  400. .func-item {
  401. flex: 1;
  402. display: flex;
  403. flex-direction: column;
  404. align-items: center;
  405. }
  406. .func-item:active {
  407. opacity: 0.7;
  408. }
  409. .func-icon-wrap {
  410. width: 88rpx;
  411. height: 88rpx;
  412. border-radius: 50%;
  413. background: #FFF8E1;
  414. display: flex;
  415. align-items: center;
  416. justify-content: center;
  417. margin-bottom: 10rpx;
  418. }
  419. .func-icon {
  420. font-size: 40rpx;
  421. }
  422. .func-label {
  423. font-size: 24rpx;
  424. color: #333;
  425. font-weight: 500;
  426. }
  427. /* ===== Section ===== */
  428. .section {
  429. margin: 20rpx 20rpx;
  430. }
  431. .section-header {
  432. display: flex;
  433. justify-content: space-between;
  434. align-items: center;
  435. margin-bottom: 20rpx;
  436. }
  437. .section-title {
  438. font-size: 30rpx;
  439. font-weight: bold;
  440. color: #333;
  441. }
  442. .section-more {
  443. font-size: 24rpx;
  444. color: #999;
  445. }
  446. /* ===== 快捷操作按钮 ===== */
  447. .quick-actions {
  448. display: flex;
  449. flex-direction: row;
  450. flex-wrap: wrap;
  451. justify-content: space-between;
  452. }
  453. .action-btn {
  454. width: 48%;
  455. background: #fff;
  456. border-radius: 16rpx;
  457. padding: 24rpx 0;
  458. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  459. display: flex;
  460. flex-direction: column;
  461. align-items: center;
  462. margin-bottom: 20rpx;
  463. }
  464. .action-btn:active {
  465. opacity: 0.8;
  466. }
  467. .action-icon {
  468. font-size: 40rpx;
  469. margin-bottom: 10rpx;
  470. }
  471. .action-text {
  472. font-size: 26rpx;
  473. color: #333;
  474. font-weight: 500;
  475. }
  476. /* ===== 底部占位 ===== */
  477. .bottom-spacer {
  478. height: 120rpx;
  479. }
  480. /* DAN 报告卡片 */
  481. .dan-card {
  482. background: #fff;
  483. border-radius: 16rpx;
  484. padding: 24rpx;
  485. margin-bottom: 16rpx;
  486. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  487. }
  488. .dan-card-header {
  489. display: flex;
  490. justify-content: space-between;
  491. align-items: center;
  492. margin-bottom: 12rpx;
  493. }
  494. .dan-report-date {
  495. font-size: 26rpx;
  496. color: #666;
  497. }
  498. .dan-status {
  499. font-size: 22rpx;
  500. padding: 4rpx 16rpx;
  501. border-radius: 8rpx;
  502. }
  503. .dan-status.confirmed {
  504. background: #D1FAE5;
  505. color: #065F46;
  506. }
  507. .dan-status.pending {
  508. background: #FEF3C7;
  509. color: #92400E;
  510. }
  511. .dan-report-desc {
  512. font-size: 28rpx;
  513. color: #333;
  514. font-weight: 500;
  515. margin-bottom: 12rpx;
  516. display: block;
  517. }
  518. .dan-card-footer {
  519. text-align: right;
  520. }
  521. .dan-view-link {
  522. font-size: 26rpx;
  523. color: #6366F1;
  524. }
  525. .dan-empty {
  526. background: #fff;
  527. border-radius: 16rpx;
  528. padding: 40rpx 24rpx;
  529. text-align: center;
  530. }
  531. .dan-empty-text {
  532. font-size: 24rpx;
  533. color: #999;
  534. }
  535. .avg-badge {
  536. font-size: 20rpx;
  537. color: #94A3B8;
  538. margin-left: 12rpx;
  539. }
  540. /* 品牌定位语 */
  541. .brand-strap {
  542. margin: 16rpx 30rpx 0;
  543. padding: 20rpx 28rpx;
  544. background: linear-gradient(135deg, #F0F0FF, rgba(99,102,241,0.12));
  545. border-radius: 16rpx;
  546. border-left: 4rpx solid #6366F1;
  547. }
  548. .brand-strap-text {
  549. font-size: 26rpx;
  550. color: #3730A3;
  551. font-weight: 500;
  552. }
  553. /* 每月引导语 */
  554. .monthly-guidance {
  555. margin: 16rpx 30rpx 0;
  556. padding: 20rpx 28rpx;
  557. background: linear-gradient(135deg, #F0F0FF, rgba(99,102,241,0.08));
  558. border-radius: 16rpx;
  559. border-left: 4rpx solid #6366F1;
  560. display: flex;
  561. align-items: center;
  562. gap: 12rpx;
  563. }
  564. .guidance-icon { font-size: 32rpx; }
  565. .guidance-text {
  566. font-size: 26rpx;
  567. color: #3730A3;
  568. font-weight: 500;
  569. }
  570. </style>