feat(t10-admin): 管理员体系前端——应用设置双 tab+单表单管理员+选人组件移植
build / build (push) Successful in 3m8s

- AppSettings.vue 应用设置页(基础/字典两 tab);修复 P86:Tabs 写 :value 而受控 prop 是 modelValue,致 ?tab=dicts 深链静默失效,改后深链实拍生效
- UserCheckModal.vue 自 notice 移植(view-ui-plus+ztree,sources 注入式数据源);public/libs 收录 jquery+ztree 本地件(index.html 引用)
- api/module/admin.ts + mock/adminapi.ts + types/admin.ts;AdminListPanel/FormAdminModal 组件
- routes/menuNav/menuRoute/modulePerms 适配管理员菜单与权限位;tree-icon 图标资源
This commit is contained in:
chenxing
2026-10-15 20:25:39 +08:00
parent 14d45f7e3c
commit d8f1a1fbfa
30 changed files with 1377 additions and 33 deletions
+94
View File
@@ -0,0 +1,94 @@
/**
* 管理员两表的本地 Mock(vite-plugin-mock,仅 `npm run dev:mock` 生效)。
*
* 用途与背景同 baseapi.ts:本机没有 UserBase 授权数据(执行手册 §5 P1 降级取证先例),
* 角色设置 tab 与「表单管理员」弹窗的两表 CRUD 往返只能在 mock 里实拍。
* 桩刻意把后端三条规则也演一遍,好让冒烟脚本能断言到"前端没有把规则写丢":
* ① 重复添加幂等(不重复建行、不报错);② 校级管理员删到剩 0 拒;③ 写操作回全量列表。
*/
import type { AdminRow } from "@/types/admin";
const ok = (data: unknown) => ({ code: 0, msg: "", data });
const fail = (msg: string) => ({ code: 500, msg, data: null });
let seq = 1;
const schoolAdmins: AdminRow[] = [];
const formAdmins: Record<string, AdminRow[]> = {};
function row(userId: number, userName: string): AdminRow {
return { id: seq++, userId, userName };
}
/** 追加(幂等:已存在的 userId 跳过,姓名快照不覆盖) */
function merge(list: AdminRow[], users: { userId: number; userName: string }[]): AdminRow[] {
for (const item of users || []) {
if (!item || item.userId == null) continue;
if (!list.some((r) => r.userId === Number(item.userId))) {
list.push(row(Number(item.userId), String(item.userName || "")));
}
}
return list;
}
/** 路径里的 id / formUuid:vite-plugin-mock 不保证把 REST 段塞进 query,直接从 url 取最稳 */
function idFrom(url: string): number | null {
const matched = /\/(\d+)(?:\?|$)/.exec(url || "");
return matched ? Number(matched[1]) : null;
}
function formUuidFrom(url: string): string {
const matched = /\/forms\/([\w-]+)\/admins/.exec(url || "");
return matched ? matched[1] : "";
}
export default [
{
url: "/api/form/admin/school-admins",
method: "get",
response: () => ok(schoolAdmins.slice())
},
{
url: "/api/form/admin/school-admins",
method: "post",
response: ({ body }: any) => ok(merge(schoolAdmins, body?.users || []))
},
{
url: "/api/form/admin/school-admins/:id",
method: "delete",
response: ({ url }: any) => {
const id = idFrom(url);
if (schoolAdmins.length <= 1) return fail("本校至少保留一名校级表单管理员,最后一名不可删除");
const index = schoolAdmins.findIndex((r) => r.id === id);
if (index < 0) return fail("校级管理员记录不存在:" + id);
schoolAdmins.splice(index, 1);
return ok(schoolAdmins.slice());
}
},
{
url: "/api/form/admin/forms/:formUuid/admins",
method: "get",
response: ({ url }: any) => ok((formAdmins[formUuidFrom(url)] || []).slice())
},
{
url: "/api/form/admin/forms/:formUuid/admins",
method: "post",
response: ({ url, body }: any) => {
const uuid = formUuidFrom(url);
formAdmins[uuid] = formAdmins[uuid] || [];
return ok(merge(formAdmins[uuid], body?.users || []));
}
},
{
url: "/api/form/admin/forms/:formUuid/admins/:id",
method: "delete",
response: ({ url }: any) => {
const uuid = formUuidFrom(url);
const id = idFrom(url);
const list = formAdmins[uuid] || [];
const index = list.findIndex((r) => r.id === id);
if (index < 0) return fail("表单管理员记录不存在:" + id);
list.splice(index, 1);
return ok(list.slice());
}
}
];
+38 -17
View File
@@ -6,7 +6,7 @@
*
* 形状严格对齐后端 FormBaseController.toFrontendTree 的下发字段
* (id/modId/name/parentId/page/level/icon/orderNumber/usable/children,信封 {code,msg,data}),
* 模块行取执行手册 §2.2 的建议表(中心管理员四模块行 + 我的填写 + 数据全量权限锚点行)。
* 模块行取执行手册 §2.2 的 v1.5 现行表(中心管理员四行 + 我的填写,共五行)。
*/
import type { IModule } from "@/types/Basics";
@@ -53,9 +53,9 @@ const OWN_MODULES: IModule[] = [
{
id: 9104,
modId: 9104,
name: "字典管理",
name: "应用设置",
parentId: 0,
page: "/admin/dicts",
page: "/admin/app",
level: 1,
icon: "zidingyi",
orderNumber: 4,
@@ -74,21 +74,11 @@ const OWN_MODULES: IModule[] = [
orderNumber: 5,
usable: true,
children: []
},
{
// path 空行 = 纯权限锚点(customId=fc:scope:all):后端按授权集合扫描消费,
// 前端导航渲染必须跳过(手册 §2.2)。modId 由后端 resolveModId 回退主键 id(非数字 customId)
id: 9106,
modId: 9106,
name: "数据全量",
parentId: 0,
page: "",
level: 1,
icon: "",
orderNumber: 6,
usable: true,
children: []
}
// v1.5(T10)起中心应用就这五行:字典管理行撤(迁入 /admin/app 的页内 tab)、
// "数据全量"path 空锚点行撤(UserBase 侧 customId 权限锚点协议退役,数据范围升级改由后端管理员两表承载)。
// 本地要复现旧六行版的登录态(V46 的"不再出现字典管理导航项")请用 t10 冒烟脚本里的临时树覆盖,
// 不要把遗留行写回这份 mock——它是现行配置的镜像。
];
/**
@@ -191,5 +181,36 @@ export default [
if (target && target.toLowerCase() !== "form-center") return ok([]);
return ok(OWN_MODULES);
}
},
{
// 选人弹窗的组织树桩(T10):形状=后端 SchoolModels.Node(isParent=true 是组、false 是人员叶子)。
// id 即 UserBase 用户主键,提交给 /school-admins 与 /forms/{uuid}/admins 的就是它。
url: "/api/base/GetSchoolAll",
method: "get",
response: () =>
ok([
{
id: 3260,
name: "云海附属中学",
isParent: true,
children: [
{
id: 5001,
name: "教务处",
isParent: true,
children: [
{ id: 186532, name: "Mock 管理员", isParent: false, children: [] },
{ id: 186601, name: "Mock 教务主任", isParent: false, children: [] }
]
},
{
id: 5002,
name: "高一段",
isParent: true,
children: [{ id: 186602, name: "Mock 老师甲", isParent: false, children: [] }]
}
]
}
])
}
];