Jelajahi Sumber

feat: 修复权限页面

doi 2 bulan lalu
induk
melakukan
a8fd858dd5

+ 47 - 0
src/api/model/role.js

@@ -0,0 +1,47 @@
+import config from "@/config";
+import http from "@/utils/request";
+
+export default {
+    list: {
+        url: `${config.API_URL}/role`,
+        name: "身份列表",
+        get: async function (params = {}) {
+            return await http.get(this.url, params);
+        },
+    },
+    create: {
+        url: `${config.API_URL}/role/create`,
+        name: "身份添加菜单树",
+        get: async function () {
+            return await http.get(this.url);
+        },
+    },
+    save: {
+        url: `${config.API_URL}/role/save`,
+        name: "身份保存",
+        post: async function (data = {}) {
+            return await http.post(this.url, data);
+        },
+    },
+    edit: {
+        url: `${config.API_URL}/role/edit`,
+        name: "身份编辑数据",
+        get: async function (id) {
+            return await http.get(`${this.url}/${id}`);
+        },
+    },
+    del: {
+        url: `${config.API_URL}/role/del`,
+        name: "身份删除",
+        delete: async function (id) {
+            return await http.delete(`${this.url}/${id}`);
+        },
+    },
+    setStatus: {
+        url: `${config.API_URL}/role/set_status`,
+        name: "身份状态",
+        get: async function (id, status) {
+            return await http.get(`${this.url}/${id}/${status}`);
+        },
+    },
+};

+ 10 - 1
src/api/model/user.js

@@ -10,6 +10,14 @@ export default {
             return await http.get(this.url, params);
         },
     },
+    /** 后台管理员列表 adminapi/user/list(权限模块) */
+    adminList: {
+        url: `${config.API_URL}/user/list`,
+        name: "管理员列表",
+        get: async function (params = {}) {
+            return await http.get(this.url, params);
+        },
+    },
     setStatus: {
         url: `${config.API_URL}/users/set_status`,
         name: "用户状态",
@@ -17,9 +25,10 @@ export default {
             return await http.get(`${this.url}/${id}/${status}`);
         },
     },
+    /** POST adminapi/user/passwd:管理员编辑 id、roles(必填);password 非空才传则改密,否则不传 */
     pwd: {
         url: `${config.API_URL}/user/passwd`,
-        name: "-",
+        name: "管理员更新",
         post: async function (params) {
             return await http.post(this.url, params);
         },

+ 30 - 1
src/config/route.js

@@ -26,6 +26,35 @@
 // 	}
 // ]
 
-const routes = []
+const routes = [
+	// {
+	// 	name: "permission",
+	// 	path: "/permission",
+	// 	meta: {
+	// 		icon: "el-icon-lock",
+	// 		title: "权限管理",
+	// 	},
+	// 	children: [
+	// 		{
+	// 			name: "permissionRoleList",
+	// 			path: "/permission/role",
+	// 			component: "permission/role/list/index",
+	// 			meta: {
+	// 				icon: "el-icon-user-filled",
+	// 				title: "身份列表",
+	// 			},
+	// 		},
+	// 		{
+	// 			name: "permissionAdminList",
+	// 			path: "/permission/admin",
+	// 			component: "permission/admin/list/index",
+	// 			meta: {
+	// 				icon: "el-icon-monitor",
+	// 				title: "管理员列表",
+	// 			},
+	// 		},
+	// 	],
+	// },
+]
 
 export default routes;

+ 378 - 0
src/views/permission/admin/index.vue

@@ -0,0 +1,378 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <div class="header-row">
+                    <span>管理员列表</span>
+                    <el-button type="primary" @click="openCreate">添加管理员</el-button>
+                </div>
+            </template>
+
+            <el-table v-loading="loading" :data="list">
+                <el-table-column prop="id" label="ID" width="80" />
+                <el-table-column prop="username" label="账号" min-width="120" show-overflow-tooltip />
+                <el-table-column prop="truename" label="姓名" min-width="100" show-overflow-tooltip />
+                <el-table-column label="身份" min-width="120" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatRoles(row) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="状态" width="100" align="center">
+                    <template #default="{ row }">
+                        <el-tag :type="Number(row.status) === 1 ? 'success' : 'info'" size="small">
+                            {{ Number(row.status) === 1 ? "正常" : "停用" }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="login_at" label="最后登录" min-width="170" show-overflow-tooltip />
+                <el-table-column label="创建时间" min-width="170" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ row.create_at || row.created_at || "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" width="200" fixed="right">
+                    <template #default="{ row }">
+                        <div class="table-row-actions">
+                            <el-button type="primary" size="small" @click="openEdit(row)">编辑</el-button>
+                            <el-button type="danger" size="small" plain @click="removeRow(row)">删除</el-button>
+                        </div>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <div class="pager">
+                <el-pagination
+                    background
+                    layout="total, prev, pager, next, sizes"
+                    :total="total"
+                    :current-page="query.page"
+                    :page-size="query.limit"
+                    :page-sizes="[5, 10, 20, 50]"
+                    @current-change="handlePageChange"
+                    @size-change="handleSizeChange"
+                />
+            </div>
+        </el-card>
+
+        <el-dialog
+            v-model="dialogVisible"
+            :title="dialogMode === 'create' ? '添加管理员' : '编辑管理员'"
+            width="440px"
+            destroy-on-close
+            @closed="onDialogClosed"
+        >
+            <el-form ref="formRef" :model="form" :rules="activeRules" label-width="90px">
+                <template v-if="dialogMode === 'create'">
+                    <el-form-item label="账号" prop="username">
+                        <el-input v-model="form.username" clearable maxlength="40" autocomplete="off" />
+                    </el-form-item>
+                    <el-form-item label="姓名" prop="truename">
+                        <el-input v-model="form.truename" clearable maxlength="40" />
+                    </el-form-item>
+                    <el-form-item label="密码" prop="password">
+                        <el-input v-model="form.password" type="password" show-password maxlength="64" autocomplete="new-password" />
+                    </el-form-item>
+                </template>
+                <template v-else>
+                    <el-form-item label="账号">
+                        <el-input :model-value="form.username" disabled />
+                    </el-form-item>
+                    <el-form-item label="姓名">
+                        <el-input :model-value="form.truename || '-'" disabled />
+                    </el-form-item>
+                    <el-form-item label="新密码" prop="password">
+                        <el-input
+                            v-model="form.password"
+                            type="password"
+                            show-password
+                            maxlength="64"
+                            autocomplete="new-password"
+                            placeholder="留空则不修改密码"
+                        />
+                    </el-form-item>
+                </template>
+                <el-form-item label="身份" prop="roles">
+                    <el-select
+                        v-model="form.roles"
+                        placeholder="请选择身份"
+                        clearable
+                        filterable
+                        :loading="roleLoading"
+                        style="width: 100%"
+                    >
+                        <el-option v-for="r in roleOptions" :key="r.id" :label="roleOptionLabel(r)" :value="String(r.id)" />
+                    </el-select>
+                </el-form-item>
+            </el-form>
+            <template #footer>
+                <el-button @click="dialogVisible = false">取消</el-button>
+                <el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+            </template>
+        </el-dialog>
+    </el-main>
+</template>
+
+<script>
+export default {
+    name: "PermissionAdminList",
+    data() {
+        return {
+            loading: false,
+            saving: false,
+            roleLoading: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10 },
+            dialogVisible: false,
+            dialogMode: "create",
+            roleOptions: [],
+            /** 身份 id -> 名称,用于列表解析 roles 数字字段 */
+            roleNameById: {},
+            form: this.getDefaultForm(),
+            rulesCreate: {
+                username: [{ required: true, message: "请输入账号", trigger: "blur" }],
+                truename: [{ required: true, message: "请输入姓名", trigger: "blur" }],
+                password: [{ required: true, message: "请输入密码", trigger: "blur" }],
+                roles: [{ required: true, message: "请选择身份", trigger: "change" }],
+            },
+            rulesEdit: {
+                roles: [{ required: true, message: "请选择身份", trigger: "change" }],
+            },
+        };
+    },
+    computed: {
+        activeRules() {
+            return this.dialogMode === "create" ? this.rulesCreate : this.rulesEdit;
+        },
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        /** 后端部分接口 code 为 1,权限相关为 200 */
+        apiOk(res) {
+            return res && (res.code === 1 || res.code === 200);
+        },
+        getDefaultForm() {
+            return { id: "", username: "", truename: "", password: "", roles: "" };
+        },
+        parseRoleRows(data) {
+            const d = data ?? {};
+            return Array.isArray(d) ? d : d.list || d.data || d.roles || [];
+        },
+        /** 身份 id → 展示名写入 roleNameById;下拉与表格均以 role_name 为主 */
+        roleOptionLabel(row) {
+            const n = row.role_name;
+            if (n != null && String(n).trim() !== "") return String(n);
+            return row.name ?? `身份#${row.id ?? ""}`;
+        },
+        normalizeAdminRoleValue(rolesField) {
+            if (rolesField == null || rolesField === "") return "";
+            if (typeof rolesField === "object" && !Array.isArray(rolesField)) {
+                const id = rolesField.id ?? rolesField.role_id;
+                return id != null ? String(id) : "";
+            }
+            return String(rolesField);
+        },
+        applyRoleRows(rows, onlyEnabledForSelect = false) {
+            const map = { ...this.roleNameById };
+            const options = [];
+            rows.forEach((r) => {
+                const id = r.id;
+                if (id == null) return;
+                const sid = String(id);
+                map[sid] = this.roleOptionLabel({ ...r }) || map[sid] || "";
+                if (!onlyEnabledForSelect || Number(r.status) === 1) {
+                    options.push(r);
+                }
+            });
+            this.roleNameById = map;
+            return options;
+        },
+        async refreshRoleLookup() {
+            const res = await this.$API.role.list.get({});
+            if (!this.apiOk(res)) return;
+            const rows = this.parseRoleRows(res.data);
+            this.applyRoleRows(rows, false);
+        },
+        formatRoles(row) {
+            const topName = row.role_name ?? row.roles_name;
+            if (topName != null && String(topName).trim() !== "") return String(topName);
+            if (row.roles != null && typeof row.roles === "object" && !Array.isArray(row.roles)) {
+                const o = row.roles;
+                if (o.role_name != null && String(o.role_name).trim() !== "") return String(o.role_name);
+                const kid = o.id != null ? String(o.id) : "";
+                if (kid) return this.roleNameById[kid] || `身份 #${kid}`;
+            }
+            if (Array.isArray(row.roles)) {
+                return row.roles
+                    .map((x) =>
+                        typeof x === "object" && x
+                            ? (x.role_name != null && String(x.role_name).trim() !== "" ? String(x.role_name) : this.roleOptionLabel(x))
+                            : this.roleNameById[String(x)] || `#${x}`
+                    )
+                    .filter(Boolean)
+                    .join("、");
+            }
+            if (row.roles != null && row.roles !== "") {
+                const key = String(row.roles);
+                const name = this.roleNameById[key];
+                return name ? name : `身份 #${key}`;
+            }
+            return "-";
+        },
+        /**
+         * 加载身份下拉。新增仅展示启用身份(status=1);编辑需全集,否则会因当前绑定身份被过滤而下拉只显示 id。
+         * @param {boolean} onlyEnabled  true:仅启用;false:全集
+         */
+        async loadRoleOptions(onlyEnabled = true) {
+            this.roleLoading = true;
+            try {
+                const params = onlyEnabled ? { status: 1 } : {};
+                const res = await this.$API.role.list.get(params);
+                if (!this.apiOk(res)) return;
+                const rows = this.parseRoleRows(res.data);
+                const list = this.applyRoleRows(rows, onlyEnabled);
+                this.roleOptions = list.length ? list : rows;
+            } finally {
+                this.roleLoading = false;
+            }
+        },
+        /** 当前管理员绑定的身份若仍不在下拉里,补一条(优先用行内 role_name) */
+        ensureRoleOptionForRow(row) {
+            const rid = this.normalizeAdminRoleValue(row.roles);
+            if (!rid) return;
+            if (this.roleOptions.some((r) => String(r.id) === rid)) return;
+            let roleName = row.role_name;
+            if (roleName != null && String(roleName).trim() === "") roleName = undefined;
+            if (!roleName && row.roles && typeof row.roles === "object" && !Array.isArray(row.roles) && row.roles.role_name) {
+                roleName = row.roles.role_name;
+            }
+            if (!roleName) roleName = this.roleNameById[rid];
+            this.roleOptions = [...this.roleOptions, { id: rid, role_name: roleName || `身份 #${rid}` }];
+        },
+        async getList() {
+            this.loading = true;
+            try {
+                await this.refreshRoleLookup();
+                const res = await this.$API.user.adminList.get(this.query);
+                if (!this.apiOk(res)) {
+                    this.$message.error(res.msg || "获取管理员列表失败");
+                    return;
+                }
+                const data = res.data || {};
+                this.list = data.list || data.data || [];
+                this.total = Number(data.count ?? data.total ?? this.list.length ?? 0);
+            } finally {
+                this.loading = false;
+            }
+        },
+        handlePageChange(page) {
+            this.query.page = page;
+            this.getList();
+        },
+        handleSizeChange(limit) {
+            this.query.limit = limit;
+            this.query.page = 1;
+            this.getList();
+        },
+        async openCreate() {
+            this.dialogMode = "create";
+            this.form = this.getDefaultForm();
+            await this.loadRoleOptions();
+            this.dialogVisible = true;
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        async openEdit(row) {
+            this.dialogMode = "edit";
+            await this.loadRoleOptions(false);
+            this.ensureRoleOptionForRow(row);
+            this.form = {
+                id: String(row.id ?? ""),
+                username: row.username ?? "",
+                truename: row.truename ?? "",
+                password: "",
+                roles: this.normalizeAdminRoleValue(row.roles),
+            };
+            this.dialogVisible = true;
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        onDialogClosed() {
+            this.dialogMode = "create";
+            this.form = this.getDefaultForm();
+        },
+        submitForm() {
+            this.$refs.formRef.validate(async (valid) => {
+                if (!valid) return false;
+                this.saving = true;
+                try {
+                    if (this.dialogMode === "create") {
+                        const res = await this.$API.user.save.post({
+                            username: this.form.username,
+                            truename: this.form.truename,
+                            password: this.form.password,
+                            roles: String(this.form.roles),
+                        });
+                        if (!this.apiOk(res)) {
+                            this.$message.error(res.msg || "保存失败");
+                            return false;
+                        }
+                        this.$message.success(res.msg || "保存成功");
+                    } else {
+                        const payload = {
+                            id: String(this.form.id),
+                            roles: String(this.form.roles),
+                        };
+                        const pwd = String(this.form.password ?? "").trim();
+                        if (pwd) payload.password = pwd;
+                        const res = await this.$API.user.pwd.post(payload);
+                        if (!this.apiOk(res)) {
+                            this.$message.error(res.msg || "保存失败");
+                            return false;
+                        }
+                        this.$message.success(res.msg || "保存成功");
+                    }
+                    this.dialogVisible = false;
+                    this.getList();
+                } finally {
+                    this.saving = false;
+                }
+            });
+        },
+        async removeRow(row) {
+            const confirm = await this.$confirm("确认删除该管理员吗?", "提示", {
+                type: "warning",
+                confirmButtonText: "删除",
+                confirmButtonClass: "el-button--danger",
+            }).catch(() => {});
+            if (confirm !== "confirm") return;
+            const res = await this.$API.user.del.post({ id: String(row.id) });
+            if (!this.apiOk(res)) {
+                this.$message.error(res.msg || "删除失败");
+                return;
+            }
+            this.$message.success(res.msg || "删除成功");
+            this.getList();
+        },
+    },
+};
+</script>
+
+<style scoped>
+.header-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}
+.table-row-actions {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    align-items: center;
+}
+.pager {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 347 - 0
src/views/permission/role/index.vue

@@ -0,0 +1,347 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <div class="header-row">
+                    <span>身份列表</span>
+                    <el-button type="primary" @click="openCreate">添加身份</el-button>
+                </div>
+            </template>
+
+            <el-table v-loading="loading" :data="list">
+                <el-table-column prop="id" label="ID" width="80" />
+                <el-table-column prop="role_name" label="身份名称" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ row.role_name ?? row.name ?? "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="状态" width="120">
+                    <template #default="{ row }">
+                        <el-switch
+                            :model-value="Number(row.status)"
+                            :active-value="1"
+                            :inactive-value="0"
+                            inline-prompt
+                            active-text="启用"
+                            inactive-text="停用"
+                            active-color="#13ce66"
+                            inactive-color="#ff4949"
+                            @change="(val) => changeStatus(row, val)"
+                        />
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" width="200" fixed="right">
+                    <template #default="{ row }">
+                        <div class="table-row-actions">
+                            <el-button type="primary" size="small" @click="openEdit(row)">编辑</el-button>
+                            <el-button type="danger" size="small" plain @click="removeRow(row)">删除</el-button>
+                        </div>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </el-card>
+
+        <el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px" destroy-on-close @closed="onDialogClosed">
+            <el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
+                <el-form-item label="名称" prop="role_name">
+                    <el-input v-model="form.role_name" clearable maxlength="60" placeholder="身份名称" />
+                </el-form-item>
+                <el-form-item label="状态">
+                    <el-radio-group v-model="form.status">
+                        <el-radio :label="1">启用</el-radio>
+                        <el-radio :label="0">停用</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+                <el-form-item label="菜单权限">
+                    <el-scrollbar max-height="360px">
+                        <el-tree
+                            v-if="dialogVisible"
+                            :key="treeKey"
+                            ref="treeRef"
+                            :data="menuTree"
+                            :props="treeProps"
+                            show-checkbox
+                            check-strictly
+                            node-key="id"
+                            default-expand-all
+                            class="perm-tree"
+                        />
+                        <div v-if="!menuTree.length" class="tree-empty">暂无菜单数据</div>
+                    </el-scrollbar>
+                </el-form-item>
+            </el-form>
+            <template #footer>
+                <el-button @click="dialogVisible = false">取消</el-button>
+                <el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+            </template>
+        </el-dialog>
+    </el-main>
+</template>
+
+<script>
+/** 必选且不可取消勾选的菜单权限 id */
+const LOCKED_RULE_IDS = new Set(["1", "3"]);
+
+export default {
+    name: "PermissionRoleList",
+    data() {
+        return {
+            loading: false,
+            saving: false,
+            list: [],
+            dialogVisible: false,
+            dialogTitle: "添加身份",
+            treeKey: 0,
+            menuTree: [],
+            form: this.getDefaultForm(),
+            rules: {
+                role_name: [{ required: true, message: "请输入身份名称", trigger: "blur" }],
+            },
+            treeProps: {
+                children: "children",
+                label: "permLabel",
+                disabled: "disabled",
+            },
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        getDefaultForm() {
+            return { id: "", role_name: "", status: 1 };
+        },
+        normalizeMenuTree(nodes) {
+            if (!Array.isArray(nodes)) return [];
+            return nodes.map((node) => {
+                const meta = node.meta && typeof node.meta === "object" ? node.meta : null;
+                const rawId = node.id ?? node.menu_id ?? node.value ?? meta?.id;
+                const id = rawId != null && rawId !== "" ? String(rawId) : rawId;
+                const label =
+                    (meta && (meta.title || meta.name)) ||
+                    node.title ||
+                    node.name ||
+                    node.menu_name ||
+                    node.label ||
+                    node.pathname ||
+                    node.path_name ||
+                    (id != null ? `节点 ${id}` : "未命名");
+                const raw = node.children ?? node.child ?? node.subs ?? node.son ?? node.nodes;
+                const children = Array.isArray(raw) && raw.length ? this.normalizeMenuTree(raw) : [];
+                const locked = id != null && LOCKED_RULE_IDS.has(String(id));
+                return {
+                    ...node,
+                    id,
+                    permLabel: label,
+                    disabled: locked,
+                    children: children.length ? children : undefined,
+                };
+            });
+        },
+        /** 后端可能直接返回数组(与 menu/list 一致),或包在 menus / authList / data 等字段中 */
+        pickMenuTree(payload) {
+            if (Array.isArray(payload)) return payload;
+            if (!payload || typeof payload !== "object") return [];
+            const d = payload;
+            const buckets = [
+                d.menus,
+                d.menu,
+                d.menuList,
+                d.menu_list,
+                d.authList,
+                d.authorize,
+                Array.isArray(d.data) ? d.data : null,
+                d.ruleList,
+                d.tree,
+                d.nodes,
+                d.routers,
+                d.routes,
+                d.list,
+            ];
+            for (let i = 0; i < buckets.length; i++) {
+                const b = buckets[i];
+                if (Array.isArray(b)) return b;
+            }
+            return [];
+        },
+        parseRulesKeys(rules) {
+            if (Array.isArray(rules)) return rules.map((x) => String(x));
+            if (typeof rules === "string" && rules.trim()) {
+                return rules
+                    .split(/[,|]/)
+                    .map((s) => s.trim())
+                    .filter(Boolean);
+            }
+            return [];
+        },
+        mergeRoleInfo(data) {
+            const d = data || {};
+            const role = d.role || d.info || {};
+            return {
+                id: role.id != null ? role.id : d.id,
+                role_name: role.role_name ?? role.name ?? d.role_name ?? "",
+                status: role.status != null ? Number(role.status) : d.status != null ? Number(d.status) : 1,
+                rules: role.rules ?? d.rules ?? d.rule ?? "",
+            };
+        },
+        apiOk(res) {
+            return res && (res.code === 1 || res.code === 200);
+        },
+        async getList() {
+            this.loading = true;
+            try {
+                const res = await this.$API.role.list.get({});
+                if (!this.apiOk(res)) {
+                    this.$message.error(res.msg || "获取身份列表失败");
+                    return;
+                }
+                const data = res.data ?? {};
+                const raw = Array.isArray(data) ? data : data.list || data.data || data.roles || [];
+                this.list = raw;
+            } finally {
+                this.loading = false;
+            }
+        },
+        collectRules() {
+            const tree = this.$refs.treeRef;
+            if (!tree) return [...LOCKED_RULE_IDS];
+            const checked = tree.getCheckedKeys(false);
+            const half = tree.getHalfCheckedKeys();
+            const picked = [...new Set([...checked, ...half].map(String))];
+            return this.ensureLockedRules(picked);
+        },
+        /** 提交与编辑回填时都必须包含必选 id */
+        ensureLockedRules(keys) {
+            const set = new Set(keys.map(String));
+            LOCKED_RULE_IDS.forEach((id) => set.add(id));
+            return [...set];
+        },
+        async openCreate() {
+            this.dialogTitle = "添加身份";
+            this.form = this.getDefaultForm();
+            this.treeKey += 1;
+            this.dialogVisible = true;
+            try {
+                const res = await this.$API.role.create.get();
+                if (!this.apiOk(res)) {
+                    this.$message.error(res.msg || "获取菜单失败");
+                    this.menuTree = [];
+                    return;
+                }
+                const d = res.data || {};
+                this.menuTree = this.normalizeMenuTree(this.pickMenuTree(d));
+                await this.$nextTick();
+                /** 新建:默认仅勾选 1、3,且这两项禁止取消 */
+                this.$refs.treeRef?.setCheckedKeys([...LOCKED_RULE_IDS]);
+            } catch (e) {
+                this.menuTree = [];
+            }
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        async openEdit(row) {
+            const id = row.id;
+            if (id == null) {
+                this.$message.error("缺少身份 ID");
+                return;
+            }
+            this.dialogTitle = "编辑身份";
+            this.treeKey += 1;
+            this.dialogVisible = true;
+            try {
+                const res = await this.$API.role.edit.get(id);
+                if (!this.apiOk(res)) {
+                    this.$message.error(res.msg || "获取身份详情失败");
+                    this.menuTree = [];
+                    return;
+                }
+                const d = res.data || {};
+                this.menuTree = this.normalizeMenuTree(this.pickMenuTree(d));
+                const merged = this.mergeRoleInfo(d);
+                this.form = {
+                    id: merged.id != null ? String(merged.id) : "",
+                    role_name: merged.role_name,
+                    status: merged.status,
+                };
+                await this.$nextTick();
+                const keys = this.ensureLockedRules(this.parseRulesKeys(merged.rules));
+                this.$refs.treeRef?.setCheckedKeys(keys);
+            } catch (e) {
+                this.menuTree = [];
+            }
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        onDialogClosed() {
+            this.menuTree = [];
+            this.form = this.getDefaultForm();
+        },
+        submitForm() {
+            this.$refs.formRef.validate(async (valid) => {
+                if (!valid) return false;
+                this.saving = true;
+                try {
+                    const payload = {
+                        role_name: this.form.role_name,
+                        status: String(this.form.status ?? 1),
+                        rules: this.collectRules(),
+                    };
+                    if (this.form.id != null && String(this.form.id) !== "") {
+                        payload.id = String(this.form.id);
+                    }
+                    const res = await this.$API.role.save.post(payload);
+                    if (!this.apiOk(res)) {
+                        this.$message.error(res.msg || "保存失败");
+                        return false;
+                    }
+                    this.$message.success(res.msg || "保存成功");
+                    this.dialogVisible = false;
+                    this.getList();
+                } finally {
+                    this.saving = false;
+                }
+            });
+        },
+        async removeRow(row) {
+            const confirm = await this.$confirm("确认删除该身份吗?", "提示", {
+                type: "warning",
+                confirmButtonText: "删除",
+                confirmButtonClass: "el-button--danger",
+            }).catch(() => {});
+            if (confirm !== "confirm") return;
+            const res = await this.$API.role.del.delete(row.id);
+            if (!this.apiOk(res)) {
+                this.$message.error(res.msg || "删除失败");
+                return;
+            }
+            this.$message.success(res.msg || "删除成功");
+            this.getList();
+        },
+        async changeStatus(row, status) {
+            const res = await this.$API.role.setStatus.get(row.id, status);
+            if (!this.apiOk(res)) {
+                this.$message.error(res.msg || "状态更新失败");
+                this.getList();
+                return;
+            }
+            this.$message.success(res.msg || "状态更新成功");
+            row.status = status;
+        },
+    },
+};
+</script>
+
+<style scoped>
+.header-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}
+.perm-tree {
+    width: 100%;
+    padding: 6px 0;
+}
+.tree-empty {
+    color: var(--el-text-color-secondary);
+    font-size: 13px;
+    padding: 12px 0;
+}
+</style>

+ 1 - 1
src/views/task/manage/list/index.vue

@@ -437,7 +437,7 @@ export default {
         async getLevelOptions() {
             this.levelLoading = true;
             try {
-                const res = await this.$API.user.levelList.get();
+                const res = await this.$API.user.levelList.get({ is_show: 1 });
                 if (res.code !== 1) {
                     this.$message.error(res.msg || "获取用户等级列表失败");
                     return;