hookehuyr

feat(打卡): 支持计数打卡类型并优化提交逻辑

添加计数打卡类型的支持,包括动态字段显示和额外数据提交
重构提交逻辑,将校验和数据处理分离到单独方法
优化UI文本显示,使其更通用化
......@@ -20,9 +20,12 @@ export function useCheckin() {
const loading = ref(false)
const message = ref('')
const fileList = ref([])
const activeType = ref('text') // 当前选中的打卡类型
const activeType = ref('') // 当前选中的打卡类型
const maxCount = ref(5)
// 打卡类型
const checkinType = computed(() => route.query.type)
// 用于记忆不同类型的文件列表
const fileListMemory = ref({
text: [],
......@@ -35,6 +38,11 @@ export function useCheckin() {
* 是否可以提交
*/
const canSubmit = computed(() => {
// 如果是计数打卡,交由组件内部校验
if (checkinType.value === 'count') {
return true
}
if (activeType.value === 'text') {
// 文字打卡:必须填写内容且长度不少于10个字符
return message.value.trim() !== '' && message.value.trim().length >= 10
......@@ -249,11 +257,13 @@ export function useCheckin() {
/**
* 提交打卡
* @param {Object} extraData - 额外提交数据
*/
const onSubmit = async () => {
const onSubmit = async (extraData = {}) => {
if (uploading.value) return
// 表单验证
if (checkinType.value !== 'count') {
if (activeType.value === 'text') {
if (message.value.trim().length < 10) {
showToast('打卡内容至少需要10个字符')
......@@ -264,10 +274,7 @@ export function useCheckin() {
showToast('请先上传文件')
return
}
// if (message.value.trim() === '') {
// showToast('请输入打卡留言')
// return
// }
}
}
uploading.value = true
......@@ -284,6 +291,7 @@ export function useCheckin() {
file_type: activeType.value,
meta_id: [],
makeup_time: route.query.is_patch ? route.query.date : '',
...extraData
}
// 如果有文件,添加文件ID
......
......@@ -16,40 +16,29 @@
<!-- 打卡内容区域 -->
<div class="section-wrapper">
<div class="section-title">打卡留言</div>
<div class="section-title">提交作业</div>
<div class="section-content">
<!-- 作业弹框选择区域 -->
<div class="mb-4">
<van-field
v-model="selectedTaskText"
is-link
readonly
label="选择作业"
placeholder="请选择本次打卡的作业"
@click="showTaskPicker = true"
class="rounded-lg border border-gray-100"
/>
<van-field v-model="selectedTaskText" is-link readonly label="选择作业" placeholder="请选择本次打卡的作业"
@click="showTaskPicker = true" class="rounded-lg border border-gray-100" />
<van-popup v-model:show="showTaskPicker" round position="bottom">
<van-picker
:columns="taskOptions"
@cancel="showTaskPicker = false"
@confirm="onConfirmTask"
/>
<van-picker :columns="taskOptions" @cancel="showTaskPicker = false"
@confirm="onConfirmTask" />
</van-popup>
</div>
<!-- 计数对象 -->
<div v-if="checkinType === 'count'" class="mb-4">
<div class="flex justify-between items-center mb-2 mx-2">
<div class="text-sm font-bold text-gray-700">计数对象</div>
<van-button size="small" type="primary" plain icon="plus" @click="showAddTargetDialog = true" class="!h-7">添加</van-button>
<div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}对象</div>
<van-button size="small" type="primary" plain icon="plus"
@click="showAddTargetDialog = true" class="!h-7">添加</van-button>
</div>
<div class="bg-gray-50 rounded-lg p-2">
<div class="flex flex-wrap gap-2">
<template v-if="targetList.length > 0">
<div
v-for="(item, index) in targetList"
:key="index"
<div v-for="(item, index) in targetList" :key="index"
class="px-4 py-1.5 rounded-full text-sm transition-colors duration-200 border cursor-pointer select-none"
:style="selectedTargets.some(t => t.name === item.name) ? {
backgroundColor: '#4caf50',
......@@ -59,45 +48,33 @@
backgroundColor: '#ffffff',
color: '#4b5563',
borderColor: '#e5e7eb'
}"
@click="toggleTarget(item)"
>
}" @click="toggleTarget(item)">
{{ item.name }}
</div>
</template>
<div v-else class="w-full text-center py-4 text-gray-400 text-sm">
暂无计数对象,请点击上方添加按钮
暂无{{ dynamicFieldText }}对象,请点击上方添加按钮
</div>
</div>
</div>
</div>
<!-- 计数次数 -->
<div v-if="checkinType === 'count'" class="mb-4 flex items-center justify-between bg-gray-50 p-3 rounded-lg">
<div class="text-sm font-bold text-gray-700">计数次数</div>
<div v-if="checkinType === 'count'"
class="mb-4 flex items-center justify-between bg-gray-50 p-3 rounded-lg">
<div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}次数</div>
<van-stepper v-model="countValue" min="1" integer input-width="80px" button-size="28px" />
</div>
<!-- 新增计数对象弹框 -->
<van-dialog
v-model:show="showAddTargetDialog"
title="添加计数对象"
show-cancel-button
confirmButtonColor="#4caf50"
:before-close="onBeforeClose"
>
<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"
<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"
/>
:rows="field.type === 'textarea' ? 2 : 1" :autosize="field.type === 'textarea'"
class="border-b border-gray-100" :required="field.required" />
</div>
</div>
</van-dialog>
......@@ -105,14 +82,14 @@
<!-- 文本输入区域 -->
<div class="text-input-area">
<van-field v-model="message" rows="6" autosize type="textarea"
:placeholder="activeType === 'text' ? '请输入打卡留言,至少需要10个字符' : '请输入打卡留言(可选)'"
:maxlength="activeType === 'text' ? 500 : 200" show-word-limit />
:placeholder="checkinType === 'count' ? '请输入留言(可选)' : (activeType === 'text' ? '请输入留言,至少需要10个字符' : '请输入留言(可选)')"
:maxlength="activeType === 'text' && checkinType !== 'count' ? 500 : 200" show-word-limit />
</div>
<!-- 打卡类型选项卡 -->
<!-- 类型选项卡 -->
<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', {
......@@ -125,7 +102,7 @@
</div>
<!-- 文件上传区域 -->
<div v-if="activeType !== 'text'" class="upload-area">
<div v-if="activeType !== '' && activeType !== 'text'" class="upload-area">
<van-uploader v-model="fileList" :max-count="maxCount" :max-size="20 * 1024 * 1024"
:before-read="beforeRead" :after-read="afterRead" @delete="onDelete"
@click-preview="onClickPreview" multiple :accept="getAcceptType()" result-type="file"
......@@ -154,9 +131,8 @@
<!-- 提交按钮 -->
<div v-if="!taskDetail.is_finish || route.query.status === 'edit'" class="submit-area">
<van-button type="primary" block size="large" :loading="uploading" :disabled="!canSubmit"
@click="onSubmit">
{{ route.query.status === 'edit' ? '保存修改' : '提交打卡' }}
<van-button type="primary" block size="large" :loading="uploading" @click="handleSubmit">
{{ route.query.status === 'edit' ? '保存修改' : '提交作业' }}
</van-button>
</div>
</div>
......@@ -224,7 +200,7 @@ import dayjs from 'dayjs'
const route = useRoute()
const router = useRouter()
useTitle('打卡详情')
useTitle('提交作业')
// 使用打卡composable
const {
......@@ -244,6 +220,9 @@ const {
initEditData
} = useCheckin()
// 动态字段文字
const dynamicFieldText = ref('感恩')
// 任务详情数据
const taskDetail = ref({})
......@@ -318,7 +297,7 @@ const confirmAddTarget = () => {
return acc
}, {})
console.log('新增计数对象信息:', formData)
console.log(`新增${dynamicFieldText.value}对象信息:`, formData)
// 添加到列表(适配原有的数据结构)
targetList.value.push({
......@@ -332,6 +311,36 @@ const confirmAddTarget = () => {
dynamicFormFields.value.forEach(field => field.value = '')
}
const handleSubmit = async () => {
// 1. 校验作业选择
if (!selectedTaskValue.value) {
showToast('请选择作业')
return
}
const extraData = {
task_option: selectedTaskValue.value // 假设字段名为 task_option
}
// 2. 计数打卡特定校验
if (checkinType.value === 'count') {
if (selectedTargets.value.length === 0) {
showToast(`请选择${dynamicFieldText.value}对象`)
return
}
if (!countValue.value || countValue.value <= 0) {
showToast(`${dynamicFieldText.value}次数必须大于0`)
return
}
// 传递额外数据
extraData.targets = selectedTargets.value.map(t => t.name) // 假设传 name
extraData.count = countValue.value
}
await onSubmit(extraData)
}
// 作品类型选项
const attachmentTypeOptions = ref([])
......@@ -422,9 +431,6 @@ const getTaskDetail = async (month) => {
if (code) {
taskDetail.value = data
// 获取作品类型数据
if (data.attachment_type && data.attachment_type.length) {
// 创建类型映射
const typeMap = {
'text': '文本',
'image': '图片',
......@@ -432,31 +438,28 @@ const getTaskDetail = async (month) => {
'video': '视频'
}
// attachment_type 和 file_type 合并成一个数组 合并以后的数组,就是学员编辑的时候,可以使用的类型
// 主要是适配先前打卡类型和后期打卡类型不一致的情况
if (route.query.status === 'edit') {
// 处理编辑模式下的类型合并
if (route.query.status === 'edit' && Array.isArray(data.attachment_type)) {
const info = await getUploadTaskInfoAPI({ i: route.query.post_id });
if (info.code) {
// 合并 attachment_type 和 file_type 数组, 里面数据需要去重复
data.attachment_type = [...new Set([...data.attachment_type, info.data.file_type])];
}
}
// 如果是数组格式,转换为对象格式
if (Array.isArray(data.attachment_type)) {
attachmentTypeOptions.value = data.attachment_type.map(key => ({
key,
value: typeMap[key] || key
}))
} else {
// 如果是对象格式,直接使用
} else if (data.attachment_type && typeof data.attachment_type === 'object') {
attachmentTypeOptions.value = Object.entries(data.attachment_type).map(([key, value]) => ({
key,
value
}))
}
} else {
// 显示4种类型
// 如果没有解析出任何类型,或者列表为空,则使用默认4种类型
if (attachmentTypeOptions.value.length === 0) {
attachmentTypeOptions.value = [
{ key: 'text', value: '文本' },
{ key: 'image', value: '图片' },
......@@ -464,6 +467,11 @@ const getTaskDetail = async (month) => {
{ key: 'video', value: '视频' }
]
}
// 设置默认选中类型(非计数打卡模式下)
if (checkinType.value !== 'count' && attachmentTypeOptions.value.length > 0 && !activeType.value) {
activeType.value = attachmentTypeOptions.value[0].key
}
}
}
......@@ -545,7 +553,7 @@ const onClickPreview = (file, detail) => {
// 图片预览由van-uploader的@click-preview事件处理,避免重复弹出
return
} else {
console.log('该文件类型不支持预览,文件名:', fileName, '打卡类型:', activeType.value)
console.log('该文件类型不支持预览,文件名:', fileName, '类型:', activeType.value)
showToast('该文件类型不支持预览')
}
}
......