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
+161
View File
@@ -0,0 +1,161 @@
<script setup lang="ts">
/**
* 管理员名单面板(T10 方案 §6.3「图二形态」:序号 / 姓名 / 操作-删除 + 添加按钮 → 选人弹窗右栏穿梭确认)。
*
* 一处实现两处用:
* - `mode="school"`:应用设置 · 角色设置 tab → `school_admin`(校级表单管理员,守 /admin/app);
* - `mode="form"`:表单列表 / 设计器的「管理员」弹窗 → `form_admin`(单表单管理员,守 /admin/forms)。
*
* 三条与直觉不同的地方:
* 1. **写操作回的是操作后的全量列表**(后端不分页,每校/每表单个位数),所以这里不再自己拼增量;
* 2. **防呆与跨校拒绝在服务端**(最后一员不可删、名单里的人必须属于本次域)——前端不做"只剩一个就禁用删除",
* 那会把后端的规则复制成两份并且必然漂移;失败就按信封 msg 提示;
* 3. **选人数据源=本校组织树**(`/api/base/GetSchoolAll`,无 schId 入参),跨校的人根本选不出来;
* 即便有人手搓请求体,落库前的归属复核仍会拒。
*/
import { computed, onMounted, ref } from "vue";
import { Message, Modal } from "view-ui-plus";
import { adminApi } from "@/api/module/admin";
import { baseApi } from "@/api/module/base";
import UserCheckModal from "@/components/UserCheckModal.vue";
import type { AdminRow } from "@/types/admin";
const props = defineProps<{
/** school=校级表单管理员;form=单表单管理员 */
mode: "school" | "form";
/** mode=form 时必填:授权落到哪张表单(对外身份是 uuid,契约 §1) */
formUuid?: string;
}>();
const rows = ref<AdminRow[]>([]);
const loading = ref(false);
const busy = ref(false);
const checkRef = ref<InstanceType<typeof UserCheckModal> | null>(null);
/** 弹窗里已勾的人(重开时回显,避免"加了三个再点进去只剩空树") */
const pickedIds = computed(() => rows.value.map((r) => r.userId));
const sources = [{ name: "本校组织", loader: () => baseApi.getSchoolAll().send() }];
const columns = computed(() => [
{ title: "序号", key: "index", width: 70, render: (h: any, p: any) => h("span", String(p.index + 1)) },
{ title: "姓名", key: "userName", minWidth: 160 },
{
title: "操作",
key: "ops",
width: 90,
render: (h: any, p: any) =>
h("a", { class: "action-del", onClick: () => confirmRemove(p.row as AdminRow) }, "删除")
}
]);
async function load() {
loading.value = true;
try {
rows.value = (await listCall().send()) || [];
} catch (e: any) {
Message.error(e?.message || "管理员名单加载失败");
} finally {
loading.value = false;
}
}
function listCall() {
return props.mode === "form" && props.formUuid
? adminApi.formAdmins(props.formUuid)
: adminApi.schoolAdmins();
}
function writeCall(users: { userId: number; userName: string }[]) {
return props.mode === "form" && props.formUuid
? adminApi.addFormAdmins(props.formUuid, { users })
: adminApi.addSchoolAdmins({ users });
}
function removeCall(id: number) {
return props.mode === "form" && props.formUuid
? adminApi.removeFormAdmin(props.formUuid, id)
: adminApi.removeSchoolAdmin(id);
}
function openPicker() {
checkRef.value?.show();
}
/** UserCheckModal 的 on-submit:[{ id, name, group... }],id 即 UserBase 用户主键 */
async function onPicked(users: any[]) {
const items = (users || [])
.filter((u) => u && u.id != null)
.map((u) => ({ userId: Number(u.id), userName: String(u.name || "").trim() }));
if (!items.length) return;
if (items.some((i) => !i.userName)) {
Message.error("选人结果里有没有姓名的行,请重选");
return;
}
busy.value = true;
try {
rows.value = (await writeCall(items).send()) || [];
Message.success("已添加管理员");
} catch (e: any) {
Message.error(e?.message || "添加失败");
} finally {
busy.value = false;
}
}
function confirmRemove(row: AdminRow) {
Modal.confirm({
title: "删除管理员",
content: `确定移除「${row.userName}」?该操作只撤销数据范围升级,不影响他已经拿到的模块行权限。`,
okText: "删除",
cancelText: "取消",
onOk: async () => {
busy.value = true;
try {
rows.value = (await removeCall(row.id).send()) || [];
} catch (e: any) {
// 防呆(最后一名不可删)与跨校拒绝都在这支:按后端 msg 原样提示
Message.error(e?.message || "删除失败");
} finally {
busy.value = false;
}
}
});
}
onMounted(load);
defineExpose({ load });
</script>
<template>
<div class="admin-panel">
<div class="toolbar">
<Button type="primary" icon="md-person-add" :disabled="busy" @click="openPicker">添加管理员</Button>
<span class="tip">
{{
mode === "school"
? "校级表单管理员:本校表单数据范围=全部,基础表单=本校提交"
: "单表单管理员:这张表单的数据查看与导出(编辑权仍走表单设计权限)"
}}
</span>
</div>
<Table :columns="columns" :data="rows" :loading="loading" size="small" :border="false" />
<UserCheckModal ref="checkRef" :sources="sources" :userids="pickedIds" @on-submit="onPicked" />
</div>
</template>
<style scoped>
.admin-panel .toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.admin-panel .tip {
color: #808695;
font-size: 12px;
}
.admin-panel :deep(.action-del) {
color: #ed4014;
}
</style>
+27
View File
@@ -0,0 +1,27 @@
<script setup lang="ts">
/**
* 单表单管理员弹窗(T10 方案 §6.4):表单列表的行操作与设计器的配置面板都挂它,
* 保证"同一弹窗"只有一份实现。每次打开都重新挂载面板,名单永远是进弹窗时拉的。
*/
import { ref } from "vue";
import AdminListPanel from "./AdminListPanel.vue";
const visible = ref(false);
const formUuid = ref("");
const formName = ref("");
function open(uuid: string, name?: string) {
if (!uuid) return;
formUuid.value = uuid;
formName.value = name || "";
visible.value = true;
}
defineExpose({ open });
</script>
<template>
<Modal v-model="visible" :title="formName ? `表单管理员|${formName}` : '表单管理员'" width="680" footer-hide>
<AdminListPanel v-if="visible" mode="form" :form-uuid="formUuid" />
</Modal>
</template>