feat(打卡): 新增添加对象弹窗组件并优化统计模块
添加 AddTargetDialog 组件用于统一处理对象添加逻辑 在 postCountModel 中实现感恩模块的统计数据显示 重构 CheckinDetailPage 使用新组件并优化表单处理逻辑
Showing
4 changed files
with
152 additions
and
50 deletions
| ... | @@ -9,6 +9,7 @@ export {} | ... | @@ -9,6 +9,7 @@ export {} |
| 9 | declare module 'vue' { | 9 | declare module 'vue' { |
| 10 | export interface GlobalComponents { | 10 | export interface GlobalComponents { |
| 11 | ActivityCard: typeof import('./components/ui/ActivityCard.vue')['default'] | 11 | ActivityCard: typeof import('./components/ui/ActivityCard.vue')['default'] |
| 12 | + AddTargetDialog: typeof import('./components/count/AddTargetDialog.vue')['default'] | ||
| 12 | AppLayout: typeof import('./components/layout/AppLayout.vue')['default'] | 13 | AppLayout: typeof import('./components/layout/AppLayout.vue')['default'] |
| 13 | AudioPlayer: typeof import('./components/ui/AudioPlayer.vue')['default'] | 14 | AudioPlayer: typeof import('./components/ui/AudioPlayer.vue')['default'] |
| 14 | BottomNav: typeof import('./components/layout/BottomNav.vue')['default'] | 15 | BottomNav: typeof import('./components/layout/BottomNav.vue')['default'] | ... | ... |
src/components/count/AddTargetDialog.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <van-dialog | ||
| 3 | + :show="show" | ||
| 4 | + :title="title" | ||
| 5 | + width="90%" | ||
| 6 | + show-cancel-button | ||
| 7 | + confirmButtonColor="#4caf50" | ||
| 8 | + :before-close="onBeforeClose" | ||
| 9 | + @update:show="updateShow" | ||
| 10 | + > | ||
| 11 | + <div class="p-4"> | ||
| 12 | + <div v-for="field in localFields" :key="field.id"> | ||
| 13 | + <van-field | ||
| 14 | + v-model="field.value" | ||
| 15 | + label-width="4rem" | ||
| 16 | + :label="field.label" | ||
| 17 | + :placeholder="'请输入' + field.label" | ||
| 18 | + :type="field.type === 'textarea' ? 'textarea' : 'text'" | ||
| 19 | + :rows="field.type === 'textarea' ? 2 : 1" | ||
| 20 | + :autosize="field.type === 'textarea'" | ||
| 21 | + class="border-b border-gray-100" | ||
| 22 | + :required="field.required" | ||
| 23 | + /> | ||
| 24 | + </div> | ||
| 25 | + </div> | ||
| 26 | + </van-dialog> | ||
| 27 | +</template> | ||
| 28 | + | ||
| 29 | +<script setup> | ||
| 30 | +import { ref, watch } from 'vue' | ||
| 31 | +import { showToast } from 'vant' | ||
| 32 | + | ||
| 33 | +const props = defineProps({ | ||
| 34 | + /** | ||
| 35 | + * 是否显示弹窗 | ||
| 36 | + */ | ||
| 37 | + show: { | ||
| 38 | + type: Boolean, | ||
| 39 | + required: true | ||
| 40 | + }, | ||
| 41 | + /** | ||
| 42 | + * 弹窗标题 | ||
| 43 | + */ | ||
| 44 | + title: { | ||
| 45 | + type: String, | ||
| 46 | + default: '添加对象' | ||
| 47 | + }, | ||
| 48 | + /** | ||
| 49 | + * 表单字段配置 | ||
| 50 | + * @type {Array<{id: string, label: string, type: string, required: boolean}>} | ||
| 51 | + */ | ||
| 52 | + fields: { | ||
| 53 | + type: Array, | ||
| 54 | + required: true | ||
| 55 | + } | ||
| 56 | +}) | ||
| 57 | + | ||
| 58 | +const emit = defineEmits(['update:show', 'confirm']) | ||
| 59 | + | ||
| 60 | +// 本地表单字段状态 | ||
| 61 | +const localFields = ref([]) | ||
| 62 | + | ||
| 63 | +// 监听弹窗显示状态,初始化表单 | ||
| 64 | +watch(() => props.show, (val) => { | ||
| 65 | + if (val) { | ||
| 66 | + // 初始化字段,添加 value 属性 | ||
| 67 | + localFields.value = props.fields.map(field => ({ | ||
| 68 | + ...field, | ||
| 69 | + value: '' | ||
| 70 | + })) | ||
| 71 | + } | ||
| 72 | +}) | ||
| 73 | + | ||
| 74 | +/** | ||
| 75 | + * 更新弹窗显示状态 | ||
| 76 | + * @param {boolean} val - 显示状态 | ||
| 77 | + */ | ||
| 78 | +const updateShow = (val) => { | ||
| 79 | + emit('update:show', val) | ||
| 80 | +} | ||
| 81 | + | ||
| 82 | +/** | ||
| 83 | + * 弹窗关闭前的回调 | ||
| 84 | + * @param {string} action - 动作类型 'confirm' | 'cancel' | ||
| 85 | + * @returns {boolean} 是否允许关闭 | ||
| 86 | + */ | ||
| 87 | +const onBeforeClose = (action) => { | ||
| 88 | + if (action === 'confirm') { | ||
| 89 | + // 校验必填项 | ||
| 90 | + for (const field of localFields.value) { | ||
| 91 | + if (field.required && !field.value.trim()) { | ||
| 92 | + showToast(`请填写${field.label}`) | ||
| 93 | + return false // 阻止关闭 | ||
| 94 | + } | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + // 收集表单数据 | ||
| 98 | + const formData = localFields.value.reduce((acc, field) => { | ||
| 99 | + acc[field.id] = field.value | ||
| 100 | + return acc | ||
| 101 | + }, {}) | ||
| 102 | + | ||
| 103 | + // 触发确认事件,传递表单数据 | ||
| 104 | + emit('confirm', formData) | ||
| 105 | + return true // 允许关闭 | ||
| 106 | + } | ||
| 107 | + return true // 取消时允许关闭 | ||
| 108 | +} | ||
| 109 | +</script> | ||
| 110 | + | ||
| 111 | +<style lang="less" scoped> | ||
| 112 | +// 使用 Tailwind CSS 类名,无需额外样式 | ||
| 113 | +</style> |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-12-11 17:26:25 | 2 | * @Date: 2025-12-11 17:26:25 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-11 17:29:23 | 4 | + * @LastEditTime: 2025-12-11 20:19:04 |
| 5 | * @FilePath: /mlaj/src/components/count/postCountModel.vue | 5 | * @FilePath: /mlaj/src/components/count/postCountModel.vue |
| 6 | * @Description: 发布作业统计模型 | 6 | * @Description: 发布作业统计模型 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | <div class="post-count-model"> | 9 | <div class="post-count-model"> |
| 10 | <!-- TODO 感恩模块还没有做 --> | 10 | <!-- TODO 感恩模块还没有做 --> |
| 11 | - <div>感恩模块还没有做</div> | 11 | + <div class="flex justify-between items-center mb-2"> |
| 12 | + <div class="text-gray-500">感恩次数: </div> | ||
| 13 | + <div class="font-bold">{{ countData.objCount }} 次</div> | ||
| 14 | + </div> | ||
| 15 | + <div class="flex justify-between items-center"> | ||
| 16 | + <div class="text-gray-500">感恩对象: </div> | ||
| 17 | + <div class="font-bold">{{ countData.objName.join('、') }}</div> | ||
| 18 | + </div> | ||
| 12 | </div> | 19 | </div> |
| 13 | </template> | 20 | </template> |
| 14 | 21 | ||
| ... | @@ -23,13 +30,20 @@ const props = defineProps({ | ... | @@ -23,13 +30,20 @@ const props = defineProps({ |
| 23 | } | 30 | } |
| 24 | }) | 31 | }) |
| 25 | 32 | ||
| 26 | - | 33 | +// mock count data |
| 34 | +const countData = ref({ | ||
| 35 | + objCount: 3, | ||
| 36 | + objName: ['张三', '李四', '王五'] | ||
| 37 | +}) | ||
| 27 | </script> | 38 | </script> |
| 28 | 39 | ||
| 29 | -<style lang="less" scoped> | 40 | +<style lang="less"> |
| 30 | .post-count-model { | 41 | .post-count-model { |
| 31 | - padding: 10px; | 42 | + color: #4caf50; |
| 32 | - border: 1px solid #ccc; | 43 | + padding: 0.5rem 1rem; |
| 33 | - border-radius: 5px; | 44 | + // border: 1px solid #eaeaea; |
| 45 | + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); | ||
| 46 | + border-radius: 10px; | ||
| 47 | + font-size: 0.85rem; | ||
| 34 | } | 48 | } |
| 35 | </style> | 49 | </style> | ... | ... |
| ... | @@ -67,17 +67,12 @@ | ... | @@ -67,17 +67,12 @@ |
| 67 | </div> | 67 | </div> |
| 68 | 68 | ||
| 69 | <!-- 新增计数对象弹框 --> | 69 | <!-- 新增计数对象弹框 --> |
| 70 | - <van-dialog v-model:show="showAddTargetDialog" :title="`添加${dynamicFieldText}对象`" show-cancel-button | 70 | + <AddTargetDialog |
| 71 | - confirmButtonColor="#4caf50" :before-close="onBeforeClose"> | 71 | + v-model:show="showAddTargetDialog" |
| 72 | - <div class="p-4"> | 72 | + :title="`添加${dynamicFieldText}对象`" |
| 73 | - <div v-for="field in dynamicFormFields" :key="field.id"> | 73 | + :fields="dynamicFormFields" |
| 74 | - <van-field v-model="field.value" :label="field.label" :placeholder="'请输入' + field.label" | 74 | + @confirm="confirmAddTarget" |
| 75 | - :type="field.type === 'textarea' ? 'textarea' : 'text'" | 75 | + /> |
| 76 | - :rows="field.type === 'textarea' ? 2 : 1" :autosize="field.type === 'textarea'" | ||
| 77 | - class="border-b border-gray-100" :required="field.required" /> | ||
| 78 | - </div> | ||
| 79 | - </div> | ||
| 80 | - </van-dialog> | ||
| 81 | 76 | ||
| 82 | <!-- 文本输入区域 --> | 77 | <!-- 文本输入区域 --> |
| 83 | <div class="text-input-area"> | 78 | <div class="text-input-area"> |
| ... | @@ -89,7 +84,7 @@ | ... | @@ -89,7 +84,7 @@ |
| 89 | <!-- 类型选项卡 --> | 84 | <!-- 类型选项卡 --> |
| 90 | <div class="checkin-tabs"> | 85 | <div class="checkin-tabs"> |
| 91 | <div class="tabs-header"> | 86 | <div class="tabs-header"> |
| 92 | - <div class="tab-title">选择类型</div> | 87 | + <div class="tab-title">附件类型</div> |
| 93 | <div class="tabs-nav"> | 88 | <div class="tabs-nav"> |
| 94 | <div v-for="option in attachmentTypeOptions" :key="option.key" | 89 | <div v-for="option in attachmentTypeOptions" :key="option.key" |
| 95 | @click="switchType(option.key)" :class="['tab-item', { | 90 | @click="switchType(option.key)" :class="['tab-item', { |
| ... | @@ -195,6 +190,7 @@ import { useTitle } from '@vueuse/core' | ... | @@ -195,6 +190,7 @@ import { useTitle } from '@vueuse/core' |
| 195 | import { useCheckin } from '@/composables/useCheckin' | 190 | import { useCheckin } from '@/composables/useCheckin' |
| 196 | import AudioPlayer from '@/components/ui/AudioPlayer.vue' | 191 | import AudioPlayer from '@/components/ui/AudioPlayer.vue' |
| 197 | import VideoPlayer from '@/components/ui/VideoPlayer.vue' | 192 | import VideoPlayer from '@/components/ui/VideoPlayer.vue' |
| 193 | +import AddTargetDialog from '@/components/count/AddTargetDialog.vue' | ||
| 198 | import { showToast, showLoadingToast } from 'vant' | 194 | import { showToast, showLoadingToast } from 'vant' |
| 199 | import dayjs from 'dayjs' | 195 | import dayjs from 'dayjs' |
| 200 | 196 | ||
| ... | @@ -293,10 +289,10 @@ const showAddTargetDialog = ref(false) | ... | @@ -293,10 +289,10 @@ const showAddTargetDialog = ref(false) |
| 293 | 289 | ||
| 294 | // 动态表单字段 Mock 数据 | 290 | // 动态表单字段 Mock 数据 |
| 295 | const dynamicFormFields = ref([ | 291 | const dynamicFormFields = ref([ |
| 296 | - { id: 'name', label: '姓名', value: '', type: 'text', required: true }, | 292 | + { id: 'name', label: '姓名', type: 'text', required: true }, |
| 297 | - { id: 'city', label: '城市', value: '', type: 'text', required: true }, | 293 | + { id: 'city', label: '城市', type: 'text', required: true }, |
| 298 | - { id: 'school', label: '单位', value: '', type: 'text', required: true }, | 294 | + { id: 'school', label: '单位', type: 'text', required: true }, |
| 299 | - { id: 'remark', label: '备注', value: '', type: 'textarea', required: true } // 新增字段 | 295 | + { id: 'remark', label: '备注备注备注', type: 'textarea', required: true } // 新增字段 |
| 300 | ]) | 296 | ]) |
| 301 | 297 | ||
| 302 | const toggleTarget = (item) => { | 298 | const toggleTarget = (item) => { |
| ... | @@ -308,30 +304,11 @@ const toggleTarget = (item) => { | ... | @@ -308,30 +304,11 @@ const toggleTarget = (item) => { |
| 308 | } | 304 | } |
| 309 | } | 305 | } |
| 310 | 306 | ||
| 311 | -const onBeforeClose = (action) => { | 307 | +/** |
| 312 | - if (action === 'confirm') { | 308 | + * 确认添加对象 |
| 313 | - // 校验必填项 | 309 | + * @param {Object} formData - 表单数据 |
| 314 | - for (const field of dynamicFormFields.value) { | 310 | + */ |
| 315 | - if (field.required && !field.value.trim()) { | 311 | +const confirmAddTarget = (formData) => { |
| 316 | - showToast(`请填写${field.label}`) | ||
| 317 | - return false // 阻止关闭 | ||
| 318 | - } | ||
| 319 | - } | ||
| 320 | - | ||
| 321 | - // 校验通过,处理提交逻辑 | ||
| 322 | - confirmAddTarget() | ||
| 323 | - return true // 允许关闭 | ||
| 324 | - } | ||
| 325 | - return true // 取消时允许关闭 | ||
| 326 | -} | ||
| 327 | - | ||
| 328 | -const confirmAddTarget = () => { | ||
| 329 | - // 收集表单数据 | ||
| 330 | - const formData = dynamicFormFields.value.reduce((acc, field) => { | ||
| 331 | - acc[field.id] = field.value | ||
| 332 | - return acc | ||
| 333 | - }, {}) | ||
| 334 | - | ||
| 335 | console.log(`新增${dynamicFieldText.value}对象信息:`, formData) | 312 | console.log(`新增${dynamicFieldText.value}对象信息:`, formData) |
| 336 | 313 | ||
| 337 | // 添加到列表(适配原有的数据结构) | 314 | // 添加到列表(适配原有的数据结构) |
| ... | @@ -341,9 +318,6 @@ const confirmAddTarget = () => { | ... | @@ -341,9 +318,6 @@ const confirmAddTarget = () => { |
| 341 | school: formData.school | 318 | school: formData.school |
| 342 | // 其他字段... | 319 | // 其他字段... |
| 343 | }) | 320 | }) |
| 344 | - | ||
| 345 | - // 重置表单 | ||
| 346 | - dynamicFormFields.value.forEach(field => field.value = '') | ||
| 347 | } | 321 | } |
| 348 | 322 | ||
| 349 | /** | 323 | /** | ... | ... |
-
Please register or login to post a comment