fix:联调权限接口

This commit is contained in:
liangdong
2026-01-13 18:32:56 +08:00
parent c6a4604d1f
commit 05496ae4c4
28 changed files with 1234 additions and 382 deletions

View File

@@ -19,6 +19,16 @@ export const login = (data: { username: string; password: string }) => {
});
};
// 获取字典映射
export const getDictMap = (ids:string) => {
return request.get(`/auth/v1/dict/type/${ids}`);
};
// 获取二级菜单数据
export const getDictMapLevel = (key:string,parentId:string) => {
return request.get(`/auth/v1/dict/type/${key}/${parentId}`);
};
/**员工公用接口*/

View File

@@ -50,12 +50,12 @@ export const batchSaveRole = (roleId: string,data: number[]) => {
// 保存角色权限
export const saveRolePermission = (data: any) => {
return request.post(`/auth/v1/backend/role/permission/save`, data);
return request.post(`/auth/v1/backend/role/permissions/save`, data);
}
// 查询角色权限
// 查询角色权限 (获取角色权限详情)
export const getRolePermission = (roleId: string) => {
return request.get(`/auth/v1/backend/role/${roleId}/permission`);
return request.get(`/auth/v1/backend/role/${roleId}/permissions`);
}
// 获取角色成员列表

View File

@@ -0,0 +1,126 @@
<template>
<div ref="tableContainerRef" class="pro-table-v2-container">
<el-table-v2
ref="tableRef"
v-bind="$attrs"
:columns="adaptedColumns"
:data="data"
:width="tableSize.width"
:height="tableSize.height"
:fixed="true"
@rows-rendered="handleRowsRendered"
>
<template #overlay v-if="loading">
<div class="v2-loading-overlay">
<el-icon class="is-loading" :size="26"><Loading /></el-icon>
</div>
</template>
</el-table-v2>
<div class="pro-table-v2-footer">
<div class="footer-left">
<span>已加载 {{ data.length }} / {{ total }} </span>
</div>
<div class="footer-right">
<span v-if="loading">加载中...</span>
<span v-else-if="noMore">已加载全部</span>
</div>
</div>
</div>
</template>
<script setup lang="tsx">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { debounce } from 'lodash-es';
import { ElTag, ElText } from 'element-plus';
import NameAvatar from "@/components/NameAvatar/index.vue";
import dayjs from 'dayjs';
const props = defineProps({
columns: { type: Array, required: true },
data: { type: Array, required: true },
total: { type: Number, default: 0 },
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["load-more"]);
// --- 内置渲染逻辑工厂 ---
const builtInRenderers = {
// 1. 人员头像渲染器 (默认读取 row.name 和 row.avatar)
member: (scope: any, col: any) => (
<div style="display:flex; align-items:center; gap:8px;">
<NameAvatar
name={scope.rowData[col.prop]}
src={scope.rowData[col.avatarKey || 'avatar']}
size={28}
/>
<span>{scope.rowData[col.prop]}</span>
</div>
),
// 2. 状态标签渲染器 (支持自定义映射)
status: (scope: any, col: any) => {
const val = scope.rowData[col.prop];
const option = col.options?.find((opt: any) => opt.value === val) || { label: val, type: 'info' };
return <ElTag type={option.type} size="small">{option.label}</ElTag>;
},
// 3. 日期格式化
date: (scope: any, col: any) => {
const val = scope.rowData[col.prop];
return <span>{val ? dayjs(val).format(col.format || 'YYYY-MM-DD HH:mm') : '-'}</span>;
},
// 4. 金额格式化
money: (scope: any, col: any) => {
const val = scope.rowData[col.prop];
return <ElText type="warning">¥ {Number(val || 0).toLocaleString()}</ElText>;
}
};
const adaptedColumns = computed(() => {
return props.columns.map((col: any) => ({
key: col.prop,
dataKey: col.prop,
title: col.label,
width: col.width || 150,
fixed: col.fixed,
align: col.align || 'left',
cellRenderer: (scope: any) => {
// 优先级 1: 用户自定义了 render 函数
if (typeof col.render === 'function') return col.render(scope);
// 优先级 2: 使用内置的 valueType 渲染器
if (col.valueType && builtInRenderers[col.valueType]) {
return builtInRenderers[col.valueType](scope, col);
}
// 优先级 3: 默认展示
return scope.rowData[col.prop] ?? '-';
},
}));
});
// --- 容器与滚动逻辑 (保持之前的一致) ---
const tableContainerRef = ref(null);
const tableSize = ref({ width: 0, height: 400 });
const noMore = computed(() => props.data.length >= props.total && props.total > 0);
const updateSize = () => {
if (tableContainerRef.value) {
const rect = tableContainerRef.value.getBoundingClientRect();
tableSize.value.width = rect.width;
tableSize.value.height = window.innerHeight - rect.top - 45;
}
};
const handleResize = debounce(updateSize, 200);
const handleRowsRendered = ({ endRowIndex }: any) => {
if (endRowIndex >= props.data.length - 5 && !props.loading && !noMore.value) {
emit("load-more");
}
};
onMounted(() => { updateSize(); window.addEventListener('resize', handleResize); });
onUnmounted(() => { window.removeEventListener('resize', handleResize); });
</script>

View File

@@ -20,7 +20,7 @@
<template #title>{{ row.meta.title }}</template>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.path">
<el-menu-item v-else :index="getFirstChildPath(item)">
<el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
<template #title>{{ item.meta.title }}</template>
</el-menu-item>
@@ -68,6 +68,18 @@ const resolvePath = (parentPath: string, childPath: string) => {
// 4. 返回拼接后的路径
return `${parent}/${child}`;
};
/**
* 获取菜单项的跳转路径
* 如果是顶级菜单且有子项,点击应跳转到第一个子项
*/
const getFirstChildPath = (item: any) => {
// 如果是顶级菜单且有子菜单
if (item.children && item.children.length > 0) {
return resolvePath(item.path, item.children[0].path);
}
// 如果没有子菜单,直接返回 item.path但要确保它是以 / 开头
return item.path.startsWith('/') ? item.path : `/${item.path}`;
};
// 处理菜单选中事件
const handleMenuSelect = (index: string) => {

View File

@@ -1,37 +0,0 @@
// 后台 - 权限管理模块-权限内容信息
// 权限状态映射
const roleDict = {
1: '启用',
0: '禁用'
}
// 权限状态颜色
const roleDictColor = {
1:'#66E5BE',
0:'#90A1B9'
}
// 创建角色类型字典
const roleTypeOptions = [
{ label: '实例角色', value: 'INSTANCE' },
{ label: '默认角色', value: 'DEFAULT' },
{ label: '系统角色', value: 'SYSTEM' }
]
// 设置权限转换为目标格式
const statusOptions = Object.keys(roleDict).map((key) => {
return {
label: roleDict[key],
value: Number(key)
}
})
export default {
roleDict,
roleDictColor,
statusOptions,
roleTypeOptions
}

View File

@@ -1,29 +1,13 @@
// 后台 - 字典管理模块-字典内容信息
// 字典状态映射
const statusDict = {
1: '正常',
0: '禁用'
}
// 字典状态颜色
const statusDictColor = {
1:'#66E5BE',
0:'#90A1B9'
}
// 设置字典转换为目标格式
const statusOptions = Object.keys(statusDict).map((key) => {
return {
label: statusDict[key],
value: Number(key)
}
})
export default {
statusDict,
statusDictColor,
statusOptions
}

View File

@@ -9,6 +9,5 @@ Object.entries(modules).forEach(([path, module]: [string, any]) => {
// 2. 统一导出
export const {
DictManage,
PermissionManage
} = components;
export default components;

79
src/hooks/useDictData.ts Normal file
View File

@@ -0,0 +1,79 @@
import { ref, reactive, onMounted } from 'vue';
import { getDictMap,getDictMapLevel } from '@/api';
// 全局静态缓存,跨组件共享
const dictCache = reactive({});
const pendingPromises = new Map();
export function useDict(codes) {
const dicts = ref({});
const loading = ref(false);
const fetchDicts = async () => {
if (!codes) return;
const codeArray = codes.split(',').map(s => s.trim());
const result = {};
const needFetch = [];
// 1. 区分哪些在缓存,哪些需要查
codeArray.forEach(code => {
if (dictCache[code]) {
result[code] = dictCache[code];
} else {
needFetch.push(code);
}
});
if (needFetch.length === 0) {
dicts.value = result;
return;
}
// 2. 处理并发合并
const fetchKey = needFetch.sort().join(',');
if (!pendingPromises.has(fetchKey)) {
const p = getDictMap(codes).then(res => {
const data = res || [];
Object.assign(dictCache, data);
pendingPromises.delete(fetchKey);
return data;
});
pendingPromises.set(fetchKey, p);
}
loading.value = true;
try {
const remoteData = await pendingPromises.get(fetchKey);
dicts.value = { ...result, ...remoteData };
} finally {
loading.value = false;
}
};
const fetchLevel = async (code, parentId) => {
if (!code || parentId === undefined) return [];
// 构造层级缓存 Key例如 "GENDER_1"
const cacheKey = `${code}_${parentId}`;
if (dictCache[cacheKey]) {
return dictCache[cacheKey];
}
loading.value = true;
try {
// 假设 getDictMapLevel 接受 code 和 parentId
const res = await getDictMapLevel(code, parentId);
const data = res || [];
// 写入全局缓存
dictCache[cacheKey] = data;
return data;
} finally {
loading.value = false;
}
};
onMounted(fetchDicts);
return { dicts, loading,refresh: fetchDicts,fetchLevel };
}

View File

@@ -302,36 +302,26 @@ export const mockBackendMenuData = [
"name": "字典管理",
"code": "dict",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "组织管理",
"code": "origanization",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "人员管理",
"code": "personnel",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "权限管理",
"code": "permission",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "流程管理",
"code": "flow",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
}
]

View File

@@ -35,7 +35,7 @@
@menu-select="handleTopMenuSelect"
/>
<!-- 右侧用户的内容 -->
<rightMenuGroup @on-stage-manage="onStageManage" />
<rightMenuGroup @on-stage-manage="(path)=>handleTopMenuSelect(path)" />
</el-header>
<el-main class="mj-main-backend-content">
<router-view />
@@ -55,6 +55,7 @@ defineOptions({
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
// 响应式断点(小屏阈值,小于此值视为小屏)
const BREAKPOINT = 1024;
@@ -104,10 +105,7 @@ const backTitle = computed(()=>{
return menuList.value.find(itv=>itv.name === 'stage')?.meta?.title || '-';
})
// 后台管理点击获取列表
const onStageManage = () =>{
selectedTopMenu.value = '/stage';
}
const topTitle = computed(() => {
return (
@@ -165,6 +163,18 @@ const sideMenuList = computed(() => {
// 处理顶部菜单选中事件
const handleTopMenuSelect = (menuPath: string) => {
selectedTopMenu.value = menuPath;
const currentModule = menuList.value.find(item => item.path === menuPath);
if (currentModule && currentModule.children && currentModule.children.length > 0) {
const firstChild = currentModule.children[0];
const targetPath = firstChild.path.startsWith("/")
? firstChild.path
: `${currentModule.path}/${firstChild.path}`;
router.push(targetPath);
selectedActiveMenu.value = targetPath;
} else {
router.push(menuPath);
}
};
// 左侧菜单选中事件
@@ -172,23 +182,37 @@ const handleSideMenuSelect = (menuPath: string) => {
selectedActiveMenu.value = menuPath;
};
// 高亮当前激活的菜单
const activeMenuByUrl = () => {
// 1. 获取当前路由路径,例如 "/stage/dict" 或 "/business/customer"
const currentPath = route.path;
// 2. 尝试从 topLevelMenuList 中直接找匹配项
let matchedMenu = topLevelMenuList.value.find(menu =>
currentPath.startsWith(menu.path)
);
// 3. 如果没找到,且路径以 /stage 开头 那就是后台管理模块
if (!matchedMenu && currentPath.startsWith('/stage')) {
selectedTopMenu.value = '/stage';
}else{
// 4. 赋值选中的菜单
if (matchedMenu) {
selectedTopMenu.value = matchedMenu.path;
} else if (topLevelMenuList.value.length > 0) {
selectedTopMenu.value = topLevelMenuList.value[0].path;
}
}
};
watch(() => route.path, () => {
activeMenuByUrl();
}, { immediate: true });
// 初始化:默认选中第一个菜单
onMounted(() => {
const currentRoutePath = router.currentRoute.value.path;
const matchedTopMenu = topLevelMenuList.value.find(menu =>
currentRoutePath.startsWith(`${menu.path}/`) || currentRoutePath === menu.path
);
if (matchedTopMenu && matchedTopMenu.path) {
selectedTopMenu.value = matchedTopMenu.path;
} else if (topLevelMenuList.value.length > 0) {
// 否则默认选中第一个菜单
const firstMenu = topLevelMenuList.value[0];
if (firstMenu && firstMenu.path) {
selectedTopMenu.value = firstMenu.path;
}
}
activeMenuByUrl();
// 监听窗口大小变化
window.addEventListener("resize", handleResize);

View File

@@ -80,7 +80,7 @@ const handleCommand = (command: string) => {
};
const onStageManage = () => {
emits("on-stage-manage");
emits("on-stage-manage",'/stage');
};
// 获取当前的用户的数据信息

View File

@@ -110,19 +110,9 @@
}"
@click="handleDictStatus(row)"
>
{{ DictManage.statusDict[row.status] }}
{{ dicts.permission_list_enable_disable.find(item=>item.value == row.status)?.label }}
</div>
</template>
<!-- <template #actions="{ row }">
<el-button link type="primary" v-if="!hasChild" @click="handleAddNext(row)"
>添加二级字段</el-button>
<el-button link type="primary" @click="handleEdit(row)"
>编辑</el-button
>
<el-button link type="danger" @click="handleDelete(row)"
>删除</el-button
>
</template> -->
</CommonTable>
<!-- 新增字段 -->
<dictFieldLevelManage
@@ -147,6 +137,8 @@ import CommonTable from "@/components/proTable/index.vue";
import dayjs from "dayjs";
import dictFieldLevelManage from "./dictFieldLevelManage.vue";
import { DictManage } from "@/dict";
import { useDict } from '@/hooks/useDictData';
const { dicts,refresh } = useDict('permission_list_enable_disable');
import {
getDictTypeValue,
deleteDictTypeValue,

View File

@@ -18,10 +18,17 @@
label-width="120px"
>
<el-form-item label="字段名称:" prop="label">
<el-input placeholder="请输入字典名称" v-model="form.label"></el-input>
<el-input
placeholder="请输入字典名称"
v-model="form.label"
></el-input>
</el-form-item>
<el-form-item label="字典值:" prop="value">
<el-input placeholder="请输入字典值" v-model="form.value" :disabled="form.id ? true : false"></el-input>
<el-input
placeholder="请输入字典值"
v-model="form.value"
:disabled="form.id ? true : false"
></el-input>
</el-form-item>
<el-form-item prop="sort">
<template #label>
@@ -34,12 +41,21 @@
</div>
</template>
<!-- 换成排序的输入框 -->
<el-input-number placeholder="请输入排序" v-model="form.sort" :min="0" controls-position="right"></el-input-number>
<el-input-number
placeholder="请输入排序"
v-model="form.sort"
:min="0"
controls-position="right"
></el-input-number>
</el-form-item>
<el-form-item label="状态:" prop="status">
<el-radio-group v-model="form.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">停用</el-radio>
<el-radio
:value="item.value"
v-for="(item, index) in dicts.permission_list_enable_disable"
:key="item.value"
>{{ item.label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item label="备注:" prop="remark">
@@ -55,7 +71,12 @@
</el-form>
<template #footer>
<el-button @click="onCancel">取消</el-button>
<el-button @click="onConfirm(ruleFormRef)" type="primary" :loading="loading">确认</el-button>
<el-button
@click="onConfirm(ruleFormRef)"
type="primary"
:loading="loading"
>确认</el-button
>
</template>
</el-dialog>
</div>
@@ -64,6 +85,8 @@
import { reactive, ref, onMounted } from "vue";
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import { saveDictTypeValue, updateDictTypeValue } from "@/api/stage/dict";
import { useDict } from "@/hooks/useDictData";
const { dicts, refresh } = useDict("permission_list_enable_disable");
defineOptions({ name: "DictFieldLevelManage" });
const loading = ref(false);
const {
@@ -74,12 +97,10 @@ const {
} = defineProps<{
dialogVisible: boolean;
row?: any;
parentId?: string|number;
parentId?: string | number;
title?: string;
}>();
const emit = defineEmits<{
(e: "update:dialogVisible", value: boolean): void;
(e: "confirm-success"): void;
@@ -90,22 +111,26 @@ const form = reactive({
label: "",
value: "",
sort: 0,
status:1,
remark:''
status: '1',
remark: "",
});
// 监听组件中传递的数据-然后进行复制操作
watch(()=>row,(newRow)=>{
if (newRow && Object.keys(newRow).length > 0) {
Object.assign(form, newRow);
watch(
() => row,
(newRow) => {
if (newRow && Object.keys(newRow).length > 0) {
Object.assign(form, newRow,{status:String(newRow.status)});
}
},{deep:true})
},
{ deep: true }
);
const rules = reactive({
label: [{ required: true, message: "请输入字段名称", trigger: "blur" }],
value: [{ required: true, message: "请输入字典值", trigger: "blur" }],
sort: [{ required: true, message: "请输入排序", trigger: "blur" }],
remark:[{ required: false, message: "请输入备注", trigger: "blur" }]
remark: [{ required: false, message: "请输入备注", trigger: "blur" }],
});
// 确定
@@ -114,16 +139,18 @@ const onConfirm = async (formEl: FormInstance | undefined) => {
await formEl.validate(async (valid, fields) => {
if (valid) {
loading.value = true;
console.log("获取外部的数据信息:",form,parentId)
console.log("获取外部的数据信息:", form, parentId);
// 如果是一级新增字段就是parentId为0 如果是添加二级字段就是parentId为父级的id
try {
const response = row.id ? await updateDictTypeValue(parentId,row.id,form) : await saveDictTypeValue(parentId,form);
ElMessage.success(row.id ? '修改成功' : '新增成功');
const response = row.id
? await updateDictTypeValue(parentId, row.id, form)
: await saveDictTypeValue(parentId, form);
ElMessage.success(row.id ? "修改成功" : "新增成功");
onCancel();
emit('confirm-success');
emit("confirm-success");
} catch (error) {
console.log('error',error);
} finally{
console.log("error", error);
} finally {
loading.value = false;
}
} else {

View File

@@ -34,8 +34,12 @@
</el-form-item>
<el-form-item label="状态:" prop="status">
<el-radio-group v-model="form.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">停用</el-radio>
<el-radio
:value="item.value"
v-for="(item, index) in dicts.permission_list_enable_disable"
:key="item.value"
>{{ item.label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item label="备注:">
@@ -60,6 +64,8 @@
import { reactive, ref, onMounted } from "vue";
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import { addDictValue, updateDictValue } from "@/api/stage/dict";
import { useDict } from "@/hooks/useDictData";
const { dicts, refresh } = useDict("permission_list_enable_disable");
defineOptions({ name: "DictManage" });
const loading = ref(false);
const {
@@ -80,14 +86,14 @@ const ruleFormRef = ref<FormInstance>();
const form = reactive({
name: "",
key: "",
status: 1,
status: '1',
remark: "",
});
// 监听组件中传递的数据-然后进行复制操作
watch(()=>row,(newRow)=>{
if (newRow && Object.keys(newRow).length > 0) {
Object.assign(form, newRow);
Object.assign(form, newRow,{status:String(newRow.status)});
}
},{deep:true})

View File

@@ -33,7 +33,7 @@
<el-option
:label="item.label"
:value="item.value"
v-for="(item, index) in DictManage.statusOptions"
v-for="(item, index) in dicts.permission_list_enable_disable"
:key="index"
/>
</el-select>
@@ -89,19 +89,9 @@
}"
@click="handleDictStatus(row)"
>
{{ DictManage.statusDict[row.status] }}
{{ dicts.permission_list_enable_disable.find(item=>item.value == row.status)?.label }}
</div>
</template>
<!-- <template #actions="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="primary" @click="handlefieldsConfig(row)"
>字段配置</el-button
>
<el-button link type="danger" @click="handleDelete(row)"
>删除</el-button
>
</template> -->
</CommonTable>
<!-- 新增-编辑字典弹窗 -->
@@ -130,6 +120,9 @@ import { DictManage } from "@/dict";
import { formatIndex } from "@/utils/utils";
import { ElMessage } from "element-plus";
import { useDict } from '@/hooks/useDictData';
const { dicts,refresh } = useDict('permission_list_enable_disable');
defineOptions({ name: "Dictionary" });
const fieldsConfigRef = ref(null);
const dictTableRef = ref(null);

View File

@@ -28,7 +28,7 @@
<div class="full-width-radio">
<BaseSegmented
v-model="form.type"
:options="PermissionManage.roleTypeOptions"
:options="dicts.permission_role_type"
/>
</div>
</el-form-item>
@@ -117,7 +117,6 @@
<script lang="ts" setup>
import BaseSegmented from "./baseSegmented.vue";
import { PermissionManage } from "@/dict";
import { Loading } from "@element-plus/icons-vue";
import type { FormInstance, FormRules } from "element-plus";
import {
@@ -127,6 +126,8 @@ import {
} from "@/api/stage/permission/index.ts";
import { getEnterprisePosition } from "@/api/stage/organization";
import { useSelectLoadMore } from "@/hooks/useSelectLoadMore";
import { useDict } from '@/hooks/useDictData';
const { dicts,refresh } = useDict('permission_role_type');
defineOptions({ name: "addRoles" });
const dialogVisible = defineModel("visible", { type: Boolean, default: false });
const props = defineProps({
@@ -135,7 +136,6 @@ const props = defineProps({
default: "",
},
});
const {
options,
remoteLoading,
@@ -219,6 +219,11 @@ const handleSubmit = (formEl: FormInstance | undefined) => {
}
});
};
// 初始化刷新角色类型值
onMounted(() => {
refresh();
});
</script>
<style lang="scss" scoped>

View File

@@ -1,16 +1,14 @@
<template>
<div class="permission-scroll-area">
<div v-for="group in permissions" :key="group.id" class="permission-group">
<div v-for="group in menuList" :key="group.id" class="permission-group">
<div class="group-header">
<el-checkbox
v-model="group.allSelected"
v-model="group.selected"
:indeterminate="group.isIndeterminate"
@change="(val) => handleGroupCheckAll(group, val)"
>
<span class="group-title">{{ group.name }}</span>
<span class="group-count"
>({{ getCheckedCount(group) }}/{{ group.children.length }})</span
>
<span class="group-count" v-if="group.children">({{ getCheckedCount(group) }}/{{ group.children.length }})</span>
</el-checkbox>
</div>
@@ -18,23 +16,19 @@
<div v-for="row in group.children" :key="row.id" class="permission-row">
<div class="row-label">
<el-checkbox
v-model="row.checked"
v-model="row.selected"
:indeterminate="row.isIndeterminate"
@change="() => handleRowChange(group, row)"
>
{{ row.name }}
</el-checkbox>
</div>
<div class="row-actions">
<el-checkbox-group
v-model="row.actions"
:disabled="!row.checked"
@change="() => handleActionChange(group, row)"
>
<el-checkbox value="add">新增</el-checkbox>
<el-checkbox value="delete" class="is-danger">删除</el-checkbox>
<el-checkbox value="import">导入</el-checkbox>
<el-checkbox value="export">导出</el-checkbox>
<el-checkbox :value="check.id" v-for="(check,checkIndex) in row.operations" :class="check.code.search('delete') > -1 ? 'is-danger' : ''">{{ check.name }}</el-checkbox>
</el-checkbox-group>
</div>
</div>
@@ -43,123 +37,55 @@
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from "vue";
defineOptions({name: "baseSegmentedMenu"});
// 模拟数据结构
const permissions = reactive([
{
id: 1,
name: "项目管理",
allSelected: false,
isIndeterminate: true,
children: [
{
id: 11,
name: "需求管理",
checked: true,
actions: ["add", "delete", "import", "export"],
},
{ id: 12, name: "项目管理", checked: false, actions: [] },
{ id: 13, name: "任务管理", checked: false, actions: [] },
],
const props = defineProps({
menuList: {
type: Array,
default: () => [],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
]);
});
// 获取已选中的子项数量
const getCheckedCount = (group) =>
group.children.filter((item) => item.checked).length;
const getCheckedCount = (group) =>{
return (group.children || []).filter((item) => item.selected).length;
}
// 处理一级全选
const handleGroupCheckAll = (group, val) => {
group.children.forEach((row) => {
row.checked = val;
row.actions = val ? ["add", "delete", "import", "export"] : [];
row.selected = val;
row.actions = val ? row.operations.map((item) => item.id) : [];
});
group.isIndeterminate = false;
};
// 处理二级勾选
const handleRowChange = (group, row) => {
if (!row.checked) row.actions = [];
row.actions = row.selected ? row.operations.map((item) => item.id) : [];
updateGroupStatus(group);
};
// 处理三级按钮勾选
const handleActionChange = (group, row) => {
if (row.actions.length > 0) row.checked = true;
if (row.actions.length > 0) row.selected = true;
const allActions = row.operations && row.operations.length;
const checkedActionsCount = row.actions ? row.actions.length : 0;
row.isIndeterminate = checkedActionsCount > 0 && checkedActionsCount < allActions;
row.selected = checkedActionsCount > 0 && checkedActionsCount === allActions;
updateGroupStatus(group);
};
// 更新父级的半选/全选状态
const updateGroupStatus = (group) => {
const checkedCount = getCheckedCount(group);
group.allSelected = checkedCount === group.children.length;
group.isIndeterminate =
checkedCount > 0 && checkedCount < group.children.length;
const children = group.children || [];
const total = children.length;
const fullyCheckedCount = children.filter(c => c.selected && !c.isIndeterminate).length;
const anyCheckedCount = children.filter(c => c.selected || c.isIndeterminate).length;
group.selected = total > 0 && fullyCheckedCount === total;
group.isIndeterminate = !group.selected && anyCheckedCount > 0;
};
</script>
<style lang="scss" scoped>
@use './baseSegmentedPermission.scss' as *;
</style>

View File

@@ -20,8 +20,6 @@
</template>
<script setup>
import { inject } from "vue";
import { computed } from "vue";
import { useFormItem } from "element-plus";
const props = defineProps({
modelValue: [String, Number],
@@ -31,6 +29,11 @@ const props = defineProps({
},
});
watch(() => props.options, (val) => {
// 1. 获取父组件的值
console.log("父组件的值111", val);
},{deep:true});
const emit = defineEmits(["update:modelValue", "change"]);
// 生成唯一ID防止页面存在多个组件时 name 冲突
@@ -42,7 +45,7 @@ const handleChange = (val) => {
emit("update:modelValue", val);
emit("change", val);
// 3. 关键:通知 el-form-item 进行校验
// 通知 el-form-item 进行校验
if (formItem) {
formItem.validate("change").catch(() => {});
}

View File

@@ -5,7 +5,7 @@
v-for="mod in modules"
:key="mod.id"
class="module-card"
:class="{ 'is-active': modelValue === mod.id }"
:class="{ 'is-active': activeModules === mod.id }"
@click="handleModuleChange(mod.id)"
>
{{ mod.name }}
@@ -13,23 +13,24 @@
</div>
<div class="field-group-list">
<template v-if="fieldGroupsChildren.length">
<div
v-for="(group, index) in fieldGroups"
:key="group.groupId"
v-for="(group, index) in fieldGroupsChildren"
:key="group.id"
class="group-container"
:class="{ 'is-collapsed': group.collapsed }"
:class="{ 'is-collapsed': collapsedIds.has(group.id) }"
>
<div class="group-header" :class="{ 'is-collapsed': group.collapsed }">
<div class="group-header" :class="{ 'is-collapsed': collapsedIds.has(group.id) }">
<div class="header-left">
<el-icon><List /></el-icon>
<span class="group-title">{{ group.groupName }}</span>
<span class="group-title">{{ group.name }}</span>
</div>
<div class="header-right">
<span class="quick-set-label">快速设置</span>
<div class="quick-actions">
<span
v-for="opt in quickOptions"
v-for="opt in dicts.permission_setting_status"
:key="opt.value"
class="quick-btn"
:class="{ 'is-disabled': isProcessing }"
@@ -38,7 +39,7 @@
{{ opt.label }}
</span>
</div>
<el-icon class="collapse-icon" @click.stop="toggleGroup(index)"
<el-icon class="collapse-icon" @click.stop="toggleGroup(group)"
><ArrowDown
/></el-icon>
</div>
@@ -56,15 +57,22 @@
</div>
<div class="field-ctrl">
<el-select v-model="field.permission">
<el-option label="可查看" value="read" />
<el-option label="可编辑" value="edit" />
<el-option label="不可查看" value="none" />
<el-option
:label="dict.label"
:value="dict.value"
v-for="(
dict, dicIndex
) in dicts.permission_setting_status"
:key="dict.value"
/>
</el-select>
</div>
</div>
</div>
</div>
</div>
</template>
<el-empty description="暂无数据~" v-else/>
</div>
</div>
</template>
@@ -74,26 +82,61 @@ import { ref, computed } from "vue";
import { List, Document, ArrowDown } from "@element-plus/icons-vue";
const props = defineProps({
// 模块列表
modules: { type: Array, default: () => [] },
// 初始选中的模块ID
modelValue: [String, Number],
fieldGroups: {
dicts:{
type:Object,
default:()=>({})
},
fieldGroupsList: {
type: Array,
default: () => [],
},
}
});
const emit = defineEmits(["update:modelValue", "update:fieldGroups", "change"]);
const emit = defineEmits(["update:fieldGroupsList"]);
const isProcessing = ref(false);
const processingGroupId = ref(null);
const currentActiveId = ref(null);
const collapsedIds = ref(new Set()); //可折叠的集合
// 渲染顶部的数据
const modules = computed(() => {
return props.fieldGroupsList.map((group) => {
return {
id: group.id,
name: group.name,
};
});
});
// 快速设置选项
const quickOptions = [
{ label: "可查看", value: "read" },
{ label: "可编辑", value: "edit" },
{ label: "不可查看", value: "none" },
];
// 动态渲染activeModules的值
const activeModules = computed(() => currentActiveId.value);
// 动态匹配对应的下级数据
const fieldGroupsChildren = computed({
get: () => {
if (!currentActiveId.value) return [];
return (
props.fieldGroupsList.find((group) => group.id === currentActiveId.value)
?.fieldGroups || []
);
},
set: () => {},
});
// 监听值的变化
watch(
() => props.fieldGroupsList,
(newList) => {
if (newList.length > 0) {
const exists = newList.find((item) => item.id === currentActiveId.value);
if (!exists) {
currentActiveId.value = newList[0].id;
}
} else {
currentActiveId.value = null;
}
},
{ immediate: true, deep: true }
);
const batchSetPermissionChunked = (group, type) => {
if (isProcessing.value) return;
@@ -105,6 +148,8 @@ const batchSetPermissionChunked = (group, type) => {
isProcessing.value = true;
processingGroupId.value = group.groupId;
// 同步更新permission的数据
group.permission = type;
const runChunk = () => {
const nextLimit = Math.min(currentIndex + chunkSize, total);
@@ -120,31 +165,27 @@ const batchSetPermissionChunked = (group, type) => {
} else {
isProcessing.value = false;
processingGroupId.value = null;
emit("update:fieldGroups", [...props.fieldGroups]);
emit("update:fieldGroupsList", [...props.fieldGroupsList]);
}
};
runChunk();
};
// 切换折叠逻辑
const toggleGroup = (index) => {
console.log("");
props.fieldGroups[index].collapsed = !props.fieldGroups[index].collapsed;
// 切换折叠面板
const toggleGroup = (group) => {
if (collapsedIds.value.has(group.id)) {
collapsedIds.value.delete(group.id);
} else {
collapsedIds.value.add(group.id);
}
};
// 切换顶部权限的模块
const handleModuleChange = (id) => {
emit("update:modelValue", id);
emit("change", id);
currentActiveId.value = id;
};
// 批量设置权限逻辑
const batchSetPermission = (group, type) => {
group.fields.forEach((field) => {
field.permission = type;
});
emit("update:fieldGroups", [...props.fieldGroups]);
};
</script>
<style lang="scss" scoped>

View File

@@ -41,11 +41,11 @@
<div
class="mj-status-dot"
:style="{
'--data-status-color': PermissionManage.roleDictColor[row.status],
'--data-status-color': DictManage.statusDictColor[row.status],
}"
@click="handleDictStatus(row)"
>
{{ PermissionManage.roleDict[row.status] }}
{{ dicts.permission_list_enable_disable.find(item=>item.value == row.status)?.label }}
</div>
</template>
@@ -78,11 +78,11 @@
<add-roles
v-model:visible="showRoles"
@on-success="onRefresh"
:detail-id="detailId"
:detail-id="selectMember?.id"
/>
<!-- 权限抽屉 -->
<permission-drawer v-model:visible="showPermission" />
<permission-drawer v-model:visible="showPermission" :checkAuth="selectMember"/>
</template>
<script setup lang="ts">
import dayjs from "dayjs";
@@ -90,7 +90,8 @@ import CommonTable from "@/components/proTable/index.vue";
import memberSelector from "@/components/memberSelector/index.vue";
import addRoles from "./addRoles.vue";
import permissionDrawer from "./permissionDrawer.vue";
import { PermissionManage } from "@/dict";
import { DictManage } from "@/dict";
import { useDict } from '@/hooks/useDictData';
import { formatIndex } from "@/utils/utils";
import {
getRoleList,
@@ -106,6 +107,7 @@ import {
} from "@/api/stage/permission/index.ts";
import { useLocalManager } from "@/hooks/useLocalManager";
defineOptions({ name: "PermissionManagement" });
const { dicts,refresh } = useDict('permission_list_enable_disable');
const activeTab = ref(1);
const tableRef = ref(null);
const searchVal = ref("");
@@ -114,7 +116,6 @@ const memberList = ref([]); // 获取成员角色列表
const total = ref(0);
const showMember = ref(false);
const showRoles = ref(false);
const detailId = ref("");
const showPermission = ref(false);
const selectMember = ref({}); //当前选择的成员数量数据
const tabList = [
@@ -208,13 +209,14 @@ const roleColumns = [
permission: ["*"],
onClick: (row) => {
showPermission.value = true;
selectMember.value = {...row};
},
},
{
label: "复制",
type: "default",
link: true,
permission: ["edit"],
permission: ["*"],
onClick: async (row) => {
try {
await copyRolePermission(row.id);
@@ -229,17 +231,17 @@ const roleColumns = [
label: "编辑",
type: "default",
link: true,
permission: ["config"],
permission: ["*"],
onClick: (row) => {
showRoles.value = true;
detailId.value = row.id;
selectMember.value = {...row};
},
},
{
label: "删除",
type: "danger",
link: true,
permission: ["delete"],
permission: ["*"],
disabledFn: (row) => {
return row.type !== "SYSTEM";
},
@@ -278,7 +280,7 @@ const tableColumns = computed(() => {
// 当前成员数量
const addMember = (row) => {
showMember.value = true;
selectMember.value = row;
selectMember.value = {...row};
getMemberList({ id: row.id });
};
@@ -309,7 +311,7 @@ watch(activeTab, () => {
tableRef.value && tableRef.value.reset();
});
// 权限
// 启用-停用
const handleDictStatus = async (row) => {
try {
row.status === 1 ? await disableRole(row.id) : await enableRole(row.id);
@@ -349,7 +351,7 @@ const getMemberList = async (params) => {
// 获取
const checkRolesText = computed(() => {
const btnText = {
const btnText = {
1: "新增角色",
}[activeTab.value];
@@ -366,10 +368,15 @@ const checkRolesText = computed(() => {
// 新增角色
const addBtnClick = () => {
if (activeTab.value === 1) {
detailId.value = "";
showRoles.value = true;
selectMember.value = {id:''}
}
};
// 初始化
onMounted(() => {
refresh();
});
</script>
<style lang="scss" scoped>
.mj-permission-management {

View File

@@ -20,7 +20,7 @@
/></el-icon>
</div>
<div class="sub-title">
正在为<span class="sub-roles">[超级管理员]&nbsp;</span>分配系统访问权限
正在为<span class="sub-roles">[{{ personAuth }}]&nbsp;</span>分配系统访问权限
</div>
</div>
<!-- 内容 -->
@@ -29,14 +29,12 @@
<div class="permission-container">
<el-tabs v-model="activeTab" class="custom-permission-tabs">
<el-tab-pane label="菜单权限" name="menu">
<base-segment-menu></base-segment-menu>
<base-segment-menu :menuList="menuList"></base-segment-menu>
</el-tab-pane>
<el-tab-pane label="字段权限" name="field">
<fieldPermissionManager
v-model="currentModule"
:modules="moduleList"
v-model:field-groups="fieldsList"
@change="onModuleTabChange"
:dicts="dicts"
v-model:field-groups-list="fieldsList"
/>
</el-tab-pane>
</el-tabs>
@@ -49,7 +47,7 @@
<div class="stats-info"></div>
<div class="actions">
<el-button link @click="drawerVisible = false">取消</el-button>
<el-button type="primary" class="btn-confirm">确认应用</el-button>
<el-button type="primary" class="btn-confirm" @click="handleSavePermission" :loading="loading">确认应用</el-button>
</div>
</div>
</template>
@@ -59,44 +57,160 @@
import { Close } from "@element-plus/icons-vue";
import baseSegmentMenu from "./baseSegmentMenu.vue";
import fieldPermissionManager from "./fieldPermissionManager.vue";
import { saveRolePermission,getRolePermission } from '@/api/stage/permission';
import { useDict } from '@/hooks/useDictData';
const { dicts,refresh } = useDict('permission_setting_status');
interface permissionProps {
permissions:Record<string,any>[];
}
defineOptions({ name: "PermissionDrawer" });
const props = defineProps({
checkAuth:{
type: Object,
default: ()=>({}),
}
});
const drawerVisible = defineModel("visible", { type: Boolean, default: false });
const activeTab = ref("menu");
const currentModule = ref(4); // 默认选中“商机详情”
const fieldsList = ref([
{
groupId: "g1",
groupName: "文本",
fields: [
{ id: "f1", name: "商机名称", permission: "read", collapsed: false },
{ id: "f2", name: "商机需求描述", permission: "read", collapsed: false },
],
},
{
groupId: "g2",
groupName: "选择",
fields: [
{ id: "f3", name: "合作类型", permission: "read", collapsed: false },
{ id: "f4", name: "需求品类", permission: "read", collapsed: false },
{ id: "f5", name: "线索/商机渠道", permission: "read", collapsed: false },
],
},
]);
const moduleList = [
{ id: 1, name: "线索详情" },
{ id: 2, name: "客户详情" },
{ id: 3, name: "工作室详情" },
{ id: 4, name: "商机详情" },
{ id: 5, name: "合同详情" },
{ id: 6, name: "项目详情" },
// ...以此类推
];
const activeTab = ref<string>("menu");
const currentModule = ref<number>(4); // 当前高亮的模块
const menuList = ref([]);
const loading = ref<boolean>(false);
// 动态展示当前人员名称
const personAuth = computed(() => {
return props.checkAuth.name;
});
const onModuleTabChange = (id) => {
console.log("切换到模块:", id);
// 这里可以调用接口更新 currentFields 的数据
// 动态的数据
const fieldsList = computed({
get:()=>{
const activeFields = [];
menuList.value.forEach(group => {
(group.children || []).forEach(row => {
if (row.selected) {
activeFields.push(row); // 将包含 fields 的整个对象传过去
}
});
});
return activeFields;
},
set:()=>{}
})
// 获取角色详情数据
const roleDetail = async (roleId) =>{
try {
const response = await getRolePermission(roleId);
menuList.value = response.map(group => {
const checkedGroup = (group.children || []).filter(c => c.selected);
const checkedCount = checkedGroup.length;
return {
...group,
isIndeterminate: checkedCount > 0 && checkedCount < group.children.length,
children:(group.children||[]).map(child=>{
const actions = child.selected ? child.operations.filter(action => action.selected).map(action => action.id) : [];
return {
...child,
isIndeterminate: actions.length > 0 && actions.length < child.operations.length,
actions,
};
})
};
});
} catch (error) {
console.log('error',error);
}
}
/**
* 非阻塞提取权限载荷
* @param {Array} menuList 原始响应式数据
*/
const extractPermissionPayload = async (menuList) => {
// 使用深拷贝,避免清洗过程中由于响应式追踪导致的 UI 卡顿
const sourceData = JSON.parse(JSON.stringify(menuList));
const result = [];
// 辅助函数处理单个功能节点Leaf Node
const formatLeafNode = (node) => {
const operationIds = (node.actions || []);
const fieldPermissions = (node.fieldGroups || []).map(group => ({
groupId: group.id,
permission: group.permission,
fields: (group.fields || []).map(field => ({
fieldId: field.id,
permission: field.permission
}))
}));
return {
leafNodeId: node.id,
operationIds: operationIds,
fieldPermissions: fieldPermissions
};
};
return new Promise((resolve) => {
let i = 0;
const chunk = () => {
const startTime = performance.now();
// 保持每一片执行时间在 16ms 以内(一帧的时间),确保不卡顿
while (i < sourceData.length && (performance.now() - startTime) < 16) {
const group = sourceData[i];
// 情况 A: 有二级菜单 (如商机管理)
if (group.children && group.children.length > 0) {
group.children.forEach(row => {
// 只要有选中的操作或二级被选中,就视为有效节点
if (row.selected || (row.actions && row.actions.length > 0)) {
result.push(formatLeafNode(row));
}
});
}
// 情况 B: 一级就是功能节点 (如团队管理无children有operations)
else {
if (group.selected || group.actions && group.actions.length > 0) {
result.push(formatLeafNode(group));
}
}
i++;
}
if (i < sourceData.length) {
requestAnimationFrame(chunk);
} else {
resolve(result);
}
};
chunk();
});
};
watch(()=>menuList.value,(val)=>{
// console.log('获取数据动态变化',val);
},{deep:true})
// 保存权限
const handleSavePermission = async () =>{
try {
loading.value = true;
const finalPayload = await extractPermissionPayload(menuList.value);
const res = await saveRolePermission({roleId:props.checkAuth.id,permissions:finalPayload});
ElMessage.success('操作成功');
drawerVisible.value = false;
} catch (error) {
console.log('save error',error);
} finally {
loading.value = false;
}
}
// 监听传入的id值的变化
watch(drawerVisible,(newVisible)=>{
if(newVisible){
const newRoleId = props.checkAuth.id;
roleDetail(newRoleId);
refresh();
}
})
</script>
<style lang="scss" scoped>
@use "./baseSegmentedPermission.scss" as *;

View File

@@ -0,0 +1,33 @@
import { componentMap } from "./routeMap";
/**
* 组装函数:将后端原始嵌套树重组为前端要求的骨架结构
* @param backendTree 后端返回的原始数据 (包含 backend, business 等顶层节点的树)
*/
export const transformBackendTree = (backendTree: any[]): any[] => {
if (!backendTree || !Array.isArray(backendTree)) return [];
// 排序操作
const sortedTree = [...backendTree].sort((a, b) => {
const sortA = a.sort ?? 999; // 兜底值,防止没有 sort 字段
const sortB = b.sort ?? 999;
return sortA - sortB;
});
return sortedTree.map(item => {
// 2. 构造当前节点
const newNode: any = {
...item,
code: componentMap[item.code],
originalCode: item.code, // 保留一份原始 code 备用
};
// 3. 递归处理 children
if (item.children && item.children.length > 0) {
newNode.children = transformBackendTree(item.children);
} else {
newNode.children = null;
}
return newNode;
});
};

View File

@@ -9,6 +9,7 @@ import TokenManager from "@/utils/storage";
import Login from "@/pages/Login/index.vue";
import HomeView from "@/pages/Layout/index.vue";
import { transformBackendTree } from './generateFinalMenu';
import { mockBackendMenuData } from "@/mock/menu";
const tokenManager = TokenManager.getInstance();
// 基础路由(不需要权限验证)
@@ -161,30 +162,9 @@ const addDynamicRoutes = async () => {
}
try {
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
let allRoutes: any[] = [];
if (userStore.isBackendUser) {
const backendResponses = await getUserInfo();
console.log("获取当前的菜单数据信息:",backendResponses);
const backendResponse = [];
allRoutes = [
{
code: "stage",
name: "管理中心",
icon: "",
meta: {
title: "管理中心",
},
children: Object.keys(backendResponse).length
? backendResponse
: mockBackendMenuData,
},
];
} else {
// TODO:获取用户端的数据信息
// response = await getUserMenus();
allRoutes = [];
}
const { modules:backendRawData } = await getUserInfo();
const allRoutes = transformBackendTree(backendRawData);
// console.log('获取最终渲染的菜单数据格式:',allRoutes);
if (allRoutes) {
// 转换路由数据
const dynamicRoutes = transformRoutes(

22
src/router/routeMap.ts Normal file
View File

@@ -0,0 +1,22 @@
const stageRoute = {
backend: "stage",
flow: "flow",
origanization: "origanization",
personnel: "personnel",
permission: "permission",
dict: "dict",
}
const businessRoute = {
business: "businessManage",
"business.customer": "customerManage",
"business.game_studio":"customerManage",
"business.opportunity":"customerManage",
}
export const componentMap: Record<string, string> = {
...stageRoute,
...businessRoute
};