fix:联调权限页面模块 优化全局table组件

This commit is contained in:
liangdong
2026-01-12 19:28:26 +08:00
parent 79e16909f0
commit a4bb81dc0a
17 changed files with 768 additions and 386 deletions

1
components.d.ts vendored
View File

@@ -13,6 +13,7 @@ declare module 'vue' {
export interface GlobalComponents {
AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
CardItem: typeof import('./src/components/cardItem/index.vue')['default']
CollapseHeader: typeof import('./src/components/collapseHeader/index.vue')['default']
Comment: typeof import('./src/components/comment/index.vue')['default']
CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
DynamicSvgIcon: typeof import('./src/components/dynamicSvgIcon/index.vue')['default']

View File

@@ -5,6 +5,11 @@ export const getRouteMenus = () => {
return request.get('/auth/v1/backend/menu');
};
// 获取当前用户信息
export const getUserInfo = () => {
return request.get('/auth/v1/my/info');
};
// 登录接口
export const login = (data: { username: string; password: string }) => {
return request.post('/auth/oauth2/token', data,{
@@ -13,3 +18,12 @@ export const login = (data: { username: string; password: string }) => {
}
});
};
/**员工公用接口*/
// 根据员工关键字查询员工
export const getEmployeeList = (params: any) => {
return request.get('/auth/v1/employee', params);
};

View File

@@ -18,6 +18,12 @@ interface addDataProps{
wxWork:wxWorkProps;
}
interface defaultProps{
keyword?:string;
pageNo:number;
pageSize:number;
}
// 查询企业
export const getEnterprise = (params: paramsProps) =>{
@@ -56,3 +62,8 @@ export const getEnterpriseDetail = () => {
export const getEnterpriseOrgDetail = (departmentId:string) => {
return request.get(`/auth/v1/backend/enterprise/department/${departmentId}`);
}
// 获取企业信息职位
export const getEnterprisePosition = (params:defaultProps) => {
return request.get(`/auth/v1/backend/enterprise/position`,params);
}

View File

@@ -26,24 +26,24 @@ export const deleteRole = (id: string) => {
return request.delete(`/auth/v1/backend/role/${id}`);
}
// 删除用户角色
export const deleteUserRole = (userId:string,roleId: string) => {
return request.delete(`/auth/v1/backend/role/user/${userId}/role/${roleId}`);
}
// 添加用户角色
export const addUserRole = (userId:string,roleId: string) => {
return request.post(`/auth/v1/backend/role/user/${userId}/role/${roleId}`);
};
// 启用角色
export const enableRole = (id: string) => {
return request.put(`/auth/v1/backend/role/enable/${id}`);
return request.put(`/auth/v1/backend/role/${id}/enable`);
}
// 禁用角色
export const disableRole = (id: string) => {
return request.put(`/auth/v1/backend/role/disable/${id}`);
return request.put(`/auth/v1/backend/role/${id}/disable`);
}
// 复制权限
export const copyRolePermission = (roleId: string) => {
return request.post(`/auth/v1/backend/role/${roleId}/copy`);
}
// 批量保存角色
export const batchSaveRole = (roleId: string,data: number[]) => {
return request.post(`auth/v1/backend/role/${roleId}/members`, data);
}
/**------------------------角色权限相关---------------------------**/

View File

@@ -0,0 +1,60 @@
<template>
<div class="mj-collapse-header">
<div class="header-left">
<div class="title">{{ title }}</div>
<slot name="extra"></slot>
</div>
<div class="header-right">
<slot name="headerRight"></slot>
</div>
</div>
</template>
<!-- 全局公用collapse表头组件 -->
<script setup lang="ts">
defineProps({
title: {
type: String,
default: "",
},
});
</script>
<style lang="scss" scoped>
$primary-blue: #0052d9;
$text-main: #1d2129;
.mj-collapse-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-right: 8px;
.header-left {
display: flex;
align-items: center;
.title {
position: relative;
display: flex;
align-items: center;
padding-left: 12px;
margin: 0;
font-size: 15px;
font-weight: 600;
color: $text-main;
// 使用伪类实现蓝色装饰条
&::before {
content: "";
position: absolute;
left: 0;
width: 3px;
height: 14px;
background-color: $primary-blue;
border-radius: 2px;
}
}
}
}
</style>

View File

@@ -22,33 +22,78 @@
<div class="drawer-body-container">
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="按姓名或部门搜索成员..."
:prefix-icon="Search"
clearable
/>
<el-button type="primary" plain @click="addNewMember"
>添加成员</el-button
<el-select
v-model="memberSearchQuery"
placeholder="按姓名搜索成员"
multiple
filterable
remote
:remote-method="remoteMethod"
:loading="remoteLoading"
collapse-tags
:teleported="false"
:fit-input-width="true"
value-key="id"
v-select-more="handleLoadMore"
@change="changeMember"
>
</div>
<div class="member-list" ref="listRef">
<transition-group name="list-fade">
<div
v-for="item in filteredMembers"
<el-option
v-for="item in selectOptions"
:key="item.id"
class="member-item"
@click="selectMember(item)"
:value="item"
:label="item.name"
>
<div class="select-item">
<name-avatar
:size="26"
:name="item.name"
:src="item.avatar"
:bgColor="'var(--mj-avatar-bg)'"
:avatarTextColor="'var(--mj-text-color)'"
/>
<el-tooltip :content="item.name">
<div class="select-item-name mj-ellipsis-one-line">
{{ item.name }}
</div>
</el-tooltip>
</div>
</el-option>
<el-option
v-if="loadMore"
disabled
class="mj-select-dropdown-loading"
value="loading"
>
<div class="status-container">
<el-icon v-if="loadMore" class="is-loading" :size="20"
><Loading
/></el-icon>
</div>
</el-option>
</el-select>
</div>
<el-scrollbar
height="calc(100vh - 216px)"
@end-reached="loadMoreList"
ref="listRef"
>
<div class="member-list">
<transition-group name="list-fade">
<div v-for="item in displayMemberList" :key="item.id" class="member-item">
<name-avatar
:size="30"
:name="item.name"
:src="item.avatar"
:bgColor="'var(--mj-avatar-bg)'"
:avatarTextColor="'var(--mj-text-color)'"
/>
<div class="member-info">
<div class="name">{{ item.name }}</div>
<div class="dept">{{ item.dept }}</div>
<div class="dept">
{{
(item.departments || []).map((dept) => dept.name).join(",")
}}
</div>
</div>
<div class="member-action">
<el-button link type="info" @click.stop="handleRemove(item.id)"
@@ -58,6 +103,7 @@
</div>
</transition-group>
</div>
</el-scrollbar>
</div>
<template #footer>
@@ -67,8 +113,10 @@
<span class="count"> {{ memberList.length }} 名成员</span>
</div>
<div class="actions">
<el-button link @click="drawerVisible = false">取消</el-button>
<el-button type="primary" class="btn-confirm">确认保存变更</el-button>
<el-button link @click="cancelMember">取消</el-button>
<el-button type="primary" class="btn-confirm" @click="saveMember"
>确认保存变更</el-button
>
</div>
</div>
</template>
@@ -77,74 +125,102 @@
<script setup lang="ts">
import { Search } from "@element-plus/icons-vue";
import NameAvatar from "@/components/NameAvatar/index.vue";
import { getEmployeeList } from "@/api";
import { useSelectLoadMore } from "@/hooks/useSelectLoadMore";
import { useLocalManager } from "@/hooks/useLocalManager";
const {
options,
remoteLoading,
loadMore,
noMore,
remoteMethod,
handleLoadMore,
} = useSelectLoadMore({
fetchApi: (p) => getEmployeeList({ ...p, kind: 1 }),
});
defineOptions({ name: "MemberSelector" });
// emit数据
const emit = defineEmits(["save-member"]);
const listRef = ref(null);
const drawerVisible = defineModel("visible", { type: Boolean });
const searchQuery = ref("");
const currentHoverId = ref("");
// 模拟数据
const memberList = ref([
{
id: 1,
name: "程彬",
dept: "数字化管理部",
type: "active-user",
active: true,
// 搜索部门的数据
const memberSearchQuery = ref([]);
const memberList = defineModel<any[]>("dataList", { default: () => [] });
const {
displayData: displayMemberList,
loadMore: handleLoadMoreList,
noMore: noMoreLocal
} = useLocalManager(() => memberList.value, 20);
// 动态返回options数据
const selectOptions = computed(() => {
const remoteData = options.value || [];
const selectedData = memberList.value || [];
// 创建一个 Map 用于去重,以 id 为 key
const optionMap = new Map();
// 1. 先把远程搜索结果放进去
remoteData.forEach(item => optionMap.set(item.id, item));
// 2. 再把已选择的数据放进去
selectedData.forEach(item => {
if (!optionMap.has(item.id)) {
optionMap.set(item.id, item);
}
});
return Array.from(optionMap.values());
});
// 监听数据变化
watch(
() => memberList.value,
(newVal) => {
memberSearchQuery.value = [...newVal];
},
{ id: 2, name: "王建国", dept: "集团财务部", type: "normal", active: false },
{ id: 3, name: "新成员", dept: "待分配部门", type: "normal", active: false },
{ id: 4, name: "新成员", dept: "待分配部门", type: "normal", active: false },
{ id: 5, name: "新成员", dept: "待分配部门", type: "normal", active: false },
{ id: 6, name: "新成员", dept: "待分配部门", type: "normal", active: false },
{ id: 7, name: "新成员", dept: "待分配部门", type: "normal", active: false },
{ id: 8, name: "新成员", dept: "待分配部门", type: "normal", active: false },
{ id: 9, name: "新成员", dept: "待分配部门", type: "normal", active: false },
{ id: 10, name: "新成员", dept: "待分配部门", type: "normal", active: false },
]);
const addNewMember = () => {
const newId = Date.now();
const newMember = {
id: newId,
name: "新成员",
dept: "待分配部门",
type: "normal",
active: true,
};
memberList.value.push(newMember);
{ immediate: true, deep: true }
);
// 添加成员 (添加成员)
const changeMember = (value) => {
memberList.value = value;
nextTick(() => {
if (listRef.value) {
listRef.value.scrollTo({
top: listRef.value.scrollHeight,
top: listRef.value?.wrapRef.scrollHeight,
behavior: "smooth",
});
}
});
};
// 搜索过滤逻辑
const filteredMembers = computed(() => {
const query = searchQuery.value.trim().toLowerCase();
if (!query) return memberList.value;
return memberList.value.filter(
(item) =>
item.name.toLowerCase().includes(query) ||
item.dept.toLowerCase().includes(query)
);
});
// 移除成员方法
const handleRemove = (id) => {
memberList.value = memberList.value.filter((m) => m.id !== id);
const handleRemove = async (id) => {
memberList.value = memberList.value.filter((item) => item.id !== id);
};
// 切换选中状态(模拟点击高亮)
const selectMember = (item) => {
// 如果需要单选高亮,可以先重置所有 active
// memberList.value.forEach(m => m.active = false)
item.active = !item.active;
// 前端加载更多 触底操作
const loadMoreList = () => {
handleLoadMoreList();
};
// 保存成员方法
const saveMember = async () => {
try {
await emit("save-member");
cancelMember();
} catch (error) {
console.log("add member error", error);
}
};
// 取消成员方法
const cancelMember = () => {
drawerVisible.value = false;
memberSearchQuery.value = [];
};
</script>
<style lang="scss" scoped>
@@ -153,20 +229,22 @@ const selectMember = (item) => {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; // 关键:允许子元素溢出滚动
min-height: 0;
}
.search-bar {
padding: 20px 24px;
display: flex;
gap: 12px;
background-color: #f8fafc;
:deep(.el-input) {
--el-input-border-radius: 2px;
}
}
.member-list {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 0 14px 20px;
box-sizing: border-box;
.list-fade-enter-active,
.list-fade-leave-active {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
@@ -190,14 +268,16 @@ const selectMember = (item) => {
align-items: center;
padding: 10px 16px;
margin-bottom: 4px;
border-radius: 8px;
border-radius: 2px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.2s;
&:hover {
--mj-avatar-bg: #d1e9ff;
--mj-text-color: #409eff;
background-color: #f0f7ff;
background-color: #f8fafc;
border-color: #f5f5f5;
.member-action {
opacity: 1;
}
@@ -225,6 +305,22 @@ const selectMember = (item) => {
}
}
.select-item {
--mj-avatar-bg: #f2f3f5;
--mj-text-color: #abb0b8;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
&:hover {
--mj-avatar-bg: #d1e9ff;
--mj-text-color: #409eff;
}
.select-item-name {
width: calc(100% - 34px);
}
}
.el-drawer__footer {
padding: 16px 24px;

View File

@@ -14,7 +14,7 @@ defineOptions({ name: "StageBreadcrumbs" });
const { title,styleClass } = defineProps<{
title: string;
styleClass: string;
styleClass?: string;
}>();
</script>
<style lang="scss" scoped>

View File

@@ -0,0 +1,49 @@
import { ref, shallowRef, computed, watch } from 'vue';
export function useLocalManager<T>(initialDataGetter: () => T[], pageSize = 20) {
// 1. 全量数据池(使用 shallowRef 保证 1W 条数据操作不卡顿)
const fullData = shallowRef<T[]>([]);
// 2. 内部展示条数
const displayCount = ref(pageSize);
// 3. 监听初始数据的变化(处理后端异步返回)
watch(initialDataGetter, (newVal) => {
if (Array.isArray(newVal)) {
fullData.value = [...newVal];
displayCount.value = pageSize;
}
}, { immediate: true });
// 4. 切片数据UI 渲染对象)
const displayData = computed(() => {
return fullData.value.slice(0, displayCount.value);
});
const noMore = computed(() => displayCount.value >= fullData.value.length);
// 加载更多
const loadMore = () => {
if (noMore.value) return;
displayCount.value += pageSize;
};
// 移除
const remove = (id: string | number, key = 'id') => {
fullData.value = fullData.value.filter(item => (item as any)[key] !== id);
};
// 新增
const add = (item: T) => {
fullData.value = [item, ...fullData.value];
};
return {
fullData,
displayData,
loadMore,
noMore,
remove,
add
};
}

View File

@@ -0,0 +1,77 @@
import { reactive, toRefs } from "vue";
import { debounce } from "lodash-es";
export function useSelectLoadMore(config: any) {
const { fetchApi, pageSize = 10, delay = 300 } = config;
const state = reactive({
options: [] as any[],
remoteLoading: false,
loadMore: false,
noMore: false,
pageNo: 1,
query: "",
});
const executeFetch = async (isRefresh: boolean) => {
// 拦截逻辑
if (!isRefresh && (state.loadMore || state.noMore)) return;
if (isRefresh) {
state.pageNo = 1;
state.noMore = false;
state.remoteLoading = true;
} else {
state.loadMore = true;
}
try {
const res = await fetchApi({
pageNo: state.pageNo,
pageSize: pageSize,
keyword: state.query,
});
const newList = res || [];
if (isRefresh) {
state.options = newList;
} else {
// 滚动加载:追加数据,不置空,不触发 select 的 loading
state.options.push(...newList);
}
// 探测 noMore
if (newList.length < pageSize) {
state.noMore = true;
} else {
state.pageNo++;
}
} finally {
// 请求结束,关闭所有状态
state.remoteLoading = false;
state.loadMore = false;
}
};
const remoteMethod = debounce(async (query: string) => {
state.query = query;
await executeFetch(true);
}, delay);
const handleLoadMore = async () => {
await executeFetch(false);
};
const onVisibleChange = async (visible: boolean) => {
if (visible && state.options.length === 0) {
await executeFetch(true);
}
};
return {
...toRefs(state),
remoteMethod,
handleLoadMore,
onVisibleChange,
};
}

View File

@@ -323,36 +323,6 @@ const handleFetchSearch = async (keyword, signal) => {
selectedUsersCache.forEach(userList => {
userList.forEach(u => selectedIds.add(u.id));
});
// FIXME:模拟接口返回的数据人员信息
await new Promise((resolve) => setTimeout(resolve, 300));
const allEmployees = [
{
id:1,
name:"张三",
},
{
id:2,
name:"李四",
},
{
id:3,
name:"王五",
},
{
id:4,
name:"赵六",
},
{
id:5,
name:"冯七 ",
},
]
return allEmployees.filter(user => {
const isMatch = user.name.toLowerCase().includes(keyword.toLowerCase());
const notSelected = !selectedIds.has(user.id);
return isMatch && notSelected;
});
// 正式请求
const queryParams = {
keyword,

View File

@@ -74,7 +74,7 @@
</el-form>
<div class="form-footer">
<span>© 2025 视界保留所有权利</span>
<span>© 2025 服链保留所有权利</span>
<div class="links">
<el-link underline="never">隐私政策</el-link>
<el-link underline="never">服务条款</el-link>

View File

@@ -8,7 +8,7 @@
destroy-on-close
:close-on-click-modal="false"
:close-on-press-escape="false"
top="10vh"
top="6vh"
>
<el-form ref="formRef" :model="form" label-position="top" :rules="rules">
<div class="row-flex">
@@ -16,44 +16,84 @@
<el-input v-model="form.name" placeholder="请输入角色名称" />
</el-form-item>
<el-form-item label="角色编码" prop="code">
<el-input v-model="form.code" placeholder="ROLE_CODE" />
<el-input
v-model="form.code"
placeholder="ROLE_CODE"
:disabled="detailId ? true : false"
/>
</el-form-item>
</div>
<el-form-item label="角色类型">
<el-form-item label="角色类型" prop="type">
<div class="full-width-radio">
<BaseSegmented v-model="form.type" :options="PermissionManage.roleTypeOptions" />
<BaseSegmented
v-model="form.type"
:options="PermissionManage.roleTypeOptions"
/>
</div>
</el-form-item>
<el-form-item prop="isOrgRelated">
<div class="feature-card light-green">
<div class="info">
<div class="title">组织架构</div>
<div class="desc">是否将该角色与系统组织架构体系进行深度关联</div>
</div>
<el-switch v-model="form.isOrgRelated" style="--el-switch-on-color: #13ce66; --el-switch-off-color: #CAD5E2" :active-value="1" :inactive-value="0" />
<el-switch
v-model="form.isOrgRelated"
style="
--el-switch-on-color: #13ce66;
--el-switch-off-color: #cad5e2;
"
:active-value="1"
:inactive-value="0"
/>
</div>
</el-form-item>
<el-form-item label="关联岗位">
<el-form-item label="关联岗位" prop="positionIds">
<el-select
v-model="form.positionIds"
placeholder="请选择关联岗位"
multiple
filterable
remote
:remote-method="remoteMethod"
:loading="remoteLoading"
collapse-tags
:fit-input-width="true"
:teleported="false"
v-select-more="handleLoadMore"
>
<el-option :label="item.label" :value="item.value" v-for="(item,index) in positionList" :key="index"/>
<el-option
:label="item.name"
:value="item.id"
v-for="(item, index) in options"
:key="item.id"
/>
<el-option v-if="loadMore" disabled value="more" class="mj-select-dropdown-loading">
<div class="status-container">
<el-icon v-if="loadMore" class="is-loading" :size="20"
><Loading
/></el-icon>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item prop="status">
<div class="feature-card light-blue">
<div class="info">
<div class="title">启用状态</div>
<div class="desc">控制该角色及其下属权限是否立即生效</div>
</div>
<el-switch v-model="form.status" :active-value="1" :inactive-value="0"/>
<el-switch
v-model="form.status"
:active-value="1"
:inactive-value="0"
/>
</div>
</el-form-item>
<el-form-item label="备注说明">
<el-form-item label="备注说明" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
@@ -67,7 +107,9 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelRoles" text>取消</el-button>
<el-button type="primary" @click="handleSubmit(formRef)">确认创建</el-button>
<el-button type="primary" @click="handleSubmit(formRef)"
>确认创建</el-button
>
</div>
</template>
</el-dialog>
@@ -76,46 +118,61 @@
<script lang="ts" setup>
import BaseSegmented from "./baseSegmented.vue";
import { PermissionManage } from "@/dict";
import type { FormInstance, FormRules } from 'element-plus';
import { Loading } from "@element-plus/icons-vue";
import type { FormInstance, FormRules } from "element-plus";
import {
addRole,
getRoleDetail
updateRole,
getRoleDetail,
} from "@/api/stage/permission/index.ts";
import { getEnterprisePosition } from "@/api/stage/organization";
import { useSelectLoadMore } from "@/hooks/useSelectLoadMore";
defineOptions({ name: "addRoles" });
const dialogVisible = defineModel("visible", { type: Boolean, default: false });
const props = defineProps({
detailId: {
type: [String, Number],
default: '',
}
default: "",
},
});
const {
options,
remoteLoading,
loadMore,
noMore,
remoteMethod,
handleLoadMore,
} = useSelectLoadMore({
fetchApi: (p) => getEnterprisePosition({ ...p }),
});
const computedTitle = computed(() => {
return props.detailId ? "编辑系统角色" : "新增系统角色";
});
const emit = defineEmits(["on-success"]);
const positionList = ref([]);
const positionList = ref([]); //岗位列表数据
const formRef = ref<FormInstance>(null);
const form = reactive<RuleForm>({
name: "",
code: "",
type: "DEFAULT",
isOrgRelated: 0, // 是否和组织架构深度关联
post: "",
positionIds: "",
status: 1,
remark: "",
});
const rules = reactive<FormInstance<RuleForm>>({
name: [
{ required: true, message: "请输入角色名称", trigger: "blur" }
],
code: [
{ required: true, message: "请输入角色编码", trigger: "blur" }
]
})
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
code: [{ required: true, message: "请输入角色编码", trigger: "blur" }],
});
// 分页信息
const pageInfo = reactive({
pageNo: 1,
pageSize: 20,
});
const cancelRoles = () => {
formRef.value && formRef.value.resetFields();
@@ -126,36 +183,41 @@ const cancelRoles = () => {
const getRoleShowDetail = async (val) => {
try {
const res = await getRoleDetail(props.detailId);
Object.assign(form,res);
Object.assign(form, res, { isOrgRelated: res.isOrgRelated ? 1 : 0 });
} catch (error) {
console.log('getRoleDetail error:',error);
console.log("getRoleDetail error:", error);
}
}
};
// 监听详情
watch(()=>dialogVisible.value,(val) => {
watch(
() => dialogVisible.value,
(val) => {
if (val && props.detailId) {
getRoleShowDetail();
}
})
}
);
// 新增编辑角色(仅系统类型可新增)
const handleSubmit = (formEl: FormInstance | undefined) => {
if (!formEl) return
if (!formEl) return;
formEl.validate(async (valid) => {
if (valid) {
try {
const res = await addRole(form);
const res = props.detailId
? await updateRole(form)
: await addRole(form);
ElMessage.success("操作成功");
cancelRoles();
emit('on-success');
emit("on-success");
} catch (error) {
console.log(error);
}
} else {
console.log('error submit!')
console.log("error submit!");
}
})
});
};
</script>
@@ -211,7 +273,7 @@ const handleSubmit = (formEl:FormInstance | undefined) => {
align-items: center;
padding: 16px;
border-radius: 4px;
margin-bottom: 22px;
flex: 1;
.info {
.title {

View File

@@ -12,7 +12,7 @@
class="auto-expand-input"
:prefix-icon="'Search'"
v-model="searchVal"
@keyup.enter="fetchTableData"
@keyup.enter="onRefresh"
></el-input>
</div>
<div class="mj-dict-actions-right">
@@ -27,7 +27,7 @@
</div>
</template>
</stageBreadcrumbs>
<template v-if="[1, 2].includes(activeTab)">
<!-- 表格 -->
<CommonTable
ref="tableRef"
@@ -61,18 +61,29 @@
link
icon="UserFilled"
type="primary"
@click.stop="addMember"
@click.stop="addMember(row)"
>{{ row.memberCount }}</el-button
>
</template>
</CommonTable>
</template>
<!-- 前线配置模块内容 -->
<template v-else> 权限配置模块 </template>
</div>
<!-- 成员管理 -->
<member-selector v-model:visible="showMember" />
<member-selector
v-model:visible="showMember"
v-model:dataList="memberList"
@save-member="handleSaveMember"
/>
<!-- 新增角色 -->
<add-roles v-model:visible="showRoles" @on-success="onRefresh" :detail-id="detailId"/>
<add-roles
v-model:visible="showRoles"
@on-success="onRefresh"
:detail-id="detailId"
/>
<!-- 权限抽屉 -->
<permission-drawer v-model:visible="showPermission" />
@@ -92,33 +103,38 @@ import {
addRole,
deleteRole,
enableRole,
disableRole
disableRole,
getRoleMemberList,
copyRolePermission
} from "@/api/stage/permission/index.ts";
import { useLocalManager } from '@/hooks/useLocalManager';
defineOptions({ name: "PermissionManagement" });
const activeTab = ref(1);
const tableRef = ref(null);
const searchVal = ref("");
const dataList = ref([]);
const memberList = ref([]); // 获取成员角色列表
const total = ref(0);
const showMember = ref(false);
const showRoles = ref(false);
const detailId = ref('');
const detailId = ref("");
const showPermission = ref(false);
const tabList = [
{
label: "角色与权限",
id: 1,
},
{
label: "用户管理",
id: 2,
},
{
label: "权限配置",
id: 3,
},
];
const {
fullData,
displayData,
loadMore,
noMore,
remove,
add
} = useLocalManager(()=>memberList.value);
const roleColumns = [
{
prop: "id",
@@ -129,7 +145,12 @@ const roleColumns = [
return `#${formatIndex(val.id)}`;
},
},
{ prop: "name", label: "角色名称", align: "center",showOverflowTooltip:true },
{
prop: "name",
label: "角色名称",
align: "center",
showOverflowTooltip: true,
},
{
prop: "memberCount",
label: "成员数量",
@@ -192,7 +213,7 @@ const roleColumns = [
label: "权限",
type: "primary",
link: true,
permission: ["edit"],
permission: ["*"],
onClick: (row) => {
showPermission.value = true;
},
@@ -202,7 +223,15 @@ const roleColumns = [
type: "default",
link: true,
permission: ["edit"],
onClick: (row) => {},
onClick: async (row) => {
try {
await copyRolePermission(row.id);
ElMessage.success("复制成功");
tableRef.value.refresh();
} catch (error) {
console.log('copy error',error);
}
},
},
{
label: "编辑",
@@ -220,80 +249,27 @@ const roleColumns = [
link: true,
permission: ["delete"],
disabledFn: (row) => {
return (row.type !== 'SYSTEM')
return row.type !== "SYSTEM";
},
onClick: async (row) => {
ElMessageBox.confirm("确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
try {
deleteRole(row.id).then(() => {
ElMessage.success("删除成功");
tableRef.value.refresh();
});
} catch (error) {
console.log('error',error);
console.log("error", error);
}
},
},
],
},
];
const userColumns = [
{
prop: "name",
label: "姓名",
align: "center",
},
{
prop: "code",
label: "账号",
align: "center",
},
{
prop: "departIds",
label: "所属部门",
align: "center",
},
{
prop: "roleIds",
label: "所属角色",
align: "center",
},
{
prop: "status",
label: "状态",
align: "center",
},
{
prop: "actions",
label: "操作",
align: "right",
fixed:"right",
actions: [
{
label: "编辑",
type: "primary",
link: true,
permission: ["edit"],
onClick: (row) => {
showPermission.value = true;
},
},
{
label: "重置密码",
type: "primary",
link: true,
permission: ["edit"],
onClick: (row) => {
showPermission.value = true;
},
},
{
label: "移除",
type: "danger",
link: true,
permission: ["edit"],
onClick: (row) => {
showPermission.value = true;
})
.catch(() => {
console.log("cancel");
});
},
},
],
@@ -303,14 +279,19 @@ const userColumns = [
const tableColumns = computed(() => {
const columns = {
1: roleColumns,
2: userColumns,
}[activeTab.value];
return columns;
});
// 当前成员数量
const addMember = () => {
const addMember = (row) => {
showMember.value = true;
getMemberList({ id: row.id });
};
// 保存成员数量
const handleSaveMember = async () => {
console.log("保存成员数量",memberList.value);
};
// 刷新列表
@@ -322,7 +303,7 @@ const onRefresh = () => {
watch(activeTab, () => {
dataList.value = [];
tableRef.value && tableRef.value.reset();
})
});
// 权限
const handleDictStatus = async (row) => {
@@ -335,52 +316,46 @@ const handleDictStatus = async (row) => {
}
};
// 请求接口获取
const getTableData = async (params) => {
console.log('每次请求的分页:',params);
await new Promise((resolve) => setTimeout(resolve, 500));
return {
records: [
{
id: 1,
name: "测试角色名称",
memberCount: 10,
permissionCount: 22,
status: 1,
type: "SYSTEM",
positionIds: "",
updateTime: 1767878508824,
updater: "更新人",
},
],
};
// 正式请求
const queryParams = {
...(searchVal.value && { name: searchVal.value }),
status:1,
type:1,
pageNo: 1,
pageSize:10
}
pageSize: 10,
};
try {
const res = await getRoleList(queryParams);
return res;
} catch (error) {
console.log("error", error);
}
};
// TODO: 后面会修改成全部获取获取成员角色列表 (分页获取)
const getMemberList = async (params) => {
// 获取成员角色列表
const queryParams = {
roleId: "",
pageNo: 1,
pageSize: 10,
};
try {
const res = await getRoleMemberList(params.id);
memberList.value = res.records;
} catch (error) {
console.log("error", error);
}
};
// 获取
const checkRolesText = computed(() => {
const btnText = {
1: "新增角色",
2: "新增用户",
}[activeTab.value];
const placeholder = {
1: "搜索角色名称...",
2: "搜索用户姓名/账号...",
}[activeTab.value];
return {
@@ -389,9 +364,6 @@ const checkRolesText = computed(() => {
};
});
// 请求数据信息
const fetchTableData = () => {};
// 新增角色
const addBtnClick = () => {
if (activeTab.value === 1) {

View File

@@ -4,7 +4,7 @@ import {
type RouteRecordRaw,
} from "vue-router";
import { useUserStore } from "@/store";
import { getRouteMenus } from "@/api";
import { getUserInfo } from "@/api";
import TokenManager from "@/utils/storage";
import Login from "@/pages/Login/index.vue";
@@ -30,7 +30,6 @@ const asyncRoutes: RouteRecordRaw[] = [
path: "/",
name: "Layout",
component: HomeView,
// redirect: '/home',
meta: {
requiresAuth: true,
},
@@ -165,7 +164,8 @@ const addDynamicRoutes = async () => {
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
let allRoutes: any[] = [];
if (userStore.isBackendUser) {
// const backendResponse = await getRouteMenus();
const backendResponses = await getUserInfo();
console.log("获取当前的菜单数据信息:",backendResponses);
const backendResponse = [];
allRoutes = [
{
@@ -257,7 +257,6 @@ router.beforeEach(async (to, _from, next) => {
try {
// 加载动态路由
await addDynamicRoutes();
console.log("当前完整路由表:", router.getRoutes());
const redirect = to.query.redirect as string;
if (to.path === "/" || to.path === "/login") {

View File

@@ -59,6 +59,7 @@ body {
// 字典状态全局样式
.mj-status-dot {
cursor: pointer;
&::before {
content: "";
display: inline-block;
@@ -102,6 +103,7 @@ body {
.filter-item {
margin-bottom: 20px;
label {
display: block;
font-size: 14px;
@@ -128,6 +130,7 @@ body {
font-weight: bold;
border-radius: 6px;
margin-top: 10px;
&:hover {
background-color: #407eff;
border-color: #407eff;
@@ -154,21 +157,42 @@ body {
width: 100%;
// 默认:一行 5 列
grid-template-columns: repeat(5, 1fr);
// 1400px 以下:觉得 5 个太挤,切到一行 4 列
@media (max-width: 1400px) {
grid-template-columns: repeat(4, 1fr);
}
// 1100px 以下 (iPad横屏区间):一行 3 列
@media (max-width: 1100px) {
grid-template-columns: repeat(3, 1fr);
}
// 768px 以下 (iPad竖屏):一行 2 列
@media (max-width: 768px) {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
// 480px 以下 (手机端):一行 1 列
@media (max-width: 480px) {
grid-template-columns: repeat(1, 1fr);
}
}
// select下拉搜索loading全局公用样式
.mj-select-dropdown-loading {
cursor: default;
background-color: transparent;
.status-container {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
color: #909399;
font-size: 13px;
height: 34px;
}
}

View File

@@ -1,9 +1,16 @@
import { useUserStore } from "@/store";
import { throttle } from "lodash-es";
function permissionDirective(el: HTMLElement, binding: any) {
const appStore = useUserStore();
const userPermissions = appStore.role;
let requiredPermissions = binding.value;
// 使用特殊值 '*' 表示跳过权限检查
const isSkipCheck =
requiredPermissions === "*" ||
(Array.isArray(requiredPermissions) && requiredPermissions.includes("*"));
if (isSkipCheck) {
return;
}
if (typeof requiredPermissions === "string") {
requiredPermissions = [requiredPermissions];
}
@@ -25,8 +32,48 @@ const permission = {
},
};
const selectMore = {
mounted(el, binding) {
const callback = binding.value;
const onScroll = throttle(
function () {
const { scrollTop, scrollHeight, clientHeight } = this;
const isBottom = scrollHeight - scrollTop <= clientHeight + 10;
if (isBottom) {
callback();
}
},
200,
{ trailing: true }
);
// 关键:轮询寻找滚动容器,因为 mounted 时下拉框可能还没渲染
const interval = setInterval(() => {
// 兼容 teleport 情况,如果 el 找不到,尝试寻找 popper
const wrapper = el.querySelector(".el-scrollbar__wrap");
if (wrapper) {
wrapper.addEventListener("scroll", onScroll);
el._scrollWrapper = wrapper;
el._onScroll = onScroll;
clearInterval(interval);
}
}, 150);
setTimeout(() => clearInterval(interval), 10000);
},
beforeUnmount(el) {
if (el._scrollWrapper && el._onScroll) {
el._scrollWrapper.removeEventListener("scroll", el._onScroll);
el._onScroll.cancel?.();
}
},
};
const directives = {
permission,
selectMore,
};
export default (app: any) => {

View File

@@ -8,6 +8,6 @@ export function getImageUrl(url: string) {
// 设置index前缀 如001 010 100种 默认兼容3位数
export const formatIndex = (index,padNum=3) => {
export const formatIndex = (index,padNum?:number=3) => {
return String(index + 1).padStart(padNum, '0');
};