feat: add admin permissions voice input analytics and feedback

This commit is contained in:
2026-08-11 17:17:59 +08:00
parent 3b9215cc3d
commit fd2da26d10
46 changed files with 1836 additions and 192 deletions

View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import { ElMessage, ElMessageBox, type ElTree } from "element-plus";
import { nextTick, onMounted, reactive, ref } from "vue";
import { api } from "../services/api";
import type { ManagedAdmin, PermissionNode } from "../types/api";
const admins = ref<ManagedAdmin[]>([]);
const tree = ref<PermissionNode[]>([]);
const permissionTree = ref<InstanceType<typeof ElTree>>();
const dialogVisible = ref(false);
const saving = ref(false);
const editingId = ref<number | null>(null);
const form = reactive({ username: "", name: "", initialPassword: "", resetPassword: "", status: 1 });
async function load() {
[admins.value, tree.value] = await Promise.all([api.administrators(), api.permissionTree()]);
}
function openCreate() {
editingId.value = null;
Object.assign(form, { username: "", name: "", initialPassword: "", resetPassword: "", status: 1 });
dialogVisible.value = true;
nextTick(() => permissionTree.value?.setCheckedKeys([]));
}
function openEdit(item: ManagedAdmin) {
editingId.value = item.id;
Object.assign(form, { username: item.username, name: item.name, initialPassword: "", resetPassword: "", status: item.status });
dialogVisible.value = true;
nextTick(() => permissionTree.value?.setCheckedKeys(item.permissions));
}
function selectedPermissions() {
return (permissionTree.value?.getCheckedKeys(false) ?? []).map(String).filter((code) => code.includes("."));
}
async function save() {
if (!form.name.trim() || (!editingId.value && (!form.username.trim() || form.initialPassword.length < 8))) {
ElMessage.warning("请完整填写账号、姓名和至少 8 位的初始密码");
return;
}
saving.value = true;
try {
const permissions = selectedPermissions();
if (editingId.value) {
await api.updateAdministrator(editingId.value, { name: form.name, status: form.status, permissions, resetPassword: form.resetPassword || null });
} else {
await api.createAdministrator({ username: form.username, name: form.name, initialPassword: form.initialPassword, status: form.status, permissions });
}
ElMessage.success(editingId.value ? "管理员已更新" : "管理员已创建");
dialogVisible.value = false;
await load();
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "保存失败");
} finally { saving.value = false; }
}
async function remove(item: ManagedAdmin) {
await ElMessageBox.confirm(`确定删除管理员“${item.name}”?删除后将无法登录。`, "删除管理员", { type: "warning", confirmButtonText: "确定删除" });
await api.deleteAdministrator(item.id);
ElMessage.success("管理员已删除");
await load();
}
onMounted(load);
</script>
<template>
<div class="admin-management">
<div class="page-head inline">
<div><h2>管理员与权限</h2><p>按页面和功能分配权限未授权的页面不会出现后端也会拒绝越权请求</p></div>
<el-button type="primary" @click="openCreate">+新增管理员</el-button>
</div>
<el-card shadow="never">
<el-table :data="admins" stripe>
<el-table-column prop="name" label="管理员" min-width="150" />
<el-table-column prop="username" label="登录账号" min-width="160" />
<el-table-column label="类型" width="130"><template #default="{ row }"><el-tag :type="row.isSuperAdmin ? 'danger' : 'info'">{{ row.isSuperAdmin ? '超级管理员' : '普通管理员' }}</el-tag></template></el-table-column>
<el-table-column label="权限" min-width="150"><template #default="{ row }">{{ row.isSuperAdmin ? '全部权限' : `${row.permissions.length} 项功能` }}</template></el-table-column>
<el-table-column label="状态" width="120"><template #default="{ row }"><el-tag :type="row.status === 1 ? 'success' : 'info'">{{ row.status === 1 ? '正常' : '已停用' }}</el-tag></template></el-table-column>
<el-table-column label="密码状态" width="150"><template #default="{ row }">{{ row.isSuperAdmin ? '环境变量托管' : (row.mustChangePassword ? '待首次修改' : '已修改') }}</template></el-table-column>
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><template v-if="!row.isSuperAdmin"><el-button link type="primary" @click="openEdit(row)">编辑</el-button><el-button link type="danger" @click="remove(row)">删除</el-button></template><span v-else class="locked">系统托管</span></template></el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" class="admin-editor-dialog" :title="editingId ? '编辑管理员' : '新增管理员'" width="820px" destroy-on-close>
<el-form class="admin-editor-form" label-position="top">
<section class="form-section">
<div class="section-title"><strong>基本信息</strong><span>用于登录和识别管理员</span></div>
<div class="form-grid">
<el-form-item label="登录账号"><el-input v-model="form.username" :disabled="!!editingId" placeholder="英文、数字、点、下划线或短横线" /></el-form-item>
<el-form-item label="管理员姓名"><el-input v-model="form.name" placeholder="请输入真实姓名或职位名称" /></el-form-item>
<el-form-item :label="editingId ? '重置密码(可选)' : '初始密码'"><el-input v-if="editingId" v-model="form.resetPassword" show-password placeholder="留空表示不重置" /><el-input v-else v-model="form.initialPassword" show-password placeholder="至少 8 位,首次登录后强制修改" /></el-form-item>
<el-form-item label="账号状态"><div class="status-control"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /><span>{{ form.status === 1 ? '正常,可以登录' : '已停用,无法登录' }}</span></div></el-form-item>
</div>
</section>
<section class="form-section permission-section">
<div class="section-title"><strong>功能权限</strong><span>勾选一级菜单可快速全选也可逐项精细分配</span></div>
<div class="permission-box"><el-tree ref="permissionTree" :data="tree.filter(n => !n.superOnly)" node-key="code" show-checkbox default-expand-all :props="{ label: 'name', children: 'children' }" /></div>
</section>
</el-form>
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存管理员</el-button></template>
</el-dialog>
</div>
</template>
<style scoped>
.admin-editor-form{display:grid;gap:22px}.form-section{padding:18px 20px;border:1px solid #e3e9e6;border-radius:12px;background:#fff}.section-title{display:flex;align-items:baseline;gap:10px;margin-bottom:18px}.section-title strong{font-size:16px}.section-title span{color:#7c8d87;font-size:13px}.form-grid{display:grid;grid-template-columns:1fr 1fr;column-gap:24px;row-gap:18px}.admin-editor-form :deep(.el-form-item){margin-bottom:0}.admin-editor-form :deep(.el-form-item__label){height:auto;padding:0 0 8px;color:#465650;font-weight:600;line-height:1.4}.status-control{display:flex;align-items:center;gap:10px;height:40px}.status-control span{color:#687a74;font-size:13px}.permission-section{background:#f8faf9}.permission-box{width:100%;max-height:390px;overflow:auto;padding:12px 14px;border:1px solid #dfe6e3;border-radius:9px;background:#fff}.locked{color:#909399}.admin-management :deep(.el-tree-node__content){height:34px;border-radius:6px}.admin-management :deep(.el-tree-node__content:hover){background:#eef6f3}.admin-management :deep(.el-dialog__body){max-height:calc(100vh - 190px);padding:20px 24px;overflow:auto}.admin-management :deep(.el-dialog__header){padding:22px 24px 16px;border-bottom:1px solid #edf1ef}.admin-management :deep(.el-dialog__footer){padding:16px 24px 20px;border-top:1px solid #edf1ef}@media(max-width:760px){.form-grid{grid-template-columns:1fr}.section-title{align-items:flex-start;flex-direction:column;gap:4px}}
</style>