hookehuyr

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

移除图片、视频、音频和文本的独立上传页面,将相关路由从路由配置中删除
清理首页中已注释的导航函数,减少代码冗余
删除对应的Vue组件文件,统一打卡功能入口
/*
* @Date: 2025-03-21 13:28:30
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-17 13:54:31
* @LastEditTime: 2026-01-24 15:29:54
* @FilePath: /mlaj/src/router/checkin.js
* @Description: 文件描述
*/
......@@ -61,42 +61,6 @@ export default [
}
},
{
path: '/checkin/image',
name: 'ImageCheckIn',
component: () => import('@/views/checkin/upload/image.vue'),
meta: {
title: '打卡图片',
requiresAuth: true
}
},
{
path: '/checkin/video',
name: 'VideoCheckIn',
component: () => import('@root/src/views/checkin/upload/video.vue'),
meta: {
title: '打卡视频',
requiresAuth: true
}
},
{
path: '/checkin/audio',
name: 'AudioCheckIn',
component: () => import('@root/src/views/checkin/upload/audio.vue'),
meta: {
title: '打卡音频',
requiresAuth: true
}
},
{
path: '/checkin/text',
name: 'TextCheckIn',
component: () => import('@root/src/views/checkin/upload/text.vue'),
meta: {
title: '打卡文本',
requiresAuth: true
}
},
{
path: '/checkin/join',
name: 'JoinCheckIn',
component: () => import('@root/src/views/checkin/JoinCheckInPage.vue'),
......
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-23 10:46:15
* @LastEditTime: 2026-01-24 15:29:28
* @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
* @Description: 用户打卡主页
-->
......@@ -460,45 +460,6 @@ const goToCheckinDetailPage = () => {
})
}
// const goToCheckinTextPage = () => {
// router.push({
// path: '/checkin/text',
// query: {
// id: route.query.id,
// type: 'text'
// }
// })
// }
// const goToCheckinImagePage = () => {
// router.push({
// path: '/checkin/image',
// query: {
// id: route.query.id,
// type: 'image'
// }
// })
// }
// const goToCheckinVideoPage = (type) => {
// router.push({
// path: '/checkin/video',
// query: {
// id: route.query.id,
// type: 'video',
// }
// })
// }
// const goToCheckinAudioPage = (type) => {
// router.push({
// path: '/checkin/audio',
// query: {
// id: route.query.id,
// type: 'audio',
// }
// })
// }
const handLike = async (post) => {
if (!post.is_liked) {
const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, })
......
<!--
* @Date: 2025-06-03 09:41:41
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-13 14:34:57
* @FilePath: /mlaj/src/views/checkin/upload/audio.vue
* @Description: 音视频文件上传组件
-->
<template>
<div class="checkin-upload-file p-4">
<div class="title text-center pb-4 font-bold">{{route.meta.title}}</div>
<!-- 文件上传区域 -->
<div class="mb-4">
<van-uploader
v-model="fileList"
:max-count="max_count"
:max-size="20 * 1024 * 1024"
:before-read="beforeRead"
:after-read="afterRead"
@delete="onDelete"
multiple
accept=".mp3,.wav,.aac"
result-type="file"
upload-icon="plus"
:deletable="false"
>
</van-uploader>
<van-row v-for="(item, index) in fileList" :key="index" class="mt-2 text-s text-gray-500">
<van-col span="22">{{ item.name }}</van-col>
<van-col span="2" @click="delItem(item)"><van-icon name="clear" /></van-col>
</van-row>
<van-divider />
<div class="mt-2 text-xs text-gray-500">最多上传{{ max_count }}个文件,每个不超过20M</div>
<div class="mt-2 text-xs text-gray-500">上传类型:&nbsp;.mp3,.wav,.aac 格式音频文件</div>
</div>
<!-- 文字留言区域 -->
<div class="mb-4 border">
<van-field
v-model="message"
rows="4"
autosize
type="textarea"
placeholder="请输入打卡留言"
/>
</div>
<!-- 提交按钮 -->
<div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
<van-button
type="primary"
block
:loading="uploading"
:disabled="!canSubmit"
@click="onSubmit"
>
提交
</van-button>
</div>
<!-- 上传加载遮罩 -->
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">上传中...</van-loading>
</div>
</van-overlay>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast, showLoadingToast } from 'vant'
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
import { qiniuFileHash } from '@/utils/qiniuFileHash';
import _ from 'lodash'
import { useTitle } from '@vueuse/core';
import { useAuth } from '@/contexts/auth'
const route = useRoute()
const router = useRouter()
const { currentUser } = useAuth()
useTitle(route.meta.title);
const max_count = ref(5);
// 文件列表
const fileList = ref([])
// 留言内容
const message = ref('')
// 上传状态
const uploading = ref(false)
// 上传loading
const loading = ref(false)
// 是否可以提交
const canSubmit = computed(() => {
return fileList.value.length > 0 && message.value.trim() !== ''
})
// 文件校验
const beforeRead = (file) => {
let flag = true
if (Array.isArray(file)) {
// 多个文件
const invalidTypes = file.filter(item => {
const fileType = item.type.toLowerCase();
return !fileType.startsWith('audio/');
})
if (invalidTypes.length) {
flag = false
showToast('请上传音频文件')
}
if (fileList.value.length + file.length > max_count.value) {
flag = false
showToast(`最大上传数量为${max_count.value}个`)
}
} else {
const fileType = file.type.toLowerCase();
if (!fileType.startsWith('audio/')) {
showToast('请上传音频文件')
flag = false
}
if (fileList.value.length + 1 > max_count.value) {
flag = false
showToast(`最大上传数量为${max_count.value}个`)
}
if ((file.size / 1024 / 1024).toFixed(2) > 20) {
flag = false
showToast('最大文件体积为20MB')
}
}
return flag
}
/**
* 获取文件哈希(与七牛云ETag一致)
* @param {File} file 文件对象
* @returns {Promise<string>} 哈希字符串
* 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。
*/
const getFileMD5 = async (file) => {
return await qiniuFileHash(file)
}
// 上传到七牛云
const uploadToQiniu = async (file, token, fileName) => {
const formData = new FormData()
formData.append('file', file)
formData.append('token', token)
formData.append('key', fileName)
const config = {
headers: { 'Content-Type': 'multipart/form-data' }
}
// 根据协议选择上传地址
const qiniuUploadUrl = window.location.protocol === 'https:'
? 'https://up.qbox.me'
: 'http://upload.qiniu.com'
return await qiniuUploadAPI(qiniuUploadUrl, formData, config)
}
// 处理单个文件上传
const handleUpload = async (file) => {
loading.value = true
try {
// 获取MD5值
const md5 = await getFileMD5(file.file)
// 获取七牛token
const tokenResult = await qiniuTokenAPI({
name: file.file.name,
hash: md5
})
// 文件已存在,直接返回
if (tokenResult.data) {
return tokenResult.data
}
// 新文件上传
if (tokenResult.token) {
const suffix = /.[^.]+$/.exec(file.file.name) || ''
const fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/file/${md5}${suffix}`
const { filekey } = await uploadToQiniu(
file.file,
tokenResult.token,
fileName
)
if (filekey) {
// 保存文件信息
const { data } = await saveFileAPI({
name: file.file.name,
filekey,
hash: md5
})
return data
}
}
return null
} catch (error) {
console.error('Upload error:', error)
return null
} finally {
loading.value = false
}
}
// 文件读取后的处理
const afterRead = async (file) => {
if (Array.isArray(file)) {
// 多文件上传
for (const item of file) {
item.status = 'uploading'
item.message = '上传中...'
const result = await handleUpload(item)
if (result) {
item.status = 'done'
item.message = '上传成功'
item.url = result.url
item.meta_id = result.meta_id
item.name = result.name
} else {
item.status = 'failed'
item.message = '上传失败'
showToast('上传失败,请重试')
}
}
} else {
// 单文件上传
file.status = 'uploading'
file.message = '上传中...'
const result = await handleUpload(file)
if (result) {
file.status = 'done'
file.message = '上传成功'
file.url = result.url
file.meta_id = result.meta_id
file.name = result.name
} else {
file.status = 'failed'
file.message = '上传失败'
showToast('上传失败,请重试')
}
}
}
// 删除文件
const onDelete = (file) => {
const index = fileList.value.indexOf(file)
if (index !== -1) {
fileList.value.splice(index, 1)
}
}
// 提交表单
const onSubmit = async () => {
if (uploading.value) return
// 检查是否所有文件都上传完成
const hasUploadingFiles = fileList.value.some(file => file.status === 'uploading')
if (hasUploadingFiles) {
showToast('请等待所有文件上传完成')
return
}
uploading.value = true
const toast = showLoadingToast({
message: '提交中...',
forbidClick: true,
})
try {
if (route.query.status === 'edit') {
// 编辑打卡接口
const { code, data } = await editUploadTaskInfoAPI({
i: route.query.post_id,
note: message.value,
meta_id: fileList.value.map(item => item.meta_id),
file_type: route.query.type,
});
if (code === 1) {
showToast('提交成功')
router.back()
}
} else {
// 新增打卡接口
const { code, data } = await addUploadTaskAPI({
task_id: route.query.id,
note: message.value,
meta_id: fileList.value.map(item => item.meta_id),
file_type: route.query.type,
});
if (code === 1) {
showToast('提交成功')
router.back()
}
}
} catch (error) {
showToast('提交失败,请重试')
} finally {
toast.close()
uploading.value = false
}
}
onMounted(async () => {
if (route.query.status === 'edit') {
const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
if (code === 1) {
fileList.value = data.files.map(item => ({
name: item.name,
url: item.value,
status: 'done',
message: '上传成功',
meta_id: item.meta_id,
}))
message.value = data.note
}
}
});
const delItem = (item) => {
const index = fileList.value.indexOf(item)
if (index!== -1) {
fileList.value.splice(index, 1)
}
}
</script>
<style lang="less" scoped>
.checkin-upload-file {
min-height: 100vh;
padding-bottom: 80px;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.preview-cover {
position: absolute;
bottom: 0;
box-sizing: border-box;
width: 100%;
padding: 4px;
color: #fff;
font-size: 12px;
text-align: center;
background: rgba(0, 0, 0, 0.3);
}
</style>
<template>
<div class="checkin-upload-image p-4">
<div class="title text-center pb-4 font-bold">{{route.meta.title}}</div>
<!-- 图片上传区域 -->
<div class="mb-4">
<van-uploader
v-model="fileList"
:max-count="max_count"
:max-size="20 * 1024 * 1024"
:before-read="beforeRead"
:after-read="afterRead"
@delete="onDelete"
multiple
accept="image/*"
result-type="file"
>
</van-uploader>
<div class="mt-2 text-xs text-gray-500">最多上传{{ max_count }}张图片,每张不超过20M</div>
<div class="mt-2 text-xs text-gray-500">上传类型:&nbsp;{{ type_text }}</div>
</div>
<!-- 文字留言区域 -->
<div class="mb-4 border">
<van-field
v-model="message"
rows="4"
autosize
type="textarea"
placeholder="请输入打卡留言"
/>
</div>
<!-- 提交按钮 -->
<div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
<van-button
type="primary"
block
:loading="uploading"
:disabled="!canSubmit"
@click="onSubmit"
>
提交
</van-button>
</div>
<!-- 上传加载遮罩 -->
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">上传中...</van-loading>
</div>
</van-overlay>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast, showLoadingToast } from 'vant'
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
import { qiniuFileHash } from '@/utils/qiniuFileHash';
import _ from 'lodash'
import { useTitle } from '@vueuse/core';
import { useAuth } from '@/contexts/auth'
const route = useRoute()
const router = useRouter()
const { currentUser } = useAuth()
useTitle(route.meta.title);
const max_count = ref(5);
// 文件列表
const fileList = ref([])
// 留言内容
const message = ref('')
// 上传状态
const uploading = ref(false)
// 上传loading
const loading = ref(false)
// 是否可以提交
const canSubmit = computed(() => {
return fileList.value.length > 0 && message.value.trim() !== ''
})
// 固定类型限制
const imageTypes = "jpg/jpeg/png";
// 文件类型中文页面显示
const type_text = computed(() => {
// return props.item.component_props.image_type;
return imageTypes;
});
// 文件校验
const beforeRead = (file) => {
const image_types = _.map(imageTypes.split("/"), (item) => `image/${item}`);
let flag = true
if (Array.isArray(file)) {
// 多张图片
const invalidTypes = file.filter(item => {
const fileType = item.type.toLowerCase();
return !image_types.some(type => fileType.includes(type.split('/')[1]));
})
if (invalidTypes.length) {
flag = false
showToast('请上传指定格式图片')
}
if (fileList.value.length + file.length > max_count.value) {
flag = false
showToast(`最大上传数量为${max_count.value}张`)
}
} else {
const fileType = file.type.toLowerCase();
if (!image_types.some(type => fileType.includes(type.split('/')[1]))) {
showToast('请上传指定格式图片')
flag = false
}
if (fileList.value.length + 1 > max_count.value) {
flag = false
showToast(`最大上传数量为${max_count.value}张`)
}
if ((file.size / 1024 / 1024).toFixed(2) > 20) {
flag = false
showToast('最大文件体积为20MB')
}
}
return flag
}
/**
* 获取文件哈希(与七牛云ETag一致)
* @param {File} file 文件对象
* @returns {Promise<string>} 哈希字符串
* 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。
*/
const getFileMD5 = async (file) => {
return await qiniuFileHash(file)
}
// 上传到七牛云
const uploadToQiniu = async (file, token, fileName) => {
const formData = new FormData()
formData.append('file', file)
formData.append('token', token)
formData.append('key', fileName)
const config = {
headers: { 'Content-Type': 'multipart/form-data' }
}
// 根据协议选择上传地址
const qiniuUploadUrl = window.location.protocol === 'https:'
? 'https://up.qbox.me'
: 'http://upload.qiniu.com'
return await qiniuUploadAPI(qiniuUploadUrl, formData, config)
}
// 处理单个文件上传
const handleUpload = async (file) => {
loading.value = true
try {
// 获取MD5值
const md5 = await getFileMD5(file.file)
// 获取七牛token
const tokenResult = await qiniuTokenAPI({
name: file.file.name,
hash: md5
})
// 文件已存在,直接返回
if (tokenResult.data) {
return tokenResult.data
}
// 新文件上传
if (tokenResult.token) {
const suffix = /.[^.]+$/.exec(file.file.name) || ''
const fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/img/${md5}${suffix}`
const { filekey, image_info } = await uploadToQiniu(
file.file,
tokenResult.token,
fileName
)
if (filekey) {
// 保存文件信息
const { data } = await saveFileAPI({
name: file.file.name,
filekey,
hash: md5,
height: image_info?.height,
width: image_info?.width
})
return data
}
}
return null
} catch (error) {
console.error('Upload error:', error)
return null
} finally {
loading.value = false
}
}
// 文件读取后的处理
const afterRead = async (file) => {
if (Array.isArray(file)) {
// 多文件上传
for (const item of file) {
item.status = 'uploading'
item.message = '上传中...'
const result = await handleUpload(item)
if (result) {
item.status = 'done'
item.message = '上传成功'
item.url = result.url
item.meta_id = result.meta_id
} else {
item.status = 'failed'
item.message = '上传失败'
showToast('上传失败,请重试')
}
}
} else {
// 单文件上传
file.status = 'uploading'
file.message = '上传中...'
const result = await handleUpload(file)
if (result) {
file.status = 'done'
file.message = '上传成功'
file.url = result.url
file.meta_id = result.meta_id
} else {
file.status = 'failed'
file.message = '上传失败'
showToast('上传失败,请重试')
}
}
}
// 删除文件
const onDelete = (file) => {
const index = fileList.value.indexOf(file)
if (index !== -1) {
fileList.value.splice(index, 1)
}
}
// 提交表单
const onSubmit = async () => {
if (uploading.value) return
// 检查是否所有文件都上传完成
const hasUploadingFiles = fileList.value.some(file => file.status === 'uploading')
if (hasUploadingFiles) {
showToast('请等待所有文件上传完成')
return
}
uploading.value = true
const toast = showLoadingToast({
message: '提交中...',
forbidClick: true,
})
try {
if (route.query.status === 'edit') {
// 编辑打卡接口
const { code, data } = await editUploadTaskInfoAPI({
i: route.query.post_id,
note: message.value,
meta_id: fileList.value.map(item => item.meta_id),
file_type: route.query.type,
});
if (code === 1) {
showToast('提交成功')
router.back()
}
} else {
// 新增打卡接口
const { code, data } = await addUploadTaskAPI({
task_id: route.query.id,
note: message.value,
meta_id: fileList.value.map(item => item.meta_id),
file_type: route.query.type,
});
if (code === 1) {
showToast('提交成功')
router.back()
}
}
} catch (error) {
showToast('提交失败,请重试')
} finally {
toast.close()
uploading.value = false
}
}
onMounted(async () => {
if (route.query.status === 'edit') {
const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
if (code === 1) {
fileList.value = data.files.map(item => ({
url: item.value,
status: 'done',
message: '上传成功',
meta_id: item.meta_id,
}))
message.value = data.note
}
}
})
</script>
<style lang="less" scoped>
.checkin-upload-image {
min-height: 100vh;
padding-bottom: 80px;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
</style>
<template>
<div class="checkin-upload-text p-4">
<div class="title text-center pb-4 font-bold">{{ route.meta.title }}</div>
<!-- 文字输入区域 -->
<div class="mb-4 border">
<van-field
v-model="message"
rows="8"
autosize
type="textarea"
placeholder="请输入打卡内容, 至少需要10个字符"
maxlength="500"
show-word-limit
/>
</div>
<!-- 提交按钮 -->
<div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
<van-button
type="primary"
block
:loading="uploading"
:disabled="!canSubmit"
@click="onSubmit"
>
提交
</van-button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast, showLoadingToast } from 'vant'
import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
import { useTitle } from '@vueuse/core';
const route = useRoute()
const router = useRouter()
useTitle(route.meta.title);
// 留言内容
const message = ref('')
// 上传状态
const uploading = ref(false)
/**
* 是否可以提交
*/
const canSubmit = computed(() => {
return message.value.trim() !== '' && message.value.trim().length >= 10
})
/**
* 提交表单
*/
const onSubmit = async () => {
if (uploading.value) return
if (message.value.trim().length < 10) {
showToast('打卡内容至少需要10个字符')
return
}
uploading.value = true
const toast = showLoadingToast({
message: '提交中...',
forbidClick: true,
})
try {
if (route.query.status === 'edit') {
// 编辑打卡接口
const { code, data } = await editUploadTaskInfoAPI({
i: route.query.post_id,
note: message.value,
meta_id: [], // 文本类型不需要文件
file_type: route.query.type,
});
if (code === 1) {
showToast('提交成功')
router.back()
}
} else {
// 新增打卡接口
const { code, data } = await addUploadTaskAPI({
task_id: route.query.id,
note: message.value,
meta_id: [], // 文本类型不需要文件
file_type: route.query.type,
});
if (code === 1) {
showToast('提交成功')
router.back()
}
}
} catch (error) {
showToast('提交失败,请重试')
} finally {
uploading.value = false
}
}
/**
* 页面挂载时的初始化逻辑
*/
onMounted(async () => {
if (route.query.status === 'edit') {
const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
if (code === 1) {
message.value = data.note
}
}
})
</script>
<style lang="less" scoped>
.checkin-upload-text {
min-height: 100vh;
padding-bottom: 80px;
}
.van-field {
border-radius: 8px;
background-color: #f8f9fa;
}
.van-field__control {
font-size: 16px;
line-height: 1.5;
}
</style>
<!--
* @Date: 2025-06-03 09:41:41
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 18:33:29
* @FilePath: /mlaj/src/views/checkin/upload/video.vue
* @Description: 音视频文件上传组件
-->
<template>
<div class="checkin-upload-file p-4">
<div class="title text-center pb-4 font-bold">{{route.meta.title}}</div>
<!-- 文件上传区域 -->
<div class="mb-4">
<van-uploader
v-model="fileList"
:max-count="max_count"
:max-size="20 * 1024 * 1024"
:before-read="beforeRead"
:after-read="afterRead"
@delete="onDelete"
multiple
accept="video/*"
result-type="file"
upload-icon="plus"
:deletable="false"
>
</van-uploader>
<van-row v-for="(item, index) in fileList" :key="index" class="mt-2 text-s text-gray-500">
<van-col span="22">{{ item.name }}</van-col>
<van-col span="2" @click="delItem(item)"><van-icon name="clear" /></van-col>
</van-row>
<van-divider />
<div class="mt-2 text-xs text-gray-500">最多上传{{ max_count }}个文件,每个不超过20M</div>
<div class="mt-2 text-xs text-gray-500">上传类型:&nbsp;视频文件</div>
</div>
<!-- 文字留言区域 -->
<div class="mb-4 border">
<van-field
v-model="message"
rows="4"
autosize
type="textarea"
placeholder="请输入打卡留言"
/>
</div>
<!-- 提交按钮 -->
<div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
<van-button
type="primary"
block
:loading="uploading"
:disabled="!canSubmit"
@click="onSubmit"
>
提交
</van-button>
</div>
<!-- 上传加载遮罩 -->
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">上传中...</van-loading>
</div>
</van-overlay>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast, showLoadingToast } from 'vant'
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
import { qiniuFileHash } from '@/utils/qiniuFileHash';
import _ from 'lodash'
import { useTitle } from '@vueuse/core';
import { useAuth } from '@/contexts/auth'
const route = useRoute()
const router = useRouter()
const { currentUser } = useAuth()
useTitle(route.meta.title);
const max_count = ref(5);
// 文件列表
const fileList = ref([])
// 留言内容
const message = ref('')
// 上传状态
const uploading = ref(false)
// 上传loading
const loading = ref(false)
// 是否可以提交
const canSubmit = computed(() => {
return fileList.value.length > 0 && message.value.trim() !== ''
})
// 文件校验
const beforeRead = (file) => {
let flag = true
if (Array.isArray(file)) {
// 多个文件
const invalidTypes = file.filter(item => {
const fileType = item.type.toLowerCase();
return !fileType.startsWith('video/');
})
if (invalidTypes.length) {
flag = false
showToast('请上传视频文件')
}
if (fileList.value.length + file.length > max_count.value) {
flag = false
showToast(`最大上传数量为${max_count.value}个`)
}
} else {
const fileType = file.type.toLowerCase();
if (!fileType.startsWith('video/')) {
showToast('请上传视频文件')
flag = false
}
if (fileList.value.length + 1 > max_count.value) {
flag = false
showToast(`最大上传数量为${max_count.value}个`)
}
if ((file.size / 1024 / 1024).toFixed(2) > 20) {
flag = false
showToast('最大文件体积为20MB')
}
}
return flag
}
/**
* 获取文件哈希(与七牛云ETag一致)
* @param {File} file 文件对象
* @returns {Promise<string>} 哈希字符串
* 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。
*/
const getFileMD5 = async (file) => {
return await qiniuFileHash(file)
}
// 上传到七牛云
const uploadToQiniu = async (file, token, fileName) => {
const formData = new FormData()
formData.append('file', file)
formData.append('token', token)
formData.append('key', fileName)
const config = {
headers: { 'Content-Type': 'multipart/form-data' }
}
// 根据协议选择上传地址
const qiniuUploadUrl = window.location.protocol === 'https:'
? 'https://up.qbox.me'
: 'http://upload.qiniu.com'
return await qiniuUploadAPI(qiniuUploadUrl, formData, config)
}
// 处理单个文件上传
const handleUpload = async (file) => {
loading.value = true
try {
// 获取MD5值
const md5 = await getFileMD5(file.file)
// 获取七牛token
const tokenResult = await qiniuTokenAPI({
name: file.file.name,
hash: md5
})
// 文件已存在,直接返回
if (tokenResult.data) {
return tokenResult.data
}
// 新文件上传
if (tokenResult.token) {
const suffix = /.[^.]+$/.exec(file.file.name) || ''
const fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/file/${md5}${suffix}`
const { filekey } = await uploadToQiniu(
file.file,
tokenResult.token,
fileName
)
if (filekey) {
// 保存文件信息
const { data } = await saveFileAPI({
name: file.file.name,
filekey,
hash: md5
})
return data
}
}
return null
} catch (error) {
console.error('Upload error:', error)
return null
} finally {
loading.value = false
}
}
// 文件读取后的处理
const afterRead = async (file) => {
if (Array.isArray(file)) {
// 多文件上传
for (const item of file) {
item.status = 'uploading'
item.message = '上传中...'
const result = await handleUpload(item)
if (result) {
item.status = 'done'
item.message = '上传成功'
item.url = result.url
item.meta_id = result.meta_id
item.name = result.name
} else {
item.status = 'failed'
item.message = '上传失败'
showToast('上传失败,请重试')
}
}
} else {
// 单文件上传
file.status = 'uploading'
file.message = '上传中...'
const result = await handleUpload(file)
if (result) {
file.status = 'done'
file.message = '上传成功'
file.url = result.url
file.meta_id = result.meta_id
file.name = result.name
} else {
file.status = 'failed'
file.message = '上传失败'
showToast('上传失败,请重试')
}
}
}
// 删除文件
const onDelete = (file) => {
const index = fileList.value.indexOf(file)
if (index !== -1) {
fileList.value.splice(index, 1)
}
}
// 提交表单
const onSubmit = async () => {
if (uploading.value) return
// 检查是否所有文件都上传完成
const hasUploadingFiles = fileList.value.some(file => file.status === 'uploading')
if (hasUploadingFiles) {
showToast('请等待所有文件上传完成')
return
}
uploading.value = true
const toast = showLoadingToast({
message: '提交中...',
forbidClick: true,
})
try {
if (route.query.status === 'edit') {
// 编辑打卡接口
const { code, data } = await editUploadTaskInfoAPI({
i: route.query.post_id,
note: message.value,
meta_id: fileList.value.map(item => item.meta_id),
file_type: route.query.type,
});
if (code === 1) {
showToast('提交成功')
router.back()
}
} else {
// 新增打卡接口
const { code, data } = await addUploadTaskAPI({
task_id: route.query.id,
note: message.value,
meta_id: fileList.value.map(item => item.meta_id),
file_type: route.query.type,
});
if (code === 1) {
showToast('提交成功')
router.back()
}
}
} catch (error) {
showToast('提交失败,请重试')
} finally {
toast.close()
uploading.value = false
}
}
onMounted(async () => {
if (route.query.status === 'edit') {
const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
if (code === 1) {
fileList.value = data.files.map(item => ({
url: item.value,
status: 'done',
message: '上传成功',
meta_id: item.meta_id,
name: item.name,
}))
message.value = data.note
}
}
});
const delItem = (item) => {
const index = fileList.value.indexOf(item)
if (index!== -1) {
fileList.value.splice(index, 1)
}
}
</script>
<style lang="less" scoped>
.checkin-upload-file {
min-height: 100vh;
padding-bottom: 80px;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
</style>