From b275ea679818e374036ac6c6a5408a877e7423f8 Mon Sep 17 00:00:00 2001 From: zg Date: Thu, 16 Jul 2026 09:54:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- memory/MEMORY.md | 1 + memory/session-20260716-1.md | 46 ++++++++ .../src/components/IconPicker.vue | 100 +++++++++++++++++- 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 memory/session-20260716-1.md diff --git a/memory/MEMORY.md b/memory/MEMORY.md index 028caf2..7dc5354 100644 --- a/memory/MEMORY.md +++ b/memory/MEMORY.md @@ -1,4 +1,5 @@ - [项目概览](project-overview.md) — 统一登录网关平台完整信息、架构、状态、已知问题 +- [2026-07-16 会话-1](session-20260716-1.md) — IconPicker 增加直接上传图片功能 - [2026-06-28 会话-1](session-20260628-1.md) — 修复登录后 /open/token 和文件下载接口重复调用 - [2026-06-09 会话](session-log.md) — 完整开发过程、决策、Bug修复、待办 - [2026-06-10 会话-1](session-20260610-1.md) — 数据库地址变更、前端环境变量整理 diff --git a/memory/session-20260716-1.md b/memory/session-20260716-1.md new file mode 100644 index 0000000..8482b70 --- /dev/null +++ b/memory/session-20260716-1.md @@ -0,0 +1,46 @@ +--- +name: session-20260716-1 +description: 2026-07-16 第1次会话 — IconPicker 增加直接上传图片功能 +metadata: + type: session + date: 2026-07-16 + sessionNumber: 1 + startTime: 2026-07-16T00:00:00+08:00 +--- + +# 2026-07-16 第1次会话 + +## 会话摘要 + +用户反馈前端图标选择器(IconPicker)缺少直接上传功能——需要先到文件管理页面上传图片,再回到表单选择。本次在 IconPicker 组件的"已上传"标签页中增加了上传按钮,支持直接在弹窗内上传图标图片,上传完成后自动选中。 + +## 关键操作 + +### IconPicker 增加上传按钮 + +- **做了什么**: 在 IconPicker.vue 的"已上传"标签页头部增加了「上传」按钮,点击后选择图片即自动上传到文件服务(fileType=icon),上传完成自动刷新列表并选中新图标 +- **涉及文件**: `mokee-gateway-web/src/components/IconPicker.vue` +- **具体改动**: + 1. 模板:在 popup-header 中添加 `` 上传按钮(仅在 tab==='upload' 时显示)+ 隐藏 `` + 2. 模板:添加 `.upload-status-bar` 上传进度条(el-progress + Loading 动画) + 3. 脚本:导入 `Upload`, `Loading` 图标和 `ElMessage` + 4. 脚本:添加 `FILE_SERVICE`, `uploading`, `uploadPercent`, `fileInputRef` 状态 + 5. 脚本:实现 `getFileServiceToken()` — 直调文件服务获取 Token(带缓存) + 6. 脚本:实现 `triggerUploadIcon()` / `onIconFileSelected()` — 选择文件后自动上传 + 7. 样式:添加 `@keyframes spin` 和 `.upload-status-bar` 样式 + +- **上传流程**: 选择图片 → 获取文件服务 Token → FormData 上传(fileType=icon)→ file.wg.zgitm.com/api/upload → 刷新列表 → 自动选中新图标 + +- **结果**: TypeScript 编译通过,IconPicker.vue 无类型错误 + +## 重要信息 + +- 文件服务 URL: `VITE_FILE_SERVICE_URL` 环境变量,默认 `https://file.wg.zgitm.com` +- 上传 API: `POST {FILE_SERVICE}/api/upload`,FormData 包含 `file` + `fileType` +- 上传 Token: `POST {FILE_SERVICE}/api/token`,body: `{ systemCode: 'gateway' }` +- 图标列表查询: `GET /zgapi/v1/admin/file/list?fileType=icon` +- 文件服务直写 `sys_file` 表,admin 后端只做元信息查询 + +## 待处理 + +- [ ] 通知用户确认前端部署后功能正常 diff --git a/mokee-gateway-web/src/components/IconPicker.vue b/mokee-gateway-web/src/components/IconPicker.vue index 3e1aba5..183927b 100644 --- a/mokee-gateway-web/src/components/IconPicker.vue +++ b/mokee-gateway-web/src/components/IconPicker.vue @@ -18,6 +18,10 @@ 内置图标 已上传 + + 上传 + + {{ filtered.length }} 个 @@ -37,6 +41,11 @@ +
+ + 正在上传图标... + +
import { ref, computed, onMounted } from 'vue' -import { ArrowDown, Close } from '@element-plus/icons-vue' +import { ArrowDown, Close, Upload, Loading } from '@element-plus/icons-vue' +import { ElMessage } from 'element-plus' import * as Icons from '@element-plus/icons-vue' import request from '@/utils/request' @@ -71,6 +81,12 @@ const search = ref('') const tab = ref('builtin') const uploadedIcons = ref<{id:string, fileName:string}[]>([]) +// 上传相关状态 +const FILE_SERVICE = import.meta.env.VITE_FILE_SERVICE_URL || 'https://file.wg.zgitm.com' +const uploading = ref(false) +const uploadPercent = ref(0) +const fileInputRef = ref() + const gatewayUrl = import.meta.env.VITE_GATEWAY_URL || '' function isUrl(v: string) { return v && (v.startsWith('http') || v.startsWith('/zgapi')) } @@ -101,6 +117,77 @@ async function fetchUploadedIcons() { } catch {} } +/** 获取文件服务上传 Token(直调文件服务,不走网关) */ +let cachedFileToken = '' +async function getFileServiceToken(): Promise { + if (cachedFileToken) return cachedFileToken + try { + const axios = (await import('axios')).default + const res = await axios.post(`${FILE_SERVICE}/api/token`, { systemCode: 'gateway' }, { timeout: 10000 }) + const token = res.data?.data?.token || '' + if (token) cachedFileToken = token + return token + } catch { return '' } +} + +/** 触发文件选择器 */ +function triggerUploadIcon() { + fileInputRef.value?.click() +} + +/** 文件选择后立即上传 */ +async function onIconFileSelected(e: Event) { + const files = (e.target as HTMLInputElement).files + if (!files || files.length === 0) return + const file = files[0] + // 重置 input 以允许重复选择同一文件 + if (fileInputRef.value) fileInputRef.value.value = '' + + uploading.value = true + uploadPercent.value = 0 + + try { + // 1. 获取上传 Token + const token = await getFileServiceToken() + if (!token) { + ElMessage.error('获取上传 Token 失败') + return + } + + // 2. 上传到文件服务(指定 fileType=icon) + const axios = (await import('axios')).default + const fd = new FormData() + fd.append('file', file) + fd.append('fileType', 'icon') + const uploadRes = await axios.post(`${FILE_SERVICE}/api/upload`, fd, { + timeout: 120000, + headers: { Authorization: 'Bearer ' + token }, + onUploadProgress: (e: any) => { + if (e.total > 0) uploadPercent.value = Math.round((e.loaded / e.total) * 100) + }, + }) + + // Token 用完即清 + cachedFileToken = '' + + // 3. 刷新已上传图标列表 + await fetchUploadedIcons() + + // 4. 自动选中新上传的图标 + const fileId = uploadRes.data?.data?.fileId + if (fileId) { + select(getFileUrl(fileId)) + } + ElMessage.success('图标上传成功') + } catch { + cachedFileToken = '' + ElMessage.error('图标上传失败') + } finally { + uploading.value = false + uploadPercent.value = 0 + } +} + function select(val: string) { emit('update:modelValue', val) visible.value = false @@ -159,10 +246,21 @@ onMounted(fetchUploadedIcons) /* 触发器中的图片预览 */ .trigger-img { width:20px; height:20px; object-fit:contain; border-radius:3px; } +/* 上传进度条 */ +.upload-status-bar { + display: flex; align-items: center; gap: 10px; + padding: 12px 20px; margin: 0 20px; + background: var(--color-primary-bg, #eef2ff); border-radius: var(--radius-md); + font-size: 13px; color: var(--color-primary); +} +.upload-status-bar .is-loading { animation: spin 1s linear infinite; } + /* 已上传图标 */ .uploaded-grid { grid-template-columns: repeat(8, 1fr); } .upload-icon-thumb { width:48px; height:48px; object-fit:contain; border-radius:var(--radius-sm); } +@keyframes spin { to { transform: rotate(360deg); } } + @media (max-width:960px) { .icon-popup { width:98vw; max-height:90vh; } .icon-grid { grid-template-columns: repeat(6, 1fr); }