self-check-entry.vue 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <template>
  2. <view class="sce-page">
  3. <view v-if="loading" class="sce-loading">
  4. <text class="sce-loading-text">加载中...</text>
  5. </view>
  6. <template v-else>
  7. <!-- 总分横幅 -->
  8. <view class="sce-hero" :style="{ background: heroGradient }">
  9. <view class="sce-hero-inner">
  10. <text class="sce-hero-label">最近自检总分</text>
  11. <view class="sce-hero-score-row">
  12. <text class="sce-hero-score">{{ lastResult && lastResult.totalScore || 0 }}</text>
  13. <text class="sce-hero-total">/45</text>
  14. </view>
  15. <text class="sce-hero-date" v-if="lastResult && lastResult.createdAt">
  16. {{ formatCreatedDate(lastResult.createdAt) }}
  17. </text>
  18. </view>
  19. </view>
  20. <!-- 维度分数条 -->
  21. <view class="sce-card" v-if="lastResult && lastResult.dimensions && lastResult.dimensions.length > 0">
  22. <view class="sce-card-title">
  23. <text class="sce-card-title-text">五维得分</text>
  24. </view>
  25. <view class="sce-dim-list">
  26. <view class="sce-dim-item" v-for="dim in lastResult.dimensions" :key="dim.dimension">
  27. <view class="sce-dim-head">
  28. <view class="sce-dim-label">
  29. <view class="sce-dim-dot" :style="{ background: dim.color }"></view>
  30. <text class="sce-dim-name">{{ dim.name }}</text>
  31. </view>
  32. <view class="sce-dim-score-wrap">
  33. <text class="sce-dim-score" :style="{ color: dim.color }">{{ dim.score }}</text>
  34. <text class="sce-dim-max">/9</text>
  35. </view>
  36. </view>
  37. <view class="sce-dim-bar">
  38. <view class="sce-dim-bar-fill" :style="{ width: (dim.score / 9 * 100) + '%', background: dim.color }"></view>
  39. </view>
  40. </view>
  41. </view>
  42. </view>
  43. <!-- 状态提示 -->
  44. <view class="sce-status" v-if="!canCheck && lastResult">
  45. <text class="sce-status-text">距下次自检还有 {{ daysLeft }} 天</text>
  46. </view>
  47. <view class="sce-status" v-else-if="canCheck && lastResult">
  48. <text class="sce-status-text">已满 15 天,可再次自检</text>
  49. </view>
  50. <!-- 趋势分析(P1-1) -->
  51. <view class="sce-trend" v-if="trendLoaded">
  52. <view class="sce-trend-header">
  53. <text class="sce-trend-title">📈 自检趋势</text>
  54. <text class="sce-trend-summary" v-if="trendSummary">{{ trendSummary }}</text>
  55. </view>
  56. <text class="sce-trend-insight" v-if="aiInsight">{{ aiInsight }}</text>
  57. <view class="sce-trend-loading" v-if="trendLoading">
  58. <text>AI 趋势分析中...</text>
  59. </view>
  60. </view>
  61. <!-- 按钮区域 -->
  62. <view class="sce-footer">
  63. <button v-if="canCheck && lastResult" class="sce-btn sce-btn-primary" @click="retake">
  64. 再次自检
  65. </button>
  66. <button v-if="canCheck && lastResult" class="sce-btn sce-btn-outline" @click="ignore">
  67. 忽略本次
  68. </button>
  69. <button v-if="!lastResult" class="sce-btn sce-btn-primary" @click="retake">
  70. 开始自检
  71. </button>
  72. <button v-if="!canCheck && lastResult" class="sce-btn sce-btn-disabled" disabled>
  73. {{ daysLeft }} 天后可再次自检
  74. </button>
  75. </view>
  76. </template>
  77. </view>
  78. </template>
  79. <script>
  80. import { getSelfCheckStatus, ignoreSelfCheck, getSelfCheckTrendAnalysis } from '@/utils/api'
  81. import { parseDate } from '@/utils/format.js'
  82. export default {
  83. data() {
  84. return {
  85. loading: true,
  86. lastResult: null,
  87. canCheck: false,
  88. daysLeft: 0,
  89. trendLoaded: false,
  90. trendLoading: false,
  91. aiInsight: '',
  92. trendSummary: ''
  93. }
  94. },
  95. computed: {
  96. heroGradient: function() {
  97. if (!this.lastResult || !this.lastResult.dimensions || this.lastResult.dimensions.length === 0) {
  98. return 'linear-gradient(135deg, #F97316, #FF8C42)'
  99. }
  100. var lowest = this.lastResult.dimensions.reduce(function(min, d) {
  101. return d.score < min.score ? d : min
  102. }, this.lastResult.dimensions[0])
  103. return 'linear-gradient(135deg, ' + lowest.color + ', ' + (lowest.color + 'bb') + ')'
  104. }
  105. },
  106. onLoad: function() {
  107. this.loadStatus()
  108. this.loadTrend()
  109. },
  110. methods: {
  111. loadStatus: function() {
  112. var self = this
  113. getSelfCheckStatus().then(function(res) {
  114. self.loading = false
  115. if (res.code === 200 && res.data) {
  116. self.lastResult = res.data.lastResult || null
  117. self.canCheck = res.data.canCheck
  118. self.daysLeft = res.data.daysLeft || 0
  119. } else {
  120. uni.showToast({ title: (res && res.message) || '加载失败', icon: 'none' })
  121. }
  122. }).catch(function() {
  123. self.loading = false
  124. uni.showToast({ title: '网络异常', icon: 'none' })
  125. })
  126. },
  127. retake: function() {
  128. uni.navigateTo({ url: '/pages/family/self-check?retake=1' })
  129. },
  130. ignore: function() {
  131. var self = this
  132. ignoreSelfCheck({ checkId: this.lastResult && this.lastResult.id }).then(function(res) {
  133. if (res.code === 200) {
  134. uni.showToast({ title: '已忽略,15 天后再次提醒', icon: 'none' })
  135. setTimeout(function() {
  136. uni.navigateBack()
  137. }, 800)
  138. } else {
  139. uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
  140. }
  141. }).catch(function() {
  142. uni.showToast({ title: '操作失败', icon: 'none' })
  143. })
  144. },
  145. formatCreatedDate: function(dateStr) {
  146. if (!dateStr) return ''
  147. var d = parseDate(dateStr)
  148. if (!d) return ''
  149. var pad = function(n) { return n < 10 ? '0' + n : '' + n }
  150. return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
  151. },
  152. loadTrend: function() {
  153. var self = this
  154. this.trendLoading = true
  155. getSelfCheckTrendAnalysis().then(function(res) {
  156. self.trendLoading = false
  157. self.trendLoaded = true
  158. if (res.code === 200 && res.data) {
  159. self.aiInsight = res.data.aiInsight || ''
  160. self.trendSummary = res.data.trendSummary || ''
  161. }
  162. }).catch(function() {
  163. self.trendLoading = false
  164. self.trendLoaded = true
  165. })
  166. }
  167. }
  168. }
  169. </script>
  170. <style scoped>
  171. .sce-page {
  172. min-height: 100vh;
  173. background: #F5FAFE;
  174. padding: 24rpx 30rpx 120rpx;
  175. box-sizing: border-box;
  176. }
  177. .sce-loading {
  178. display: flex;
  179. justify-content: center;
  180. align-items: center;
  181. height: 50vh;
  182. }
  183. .sce-loading-text {
  184. font-size: 28rpx;
  185. color: #999;
  186. }
  187. .sce-hero {
  188. background: linear-gradient(135deg, #F97316, #FF8C42);
  189. border-radius: 24rpx;
  190. padding: 40rpx 30rpx;
  191. margin-bottom: 24rpx;
  192. box-shadow: 0 6rpx 20rpx rgba(249, 114, 22, 0.25);
  193. }
  194. .sce-hero-inner {
  195. display: flex;
  196. flex-direction: column;
  197. align-items: center;
  198. }
  199. .sce-hero-label {
  200. font-size: 26rpx;
  201. color: rgba(255, 255, 255, 0.9);
  202. }
  203. .sce-hero-score-row {
  204. display: flex;
  205. align-items: baseline;
  206. margin-top: 10rpx;
  207. }
  208. .sce-hero-score {
  209. font-size: 88rpx;
  210. font-weight: 700;
  211. color: #fff;
  212. }
  213. .sce-hero-total {
  214. font-size: 30rpx;
  215. color: rgba(255, 255, 255, 0.7);
  216. margin-left: 6rpx;
  217. }
  218. .sce-hero-date {
  219. display: block;
  220. text-align: center;
  221. font-size: 22rpx;
  222. color: rgba(255, 255, 255, 0.8);
  223. margin-top: 16rpx;
  224. }
  225. .sce-card {
  226. background: #fff;
  227. border-radius: 24rpx;
  228. padding: 30rpx 26rpx;
  229. margin-bottom: 24rpx;
  230. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
  231. }
  232. .sce-card-title {
  233. margin-bottom: 24rpx;
  234. }
  235. .sce-card-title-text {
  236. font-size: 32rpx;
  237. font-weight: 600;
  238. color: #333;
  239. }
  240. .sce-dim-list {
  241. display: flex;
  242. flex-direction: column;
  243. }
  244. .sce-dim-item {
  245. margin-bottom: 24rpx;
  246. }
  247. .sce-dim-item:last-child {
  248. margin-bottom: 0;
  249. }
  250. .sce-dim-head {
  251. display: flex;
  252. align-items: center;
  253. justify-content: space-between;
  254. }
  255. .sce-dim-label {
  256. display: flex;
  257. align-items: center;
  258. }
  259. .sce-dim-dot {
  260. width: 28rpx;
  261. height: 28rpx;
  262. border-radius: 50%;
  263. margin-right: 12rpx;
  264. }
  265. .sce-dim-name {
  266. font-size: 30rpx;
  267. font-weight: 600;
  268. color: #333;
  269. }
  270. .sce-dim-score-wrap {
  271. display: flex;
  272. align-items: baseline;
  273. }
  274. .sce-dim-score {
  275. font-size: 40rpx;
  276. font-weight: 700;
  277. }
  278. .sce-dim-max {
  279. font-size: 22rpx;
  280. color: #BBB;
  281. }
  282. .sce-dim-bar {
  283. height: 12rpx;
  284. background: #F0F0F0;
  285. border-radius: 6rpx;
  286. margin-top: 14rpx;
  287. overflow: hidden;
  288. }
  289. .sce-dim-bar-fill {
  290. height: 100%;
  291. border-radius: 6rpx;
  292. transition: width 0.5s ease;
  293. }
  294. .sce-status {
  295. background: #FFF7ED;
  296. border-radius: 16rpx;
  297. padding: 24rpx 20rpx;
  298. margin-bottom: 24rpx;
  299. text-align: center;
  300. }
  301. .sce-status-text {
  302. font-size: 26rpx;
  303. color: #E65100;
  304. }
  305. .sce-footer {
  306. display: flex;
  307. flex-direction: column;
  308. gap: 16rpx;
  309. margin-top: 20rpx;
  310. }
  311. .sce-btn {
  312. height: 88rpx;
  313. line-height: 88rpx;
  314. font-size: 30rpx;
  315. border-radius: 44rpx;
  316. text-align: center;
  317. border: none;
  318. }
  319. .sce-btn-primary {
  320. background: linear-gradient(135deg, #F97316, #FF8C42);
  321. color: #fff;
  322. font-weight: 600;
  323. }
  324. .sce-btn-outline {
  325. background: #fff;
  326. color: #F97316;
  327. border: 2rpx solid #F97316;
  328. }
  329. .sce-btn-disabled {
  330. background: #E5E7EB;
  331. color: #999;
  332. font-weight: 400;
  333. }
  334. .sce-trend {
  335. background: #fff;
  336. border-radius: 24rpx;
  337. padding: 30rpx 26rpx;
  338. margin-bottom: 24rpx;
  339. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
  340. }
  341. .sce-trend-header {
  342. display: flex;
  343. align-items: center;
  344. justify-content: space-between;
  345. margin-bottom: 12rpx;
  346. }
  347. .sce-trend-title {
  348. font-size: 30rpx;
  349. font-weight: 600;
  350. color: #333;
  351. }
  352. .sce-trend-summary {
  353. font-size: 24rpx;
  354. color: #F97316;
  355. font-weight: 600;
  356. }
  357. .sce-trend-insight {
  358. display: block;
  359. font-size: 26rpx;
  360. color: #555;
  361. line-height: 1.6;
  362. }
  363. .sce-trend-loading {
  364. padding: 10rpx 0;
  365. }
  366. .sce-trend-loading text {
  367. font-size: 24rpx;
  368. color: #999;
  369. }
  370. </style>