hookehuyr

refactor(checkin): 移除独立的媒体打卡页面以简化路由结构

移除图片、视频、音频和文本的独立上传页面,将相关路由从路由配置中删除
清理首页中已注释的导航函数,减少代码冗余
删除对应的Vue组件文件,统一打卡功能入口
1 /* 1 /*
2 * @Date: 2025-03-21 13:28:30 2 * @Date: 2025-03-21 13:28:30
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-11-17 13:54:31 4 + * @LastEditTime: 2026-01-24 15:29:54
5 * @FilePath: /mlaj/src/router/checkin.js 5 * @FilePath: /mlaj/src/router/checkin.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -61,42 +61,6 @@ export default [ ...@@ -61,42 +61,6 @@ export default [
61 } 61 }
62 }, 62 },
63 { 63 {
64 - path: '/checkin/image',
65 - name: 'ImageCheckIn',
66 - component: () => import('@/views/checkin/upload/image.vue'),
67 - meta: {
68 - title: '打卡图片',
69 - requiresAuth: true
70 - }
71 - },
72 - {
73 - path: '/checkin/video',
74 - name: 'VideoCheckIn',
75 - component: () => import('@root/src/views/checkin/upload/video.vue'),
76 - meta: {
77 - title: '打卡视频',
78 - requiresAuth: true
79 - }
80 - },
81 - {
82 - path: '/checkin/audio',
83 - name: 'AudioCheckIn',
84 - component: () => import('@root/src/views/checkin/upload/audio.vue'),
85 - meta: {
86 - title: '打卡音频',
87 - requiresAuth: true
88 - }
89 - },
90 - {
91 - path: '/checkin/text',
92 - name: 'TextCheckIn',
93 - component: () => import('@root/src/views/checkin/upload/text.vue'),
94 - meta: {
95 - title: '打卡文本',
96 - requiresAuth: true
97 - }
98 - },
99 - {
100 path: '/checkin/join', 64 path: '/checkin/join',
101 name: 'JoinCheckIn', 65 name: 'JoinCheckIn',
102 component: () => import('@root/src/views/checkin/JoinCheckInPage.vue'), 66 component: () => import('@root/src/views/checkin/JoinCheckInPage.vue'),
......
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: 2026-01-23 10:46:15 4 + * @LastEditTime: 2026-01-24 15:29:28
5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue 5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
6 * @Description: 用户打卡主页 6 * @Description: 用户打卡主页
7 --> 7 -->
...@@ -460,45 +460,6 @@ const goToCheckinDetailPage = () => { ...@@ -460,45 +460,6 @@ const goToCheckinDetailPage = () => {
460 }) 460 })
461 } 461 }
462 462
463 -// const goToCheckinTextPage = () => {
464 -// router.push({
465 -// path: '/checkin/text',
466 -// query: {
467 -// id: route.query.id,
468 -// type: 'text'
469 -// }
470 -// })
471 -// }
472 -
473 -// const goToCheckinImagePage = () => {
474 -// router.push({
475 -// path: '/checkin/image',
476 -// query: {
477 -// id: route.query.id,
478 -// type: 'image'
479 -// }
480 -// })
481 -// }
482 -// const goToCheckinVideoPage = (type) => {
483 -// router.push({
484 -// path: '/checkin/video',
485 -// query: {
486 -// id: route.query.id,
487 -// type: 'video',
488 -// }
489 -// })
490 -// }
491 -
492 -// const goToCheckinAudioPage = (type) => {
493 -// router.push({
494 -// path: '/checkin/audio',
495 -// query: {
496 -// id: route.query.id,
497 -// type: 'audio',
498 -// }
499 -// })
500 -// }
501 -
502 const handLike = async (post) => { 463 const handLike = async (post) => {
503 if (!post.is_liked) { 464 if (!post.is_liked) {
504 const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, }) 465 const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, })
......
1 -<!--
2 - * @Date: 2025-06-03 09:41:41
3 - * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-06-13 14:34:57
5 - * @FilePath: /mlaj/src/views/checkin/upload/audio.vue
6 - * @Description: 音视频文件上传组件
7 --->
8 -<template>
9 - <div class="checkin-upload-file p-4">
10 - <div class="title text-center pb-4 font-bold">{{route.meta.title}}</div>
11 - <!-- 文件上传区域 -->
12 - <div class="mb-4">
13 - <van-uploader
14 - v-model="fileList"
15 - :max-count="max_count"
16 - :max-size="20 * 1024 * 1024"
17 - :before-read="beforeRead"
18 - :after-read="afterRead"
19 - @delete="onDelete"
20 - multiple
21 - accept=".mp3,.wav,.aac"
22 - result-type="file"
23 - upload-icon="plus"
24 - :deletable="false"
25 - >
26 - </van-uploader>
27 - <van-row v-for="(item, index) in fileList" :key="index" class="mt-2 text-s text-gray-500">
28 - <van-col span="22">{{ item.name }}</van-col>
29 - <van-col span="2" @click="delItem(item)"><van-icon name="clear" /></van-col>
30 - </van-row>
31 - <van-divider />
32 - <div class="mt-2 text-xs text-gray-500">最多上传{{ max_count }}个文件,每个不超过20M</div>
33 - <div class="mt-2 text-xs text-gray-500">上传类型:&nbsp;.mp3,.wav,.aac 格式音频文件</div>
34 - </div>
35 -
36 - <!-- 文字留言区域 -->
37 - <div class="mb-4 border">
38 - <van-field
39 - v-model="message"
40 - rows="4"
41 - autosize
42 - type="textarea"
43 - placeholder="请输入打卡留言"
44 - />
45 - </div>
46 -
47 - <!-- 提交按钮 -->
48 - <div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
49 - <van-button
50 - type="primary"
51 - block
52 - :loading="uploading"
53 - :disabled="!canSubmit"
54 - @click="onSubmit"
55 - >
56 - 提交
57 - </van-button>
58 - </div>
59 -
60 - <!-- 上传加载遮罩 -->
61 - <van-overlay :show="loading">
62 - <div class="wrapper" @click.stop>
63 - <van-loading vertical color="#FFFFFF">上传中...</van-loading>
64 - </div>
65 - </van-overlay>
66 - </div>
67 -</template>
68 -
69 -<script setup>
70 -import { ref, computed } from 'vue'
71 -import { useRoute, useRouter } from 'vue-router'
72 -import { showToast, showLoadingToast } from 'vant'
73 -import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
74 -import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
75 -import { qiniuFileHash } from '@/utils/qiniuFileHash';
76 -import _ from 'lodash'
77 -import { useTitle } from '@vueuse/core';
78 -import { useAuth } from '@/contexts/auth'
79 -
80 -const route = useRoute()
81 -const router = useRouter()
82 -const { currentUser } = useAuth()
83 -useTitle(route.meta.title);
84 -
85 -const max_count = ref(5);
86 -
87 -// 文件列表
88 -const fileList = ref([])
89 -// 留言内容
90 -const message = ref('')
91 -// 上传状态
92 -const uploading = ref(false)
93 -// 上传loading
94 -const loading = ref(false)
95 -
96 -// 是否可以提交
97 -const canSubmit = computed(() => {
98 - return fileList.value.length > 0 && message.value.trim() !== ''
99 -})
100 -
101 -// 文件校验
102 -const beforeRead = (file) => {
103 - let flag = true
104 -
105 - if (Array.isArray(file)) {
106 - // 多个文件
107 - const invalidTypes = file.filter(item => {
108 - const fileType = item.type.toLowerCase();
109 - return !fileType.startsWith('audio/');
110 - })
111 - if (invalidTypes.length) {
112 - flag = false
113 - showToast('请上传音频文件')
114 - }
115 - if (fileList.value.length + file.length > max_count.value) {
116 - flag = false
117 - showToast(`最大上传数量为${max_count.value}个`)
118 - }
119 - } else {
120 - const fileType = file.type.toLowerCase();
121 - if (!fileType.startsWith('audio/')) {
122 - showToast('请上传音频文件')
123 - flag = false
124 - }
125 - if (fileList.value.length + 1 > max_count.value) {
126 - flag = false
127 - showToast(`最大上传数量为${max_count.value}个`)
128 - }
129 - if ((file.size / 1024 / 1024).toFixed(2) > 20) {
130 - flag = false
131 - showToast('最大文件体积为20MB')
132 - }
133 - }
134 - return flag
135 -}
136 -
137 -/**
138 - * 获取文件哈希(与七牛云ETag一致)
139 - * @param {File} file 文件对象
140 - * @returns {Promise<string>} 哈希字符串
141 - * 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。
142 - */
143 -const getFileMD5 = async (file) => {
144 - return await qiniuFileHash(file)
145 -}
146 -
147 -// 上传到七牛云
148 -const uploadToQiniu = async (file, token, fileName) => {
149 - const formData = new FormData()
150 - formData.append('file', file)
151 - formData.append('token', token)
152 - formData.append('key', fileName)
153 -
154 - const config = {
155 - headers: { 'Content-Type': 'multipart/form-data' }
156 - }
157 -
158 - // 根据协议选择上传地址
159 - const qiniuUploadUrl = window.location.protocol === 'https:'
160 - ? 'https://up.qbox.me'
161 - : 'http://upload.qiniu.com'
162 -
163 - return await qiniuUploadAPI(qiniuUploadUrl, formData, config)
164 -}
165 -
166 -// 处理单个文件上传
167 -const handleUpload = async (file) => {
168 - loading.value = true
169 - try {
170 - // 获取MD5值
171 - const md5 = await getFileMD5(file.file)
172 -
173 - // 获取七牛token
174 - const tokenResult = await qiniuTokenAPI({
175 - name: file.file.name,
176 - hash: md5
177 - })
178 -
179 - // 文件已存在,直接返回
180 - if (tokenResult.data) {
181 - return tokenResult.data
182 - }
183 -
184 - // 新文件上传
185 - if (tokenResult.token) {
186 - const suffix = /.[^.]+$/.exec(file.file.name) || ''
187 - const fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/file/${md5}${suffix}`
188 -
189 - const { filekey } = await uploadToQiniu(
190 - file.file,
191 - tokenResult.token,
192 - fileName
193 - )
194 -
195 - if (filekey) {
196 - // 保存文件信息
197 - const { data } = await saveFileAPI({
198 - name: file.file.name,
199 - filekey,
200 - hash: md5
201 - })
202 - return data
203 - }
204 - }
205 - return null
206 - } catch (error) {
207 - console.error('Upload error:', error)
208 - return null
209 - } finally {
210 - loading.value = false
211 - }
212 -}
213 -
214 -// 文件读取后的处理
215 -const afterRead = async (file) => {
216 - if (Array.isArray(file)) {
217 - // 多文件上传
218 - for (const item of file) {
219 - item.status = 'uploading'
220 - item.message = '上传中...'
221 - const result = await handleUpload(item)
222 - if (result) {
223 - item.status = 'done'
224 - item.message = '上传成功'
225 - item.url = result.url
226 - item.meta_id = result.meta_id
227 - item.name = result.name
228 - } else {
229 - item.status = 'failed'
230 - item.message = '上传失败'
231 - showToast('上传失败,请重试')
232 - }
233 - }
234 - } else {
235 - // 单文件上传
236 - file.status = 'uploading'
237 - file.message = '上传中...'
238 - const result = await handleUpload(file)
239 - if (result) {
240 - file.status = 'done'
241 - file.message = '上传成功'
242 - file.url = result.url
243 - file.meta_id = result.meta_id
244 - file.name = result.name
245 - } else {
246 - file.status = 'failed'
247 - file.message = '上传失败'
248 - showToast('上传失败,请重试')
249 - }
250 - }
251 -}
252 -
253 -// 删除文件
254 -const onDelete = (file) => {
255 - const index = fileList.value.indexOf(file)
256 - if (index !== -1) {
257 - fileList.value.splice(index, 1)
258 - }
259 -}
260 -
261 -// 提交表单
262 -const onSubmit = async () => {
263 - if (uploading.value) return
264 -
265 - // 检查是否所有文件都上传完成
266 - const hasUploadingFiles = fileList.value.some(file => file.status === 'uploading')
267 - if (hasUploadingFiles) {
268 - showToast('请等待所有文件上传完成')
269 - return
270 - }
271 -
272 - uploading.value = true
273 - const toast = showLoadingToast({
274 - message: '提交中...',
275 - forbidClick: true,
276 - })
277 -
278 - try {
279 - if (route.query.status === 'edit') {
280 - // 编辑打卡接口
281 - const { code, data } = await editUploadTaskInfoAPI({
282 - i: route.query.post_id,
283 - note: message.value,
284 - meta_id: fileList.value.map(item => item.meta_id),
285 - file_type: route.query.type,
286 - });
287 - if (code === 1) {
288 - showToast('提交成功')
289 - router.back()
290 - }
291 - } else {
292 - // 新增打卡接口
293 - const { code, data } = await addUploadTaskAPI({
294 - task_id: route.query.id,
295 - note: message.value,
296 - meta_id: fileList.value.map(item => item.meta_id),
297 - file_type: route.query.type,
298 - });
299 - if (code === 1) {
300 - showToast('提交成功')
301 - router.back()
302 - }
303 - }
304 - } catch (error) {
305 - showToast('提交失败,请重试')
306 - } finally {
307 - toast.close()
308 - uploading.value = false
309 - }
310 -}
311 -
312 -onMounted(async () => {
313 - if (route.query.status === 'edit') {
314 - const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
315 - if (code === 1) {
316 - fileList.value = data.files.map(item => ({
317 - name: item.name,
318 - url: item.value,
319 - status: 'done',
320 - message: '上传成功',
321 - meta_id: item.meta_id,
322 - }))
323 - message.value = data.note
324 - }
325 - }
326 -});
327 -
328 -const delItem = (item) => {
329 - const index = fileList.value.indexOf(item)
330 - if (index!== -1) {
331 - fileList.value.splice(index, 1)
332 - }
333 -}
334 -</script>
335 -
336 -<style lang="less" scoped>
337 -.checkin-upload-file {
338 - min-height: 100vh;
339 - padding-bottom: 80px;
340 -}
341 -
342 -.wrapper {
343 - display: flex;
344 - align-items: center;
345 - justify-content: center;
346 - height: 100%;
347 -}
348 -
349 -.preview-cover {
350 - position: absolute;
351 - bottom: 0;
352 - box-sizing: border-box;
353 - width: 100%;
354 - padding: 4px;
355 - color: #fff;
356 - font-size: 12px;
357 - text-align: center;
358 - background: rgba(0, 0, 0, 0.3);
359 - }
360 -</style>
1 -<template>
2 - <div class="checkin-upload-image p-4">
3 - <div class="title text-center pb-4 font-bold">{{route.meta.title}}</div>
4 - <!-- 图片上传区域 -->
5 - <div class="mb-4">
6 - <van-uploader
7 - v-model="fileList"
8 - :max-count="max_count"
9 - :max-size="20 * 1024 * 1024"
10 - :before-read="beforeRead"
11 - :after-read="afterRead"
12 - @delete="onDelete"
13 - multiple
14 - accept="image/*"
15 - result-type="file"
16 - >
17 - </van-uploader>
18 - <div class="mt-2 text-xs text-gray-500">最多上传{{ max_count }}张图片,每张不超过20M</div>
19 - <div class="mt-2 text-xs text-gray-500">上传类型:&nbsp;{{ type_text }}</div>
20 - </div>
21 -
22 - <!-- 文字留言区域 -->
23 - <div class="mb-4 border">
24 - <van-field
25 - v-model="message"
26 - rows="4"
27 - autosize
28 - type="textarea"
29 - placeholder="请输入打卡留言"
30 - />
31 - </div>
32 -
33 - <!-- 提交按钮 -->
34 - <div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
35 - <van-button
36 - type="primary"
37 - block
38 - :loading="uploading"
39 - :disabled="!canSubmit"
40 - @click="onSubmit"
41 - >
42 - 提交
43 - </van-button>
44 - </div>
45 -
46 - <!-- 上传加载遮罩 -->
47 - <van-overlay :show="loading">
48 - <div class="wrapper" @click.stop>
49 - <van-loading vertical color="#FFFFFF">上传中...</van-loading>
50 - </div>
51 - </van-overlay>
52 - </div>
53 -</template>
54 -
55 -<script setup>
56 -import { ref, computed } from 'vue'
57 -import { useRoute, useRouter } from 'vue-router'
58 -import { showToast, showLoadingToast } from 'vant'
59 -import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
60 -import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
61 -import { qiniuFileHash } from '@/utils/qiniuFileHash';
62 -import _ from 'lodash'
63 -import { useTitle } from '@vueuse/core';
64 -import { useAuth } from '@/contexts/auth'
65 -
66 -const route = useRoute()
67 -const router = useRouter()
68 -const { currentUser } = useAuth()
69 -useTitle(route.meta.title);
70 -
71 -const max_count = ref(5);
72 -
73 -// 文件列表
74 -const fileList = ref([])
75 -// 留言内容
76 -const message = ref('')
77 -// 上传状态
78 -const uploading = ref(false)
79 -// 上传loading
80 -const loading = ref(false)
81 -
82 -// 是否可以提交
83 -const canSubmit = computed(() => {
84 - return fileList.value.length > 0 && message.value.trim() !== ''
85 -})
86 -
87 -// 固定类型限制
88 -const imageTypes = "jpg/jpeg/png";
89 -
90 -// 文件类型中文页面显示
91 -const type_text = computed(() => {
92 - // return props.item.component_props.image_type;
93 - return imageTypes;
94 -});
95 -
96 -// 文件校验
97 -const beforeRead = (file) => {
98 - const image_types = _.map(imageTypes.split("/"), (item) => `image/${item}`);
99 - let flag = true
100 -
101 - if (Array.isArray(file)) {
102 - // 多张图片
103 - const invalidTypes = file.filter(item => {
104 - const fileType = item.type.toLowerCase();
105 - return !image_types.some(type => fileType.includes(type.split('/')[1]));
106 - })
107 - if (invalidTypes.length) {
108 - flag = false
109 - showToast('请上传指定格式图片')
110 - }
111 - if (fileList.value.length + file.length > max_count.value) {
112 - flag = false
113 - showToast(`最大上传数量为${max_count.value}张`)
114 - }
115 - } else {
116 - const fileType = file.type.toLowerCase();
117 - if (!image_types.some(type => fileType.includes(type.split('/')[1]))) {
118 - showToast('请上传指定格式图片')
119 - flag = false
120 - }
121 - if (fileList.value.length + 1 > max_count.value) {
122 - flag = false
123 - showToast(`最大上传数量为${max_count.value}张`)
124 - }
125 - if ((file.size / 1024 / 1024).toFixed(2) > 20) {
126 - flag = false
127 - showToast('最大文件体积为20MB')
128 - }
129 - }
130 - return flag
131 -}
132 -
133 -/**
134 - * 获取文件哈希(与七牛云ETag一致)
135 - * @param {File} file 文件对象
136 - * @returns {Promise<string>} 哈希字符串
137 - * 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。
138 - */
139 -const getFileMD5 = async (file) => {
140 - return await qiniuFileHash(file)
141 -}
142 -
143 -// 上传到七牛云
144 -const uploadToQiniu = async (file, token, fileName) => {
145 - const formData = new FormData()
146 - formData.append('file', file)
147 - formData.append('token', token)
148 - formData.append('key', fileName)
149 -
150 - const config = {
151 - headers: { 'Content-Type': 'multipart/form-data' }
152 - }
153 -
154 - // 根据协议选择上传地址
155 - const qiniuUploadUrl = window.location.protocol === 'https:'
156 - ? 'https://up.qbox.me'
157 - : 'http://upload.qiniu.com'
158 -
159 - return await qiniuUploadAPI(qiniuUploadUrl, formData, config)
160 -}
161 -
162 -// 处理单个文件上传
163 -const handleUpload = async (file) => {
164 - loading.value = true
165 - try {
166 - // 获取MD5值
167 - const md5 = await getFileMD5(file.file)
168 -
169 - // 获取七牛token
170 - const tokenResult = await qiniuTokenAPI({
171 - name: file.file.name,
172 - hash: md5
173 - })
174 -
175 - // 文件已存在,直接返回
176 - if (tokenResult.data) {
177 - return tokenResult.data
178 - }
179 -
180 - // 新文件上传
181 - if (tokenResult.token) {
182 - const suffix = /.[^.]+$/.exec(file.file.name) || ''
183 - const fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/img/${md5}${suffix}`
184 -
185 - const { filekey, image_info } = await uploadToQiniu(
186 - file.file,
187 - tokenResult.token,
188 - fileName
189 - )
190 -
191 - if (filekey) {
192 - // 保存文件信息
193 - const { data } = await saveFileAPI({
194 - name: file.file.name,
195 - filekey,
196 - hash: md5,
197 - height: image_info?.height,
198 - width: image_info?.width
199 - })
200 - return data
201 - }
202 - }
203 - return null
204 - } catch (error) {
205 - console.error('Upload error:', error)
206 - return null
207 - } finally {
208 - loading.value = false
209 - }
210 -}
211 -
212 -// 文件读取后的处理
213 -const afterRead = async (file) => {
214 - if (Array.isArray(file)) {
215 - // 多文件上传
216 - for (const item of file) {
217 - item.status = 'uploading'
218 - item.message = '上传中...'
219 - const result = await handleUpload(item)
220 - if (result) {
221 - item.status = 'done'
222 - item.message = '上传成功'
223 - item.url = result.url
224 - item.meta_id = result.meta_id
225 - } else {
226 - item.status = 'failed'
227 - item.message = '上传失败'
228 - showToast('上传失败,请重试')
229 - }
230 - }
231 - } else {
232 - // 单文件上传
233 - file.status = 'uploading'
234 - file.message = '上传中...'
235 - const result = await handleUpload(file)
236 - if (result) {
237 - file.status = 'done'
238 - file.message = '上传成功'
239 - file.url = result.url
240 - file.meta_id = result.meta_id
241 - } else {
242 - file.status = 'failed'
243 - file.message = '上传失败'
244 - showToast('上传失败,请重试')
245 - }
246 - }
247 -}
248 -
249 -// 删除文件
250 -const onDelete = (file) => {
251 - const index = fileList.value.indexOf(file)
252 - if (index !== -1) {
253 - fileList.value.splice(index, 1)
254 - }
255 -}
256 -
257 -// 提交表单
258 -const onSubmit = async () => {
259 - if (uploading.value) return
260 -
261 - // 检查是否所有文件都上传完成
262 - const hasUploadingFiles = fileList.value.some(file => file.status === 'uploading')
263 - if (hasUploadingFiles) {
264 - showToast('请等待所有文件上传完成')
265 - return
266 - }
267 -
268 - uploading.value = true
269 - const toast = showLoadingToast({
270 - message: '提交中...',
271 - forbidClick: true,
272 - })
273 -
274 - try {
275 - if (route.query.status === 'edit') {
276 - // 编辑打卡接口
277 - const { code, data } = await editUploadTaskInfoAPI({
278 - i: route.query.post_id,
279 - note: message.value,
280 - meta_id: fileList.value.map(item => item.meta_id),
281 - file_type: route.query.type,
282 - });
283 - if (code === 1) {
284 - showToast('提交成功')
285 - router.back()
286 - }
287 - } else {
288 - // 新增打卡接口
289 - const { code, data } = await addUploadTaskAPI({
290 - task_id: route.query.id,
291 - note: message.value,
292 - meta_id: fileList.value.map(item => item.meta_id),
293 - file_type: route.query.type,
294 - });
295 - if (code === 1) {
296 - showToast('提交成功')
297 - router.back()
298 - }
299 - }
300 - } catch (error) {
301 - showToast('提交失败,请重试')
302 - } finally {
303 - toast.close()
304 - uploading.value = false
305 - }
306 -}
307 -
308 -onMounted(async () => {
309 - if (route.query.status === 'edit') {
310 - const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
311 - if (code === 1) {
312 - fileList.value = data.files.map(item => ({
313 - url: item.value,
314 - status: 'done',
315 - message: '上传成功',
316 - meta_id: item.meta_id,
317 - }))
318 - message.value = data.note
319 - }
320 - }
321 -})
322 -</script>
323 -
324 -<style lang="less" scoped>
325 -.checkin-upload-image {
326 - min-height: 100vh;
327 - padding-bottom: 80px;
328 -}
329 -
330 -.wrapper {
331 - display: flex;
332 - align-items: center;
333 - justify-content: center;
334 - height: 100%;
335 -}
336 -</style>
1 -<template>
2 - <div class="checkin-upload-text p-4">
3 - <div class="title text-center pb-4 font-bold">{{ route.meta.title }}</div>
4 -
5 - <!-- 文字输入区域 -->
6 - <div class="mb-4 border">
7 - <van-field
8 - v-model="message"
9 - rows="8"
10 - autosize
11 - type="textarea"
12 - placeholder="请输入打卡内容, 至少需要10个字符"
13 - maxlength="500"
14 - show-word-limit
15 - />
16 - </div>
17 -
18 - <!-- 提交按钮 -->
19 - <div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
20 - <van-button
21 - type="primary"
22 - block
23 - :loading="uploading"
24 - :disabled="!canSubmit"
25 - @click="onSubmit"
26 - >
27 - 提交
28 - </van-button>
29 - </div>
30 - </div>
31 -</template>
32 -
33 -<script setup>
34 -import { ref, computed, onMounted } from 'vue'
35 -import { useRoute, useRouter } from 'vue-router'
36 -import { showToast, showLoadingToast } from 'vant'
37 -import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
38 -import { useTitle } from '@vueuse/core';
39 -
40 -const route = useRoute()
41 -const router = useRouter()
42 -useTitle(route.meta.title);
43 -
44 -// 留言内容
45 -const message = ref('')
46 -// 上传状态
47 -const uploading = ref(false)
48 -
49 -/**
50 - * 是否可以提交
51 - */
52 -const canSubmit = computed(() => {
53 - return message.value.trim() !== '' && message.value.trim().length >= 10
54 -})
55 -
56 -/**
57 - * 提交表单
58 - */
59 -const onSubmit = async () => {
60 - if (uploading.value) return
61 -
62 - if (message.value.trim().length < 10) {
63 - showToast('打卡内容至少需要10个字符')
64 - return
65 - }
66 -
67 - uploading.value = true
68 - const toast = showLoadingToast({
69 - message: '提交中...',
70 - forbidClick: true,
71 - })
72 -
73 - try {
74 - if (route.query.status === 'edit') {
75 - // 编辑打卡接口
76 - const { code, data } = await editUploadTaskInfoAPI({
77 - i: route.query.post_id,
78 - note: message.value,
79 - meta_id: [], // 文本类型不需要文件
80 - file_type: route.query.type,
81 - });
82 - if (code === 1) {
83 - showToast('提交成功')
84 - router.back()
85 - }
86 - } else {
87 - // 新增打卡接口
88 - const { code, data } = await addUploadTaskAPI({
89 - task_id: route.query.id,
90 - note: message.value,
91 - meta_id: [], // 文本类型不需要文件
92 - file_type: route.query.type,
93 - });
94 - if (code === 1) {
95 - showToast('提交成功')
96 - router.back()
97 - }
98 - }
99 - } catch (error) {
100 - showToast('提交失败,请重试')
101 - } finally {
102 - uploading.value = false
103 - }
104 -}
105 -
106 -/**
107 - * 页面挂载时的初始化逻辑
108 - */
109 -onMounted(async () => {
110 - if (route.query.status === 'edit') {
111 - const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
112 - if (code === 1) {
113 - message.value = data.note
114 - }
115 - }
116 -})
117 -</script>
118 -
119 -<style lang="less" scoped>
120 -.checkin-upload-text {
121 - min-height: 100vh;
122 - padding-bottom: 80px;
123 -}
124 -
125 -.van-field {
126 - border-radius: 8px;
127 - background-color: #f8f9fa;
128 -}
129 -
130 -.van-field__control {
131 - font-size: 16px;
132 - line-height: 1.5;
133 -}
134 -</style>
1 -<!--
2 - * @Date: 2025-06-03 09:41:41
3 - * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-07-02 18:33:29
5 - * @FilePath: /mlaj/src/views/checkin/upload/video.vue
6 - * @Description: 音视频文件上传组件
7 --->
8 -<template>
9 - <div class="checkin-upload-file p-4">
10 - <div class="title text-center pb-4 font-bold">{{route.meta.title}}</div>
11 - <!-- 文件上传区域 -->
12 - <div class="mb-4">
13 - <van-uploader
14 - v-model="fileList"
15 - :max-count="max_count"
16 - :max-size="20 * 1024 * 1024"
17 - :before-read="beforeRead"
18 - :after-read="afterRead"
19 - @delete="onDelete"
20 - multiple
21 - accept="video/*"
22 - result-type="file"
23 - upload-icon="plus"
24 - :deletable="false"
25 - >
26 - </van-uploader>
27 - <van-row v-for="(item, index) in fileList" :key="index" class="mt-2 text-s text-gray-500">
28 - <van-col span="22">{{ item.name }}</van-col>
29 - <van-col span="2" @click="delItem(item)"><van-icon name="clear" /></van-col>
30 - </van-row>
31 - <van-divider />
32 - <div class="mt-2 text-xs text-gray-500">最多上传{{ max_count }}个文件,每个不超过20M</div>
33 - <div class="mt-2 text-xs text-gray-500">上传类型:&nbsp;视频文件</div>
34 - </div>
35 -
36 - <!-- 文字留言区域 -->
37 - <div class="mb-4 border">
38 - <van-field
39 - v-model="message"
40 - rows="4"
41 - autosize
42 - type="textarea"
43 - placeholder="请输入打卡留言"
44 - />
45 - </div>
46 -
47 - <!-- 提交按钮 -->
48 - <div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
49 - <van-button
50 - type="primary"
51 - block
52 - :loading="uploading"
53 - :disabled="!canSubmit"
54 - @click="onSubmit"
55 - >
56 - 提交
57 - </van-button>
58 - </div>
59 -
60 - <!-- 上传加载遮罩 -->
61 - <van-overlay :show="loading">
62 - <div class="wrapper" @click.stop>
63 - <van-loading vertical color="#FFFFFF">上传中...</van-loading>
64 - </div>
65 - </van-overlay>
66 - </div>
67 -</template>
68 -
69 -<script setup>
70 -import { ref, computed } from 'vue'
71 -import { useRoute, useRouter } from 'vue-router'
72 -import { showToast, showLoadingToast } from 'vant'
73 -import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
74 -import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
75 -import { qiniuFileHash } from '@/utils/qiniuFileHash';
76 -import _ from 'lodash'
77 -import { useTitle } from '@vueuse/core';
78 -import { useAuth } from '@/contexts/auth'
79 -
80 -const route = useRoute()
81 -const router = useRouter()
82 -const { currentUser } = useAuth()
83 -useTitle(route.meta.title);
84 -
85 -const max_count = ref(5);
86 -
87 -// 文件列表
88 -const fileList = ref([])
89 -// 留言内容
90 -const message = ref('')
91 -// 上传状态
92 -const uploading = ref(false)
93 -// 上传loading
94 -const loading = ref(false)
95 -
96 -// 是否可以提交
97 -const canSubmit = computed(() => {
98 - return fileList.value.length > 0 && message.value.trim() !== ''
99 -})
100 -
101 -// 文件校验
102 -const beforeRead = (file) => {
103 - let flag = true
104 -
105 - if (Array.isArray(file)) {
106 - // 多个文件
107 - const invalidTypes = file.filter(item => {
108 - const fileType = item.type.toLowerCase();
109 - return !fileType.startsWith('video/');
110 - })
111 - if (invalidTypes.length) {
112 - flag = false
113 - showToast('请上传视频文件')
114 - }
115 - if (fileList.value.length + file.length > max_count.value) {
116 - flag = false
117 - showToast(`最大上传数量为${max_count.value}个`)
118 - }
119 - } else {
120 - const fileType = file.type.toLowerCase();
121 - if (!fileType.startsWith('video/')) {
122 - showToast('请上传视频文件')
123 - flag = false
124 - }
125 - if (fileList.value.length + 1 > max_count.value) {
126 - flag = false
127 - showToast(`最大上传数量为${max_count.value}个`)
128 - }
129 - if ((file.size / 1024 / 1024).toFixed(2) > 20) {
130 - flag = false
131 - showToast('最大文件体积为20MB')
132 - }
133 - }
134 - return flag
135 -}
136 -
137 -/**
138 - * 获取文件哈希(与七牛云ETag一致)
139 - * @param {File} file 文件对象
140 - * @returns {Promise<string>} 哈希字符串
141 - * 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。
142 - */
143 -const getFileMD5 = async (file) => {
144 - return await qiniuFileHash(file)
145 -}
146 -
147 -// 上传到七牛云
148 -const uploadToQiniu = async (file, token, fileName) => {
149 - const formData = new FormData()
150 - formData.append('file', file)
151 - formData.append('token', token)
152 - formData.append('key', fileName)
153 -
154 - const config = {
155 - headers: { 'Content-Type': 'multipart/form-data' }
156 - }
157 -
158 - // 根据协议选择上传地址
159 - const qiniuUploadUrl = window.location.protocol === 'https:'
160 - ? 'https://up.qbox.me'
161 - : 'http://upload.qiniu.com'
162 -
163 - return await qiniuUploadAPI(qiniuUploadUrl, formData, config)
164 -}
165 -
166 -// 处理单个文件上传
167 -const handleUpload = async (file) => {
168 - loading.value = true
169 - try {
170 - // 获取MD5值
171 - const md5 = await getFileMD5(file.file)
172 -
173 - // 获取七牛token
174 - const tokenResult = await qiniuTokenAPI({
175 - name: file.file.name,
176 - hash: md5
177 - })
178 -
179 - // 文件已存在,直接返回
180 - if (tokenResult.data) {
181 - return tokenResult.data
182 - }
183 -
184 - // 新文件上传
185 - if (tokenResult.token) {
186 - const suffix = /.[^.]+$/.exec(file.file.name) || ''
187 - const fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/file/${md5}${suffix}`
188 -
189 - const { filekey } = await uploadToQiniu(
190 - file.file,
191 - tokenResult.token,
192 - fileName
193 - )
194 -
195 - if (filekey) {
196 - // 保存文件信息
197 - const { data } = await saveFileAPI({
198 - name: file.file.name,
199 - filekey,
200 - hash: md5
201 - })
202 - return data
203 - }
204 - }
205 - return null
206 - } catch (error) {
207 - console.error('Upload error:', error)
208 - return null
209 - } finally {
210 - loading.value = false
211 - }
212 -}
213 -
214 -// 文件读取后的处理
215 -const afterRead = async (file) => {
216 - if (Array.isArray(file)) {
217 - // 多文件上传
218 - for (const item of file) {
219 - item.status = 'uploading'
220 - item.message = '上传中...'
221 - const result = await handleUpload(item)
222 - if (result) {
223 - item.status = 'done'
224 - item.message = '上传成功'
225 - item.url = result.url
226 - item.meta_id = result.meta_id
227 - item.name = result.name
228 - } else {
229 - item.status = 'failed'
230 - item.message = '上传失败'
231 - showToast('上传失败,请重试')
232 - }
233 - }
234 - } else {
235 - // 单文件上传
236 - file.status = 'uploading'
237 - file.message = '上传中...'
238 - const result = await handleUpload(file)
239 - if (result) {
240 - file.status = 'done'
241 - file.message = '上传成功'
242 - file.url = result.url
243 - file.meta_id = result.meta_id
244 - file.name = result.name
245 - } else {
246 - file.status = 'failed'
247 - file.message = '上传失败'
248 - showToast('上传失败,请重试')
249 - }
250 - }
251 -}
252 -
253 -// 删除文件
254 -const onDelete = (file) => {
255 - const index = fileList.value.indexOf(file)
256 - if (index !== -1) {
257 - fileList.value.splice(index, 1)
258 - }
259 -}
260 -
261 -// 提交表单
262 -const onSubmit = async () => {
263 - if (uploading.value) return
264 -
265 - // 检查是否所有文件都上传完成
266 - const hasUploadingFiles = fileList.value.some(file => file.status === 'uploading')
267 - if (hasUploadingFiles) {
268 - showToast('请等待所有文件上传完成')
269 - return
270 - }
271 -
272 - uploading.value = true
273 - const toast = showLoadingToast({
274 - message: '提交中...',
275 - forbidClick: true,
276 - })
277 -
278 - try {
279 - if (route.query.status === 'edit') {
280 - // 编辑打卡接口
281 - const { code, data } = await editUploadTaskInfoAPI({
282 - i: route.query.post_id,
283 - note: message.value,
284 - meta_id: fileList.value.map(item => item.meta_id),
285 - file_type: route.query.type,
286 - });
287 - if (code === 1) {
288 - showToast('提交成功')
289 - router.back()
290 - }
291 - } else {
292 - // 新增打卡接口
293 - const { code, data } = await addUploadTaskAPI({
294 - task_id: route.query.id,
295 - note: message.value,
296 - meta_id: fileList.value.map(item => item.meta_id),
297 - file_type: route.query.type,
298 - });
299 - if (code === 1) {
300 - showToast('提交成功')
301 - router.back()
302 - }
303 - }
304 - } catch (error) {
305 - showToast('提交失败,请重试')
306 - } finally {
307 - toast.close()
308 - uploading.value = false
309 - }
310 -}
311 -
312 -onMounted(async () => {
313 - if (route.query.status === 'edit') {
314 - const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
315 - if (code === 1) {
316 - fileList.value = data.files.map(item => ({
317 - url: item.value,
318 - status: 'done',
319 - message: '上传成功',
320 - meta_id: item.meta_id,
321 - name: item.name,
322 - }))
323 - message.value = data.note
324 - }
325 - }
326 -});
327 -
328 -const delItem = (item) => {
329 - const index = fileList.value.indexOf(item)
330 - if (index!== -1) {
331 - fileList.value.splice(index, 1)
332 - }
333 -}
334 -</script>
335 -
336 -<style lang="less" scoped>
337 -.checkin-upload-file {
338 - min-height: 100vh;
339 - padding-bottom: 80px;
340 -}
341 -
342 -.wrapper {
343 - display: flex;
344 - align-items: center;
345 - justify-content: center;
346 - height: 100%;
347 -}
348 -</style>