猫妈狗爸伴宠师小程序前端代码
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.

345 lines
8.3 KiB

2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks 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获取用户信息
1 month ago
2 weeks ago
2 weeks 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. @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. } from 'vue'
  73. import {
  74. addBaseAnswer,
  75. addTrainAnswer
  76. } from '@/api/examination'
  77. import {
  78. store
  79. } from '@/store'
  80. const userId = computed(() => {
  81. return store.state.user.userInfo.userId
  82. })
  83. const props = defineProps({
  84. index: {
  85. type: Number,
  86. default: null,
  87. },
  88. data: {
  89. type: Object,
  90. default () {
  91. return {}
  92. }
  93. },
  94. modelValue: {
  95. type: [String, Number, Array],
  96. default: null,
  97. },
  98. mode: {
  99. type: String,
  100. default: 'edit', // edit | display
  101. },
  102. type: {
  103. type: String,
  104. default: null, // '基本' | '培训'
  105. }
  106. })
  107. const min = 700
  108. const emit = defineEmits(['update:modelValue'])
  109. // 判断是否是多选题
  110. const isMultipleChoice = computed(() => {
  111. if (!props.data.options || !props.data.options.length) return false
  112. const correctAnswers = props.data.options.filter(option => option.isTrue === 1 || option.isTrue === true)
  113. return correctAnswers.length > 1
  114. })
  115. const value = computed({
  116. set(val) {
  117. emit('update:modelValue', val)
  118. },
  119. get() {
  120. if (isMultipleChoice.value) {
  121. // 多选题,确保返回数组
  122. return Array.isArray(props.modelValue) ? props.modelValue : (props.modelValue ? [props.modelValue] : [])
  123. } else {
  124. // 单选题,返回单个值
  125. return Array.isArray(props.modelValue) ? props.modelValue[0] : props.modelValue
  126. }
  127. }
  128. })
  129. // 判断选项是否被选中
  130. const isOptionSelected = (optionId) => {
  131. if (isMultipleChoice.value) {
  132. return Array.isArray(value.value) && value.value.includes(optionId)
  133. } else {
  134. return value.value === optionId
  135. }
  136. }
  137. // 显示模式下判断选项是否被用户选中
  138. const isDisplayOptionSelected = (optionId) => {
  139. if (props.mode !== 'display') return false
  140. // 方法1: 从 props.data.value 获取用户答案(直接传入的答案)
  141. if (props.data.value !== undefined && props.data.value !== null) {
  142. if (Array.isArray(props.data.value)) {
  143. return props.data.value.includes(optionId)
  144. } else {
  145. return props.data.value === optionId
  146. }
  147. }
  148. // 方法2: 从 userAnswerBaseList 获取用户答案(从后端获取的答案数据)
  149. if (props.data.userAnswerBaseList && props.data.userAnswerBaseList.length > 0) {
  150. return props.data.userAnswerBaseList.some(answer => answer.answerId === optionId)
  151. }
  152. // 方法3: 从 modelValue 获取答案(当前组件的值)
  153. if (props.modelValue !== undefined && props.modelValue !== null) {
  154. if (Array.isArray(props.modelValue)) {
  155. return props.modelValue.includes(optionId)
  156. } else {
  157. return props.modelValue === optionId
  158. }
  159. }
  160. return false
  161. }
  162. const onChange = (val) => {
  163. if (props.data.typeAnswer == 1) {
  164. // 填空题处理
  165. value.value = val
  166. } else if (props.data.typeAnswer == 0) {
  167. // 选择题处理
  168. if (isMultipleChoice.value) {
  169. // 多选题处理
  170. let currentValue = Array.isArray(value.value) ? [...value.value] : []
  171. const index = currentValue.indexOf(val)
  172. if (index > -1) {
  173. // 已选中,取消选择
  174. currentValue.splice(index, 1)
  175. } else {
  176. // 未选中,添加选择
  177. currentValue.push(val)
  178. }
  179. value.value = currentValue
  180. } else {
  181. // 单选题处理
  182. value.value = val
  183. }
  184. }
  185. // const data = {
  186. // userId: userId.value,
  187. // questionId: props.data.id,
  188. // }
  189. // if (props.type === '基本') {
  190. // data.answerId = val
  191. // addBaseAnswer(data)
  192. // } else if (props.type === '培训') {
  193. // data.answer = val
  194. // addTrainAnswer(data)
  195. // }
  196. }
  197. </script>
  198. <style lang="scss" scoped>
  199. .question {
  200. color: #000000;
  201. .question-type {
  202. color: #FFBF60;
  203. font-size: 22rpx;
  204. margin-left: 10rpx;
  205. }
  206. }
  207. .option {
  208. background-color: #F3F3F3;
  209. color: #707070;
  210. line-height: 37rpx;
  211. padding: 23rpx;
  212. border-radius: 28rpx;
  213. position: relative;
  214. &+& {
  215. margin-top: 20rpx;
  216. }
  217. .icon {
  218. position: absolute;
  219. right: 45rpx;
  220. bottom: 23rpx;
  221. display: none;
  222. }
  223. }
  224. .textarea {
  225. background-color: #F3F3F3;
  226. padding: 23rpx;
  227. border-radius: 16rpx;
  228. .highlight {
  229. color: #FF2A2A;
  230. font-size: 28rpx;
  231. }
  232. }
  233. .question__view.edit {
  234. .option.is-selected {
  235. background-color: rgba($color: #FFBF60, $alpha: 0.22);
  236. color: #FFBF60;
  237. }
  238. }
  239. .question__view.display {
  240. .option {
  241. &.is-correct {
  242. background-color: rgba($color: #05C160, $alpha: 0.08);
  243. color: #05C160;
  244. .icon-correct {
  245. display: block;
  246. }
  247. }
  248. &.is-error {
  249. background-color: rgba($color: #FFEBCE, $alpha: 0.36);
  250. color: #FF2A2A;
  251. .icon-error {
  252. display: block;
  253. }
  254. }
  255. &.is-correct-selected {
  256. background-color: rgba($color: #05C160, $alpha: 0.15);
  257. color: #05C160;
  258. border: 2px solid #05C160;
  259. }
  260. }
  261. .answer-tags {
  262. margin-top: 10rpx;
  263. display: flex;
  264. gap: 10rpx;
  265. flex-wrap: wrap;
  266. }
  267. .user-choice-tag {
  268. background-color: #FFBF60;
  269. color: white;
  270. font-size: 20rpx;
  271. padding: 6rpx 12rpx;
  272. border-radius: 12rpx;
  273. display: inline-block;
  274. }
  275. .correct-answer-tag {
  276. background-color: #05C160;
  277. color: white;
  278. font-size: 20rpx;
  279. padding: 6rpx 12rpx;
  280. border-radius: 12rpx;
  281. display: inline-block;
  282. }
  283. }
  284. .textarea {
  285. .user-answer-label, .correct-answer-label {
  286. font-size: 24rpx;
  287. color: #999;
  288. margin-bottom: 10rpx;
  289. }
  290. .user-answer {
  291. font-size: 28rpx;
  292. color: #333;
  293. margin-bottom: 15rpx;
  294. padding: 10rpx;
  295. background-color: rgba($color: #FFBF60, $alpha: 0.1);
  296. border-radius: 8rpx;
  297. }
  298. .highlight {
  299. color: #05C160;
  300. font-size: 28rpx;
  301. font-weight: bold;
  302. padding: 10rpx;
  303. background-color: rgba($color: #05C160, $alpha: 0.1);
  304. border-radius: 8rpx;
  305. }
  306. }
  307. </style>