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>
This diff is collapsed. Click to expand it.
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',
......