hookehuyr

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

添加 AddTargetDialog 组件用于统一处理对象添加逻辑
在 postCountModel 中实现感恩模块的统计数据显示
重构 CheckinDetailPage 使用新组件并优化表单处理逻辑
......@@ -9,6 +9,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ActivityCard: typeof import('./components/ui/ActivityCard.vue')['default']
AddTargetDialog: typeof import('./components/count/AddTargetDialog.vue')['default']
AppLayout: typeof import('./components/layout/AppLayout.vue')['default']
AudioPlayer: typeof import('./components/ui/AudioPlayer.vue')['default']
BottomNav: typeof import('./components/layout/BottomNav.vue')['default']
......
<template>
<van-dialog
:show="show"
:title="title"
width="90%"
show-cancel-button
confirmButtonColor="#4caf50"
:before-close="onBeforeClose"
@update:show="updateShow"
>
<div class="p-4">
<div v-for="field in localFields" :key="field.id">
<van-field
v-model="field.value"
label-width="4rem"
:label="field.label"
:placeholder="'请输入' + field.label"
:type="field.type === 'textarea' ? 'textarea' : 'text'"
:rows="field.type === 'textarea' ? 2 : 1"
:autosize="field.type === 'textarea'"
class="border-b border-gray-100"
:required="field.required"
/>
</div>
</div>
</van-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { showToast } from 'vant'
const props = defineProps({
/**
* 是否显示弹窗
*/
show: {
type: Boolean,
required: true
},
/**
* 弹窗标题
*/
title: {
type: String,
default: '添加对象'
},
/**
* 表单字段配置
* @type {Array<{id: string, label: string, type: string, required: boolean}>}
*/
fields: {
type: Array,
required: true
}
})
const emit = defineEmits(['update:show', 'confirm'])
// 本地表单字段状态
const localFields = ref([])
// 监听弹窗显示状态,初始化表单
watch(() => props.show, (val) => {
if (val) {
// 初始化字段,添加 value 属性
localFields.value = props.fields.map(field => ({
...field,
value: ''
}))
}
})
/**
* 更新弹窗显示状态
* @param {boolean} val - 显示状态
*/
const updateShow = (val) => {
emit('update:show', val)
}
/**
* 弹窗关闭前的回调
* @param {string} action - 动作类型 'confirm' | 'cancel'
* @returns {boolean} 是否允许关闭
*/
const onBeforeClose = (action) => {
if (action === 'confirm') {
// 校验必填项
for (const field of localFields.value) {
if (field.required && !field.value.trim()) {
showToast(`请填写${field.label}`)
return false // 阻止关闭
}
}
// 收集表单数据
const formData = localFields.value.reduce((acc, field) => {
acc[field.id] = field.value
return acc
}, {})
// 触发确认事件,传递表单数据
emit('confirm', formData)
return true // 允许关闭
}
return true // 取消时允许关闭
}
</script>
<style lang="less" scoped>
// 使用 Tailwind CSS 类名,无需额外样式
</style>
<!--
* @Date: 2025-12-11 17:26:25
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-11 17:29:23
* @LastEditTime: 2025-12-11 20:19:04
* @FilePath: /mlaj/src/components/count/postCountModel.vue
* @Description: 发布作业统计模型
-->
<template>
<div class="post-count-model">
<!-- TODO 感恩模块还没有做 -->
<div>感恩模块还没有做</div>
<div class="flex justify-between items-center mb-2">
<div class="text-gray-500">感恩次数: </div>
<div class="font-bold">{{ countData.objCount }} 次</div>
</div>
<div class="flex justify-between items-center">
<div class="text-gray-500">感恩对象: </div>
<div class="font-bold">{{ countData.objName.join('、') }}</div>
</div>
</div>
</template>
......@@ -23,13 +30,20 @@ const props = defineProps({
}
})
// mock count data
const countData = ref({
objCount: 3,
objName: ['张三', '李四', '王五']
})
</script>
<style lang="less" scoped>
<style lang="less">
.post-count-model {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
color: #4caf50;
padding: 0.5rem 1rem;
// border: 1px solid #eaeaea;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
border-radius: 10px;
font-size: 0.85rem;
}
</style>
......
......@@ -67,17 +67,12 @@
</div>
<!-- 新增计数对象弹框 -->
<van-dialog v-model:show="showAddTargetDialog" :title="`添加${dynamicFieldText}对象`" show-cancel-button
confirmButtonColor="#4caf50" :before-close="onBeforeClose">
<div class="p-4">
<div v-for="field in dynamicFormFields" :key="field.id">
<van-field v-model="field.value" :label="field.label" :placeholder="'请输入' + field.label"
:type="field.type === 'textarea' ? 'textarea' : 'text'"
:rows="field.type === 'textarea' ? 2 : 1" :autosize="field.type === 'textarea'"
class="border-b border-gray-100" :required="field.required" />
</div>
</div>
</van-dialog>
<AddTargetDialog
v-model:show="showAddTargetDialog"
:title="`添加${dynamicFieldText}对象`"
:fields="dynamicFormFields"
@confirm="confirmAddTarget"
/>
<!-- 文本输入区域 -->
<div class="text-input-area">
......@@ -89,7 +84,7 @@
<!-- 类型选项卡 -->
<div class="checkin-tabs">
<div class="tabs-header">
<div class="tab-title">选择类型</div>
<div class="tab-title">附件类型</div>
<div class="tabs-nav">
<div v-for="option in attachmentTypeOptions" :key="option.key"
@click="switchType(option.key)" :class="['tab-item', {
......@@ -195,6 +190,7 @@ import { useTitle } from '@vueuse/core'
import { useCheckin } from '@/composables/useCheckin'
import AudioPlayer from '@/components/ui/AudioPlayer.vue'
import VideoPlayer from '@/components/ui/VideoPlayer.vue'
import AddTargetDialog from '@/components/count/AddTargetDialog.vue'
import { showToast, showLoadingToast } from 'vant'
import dayjs from 'dayjs'
......@@ -293,10 +289,10 @@ const showAddTargetDialog = ref(false)
// 动态表单字段 Mock 数据
const dynamicFormFields = ref([
{ id: 'name', label: '姓名', value: '', type: 'text', required: true },
{ id: 'city', label: '城市', value: '', type: 'text', required: true },
{ id: 'school', label: '单位', value: '', type: 'text', required: true },
{ id: 'remark', label: '备注', value: '', type: 'textarea', required: true } // 新增字段
{ id: 'name', label: '姓名', type: 'text', required: true },
{ id: 'city', label: '城市', type: 'text', required: true },
{ id: 'school', label: '单位', type: 'text', required: true },
{ id: 'remark', label: '备注备注备注', type: 'textarea', required: true } // 新增字段
])
const toggleTarget = (item) => {
......@@ -308,30 +304,11 @@ const toggleTarget = (item) => {
}
}
const onBeforeClose = (action) => {
if (action === 'confirm') {
// 校验必填项
for (const field of dynamicFormFields.value) {
if (field.required && !field.value.trim()) {
showToast(`请填写${field.label}`)
return false // 阻止关闭
}
}
// 校验通过,处理提交逻辑
confirmAddTarget()
return true // 允许关闭
}
return true // 取消时允许关闭
}
const confirmAddTarget = () => {
// 收集表单数据
const formData = dynamicFormFields.value.reduce((acc, field) => {
acc[field.id] = field.value
return acc
}, {})
/**
* 确认添加对象
* @param {Object} formData - 表单数据
*/
const confirmAddTarget = (formData) => {
console.log(`新增${dynamicFieldText.value}对象信息:`, formData)
// 添加到列表(适配原有的数据结构)
......@@ -341,9 +318,6 @@ const confirmAddTarget = () => {
school: formData.school
// 其他字段...
})
// 重置表单
dynamicFormFields.value.forEach(field => field.value = '')
}
/**
......