hookehuyr

feat(打卡): 添加感恩对象管理功能

- 新增CheckinTargetList组件用于展示和管理感恩对象
- 实现感恩对象的添加、编辑、删除和选择功能
- 在打卡详情页集成感恩对象管理组件
- 更新API接口支持感恩对象相关操作
- 优化动态表单字段处理逻辑
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))
......
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 * 更新弹窗显示状态
......
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',
......