fix:联调权限页面模块 优化全局table组件
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -13,6 +13,7 @@ declare module 'vue' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
|
AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
|
||||||
CardItem: typeof import('./src/components/cardItem/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']
|
Comment: typeof import('./src/components/comment/index.vue')['default']
|
||||||
CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
||||||
DynamicSvgIcon: typeof import('./src/components/dynamicSvgIcon/index.vue')['default']
|
DynamicSvgIcon: typeof import('./src/components/dynamicSvgIcon/index.vue')['default']
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ export const getRouteMenus = () => {
|
|||||||
return request.get('/auth/v1/backend/menu');
|
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 }) => {
|
export const login = (data: { username: string; password: string }) => {
|
||||||
return request.post('/auth/oauth2/token', data,{
|
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);
|
||||||
|
};
|
||||||
@@ -18,6 +18,12 @@ interface addDataProps{
|
|||||||
wxWork:wxWorkProps;
|
wxWork:wxWorkProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface defaultProps{
|
||||||
|
keyword?:string;
|
||||||
|
pageNo:number;
|
||||||
|
pageSize:number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 查询企业
|
// 查询企业
|
||||||
export const getEnterprise = (params: paramsProps) =>{
|
export const getEnterprise = (params: paramsProps) =>{
|
||||||
@@ -56,3 +62,8 @@ export const getEnterpriseDetail = () => {
|
|||||||
export const getEnterpriseOrgDetail = (departmentId:string) => {
|
export const getEnterpriseOrgDetail = (departmentId:string) => {
|
||||||
return request.get(`/auth/v1/backend/enterprise/department/${departmentId}`);
|
return request.get(`/auth/v1/backend/enterprise/department/${departmentId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取企业信息职位
|
||||||
|
export const getEnterprisePosition = (params:defaultProps) => {
|
||||||
|
return request.get(`/auth/v1/backend/enterprise/position`,params);
|
||||||
|
}
|
||||||
@@ -26,24 +26,24 @@ export const deleteRole = (id: string) => {
|
|||||||
return request.delete(`/auth/v1/backend/role/${id}`);
|
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) => {
|
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) => {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**------------------------角色权限相关---------------------------**/
|
/**------------------------角色权限相关---------------------------**/
|
||||||
|
|||||||
60
src/components/collapseHeader/index.vue
Normal file
60
src/components/collapseHeader/index.vue
Normal 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>
|
||||||
@@ -22,33 +22,78 @@
|
|||||||
|
|
||||||
<div class="drawer-body-container">
|
<div class="drawer-body-container">
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<el-input
|
<el-select
|
||||||
v-model="searchQuery"
|
v-model="memberSearchQuery"
|
||||||
placeholder="按姓名或部门搜索成员..."
|
placeholder="按姓名搜索成员"
|
||||||
:prefix-icon="Search"
|
multiple
|
||||||
clearable
|
filterable
|
||||||
/>
|
remote
|
||||||
<el-button type="primary" plain @click="addNewMember"
|
:remote-method="remoteMethod"
|
||||||
>添加成员</el-button
|
:loading="remoteLoading"
|
||||||
|
collapse-tags
|
||||||
|
:teleported="false"
|
||||||
|
:fit-input-width="true"
|
||||||
|
value-key="id"
|
||||||
|
v-select-more="handleLoadMore"
|
||||||
|
@change="changeMember"
|
||||||
>
|
>
|
||||||
</div>
|
<el-option
|
||||||
<div class="member-list" ref="listRef">
|
v-for="item in selectOptions"
|
||||||
<transition-group name="list-fade">
|
|
||||||
<div
|
|
||||||
v-for="item in filteredMembers"
|
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="member-item"
|
:value="item"
|
||||||
@click="selectMember(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
|
<name-avatar
|
||||||
:size="30"
|
:size="30"
|
||||||
:name="item.name"
|
:name="item.name"
|
||||||
|
:src="item.avatar"
|
||||||
:bgColor="'var(--mj-avatar-bg)'"
|
:bgColor="'var(--mj-avatar-bg)'"
|
||||||
:avatarTextColor="'var(--mj-text-color)'"
|
:avatarTextColor="'var(--mj-text-color)'"
|
||||||
/>
|
/>
|
||||||
<div class="member-info">
|
<div class="member-info">
|
||||||
<div class="name">{{ item.name }}</div>
|
<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>
|
||||||
<div class="member-action">
|
<div class="member-action">
|
||||||
<el-button link type="info" @click.stop="handleRemove(item.id)"
|
<el-button link type="info" @click.stop="handleRemove(item.id)"
|
||||||
@@ -58,6 +103,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -67,8 +113,10 @@
|
|||||||
<span class="count">共 {{ memberList.length }} 名成员</span>
|
<span class="count">共 {{ memberList.length }} 名成员</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<el-button link @click="drawerVisible = false">取消</el-button>
|
<el-button link @click="cancelMember">取消</el-button>
|
||||||
<el-button type="primary" class="btn-confirm">确认保存变更</el-button>
|
<el-button type="primary" class="btn-confirm" @click="saveMember"
|
||||||
|
>确认保存变更</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -77,74 +125,102 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Search } from "@element-plus/icons-vue";
|
import { Search } from "@element-plus/icons-vue";
|
||||||
import NameAvatar from "@/components/NameAvatar/index.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" });
|
defineOptions({ name: "MemberSelector" });
|
||||||
|
|
||||||
|
// emit数据
|
||||||
|
const emit = defineEmits(["save-member"]);
|
||||||
const listRef = ref(null);
|
const listRef = ref(null);
|
||||||
const drawerVisible = defineModel("visible", { type: Boolean });
|
const drawerVisible = defineModel("visible", { type: Boolean });
|
||||||
const searchQuery = ref("");
|
// 搜索部门的数据
|
||||||
const currentHoverId = ref("");
|
const memberSearchQuery = ref([]);
|
||||||
// 模拟数据
|
|
||||||
const memberList = ref([
|
const memberList = defineModel<any[]>("dataList", { default: () => [] });
|
||||||
{
|
|
||||||
id: 1,
|
const {
|
||||||
name: "程彬",
|
displayData: displayMemberList,
|
||||||
dept: "数字化管理部",
|
loadMore: handleLoadMoreList,
|
||||||
type: "active-user",
|
noMore: noMoreLocal
|
||||||
active: true,
|
} = 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 },
|
{ immediate: true, deep: true }
|
||||||
{ 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);
|
|
||||||
|
|
||||||
|
// 添加成员 (添加成员)
|
||||||
|
const changeMember = (value) => {
|
||||||
|
memberList.value = value;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (listRef.value) {
|
if (listRef.value) {
|
||||||
listRef.value.scrollTo({
|
listRef.value.scrollTo({
|
||||||
top: listRef.value.scrollHeight,
|
top: listRef.value?.wrapRef.scrollHeight,
|
||||||
behavior: "smooth",
|
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) => {
|
const handleRemove = async (id) => {
|
||||||
memberList.value = memberList.value.filter((m) => m.id !== id);
|
memberList.value = memberList.value.filter((item) => item.id !== id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换选中状态(模拟点击高亮)
|
// 前端加载更多 触底操作
|
||||||
const selectMember = (item) => {
|
const loadMoreList = () => {
|
||||||
// 如果需要单选高亮,可以先重置所有 active
|
handleLoadMoreList();
|
||||||
// memberList.value.forEach(m => m.active = false)
|
};
|
||||||
item.active = !item.active;
|
// 保存成员方法
|
||||||
|
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>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -153,20 +229,22 @@ const selectMember = (item) => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0; // 关键:允许子元素溢出滚动
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
:deep(.el-input) {
|
||||||
|
--el-input-border-radius: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-list {
|
.member-list {
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding: 0 14px 20px;
|
padding: 0 14px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
.list-fade-enter-active,
|
.list-fade-enter-active,
|
||||||
.list-fade-leave-active {
|
.list-fade-leave-active {
|
||||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
@@ -190,14 +268,16 @@ const selectMember = (item) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border-radius: 8px;
|
border-radius: 2px;
|
||||||
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--mj-avatar-bg: #d1e9ff;
|
--mj-avatar-bg: #d1e9ff;
|
||||||
--mj-text-color: #409eff;
|
--mj-text-color: #409eff;
|
||||||
background-color: #f0f7ff;
|
background-color: #f8fafc;
|
||||||
|
border-color: #f5f5f5;
|
||||||
.member-action {
|
.member-action {
|
||||||
opacity: 1;
|
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 {
|
.el-drawer__footer {
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defineOptions({ name: "StageBreadcrumbs" });
|
|||||||
|
|
||||||
const { title,styleClass } = defineProps<{
|
const { title,styleClass } = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
styleClass: string;
|
styleClass?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
49
src/hooks/useLocalManager.ts
Normal file
49
src/hooks/useLocalManager.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
77
src/hooks/useSelectLoadMore.ts
Normal file
77
src/hooks/useSelectLoadMore.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -323,36 +323,6 @@ const handleFetchSearch = async (keyword, signal) => {
|
|||||||
selectedUsersCache.forEach(userList => {
|
selectedUsersCache.forEach(userList => {
|
||||||
userList.forEach(u => selectedIds.add(u.id));
|
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 = {
|
const queryParams = {
|
||||||
keyword,
|
keyword,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<span>© 2025 智视界保留所有权利</span>
|
<span>© 2025 智服链保留所有权利</span>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<el-link underline="never">隐私政策</el-link>
|
<el-link underline="never">隐私政策</el-link>
|
||||||
<el-link underline="never">服务条款</el-link>
|
<el-link underline="never">服务条款</el-link>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
destroy-on-close
|
destroy-on-close
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:close-on-press-escape="false"
|
:close-on-press-escape="false"
|
||||||
top="10vh"
|
top="6vh"
|
||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="form" label-position="top" :rules="rules">
|
<el-form ref="formRef" :model="form" label-position="top" :rules="rules">
|
||||||
<div class="row-flex">
|
<div class="row-flex">
|
||||||
@@ -16,44 +16,84 @@
|
|||||||
<el-input v-model="form.name" placeholder="请输入角色名称" />
|
<el-input v-model="form.name" placeholder="请输入角色名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色编码" prop="code">
|
<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>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form-item label="角色类型">
|
<el-form-item label="角色类型" prop="type">
|
||||||
<div class="full-width-radio">
|
<div class="full-width-radio">
|
||||||
<BaseSegmented v-model="form.type" :options="PermissionManage.roleTypeOptions" />
|
<BaseSegmented
|
||||||
|
v-model="form.type"
|
||||||
|
:options="PermissionManage.roleTypeOptions"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item prop="isOrgRelated">
|
||||||
<div class="feature-card light-green">
|
<div class="feature-card light-green">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="title">组织架构</div>
|
<div class="title">组织架构</div>
|
||||||
<div class="desc">是否将该角色与系统组织架构体系进行深度关联</div>
|
<div class="desc">是否将该角色与系统组织架构体系进行深度关联</div>
|
||||||
</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>
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="关联岗位">
|
<el-form-item label="关联岗位" prop="positionIds">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="form.positionIds"
|
v-model="form.positionIds"
|
||||||
placeholder="请选择关联岗位"
|
placeholder="请选择关联岗位"
|
||||||
multiple
|
multiple
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="remoteMethod"
|
||||||
|
:loading="remoteLoading"
|
||||||
collapse-tags
|
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-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item prop="status">
|
||||||
<div class="feature-card light-blue">
|
<div class="feature-card light-blue">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="title">启用状态</div>
|
<div class="title">启用状态</div>
|
||||||
<div class="desc">控制该角色及其下属权限是否立即生效</div>
|
<div class="desc">控制该角色及其下属权限是否立即生效</div>
|
||||||
</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>
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注说明">
|
<el-form-item label="备注说明" prop="remark">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.remark"
|
v-model="form.remark"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
@@ -67,7 +107,9 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button @click="cancelRoles" text>取消</el-button>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -76,46 +118,61 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import BaseSegmented from "./baseSegmented.vue";
|
import BaseSegmented from "./baseSegmented.vue";
|
||||||
import { PermissionManage } from "@/dict";
|
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 {
|
import {
|
||||||
addRole,
|
addRole,
|
||||||
getRoleDetail
|
updateRole,
|
||||||
|
getRoleDetail,
|
||||||
} from "@/api/stage/permission/index.ts";
|
} from "@/api/stage/permission/index.ts";
|
||||||
|
import { getEnterprisePosition } from "@/api/stage/organization";
|
||||||
|
import { useSelectLoadMore } from "@/hooks/useSelectLoadMore";
|
||||||
defineOptions({ name: "addRoles" });
|
defineOptions({ name: "addRoles" });
|
||||||
const dialogVisible = defineModel("visible", { type: Boolean, default: false });
|
const dialogVisible = defineModel("visible", { type: Boolean, default: false });
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
detailId: {
|
detailId: {
|
||||||
type: [String,Number],
|
type: [String, Number],
|
||||||
default: '',
|
default: "",
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
remoteLoading,
|
||||||
|
loadMore,
|
||||||
|
noMore,
|
||||||
|
remoteMethod,
|
||||||
|
handleLoadMore,
|
||||||
|
} = useSelectLoadMore({
|
||||||
|
fetchApi: (p) => getEnterprisePosition({ ...p }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const computedTitle = computed(() => {
|
const computedTitle = computed(() => {
|
||||||
return props.detailId ? "编辑系统角色" : "新增系统角色";
|
return props.detailId ? "编辑系统角色" : "新增系统角色";
|
||||||
});
|
});
|
||||||
const emit = defineEmits(["on-success"]);
|
const emit = defineEmits(["on-success"]);
|
||||||
const positionList = ref([]);
|
const positionList = ref([]); //岗位列表数据
|
||||||
const formRef = ref<FormInstance>(null);
|
const formRef = ref<FormInstance>(null);
|
||||||
const form = reactive<RuleForm>({
|
const form = reactive<RuleForm>({
|
||||||
name: "",
|
name: "",
|
||||||
code: "",
|
code: "",
|
||||||
type: "DEFAULT",
|
type: "DEFAULT",
|
||||||
isOrgRelated: 0, // 是否和组织架构深度关联
|
isOrgRelated: 0, // 是否和组织架构深度关联
|
||||||
post: "",
|
positionIds: "",
|
||||||
status: 1,
|
status: 1,
|
||||||
remark: "",
|
remark: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const rules = reactive<FormInstance<RuleForm>>({
|
const rules = reactive<FormInstance<RuleForm>>({
|
||||||
name: [
|
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
|
||||||
{ required: true, message: "请输入角色名称", trigger: "blur" }
|
code: [{ required: true, message: "请输入角色编码", trigger: "blur" }],
|
||||||
],
|
});
|
||||||
code: [
|
|
||||||
{ required: true, message: "请输入角色编码", trigger: "blur" }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pageInfo = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
const cancelRoles = () => {
|
const cancelRoles = () => {
|
||||||
formRef.value && formRef.value.resetFields();
|
formRef.value && formRef.value.resetFields();
|
||||||
@@ -123,39 +180,44 @@ const cancelRoles = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取角色详情
|
// 获取角色详情
|
||||||
const getRoleShowDetail = async (val) =>{
|
const getRoleShowDetail = async (val) => {
|
||||||
try {
|
try {
|
||||||
const res = await getRoleDetail(props.detailId);
|
const res = await getRoleDetail(props.detailId);
|
||||||
Object.assign(form,res);
|
Object.assign(form, res, { isOrgRelated: res.isOrgRelated ? 1 : 0 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('getRoleDetail error:',error);
|
console.log("getRoleDetail error:", error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// 监听详情
|
// 监听详情
|
||||||
watch(()=>dialogVisible.value,(val) => {
|
watch(
|
||||||
if(val && props.detailId){
|
() => dialogVisible.value,
|
||||||
|
(val) => {
|
||||||
|
if (val && props.detailId) {
|
||||||
getRoleShowDetail();
|
getRoleShowDetail();
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const handleSubmit = (formEl:FormInstance | undefined) => {
|
// 新增编辑角色(仅系统类型可新增)
|
||||||
if (!formEl) return
|
const handleSubmit = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
formEl.validate(async (valid) => {
|
formEl.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
try {
|
try {
|
||||||
const res = await addRole(form);
|
const res = props.detailId
|
||||||
|
? await updateRole(form)
|
||||||
|
: await addRole(form);
|
||||||
ElMessage.success("操作成功");
|
ElMessage.success("操作成功");
|
||||||
cancelRoles();
|
cancelRoles();
|
||||||
emit('on-success');
|
emit("on-success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('error submit!')
|
console.log("error submit!");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -200,7 +262,7 @@ const handleSubmit = (formEl:FormInstance | undefined) => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.full-width-radio{
|
.full-width-radio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +273,7 @@ const handleSubmit = (formEl:FormInstance | undefined) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 22px;
|
flex: 1;
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
class="auto-expand-input"
|
class="auto-expand-input"
|
||||||
:prefix-icon="'Search'"
|
:prefix-icon="'Search'"
|
||||||
v-model="searchVal"
|
v-model="searchVal"
|
||||||
@keyup.enter="fetchTableData"
|
@keyup.enter="onRefresh"
|
||||||
></el-input>
|
></el-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="mj-dict-actions-right">
|
<div class="mj-dict-actions-right">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</stageBreadcrumbs>
|
</stageBreadcrumbs>
|
||||||
|
<template v-if="[1, 2].includes(activeTab)">
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<CommonTable
|
<CommonTable
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
@@ -61,18 +61,29 @@
|
|||||||
link
|
link
|
||||||
icon="UserFilled"
|
icon="UserFilled"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click.stop="addMember"
|
@click.stop="addMember(row)"
|
||||||
>{{ row.memberCount }}</el-button
|
>{{ row.memberCount }}</el-button
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</CommonTable>
|
</CommonTable>
|
||||||
|
</template>
|
||||||
|
<!-- 前线配置模块内容 -->
|
||||||
|
<template v-else> 权限配置模块 </template>
|
||||||
</div>
|
</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" />
|
<permission-drawer v-model:visible="showPermission" />
|
||||||
@@ -92,33 +103,38 @@ import {
|
|||||||
addRole,
|
addRole,
|
||||||
deleteRole,
|
deleteRole,
|
||||||
enableRole,
|
enableRole,
|
||||||
disableRole
|
disableRole,
|
||||||
|
getRoleMemberList,
|
||||||
|
copyRolePermission
|
||||||
} from "@/api/stage/permission/index.ts";
|
} from "@/api/stage/permission/index.ts";
|
||||||
|
import { useLocalManager } from '@/hooks/useLocalManager';
|
||||||
defineOptions({ name: "PermissionManagement" });
|
defineOptions({ name: "PermissionManagement" });
|
||||||
const activeTab = ref(1);
|
const activeTab = ref(1);
|
||||||
const tableRef = ref(null);
|
const tableRef = ref(null);
|
||||||
const searchVal = ref("");
|
const searchVal = ref("");
|
||||||
const dataList = ref([]);
|
const dataList = ref([]);
|
||||||
|
const memberList = ref([]); // 获取成员角色列表
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const showMember = ref(false);
|
const showMember = ref(false);
|
||||||
const showRoles = ref(false);
|
const showRoles = ref(false);
|
||||||
const detailId = ref('');
|
const detailId = ref("");
|
||||||
const showPermission = ref(false);
|
const showPermission = ref(false);
|
||||||
const tabList = [
|
const tabList = [
|
||||||
{
|
{
|
||||||
label: "角色与权限",
|
label: "角色与权限",
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "用户管理",
|
|
||||||
id: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "权限配置",
|
|
||||||
id: 3,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const {
|
||||||
|
fullData,
|
||||||
|
displayData,
|
||||||
|
loadMore,
|
||||||
|
noMore,
|
||||||
|
remove,
|
||||||
|
add
|
||||||
|
} = useLocalManager(()=>memberList.value);
|
||||||
|
|
||||||
const roleColumns = [
|
const roleColumns = [
|
||||||
{
|
{
|
||||||
prop: "id",
|
prop: "id",
|
||||||
@@ -129,7 +145,12 @@ const roleColumns = [
|
|||||||
return `#${formatIndex(val.id)}`;
|
return `#${formatIndex(val.id)}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ prop: "name", label: "角色名称", align: "center",showOverflowTooltip:true },
|
{
|
||||||
|
prop: "name",
|
||||||
|
label: "角色名称",
|
||||||
|
align: "center",
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "memberCount",
|
prop: "memberCount",
|
||||||
label: "成员数量",
|
label: "成员数量",
|
||||||
@@ -185,14 +206,14 @@ const roleColumns = [
|
|||||||
prop: "actions",
|
prop: "actions",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
align: "right",
|
align: "right",
|
||||||
fixed:"right",
|
fixed: "right",
|
||||||
width: "200",
|
width: "200",
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: "权限",
|
label: "权限",
|
||||||
type: "primary",
|
type: "primary",
|
||||||
link: true,
|
link: true,
|
||||||
permission: ["edit"],
|
permission: ["*"],
|
||||||
onClick: (row) => {
|
onClick: (row) => {
|
||||||
showPermission.value = true;
|
showPermission.value = true;
|
||||||
},
|
},
|
||||||
@@ -202,7 +223,15 @@ const roleColumns = [
|
|||||||
type: "default",
|
type: "default",
|
||||||
link: true,
|
link: true,
|
||||||
permission: ["edit"],
|
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: "编辑",
|
label: "编辑",
|
||||||
@@ -220,80 +249,27 @@ const roleColumns = [
|
|||||||
link: true,
|
link: true,
|
||||||
permission: ["delete"],
|
permission: ["delete"],
|
||||||
disabledFn: (row) => {
|
disabledFn: (row) => {
|
||||||
return (row.type !== 'SYSTEM')
|
return row.type !== "SYSTEM";
|
||||||
},
|
},
|
||||||
onClick: async (row) => {
|
onClick: async (row) => {
|
||||||
|
ElMessageBox.confirm("确定要删除吗?", "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
try {
|
try {
|
||||||
deleteRole(row.id).then(() => {
|
deleteRole(row.id).then(() => {
|
||||||
ElMessage.success("删除成功");
|
ElMessage.success("删除成功");
|
||||||
tableRef.value.refresh();
|
tableRef.value.refresh();
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error',error);
|
console.log("error", error);
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
},
|
.catch(() => {
|
||||||
],
|
console.log("cancel");
|
||||||
},
|
});
|
||||||
];
|
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -303,14 +279,19 @@ const userColumns = [
|
|||||||
const tableColumns = computed(() => {
|
const tableColumns = computed(() => {
|
||||||
const columns = {
|
const columns = {
|
||||||
1: roleColumns,
|
1: roleColumns,
|
||||||
2: userColumns,
|
|
||||||
}[activeTab.value];
|
}[activeTab.value];
|
||||||
return columns;
|
return columns;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 当前成员数量
|
// 当前成员数量
|
||||||
const addMember = () => {
|
const addMember = (row) => {
|
||||||
showMember.value = true;
|
showMember.value = true;
|
||||||
|
getMemberList({ id: row.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存成员数量
|
||||||
|
const handleSaveMember = async () => {
|
||||||
|
console.log("保存成员数量",memberList.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 刷新列表
|
// 刷新列表
|
||||||
@@ -319,10 +300,10 @@ const onRefresh = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 监听Tabs变化
|
// 监听Tabs变化
|
||||||
watch(activeTab,()=>{
|
watch(activeTab, () => {
|
||||||
dataList.value = [];
|
dataList.value = [];
|
||||||
tableRef.value && tableRef.value.reset();
|
tableRef.value && tableRef.value.reset();
|
||||||
})
|
});
|
||||||
|
|
||||||
// 权限
|
// 权限
|
||||||
const handleDictStatus = async (row) => {
|
const handleDictStatus = async (row) => {
|
||||||
@@ -335,52 +316,46 @@ const handleDictStatus = async (row) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 请求接口获取
|
||||||
const getTableData = async (params) => {
|
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 = {
|
const queryParams = {
|
||||||
...(searchVal.value && { name:searchVal.value }),
|
...(searchVal.value && { name: searchVal.value }),
|
||||||
status:1,
|
pageNo: 1,
|
||||||
type:1,
|
pageSize: 10,
|
||||||
pageNo:1,
|
};
|
||||||
pageSize:10
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const res = await getRoleList(queryParams);
|
const res = await getRoleList(queryParams);
|
||||||
return res;
|
return res;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("error", 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 checkRolesText = computed(() => {
|
||||||
const btnText = {
|
const btnText = {
|
||||||
1: "新增角色",
|
1: "新增角色",
|
||||||
2: "新增用户",
|
|
||||||
}[activeTab.value];
|
}[activeTab.value];
|
||||||
|
|
||||||
const placeholder = {
|
const placeholder = {
|
||||||
1: "搜索角色名称...",
|
1: "搜索角色名称...",
|
||||||
2: "搜索用户姓名/账号...",
|
|
||||||
}[activeTab.value];
|
}[activeTab.value];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -389,9 +364,6 @@ const checkRolesText = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 请求数据信息
|
|
||||||
const fetchTableData = () => {};
|
|
||||||
|
|
||||||
// 新增角色
|
// 新增角色
|
||||||
const addBtnClick = () => {
|
const addBtnClick = () => {
|
||||||
if (activeTab.value === 1) {
|
if (activeTab.value === 1) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
type RouteRecordRaw,
|
type RouteRecordRaw,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import { getRouteMenus } from "@/api";
|
import { getUserInfo } from "@/api";
|
||||||
import TokenManager from "@/utils/storage";
|
import TokenManager from "@/utils/storage";
|
||||||
|
|
||||||
import Login from "@/pages/Login/index.vue";
|
import Login from "@/pages/Login/index.vue";
|
||||||
@@ -30,7 +30,6 @@ const asyncRoutes: RouteRecordRaw[] = [
|
|||||||
path: "/",
|
path: "/",
|
||||||
name: "Layout",
|
name: "Layout",
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
// redirect: '/home',
|
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
@@ -165,7 +164,8 @@ const addDynamicRoutes = async () => {
|
|||||||
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
|
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
|
||||||
let allRoutes: any[] = [];
|
let allRoutes: any[] = [];
|
||||||
if (userStore.isBackendUser) {
|
if (userStore.isBackendUser) {
|
||||||
// const backendResponse = await getRouteMenus();
|
const backendResponses = await getUserInfo();
|
||||||
|
console.log("获取当前的菜单数据信息:",backendResponses);
|
||||||
const backendResponse = [];
|
const backendResponse = [];
|
||||||
allRoutes = [
|
allRoutes = [
|
||||||
{
|
{
|
||||||
@@ -257,7 +257,6 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
try {
|
try {
|
||||||
// 加载动态路由
|
// 加载动态路由
|
||||||
await addDynamicRoutes();
|
await addDynamicRoutes();
|
||||||
console.log("当前完整路由表:", router.getRoutes());
|
|
||||||
const redirect = to.query.redirect as string;
|
const redirect = to.query.redirect as string;
|
||||||
|
|
||||||
if (to.path === "/" || to.path === "/login") {
|
if (to.path === "/" || to.path === "/login") {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ body {
|
|||||||
--mj-border-color:#{$mj-border-color};
|
--mj-border-color:#{$mj-border-color};
|
||||||
--mj-padding-standard:#{$mj-padding-standard};
|
--mj-padding-standard:#{$mj-padding-standard};
|
||||||
--mj-popper-radius: 8px;
|
--mj-popper-radius: 8px;
|
||||||
--el-color-primary:#2b65f6;
|
--el-color-primary: #2b65f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ body {
|
|||||||
// 字典状态全局样式
|
// 字典状态全局样式
|
||||||
.mj-status-dot {
|
.mj-status-dot {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -102,7 +103,8 @@ body {
|
|||||||
|
|
||||||
.filter-item {
|
.filter-item {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
label{
|
|
||||||
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
@@ -112,7 +114,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-select {
|
.custom-select {
|
||||||
--el-border-color:transparent;
|
--el-border-color: transparent;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -128,6 +130,7 @@ body {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #407eff;
|
background-color: #407eff;
|
||||||
border-color: #407eff;
|
border-color: #407eff;
|
||||||
@@ -148,27 +151,48 @@ body {
|
|||||||
|
|
||||||
// 卡片的全局样式
|
// 卡片的全局样式
|
||||||
|
|
||||||
.mj-card-container{
|
.mj-card-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
// 默认:一行 5 列
|
// 默认:一行 5 列
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
|
||||||
// 1400px 以下:觉得 5 个太挤,切到一行 4 列
|
// 1400px 以下:觉得 5 个太挤,切到一行 4 列
|
||||||
@media (max-width: 1400px) {
|
@media (max-width: 1400px) {
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1100px 以下 (iPad横屏区间):一行 3 列
|
// 1100px 以下 (iPad横屏区间):一行 3 列
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 768px 以下 (iPad竖屏):一行 2 列
|
// 768px 以下 (iPad竖屏):一行 2 列
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 480px 以下 (手机端):一行 1 列
|
// 480px 以下 (手机端):一行 1 列
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
grid-template-columns: repeat(1, 1fr);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
|
import { throttle } from "lodash-es";
|
||||||
function permissionDirective(el: HTMLElement,binding:any) {
|
function permissionDirective(el: HTMLElement, binding: any) {
|
||||||
const appStore = useUserStore();
|
const appStore = useUserStore();
|
||||||
const userPermissions = appStore.role;
|
const userPermissions = appStore.role;
|
||||||
let requiredPermissions = binding.value;
|
let requiredPermissions = binding.value;
|
||||||
|
// 使用特殊值 '*' 表示跳过权限检查
|
||||||
|
const isSkipCheck =
|
||||||
|
requiredPermissions === "*" ||
|
||||||
|
(Array.isArray(requiredPermissions) && requiredPermissions.includes("*"));
|
||||||
|
if (isSkipCheck) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof requiredPermissions === "string") {
|
if (typeof requiredPermissions === "string") {
|
||||||
requiredPermissions = [requiredPermissions];
|
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 = {
|
const directives = {
|
||||||
permission,
|
permission,
|
||||||
|
selectMore,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (app: any) => {
|
export default (app: any) => {
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export function getImageUrl(url: string) {
|
|||||||
|
|
||||||
|
|
||||||
// 设置index前缀 如:001 010 100种 默认兼容3位数
|
// 设置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');
|
return String(index + 1).padStart(padNum, '0');
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user