hookehuyr

feat(打卡): 重构计数打卡功能,支持表单数据复用

- 修改计数对象为列表项,增加确认模式
- 重构动态表单字段处理逻辑
- 更新API接口参数为gratitude_form_list
- 优化计数打卡的提交逻辑
/*
* @Date: 2025-06-06 09:26:16
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-16 11:52:53
* @LastEditTime: 2025-12-16 17:54:43
* @FilePath: /mlaj/src/api/checkin.js
* @Description: 签到模块相关接口
*/
......@@ -22,6 +22,7 @@ const Api = {
CHECKIN_TEACHER_LIST: '/srv/?a=checkin&t=teacher_list',
CHECKIN_TEACHER_REVIEW: '/srv/?a=checkin&t=teacher_review',
CHECKIN_TEACHER_CHECKED_DATES: '/srv/?a=checkin&t=teacher_checked_dates',
CHECKIN_TEACHER_REUSE_GRATITUDE_FORM: '/srv/?a=checkin&t=reuse_gratitude_form',
}
/**
......@@ -75,7 +76,7 @@ export const checkinTaskAPI = (params) => fn(fetch.post(Api.TASK_CHECKIN, param
* @param file_type 上传附件的类型 image=上传图片,video=视频,audio=音频
* @param makeup_time 补卡时间
* @param gratitude_count 感恩次数
* @param gratitude_people_ids 感恩对象ID数组 [id1,id2,id3]
* @param gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}]
* @returns
*/
export const addUploadTaskAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_ADD, params))
......@@ -106,7 +107,7 @@ export const getUploadTaskListAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_L
* avatar 打卡人头像, created_time 打卡时间, created_time_desc 打卡时间描述, note 打卡内容, files[{meta_id,name,value,extension}] 附件列表,
* file_type 上传附件的类型 image=上传图片,video=视频,audio=音频, like_count 点赞数, is_my 是不是我的打卡, is_like 我是否已经点赞, is_makeup 是否补卡
* gratitude_count 感恩次数
* gratitude_people 感恩对象列表 [{id,name,city,unit}]
* gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}]
* }
*/
export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_INFO, params))
......@@ -118,7 +119,7 @@ export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_IN
* @param meta_id[] 附件ID列表
* @param file_type 上传附件的类型 image=上传图片,video=视频,audio=音频
* @param gratitude_count 感恩次数
* @param gratitude_people_ids 感恩对象ID数组 [id1,id2,id3]
* @param gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}]
* @returns
*/
export const editUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_EDIT, params))
......@@ -183,3 +184,10 @@ export const checkinTaskReviewAPI = (params) => fn(fetch.post(Api.CHECKIN_TEACH
* @returns data: { my_checkin_dates 已打卡日期列表 }
*/
export const getCheckinTeacherCheckedDatesAPI = (params) => fn(fetch.get(Api.CHECKIN_TEACHER_CHECKED_DATES, params))
/**
* @description: 复用感恩表单数据
* @param subtask_id 小作业ID
* @returns
*/
export const reuseGratitudeFormAPI = (params) => fn(fetch.post(Api.CHECKIN_TEACHER_REUSE_GRATITUDE_FORM, params))
......
/*
* @Date: 2025-06-06 09:26:16
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-16 13:59:55
* @LastEditTime: 2025-12-16 17:52:19
* @FilePath: /mlaj/src/api/gratitude.js
* @Description: 计数模块相关接口
*/
......
......@@ -12,7 +12,7 @@
<div v-for="field in localFields" :key="field.id">
<van-field
v-model="field.value"
label-width="4rem"
label-width="5rem"
:label="field.label"
:placeholder="'请输入' + field.label"
:type="field.type === 'textarea' ? 'textarea' : 'text'"
......@@ -43,7 +43,7 @@ const props = defineProps({
*/
title: {
type: String,
default: '添加对象'
default: '添加列表项'
},
/**
* 表单字段配置
......@@ -101,14 +101,8 @@ const onBeforeClose = (action) => {
}
}
// 收集表单数据
const formData = localFields.value.reduce((acc, field) => {
acc[field.id] = field.value
return acc
}, {})
// 触发确认事件,传递表单数据
emit('confirm', formData)
// 触发确认事件,传递表单数据 (保持与 fields 结构一致)
emit('confirm', localFields.value)
return true // 允许关闭
}
return true // 取消时允许关闭
......
<!--
* @Date: 2025-12-16 11:44:27
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-16 14:17:38
* @LastEditTime: 2025-12-16 17:59:29
* @FilePath: /mlaj/src/components/count/CheckinTargetList.vue
* @Description: 打卡动态对象列表组件
-->
......@@ -9,7 +9,7 @@
<div class="mb-4">
<div class="flex justify-between items-center mb-2 mx-2">
<div class="flex items-center gap-2">
<div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}对象</div>
<div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}列表</div>
<div class="text-xs text-gray-400 font-normal scale-90 origin-left">(长按可编辑/删除)</div>
</div>
<van-button size="small" type="primary" plain icon="plus" @click="onAdd" class="!h-7">添加</van-button>
......@@ -49,7 +49,7 @@
</div>
</template>
<div v-else class="w-full text-center py-4 text-gray-400 text-sm">
暂无{{ dynamicFieldText }}对象,请点击上方添加按钮
暂无{{ dynamicFieldText }}列表,请点击上方添加按钮
</div>
</div>
</div>
......
......@@ -307,13 +307,20 @@ export function useCheckin() {
let result
if (route.query.status === 'edit') {
// 编辑打卡
result = await editUploadTaskInfoAPI({
const editData = {
i: route.query.post_id,
subtask_id: submitData.subtask_id || route.query.subtask_id,
note: submitData.note,
meta_id: submitData.meta_id,
file_type: submitData.file_type,
})
}
// 如果有计数对象列表,也需要传递
if (submitData.gratitude_form_list) {
editData.gratitude_form_list = submitData.gratitude_form_list
}
result = await editUploadTaskInfoAPI(editData)
} else {
// 新增打卡
result = await addUploadTaskAPI(submitData)
......
......@@ -58,7 +58,7 @@
<!-- 新增计数对象弹框 -->
<AddTargetDialog
v-model:show="showAddTargetDialog"
:title="editingTarget ? `编辑${dynamicFieldText}对象` : `添加${dynamicFieldText}对象`"
:title="editingTarget ? (isConfirmMode ? `确认${dynamicFieldText}项` : `编辑${dynamicFieldText}项`) : `添加${dynamicFieldText}项`"
:fields="dynamicFormFields"
:initial-values="editingTarget"
@confirm="confirmAddTarget"
......@@ -174,9 +174,8 @@
<script setup>
import { ref, computed, onMounted, nextTick, reactive, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getTaskDetailAPI, getUploadTaskInfoAPI, getSubtaskListAPI } from "@/api/checkin"
import { getTaskDetailAPI, getUploadTaskInfoAPI, getSubtaskListAPI, reuseGratitudeFormAPI } from "@/api/checkin"
import { getTeacherFindSettingsAPI } from '@/api/teacher'
import { getGratitudeListAPI, gratitudeAddAPI, gratitudeEditAPI, gratitudeDelAPI } from '@/api/gratitude'
import { useTitle } from '@vueuse/core'
import { useCheckin } from '@/composables/useCheckin'
import AudioPlayer from '@/components/ui/AudioPlayer.vue'
......@@ -225,34 +224,48 @@ const taskType = computed(() => route.query.task_type)
const showTaskPicker = ref(false)
const taskOptions = ref([])
const fetchTargetList = async (person_type) => {
const { code, data } = await getGratitudeListAPI({ person_type })
const fetchTargetList = async (subtask_id) => {
const { code, data } = await reuseGratitudeFormAPI({ subtask_id })
if (code) {
targetList.value = data.gratitude_people || []
}
targetList.value = [{
id: 1,
name: '张三',
city: '北京',
unit: '公司',
}, {
id: 2,
name: '李四',
city: '上海',
unit: '公司',
}]
}
// 动态表单字段 (默认值,实际会根据选择的作业动态更新)
const dynamicFormFields = ref([])
const personType = ref('') // 动态表单字段中的person_type
// 确认作业选择
const onConfirmTask = ({ selectedOptions }) => {
const option = selectedOptions[0]
selectedTaskText.value = option.text
selectedTaskValue.value = [option.value]
isMakeup.value = !!option.is_makeup
showTaskPicker.value = false
personType.value = option.person_type
// 动态表单字段映射
/**
* 更新动态表单字段
* @param {Object} option - 选中的作业选项
*/
const updateDynamicFormFields = (option) => {
if (option.field_list && Array.isArray(option.field_list)) {
dynamicFormFields.value = option.field_list.map(field => ({
id: field.field_name,
label: field.label,
dynamicFormFields.value = option.field_list.map(field => {
// 尝试多种方式获取ID
const id = field.field_name || field.id || field.name || field.key || field.field
if (!id) {
console.warn('动态表单字段缺少ID:', field)
}
return {
id: id,
label: field.label || '未命名',
type: 'text', // 默认类型,如果后端有类型字段可替换
required: true // 默认必填,如果后端有必填字段可替换
}))
}
})
// 确保如果有city字段,类型为textarea
const cityField = dynamicFormFields.value.find(f => f.id === 'city')
if (cityField) {
......@@ -271,10 +284,23 @@ const onConfirmTask = ({ selectedOptions }) => {
{ id: 'unit', label: '单位', type: 'textarea', required: true },
]
}
}
// 确认作业选择
const onConfirmTask = ({ selectedOptions }) => {
const option = selectedOptions[0]
selectedTaskText.value = option.text
selectedTaskValue.value = [option.value]
isMakeup.value = !!option.is_makeup
showTaskPicker.value = false
personType.value = option.person_type
// 更新动态表单字段
updateDynamicFormFields(option)
// 如果是计数打卡,根据选中的作业ID查询计数对象
if (taskType.value === 'count') {
fetchTargetList(personType.value)
fetchTargetList(selectedTaskValue.value[0])
}
}
......@@ -293,63 +319,77 @@ const selectedTargets = ref([])
const targetList = ref([])
const showAddTargetDialog = ref(false)
const editingTarget = ref(null)
const isConfirmMode = ref(false) // 是否为确认模式(首次点击选中)
const toggleTarget = (item) => {
const index = selectedTargets.value.findIndex(t => t.name === item.name)
if (index > -1) {
// 取消选中
selectedTargets.value.splice(index, 1)
} else {
// 选中逻辑:如果是第一次选中(未确认过),则弹出确认框
if (!item.has_confirmed) {
editingTarget.value = item
isConfirmMode.value = true
showAddTargetDialog.value = true
} else {
// 已确认过,直接选中
selectedTargets.value.push(item)
}
}
}
const openAddTargetDialog = () => {
editingTarget.value = null; // 重置编辑对象
isConfirmMode.value = false;
showAddTargetDialog.value = true;
}
/**
* 确认添加/编辑对象
* @param {Object} formData - 表单数据
* @param {Array} formFields - 表单字段数组
*/
const confirmAddTarget = async (formData) => {
console.log(`${editingTarget.value ? '编辑' : '新增'}${dynamicFieldText.value}对象信息:`, formData)
const confirmAddTarget = async (formFields) => {
// 将表单字段数组转换为对象
const formData = formFields.reduce((acc, field) => {
if (field.id) {
acc[field.id] = field.value
}
return acc
}, {})
if (editingTarget.value) {
// 编辑模式
// 编辑模式或确认模式
const index = targetList.value.findIndex(t => t === editingTarget.value)
if (index > -1) {
const { code } = await gratitudeEditAPI({ ...editingTarget.value })
if (code) {
// 更新对象
targetList.value[index] = { ...targetList.value[index], ...formData }
if (isConfirmMode.value) {
targetList.value[index].has_confirmed = true // 标记为已确认
}
// 如果在选中列表中,也需要更新
const selectedIndex = selectedTargets.value.findIndex(t => t.id === editingTarget.value.id)
if (selectedIndex > -1) {
selectedTargets.value[selectedIndex] = { ...selectedTargets.value[selectedIndex], ...formData }
}
// 如果是确认模式,确认后自动加入选中列表
if (isConfirmMode.value && selectedIndex === -1) {
selectedTargets.value.push(targetList.value[index])
}
showToast(isConfirmMode.value ? '确认成功' : '修改成功')
}
showToast('修改成功')
} else {
// 新增模式
try {
const res = await gratitudeAddAPI({
...formData,
person_type: personType.value
})
if (res.code) {
// 新增成功,更新本地列表
targetList.value.push({
...formData,
})
console.warn(formData);
console.warn(targetList.value);
showToast('新增成功')
}
} catch (error) {
showToast(`新增失败:${error.message || '未知错误'}`)
}
}
showAddTargetDialog.value = false;
}
......@@ -359,6 +399,7 @@ const confirmAddTarget = async (formData) => {
*/
const handleTargetEdit = (item) => {
editingTarget.value = item
isConfirmMode.value = false // 明确设置为非确认模式
showAddTargetDialog.value = true
}
......@@ -410,31 +451,31 @@ const isSubmitDisabled = computed(() => {
}
})
/**
* 提交打卡
*/
const handleSubmit = async () => {
// 1. 校验作业选择
if (!selectedTaskValue.value || selectedTaskValue.value.length === 0) {
showToast('请选择作业')
// 计数打卡校验
if (taskType.value === 'count') {
if (selectedTaskValue.value.length === 0) {
const taskText = taskOptions.value.find(t => t.value === selectedTaskValue.value[0])?.text || '作业'
showToast(`请选择${taskText}`)
return
}
const extraData = {
subtask_id: selectedTaskValue.value[0] // 小作业ID
}
// 2. 计数打卡特定校验
if (taskType.value === 'count') {
if (selectedTargets.value.length === 0) {
showToast(`请选择${dynamicFieldText.value}对象`)
const targetText = dynamicFieldText.value || '对象'
showToast(`请选择${targetText}`)
return
}
if (!countValue.value || countValue.value <= 0) {
showToast(`${dynamicFieldText.value}次数必须大于0`)
return
}
// 传递额外数据
extraData.targets = selectedTargets.value.map(t => t.id)
extraData.count = countValue.value
const extraData = {
subtask_id: selectedTaskValue.value.length > 0 ? selectedTaskValue.value[0] : ''
}
// 如果是计数打卡,添加选中的计数对象列表
if (taskType.value === 'count') {
extraData.gratitude_form_list = selectedTargets.value
}
await onSubmit(extraData)
......@@ -883,11 +924,13 @@ onMounted(async () => {
selectedTaskText.value = option.text
isMakeup.value = !!option.is_makeup
personType.value = option.person_type
// 初始化动态表单字段
updateDynamicFormFields(option)
}
// 如果是计数打卡,根据选中的作业ID查询计数对象
if (taskType.value === 'count') {
fetchTargetList(personType.value)
fetchTargetList(selectedTaskValue.value[0])
}
}
......