feat(打卡): 添加感恩对象管理功能
- 新增CheckinTargetList组件用于展示和管理感恩对象 - 实现感恩对象的添加、编辑、删除和选择功能 - 在打卡详情页集成感恩对象管理组件 - 更新API接口支持感恩对象相关操作 - 优化动态表单字段处理逻辑
Showing
7 changed files
with
436 additions
and
88 deletions
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-06-06 09:26:16 | 2 | * @Date: 2025-06-06 09:26:16 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-13 21:13:42 | 4 | + * @LastEditTime: 2025-12-16 11:52:53 |
| 5 | * @FilePath: /mlaj/src/api/checkin.js | 5 | * @FilePath: /mlaj/src/api/checkin.js |
| 6 | * @Description: 签到模块相关接口 | 6 | * @Description: 签到模块相关接口 |
| 7 | */ | 7 | */ |
| ... | @@ -45,7 +45,18 @@ export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, p | ... | @@ -45,7 +45,18 @@ export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, p |
| 45 | * @description: 小作业列表 | 45 | * @description: 小作业列表 |
| 46 | * @param task_id 大作业ID | 46 | * @param task_id 大作业ID |
| 47 | * @param date 日期(用来判断是否可以补卡) | 47 | * @param date 日期(用来判断是否可以补卡) |
| 48 | - * @returns data: [{id 作业id,title 作业名称 ,cycle 作业周期 [0=本周期 | 30=每月 | 7=每周 | 1=每日],frequency 交作业的频次,attachment_type 上传附件的类型 [text=文本 image=图片 video=视频 audio=音频],begin_date 开始时间,end_date 结束时间,is_finish 作业在当前周期是否已经达标, is_gray 作业是否应该置灰, is_makeup 是否可以补卡}] | 48 | + * @returns data: [{ |
| 49 | + * id 作业id, | ||
| 50 | + * title 作业名称 , | ||
| 51 | + * cycle 作业周期 [0=本周期 | 30=每月 | 7=每周 | 1=每日], | ||
| 52 | + * frequency 交作业的频次,attachment_type 上传附件的类型 [text=文本 image=图片 video=视频 audio=音频], | ||
| 53 | + * begin_date 开始时间, | ||
| 54 | + * end_date 结束时间, | ||
| 55 | + * is_finish 作业在当前周期是否已经达标, | ||
| 56 | + * is_gray 作业是否应该置灰, | ||
| 57 | + * is_makeup 是否可以补卡 | ||
| 58 | + * field_list 动态表单字段列表 [{field_name,label,type}] | ||
| 59 | + * }] | ||
| 49 | */ | 60 | */ |
| 50 | export const getSubtaskListAPI = (params) => fn(fetch.get(Api.GET_SUBTASK_LIST, params)) | 61 | export const getSubtaskListAPI = (params) => fn(fetch.get(Api.GET_SUBTASK_LIST, params)) |
| 51 | 62 | ||
| ... | @@ -63,6 +74,8 @@ export const checkinTaskAPI = (params) => fn(fetch.post(Api.TASK_CHECKIN, param | ... | @@ -63,6 +74,8 @@ export const checkinTaskAPI = (params) => fn(fetch.post(Api.TASK_CHECKIN, param |
| 63 | * @param meta_id[] 附件ID列表 | 74 | * @param meta_id[] 附件ID列表 |
| 64 | * @param file_type 上传附件的类型 image=上传图片,video=视频,audio=音频 | 75 | * @param file_type 上传附件的类型 image=上传图片,video=视频,audio=音频 |
| 65 | * @param makeup_time 补卡时间 | 76 | * @param makeup_time 补卡时间 |
| 77 | + * @param gratitude_count 感恩次数 | ||
| 78 | + * @param gratitude_people_ids 感恩对象ID数组 [id1,id2,id3] | ||
| 66 | * @returns | 79 | * @returns |
| 67 | */ | 80 | */ |
| 68 | export const addUploadTaskAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_ADD, params)) | 81 | export const addUploadTaskAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_ADD, params)) |
| ... | @@ -79,7 +92,10 @@ export const addUploadTaskAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_ADD, | ... | @@ -79,7 +92,10 @@ export const addUploadTaskAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_ADD, |
| 79 | * @returns data: [{id 打卡动态ID, status 审批状态 3=待审批,5=审批通过,7=审批不通过, created_by 打卡人ID, username 打卡人昵称 | 92 | * @returns data: [{id 打卡动态ID, status 审批状态 3=待审批,5=审批通过,7=审批不通过, created_by 打卡人ID, username 打卡人昵称 |
| 80 | * avatar 打卡人头像, created_time 打卡时间, created_time_desc 打卡时间描述, note 打卡内容, files[{meta_id,name,value,extension}] 附件列表, | 93 | * avatar 打卡人头像, created_time 打卡时间, created_time_desc 打卡时间描述, note 打卡内容, files[{meta_id,name,value,extension}] 附件列表, |
| 81 | * file_type 上传附件的类型 image=上传图片,video=视频,audio=音频, like_count 点赞数, is_my 是不是我的打卡, is_like 我是否已经点赞, is_makeup 是否补卡 | 94 | * file_type 上传附件的类型 image=上传图片,video=视频,audio=音频, like_count 点赞数, is_my 是不是我的打卡, is_like 我是否已经点赞, is_makeup 是否补卡 |
| 82 | - * subtask_title 小作业标题}] | 95 | + * subtask_title 小作业标题 |
| 96 | + * gratitude_count 感恩次数 | ||
| 97 | + * gratitude_people 感恩对象列表 [{id,name,city,unit}] | ||
| 98 | + * }] | ||
| 83 | */ | 99 | */ |
| 84 | export const getUploadTaskListAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_LIST, params)) | 100 | export const getUploadTaskListAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_LIST, params)) |
| 85 | 101 | ||
| ... | @@ -88,7 +104,10 @@ export const getUploadTaskListAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_L | ... | @@ -88,7 +104,10 @@ export const getUploadTaskListAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_L |
| 88 | * @param i 打卡动态ID | 104 | * @param i 打卡动态ID |
| 89 | * @returns data: {id 打卡动态ID, subtask_id 小作业ID, status 审批状态 3=待审批,5=审批通过,7=审批不通过, created_by 打卡人ID, username 打卡人昵称 | 105 | * @returns data: {id 打卡动态ID, subtask_id 小作业ID, status 审批状态 3=待审批,5=审批通过,7=审批不通过, created_by 打卡人ID, username 打卡人昵称 |
| 90 | * avatar 打卡人头像, created_time 打卡时间, created_time_desc 打卡时间描述, note 打卡内容, files[{meta_id,name,value,extension}] 附件列表, | 106 | * avatar 打卡人头像, created_time 打卡时间, created_time_desc 打卡时间描述, note 打卡内容, files[{meta_id,name,value,extension}] 附件列表, |
| 91 | - * file_type 上传附件的类型 image=上传图片,video=视频,audio=音频, like_count 点赞数, is_my 是不是我的打卡, is_like 我是否已经点赞, is_makeup 是否补卡} | 107 | + * file_type 上传附件的类型 image=上传图片,video=视频,audio=音频, like_count 点赞数, is_my 是不是我的打卡, is_like 我是否已经点赞, is_makeup 是否补卡 |
| 108 | + * gratitude_count 感恩次数 | ||
| 109 | + * gratitude_people 感恩对象列表 [{id,name,city,unit}] | ||
| 110 | + * } | ||
| 92 | */ | 111 | */ |
| 93 | export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_INFO, params)) | 112 | export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_INFO, params)) |
| 94 | 113 | ||
| ... | @@ -98,6 +117,8 @@ export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_IN | ... | @@ -98,6 +117,8 @@ export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_IN |
| 98 | * @param note 打卡文字 | 117 | * @param note 打卡文字 |
| 99 | * @param meta_id[] 附件ID列表 | 118 | * @param meta_id[] 附件ID列表 |
| 100 | * @param file_type 上传附件的类型 image=上传图片,video=视频,audio=音频 | 119 | * @param file_type 上传附件的类型 image=上传图片,video=视频,audio=音频 |
| 120 | + * @param gratitude_count 感恩次数 | ||
| 121 | + * @param gratitude_people_ids 感恩对象ID数组 [id1,id2,id3] | ||
| 101 | * @returns | 122 | * @returns |
| 102 | */ | 123 | */ |
| 103 | export const editUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_EDIT, params)) | 124 | export const editUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_EDIT, params)) | ... | ... |
src/api/gratitude.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2025-06-06 09:26:16 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-12-16 13:59:55 | ||
| 5 | + * @FilePath: /mlaj/src/api/gratitude.js | ||
| 6 | + * @Description: 计数模块相关接口 | ||
| 7 | + */ | ||
| 8 | + | ||
| 9 | +import { fn, fetch } from './fn' | ||
| 10 | + | ||
| 11 | +const Api = { | ||
| 12 | + GET_GRATITUDE_LIST: '/srv/?a=gratitude_people&t=list', | ||
| 13 | + POST_GRATITUDE_ADD: '/srv/?a=gratitude_people&t=add', | ||
| 14 | + POST_GRATITUDE_EDIT: '/srv/?a=gratitude_people&t=edit', | ||
| 15 | + POST_GRATITUDE_DEL: '/srv/?a=gratitude_people&t=del', | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * @description: 计数对象列表 | ||
| 20 | + * @param: person_type 计数对象类型 teacher=老师, doctor=医生, parent=父母, sage=圣贤 | ||
| 21 | + * @return: data.gratitude_people [{ id 计数对象ID, name 计数对象名称, city 地址, unit 单位 }] | ||
| 22 | + */ | ||
| 23 | + | ||
| 24 | +export const getGratitudeListAPI = (params) => fn(fetch.get(Api.GET_GRATITUDE_LIST, params)) | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * @description: 添加感恩对象 | ||
| 28 | + * @param person_type 计数对象类型 teacher=老师, doctor=医生, parent=父母, sage=圣贤 | ||
| 29 | + * 下面的值应该是动态字段绑定的值, 通过field_list获取里面的field_name, 再根据field_name绑定值 | ||
| 30 | + * @param name 感恩对象名称 | ||
| 31 | + * @param city 地址 | ||
| 32 | + * @param unit 单位 | ||
| 33 | + * @returns | ||
| 34 | + */ | ||
| 35 | +export const gratitudeAddAPI = (params) => fn(fetch.post(Api.POST_GRATITUDE_ADD, params)) | ||
| 36 | + | ||
| 37 | +/** | ||
| 38 | + * @description: 编辑感恩对象 | ||
| 39 | + * @param id 感恩对象ID | ||
| 40 | + * 下面的值应该是动态字段绑定的值, 通过field_list获取里面的field_name, 再根据field_name绑定值 | ||
| 41 | + * @param name 感恩对象名称 | ||
| 42 | + * @param city 地址 | ||
| 43 | + * @param unit 单位 | ||
| 44 | + * @returns | ||
| 45 | + */ | ||
| 46 | +export const gratitudeEditAPI = (params) => fn(fetch.post(Api.POST_GRATITUDE_EDIT, params)) | ||
| 47 | + | ||
| 48 | +/** | ||
| 49 | + * @description: 删除感恩对象 | ||
| 50 | + * @param id 感恩对象ID | ||
| 51 | + * @returns | ||
| 52 | + */ | ||
| 53 | +export const gratitudeDelAPI = (params) => fn(fetch.post(Api.POST_GRATITUDE_DEL, params)) |
| ... | @@ -16,6 +16,7 @@ declare module 'vue' { | ... | @@ -16,6 +16,7 @@ declare module 'vue' { |
| 16 | CheckinCard: typeof import('./components/checkin/CheckinCard.vue')['default'] | 16 | CheckinCard: typeof import('./components/checkin/CheckinCard.vue')['default'] |
| 17 | CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default'] | 17 | CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default'] |
| 18 | CheckInList: typeof import('./components/ui/CheckInList.vue')['default'] | 18 | CheckInList: typeof import('./components/ui/CheckInList.vue')['default'] |
| 19 | + CheckinTargetList: typeof import('./components/count/CheckinTargetList.vue')['default'] | ||
| 19 | CollapsibleCalendar: typeof import('./components/ui/CollapsibleCalendar.vue')['default'] | 20 | CollapsibleCalendar: typeof import('./components/ui/CollapsibleCalendar.vue')['default'] |
| 20 | ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default'] | 21 | ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default'] |
| 21 | CourseCard: typeof import('./components/ui/CourseCard.vue')['default'] | 22 | CourseCard: typeof import('./components/ui/CourseCard.vue')['default'] | ... | ... |
| ... | @@ -52,6 +52,13 @@ const props = defineProps({ | ... | @@ -52,6 +52,13 @@ const props = defineProps({ |
| 52 | fields: { | 52 | fields: { |
| 53 | type: Array, | 53 | type: Array, |
| 54 | required: true | 54 | required: true |
| 55 | + }, | ||
| 56 | + /** | ||
| 57 | + * 初始数据(用于编辑回显) | ||
| 58 | + */ | ||
| 59 | + initialValues: { | ||
| 60 | + type: Object, | ||
| 61 | + default: () => ({}) | ||
| 55 | } | 62 | } |
| 56 | }) | 63 | }) |
| 57 | 64 | ||
| ... | @@ -60,16 +67,16 @@ const emit = defineEmits(['update:show', 'confirm']) | ... | @@ -60,16 +67,16 @@ const emit = defineEmits(['update:show', 'confirm']) |
| 60 | // 本地表单字段状态 | 67 | // 本地表单字段状态 |
| 61 | const localFields = ref([]) | 68 | const localFields = ref([]) |
| 62 | 69 | ||
| 63 | -// 监听弹窗显示状态,初始化表单 | 70 | +// 监听弹窗显示状态和字段配置变化,初始化表单 |
| 64 | -watch(() => props.show, (val) => { | 71 | +watch([() => props.show, () => props.fields], ([showVal, fieldsVal]) => { |
| 65 | - if (val) { | 72 | + if (showVal) { |
| 66 | // 初始化字段,添加 value 属性 | 73 | // 初始化字段,添加 value 属性 |
| 67 | - localFields.value = props.fields.map(field => ({ | 74 | + localFields.value = fieldsVal.map(field => ({ |
| 68 | ...field, | 75 | ...field, |
| 69 | - value: '' | 76 | + value: (props.initialValues && props.initialValues[field.id]) || '' |
| 70 | })) | 77 | })) |
| 71 | } | 78 | } |
| 72 | -}) | 79 | +}, { immediate: true, deep: true }) |
| 73 | 80 | ||
| 74 | /** | 81 | /** |
| 75 | * 更新弹窗显示状态 | 82 | * 更新弹窗显示状态 | ... | ... |
src/components/count/CheckinTargetList.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-12-16 11:44:27 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-12-16 13:53:45 | ||
| 5 | + * @FilePath: /mlaj/src/components/count/CheckinTargetList.vue | ||
| 6 | + * @Description: 打卡动态对象列表组件 | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <div class="mb-4"> | ||
| 10 | + <div class="flex justify-between items-center mb-2 mx-2"> | ||
| 11 | + <div class="flex items-center gap-2"> | ||
| 12 | + <div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}对象</div> | ||
| 13 | + <div class="text-xs text-gray-400 font-normal scale-90 origin-left">(长按可编辑/删除)</div> | ||
| 14 | + </div> | ||
| 15 | + <van-button size="small" type="primary" plain icon="plus" @click="onAdd" class="!h-7">添加</van-button> | ||
| 16 | + </div> | ||
| 17 | + | ||
| 18 | + <div class="bg-gray-50 rounded-lg p-2"> | ||
| 19 | + <div class="flex flex-wrap gap-2"> | ||
| 20 | + <template v-if="targetList.length > 0"> | ||
| 21 | + <div v-for="(item, index) in targetList" :key="index" | ||
| 22 | + class="px-4 py-1.5 rounded-full text-sm transition-colors duration-200 border cursor-pointer select-none relative" | ||
| 23 | + :style="selectedTargets.some(t => t.id === item.id) ? { | ||
| 24 | + backgroundColor: '#4caf50', | ||
| 25 | + color: '#ffffff', | ||
| 26 | + borderColor: '#4caf50' | ||
| 27 | + } : { | ||
| 28 | + backgroundColor: '#ffffff', | ||
| 29 | + color: '#4b5563', | ||
| 30 | + borderColor: '#e5e7eb' | ||
| 31 | + }" | ||
| 32 | + @click="onClick(item)" | ||
| 33 | + @touchstart="onTouchStart(item)" | ||
| 34 | + @touchend="onTouchEnd" | ||
| 35 | + @touchmove="onTouchMove" | ||
| 36 | + @mousedown="onMouseDown(item)" | ||
| 37 | + @mouseup="onMouseUp" | ||
| 38 | + @mouseleave="onMouseUp" | ||
| 39 | + > | ||
| 40 | + {{ item.name }} | ||
| 41 | + </div> | ||
| 42 | + </template> | ||
| 43 | + <div v-else class="w-full text-center py-4 text-gray-400 text-sm"> | ||
| 44 | + 暂无{{ dynamicFieldText }}对象,请点击上方添加按钮 | ||
| 45 | + </div> | ||
| 46 | + </div> | ||
| 47 | + </div> | ||
| 48 | + | ||
| 49 | + <!-- 操作菜单 --> | ||
| 50 | + <van-action-sheet | ||
| 51 | + v-model:show="showActionSheet" | ||
| 52 | + :actions="actions" | ||
| 53 | + cancel-text="取消" | ||
| 54 | + close-on-click-action | ||
| 55 | + @select="onSelectAction" | ||
| 56 | + /> | ||
| 57 | + </div> | ||
| 58 | +</template> | ||
| 59 | + | ||
| 60 | +<script setup> | ||
| 61 | +import { ref } from 'vue' | ||
| 62 | +import { showConfirmDialog } from 'vant' | ||
| 63 | + | ||
| 64 | +/** | ||
| 65 | + * 计数对象列表组件 | ||
| 66 | + * @description 展示可供选择的计数对象列表,支持选择和添加 | ||
| 67 | + */ | ||
| 68 | + | ||
| 69 | +const props = defineProps({ | ||
| 70 | + /** | ||
| 71 | + * 动态字段文本 (e.g. "感恩", "念佛") | ||
| 72 | + */ | ||
| 73 | + dynamicFieldText: { | ||
| 74 | + type: String, | ||
| 75 | + default: '计数' | ||
| 76 | + }, | ||
| 77 | + /** | ||
| 78 | + * 所有可用的对象列表 | ||
| 79 | + */ | ||
| 80 | + targetList: { | ||
| 81 | + type: Array, | ||
| 82 | + default: () => [] | ||
| 83 | + }, | ||
| 84 | + /** | ||
| 85 | + * 当前选中的对象列表 | ||
| 86 | + */ | ||
| 87 | + selectedTargets: { | ||
| 88 | + type: Array, | ||
| 89 | + default: () => [] | ||
| 90 | + } | ||
| 91 | +}) | ||
| 92 | + | ||
| 93 | +const emit = defineEmits(['add', 'toggle', 'edit', 'delete']) | ||
| 94 | + | ||
| 95 | +/** | ||
| 96 | + * 点击添加按钮 | ||
| 97 | + */ | ||
| 98 | +const onAdd = () => { | ||
| 99 | + emit('add') | ||
| 100 | +} | ||
| 101 | + | ||
| 102 | +// 长按相关逻辑 | ||
| 103 | +const longPressTimer = ref(null) | ||
| 104 | +const isLongPress = ref(false) | ||
| 105 | +const showActionSheet = ref(false) | ||
| 106 | +const currentItem = ref(null) | ||
| 107 | + | ||
| 108 | +const actions = [ | ||
| 109 | + { name: '编辑', color: '#1989fa', action: 'edit' }, | ||
| 110 | + { name: '删除', color: '#ee0a24', action: 'delete' } | ||
| 111 | +] | ||
| 112 | + | ||
| 113 | +const startLongPress = (item) => { | ||
| 114 | + isLongPress.value = false | ||
| 115 | + longPressTimer.value = setTimeout(() => { | ||
| 116 | + isLongPress.value = true | ||
| 117 | + currentItem.value = item | ||
| 118 | + showActionSheet.value = true | ||
| 119 | + // 震动反馈 (如果设备支持) | ||
| 120 | + if (navigator.vibrate) { | ||
| 121 | + navigator.vibrate(50) | ||
| 122 | + } | ||
| 123 | + }, 500) | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +const clearLongPress = () => { | ||
| 127 | + if (longPressTimer.value) { | ||
| 128 | + clearTimeout(longPressTimer.value) | ||
| 129 | + longPressTimer.value = null | ||
| 130 | + } | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +// Touch events | ||
| 134 | +const onTouchStart = (item) => { | ||
| 135 | + startLongPress(item) | ||
| 136 | +} | ||
| 137 | + | ||
| 138 | +const onTouchEnd = () => { | ||
| 139 | + clearLongPress() | ||
| 140 | +} | ||
| 141 | + | ||
| 142 | +const onTouchMove = () => { | ||
| 143 | + clearLongPress() | ||
| 144 | +} | ||
| 145 | + | ||
| 146 | +// Mouse events (for PC debugging) | ||
| 147 | +const onMouseDown = (item) => { | ||
| 148 | + startLongPress(item) | ||
| 149 | +} | ||
| 150 | + | ||
| 151 | +const onMouseUp = () => { | ||
| 152 | + clearLongPress() | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +/** | ||
| 156 | + * 点击项 | ||
| 157 | + * @param {Object} item | ||
| 158 | + */ | ||
| 159 | +const onClick = (item) => { | ||
| 160 | + // 如果是长按触发的结束,不执行点击 | ||
| 161 | + if (isLongPress.value) { | ||
| 162 | + // 重置状态 | ||
| 163 | + setTimeout(() => { | ||
| 164 | + isLongPress.value = false | ||
| 165 | + }, 0) | ||
| 166 | + return | ||
| 167 | + } | ||
| 168 | + emit('toggle', item) | ||
| 169 | +} | ||
| 170 | + | ||
| 171 | +/** | ||
| 172 | + * 选中操作 | ||
| 173 | + */ | ||
| 174 | +const onSelectAction = (action) => { | ||
| 175 | + if (action.action === 'edit') { | ||
| 176 | + emit('edit', currentItem.value) | ||
| 177 | + } else if (action.action === 'delete') { | ||
| 178 | + confirmDelete() | ||
| 179 | + } | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +// 删除相关 | ||
| 183 | +const confirmDelete = () => { | ||
| 184 | + if (!currentItem.value) return | ||
| 185 | + | ||
| 186 | + showConfirmDialog({ | ||
| 187 | + title: '确认删除', | ||
| 188 | + message: `确定要删除"${currentItem.value.name}"吗?`, | ||
| 189 | + }) | ||
| 190 | + .then(() => { | ||
| 191 | + emit('delete', currentItem.value) | ||
| 192 | + }) | ||
| 193 | + .catch(() => { | ||
| 194 | + // on cancel | ||
| 195 | + }) | ||
| 196 | +} | ||
| 197 | +</script> |
| ... | @@ -37,36 +37,16 @@ | ... | @@ -37,36 +37,16 @@ |
| 37 | </template> | 37 | </template> |
| 38 | </div> | 38 | </div> |
| 39 | <!-- 计数对象 --> | 39 | <!-- 计数对象 --> |
| 40 | - <div v-if="taskType === 'count' && selectedTaskValue && selectedTaskValue.length > 0" class="mb-4"> | 40 | + <CheckinTargetList |
| 41 | - <div class="flex justify-between items-center mb-2 mx-2"> | 41 | + v-if="taskType === 'count' && selectedTaskValue && selectedTaskValue.length > 0" |
| 42 | - <div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}对象</div> | 42 | + :dynamic-field-text="dynamicFieldText" |
| 43 | - <van-button size="small" type="primary" plain icon="plus" | 43 | + :target-list="targetList" |
| 44 | - @click="openAddTargetDialog" class="!h-7">添加</van-button> | 44 | + :selected-targets="selectedTargets" |
| 45 | - </div> | 45 | + @add="openAddTargetDialog" |
| 46 | - | 46 | + @toggle="toggleTarget" |
| 47 | - <div class="bg-gray-50 rounded-lg p-2"> | 47 | + @edit="handleTargetEdit" |
| 48 | - <div class="flex flex-wrap gap-2"> | 48 | + @delete="handleTargetDelete" |
| 49 | - <template v-if="targetList.length > 0"> | 49 | + /> |
| 50 | - <div v-for="(item, index) in targetList" :key="index" | ||
| 51 | - class="px-4 py-1.5 rounded-full text-sm transition-colors duration-200 border cursor-pointer select-none" | ||
| 52 | - :style="selectedTargets.some(t => t.name === item.name) ? { | ||
| 53 | - backgroundColor: '#4caf50', | ||
| 54 | - color: '#ffffff', | ||
| 55 | - borderColor: '#4caf50' | ||
| 56 | - } : { | ||
| 57 | - backgroundColor: '#ffffff', | ||
| 58 | - color: '#4b5563', | ||
| 59 | - borderColor: '#e5e7eb' | ||
| 60 | - }" @click="toggleTarget(item)"> | ||
| 61 | - {{ item.name }} | ||
| 62 | - </div> | ||
| 63 | - </template> | ||
| 64 | - <div v-else class="w-full text-center py-4 text-gray-400 text-sm"> | ||
| 65 | - 暂无{{ dynamicFieldText }}对象,请点击上方添加按钮 | ||
| 66 | - </div> | ||
| 67 | - </div> | ||
| 68 | - </div> | ||
| 69 | - </div> | ||
| 70 | 50 | ||
| 71 | <!-- 计数次数 --> | 51 | <!-- 计数次数 --> |
| 72 | <div v-if="taskType === 'count'" | 52 | <div v-if="taskType === 'count'" |
| ... | @@ -78,8 +58,9 @@ | ... | @@ -78,8 +58,9 @@ |
| 78 | <!-- 新增计数对象弹框 --> | 58 | <!-- 新增计数对象弹框 --> |
| 79 | <AddTargetDialog | 59 | <AddTargetDialog |
| 80 | v-model:show="showAddTargetDialog" | 60 | v-model:show="showAddTargetDialog" |
| 81 | - :title="`添加${dynamicFieldText}对象`" | 61 | + :title="editingTarget ? `编辑${dynamicFieldText}对象` : `添加${dynamicFieldText}对象`" |
| 82 | :fields="dynamicFormFields" | 62 | :fields="dynamicFormFields" |
| 63 | + :initial-values="editingTarget" | ||
| 83 | @confirm="confirmAddTarget" | 64 | @confirm="confirmAddTarget" |
| 84 | /> | 65 | /> |
| 85 | 66 | ||
| ... | @@ -195,11 +176,13 @@ import { ref, computed, onMounted, nextTick, reactive, watch } from 'vue' | ... | @@ -195,11 +176,13 @@ import { ref, computed, onMounted, nextTick, reactive, watch } from 'vue' |
| 195 | import { useRoute, useRouter } from 'vue-router' | 176 | import { useRoute, useRouter } from 'vue-router' |
| 196 | import { getTaskDetailAPI, getUploadTaskInfoAPI, getSubtaskListAPI } from "@/api/checkin" | 177 | import { getTaskDetailAPI, getUploadTaskInfoAPI, getSubtaskListAPI } from "@/api/checkin" |
| 197 | import { getTeacherFindSettingsAPI } from '@/api/teacher' | 178 | import { getTeacherFindSettingsAPI } from '@/api/teacher' |
| 179 | +import { getGratitudeListAPI, gratitudeAddAPI, gratitudeEditAPI, gratitudeDelAPI } from '@/api/gratitude' | ||
| 198 | import { useTitle } from '@vueuse/core' | 180 | import { useTitle } from '@vueuse/core' |
| 199 | import { useCheckin } from '@/composables/useCheckin' | 181 | import { useCheckin } from '@/composables/useCheckin' |
| 200 | import AudioPlayer from '@/components/ui/AudioPlayer.vue' | 182 | import AudioPlayer from '@/components/ui/AudioPlayer.vue' |
| 201 | import VideoPlayer from '@/components/ui/VideoPlayer.vue' | 183 | import VideoPlayer from '@/components/ui/VideoPlayer.vue' |
| 202 | import AddTargetDialog from '@/components/count/AddTargetDialog.vue' | 184 | import AddTargetDialog from '@/components/count/AddTargetDialog.vue' |
| 185 | +import CheckinTargetList from '@/components/count/CheckinTargetList.vue' | ||
| 203 | import { showToast, showLoadingToast } from 'vant' | 186 | import { showToast, showLoadingToast } from 'vant' |
| 204 | import dayjs from 'dayjs' | 187 | import dayjs from 'dayjs' |
| 205 | 188 | ||
| ... | @@ -242,32 +225,29 @@ const taskType = computed(() => route.query.task_type) | ... | @@ -242,32 +225,29 @@ const taskType = computed(() => route.query.task_type) |
| 242 | const showTaskPicker = ref(false) | 225 | const showTaskPicker = ref(false) |
| 243 | const taskOptions = ref([]) | 226 | const taskOptions = ref([]) |
| 244 | 227 | ||
| 245 | -// TODO: 模拟任务选项数据, 不同作业有不同的计数对象, 这里根据作业ID模拟不同的计数对象 | 228 | +const fetchTargetList = async (person_type) => { |
| 246 | -const mockData = { | 229 | + const { code, data } = await getGratitudeListAPI({ person_type }) |
| 247 | - 'task1': [ | 230 | + if (code) { |
| 248 | - { name: '张老师', city: '北京', school: '北京大学' }, | 231 | + targetList.value = data.gratitude_people || [] |
| 249 | - { name: '李老师', city: '上海', school: '复旦大学' } | 232 | + } |
| 250 | - ], | 233 | + |
| 251 | - 'task2': [ | 234 | + // TODO: 暂时mock数据 |
| 252 | - { name: '王老师', city: '广州', school: '中山大学' }, | 235 | + targetList.value = [{ |
| 253 | - { name: '赵老师', city: '深圳', school: '深圳大学' } | 236 | + id: '1', |
| 254 | - ], | 237 | + name: '张三', |
| 255 | - 'task3': [ | 238 | + city: '北京', |
| 256 | - { name: '孙老师', city: '杭州', school: '浙江大学' } | 239 | + unit: '公司' |
| 257 | - ] | 240 | + }, { |
| 241 | + id: '2', | ||
| 242 | + name: '李四', | ||
| 243 | + city: '上海', | ||
| 244 | + unit: '公司' | ||
| 245 | + }] | ||
| 258 | } | 246 | } |
| 259 | 247 | ||
| 260 | -const fetchTargetList = async (subTaskId) => { | 248 | +// 动态表单字段 (默认值,实际会根据选择的作业动态更新) |
| 261 | - // 模拟接口调用延迟 | 249 | +const dynamicFormFields = ref([]) |
| 262 | - setTimeout(() => { | 250 | +const personType = ref('') // 动态表单字段中的person_type |
| 263 | - targetList.value = mockData[subTaskId] || [] | ||
| 264 | - showLoadingToast({ | ||
| 265 | - message: '加载成功', | ||
| 266 | - type: 'success', | ||
| 267 | - duration: 1000 | ||
| 268 | - }) | ||
| 269 | - }, 500) | ||
| 270 | -} | ||
| 271 | 251 | ||
| 272 | // 确认作业选择 | 252 | // 确认作业选择 |
| 273 | const onConfirmTask = ({ selectedOptions }) => { | 253 | const onConfirmTask = ({ selectedOptions }) => { |
| ... | @@ -277,9 +257,44 @@ const onConfirmTask = ({ selectedOptions }) => { | ... | @@ -277,9 +257,44 @@ const onConfirmTask = ({ selectedOptions }) => { |
| 277 | isMakeup.value = !!option.is_makeup | 257 | isMakeup.value = !!option.is_makeup |
| 278 | showTaskPicker.value = false | 258 | showTaskPicker.value = false |
| 279 | 259 | ||
| 260 | + // 动态表单字段映射 | ||
| 261 | + if (option.field_list && Array.isArray(option.field_list)) { | ||
| 262 | + dynamicFormFields.value = option.field_list.map(field => ({ | ||
| 263 | + id: field.field_name, | ||
| 264 | + label: field.label, | ||
| 265 | + type: 'text', // 默认类型,如果后端有类型字段可替换 | ||
| 266 | + required: true // 默认必填,如果后端有必填字段可替换 | ||
| 267 | + })) | ||
| 268 | + // 确保如果有city字段,类型为textarea | ||
| 269 | + const cityField = dynamicFormFields.value.find(f => f.id === 'city') | ||
| 270 | + if (cityField) { | ||
| 271 | + cityField.type = 'textarea' | ||
| 272 | + } | ||
| 273 | + // 确保如果有unit字段,类型为textarea | ||
| 274 | + const unitField = dynamicFormFields.value.find(f => f.id === 'unit') | ||
| 275 | + if (unitField) { | ||
| 276 | + unitField.type = 'textarea' | ||
| 277 | + } | ||
| 278 | + } else { | ||
| 279 | + // 如果没有配置字段,使用默认字段 (兼容旧数据或Mock) | ||
| 280 | + dynamicFormFields.value = [ | ||
| 281 | + { id: 'name', label: '姓名', type: 'text', required: true }, | ||
| 282 | + { id: 'city', label: '城市', type: 'textarea', required: true }, | ||
| 283 | + { id: 'unit', label: '单位', type: 'textarea', required: true }, | ||
| 284 | + ] | ||
| 285 | + } | ||
| 286 | + | ||
| 287 | + // TODO: 暂时mock数据 | ||
| 288 | + dynamicFormFields.value = [ | ||
| 289 | + { id: 'name', label: '姓名', type: 'text', required: true }, | ||
| 290 | + { id: 'city', label: '城市', type: 'textarea', required: true }, | ||
| 291 | + { id: 'unit', label: '单位', type: 'textarea', required: true }, | ||
| 292 | + ] | ||
| 293 | + | ||
| 280 | // 如果是计数打卡,根据选中的作业ID查询计数对象 | 294 | // 如果是计数打卡,根据选中的作业ID查询计数对象 |
| 281 | if (taskType.value === 'count') { | 295 | if (taskType.value === 'count') { |
| 282 | - fetchTargetList(option.value) | 296 | + fetchTargetList(option.person_type) |
| 297 | + personType.value = option.person_type | ||
| 283 | } | 298 | } |
| 284 | } | 299 | } |
| 285 | 300 | ||
| ... | @@ -291,22 +306,13 @@ const onConfirmTask = ({ selectedOptions }) => { | ... | @@ -291,22 +306,13 @@ const onConfirmTask = ({ selectedOptions }) => { |
| 291 | // } | 306 | // } |
| 292 | // }) | 307 | // }) |
| 293 | 308 | ||
| 294 | -/********* TODO: *******/ | ||
| 295 | - | ||
| 296 | // 计数打卡相关逻辑 | 309 | // 计数打卡相关逻辑 |
| 297 | const countValue = ref(1) | 310 | const countValue = ref(1) |
| 298 | const selectedTargets = ref([]) | 311 | const selectedTargets = ref([]) |
| 299 | // Mock 老师数据 | 312 | // Mock 老师数据 |
| 300 | const targetList = ref([]) | 313 | const targetList = ref([]) |
| 301 | const showAddTargetDialog = ref(false) | 314 | const showAddTargetDialog = ref(false) |
| 302 | - | 315 | +const editingTarget = ref(null) |
| 303 | -// TODO: 动态表单字段 Mock 数据 | ||
| 304 | -const dynamicFormFields = ref([ | ||
| 305 | - { id: 'name', label: '姓名', type: 'text', required: true }, | ||
| 306 | - { id: 'city', label: '城市', type: 'text', required: true }, | ||
| 307 | - { id: 'school', label: '单位', type: 'text', required: true }, | ||
| 308 | - { id: 'remark', label: '备注备注备注', type: 'textarea', required: true } // 新增字段 | ||
| 309 | -]) | ||
| 310 | 316 | ||
| 311 | const toggleTarget = (item) => { | 317 | const toggleTarget = (item) => { |
| 312 | const index = selectedTargets.value.findIndex(t => t.name === item.name) | 318 | const index = selectedTargets.value.findIndex(t => t.name === item.name) |
| ... | @@ -318,25 +324,84 @@ const toggleTarget = (item) => { | ... | @@ -318,25 +324,84 @@ const toggleTarget = (item) => { |
| 318 | } | 324 | } |
| 319 | 325 | ||
| 320 | const openAddTargetDialog = () => { | 326 | const openAddTargetDialog = () => { |
| 327 | + editingTarget.value = null; // 重置编辑对象 | ||
| 321 | showAddTargetDialog.value = true; | 328 | showAddTargetDialog.value = true; |
| 322 | } | 329 | } |
| 323 | 330 | ||
| 324 | /** | 331 | /** |
| 325 | - * 确认添加对象 | 332 | + * 确认添加/编辑对象 |
| 326 | * @param {Object} formData - 表单数据 | 333 | * @param {Object} formData - 表单数据 |
| 327 | */ | 334 | */ |
| 328 | -const confirmAddTarget = (formData) => { | 335 | +const confirmAddTarget = async (formData) => { |
| 329 | - console.log(`新增${dynamicFieldText.value}对象信息:`, formData) | 336 | + console.log(`${editingTarget.value ? '编辑' : '新增'}${dynamicFieldText.value}对象信息:`, formData) |
| 330 | 337 | ||
| 331 | - // TODO: 这里根据实际情况调整动态字段的处理逻辑, 暂时没有正式数据, 要等一等. | 338 | + if (editingTarget.value) { |
| 339 | + // 编辑模式 | ||
| 340 | + const index = targetList.value.findIndex(t => t === editingTarget.value) | ||
| 341 | + if (index > -1) { | ||
| 342 | + const { code } = await gratitudeEditAPI({ ...editingTarget.value }) | ||
| 343 | + if (code) { | ||
| 344 | + // 更新对象 | ||
| 345 | + targetList.value[index] = { ...targetList.value[index], ...formData } | ||
| 332 | 346 | ||
| 333 | - // 添加到列表(适配原有的数据结构) | 347 | + // 如果在选中列表中,也需要更新 |
| 348 | + const selectedIndex = selectedTargets.value.findIndex(t => t.id === editingTarget.value.id) | ||
| 349 | + if (selectedIndex > -1) { | ||
| 350 | + selectedTargets.value[selectedIndex] = { ...selectedTargets.value[selectedIndex], ...formData } | ||
| 351 | + } | ||
| 352 | + } | ||
| 353 | + } | ||
| 354 | + showToast('修改成功') | ||
| 355 | + } else { | ||
| 356 | + // 新增模式 | ||
| 357 | + try { | ||
| 358 | + const res = await gratitudeAddAPI({ | ||
| 359 | + ...formData, | ||
| 360 | + person_type: personType.value | ||
| 361 | + }) | ||
| 362 | + if (res.code) { | ||
| 363 | + // 新增成功,更新本地列表 | ||
| 334 | targetList.value.push({ | 364 | targetList.value.push({ |
| 335 | - name: formData.name, | 365 | + ...formData, |
| 336 | - city: formData.city, | ||
| 337 | - school: formData.school | ||
| 338 | - // 其他字段... | ||
| 339 | }) | 366 | }) |
| 367 | + showToast('新增成功') | ||
| 368 | + } | ||
| 369 | + } catch (error) { | ||
| 370 | + showToast(`新增失败:${error.message || '未知错误'}`) | ||
| 371 | + } | ||
| 372 | + } | ||
| 373 | + | ||
| 374 | + showAddTargetDialog.value = false; | ||
| 375 | +} | ||
| 376 | + | ||
| 377 | +/** | ||
| 378 | + * 处理对象编辑 | ||
| 379 | + */ | ||
| 380 | +const handleTargetEdit = (item) => { | ||
| 381 | + editingTarget.value = item | ||
| 382 | + showAddTargetDialog.value = true | ||
| 383 | +} | ||
| 384 | + | ||
| 385 | +/** | ||
| 386 | + * 处理对象删除 | ||
| 387 | + */ | ||
| 388 | +const handleTargetDelete = async (item) => { | ||
| 389 | + const { code } = await gratitudeDeleteAPI({ id: item.id }) | ||
| 390 | + if (code) { | ||
| 391 | + // 删除成功,更新本地列表 | ||
| 392 | + const targetIndex = targetList.value.findIndex(t => t.id === item.id) | ||
| 393 | + if (targetIndex > -1) { | ||
| 394 | + targetList.value.splice(targetIndex, 1) | ||
| 395 | + } | ||
| 396 | + | ||
| 397 | + // 从选中列表中也删除 | ||
| 398 | + const selectedIndex = selectedTargets.value.findIndex(t => t.id === item.id) | ||
| 399 | + if (selectedIndex > -1) { | ||
| 400 | + selectedTargets.value.splice(selectedIndex, 1) | ||
| 401 | + } | ||
| 402 | + | ||
| 403 | + showToast('删除成功') | ||
| 404 | + } | ||
| 340 | } | 405 | } |
| 341 | 406 | ||
| 342 | /** | 407 | /** |
| ... | @@ -388,7 +453,7 @@ const handleSubmit = async () => { | ... | @@ -388,7 +453,7 @@ const handleSubmit = async () => { |
| 388 | } | 453 | } |
| 389 | 454 | ||
| 390 | // 传递额外数据 | 455 | // 传递额外数据 |
| 391 | - extraData.targets = selectedTargets.value.map(t => t.name) // 假设传 name | 456 | + extraData.targets = selectedTargets.value.map(t => t.id) |
| 392 | extraData.count = countValue.value | 457 | extraData.count = countValue.value |
| 393 | } | 458 | } |
| 394 | 459 | ||
| ... | @@ -825,6 +890,8 @@ onMounted(async () => { | ... | @@ -825,6 +890,8 @@ onMounted(async () => { |
| 825 | text: item.is_makeup ? '补卡:' + item.title : item.title, | 890 | text: item.is_makeup ? '补卡:' + item.title : item.title, |
| 826 | value: item.id, | 891 | value: item.id, |
| 827 | is_makeup: item.is_makeup, // 是否为补录 | 892 | is_makeup: item.is_makeup, // 是否为补录 |
| 893 | + field_list: item.field_list || [], // 动态字段列表 | ||
| 894 | + person_type: item.person_type || '', // 打卡对象类型 | ||
| 828 | })) | 895 | })) |
| 829 | ] | 896 | ] |
| 830 | } | 897 | } |
| ... | @@ -839,7 +906,8 @@ onMounted(async () => { | ... | @@ -839,7 +906,8 @@ onMounted(async () => { |
| 839 | 906 | ||
| 840 | // 如果是计数打卡,根据选中的作业ID查询计数对象 | 907 | // 如果是计数打卡,根据选中的作业ID查询计数对象 |
| 841 | if (taskType.value === 'count') { | 908 | if (taskType.value === 'count') { |
| 842 | - fetchTargetList(option.value) | 909 | + fetchTargetList(option.person_type) |
| 910 | + personType.value = option.person_type | ||
| 843 | } | 911 | } |
| 844 | } | 912 | } |
| 845 | 913 | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-05-29 15:34:17 | 2 | * @Date: 2025-05-29 15:34:17 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-15 14:46:49 | 4 | + * @LastEditTime: 2025-12-16 13:54:46 |
| 5 | * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue | 5 | * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -398,6 +398,7 @@ const goToCheckinDetailPage = () => { | ... | @@ -398,6 +398,7 @@ const goToCheckinDetailPage = () => { |
| 398 | path: '/checkin/detail', | 398 | path: '/checkin/detail', |
| 399 | query: { | 399 | query: { |
| 400 | post_id: route.query.id, | 400 | post_id: route.query.id, |
| 401 | + task_id: route.query.id, | ||
| 401 | subtask_id: selectedSubtaskId.value, | 402 | subtask_id: selectedSubtaskId.value, |
| 402 | date: current_date, | 403 | date: current_date, |
| 403 | is_patch: isPatchCheckin.value ? '1' : '0', | 404 | is_patch: isPatchCheckin.value ? '1' : '0', | ... | ... |
-
Please register or login to post a comment