From 5a19a4ed7945f2d706315cb18f7c36c6d095697b Mon Sep 17 00:00:00 2001 From: chenxing Date: Sun, 11 Oct 2026 21:40:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=B0=8F=E7=BB=84=E4=BB=B6=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=94=B9=E7=89=88):=20=E5=8D=A1=E7=89=87=E7=BD=91?= =?UTF-8?q?=E6=A0=BC=E7=AE=A1=E7=90=86=E5=8F=B0=20+=20=E6=A0=A1=E7=BA=A7?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E5=B1=82=20+=20=E9=80=80=E5=BD=B9=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E8=AE=BE=E7=BD=AE/=E5=BA=94=E7=94=A8=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端 - widget_catalog 加 3 列:scope_type(三态可见范围)/ role_types(可见角色)/ role_alias(角色专属命名 JSON) - 新增两表:widget_catalog_school(点名表,uk_widget_school)、widget_school_setting(校级覆盖层,uk_widget_sch) - WidgetController 新增 GetWidgetCatalogList / SaveWidgetCatalog / DeleteWidgetCatalog / GetWidgetSchoolSetting / SaveWidgetSchoolSetting;权限拆 checkSuperAdmin(仅超管)与 checkSchAdmin(超管或 roleId∈{2,5,7}) - SaveWidgetSchoolSetting 强制 schId 取当前登录校,前端传参不采信(防越权) - 显示名三级链:校级别名 > 角色专属命名 > 组件名;role_alias 坏 JSON 降级不抛 - 启动期幂等 seeding(空库才写 3 条内置目录行) - 手工 DDL 脚本 resources/sql/widget_catalog_v2.sql(含回滚) 前端 - WidgetManage 重写为卡片网格:超管/校管双视角、类型页签 + 搜索、拖拽排序、启停、 删除、登记内置组件、空态引导 - 新增 WidgetCard / WidgetEditModal / WidgetRegisterModal - UserSettingApi 加 6 个方法;types 加 WidgetCatalog 接口 - 管理端列表整行回传(SaveWidgetCatalog 为整行 merge) 退役 - 删除 SysSetting.vue / SysTempManage.vue / AppSettingApi.ts 与后端 AppSettingController / AppCategory(SQL) 全链路,清理路由、Settings 导航与样式分支 - 保留 SysSettingApi.ts(壁纸/logo 池管理仍在使用) --- src/api/AppSettingApi.ts | 63 -- src/api/UserSettingApi.ts | 31 +- src/components/Header.vue | 5 - src/router/routes.ts | 14 - src/types/index.ts | 30 + src/views/Settings.vue | 42 +- src/views/SysSetting.vue | 331 ------- src/views/SysTempManage.vue | 235 ----- src/views/msg/index.vue | 2 +- src/views/widget/WidgetCard.vue | 403 ++++++++ src/views/widget/WidgetEditModal.vue | 1143 ++++++++++++++++++++++ src/views/widget/WidgetManage.vue | 942 +++++++++++------- src/views/widget/WidgetRegisterModal.vue | 342 +++++++ 13 files changed, 2559 insertions(+), 1024 deletions(-) delete mode 100644 src/api/AppSettingApi.ts delete mode 100644 src/views/SysSetting.vue delete mode 100644 src/views/SysTempManage.vue create mode 100644 src/views/widget/WidgetCard.vue create mode 100644 src/views/widget/WidgetEditModal.vue create mode 100644 src/views/widget/WidgetRegisterModal.vue diff --git a/src/api/AppSettingApi.ts b/src/api/AppSettingApi.ts deleted file mode 100644 index 562ba08..0000000 --- a/src/api/AppSettingApi.ts +++ /dev/null @@ -1,63 +0,0 @@ -import Abstract from "@/utils/axios/abstract"; -import { PageData, PageParams } from "@/types/Basics"; - -const path = "/api"; - -export interface AppCategoryItem { - id?: number; - name: string; - apiUrl: string; - dataKey: string; - paramsKey: string; - roleType: number; - cId?: number; - cDate?: string; - resDate?: string; -} - -/** 系统设置-应用管理(应用类别 CRUD + 模板管理) */ -class AppSettingApi extends Abstract { - /** 应用分页列表 */ - getAppCategoryList(params: PageParams & { roleType: number; name?: string }) { - const url = path + "/GetAppCategoryList"; - return this.getRequest({ url, params }); - } - - /** 保存应用(新增/编辑) */ - saveAppCategory(data: AppCategoryItem) { - const url = path + "/SaveAppCategory"; - return this.postRequest({ url, data }); - } - - /** 删除应用 */ - deleteAppCategory(id: number) { - const url = path + "/DelAppCategory"; - return this.postRequest({ url, params: { id } }); - } - - /** 查询应用详情 */ - findAppCategory(id: number) { - const url = path + "/FindAppCategory"; - return this.getRequest({ url, params: { id } }); - } - - /** 刷新资源时间戳(上传模板后调用) */ - updateResDate(id: number) { - const url = path + "/UpdateResDate"; - return this.postRequest({ url, params: { id } }); - } - - /** 模板文件列表 */ - getTempList(appModId: number) { - const url = path + "/GetTempList"; - return this.getRequest({ url, params: { appModId } }); - } - - /** 保存模板文件内容(JSON body,避免大文件内容超出 URL/请求头长度限制) */ - saveTempFile(appModId: number, fileName: string, content: string) { - const url = path + "/SaveTempFile"; - return this.postRequest({ url, data: { appModId, fileName, content } }); - } -} - -export default new AppSettingApi(); diff --git a/src/api/UserSettingApi.ts b/src/api/UserSettingApi.ts index 89f0109..70a12b8 100644 --- a/src/api/UserSettingApi.ts +++ b/src/api/UserSettingApi.ts @@ -1,4 +1,4 @@ -import { DesktopAppTypeEnum, LightApp, UserSettingInfo, WidgetPositionResp } from "@/types"; +import { DesktopAppTypeEnum, LightApp, UserSettingInfo, WidgetCatalog, WidgetPositionResp } from "@/types"; import { BaseDict, PageData, PageParams } from "@/types/Basics"; import Abstract from "@/utils/axios/abstract"; @@ -107,13 +107,38 @@ class UserSettings extends Abstract { getWidgetCatalogList() { const url = path + "/GetWidgetCatalogList"; - return this.getRequest({ url }); + return this.getRequest({ url }); } - saveWidgetCatalog(catalog: Record) { + /** 新增/编辑全局目录(仅超管):scopeType/roleTypes/roleAlias/scopeSchIds 一并提交 */ + saveWidgetCatalog(catalog: Partial & Record) { const url = path + "/SaveWidgetCatalog"; return this.postRequest({ url, data: catalog }); } + + /** 删除全局目录(仅超管),后端连带删点名表与校级覆盖行 */ + deleteWidgetCatalog(id: number) { + const url = path + "/DeleteWidgetCatalog"; + return this.postRequest({ url, params: { id } }); + } + + /** 读本校覆盖行(编辑回显):schId 由后端取当前登录校 */ + getWidgetSchoolSetting(widgetId: number) { + const url = path + "/GetWidgetSchoolSetting"; + return this.getRequest({ url, params: { widgetId } }); + } + + /** 保存本校覆盖(校管/超管):schId 由后端强制取当前登录校,前端不传 */ + saveWidgetSchoolSetting(setting: Record) { + const url = path + "/SaveWidgetSchoolSetting"; + return this.postRequest({ url, data: setting }); + } + + /** 机构树(仅超管):递归裁剪到学校层,children 即学校列表,用于可见范围多选 */ + getSchTree() { + const url = path + "/getSchTree"; + return this.getRequest({ url }); + } } export default new UserSettings(); diff --git a/src/components/Header.vue b/src/components/Header.vue index f698f74..f4d65d5 100644 --- a/src/components/Header.vue +++ b/src/components/Header.vue @@ -613,11 +613,6 @@ async function toggleWidgetShow() { await UserSettingApi.toggleWidgetShow(id); } -function linkSysSetting() { - isUserCardShow.value = false; - router.push("/syssetting"); -} - function linkBackendManage() { isUserCardShow.value = false; const url = `${$conf.baseHost}/userbase/front/index.html`; diff --git a/src/router/routes.ts b/src/router/routes.ts index 5f07ed3..4b5b794 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -7,8 +7,6 @@ import LightApp from "@/views/LightApp.vue"; import Stats from "@/views/Stats.vue"; import StatsRelay from "@/views/StatsRelay/index.vue"; import TeacherCopilot from "@/views/TeacherCopilot.vue"; -import SysSetting from "@/views/SysSetting.vue"; -import SysTempManage from "@/views/SysTempManage.vue"; const routes: RouteRecordRaw[] = [ { @@ -59,18 +57,6 @@ const routes: RouteRecordRaw[] = [ component: TeacherCopilot, meta: { title: "教师助手" }, }, - { - path: "/syssetting", - name: "SysSetting", - component: SysSetting, - meta: { title: "系统设置" }, - }, - { - path: "/syssetting/tempmanage/:id", - name: "SysTempManage", - component: SysTempManage, - meta: { title: "模板管理" }, - }, { path: "/msg", name: "Msg", diff --git a/src/types/index.ts b/src/types/index.ts index bda2e9c..2235940 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -51,6 +51,36 @@ export interface WidgetPositionResp extends WidgetPosition { schId?: number; } +/** 小组件候选池目录行(widget_catalog + 管理端视角附加字段) */ +export interface WidgetCatalog { + id?: number; + /** 组件键:内置 = 前端 widgetRegistry 的键;自定义 = 自定义标识 */ + code: string; + /** 显示名(校管视角已按「校级别名 > 角色命名 > 名称」合成) */ + name: string; + /** 1 = 前端内置组件 2 = 自定义 HTML 片段 */ + compType: number; + content?: string | null; + dataCode?: string | null; + /** 下发范围:0 = 全部学校 1 = 白名单 2 = 黑名单 */ + scopeType?: number; + /** 点名的学校 id 列表(scopeType=1/2 时有效) */ + scopeSchIds?: number[]; + /** 可见角色:逗号分隔 RoleTypeEnum;NULL/空 = 全角色 */ + roleTypes?: string | null; + /** 角色专属命名 JSON 文本:{"2":"全校课表"} */ + roleAlias?: string | null; + /** 校管视角:本校别名 */ + alias?: string | null; + /** 校管视角:超管下发的全局名称 */ + originalName?: string | null; + /** 校管视角:本校是否已有覆盖行 */ + covered?: boolean; + sort?: number; + enabled?: number; + updateTime?: string | null; +} + export interface BschTransferRoleInfo { roleInfo: RoleInfo; ticket: string; diff --git a/src/views/Settings.vue b/src/views/Settings.vue index a96a6b5..55087ba 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -22,13 +22,8 @@ - -
- -
- - -
+ +
@@ -722,7 +717,6 @@ import { ref, reactive, computed, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { useDefaultStore } from "@/store"; import useCurrentInstance from "@/utils/hook/useCurrentInstance"; -import SysSetting from "@/views/SysSetting.vue"; import WidgetManage from "@/views/widget/WidgetManage.vue"; import BaseApi from "@/api/BaseApi"; import SysSettingApi from "@/api/SysSettingApi"; @@ -747,7 +741,7 @@ import { Message } from "view-ui-plus"; * profile 读 store.userInfo(手机/邮箱本人可改:UpdateContact 直写 UserBase,2026-10-02 任务B); * password 走 updatePassword + 确认登出流程(照抄 UserInfoModal); * avatar 移植 UserInfoModal 直传上传(「使用默认头像」保持占位,存储方案本轮不做); - * logo/wallpaper 池管理(列表 + 直传 + 删除)照抄 SysSetting.vue,LOGO 保存走 saveSchLogo(schId 后端按登录人推断); + * logo/wallpaper 池管理(列表 + 直传 + 删除)照抄旧系统设置页管理语义,LOGO 保存走 saveSchLogo(schId 后端按登录人推断); * 壁纸「点选即生效」的 checkWallpaper 个人语义只保留在 my 层,默认层只维护可选池(不再代用户指定)。 */ @@ -757,13 +751,13 @@ const store = useDefaultStore(); const { $conf } = useCurrentInstance(); const isAdmin = computed(() => !!store.getIsAdmin); -/** 超管判据与 SysSetting.vue 的 isSuperAdmin 同源(userInfo.type==1);徽标三态用 */ +/** 超管判据 = userInfo.type==1;徽标三态用 */ const isSuper = computed(() => store.userInfo?.type == 1); /** 普通用户不可用的管理 pane(导航隐藏;URL 直达渲染 noperm 黄条,不白屏不报错)。 * 三层化(S4)后「头像/主题/壁纸」所有身份都可用——每角色只编辑自己那一层,不再是管理项 */ -const ADMIN_PANES = ["logo", "sidebar", "sys", "statset", "widget"]; -const ALL_PANES = ["profile", "password", "avatar", "theme", "wallpaper", "desktop", "logo", "sidebar", "sys", "statset", "widget", "noperm"]; +const ADMIN_PANES = ["logo", "sidebar", "statset", "widget"]; +const ALL_PANES = ["profile", "password", "avatar", "theme", "wallpaper", "desktop", "logo", "sidebar", "statset", "widget", "noperm"]; /** 历史 ?pane= 键兼容(S4 重命名:mytheme/schooltheme→theme、mywallpaper→wallpaper),保证旧链接不 404/不白屏 */ const LEGACY_PANE_KEYS: Record = { @@ -793,8 +787,6 @@ watch(curPane, pane => { }); const isDenied = computed(() => !isAdmin.value && ADMIN_PANES.includes(curPane.value)); -/** sys pane 内嵌旧路由页 SysSetting(管理员态自带机构树+内容双卡) */ -const showSys = computed(() => isAdmin.value && curPane.value === "sys"); function setPane(key: string) { curPane.value = key; @@ -823,7 +815,6 @@ const NAV_PORTAL: NavItem[] = [ { key: "sidebar", name: "边栏设置", icon: svgWrap('') }, ]; const NAV_SYSTEM: NavItem[] = [ - { key: "sys", name: "系统设置", icon: svgWrap('') }, { key: "statset", name: "统计设置", icon: svgWrap('') }, { key: "widget", name: "小组件管理", icon: svgWrap('') }, ]; @@ -984,7 +975,7 @@ function onAvatarFormatError() { Message.error("仅支持 jpg/jpeg/png/gif/webp 格式图片!"); } -/* ===== logo / wallpaper:管理列表 + 直传 + 删除(照抄 SysSetting.vue 管理语义) ===== */ +/* ===== logo / wallpaper:管理列表 + 直传 + 删除(沿用旧系统设置页管理语义) ===== */ const logoManageList = ref([]); const wallpaperManageList = ref([]); const logoSelUrl = ref(""); @@ -2029,7 +2020,7 @@ watch( diff --git a/src/views/SysTempManage.vue b/src/views/SysTempManage.vue deleted file mode 100644 index 0c88563..0000000 --- a/src/views/SysTempManage.vue +++ /dev/null @@ -1,235 +0,0 @@ - - - - - diff --git a/src/views/msg/index.vue b/src/views/msg/index.vue index 6ba3005..59f99cf 100644 --- a/src/views/msg/index.vue +++ b/src/views/msg/index.vue @@ -429,7 +429,7 @@ function fmtTime(sendTime?: string) { diff --git a/src/views/widget/WidgetEditModal.vue b/src/views/widget/WidgetEditModal.vue new file mode 100644 index 0000000..06d20dc --- /dev/null +++ b/src/views/widget/WidgetEditModal.vue @@ -0,0 +1,1143 @@ + + + + + diff --git a/src/views/widget/WidgetManage.vue b/src/views/widget/WidgetManage.vue index 879f035..ac48b59 100644 --- a/src/views/widget/WidgetManage.vue +++ b/src/views/widget/WidgetManage.vue @@ -1,365 +1,651 @@ +
-

- 内置组件的内容在 front/src/components/widgets/ 里,此处只登记键与展示名,编辑器置灰; - 上传走 FileReader 读成文本填入编辑器,不落地成文件、不产生任何 URL。 -

- +
+ ℹ️ + 可见范围三态(超管专属):全部学校 / 指定学校可见(白名单,只有选中的学校下发)/ + 指定学校不可见(黑名单,除选中学校外都下发)。
校级可覆盖项(校管理员):启用 / 可见角色 / 排序 / 别名。
+
+ + +
diff --git a/src/views/widget/WidgetRegisterModal.vue b/src/views/widget/WidgetRegisterModal.vue new file mode 100644 index 0000000..3e5753e --- /dev/null +++ b/src/views/widget/WidgetRegisterModal.vue @@ -0,0 +1,342 @@ + + + + +