猫妈狗爸伴宠师小程序前端代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

385 lines
9.4 KiB

2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
fix(订单管理): 修复宠物档案跳转缺少订单ID的问题 修复订单详情页跳转宠物档案页面时未传递orderId参数的问题 ``` ```msg refactor(认证考试): 重构考试答案提交逻辑 将单个题目提交改为批量提交,优化考试流程: 1. 基础考试和培训考试都改为最后统一提交答案 2. 添加加载状态提示 3. 使用Promise.all处理并发请求 ``` ```msg fix(认证考试): 修复考试完成状态判断逻辑 修改answeBaseIsFinish和answeTrainIsFinish接口的返回判断逻辑,从检查code改为检查data字段 ``` ```msg feat(认证考试): 新增重新考试和成为伴宠师接口 1. 添加retakeExam和appletUsersTeacher接口 2. 在错误详情页添加重新考试功能 3. 在考试完成页添加成为伴宠师功能 ``` ```msg style(时间轴组件): 优化操作按钮布局 1. 添加按钮间距(gap) 2. 使用flex:1使按钮等宽 3. 根据状态显示不同按钮文本 4. 添加serviceBtn属性控制档案按钮显示 ``` ```msg refactor(订单弹窗): 重构服务档案弹窗组件 1. 使用timelineService组件替代原有实现 2. 简化数据结构处理 3. 添加状态判断逻辑 4. 优化弹窗标题和样式 ``` ```msg fix(表单验证): 添加认证考试结束页表单验证 1. 添加姓名、电话、地址的必填验证 2. 添加格式验证(电话格式、姓名格式) 3. 添加长度验证 4. 添加错误状态样式 5. 优化错误提示体验 ``` ```msg refactor(工作台): 重构伴宠师申请流程 1. 优化申请条件判断逻辑 2. 添加用户状态检查 3. 完善考试状态跳转逻辑 4. 统一使用store获取用户信息
3 months ago
2 months ago
2 months ago
  1. <template>
  2. <view class="mt32 question__view" :class="[props.mode]">
  3. <view class="size-28 mb20 question">
  4. {{ props.data.title }}
  5. <text class="question-type" v-if="props.data.typeAnswer == 0">
  6. {{ isMultipleChoice ? '(多选题)' : '(单选题)' }}
  7. </text>
  8. <text class="question-type" v-else-if="props.data.typeAnswer == 1">
  9. (填空题)
  10. </text>
  11. <!-- {{ `${props.index + 1}${props.data.title}` }} -->
  12. </view>
  13. <template v-if="props.mode === 'edit'">
  14. <template v-if="props.data.typeAnswer == 0">
  15. <view class="size-28 option" v-for="(option, oIdx) in props.data.options"
  16. :key="`${props.index}-option-${oIdx}`"
  17. :class="[isOptionSelected(option.id) ? 'is-selected' : '']"
  18. @click="onChange(option.id)">
  19. {{ option.title }}
  20. </view>
  21. </template>
  22. <template v-else>
  23. <view class="textarea">
  24. <textarea v-model="value" :placeholder="`请输入您的答案,不得低于${props.data.numberWords || 0}个字`" :rows="10"
  25. @input="onChange($event.detail.value)" @blur="onChange($event.detail.value)" :maxlength="2000"></textarea>
  26. </view>
  27. </template>
  28. </template>
  29. <template v-else>
  30. <template v-if="props.data.typeAnswer == 0">
  31. <view class="size-28 option" v-for="(option, oIdx) in props.data.options"
  32. :key="`${props.index}-option-${oIdx}`" :class="[
  33. option.isTrue ? 'is-correct' : '',
  34. isDisplayOptionSelected(option.id) && !option.isTrue ? 'is-error' : '',
  35. isDisplayOptionSelected(option.id) && option.isTrue ? 'is-correct-selected' : ''
  36. ]">
  37. {{ option.title }}
  38. <!-- 正确答案标识 -->
  39. <view class="icon icon-correct" v-if="option.isTrue">
  40. <up-icon name="checkmark" color="#05C160" size="35rpx"></up-icon>
  41. </view>
  42. <!-- 错误选择标识 -->
  43. <view class="icon icon-error" v-if="isDisplayOptionSelected(option.id) && !option.isTrue">
  44. <up-icon name="close" color="#FF2A2A" size="35rpx"></up-icon>
  45. </view>
  46. <!-- 用户选择标识和正确答案标识 -->
  47. <view class="answer-tags" v-if="isDisplayOptionSelected(option.id) || option.isTrue">
  48. <view class="user-choice-tag" v-if="isDisplayOptionSelected(option.id)">
  49. 我的选择
  50. </view>
  51. <view class="correct-answer-tag" v-if="option.isTrue">
  52. 正确答案
  53. </view>
  54. </view>
  55. </view>
  56. </template>
  57. <template v-else>
  58. <view class="textarea">
  59. <view class="user-answer-label">我的答案</view>
  60. <view class="user-answer">{{ props.data.value || '未作答' }}</view>
  61. <view class="correct-answer-label" v-if="props.data.answer">正确答案</view>
  62. <view class="highlight" v-if="props.data.answer">{{ props.data.answer }}</view>
  63. </view>
  64. </template>
  65. </template>
  66. </view>
  67. </template>
  68. <script setup>
  69. import {
  70. computed,
  71. watch,
  72. onMounted
  73. } from 'vue'
  74. import {
  75. addBaseAnswer,
  76. addTrainAnswer
  77. } from '@/api/examination'
  78. import {
  79. store
  80. } from '@/store'
  81. import examStorage from '@/utils/examStorage'
  82. const userId = computed(() => {
  83. return store.state.user.userInfo.userId
  84. })
  85. const props = defineProps({
  86. index: {
  87. type: Number,
  88. default: null,
  89. },
  90. data: {
  91. type: Object,
  92. default () {
  93. return {}
  94. }
  95. },
  96. modelValue: {
  97. type: [String, Number, Array],
  98. default: null,
  99. },
  100. mode: {
  101. type: String,
  102. default: 'edit', // edit | display
  103. },
  104. type: {
  105. type: String,
  106. default: null, // '基本' | '培训'
  107. }
  108. })
  109. const min = 700
  110. const emit = defineEmits(['update:modelValue'])
  111. // 判断是否是多选题
  112. const isMultipleChoice = computed(() => {
  113. if (!props.data.options || !props.data.options.length) return false
  114. const correctAnswers = props.data.options.filter(option => option.isTrue === 1 || option.isTrue === true)
  115. return correctAnswers.length > 1
  116. })
  117. const value = computed({
  118. set(val) {
  119. emit('update:modelValue', val)
  120. },
  121. get() {
  122. if (isMultipleChoice.value) {
  123. // 多选题,确保返回数组
  124. return Array.isArray(props.modelValue) ? props.modelValue : (props.modelValue ? [props.modelValue] : [])
  125. } else {
  126. // 单选题,返回单个值
  127. return Array.isArray(props.modelValue) ? props.modelValue[0] : props.modelValue
  128. }
  129. }
  130. })
  131. // 判断选项是否被选中
  132. const isOptionSelected = (optionId) => {
  133. if (isMultipleChoice.value) {
  134. return Array.isArray(value.value) && value.value.includes(optionId)
  135. } else {
  136. return value.value === optionId
  137. }
  138. }
  139. // 显示模式下判断选项是否被用户选中
  140. const isDisplayOptionSelected = (optionId) => {
  141. if (props.mode !== 'display') return false
  142. // 方法1: 从 props.data.value 获取用户答案(直接传入的答案)
  143. if (props.data.value !== undefined && props.data.value !== null) {
  144. if (Array.isArray(props.data.value)) {
  145. return props.data.value.includes(optionId)
  146. } else {
  147. return props.data.value === optionId
  148. }
  149. }
  150. // 方法2: 从 userAnswerBaseList 获取用户答案(从后端获取的答案数据)
  151. if (props.data.userAnswerBaseList && props.data.userAnswerBaseList.length > 0) {
  152. return props.data.userAnswerBaseList.some(answer => answer.answerId === optionId)
  153. }
  154. // 方法3: 从 modelValue 获取答案(当前组件的值)
  155. if (props.modelValue !== undefined && props.modelValue !== null) {
  156. if (Array.isArray(props.modelValue)) {
  157. return props.modelValue.includes(optionId)
  158. } else {
  159. return props.modelValue === optionId
  160. }
  161. }
  162. return false
  163. }
  164. // 从本地存储加载答案数据
  165. const loadAnswerFromStorage = () => {
  166. if (props.mode === 'edit' && props.type && props.data.id) {
  167. const savedAnswer = examStorage.getAnswer(props.type, props.data.id)
  168. if (savedAnswer !== null) {
  169. // 如果有保存的答案,直接更新父组件的值
  170. emit('update:modelValue', savedAnswer)
  171. }
  172. }
  173. }
  174. // 保存答案到本地存储
  175. const saveAnswerToStorage = (answer) => {
  176. if (props.mode === 'edit' && props.type && props.data.id) {
  177. examStorage.saveAnswer(props.type, props.data.id, answer)
  178. }
  179. }
  180. const onChange = (val) => {
  181. let newValue
  182. if (props.data.typeAnswer == 1) {
  183. // 填空题处理
  184. newValue = val
  185. } else if (props.data.typeAnswer == 0) {
  186. // 选择题处理
  187. if (isMultipleChoice.value) {
  188. // 多选题处理
  189. let currentValue = Array.isArray(value.value) ? [...value.value] : []
  190. const index = currentValue.indexOf(val)
  191. if (index > -1) {
  192. // 已选中,取消选择
  193. currentValue.splice(index, 1)
  194. } else {
  195. // 未选中,添加选择
  196. currentValue.push(val)
  197. }
  198. newValue = currentValue
  199. } else {
  200. // 单选题处理
  201. newValue = val
  202. }
  203. }
  204. // 更新组件值
  205. value.value = newValue
  206. // 立即保存答案到本地存储
  207. saveAnswerToStorage(newValue)
  208. // const data = {
  209. // userId: userId.value,
  210. // questionId: props.data.id,
  211. // }
  212. // if (props.type === '基本') {
  213. // data.answerId = val
  214. // addBaseAnswer(data)
  215. // } else if (props.type === '培训') {
  216. // data.answer = val
  217. // addTrainAnswer(data)
  218. // }
  219. }
  220. // 监听题目数据变化,当题目ID存在时加载本地存储的答案
  221. watch(() => props.data.id, (newId) => {
  222. if (newId) {
  223. loadAnswerFromStorage()
  224. }
  225. }, { immediate: true })
  226. // 组件挂载时加载本地存储的答案
  227. onMounted(() => {
  228. loadAnswerFromStorage()
  229. })
  230. </script>
  231. <style lang="scss" scoped>
  232. .question {
  233. color: #000000;
  234. .question-type {
  235. color: #FFBF60;
  236. font-size: 22rpx;
  237. margin-left: 10rpx;
  238. }
  239. }
  240. .option {
  241. background-color: #F3F3F3;
  242. color: #707070;
  243. line-height: 37rpx;
  244. padding: 23rpx;
  245. border-radius: 28rpx;
  246. position: relative;
  247. &+& {
  248. margin-top: 20rpx;
  249. }
  250. .icon {
  251. position: absolute;
  252. right: 45rpx;
  253. bottom: 23rpx;
  254. display: none;
  255. }
  256. }
  257. .textarea {
  258. background-color: #F3F3F3;
  259. padding: 23rpx;
  260. border-radius: 16rpx;
  261. .highlight {
  262. color: #FF2A2A;
  263. font-size: 28rpx;
  264. }
  265. }
  266. .question__view.edit {
  267. .option.is-selected {
  268. background-color: rgba($color: #FFBF60, $alpha: 0.22);
  269. color: #FFBF60;
  270. }
  271. }
  272. .question__view.display {
  273. .option {
  274. &.is-correct {
  275. background-color: rgba($color: #05C160, $alpha: 0.08);
  276. color: #05C160;
  277. .icon-correct {
  278. display: block;
  279. }
  280. }
  281. &.is-error {
  282. background-color: rgba($color: #FFEBCE, $alpha: 0.36);
  283. color: #FF2A2A;
  284. .icon-error {
  285. display: block;
  286. }
  287. }
  288. &.is-correct-selected {
  289. background-color: rgba($color: #05C160, $alpha: 0.15);
  290. color: #05C160;
  291. border: 2px solid #05C160;
  292. }
  293. }
  294. .answer-tags {
  295. margin-top: 10rpx;
  296. display: flex;
  297. gap: 10rpx;
  298. flex-wrap: wrap;
  299. }
  300. .user-choice-tag {
  301. background-color: #FFBF60;
  302. color: white;
  303. font-size: 20rpx;
  304. padding: 6rpx 12rpx;
  305. border-radius: 12rpx;
  306. display: inline-block;
  307. }
  308. .correct-answer-tag {
  309. background-color: #05C160;
  310. color: white;
  311. font-size: 20rpx;
  312. padding: 6rpx 12rpx;
  313. border-radius: 12rpx;
  314. display: inline-block;
  315. }
  316. }
  317. .textarea {
  318. .user-answer-label, .correct-answer-label {
  319. font-size: 24rpx;
  320. color: #999;
  321. margin-bottom: 10rpx;
  322. }
  323. .user-answer {
  324. font-size: 28rpx;
  325. color: #333;
  326. margin-bottom: 15rpx;
  327. padding: 10rpx;
  328. background-color: rgba($color: #FFBF60, $alpha: 0.1);
  329. border-radius: 8rpx;
  330. }
  331. .highlight {
  332. color: #05C160;
  333. font-size: 28rpx;
  334. font-weight: bold;
  335. padding: 10rpx;
  336. background-color: rgba($color: #05C160, $alpha: 0.1);
  337. border-radius: 8rpx;
  338. }
  339. }
  340. </style>