first commit
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="角色详情" width="600px" @close="handleClose">
|
||||
<el-descriptions :column="1" border v-loading="loading">
|
||||
<el-descriptions-item label="角色ID">
|
||||
{{ roleDetail.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色名称">
|
||||
{{ roleDetail.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="roleDetail.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ roleDetail.status === 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="权限列表">
|
||||
<div v-if="menuNames.length > 0" style="max-height: 300px; overflow-y: auto;">
|
||||
<el-tag
|
||||
v-for="(name, index) in menuNames"
|
||||
:key="index"
|
||||
style="margin: 4px;"
|
||||
size="small"
|
||||
>
|
||||
{{ name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else style="color: var(--el-text-color-secondary);">
|
||||
暂无权限
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ roleDetail.create_time }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ roleDetail.update_time }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getRoleById } from "@/api/role";
|
||||
import { getAllMenus } from "@/api/menu";
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
roleId?: number | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
roleId: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const roleDetail = ref<any>({});
|
||||
const allMenus = ref<any[]>([]);
|
||||
|
||||
// 监听 modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val && props.roleId) {
|
||||
loadRoleDetail();
|
||||
loadMenus();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 visible
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 解析权限ID
|
||||
const parseRights = (rights: any): number[] => {
|
||||
if (!rights) return [];
|
||||
if (Array.isArray(rights)) return rights;
|
||||
if (typeof rights === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(rights);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 递归获取所有菜单ID和名称的映射
|
||||
const getMenuMap = (menus: any[]): Map<number, string> => {
|
||||
const map = new Map<number, string>();
|
||||
const traverse = (items: any[]) => {
|
||||
for (const item of items) {
|
||||
map.set(item.id, item.title || item.name || `菜单${item.id}`);
|
||||
if (item.children && item.children.length > 0) {
|
||||
traverse(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
traverse(menus);
|
||||
return map;
|
||||
};
|
||||
|
||||
// 计算菜单名称列表
|
||||
const menuNames = computed(() => {
|
||||
if (!roleDetail.value.rights) return [];
|
||||
const rightIds = parseRights(roleDetail.value.rights);
|
||||
const menuMap = getMenuMap(allMenus.value);
|
||||
return rightIds
|
||||
.map((id) => menuMap.get(id))
|
||||
.filter((name) => name !== undefined) as string[];
|
||||
});
|
||||
|
||||
// 加载角色详情
|
||||
const loadRoleDetail = async () => {
|
||||
if (!props.roleId) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getRoleById(props.roleId);
|
||||
if (res.code === 200) {
|
||||
roleDetail.value = res.data || {};
|
||||
} else {
|
||||
ElMessage.error(res.msg || "获取角色详情失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载角色详情失败:", error);
|
||||
ElMessage.error("获取角色详情失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载菜单树
|
||||
const loadMenus = async () => {
|
||||
try {
|
||||
const res = await getAllMenus();
|
||||
if (res.code === 200) {
|
||||
allMenus.value = res.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载菜单失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
roleDetail.value = {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 120px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑角色' : '添加角色'"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入角色名称"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<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="权限设置" prop="rights">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="menuTree"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:props="{ children: 'children', label: 'title' }"
|
||||
:default-checked-keys="form.rights"
|
||||
style="width: 100%; border: 1px solid var(--el-border-color); border-radius: 4px; padding: 10px; max-height: 400px; overflow-y: auto;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { createRole, updateRole } from "@/api/role";
|
||||
import { getAllMenus } from "@/api/menu";
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
role?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
role: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const submitting = ref(false);
|
||||
const formRef = ref();
|
||||
const treeRef = ref();
|
||||
const menuTree = ref<any[]>([]);
|
||||
|
||||
const form = ref({
|
||||
name: "",
|
||||
status: 1,
|
||||
rights: [] as number[],
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: "请输入角色名称", trigger: "blur" },
|
||||
{ min: 2, max: 50, message: "角色名称长度在 2 到 50 个字符", trigger: "blur" },
|
||||
],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "change" }],
|
||||
};
|
||||
|
||||
// 监听 modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
loadMenus();
|
||||
if (props.role) {
|
||||
isEdit.value = true;
|
||||
form.value = {
|
||||
name: props.role.name,
|
||||
status: props.role.status,
|
||||
rights: parseRights(props.role.rights),
|
||||
};
|
||||
// 等待树加载完成后设置选中状态
|
||||
nextTick(() => {
|
||||
if (treeRef.value) {
|
||||
treeRef.value.setCheckedKeys(form.value.rights);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 visible
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 解析权限字符串
|
||||
const parseRights = (rights: any): number[] => {
|
||||
if (!rights) return [];
|
||||
if (Array.isArray(rights)) return rights;
|
||||
if (typeof rights === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(rights);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 加载菜单树
|
||||
const loadMenus = async () => {
|
||||
try {
|
||||
const res = await getAllMenus();
|
||||
if (res.code === 200) {
|
||||
menuTree.value = res.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载菜单失败:", error);
|
||||
ElMessage.error("加载菜单失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
name: "",
|
||||
status: 1,
|
||||
rights: [],
|
||||
};
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
if (treeRef.value) {
|
||||
treeRef.value.setCheckedKeys([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
|
||||
// 获取选中的菜单ID
|
||||
const checkedKeys = treeRef.value.getCheckedKeys();
|
||||
const halfCheckedKeys = treeRef.value.getHalfCheckedKeys();
|
||||
const allCheckedKeys = [...checkedKeys, ...halfCheckedKeys];
|
||||
|
||||
const submitData = {
|
||||
name: form.value.name,
|
||||
status: form.value.status,
|
||||
rights: allCheckedKeys,
|
||||
};
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
let res;
|
||||
if (isEdit.value && props.role) {
|
||||
res = await updateRole(props.role.id, submitData);
|
||||
} else {
|
||||
res = await createRole(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEdit.value ? "更新成功" : "创建成功");
|
||||
emit("success");
|
||||
handleClose();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== false) {
|
||||
// 不是表单验证错误
|
||||
ElMessage.error(error.message || "操作失败");
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-tree) {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>角色管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加角色
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="refresh">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<div v-else>
|
||||
<el-table :data="roles" stripe style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="角色名称"
|
||||
min-width="150"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="rights"
|
||||
label="权限"
|
||||
min-width="150"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleView(row)"
|
||||
>
|
||||
<el-icon><View /></el-icon>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.default !== 1 && row.default !== 2"
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.id !== 1"
|
||||
size="small"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<RoleEditDialog
|
||||
v-model="editDialogVisible"
|
||||
:role="currentRole"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<RoleDetailDialog
|
||||
v-model="detailDialogVisible"
|
||||
:roleId="currentRoleId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, View, Edit, Delete, Refresh } from "@element-plus/icons-vue";
|
||||
import { getAllRoles, getRoleById, deleteRole } from "@/api/role";
|
||||
import RoleEditDialog from "./components/edit.vue";
|
||||
import RoleDetailDialog from "./components/detail.vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
// 使用 auth store 获取用户信息
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const roles = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
const editDialogVisible = ref(false);
|
||||
const detailDialogVisible = ref(false);
|
||||
const currentRole = ref<any>(null);
|
||||
const currentRoleId = ref<number | null>(null);
|
||||
|
||||
// 获取用户信息
|
||||
const userInfo = authStore.user;
|
||||
if (userInfo && userInfo.id) {
|
||||
// console.log('用户名:', userInfo.username || userInfo.nickname);
|
||||
// console.log('用户ID:', userInfo.id);
|
||||
// console.log('角色:', userInfo.role);
|
||||
} else {
|
||||
// console.log('未找到用户信息或用户未登录');
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
if (res.code === 200) {
|
||||
roles.value = res.data || [];
|
||||
console.log('角色列表:', roles.value);
|
||||
} else {
|
||||
error.value = res.msg || "获取角色列表失败";
|
||||
ElMessage.error(error.value);
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "获取角色列表失败";
|
||||
ElMessage.error(error.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
await fetchRoles();
|
||||
if (!error.value) {
|
||||
ElMessage.success("刷新成功");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加角色
|
||||
function handleAdd() {
|
||||
currentRole.value = null;
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function handleView(row: any) {
|
||||
currentRoleId.value = row.id;
|
||||
detailDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑角色
|
||||
function handleEdit(row: any) {
|
||||
currentRole.value = { ...row };
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除角色「${row.name}」吗?删除后不可恢复。`,
|
||||
"警告",
|
||||
{ type: "warning" }
|
||||
);
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await deleteRole(row.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchRoles();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "删除失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "删除失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRoles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
padding: 32px 0 16px 0;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 5px;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user