feat(移动端): 新增支出使用 COS 上传附件

1. API 增强 (src/api/index.ts):
   - 新增 uploadFile 函数:支持文件上传到腾讯云 COS
   - 新增 getFileList 函数:获取文件列表
   - 新增 deleteFile 函数:删除文件

2. 新增支出页面优化 (src/views/expense/Add.vue):
   - 修改附件上传逻辑:从 base64 改为 COS 上传
   - onAfterRead: 调用 uploadFile API 上传到 COS
   - 获取 COS 返回的文件路径并存储
   - 提交时将 COS 路径数组转为逗号分隔字符串
   - 图片预览直接使用 COS URL
   - 添加上传进度提示和成功/失败反馈

技术实现:
- 使用 FormData 进行 multipart/form-data 上传
- 业务类型标识为'expense'
- 附件以 COS 完整 URL 形式存储(逗号分隔)
- 支持多图片上传(最多 9 张)
- 每张图片独立上传到 COS,获得永久可访问链接
This commit is contained in:
zhangjf 2026-03-01 22:23:59 +08:00
parent da4488dccc
commit a74875eeda
2 changed files with 52 additions and 19 deletions

View File

@ -23,6 +23,30 @@ export function updatePassword(data: { oldPassword: string; newPassword: string;
return request.put('/sys/profile/password', data) return request.put('/sys/profile/password', data)
} }
// ===================== 文件管理 =====================
export function uploadFile(file: File, businessType?: string, businessId?: number, description?: string) {
const formData = new FormData()
formData.append('file', file)
if (businessType) formData.append('businessType', businessType)
if (businessId) formData.append('businessId', String(businessId))
if (description) formData.append('description', description)
return request.post('/file/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export function getFileList(params?: { pageNum: number; pageSize: number; businessType?: string; businessId?: number; fileType?: string }) {
return request.get('/file/page', { params })
}
export function deleteFile(id: number) {
return request.delete(`/file/${id}`)
}
// ===================== 项目管理 ===================== // ===================== 项目管理 =====================
export function getProjectList(params?: { pageNum: number; pageSize: number; keyword?: string }) { export function getProjectList(params?: { pageNum: number; pageSize: number; keyword?: string }) {

View File

@ -87,14 +87,14 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast, showSuccessToast, showFailToast, ImagePreview } from 'vant' import { showToast, showSuccessToast, showFailToast, ImagePreview } from 'vant'
import { createExpense, getExpenseTypeTree } from '@/api' import { createExpense, getExpenseTypeTree, uploadFile } from '@/api'
const router = useRouter() const router = useRouter()
const loading = ref(false) const loading = ref(false)
const showTypePicker = ref(false) const showTypePicker = ref(false)
const showDatePicker = ref(false) const showDatePicker = ref(false)
const fileList = ref<any[]>([]) const fileList = ref<any[]>([])
const uploadedAttachments = ref<string[]>([]) const uploadedAttachments = ref<string[]>([]) // COS
const form = reactive({ const form = reactive({
title: '', title: '',
@ -120,22 +120,29 @@ const onDateConfirm = ({ selectedValues }: any) => {
showDatePicker.value = false showDatePicker.value = false
} }
// // - 使 COS
const onAfterRead = async (file: any) => { const onAfterRead = async (file: any) => {
// API if (!file.file) return
// 使 fund-file base64
if (file.file) { try {
const reader = new FileReader() //
reader.onload = (e) => { showToast('上传中...')
const base64 = e.target?.result as string
// base64 data:image/jpeg;base64, // uploadFile API COS
const base64Data = base64.split(',')[1] const res: any = await uploadFile(file.file, 'expense', undefined, '支出附件')
if (base64Data) {
uploadedAttachments.value.push(base64Data) //
console.log('已添加附件:', base64Data.substring(0, 50) + '...') const filePath = res.data?.filePath || res.data?.url
} if (filePath) {
uploadedAttachments.value.push(filePath)
console.log('文件上传成功:', filePath)
showSuccessToast('上传成功')
} else {
showFailToast('上传失败:未获取文件路径')
} }
reader.readAsDataURL(file.file) } catch (error: any) {
console.error('上传失败:', error)
showFailToast(error.message || '上传失败')
} }
} }
@ -145,10 +152,12 @@ const onBeforeDelete = (file: any, detail: any) => {
return true return true
} }
// // URL
const onPreviewImage = (index: number) => { const onPreviewImage = (index: number) => {
//
// 使 URL
ImagePreview.show({ ImagePreview.show({
images: uploadedAttachments.value.map(b64 => `data:image/jpeg;base64,${b64}`), images: uploadedAttachments.value,
startPosition: index, startPosition: index,
}) })
} }
@ -176,7 +185,7 @@ const handleSubmit = async () => {
// LocalDateTime // LocalDateTime
const expenseDateTime = form.expenseDate ? `${form.expenseDate}T12:00:00` : null const expenseDateTime = form.expenseDate ? `${form.expenseDate}T12:00:00` : null
// base64 // COS
const attachmentsStr = uploadedAttachments.value.length > 0 const attachmentsStr = uploadedAttachments.value.length > 0
? uploadedAttachments.value.join(',') ? uploadedAttachments.value.join(',')
: null : null