- 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:
@@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<Modal
|
||||
v-model="modal"
|
||||
title="选择用户"
|
||||
width="720px"
|
||||
:mask-closable="false"
|
||||
:closable="false"
|
||||
>
|
||||
<Tabs
|
||||
v-if="sources.length > 1"
|
||||
:key="tabKey"
|
||||
type="card"
|
||||
:value="String(tabIndex)"
|
||||
@on-click="tabChange"
|
||||
>
|
||||
<TabPane v-for="(item, index) in sources" :key="index" :label="item.name" :name="index + ''"></TabPane>
|
||||
</Tabs>
|
||||
<AutoComplete
|
||||
@on-search="handleSearch"
|
||||
@on-select="handleSelect"
|
||||
placeholder="输入名字搜索"
|
||||
v-model="searchTreeName"
|
||||
:clearable="true"
|
||||
class="auto-box"
|
||||
>
|
||||
<Option v-for="item in searchResult" :key="item.id" :value="item.name">
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="fr info">{{ item.group }}</span>
|
||||
</Option>
|
||||
</AutoComplete>
|
||||
<div class="tree-wrap fix">
|
||||
<div class="tree-panel fl">
|
||||
<div class="tree-box">
|
||||
<div :id="ztreeDomId" class="ztree"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow-box fl">
|
||||
<svg
|
||||
t="1626916477711"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="36"
|
||||
height="36"
|
||||
>
|
||||
<path
|
||||
d="M138.935 256.078v511.844h63.42V256.078h-63.42z m126.842 0v511.844h127.215V256.078H265.777zM885.064 512L583.254 64.323v191.755H456.414v511.844h126.842v191.755L885.064 512z"
|
||||
fill="#2d8cf0"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-panel fl">
|
||||
<div class="title-box fix">
|
||||
<Checkbox @on-change="checkAllUser" v-model="isCheckUserAll">全选</Checkbox>
|
||||
<div class="fr">{{ tempUserList.length }}</div>
|
||||
<a
|
||||
href="javascript:;"
|
||||
@click="delCheckUser"
|
||||
v-if="tempUserList.findIndex((x: any) => x.isCheck) > -1"
|
||||
>删除选中</a
|
||||
>
|
||||
</div>
|
||||
<ul class="user-box">
|
||||
<li
|
||||
v-for="item in tempUserList"
|
||||
:key="item.id"
|
||||
:class="{ active: item.isCheck }"
|
||||
@click="checkUserOne(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<span class="info">{{ item.group }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button type="primary" @click="submit">提交</Button>
|
||||
<Button @click="modal = false" style="margin-left: 8px">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* 选择用户弹窗(T10 自 notice/front/src/components/UserCheckModal.vue 移植;源头再早是旧前端
|
||||
* oldfront/src/components/userCheck.vue 适配 Vue3)。
|
||||
* 左侧为带复选框的组织树(部门→人员,叶子=人员),勾选/取消按父子级联;
|
||||
* 右侧为已选人员列表,支持全选/删除选中;提交返回去重后的用户 id 数组与用户对象数组。
|
||||
*
|
||||
* 相对源文件的唯一改动:两个 ztree 回调里未使用的形参加下划线前缀——本仓 type-check 开了
|
||||
* noUnusedParameters,位置参数不能删(ztree 按 (event, treeId, treeNode) 顺序回调)。
|
||||
* 依赖:全局 $ / $.fn.zTree 与 zTreeStyle 由 index.html 的 public/libs 静态引入(同 notice)。
|
||||
*/
|
||||
import { Message } from "view-ui-plus";
|
||||
import { requireImg } from "@/utils/hook/useImg";
|
||||
|
||||
interface SelectSource {
|
||||
name: string;
|
||||
loader: () => Promise<any[]>;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "UserCheckModal",
|
||||
props: {
|
||||
/** 数据源 tab:{ name, loader },loader 返回 ztree 树数据(children 嵌套,叶子=人员) */
|
||||
sources: {
|
||||
type: Array as () => SelectSource[],
|
||||
default: () => [],
|
||||
},
|
||||
/** 已选用户 id(打开时自动勾选) */
|
||||
userids: {
|
||||
type: Array as () => number[],
|
||||
default: () => [],
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:userids", "on-submit"],
|
||||
data() {
|
||||
return {
|
||||
ztreeDomId: "user_check_tree_" + Math.floor(Math.random() * 1e8),
|
||||
modal: false,
|
||||
tabIndex: 0,
|
||||
/** 每次打开弹窗自增,强制重建 Tabs,避免上次 tab 高亮残留与数据错位 */
|
||||
tabKey: 0,
|
||||
/** 树加载序号:tab 快速切换时只渲染最后一次请求的结果 */
|
||||
treeSeq: 0,
|
||||
isCheckUserAll: false,
|
||||
cacheMap: new Map(),
|
||||
currentTree: [] as any[],
|
||||
searchTreeName: "",
|
||||
searchResult: [] as any[],
|
||||
tempUserList: [] as any[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** 当前树扁平化(含父级名称,供搜索展示) */
|
||||
currentTreeFlatten(): any[] {
|
||||
const flatten = (data: any[], pid: number, pname: string): any[] =>
|
||||
data.reduce(
|
||||
(arr: any[], { id, name, children = [], type }: any) =>
|
||||
arr.concat([{ id, name, pid, type, group: pname }], flatten(children, id, name)),
|
||||
[]
|
||||
);
|
||||
return flatten(this.currentTree, 0, "");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.modal = true;
|
||||
// 重置到第一个 tab:显式赋值 + key 重建 Tabs,保证高亮与数据一致
|
||||
this.tabIndex = 0;
|
||||
this.tabKey++;
|
||||
this.changeEmpty();
|
||||
this.initTree();
|
||||
},
|
||||
tabChange(index: string) {
|
||||
const idx = Number(index);
|
||||
// 严格比较:v-model 场景下 iview 会把值写成字符串 name,
|
||||
// 宽松相等("1" == 1)会误判“未切换”导致 tab 永远不生效
|
||||
if (idx !== 0 && this.tabIndex === idx) return;
|
||||
this.tabIndex = idx;
|
||||
this.changeEmpty();
|
||||
this.initTree();
|
||||
},
|
||||
handleSearch(query: string) {
|
||||
let arr: any[] = [];
|
||||
const searchArr = this.currentTreeFlatten.filter(
|
||||
(x: any) => x.type == 2 && x.name.indexOf(query) != -1
|
||||
);
|
||||
for (const item of searchArr) {
|
||||
!arr
|
||||
.map((x) => this.getSplitId(x.id))
|
||||
.includes(this.getSplitId(item.id)) && arr.push(item);
|
||||
}
|
||||
this.searchResult = arr;
|
||||
},
|
||||
handleSelect(query: string) {
|
||||
if (query.indexOf(",") != -1) query = query.split(",")[0];
|
||||
var treeObj = ($.fn as any).zTree.getZTreeObj(this.ztreeDomId);
|
||||
var node = treeObj.getNodeByParam("name", query, null);
|
||||
if (!!node) treeObj.checkNode(node, true, true, true);
|
||||
},
|
||||
/**
|
||||
* ztree 数据预处理:拼接 pid 保证同人多组时 id 可区分;叶子标记 type=2。
|
||||
* 组/用户判定优先用接口 isParent(组=文件夹),缺失时按有无子节点兜底;
|
||||
* 并设置组/用户区分图标。
|
||||
*/
|
||||
formatTree(tree: any[], level = 0, pid: number = 0) {
|
||||
tree.forEach((element: any) => {
|
||||
element.open = level == 0;
|
||||
element.pid = pid;
|
||||
element.id = element.id + "," + pid;
|
||||
element.checked = false;
|
||||
if (typeof element.isParent !== "undefined") {
|
||||
element.type = element.isParent ? 1 : 2;
|
||||
} else {
|
||||
element.type = element.children && element.children.length > 0 ? 1 : 2;
|
||||
}
|
||||
element.icon = requireImg(
|
||||
element.type == 1 ? "tree-icon-group.png" : "tree-icon-item.png",
|
||||
"image"
|
||||
);
|
||||
if (typeof element.children !== "undefined" && element.children.length > 0) {
|
||||
element.children = this.formatTree(element.children, level + 1, this.getSplitId(element.id));
|
||||
}
|
||||
});
|
||||
return tree;
|
||||
},
|
||||
initTree() {
|
||||
const _this = this;
|
||||
// 竞态守卫:慢请求返回时若已切换 tab,丢弃结果
|
||||
const seq = ++this.treeSeq;
|
||||
const cacheKey = this.tabIndex;
|
||||
const source = this.sources[this.tabIndex];
|
||||
if (!source) return;
|
||||
const cached = this.cacheMap.get(cacheKey);
|
||||
if (cached) {
|
||||
this.createTree(cached);
|
||||
return;
|
||||
}
|
||||
source
|
||||
.loader()
|
||||
.then((data: any[]) => {
|
||||
if (seq !== _this.treeSeq) return;
|
||||
_this.cacheMap.set(cacheKey, data || []);
|
||||
_this.createTree(data || []);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (seq === _this.treeSeq) Message.error(e?.message || "加载组织数据失败");
|
||||
});
|
||||
},
|
||||
createTree(rawData: any[]) {
|
||||
const _this = this;
|
||||
var jtree = JSON.parse(JSON.stringify(rawData));
|
||||
var ftree = _this.formatTree(jtree);
|
||||
_this.currentTree = ftree;
|
||||
|
||||
let treeSetting: any = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showIcon: true,
|
||||
},
|
||||
check: {
|
||||
enable: true,
|
||||
chkboxType: { Y: "ps", N: "ps" },
|
||||
},
|
||||
callback: {},
|
||||
};
|
||||
/** 勾选变化后同步右侧已选列表:收集勾选节点子树内的所有叶子 */
|
||||
function formatUser(treeNode: any, checked: boolean) {
|
||||
const recursionCheckIds = (tree: any): any[] => {
|
||||
let arrIds: any[] = [];
|
||||
for (const key in tree) {
|
||||
const element = tree[key];
|
||||
if (element.type == 2 && !element.chkDisabled) {
|
||||
arrIds.push({ id: element.id, tid: element.tId });
|
||||
}
|
||||
if (typeof element.children !== "undefined" && element.children) {
|
||||
arrIds = [...arrIds, ...recursionCheckIds(element.children)];
|
||||
}
|
||||
}
|
||||
return arrIds;
|
||||
};
|
||||
const checkIds = recursionCheckIds([treeNode]);
|
||||
const checkUserList = _this.currentTreeFlatten.filter((x: any) =>
|
||||
checkIds.map((x) => x.id).includes(x.id)
|
||||
);
|
||||
const checkUserArr: any[] = [];
|
||||
for (const element of checkUserList) {
|
||||
var findObj = checkIds.find((x) => x.id == element.id);
|
||||
if (
|
||||
!!findObj &&
|
||||
checkUserArr.findIndex((x) => x.id == element.id) === -1 &&
|
||||
_this.tempUserList.findIndex((x) => x.id == element.id) === -1
|
||||
) {
|
||||
checkUserArr.push({ ...element, isCheck: false, tid: findObj.tid });
|
||||
}
|
||||
}
|
||||
if (checked) {
|
||||
_this.tempUserList = _this.tempUserList.concat(checkUserArr);
|
||||
} else {
|
||||
var ids = checkUserList.map((x) => x.id);
|
||||
_this.tempUserList = _this.tempUserList.filter((x) => !ids.includes(x.id));
|
||||
}
|
||||
}
|
||||
treeSetting.callback.onClick = (_event: any, treeId: string, treeNode: any) => {
|
||||
let check = treeNode.checked;
|
||||
let treeObj = ($.fn as any).zTree.getZTreeObj(treeId);
|
||||
treeObj.checkNode(treeNode, !check, true);
|
||||
formatUser(treeNode, treeNode.checked);
|
||||
};
|
||||
treeSetting.callback.onCheck = (_event: any, _treeId: string, treeNode: any) => {
|
||||
formatUser(treeNode, treeNode.checked);
|
||||
};
|
||||
var treeObj = ($.fn as any).zTree.init($("#" + _this.ztreeDomId), treeSetting, ftree);
|
||||
// 回显:已选用户打勾(callbackFlag=true 会触发 onCheck,同步右侧列表)
|
||||
var nodes = treeObj.getNodesByFilter((x: any) =>
|
||||
_this.userids.includes(_this.getSplitId(x.id))
|
||||
);
|
||||
var arr: any[] = [];
|
||||
nodes.forEach((element: any) => {
|
||||
var splitId = _this.getSplitId(element.id); //重复id只选择第一个
|
||||
if (!arr.includes(splitId)) treeObj.checkNode(element, true, true, true);
|
||||
if (_this.isDisabled) treeObj.setChkDisabled(element, true);
|
||||
arr.push(splitId);
|
||||
});
|
||||
},
|
||||
checkAllUser(check: boolean) {
|
||||
this.tempUserList.forEach((element: any) => {
|
||||
element.isCheck = check;
|
||||
});
|
||||
},
|
||||
checkUserOne(item: any) {
|
||||
item.isCheck = !item.isCheck;
|
||||
const tlist = this.tempUserList;
|
||||
this.isCheckUserAll =
|
||||
tlist.length > 0 && tlist.filter((x) => x.isCheck).length == tlist.length;
|
||||
},
|
||||
delCheckUser() {
|
||||
const _this = this;
|
||||
const tlist = _this.tempUserList;
|
||||
const delArr = tlist.filter((item: any) => item.isCheck);
|
||||
var treeObj = ($.fn as any).zTree.getZTreeObj(_this.ztreeDomId);
|
||||
delArr.forEach((element: any) => {
|
||||
var node = treeObj.getNodeByTId(element.tid);
|
||||
treeObj.checkNode(node, false, true, true);
|
||||
});
|
||||
_this.isCheckUserAll = false;
|
||||
},
|
||||
changeEmpty() {
|
||||
this.tempUserList = [];
|
||||
this.searchTreeName = "";
|
||||
this.searchResult = [];
|
||||
},
|
||||
submit() {
|
||||
const _this = this;
|
||||
let idsArr = this.tempUserList.map((x: any) => _this.getSplitId(x.id));
|
||||
idsArr = Array.from(new Set(idsArr));
|
||||
this.$emit("update:userids", idsArr);
|
||||
function delKey(obj: any, ...args: string[]) {
|
||||
args.forEach((v) => delete obj[v]);
|
||||
return obj;
|
||||
}
|
||||
const objArr: any[] = [];
|
||||
idsArr.forEach((element) => {
|
||||
let findObj = _this.tempUserList.find((x: any) => _this.getSplitId(x.id) == element);
|
||||
findObj = delKey({ ...findObj }, "id", "isCheck", "tid", "pid", "type");
|
||||
objArr.push({ ...findObj, id: element });
|
||||
});
|
||||
this.$emit("on-submit", objArr);
|
||||
this.modal = false;
|
||||
},
|
||||
/** 树节点 id 形如 "1,301"(原id,父id),取真实 id */
|
||||
getSplitId(strId: any) {
|
||||
if (typeof strId === "string" && strId.indexOf(",") != -1) return parseInt(strId.split(",")[0]);
|
||||
return strId;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tree-wrap {
|
||||
height: 430px;
|
||||
position: relative;
|
||||
}
|
||||
.tree-panel {
|
||||
border: 1px solid #eee;
|
||||
width: 280px;
|
||||
height: 430px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tree-box {
|
||||
padding: 4px 0;
|
||||
height: 428px;
|
||||
width: auto;
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
}
|
||||
.user-panel {
|
||||
border: 1px solid #eee;
|
||||
width: 280px;
|
||||
.title-box {
|
||||
background: #f7f7f7;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
padding: 0 12px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
float: right;
|
||||
margin-right: 20px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
.user-box {
|
||||
height: 396px;
|
||||
overflow-y: auto;
|
||||
li {
|
||||
line-height: 32px;
|
||||
padding-left: 12px;
|
||||
cursor: pointer;
|
||||
.info {
|
||||
float: right;
|
||||
margin-right: 12px;
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
li:hover {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
li.active {
|
||||
background: #e4edf6;
|
||||
}
|
||||
}
|
||||
}
|
||||
.arrow-box {
|
||||
width: 128px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
margin-top: -17px;
|
||||
}
|
||||
.auto-box {
|
||||
width: 200px;
|
||||
margin-bottom: 10px;
|
||||
:deep(.ivu-select-item .info) {
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
:deep(.ztree li) {
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ztree li a) {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.ztree li a:hover) {
|
||||
text-decoration: none;
|
||||
}
|
||||
:deep(.ivu-auto-complete .ivu-select-dropdown-list) {
|
||||
max-height: 310px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user