|
@@ -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>
|