From 073f7f882b09250c8827c9759dc649c62a4c009e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Thu, 18 Jun 2026 15:05:02 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=8E=A8=E5=B9=BF=E5=91=98=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/src/api/core/file-gallery.ts | 29 ++ apps/web-antd/src/api/core/upload.ts | 2 +- .../src/components/form/components/avatar.vue | 84 ++--- .../form/components/gallery-pick-link.vue | 82 +++++ .../form/components/image-gallery-picker.vue | 200 ++++++++++++ .../form/components/upload-image.vue | 184 ++++------- apps/web-antd/src/utils/oss-upload.ts | 2 +- .../views/system/doctor-input/config/form.ts | 13 + .../src/views/system/file-gallery/index.vue | 169 ++++++++++ .../src/views/system/system-config/index.vue | 294 +++++++++--------- 10 files changed, 750 insertions(+), 309 deletions(-) create mode 100644 apps/web-antd/src/api/core/file-gallery.ts create mode 100644 apps/web-antd/src/components/form/components/gallery-pick-link.vue create mode 100644 apps/web-antd/src/components/form/components/image-gallery-picker.vue create mode 100644 apps/web-antd/src/views/system/file-gallery/index.vue diff --git a/apps/web-antd/src/api/core/file-gallery.ts b/apps/web-antd/src/api/core/file-gallery.ts new file mode 100644 index 00000000..68263d3f --- /dev/null +++ b/apps/web-antd/src/api/core/file-gallery.ts @@ -0,0 +1,29 @@ +import { requestClient } from '#/api/request'; + +const prefix = 'file-gallery/'; + +export interface FileGalleryItem { + id: number; + url: string; + file_size: number; + file_size_text: string; + created_at?: string; + original_name?: string; +} + +export async function getFileGalleryList(params?: { page?: number; page_size?: number; pid?: number; type?: number }) { + return requestClient.get<{ + items: FileGalleryItem[]; + total: number; + page: number; + page_size: number; + }>(`${prefix}list`, { params }); +} + +export async function deleteFileGalleryItem(id: number) { + return requestClient.post(`${prefix}delete`, { id }); +} + +export async function syncFileGalleryFromOss() { + return requestClient.post<{ added: number; skipped: number }>(`${prefix}sync-oss`); +} diff --git a/apps/web-antd/src/api/core/upload.ts b/apps/web-antd/src/api/core/upload.ts index ccde4b9c..bba6b0ef 100644 --- a/apps/web-antd/src/api/core/upload.ts +++ b/apps/web-antd/src/api/core/upload.ts @@ -61,7 +61,7 @@ export async function getOssSignature() { /** * OSS 直传成功后登记 xk_file */ -export async function registerOssFile(data: { url: string; type?: number; source?: number }) { +export async function registerOssFile(data: { url: string; type?: number; source?: number; file_size?: number }) { return requestClient.post('/upload/register-oss-file', data); } diff --git a/apps/web-antd/src/components/form/components/avatar.vue b/apps/web-antd/src/components/form/components/avatar.vue index 8186d853..11a805cf 100644 --- a/apps/web-antd/src/components/form/components/avatar.vue +++ b/apps/web-antd/src/components/form/components/avatar.vue @@ -2,7 +2,7 @@ import { useVModel } from '@vueuse/core'; import { Upload } from 'ant-design-vue'; -// 导入上传相关API和工具函数 +import GalleryPickLink from '#/components/form/components/gallery-pick-link.vue'; import { uploadFile } from '#/api/core/upload'; import { uploadToOss } from '#/utils/oss-upload'; import { @@ -26,93 +26,77 @@ const mValue = useVModel(props, 'value', emits, { defaultValue: props.value, passive: true, }); -/** - * 自定义上传请求处理函数 - * - * 该函数根据preferences配置的上传方式,选择使用OSS直传或后端上传: - * - 'direct': OSS直传模式,文件直接从浏览器上传到阿里云OSS - * - 'backend': 后端上传模式,文件先上传到后端服务器,再由后端上传到OSS - * - * 上传方式说明: - * 1. OSS直传(direct): - * - 调用uploadToOss函数,直接上传到OSS - * - 减少服务器负载,上传速度更快 - * - * 2. 后端上传(backend): - * - 调用uploadFile API,通过后端服务器上传 - * - 兼容现有功能,所有上传逻辑由后端统一处理 - * - * 兼容性处理: - * - 两种上传方式返回的数据结构保持一致:{ url: string } - * - 上传成功后,将URL赋值给mValue,触发组件更新 - * - * @param e 上传事件对象,包含file字段(文件对象) - */ + const customRequest = async (e: any) => { try { const actualFile = resolveUploadFile(e.file as File); - // 从preferences中读取上传方式配置 - // uploadMethod可能的值:'direct'(OSS直传)或 'backend'(后端上传) const uploadMethod = preferences.app.uploadMethod || 'direct'; let data: { url: string }; - // 根据配置选择上传方式 if (uploadMethod === 'direct') { - // OSS直传模式:文件直接从浏览器上传到OSS - // uploadToOss函数会处理签名获取、文件上传等所有逻辑 data = await uploadToOss({ file: actualFile, }); } else { - // 后端上传模式:文件先上传到后端服务器,再由后端上传到OSS - // uploadFile函数会将文件发送到后端API(/upload/image) data = await uploadFile({ file: actualFile, }); } - // 上传成功,将URL赋值给mValue - // mValue是双向绑定的值,更新后会触发父组件的更新 mValue.value = data.url; } catch (error) { - // 上传失败,记录错误信息 - // 注意:这里没有显示错误提示,如果需要可以添加message.error console.error('头像上传失败', error); } }; + +function onGallerySelect(urls: string[]) { + if (urls[0]) { + mValue.value = urls[0]; + } +} + const handleRemove = (e: Event) => { e.stopPropagation(); mValue.value = ''; }; diff --git a/apps/web-antd/src/components/form/components/image-gallery-picker.vue b/apps/web-antd/src/components/form/components/image-gallery-picker.vue new file mode 100644 index 00000000..3ab4ddab --- /dev/null +++ b/apps/web-antd/src/components/form/components/image-gallery-picker.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/apps/web-antd/src/components/form/components/upload-image.vue b/apps/web-antd/src/components/form/components/upload-image.vue index 00cb0ddf..8d0bc807 100644 --- a/apps/web-antd/src/components/form/components/upload-image.vue +++ b/apps/web-antd/src/components/form/components/upload-image.vue @@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'; import { Modal, Upload } from 'ant-design-vue'; import type { UploadFile } from 'ant-design-vue'; -// 导入上传相关API和工具函数 +import GalleryPickLink from '#/components/form/components/gallery-pick-link.vue'; import { uploadFile } from '#/api/core/upload'; import { uploadToOss } from '#/utils/oss-upload'; import { @@ -13,7 +13,6 @@ import { import { preferences } from '@vben/preferences'; import { Icon } from '#/components/icon'; -// 定义接口 interface FileItem { uid: string; name: string; @@ -21,7 +20,6 @@ interface FileItem { url: string; } -// 定义props interface Props { modelValue: string[]; multiple?: boolean; @@ -40,7 +38,6 @@ const emit = defineEmits<{ const fileList = ref([]); -// 初始化fileList const initFileList = () => { fileList.value = props.modelValue.map((url) => ({ uid: url, @@ -50,23 +47,26 @@ const initFileList = () => { })); }; -// 初始化 initFileList(); -// 监听外部modelValue变化,同步到fileList -watch(() => props.modelValue, (newVal) => { - const currentUrls = fileList.value.filter(file => file.status === 'done').map(file => file.url); - if (JSON.stringify(newVal) !== JSON.stringify(currentUrls)) { - fileList.value = newVal.map((url) => ({ - uid: url, - name: url.split('/').pop() || 'file', - status: 'done' as const, - url, - })); - } -}, { deep: true }); +watch( + () => props.modelValue, + (newVal) => { + const currentUrls = fileList.value + .filter((file) => file.status === 'done') + .map((file) => file.url); + if (JSON.stringify(newVal) !== JSON.stringify(currentUrls)) { + fileList.value = newVal.map((url) => ({ + uid: url, + name: url.split('/').pop() || 'file', + status: 'done' as const, + url, + })); + } + }, + { deep: true }, +); -// 统一通过updateModelValue函数更新modelValue const updateModelValue = () => { const urls = fileList.value .filter((file) => file.status === 'done' && file.url) @@ -74,36 +74,10 @@ const updateModelValue = () => { emit('update:modelValue', urls); }; -/** - * 自定义上传请求处理函数 - * - * 该函数根据preferences配置的上传方式,选择使用OSS直传或后端上传: - * - 'direct': OSS直传模式,文件直接从浏览器上传到阿里云OSS - * - 'backend': 后端上传模式,文件先上传到后端服务器,再由后端上传到OSS - * - * 两种上传方式的区别: - * 1. OSS直传(direct): - * - 优点:减少服务器负载,上传速度更快,用户体验更好 - * - 缺点:需要后端提供签名接口,配置相对复杂 - * - 实现:调用uploadToOss函数,直接上传到OSS - * - * 2. 后端上传(backend): - * - 优点:兼容现有功能,所有上传逻辑由后端统一处理 - * - 缺点:增加服务器负载,上传速度相对较慢 - * - 实现:调用uploadFile API,通过后端服务器上传 - * - * 兼容性处理: - * - 两种上传方式返回的数据结构保持一致:{ url: string } - * - 确保无论使用哪种方式,组件的行为都是一致的 - * - 如果上传失败,会更新文件状态为error,并调用onError回调 - * - * @param options 上传选项,包含file、onProgress、onSuccess、onError等 - */ const customRequest = async (options: any) => { const { file, onProgress, onSuccess, onError } = options; const actualFile = resolveUploadFile(file as File); - // 创建上传中的文件项,用于在UI中显示上传状态 const uploadingFile: FileItem = { uid: file.uid, name: file.name, @@ -111,107 +85,80 @@ const customRequest = async (options: any) => { url: '', }; - // 将上传中的文件添加到文件列表 fileList.value = [...fileList.value, uploadingFile]; try { - // 从preferences中读取上传方式配置 - // uploadMethod可能的值:'direct'(OSS直传)或 'backend'(后端上传) const uploadMethod = preferences.app.uploadMethod || 'direct'; + const res = + uploadMethod === 'direct' + ? await uploadToOss({ + file: actualFile, + onProgress: (percent) => { + if (onProgress) { + onProgress(percent); + } + }, + }) + : await uploadFile({ file: actualFile }); - let res: { url: string }; - - // 根据配置选择上传方式 - if (uploadMethod === 'direct') { - // OSS直传模式:文件直接从浏览器上传到OSS - // uploadToOss函数会: - // 1. 从后端获取OSS签名信息 - // 2. 生成全局唯一的文件名(UUID + 时间戳) - // 3. 使用FormData构造POST请求直接上传到OSS - // 4. 处理上传进度和错误 - // 5. 返回上传后的文件URL - res = await uploadToOss({ - file: actualFile, - onProgress: (percent) => { - // 将OSS上传进度传递给组件 - // percent范围:0-100,表示上传百分比 - if (onProgress) { - onProgress(percent); - } - }, - }); - } else { - // 后端上传模式:文件先上传到后端服务器,再由后端上传到OSS - // uploadFile函数会: - // 1. 将文件发送到后端API(/upload/image) - // 2. 后端接收文件后上传到OSS - // 3. 返回上传后的文件URL - res = await uploadFile({ - file: actualFile, - }); - } - - // 上传成功,更新文件状态为完成 - // 将上传结果中的URL赋值给文件项 - const updatedFileList = fileList.value.map(item => - item.uid === file.uid - ? { ...item, status: 'done' as const, url: res.url } - : item + fileList.value = fileList.value.map((item) => + item.uid === file.uid ? { ...item, status: 'done' as const, url: res.url } : item, ); - - fileList.value = updatedFileList; - - // 更新组件的modelValue,触发父组件的更新 updateModelValue(); - - // 调用成功回调,通知上传组件上传已完成 onSuccess(res); } catch (error) { - // 上传失败,记录错误信息 console.error('上传失败', error); - - // 更新文件状态为错误 - // 错误状态的文件会在UI中显示错误图标 - const updatedFileList = fileList.value.map(item => - item.uid === file.uid - ? { ...item, status: 'error' as const } - : item + fileList.value = fileList.value.map((item) => + item.uid === file.uid ? { ...item, status: 'error' as const } : item, ); - - fileList.value = updatedFileList; - - // 调用错误回调,通知上传组件上传失败 onError(error); } }; -// 处理文件删除 const handleRemove = (file: UploadFile) => { fileList.value = fileList.value.filter((item) => item.uid !== file.uid); updateModelValue(); }; -// 预览功能 const previewVisible = ref(false); const previewImage = ref(''); const previewTitle = ref(''); +function onGallerySelect(urls: string[]) { + const remain = props.maxCount - fileList.value.length; + const picked = props.multiple ? urls.slice(0, remain) : urls.slice(0, 1); + for (const url of picked) { + if (fileList.value.some((f) => f.url === url)) { + continue; + } + fileList.value = [ + ...fileList.value, + { + uid: url, + name: url.split('/').pop() || 'file', + status: 'done' as const, + url, + }, + ]; + } + updateModelValue(); +} + const handlePreview = async (file: UploadFile) => { previewImage.value = file.url || ''; previewVisible.value = true; previewTitle.value = file.name || file.url?.split('/').pop() || ''; }; -// 计算是否显示上传按钮 const showUploadButton = computed(() => - props.multiple - ? fileList.value.length < props.maxCount - : fileList.value.length === 0, + props.multiple ? fileList.value.length < props.maxCount : fileList.value.length === 0, ); + +const galleryRemainCount = computed(() => Math.max(props.maxCount - fileList.value.length, 0));