hookehuyr

feat(打卡): 新增添加对象弹窗组件并优化统计模块

添加 AddTargetDialog 组件用于统一处理对象添加逻辑
在 postCountModel 中实现感恩模块的统计数据显示
重构 CheckinDetailPage 使用新组件并优化表单处理逻辑
...@@ -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']
......
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 /**
......