self-check.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. <template>
  2. <view class="sc-page">
  3. <!-- 顶部说明 -->
  4. <view class="sc-hero">
  5. <text class="sc-hero-title">五维家庭自检</text>
  6. <text class="sc-hero-sub">15题 · 约5分钟 · 生成五行相生寻源建议</text>
  7. </view>
  8. <!-- 加载 -->
  9. <view v-if="loading" class="sc-loading">
  10. <text class="sc-loading-text">加载中...</text>
  11. </view>
  12. <template v-else>
  13. <!-- 维度导航 -->
  14. <view class="sc-dim-nav">
  15. <view v-for="dim in dimensions" :key="dim.code" class="sc-dim-nav-item">
  16. <view class="sc-dim-nav-dot" :style="{ background: dim.color }">
  17. <text class="sc-dim-nav-char">{{ dim.name }}</text>
  18. </view>
  19. <view class="sc-dim-nav-dots">
  20. <view
  21. v-for="q in dim.questions"
  22. :key="q.id"
  23. class="sc-dim-nav-dot-mini"
  24. :class="{ done: answers[q.id] }"
  25. :style="answers[q.id] ? 'background:' + dim.color : 'background:transparent'"
  26. ></view>
  27. </view>
  28. </view>
  29. </view>
  30. <!-- 进度条 -->
  31. <view class="sc-progress-row">
  32. <text class="sc-progress-text">已完成 {{ answeredCount }}/{{ questions.length }} 题</text>
  33. <text class="sc-progress-pct">{{ progress }}%</text>
  34. </view>
  35. <view class="sc-progress-bar">
  36. <view class="sc-progress-fill" :style="{ width: progress + '%' }"></view>
  37. </view>
  38. <!-- 题目分组(按维度) -->
  39. <view v-for="dim in dimensions" :key="dim.code" class="sc-section">
  40. <view class="sc-section-header" :style="{ borderLeftColor: dim.color }">
  41. <text class="sc-section-name">{{ dim.name }}</text>
  42. <text class="sc-section-element" :style="{ color: dim.color }">{{ dim.element }}</text>
  43. <text class="sc-section-slogan">{{ dim.slogan }}</text>
  44. <text class="sc-section-count">{{ dim.answeredCount }}/{{ dim.questions.length }}</text>
  45. </view>
  46. <view v-for="q in dim.questions" :key="q.id" class="sc-question">
  47. <text class="sc-q-text">{{ q.text }}</text>
  48. <view class="sc-opts">
  49. <view
  50. v-for="opt in q.options"
  51. :key="opt.value"
  52. class="sc-opt"
  53. :class="{ 'sc-opt-sel': answers[q.id] === opt.value }"
  54. @click="selectOption(q.id, opt.value)"
  55. >
  56. <text class="sc-opt-label">{{ opt.label }}</text>
  57. <view class="sc-opt-radio" :class="{ 'sc-opt-radio-on': answers[q.id] === opt.value }"></view>
  58. </view>
  59. </view>
  60. </view>
  61. </view>
  62. <!-- 提交 -->
  63. <view class="sc-footer">
  64. <button class="sc-submit-btn" :disabled="!canSubmit || submitLoading" @click="submit">
  65. {{ submitLoading ? '提交中...' : '提交自检' }}
  66. </button>
  67. <text class="sc-submit-tip" v-if="!canSubmit && !submitLoading">至少完成10题,且每个维度至少回答1题</text>
  68. </view>
  69. </template>
  70. </view>
  71. </template>
  72. <script>
  73. import { getSelfCheckQuestions, submitSelfCheck } from '@/utils/api'
  74. export default {
  75. data() {
  76. return {
  77. loading: true,
  78. submitLoading: false,
  79. isRetake: false,
  80. questions: [],
  81. answers: {},
  82. dimMeta: {
  83. body: { name: '身', element: '土', color: '#FF8C42', slogan: '主动健康' },
  84. wisdom: { name: '智', element: '金', color: '#6366F1', slogan: '赋能未来' },
  85. wealth: { name: '富', element: '水', color: '#F59E0B', slogan: '利他创富' },
  86. action: { name: '行', element: '木', color: '#10B981', slogan: '人际和谐' },
  87. mind: { name: '心', element: '火', color: '#FF6B9D', slogan: '大爱相随' }
  88. }
  89. }
  90. },
  91. computed: {
  92. dimensions: function() {
  93. var order = ['body', 'wisdom', 'wealth', 'action', 'mind']
  94. var meta = this.dimMeta
  95. var result = []
  96. for (var i = 0; i < order.length; i++) {
  97. var code = order[i]
  98. var dimQuestions = this.questions.filter(function(q) { return q.dimension === code })
  99. if (dimQuestions.length === 0) {
  100. continue
  101. }
  102. var self = this
  103. var answered = dimQuestions.filter(function(q) { return self.answers[q.id] }).length
  104. var m = meta[code] || {}
  105. result.push({
  106. code: code,
  107. name: m.name || dimQuestions[0].dimensionName || code,
  108. element: m.element || dimQuestions[0].element || '',
  109. color: m.color || dimQuestions[0].color || '#F97316',
  110. slogan: m.slogan || '',
  111. questions: dimQuestions,
  112. answeredCount: answered
  113. })
  114. }
  115. return result
  116. },
  117. answeredCount: function() {
  118. return Object.keys(this.answers).length
  119. },
  120. progress: function() {
  121. if (!this.questions.length) {
  122. return 0
  123. }
  124. return Math.round(this.answeredCount / this.questions.length * 100)
  125. },
  126. canSubmit: function() {
  127. if (this.answeredCount < 10) {
  128. return false
  129. }
  130. var dims = this.dimensions
  131. for (var i = 0; i < dims.length; i++) {
  132. if (dims[i].answeredCount < 1) {
  133. return false
  134. }
  135. }
  136. return true
  137. }
  138. },
  139. methods: {
  140. loadQuestions: function() {
  141. var self = this
  142. var apiCall = this.isRetake ? getSelfCheckQuestions({ retake: true }) : getSelfCheckQuestions()
  143. apiCall.then(function(res) {
  144. self.loading = false
  145. if (res.code === 200 && res.data) {
  146. self.questions = res.data || []
  147. } else {
  148. uni.showToast({ title: (res && res.message) || '加载失败', icon: 'none' })
  149. }
  150. }).catch(function() {
  151. self.loading = false
  152. uni.showToast({ title: '网络异常,请重试', icon: 'none' })
  153. })
  154. },
  155. selectOption: function(questionId, value) {
  156. var next = {}
  157. var keys = Object.keys(this.answers)
  158. for (var i = 0; i < keys.length; i++) {
  159. next[keys[i]] = this.answers[keys[i]]
  160. }
  161. next[questionId] = value
  162. this.answers = next
  163. },
  164. submit: function() {
  165. var self = this
  166. if (!this.canSubmit || this.submitLoading) {
  167. return
  168. }
  169. this.submitLoading = true
  170. var answersList = []
  171. var ids = Object.keys(this.answers)
  172. for (var i = 0; i < ids.length; i++) {
  173. answersList.push({ questionId: Number(ids[i]), answer: this.answers[ids[i]] })
  174. }
  175. submitSelfCheck({ answers: answersList }).then(function(res) {
  176. self.submitLoading = false
  177. if (res.code === 200 && res.data) {
  178. uni.setStorageSync('lastSelfCheckResult', res.data)
  179. uni.navigateTo({ url: '/pages/family/self-check-result?id=' + res.data.id })
  180. } else {
  181. uni.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
  182. }
  183. }).catch(function() {
  184. self.submitLoading = false
  185. uni.showToast({ title: '提交失败,请重试', icon: 'none' })
  186. })
  187. }
  188. },
  189. onLoad: function(options) {
  190. if (options && options.retake === '1') {
  191. this.isRetake = true
  192. }
  193. this.loadQuestions()
  194. }
  195. }
  196. </script>
  197. <style scoped>
  198. .sc-page {
  199. min-height: 100vh;
  200. background: #F5FAFE;
  201. padding: 24rpx 30rpx 120rpx;
  202. box-sizing: border-box;
  203. }
  204. .sc-hero {
  205. padding: 30rpx 10rpx 20rpx;
  206. }
  207. .sc-hero-title {
  208. display: block;
  209. font-size: 40rpx;
  210. font-weight: 700;
  211. color: #333;
  212. }
  213. .sc-hero-sub {
  214. display: block;
  215. font-size: 24rpx;
  216. color: #999;
  217. margin-top: 8rpx;
  218. }
  219. .sc-loading {
  220. display: flex;
  221. justify-content: center;
  222. align-items: center;
  223. height: 50vh;
  224. }
  225. .sc-loading-text {
  226. font-size: 28rpx;
  227. color: #999;
  228. }
  229. /* 维度导航 */
  230. .sc-dim-nav {
  231. display: flex;
  232. justify-content: space-between;
  233. background: #fff;
  234. border-radius: 20rpx;
  235. padding: 24rpx 20rpx;
  236. margin-bottom: 24rpx;
  237. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  238. }
  239. .sc-dim-nav-item {
  240. display: flex;
  241. flex-direction: column;
  242. align-items: center;
  243. }
  244. .sc-dim-nav-dot {
  245. width: 56rpx;
  246. height: 56rpx;
  247. border-radius: 50%;
  248. display: flex;
  249. align-items: center;
  250. justify-content: center;
  251. }
  252. .sc-dim-nav-char {
  253. color: #fff;
  254. font-size: 26rpx;
  255. font-weight: 600;
  256. }
  257. .sc-dim-nav-dots {
  258. display: flex;
  259. margin-top: 10rpx;
  260. }
  261. .sc-dim-nav-dot-mini {
  262. width: 12rpx;
  263. height: 12rpx;
  264. border-radius: 50%;
  265. background: #E5E7EB;
  266. margin: 0 4rpx;
  267. }
  268. .sc-dim-nav-dot-mini.done {
  269. background: #F97316;
  270. }
  271. /* 进度 */
  272. .sc-progress-row {
  273. display: flex;
  274. justify-content: space-between;
  275. align-items: center;
  276. margin-bottom: 10rpx;
  277. }
  278. .sc-progress-text {
  279. font-size: 24rpx;
  280. color: #666;
  281. }
  282. .sc-progress-pct {
  283. font-size: 24rpx;
  284. color: #F97316;
  285. font-weight: 600;
  286. }
  287. .sc-progress-bar {
  288. height: 10rpx;
  289. background: #E8E8E8;
  290. border-radius: 5rpx;
  291. margin-bottom: 30rpx;
  292. overflow: hidden;
  293. }
  294. .sc-progress-fill {
  295. height: 100%;
  296. background: linear-gradient(90deg, #F97316, #FF8C42);
  297. border-radius: 5rpx;
  298. transition: width 0.3s ease;
  299. }
  300. /* 维度分组 */
  301. .sc-section {
  302. margin-bottom: 30rpx;
  303. }
  304. .sc-section-header {
  305. display: flex;
  306. align-items: center;
  307. background: #fff;
  308. border-left: 8rpx solid #F97316;
  309. border-radius: 12rpx;
  310. padding: 20rpx 24rpx;
  311. margin-bottom: 16rpx;
  312. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.04);
  313. }
  314. .sc-section-name {
  315. font-size: 32rpx;
  316. font-weight: 700;
  317. color: #333;
  318. margin-right: 12rpx;
  319. }
  320. .sc-section-element {
  321. font-size: 26rpx;
  322. font-weight: 600;
  323. margin-right: 16rpx;
  324. }
  325. .sc-section-slogan {
  326. flex: 1;
  327. font-size: 24rpx;
  328. color: #999;
  329. }
  330. .sc-section-count {
  331. font-size: 24rpx;
  332. color: #F97316;
  333. font-weight: 600;
  334. }
  335. /* 题目卡片 */
  336. .sc-question {
  337. background: #fff;
  338. border-radius: 20rpx;
  339. padding: 30rpx 26rpx 26rpx;
  340. margin-bottom: 16rpx;
  341. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  342. }
  343. .sc-q-text {
  344. display: block;
  345. font-size: 30rpx;
  346. font-weight: 500;
  347. color: #333;
  348. line-height: 1.5;
  349. }
  350. .sc-opts {
  351. margin-top: 24rpx;
  352. }
  353. .sc-opt {
  354. display: flex;
  355. align-items: center;
  356. justify-content: space-between;
  357. padding: 22rpx 24rpx;
  358. border: 2rpx solid #E5E7EB;
  359. border-radius: 14rpx;
  360. margin-bottom: 14rpx;
  361. transition: all 0.2s;
  362. }
  363. .sc-opt-sel {
  364. border-color: #F97316;
  365. background: #FFF7ED;
  366. }
  367. .sc-opt-label {
  368. flex: 1;
  369. font-size: 28rpx;
  370. color: #333;
  371. line-height: 1.4;
  372. }
  373. .sc-opt-radio {
  374. width: 36rpx;
  375. height: 36rpx;
  376. border: 2rpx solid #D1D5DB;
  377. border-radius: 50%;
  378. margin-left: 20rpx;
  379. box-sizing: border-box;
  380. }
  381. .sc-opt-radio-on {
  382. border: 10rpx solid #F97316;
  383. }
  384. /* 提交 */
  385. .sc-footer {
  386. margin-top: 20rpx;
  387. }
  388. .sc-submit-btn {
  389. height: 92rpx;
  390. line-height: 92rpx;
  391. background: linear-gradient(135deg, #F97316, #FF8C42);
  392. color: #fff;
  393. font-size: 32rpx;
  394. font-weight: 600;
  395. border-radius: 46rpx;
  396. border: none;
  397. text-align: center;
  398. }
  399. .sc-submit-btn[disabled] {
  400. opacity: 0.5;
  401. }
  402. .sc-submit-tip {
  403. display: block;
  404. text-align: center;
  405. font-size: 24rpx;
  406. color: #999;
  407. margin-top: 16rpx;
  408. }
  409. </style>