first commit

This commit is contained in:
2026-01-26 09:32:17 +08:00
commit 9429a700d0
2928 changed files with 30330 additions and 0 deletions
@@ -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>
+217
View File
@@ -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>
+226
View File
@@ -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>