From 4c245f0d1f05f32851a73802f1b1f43457bfb4c4 Mon Sep 17 00:00:00 2001 From: duanshuwen Date: Thu, 27 Aug 2026 09:08:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin,=20admin-ui):=20=E5=AE=9E=E7=8E=B0Ru?= =?UTF-8?q?oYi=E9=A3=8E=E6=A0=BC=E5=90=8E=E5=8F=B0=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E7=AE=A1=E7=90=86=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增服务端菜单校验与归一化逻辑,完善菜单相关Schema;前端实现条件渲染的菜单编辑器,支持搜索式图标选择器与父级树过滤。新增媒体URL统一处理工具修复管理端本地静态资源路径映射问题,更新全部相关文档、技术决策记录与测试用例。本次变更不影响WonderQ-MiniAPP端。 --- .gitignore | 1 + WonderQ-Admin-UI-Vue/README.md | 3 + .../src/components/layout/LayoutIcon.vue | 86 ++++++++++++ .../components/media/SingleMediaEditor.vue | 15 ++- .../system/SystemResourceEditor.vue | 127 ++++++++++++++++-- .../components/wanfa/ImageGalleryEditor.vue | 3 +- .../src/lib/media-url.test.ts | 21 +++ WonderQ-Admin-UI-Vue/src/lib/media-url.ts | 19 +++ WonderQ-Admin-UI-Vue/src/lib/menu-display.ts | 56 +++++++- .../src/lib/system-menu-form.test.ts | 42 ++++++ .../src/lib/system-menu-form.ts | 61 +++++++++ .../src/pages/ConciergePage.vue | 3 +- WonderQ-Admin-UI-Vue/src/pages/HomePage.vue | 3 +- WonderQ-Admin-UI-Vue/src/pages/MediaPage.vue | 3 +- WonderQ-Admin-UI-Vue/src/pages/WanfaPage.vue | 3 +- WonderQ-Admin-UI-Vue/src/styles.css | 11 ++ WonderQ-Admin/README.md | 2 + WonderQ-Admin/app/menu_rules.py | 54 ++++++++ WonderQ-Admin/app/rbac.py | 3 + WonderQ-Admin/app/routers/system.py | 60 +++++++-- WonderQ-Admin/app/schemas.py | 16 +++ WonderQ-Admin/app/serializers.py | 2 +- WonderQ-Admin/tests/test_admin_rbac.py | 2 + WonderQ-Admin/tests/test_media_urls.py | 2 + WonderQ-Admin/tests/test_system.py | 22 +++ docs/README.md | 2 +- docs/admin-api-requirements.md | 14 ++ docs/decisions/0003-admin-menu-editor.md | 25 ++++ 28 files changed, 625 insertions(+), 36 deletions(-) create mode 100644 WonderQ-Admin-UI-Vue/src/lib/media-url.test.ts create mode 100644 WonderQ-Admin-UI-Vue/src/lib/media-url.ts create mode 100644 WonderQ-Admin-UI-Vue/src/lib/system-menu-form.test.ts create mode 100644 WonderQ-Admin-UI-Vue/src/lib/system-menu-form.ts create mode 100644 WonderQ-Admin/app/menu_rules.py create mode 100644 docs/decisions/0003-admin-menu-editor.md diff --git a/.gitignore b/.gitignore index 057e908..80d0967 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +**/public/ apps/*/dist/ .pnpm-store .env diff --git a/WonderQ-Admin-UI-Vue/README.md b/WonderQ-Admin-UI-Vue/README.md index 1c2082c..be84eaf 100644 --- a/WonderQ-Admin-UI-Vue/README.md +++ b/WonderQ-Admin-UI-Vue/README.md @@ -13,6 +13,8 @@ yarn dev 默认通过 Vite `/api` 代理访问 `http://localhost:4000`,可使用 `VITE_API_PROXY_TARGET` 覆盖。生产构建使用 `yarn build`,静态产物由反向代理挂载到 `/admin`。 +管理端会将后端返回的本地兜底图片 `/assets/...` 映射到管理端静态资源 `/admin/assets/...`;首页配置所需的本地兜底图片已随管理端构建提供。OSS 图片仍使用后端返回的临时可访问 URL。 + ## 认证约定 - Access Token 只保存在运行时内存,不写入 localStorage。 @@ -20,6 +22,7 @@ yarn dev - 401 会自动调用 `/api/admin/auth/refresh` 并重试一次;刷新失败后返回登录页。 - 菜单和按钮权限来自 `/api/admin/system/profile`,前端只加载预注册组件。 - 管理端界面图标统一使用 `@element-plus/icons-vue`;菜单图标通过 `src/components/layout/LayoutIcon.vue` 的白名单适配器映射,未知图标回退为默认图标。 +- 菜单管理按 RuoYi 风格提供目录、页面、按钮三类编辑字段;父级菜单使用服务端树,排除按钮和当前编辑分支,图标选择器支持搜索、清空和 Element Plus 图标网格。 - `ConciergeDetail.icon` 等接口字段是业务内容数据,不属于 UI 图标,不在图标迁移范围内。 ## 验证 diff --git a/WonderQ-Admin-UI-Vue/src/components/layout/LayoutIcon.vue b/WonderQ-Admin-UI-Vue/src/components/layout/LayoutIcon.vue index a84f50d..4e91331 100644 --- a/WonderQ-Admin-UI-Vue/src/components/layout/LayoutIcon.vue +++ b/WonderQ-Admin-UI-Vue/src/components/layout/LayoutIcon.vue @@ -3,20 +3,63 @@ import { computed, type Component } from "vue"; import { ArrowDown, Back as BackIcon, + Bell, + Calendar, + ChatLineRound, + CircleCheck, + CircleClose, + Clock, Close as CloseIcon, Collection as CollectionIcon, + CollectionTag, + Connection, + DataAnalysis, + DataBoard, + Delete, + Document, + Download, + Edit, FullScreen as FullScreenIcon, + Folder, + FolderOpened, Grid as GridIcon, Guide, + Help, + House, + Key, + List, + Location, + MapLocation, Menu as MenuIcon, + Message, + Monitor, Moon as MoonIcon, More as MoreIcon, + Notification, + Odometer, + Operation, + Paperclip, + Picture, + PieChart, + Plus, + Rank, + Reading, Refresh as RefreshIcon, Right as RightIcon, Search as SearchIcon, Setting as SettingIcon, + Share, + ShoppingBag, Sunny as SunnyIcon, + Tickets, + Tools, + TrendCharts, + Upload, User as UserIcon, + UserFilled, + View, + Warning, + ZoomIn, } from "@element-plus/icons-vue"; import type { LayoutIconName } from "@/lib/menu-display"; @@ -46,6 +89,49 @@ const iconRegistry: Record = { Back: BackIcon, Right: RightIcon, Chevron: ArrowDown, + Bell, + Calendar, + ChatLineRound, + CircleCheck, + CircleClose, + Clock, + CollectionTag, + Connection, + DataAnalysis, + DataBoard, + Delete, + Document, + Download, + Edit, + Folder, + FolderOpened, + Help, + House, + Key, + List, + Location, + MapLocation, + Message, + Monitor, + Notification, + Odometer, + Operation, + Paperclip, + Picture, + PieChart, + Plus, + Rank, + Reading, + Share, + ShoppingBag, + Tickets, + Tools, + TrendCharts, + Upload, + UserFilled, + View, + Warning, + ZoomIn, }; const iconComponent = computed(() => iconRegistry[props.name] ?? MenuIcon); diff --git a/WonderQ-Admin-UI-Vue/src/components/media/SingleMediaEditor.vue b/WonderQ-Admin-UI-Vue/src/components/media/SingleMediaEditor.vue index e0f33fc..fec288e 100644 --- a/WonderQ-Admin-UI-Vue/src/components/media/SingleMediaEditor.vue +++ b/WonderQ-Admin-UI-Vue/src/components/media/SingleMediaEditor.vue @@ -2,6 +2,7 @@ import { ref } from "vue"; import { uploadMediaAsset } from "@/api/client"; import { validateMediaFile } from "@/lib/media"; +import { resolveAdminMediaUrl } from "@/lib/media-url"; const props = defineProps<{ modelValue: string; @@ -13,6 +14,11 @@ const emit = defineEmits<{ (event: "update:modelValue", value: string): void }>( const input = ref(null); const uploading = ref(false); const error = ref(""); +const previewError = ref(false); + +function previewUrl(value: string) { + return resolveAdminMediaUrl(value); +} function openPicker() { input.value?.click(); } @@ -30,6 +36,7 @@ async function upload(event: Event) { error.value = ""; try { const asset = await uploadMediaAsset(file, props.group); + previewError.value = false; emit("update:modelValue", asset.url); } catch (uploadError) { error.value = uploadError instanceof Error ? uploadError.message : "图片上传失败,请重试"; @@ -38,7 +45,10 @@ async function upload(event: Event) { } } -function clear() { emit("update:modelValue", ""); } +function clear() { + previewError.value = false; + emit("update:modelValue", ""); +}