feat: 移除mock并接入真实权限控制
This commit is contained in:
+117
-7
@@ -58,17 +58,74 @@ export interface Role extends RoleOption {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** 员工列表是服务端分页,筛选条件统一通过查询参数传递。 */
|
||||
export interface EmployeeListParams {
|
||||
storeId?: number;
|
||||
status?: EmployeeStatus;
|
||||
keyword?: string;
|
||||
export interface PermissionPolicyMenu {
|
||||
key: string;
|
||||
title: string;
|
||||
actions: string[];
|
||||
}
|
||||
|
||||
export interface PermissionPolicy {
|
||||
roleId: number;
|
||||
roleCode: string;
|
||||
roleName: string;
|
||||
roleDescription: string | null;
|
||||
isSystem: boolean;
|
||||
editable: boolean;
|
||||
scope: string;
|
||||
permissions: string[];
|
||||
menus: PermissionPolicyMenu[];
|
||||
}
|
||||
|
||||
export interface PermissionDefinition {
|
||||
code: string;
|
||||
title: string;
|
||||
description: string;
|
||||
groupKey: string;
|
||||
groupTitle: string;
|
||||
}
|
||||
|
||||
export interface PermissionDefinitionGroup {
|
||||
key: string;
|
||||
title: string;
|
||||
permissions: PermissionDefinition[];
|
||||
}
|
||||
|
||||
export interface PermissionDefinitionMenu {
|
||||
key: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
permission: string;
|
||||
actions: string[];
|
||||
actionLabels: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PermissionDefinitions {
|
||||
permissions: PermissionDefinition[];
|
||||
groups: PermissionDefinitionGroup[];
|
||||
menus: PermissionDefinitionMenu[];
|
||||
}
|
||||
|
||||
/** 列表接口是服务端分页,筛选条件统一通过查询参数传递。 */
|
||||
export interface PageParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface StoreListParams {
|
||||
export interface EmployeeListParams extends PageParams {
|
||||
storeId?: number;
|
||||
status?: EmployeeStatus;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export interface StoreListParams extends PageParams {
|
||||
includeInactive?: boolean;
|
||||
status?: StoreStatus;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export interface RoleListParams extends PageParams {
|
||||
keyword?: string;
|
||||
isSystem?: boolean;
|
||||
}
|
||||
|
||||
export interface StorePayload {
|
||||
@@ -111,17 +168,42 @@ export interface PaginatedData<T> {
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
export type PermissionPolicyResult = ApiResult<PermissionPolicy[]>;
|
||||
export type PermissionDefinitionsResult = ApiResult<PermissionDefinitions>;
|
||||
|
||||
/** 门店接口:管理门店基础资料,并给员工下拉选项提供数据源。 */
|
||||
export const listStores = (params?: StoreListParams) => {
|
||||
export const listStoreOptions = () => {
|
||||
return http.request<ApiResult<StoreOption[]>>("get", `${API_PREFIX}/stores`);
|
||||
};
|
||||
|
||||
export const listAllStores = (
|
||||
params?: Pick<StoreListParams, "includeInactive">
|
||||
) => {
|
||||
return http.request<ApiResult<Store[]>>("get", `${API_PREFIX}/stores`, {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
export const listStores = (params: StoreListParams) => {
|
||||
return http.request<ApiResult<PaginatedData<Store>>>(
|
||||
"get",
|
||||
`${API_PREFIX}/stores`,
|
||||
{ params }
|
||||
);
|
||||
};
|
||||
|
||||
export const listRoles = () => {
|
||||
return http.request<ApiResult<Role[]>>("get", `${API_PREFIX}/roles`);
|
||||
};
|
||||
|
||||
export const listRolePage = (params: RoleListParams) => {
|
||||
return http.request<ApiResult<PaginatedData<Role>>>(
|
||||
"get",
|
||||
`${API_PREFIX}/roles`,
|
||||
{ params }
|
||||
);
|
||||
};
|
||||
|
||||
export const getStore = (id: number) => {
|
||||
return http.request<ApiResult<Store>>("get", `${API_PREFIX}/stores/${id}`);
|
||||
};
|
||||
@@ -204,3 +286,31 @@ export const updateEmployeeStatus = (id: number, status: EmployeeStatus) => {
|
||||
export const deleteEmployee = (id: number) => {
|
||||
return http.request<void>("delete", `${API_PREFIX}/employees/${id}`);
|
||||
};
|
||||
|
||||
/** 权限接口:读取权限定义、查看角色策略,并把权限点分配给角色。 */
|
||||
export const getPermissionPolicies = () => {
|
||||
return http.request<PermissionPolicyResult>(
|
||||
"get",
|
||||
`${API_PREFIX}/permissions/policies`
|
||||
);
|
||||
};
|
||||
|
||||
export const getPermissionDefinitions = () => {
|
||||
return http.request<PermissionDefinitionsResult>(
|
||||
"get",
|
||||
`${API_PREFIX}/permissions/definitions`
|
||||
);
|
||||
};
|
||||
|
||||
export const updateRolePermissions = (
|
||||
roleId: number,
|
||||
permissions: string[]
|
||||
) => {
|
||||
return http.request<ApiResult<PermissionPolicy>>(
|
||||
"put",
|
||||
`${API_PREFIX}/permissions/roles/${roleId}`,
|
||||
{
|
||||
data: { permissions }
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
type Result = {
|
||||
success: boolean;
|
||||
data: Array<any>;
|
||||
};
|
||||
|
||||
export const getAsyncRoutes = () => {
|
||||
return http.request<Result>("get", "/get-async-routes");
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineComponent, Fragment } from "vue";
|
||||
import { hasAuth } from "@/router/utils";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Auth",
|
||||
@@ -12,7 +12,7 @@ export default defineComponent({
|
||||
setup(props, { slots }) {
|
||||
return () => {
|
||||
if (!slots) return null;
|
||||
return hasAuth(props.value) ? (
|
||||
return hasPerms(props.value) ? (
|
||||
<Fragment>{slots.default?.()}</Fragment>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { hasAuth } from "@/router/utils";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
export const auth: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<string | Array<string>>) {
|
||||
const { value } = binding;
|
||||
if (value) {
|
||||
!hasAuth(value) && el.parentNode?.removeChild(el);
|
||||
!hasPerms(value) && el.parentNode?.removeChild(el);
|
||||
} else {
|
||||
throw new Error(
|
||||
"[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\""
|
||||
"[Directive: auth]: need permissions! Like v-auth=\"['store:manage']\""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -30,6 +30,14 @@ export const routerArrays: Array<RouteConfigs> = [
|
||||
title: "员工管理",
|
||||
icon: "ep/user-filled"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/permissions",
|
||||
name: "PermissionPolicies",
|
||||
meta: {
|
||||
title: "权限策略",
|
||||
icon: "ep/key"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -40,7 +48,6 @@ export type routeMetaType = {
|
||||
savedPosition?: boolean;
|
||||
permission?: string | Array<string>;
|
||||
menuKey?: string;
|
||||
auths?: Array<string>;
|
||||
};
|
||||
|
||||
export type RouteConfigs = {
|
||||
|
||||
+7
-1
@@ -173,7 +173,13 @@ router.beforeEach(async (to: ToRouteType, _from, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasRoutePermission(to.meta, useUserStoreHook().permissions ?? [])) {
|
||||
if (
|
||||
!hasRoutePermission(
|
||||
to.meta,
|
||||
useUserStoreHook().permissions ?? [],
|
||||
useUserStoreHook().permissionMenus ?? []
|
||||
)
|
||||
) {
|
||||
next({ path: "/access-denied" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ const Layout = () => import("@/layout/index.vue");
|
||||
/**
|
||||
* 权限管理业务菜单。
|
||||
*
|
||||
* 三个子页面都是静态路由,菜单展示顺序由这里的 children 决定;
|
||||
* 子页面都是静态路由,菜单展示顺序由这里的 children 决定;
|
||||
* 默认访问该模块时进入员工管理,保证和登录/登出后的主工作流一致。
|
||||
*/
|
||||
export default {
|
||||
@@ -49,6 +49,17 @@ export default {
|
||||
permission: ["employee:view:all", "employee:view:store"],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/permissions",
|
||||
name: "PermissionPolicies",
|
||||
component: () => import("@/views/permissions/index.vue"),
|
||||
meta: {
|
||||
title: "权限策略",
|
||||
menuKey: "permissions",
|
||||
permission: "permission:view",
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
} satisfies RouteConfigsTable;
|
||||
|
||||
+33
-141
@@ -9,25 +9,16 @@ import { router } from "./index";
|
||||
import { isProxy, toRaw } from "vue";
|
||||
import { useTimeoutFn } from "@vueuse/core";
|
||||
import {
|
||||
isString,
|
||||
cloneDeep,
|
||||
isAllEmpty,
|
||||
intersection,
|
||||
storageLocal,
|
||||
isIncludeAllChildren
|
||||
storageLocal
|
||||
} from "@pureadmin/utils";
|
||||
import { getConfig } from "@/config";
|
||||
import { buildHierarchyTree } from "@/utils/tree";
|
||||
import { userKey, type DataInfo } from "@/utils/auth";
|
||||
import { type menuType, routerArrays } from "@/layout/types";
|
||||
import { userKey, type DataInfo, type PermissionMenuInfo } from "@/utils/auth";
|
||||
import type { menuType } from "@/layout/types";
|
||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
const IFrame = () => import("@/layout/frame.vue");
|
||||
// https://cn.vitejs.dev/guide/features.html#glob-import
|
||||
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
|
||||
|
||||
// 动态路由
|
||||
import { getAsyncRoutes } from "@/api/routes";
|
||||
|
||||
function handRank(routeInfo: any) {
|
||||
const { name, path, parentId, meta } = routeInfo;
|
||||
@@ -81,26 +72,49 @@ function isOneOfArray(a: Array<string>, b: Array<string>) {
|
||||
: true;
|
||||
}
|
||||
|
||||
function hasRoutePermission(meta: CustomizeRouteMeta, permissions: string[]) {
|
||||
const required = meta?.permission;
|
||||
function hasWildcardPermission(permissions: string[]) {
|
||||
return permissions.includes("*") || permissions.includes("*:*:*");
|
||||
}
|
||||
|
||||
if (!required) return true;
|
||||
if (permissions.includes("*") || permissions.includes("*:*:*")) return true;
|
||||
function hasMenuAccess(
|
||||
meta: CustomizeRouteMeta,
|
||||
permissions: string[],
|
||||
permissionMenus: PermissionMenuInfo[] = []
|
||||
) {
|
||||
if (!meta?.menuKey) return true;
|
||||
if (hasWildcardPermission(permissions)) return true;
|
||||
|
||||
return permissionMenus.some(menu => menu.key === meta.menuKey);
|
||||
}
|
||||
|
||||
function hasRoutePermission(
|
||||
meta: CustomizeRouteMeta,
|
||||
permissions: string[],
|
||||
permissionMenus: PermissionMenuInfo[] = []
|
||||
) {
|
||||
const required = meta?.permission;
|
||||
const hasMenu = hasMenuAccess(meta, permissions, permissionMenus);
|
||||
|
||||
if (!required) return hasMenu;
|
||||
if (hasWildcardPermission(permissions)) return true;
|
||||
|
||||
const requiredList = Array.isArray(required) ? required : [required];
|
||||
|
||||
return requiredList.some(permission => permissions.includes(permission));
|
||||
return (
|
||||
hasMenu && requiredList.some(permission => permissions.includes(permission))
|
||||
);
|
||||
}
|
||||
|
||||
/** 从localStorage里取出当前登录用户权限码,过滤无权限菜单 */
|
||||
/** 从 localStorage 取出后端菜单和权限码,过滤无权限菜单 */
|
||||
function filterNoPermissionTree(data: RouteComponent[]) {
|
||||
const userInfo = storageLocal().getItem<DataInfo<number>>(userKey);
|
||||
const currentRoles = userInfo?.roles ?? [];
|
||||
const currentPermissions = userInfo?.permissions ?? [];
|
||||
const currentMenus = userInfo?.permissionMenus ?? [];
|
||||
const newTree = cloneDeep(data).filter((v: any) => {
|
||||
return (
|
||||
isOneOfArray(v.meta?.roles, currentRoles) &&
|
||||
hasRoutePermission(v.meta, currentPermissions)
|
||||
hasRoutePermission(v.meta, currentPermissions, currentMenus)
|
||||
);
|
||||
});
|
||||
newTree.forEach(
|
||||
@@ -169,78 +183,6 @@ function addPathMatch() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理动态路由(后端返回的路由) */
|
||||
function handleAsyncRoutes(routeList) {
|
||||
if (routeList.length === 0) {
|
||||
usePermissionStoreHook().handleWholeMenus(routeList);
|
||||
} else {
|
||||
formatFlatteningRoutes(addAsyncRoutes(routeList)).map(
|
||||
(v: RouteRecordRaw) => {
|
||||
// 防止重复添加路由
|
||||
if (
|
||||
router.options.routes[0].children.findIndex(
|
||||
value => value.path === v.path
|
||||
) !== -1
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
// 切记将路由push到routes后还需要使用addRoute,这样路由才能正常跳转
|
||||
router.options.routes[0].children.push(v);
|
||||
// 最终路由进行升序
|
||||
ascending(router.options.routes[0].children);
|
||||
if (!router.hasRoute(v?.name)) router.addRoute(v);
|
||||
const flattenRouters: any = router
|
||||
.getRoutes()
|
||||
.find(n => n.path === "/");
|
||||
// 保持router.options.routes[0].children与path为"/"的children一致,防止数据不一致导致异常
|
||||
flattenRouters.children = router.options.routes[0].children;
|
||||
router.addRoute(flattenRouters);
|
||||
}
|
||||
}
|
||||
);
|
||||
usePermissionStoreHook().handleWholeMenus(routeList);
|
||||
}
|
||||
if (!useMultiTagsStoreHook().getMultiTagsCache) {
|
||||
useMultiTagsStoreHook().handleTags("equal", [
|
||||
...routerArrays,
|
||||
...usePermissionStoreHook().flatteningRoutes.filter(
|
||||
v => v?.meta?.fixedTag
|
||||
)
|
||||
]);
|
||||
}
|
||||
addPathMatch();
|
||||
}
|
||||
|
||||
/** 初始化路由(`new Promise` 写法防止在异步请求中造成无限循环)*/
|
||||
function initRouter() {
|
||||
if (getConfig()?.CachingAsyncRoutes) {
|
||||
// 开启动态路由缓存本地localStorage
|
||||
const key = "async-routes";
|
||||
const asyncRouteList = storageLocal().getItem(key) as any;
|
||||
if (asyncRouteList && asyncRouteList?.length > 0) {
|
||||
return new Promise(resolve => {
|
||||
handleAsyncRoutes(asyncRouteList);
|
||||
resolve(router);
|
||||
});
|
||||
} else {
|
||||
return new Promise(resolve => {
|
||||
getAsyncRoutes().then(({ data }) => {
|
||||
handleAsyncRoutes(cloneDeep(data));
|
||||
storageLocal().setItem(key, data);
|
||||
resolve(router);
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return new Promise(resolve => {
|
||||
getAsyncRoutes().then(({ data }) => {
|
||||
handleAsyncRoutes(cloneDeep(data));
|
||||
resolve(router);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多级嵌套路由处理成一维数组
|
||||
* @param routesList 传入路由
|
||||
@@ -320,35 +262,6 @@ function handleAliveRoute({ name }: ToRouteType, mode?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 过滤后端传来的动态路由 重新生成规范路由 */
|
||||
function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
|
||||
if (!arrRoutes || !arrRoutes.length) return;
|
||||
const modulesRoutesKeys = Object.keys(modulesRoutes);
|
||||
arrRoutes.forEach((v: RouteRecordRaw) => {
|
||||
// 将backstage属性加入meta,标识此路由为后端返回路由
|
||||
v.meta.backstage = true;
|
||||
// 父级的redirect属性取值:如果子级存在且父级的redirect属性不存在,默认取第一个子级的path;如果子级存在且父级的redirect属性存在,取存在的redirect属性,会覆盖默认值
|
||||
if (v?.children && v.children.length && !v.redirect)
|
||||
v.redirect = v.children[0].path;
|
||||
// 父级的name属性取值:如果子级存在且父级的name属性不存在,默认取第一个子级的name;如果子级存在且父级的name属性存在,取存在的name属性,会覆盖默认值(注意:测试中发现父级的name不能和子级name重复,如果重复会造成重定向无效(跳转404),所以这里给父级的name起名的时候后面会自动加上`Parent`,避免重复)
|
||||
if (v?.children && v.children.length && !v.name)
|
||||
v.name = (v.children[0].name as string) + "Parent";
|
||||
if (v.meta?.frameSrc) {
|
||||
v.component = IFrame;
|
||||
} else {
|
||||
// 对后端传component组件路径和不传做兼容(如果后端传component组件路径,那么path可以随便写,如果不传,component组件路径会跟path保持一致)
|
||||
const index = v?.component
|
||||
? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any))
|
||||
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
|
||||
v.component = modulesRoutes[modulesRoutesKeys[index]];
|
||||
}
|
||||
if (v?.children && v.children.length) {
|
||||
addAsyncRoutes(v.children);
|
||||
}
|
||||
});
|
||||
return arrRoutes;
|
||||
}
|
||||
|
||||
/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
|
||||
function getHistoryMode(routerHistory): RouterHistory {
|
||||
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
|
||||
@@ -372,23 +285,6 @@ function getHistoryMode(routerHistory): RouterHistory {
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前页面按钮级别的权限 */
|
||||
function getAuths(): Array<string> {
|
||||
return router.currentRoute.value.meta.auths as Array<string>;
|
||||
}
|
||||
|
||||
/** 是否有按钮级别的权限(根据路由`meta`中的`auths`字段进行判断)*/
|
||||
function hasAuth(value: string | Array<string>): boolean {
|
||||
if (!value) return false;
|
||||
/** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */
|
||||
const metaAuths = getAuths();
|
||||
if (!metaAuths) return false;
|
||||
const isAuths = isString(value)
|
||||
? metaAuths.includes(value)
|
||||
: isIncludeAllChildren(value, metaAuths);
|
||||
return isAuths ? true : false;
|
||||
}
|
||||
|
||||
function handleTopMenu(route) {
|
||||
if (route?.children && route.children.length > 1) {
|
||||
if (route.redirect) {
|
||||
@@ -411,17 +307,13 @@ function getTopMenu(tag = false): menuType {
|
||||
}
|
||||
|
||||
export {
|
||||
hasAuth,
|
||||
getAuths,
|
||||
ascending,
|
||||
filterTree,
|
||||
initRouter,
|
||||
getTopMenu,
|
||||
addPathMatch,
|
||||
isOneOfArray,
|
||||
hasRoutePermission,
|
||||
getHistoryMode,
|
||||
addAsyncRoutes,
|
||||
getParentPaths,
|
||||
findRouteByPath,
|
||||
handleAliveRoute,
|
||||
|
||||
@@ -100,6 +100,15 @@ export const hasPerms = (value: string | Array<string>): boolean => {
|
||||
: isIncludeAllChildren(value, permissions);
|
||||
};
|
||||
|
||||
/** 是否拥有后端返回的菜单入口。 */
|
||||
export const hasMenuAccess = (menuKey: string): boolean => {
|
||||
if (hasPerms("*")) return true;
|
||||
|
||||
const menus = getUserInfo()?.permissionMenus ?? [];
|
||||
|
||||
return menus.some(item => item.key === menuKey);
|
||||
};
|
||||
|
||||
/** 是否拥有后端菜单动作权限,用于比权限码更细的按钮显隐。 */
|
||||
export const hasMenuAction = (menuKey: string, action: string): boolean => {
|
||||
if (hasPerms("*")) return true;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { removeToken, setToken, type DataInfo } from "./auth";
|
||||
import { subBefore, getQueryMap } from "@pureadmin/utils";
|
||||
|
||||
/**
|
||||
* 简版前端单点登录,根据实际业务自行编写,平台启动后本地可以跳后面这个链接进行测试 http://localhost:8848/#/permission/page/index?username=sso&roles=admin&accessToken=eyJhbGciOiJIUzUxMiJ9.admin
|
||||
* 简版前端单点登录,根据实际业务自行编写,平台启动后本地可以跳后面这个链接进行测试 http://localhost:8848/#/employees?username=sso&roles=admin&accessToken=eyJhbGciOiJIUzUxMiJ9.admin
|
||||
* 划重点:
|
||||
* 判断是否为单点登录,不为则直接返回不再进行任何逻辑处理,下面是单点登录后的逻辑处理
|
||||
* 1.清空本地旧信息;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getEmployee,
|
||||
listEmployees,
|
||||
listRoles,
|
||||
listStores,
|
||||
listStoreOptions,
|
||||
updateEmployee,
|
||||
updateEmployeeStatus,
|
||||
type Employee,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
type RoleOption,
|
||||
type StoreOption
|
||||
} from "@/api/access";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { hasMenuAction, hasPerms } from "@/utils/auth";
|
||||
|
||||
import Plus from "~icons/ep/plus";
|
||||
import Search from "~icons/ep/search";
|
||||
@@ -107,7 +107,12 @@ const inactiveCount = computed(
|
||||
() => employees.value.filter(item => item.status === "INACTIVE").length
|
||||
);
|
||||
const dialogTitle = computed(() => (form.id ? "编辑员工" : "新增员工"));
|
||||
const canManageEmployees = computed(() => hasPerms("employee:manage"));
|
||||
const canCreateEmployee = computed(() => hasMenuAction("employees", "create"));
|
||||
const canUpdateEmployee = computed(() => hasMenuAction("employees", "update"));
|
||||
const canDeleteEmployee = computed(() => hasMenuAction("employees", "delete"));
|
||||
const canOperateEmployee = computed(
|
||||
() => canUpdateEmployee.value || canDeleteEmployee.value
|
||||
);
|
||||
const canViewAllEmployees = computed(() => hasPerms("employee:view:all"));
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
@@ -158,15 +163,17 @@ function buildPayload(): EmployeePayload {
|
||||
/** 门店和角色是员工表单的基础字典,打开弹窗前必须先加载。 */
|
||||
async function fetchCatalog() {
|
||||
const shouldLoadStores =
|
||||
canViewAllEmployees.value || canManageEmployees.value;
|
||||
const shouldLoadRoles = canManageEmployees.value;
|
||||
canViewAllEmployees.value ||
|
||||
canCreateEmployee.value ||
|
||||
canUpdateEmployee.value;
|
||||
const shouldLoadRoles = canCreateEmployee.value || canUpdateEmployee.value;
|
||||
|
||||
if (!shouldLoadStores && !shouldLoadRoles) return;
|
||||
|
||||
catalogLoading.value = true;
|
||||
try {
|
||||
const [storeResult, roleResult] = await Promise.all([
|
||||
shouldLoadStores ? listStores() : Promise.resolve({ data: [] }),
|
||||
shouldLoadStores ? listStoreOptions() : Promise.resolve({ data: [] }),
|
||||
shouldLoadRoles ? listRoles() : Promise.resolve({ data: [] })
|
||||
]);
|
||||
stores.value = storeResult.data;
|
||||
@@ -227,14 +234,14 @@ function handleSizeChange(pageSize: number) {
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canCreateEmployee.value) return;
|
||||
resetFormState();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
/** 编辑前重新拉详情,确保角色绑定不是来自列表摘要的过期数据。 */
|
||||
async function openEditDialog(row: Employee) {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canUpdateEmployee.value) return;
|
||||
try {
|
||||
const result = await getEmployee(row.id);
|
||||
const employee = result.data;
|
||||
@@ -255,7 +262,7 @@ async function openEditDialog(row: Employee) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (form.id ? !canUpdateEmployee.value : !canCreateEmployee.value) return;
|
||||
await formRef.value?.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
@@ -280,7 +287,7 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function toggleStatus(row: Employee) {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canUpdateEmployee.value) return;
|
||||
const nextStatus: EmployeeStatus =
|
||||
row.status === "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
const action = nextStatus === "ACTIVE" ? "启用" : "停用";
|
||||
@@ -306,7 +313,7 @@ async function toggleStatus(row: Employee) {
|
||||
}
|
||||
|
||||
async function removeEmployee(row: Employee) {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canDeleteEmployee.value) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除后员工「${row.name}」会被软删除并停用,确认继续?`,
|
||||
@@ -345,7 +352,7 @@ onMounted(async () => {
|
||||
<h1>员工管理</h1>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManageEmployees"
|
||||
v-if="canCreateEmployee"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openCreateDialog"
|
||||
@@ -470,13 +477,14 @@ onMounted(async () => {
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageEmployees"
|
||||
v-if="canOperateEmployee"
|
||||
label="操作"
|
||||
width="260"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="canUpdateEmployee"
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
@@ -485,6 +493,7 @@ onMounted(async () => {
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canUpdateEmployee"
|
||||
link
|
||||
:type="row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||
:icon="row.status === 'ACTIVE' ? CircleClose : CircleCheck"
|
||||
@@ -493,6 +502,7 @@ onMounted(async () => {
|
||||
{{ row.status === "ACTIVE" ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteEmployee"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@@ -618,9 +628,9 @@ onMounted(async () => {
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
@@ -733,9 +743,9 @@ onMounted(async () => {
|
||||
|
||||
.pagination-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
@@ -761,8 +771,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
@@ -781,8 +791,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { hasAuth, getAuths } from "@/router/utils";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionButtonRouter"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">当前拥有的code列表:{{ getAuths() }}</p>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">组件方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<Auth value="permission:btn:add">
|
||||
<el-button plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth :value="['permission:btn:edit']">
|
||||
<el-button plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth
|
||||
:value="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
>
|
||||
<el-button plain type="danger">
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">函数方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasAuth([
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
])
|
||||
"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
指令方式判断权限(该方式不能动态修改权限)
|
||||
</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-auth="'permission:btn:add'" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-auth="['permission:btn:edit']" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-auth="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,109 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
|
||||
const { permissions } = useUserStoreHook();
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionButtonLogin"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">当前拥有的code列表:{{ permissions }}</p>
|
||||
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2!">
|
||||
*:*:* 代表拥有全部按钮级别权限
|
||||
</p>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">组件方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<Perms value="permission:btn:add">
|
||||
<el-button plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
<Perms :value="['permission:btn:edit']">
|
||||
<el-button plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
<Perms
|
||||
:value="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
>
|
||||
<el-button plain type="danger">
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">函数方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-if="hasPerms('permission:btn:add')" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPerms(['permission:btn:edit'])"
|
||||
plain
|
||||
type="primary"
|
||||
>
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasPerms([
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
])
|
||||
"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
指令方式判断权限(该方式不能动态修改权限)
|
||||
</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-perms="'permission:btn:add'" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-perms="['permission:btn:edit']" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { initRouter } from "@/router/utils";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { type CSSProperties, ref, computed } from "vue";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionPage"
|
||||
});
|
||||
|
||||
const elStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
width: "85vw",
|
||||
justifyContent: "start"
|
||||
};
|
||||
});
|
||||
|
||||
const username = ref(useUserStoreHook()?.username);
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "admin",
|
||||
label: "管理员角色"
|
||||
},
|
||||
{
|
||||
value: "common",
|
||||
label: "普通角色"
|
||||
}
|
||||
];
|
||||
|
||||
function onChange() {
|
||||
useUserStoreHook()
|
||||
.loginByUsername({ username: username.value, password: "admin123" })
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
storageLocal().removeItem("async-routes");
|
||||
usePermissionStoreHook().clearAllCachePage();
|
||||
initRouter();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">
|
||||
模拟后台根据不同角色返回对应路由,观察左侧菜单变化(管理员角色可查看系统管理菜单、普通角色不可查看系统管理菜单)
|
||||
</p>
|
||||
<el-card shadow="never" :style="elStyle">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>当前角色:{{ username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="username" class="w-[160px]!" @change="onChange">
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,533 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
getPermissionDefinitions,
|
||||
getPermissionPolicies,
|
||||
updateRolePermissions,
|
||||
type PermissionDefinitionGroup,
|
||||
type PermissionPolicy
|
||||
} from "@/api/access";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { hasMenuAction } from "@/utils/auth";
|
||||
|
||||
import Check from "~icons/ep/check";
|
||||
import Edit from "~icons/ep/edit";
|
||||
import Refresh from "~icons/ep/refresh";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionPolicies"
|
||||
});
|
||||
|
||||
const tableLoading = ref(false);
|
||||
const saving = ref(false);
|
||||
const drawerVisible = ref(false);
|
||||
const policies = ref<PermissionPolicy[]>([]);
|
||||
const definitionGroups = ref<PermissionDefinitionGroup[]>([]);
|
||||
const editingPolicy = ref<PermissionPolicy | null>(null);
|
||||
const checkedPermissions = ref<string[]>([]);
|
||||
|
||||
const canUpdatePermissions = computed(() =>
|
||||
hasMenuAction("permissions", "update")
|
||||
);
|
||||
const editableRoleCount = computed(
|
||||
() => policies.value.filter(item => item.editable).length
|
||||
);
|
||||
const menuTotal = computed(() =>
|
||||
policies.value.reduce((total, item) => total + item.menus.length, 0)
|
||||
);
|
||||
const permissionTotal = computed(() =>
|
||||
policies.value.reduce(
|
||||
(total, item) =>
|
||||
total + item.permissions.filter(permission => permission !== "*").length,
|
||||
0
|
||||
)
|
||||
);
|
||||
const drawerTitle = computed(() =>
|
||||
editingPolicy.value ? `分配权限:${editingPolicy.value.roleName}` : "分配权限"
|
||||
);
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
const message = (
|
||||
error as { response?: { data?: { error?: { message?: string } } } }
|
||||
)?.response?.data?.error?.message;
|
||||
|
||||
if (message) return message;
|
||||
if (error instanceof Error && error.message) return error.message;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function formatActions(actions: string[]) {
|
||||
const actionMap: Record<string, string> = {
|
||||
view: "查看",
|
||||
create: "新增",
|
||||
update: "编辑",
|
||||
delete: "删除"
|
||||
};
|
||||
|
||||
return actions.map(action => actionMap[action] ?? action).join(" / ");
|
||||
}
|
||||
|
||||
function formatPermissionTitle(code: string) {
|
||||
for (const group of definitionGroups.value) {
|
||||
const found = group.permissions.find(permission => permission.code === code);
|
||||
if (found) return found.title;
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function isGroupChecked(group: PermissionDefinitionGroup) {
|
||||
const codes = group.permissions.map(permission => permission.code);
|
||||
return codes.every(code => checkedPermissions.value.includes(code));
|
||||
}
|
||||
|
||||
function isGroupIndeterminate(group: PermissionDefinitionGroup) {
|
||||
const codes = group.permissions.map(permission => permission.code);
|
||||
const checkedCount = codes.filter(code =>
|
||||
checkedPermissions.value.includes(code)
|
||||
).length;
|
||||
|
||||
return checkedCount > 0 && checkedCount < codes.length;
|
||||
}
|
||||
|
||||
function toggleGroup(group: PermissionDefinitionGroup, checked: boolean) {
|
||||
const nextPermissions = new Set(checkedPermissions.value);
|
||||
|
||||
for (const permission of group.permissions) {
|
||||
if (checked) {
|
||||
nextPermissions.add(permission.code);
|
||||
} else {
|
||||
nextPermissions.delete(permission.code);
|
||||
}
|
||||
}
|
||||
|
||||
checkedPermissions.value = [...nextPermissions];
|
||||
}
|
||||
|
||||
function openEditor(policy: PermissionPolicy) {
|
||||
if (!policy.editable || !canUpdatePermissions.value) return;
|
||||
|
||||
editingPolicy.value = policy;
|
||||
checkedPermissions.value = policy.permissions.filter(
|
||||
permission => permission !== "*"
|
||||
);
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
async function fetchPermissionData() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const [policyResult, definitionResult] = await Promise.all([
|
||||
getPermissionPolicies(),
|
||||
getPermissionDefinitions()
|
||||
]);
|
||||
|
||||
policies.value = policyResult.data;
|
||||
definitionGroups.value = definitionResult.data.groups;
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "加载权限数据失败"));
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePermissions() {
|
||||
if (!editingPolicy.value) return;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateRolePermissions(
|
||||
editingPolicy.value.roleId,
|
||||
checkedPermissions.value
|
||||
);
|
||||
await fetchPermissionData();
|
||||
await useUserStoreHook().loadAuthContext(true);
|
||||
drawerVisible.value = false;
|
||||
ElMessage.success("权限已更新");
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "保存权限失败"));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchPermissionData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="permission-page">
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">动态权限分配</p>
|
||||
<h1>权限策略</h1>
|
||||
</div>
|
||||
<el-button :icon="Refresh" @click="fetchPermissionData">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span>角色策略</span>
|
||||
<strong>{{ policies.length }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>可分配角色</span>
|
||||
<strong>{{ editableRoleCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>权限码</span>
|
||||
<strong>{{ permissionTotal }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>菜单授权</span>
|
||||
<strong>{{ menuTotal }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-shell">
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="policies"
|
||||
row-key="roleCode"
|
||||
stripe
|
||||
class="permission-table"
|
||||
>
|
||||
<el-table-column label="角色" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="role-cell">
|
||||
<strong>{{ row.roleName }}</strong>
|
||||
<span>{{ row.roleCode }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="scope" label="作用范围" min-width="140" />
|
||||
<el-table-column label="权限码" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<div class="tag-list">
|
||||
<el-tag
|
||||
v-for="permission in row.permissions"
|
||||
:key="permission"
|
||||
effect="plain"
|
||||
size="small"
|
||||
>
|
||||
{{ permission === "*" ? "全部权限" : formatPermissionTitle(permission) }}
|
||||
</el-tag>
|
||||
<span v-if="row.permissions.length === 0" class="muted-text">
|
||||
未分配
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="菜单与动作" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<div class="menu-list">
|
||||
<div v-for="menu in row.menus" :key="menu.key" class="menu-item">
|
||||
<strong>{{ menu.title }}</strong>
|
||||
<span>{{ formatActions(menu.actions) }}</span>
|
||||
</div>
|
||||
<span v-if="row.menus.length === 0" class="muted-text">
|
||||
无后台菜单
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="132" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
:content="row.editable ? '分配权限' : '系统最高权限不可编辑'"
|
||||
placement="top"
|
||||
>
|
||||
<span>
|
||||
<el-button
|
||||
:icon="Edit"
|
||||
:disabled="!row.editable || !canUpdatePermissions"
|
||||
text
|
||||
type="primary"
|
||||
@click="openEditor(row)"
|
||||
>
|
||||
分配
|
||||
</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
size="520px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="drawer-body">
|
||||
<section
|
||||
v-for="group in definitionGroups"
|
||||
:key="group.key"
|
||||
class="permission-group"
|
||||
>
|
||||
<div class="group-heading">
|
||||
<strong>{{ group.title }}</strong>
|
||||
<el-checkbox
|
||||
:model-value="isGroupChecked(group)"
|
||||
:indeterminate="isGroupIndeterminate(group)"
|
||||
@change="value => toggleGroup(group, Boolean(value))"
|
||||
>
|
||||
全选
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div class="permission-options">
|
||||
<el-checkbox-group v-model="checkedPermissions">
|
||||
<el-checkbox
|
||||
v-for="permission in group.permissions"
|
||||
:key="permission.code"
|
||||
:label="permission.code"
|
||||
border
|
||||
>
|
||||
<span class="permission-option">
|
||||
<strong>{{ permission.title }}</strong>
|
||||
<small>{{ permission.code }}</small>
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="drawerVisible = false">取消</el-button>
|
||||
<el-button
|
||||
:icon="Check"
|
||||
:loading="saving"
|
||||
type="primary"
|
||||
@click="savePermissions"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.permission-page {
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
background: #f6f8fb;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
margin: 4px 0 0;
|
||||
font-size: 24px;
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-shell {
|
||||
padding: 8px 0 14px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.permission-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.role-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
||||
strong {
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
|
||||
strong {
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.muted-text {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.permission-group {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.permission-group:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.permission-group:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.group-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
strong {
|
||||
font-size: 15px;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-options {
|
||||
:deep(.el-checkbox-group) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox.is-bordered) {
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-option {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
|
||||
strong {
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
small {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.permission-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.permission-options :deep(.el-checkbox-group) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+107
-37
@@ -9,12 +9,12 @@ import {
|
||||
import {
|
||||
createRole,
|
||||
deleteRole,
|
||||
listRoles,
|
||||
listRolePage,
|
||||
updateRole,
|
||||
type Role,
|
||||
type RolePayload
|
||||
} from "@/api/access";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { hasMenuAction } from "@/utils/auth";
|
||||
|
||||
import Plus from "~icons/ep/plus";
|
||||
import Search from "~icons/ep/search";
|
||||
@@ -40,7 +40,16 @@ const formRef = ref<FormInstance>();
|
||||
const roles = ref<Role[]>([]);
|
||||
|
||||
const query = reactive({
|
||||
keyword: ""
|
||||
keyword: "",
|
||||
isSystem: undefined as boolean | undefined,
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
/** 角色管理列表由后端分页,这里只记录当前筛选结果的分页摘要。 */
|
||||
const pagination = reactive({
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
const form = reactive<RoleFormState>({
|
||||
@@ -72,26 +81,15 @@ const describedCount = computed(
|
||||
() => roles.value.filter(item => Boolean(item.description)).length
|
||||
);
|
||||
const systemRoleCount = computed(
|
||||
() => roles.value.filter(item => item.code === "admin").length
|
||||
() => roles.value.filter(item => item.isSystem).length
|
||||
);
|
||||
const dialogTitle = computed(() => (form.id ? "编辑角色" : "新增角色"));
|
||||
const canManageRoles = computed(() => hasPerms("role:manage"));
|
||||
|
||||
function applyRoleQuery(items: Role[]) {
|
||||
const keyword = query.keyword.trim().toLowerCase();
|
||||
|
||||
if (keyword.length === 0) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.filter(role => {
|
||||
return (
|
||||
role.code.toLowerCase().includes(keyword) ||
|
||||
role.name.toLowerCase().includes(keyword) ||
|
||||
(role.description ?? "").toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
}
|
||||
const canCreateRole = computed(() => hasMenuAction("roles", "create"));
|
||||
const canUpdateRole = computed(() => hasMenuAction("roles", "update"));
|
||||
const canDeleteRole = computed(() => hasMenuAction("roles", "delete"));
|
||||
const canOperateRole = computed(
|
||||
() => canUpdateRole.value || canDeleteRole.value
|
||||
);
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
const message = (
|
||||
@@ -134,12 +132,19 @@ function buildPayload(): RolePayload {
|
||||
};
|
||||
}
|
||||
|
||||
/** 角色查询必须走接口,避免搜索条件只在前端过滤当前缓存。 */
|
||||
/** 角色筛选、分页必须走接口,避免查询条件只在前端过滤当前缓存。 */
|
||||
async function fetchRoles() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const result = await listRoles();
|
||||
roles.value = applyRoleQuery(result.data);
|
||||
const result = await listRolePage({
|
||||
keyword: query.keyword.trim() || undefined,
|
||||
isSystem: query.isSystem,
|
||||
page: query.page,
|
||||
pageSize: query.pageSize
|
||||
});
|
||||
roles.value = result.data.items;
|
||||
pagination.total = result.data.pagination.total;
|
||||
pagination.totalPages = result.data.pagination.totalPages;
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "加载角色列表失败"));
|
||||
} finally {
|
||||
@@ -149,22 +154,37 @@ async function fetchRoles() {
|
||||
|
||||
function handleReset() {
|
||||
query.keyword = "";
|
||||
query.isSystem = undefined;
|
||||
query.page = 1;
|
||||
query.pageSize = 20;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
query.keyword = query.keyword.trim();
|
||||
query.page = 1;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
query.page = page;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function handleSizeChange(pageSize: number) {
|
||||
query.page = 1;
|
||||
query.pageSize = pageSize;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageRoles.value) return;
|
||||
if (!canCreateRole.value) return;
|
||||
resetFormState();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: Role) {
|
||||
if (!canManageRoles.value || row.isSystem) return;
|
||||
if (!canUpdateRole.value || row.isSystem) return;
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -176,7 +196,7 @@ function openEditDialog(row: Role) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!canManageRoles.value) return;
|
||||
if (form.id ? !canUpdateRole.value : !canCreateRole.value) return;
|
||||
await formRef.value?.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
@@ -201,7 +221,7 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function removeRole(row: Role) {
|
||||
if (!canManageRoles.value || row.isSystem) return;
|
||||
if (!canDeleteRole.value || row.isSystem) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除后角色「${row.name}」无法再绑定员工,确认继续?`,
|
||||
@@ -214,6 +234,10 @@ async function removeRole(row: Role) {
|
||||
);
|
||||
await deleteRole(row.id);
|
||||
ElMessage.success("角色已删除");
|
||||
|
||||
if (roles.value.length === 1 && query.page > 1) {
|
||||
query.page -= 1;
|
||||
}
|
||||
fetchRoles();
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
@@ -233,7 +257,7 @@ onMounted(fetchRoles);
|
||||
<h1>角色管理</h1>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManageRoles"
|
||||
v-if="canCreateRole"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openCreateDialog"
|
||||
@@ -245,14 +269,14 @@ onMounted(fetchRoles);
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span>总角色</span>
|
||||
<strong>{{ roles.length }}</strong>
|
||||
<strong>{{ pagination.total }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>已配置说明</span>
|
||||
<span>当前页有说明</span>
|
||||
<strong>{{ describedCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>系统角色</span>
|
||||
<span>当前页系统角色</span>
|
||||
<strong>{{ systemRoleCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
@@ -262,6 +286,16 @@ onMounted(fetchRoles);
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-select
|
||||
v-model="query.isSystem"
|
||||
clearable
|
||||
placeholder="全部类型"
|
||||
class="toolbar-control"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="系统内置" :value="true" />
|
||||
<el-option label="自定义角色" :value="false" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
@@ -305,14 +339,14 @@ onMounted(fetchRoles);
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageRoles"
|
||||
v-if="canOperateRole"
|
||||
label="操作"
|
||||
width="170"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="!row.isSystem"
|
||||
v-if="canUpdateRole && !row.isSystem"
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
@@ -321,7 +355,7 @@ onMounted(fetchRoles);
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!row.isSystem"
|
||||
v-if="canDeleteRole && !row.isSystem"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@@ -333,6 +367,22 @@ onMounted(fetchRoles);
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-row">
|
||||
<span
|
||||
>共 {{ pagination.total }} 条,{{ pagination.totalPages }} 页</span
|
||||
>
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.pageSize"
|
||||
background
|
||||
layout="sizes, prev, pager, next"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -391,9 +441,9 @@ onMounted(fetchRoles);
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
@@ -459,6 +509,10 @@ onMounted(fetchRoles);
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.toolbar-control {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -474,6 +528,16 @@ onMounted(fetchRoles);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.role-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -510,8 +574,8 @@ onMounted(fetchRoles);
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
@@ -519,6 +583,7 @@ onMounted(fetchRoles);
|
||||
}
|
||||
|
||||
.keyword-input,
|
||||
.toolbar-control,
|
||||
.toolbar-actions {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
@@ -528,6 +593,11 @@ onMounted(fetchRoles);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
+88
-34
@@ -15,7 +15,7 @@ import {
|
||||
type StorePayload,
|
||||
type StoreStatus
|
||||
} from "@/api/access";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { hasMenuAction } from "@/utils/auth";
|
||||
|
||||
import Plus from "~icons/ep/plus";
|
||||
import Search from "~icons/ep/search";
|
||||
@@ -42,7 +42,15 @@ const stores = ref<Store[]>([]);
|
||||
|
||||
const query = reactive({
|
||||
status: undefined as StoreStatus | undefined,
|
||||
keyword: ""
|
||||
keyword: "",
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
/** 门店管理列表由后端分页,这里只记录当前筛选结果的分页摘要。 */
|
||||
const pagination = reactive({
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
const form = reactive<StoreFormState>({
|
||||
@@ -71,23 +79,12 @@ const inactiveCount = computed(
|
||||
() => stores.value.filter(item => item.status === "INACTIVE").length
|
||||
);
|
||||
const dialogTitle = computed(() => (form.id ? "编辑门店" : "新增门店"));
|
||||
const canManageStores = computed(() => hasPerms("store:manage"));
|
||||
|
||||
function applyStoreQuery(items: Store[]) {
|
||||
const keyword = query.keyword.trim().toLowerCase();
|
||||
|
||||
return items.filter(store => {
|
||||
const matchedStatus =
|
||||
query.status === undefined || store.status === query.status;
|
||||
const matchedKeyword =
|
||||
keyword.length === 0 ||
|
||||
store.name.toLowerCase().includes(keyword) ||
|
||||
(store.address ?? "").toLowerCase().includes(keyword) ||
|
||||
(store.phone ?? "").toLowerCase().includes(keyword);
|
||||
|
||||
return matchedStatus && matchedKeyword;
|
||||
});
|
||||
}
|
||||
const canCreateStore = computed(() => hasMenuAction("stores", "create"));
|
||||
const canUpdateStore = computed(() => hasMenuAction("stores", "update"));
|
||||
const canDeleteStore = computed(() => hasMenuAction("stores", "delete"));
|
||||
const canOperateStore = computed(
|
||||
() => canUpdateStore.value || canDeleteStore.value
|
||||
);
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
const message = (
|
||||
@@ -133,14 +130,19 @@ function buildPayload(): StorePayload {
|
||||
};
|
||||
}
|
||||
|
||||
/** 门店筛选必须走接口,避免查询条件只停留在当前页面内存里。 */
|
||||
/** 门店筛选、分页必须走接口,避免查询条件只停留在当前页面内存里。 */
|
||||
async function fetchStores() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const result = await listStores({
|
||||
includeInactive: true
|
||||
status: query.status,
|
||||
keyword: query.keyword.trim() || undefined,
|
||||
page: query.page,
|
||||
pageSize: query.pageSize
|
||||
});
|
||||
stores.value = applyStoreQuery(result.data);
|
||||
stores.value = result.data.items;
|
||||
pagination.total = result.data.pagination.total;
|
||||
pagination.totalPages = result.data.pagination.totalPages;
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "加载门店列表失败"));
|
||||
} finally {
|
||||
@@ -151,22 +153,36 @@ async function fetchStores() {
|
||||
function handleReset() {
|
||||
query.status = undefined;
|
||||
query.keyword = "";
|
||||
query.page = 1;
|
||||
query.pageSize = 20;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
query.keyword = query.keyword.trim();
|
||||
query.page = 1;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
query.page = page;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function handleSizeChange(pageSize: number) {
|
||||
query.page = 1;
|
||||
query.pageSize = pageSize;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canCreateStore.value) return;
|
||||
resetFormState();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: Store) {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canUpdateStore.value) return;
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -179,7 +195,7 @@ function openEditDialog(row: Store) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!canManageStores.value) return;
|
||||
if (form.id ? !canUpdateStore.value : !canCreateStore.value) return;
|
||||
await formRef.value?.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
@@ -204,7 +220,7 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function toggleStatus(row: Store) {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canUpdateStore.value) return;
|
||||
const nextStatus: StoreStatus =
|
||||
row.status === "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
const action = nextStatus === "ACTIVE" ? "启用" : "停用";
|
||||
@@ -230,7 +246,7 @@ async function toggleStatus(row: Store) {
|
||||
}
|
||||
|
||||
async function removeStore(row: Store) {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canDeleteStore.value) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除后门店「${row.name}」不会再出现在员工新增下拉里,确认继续?`,
|
||||
@@ -243,6 +259,10 @@ async function removeStore(row: Store) {
|
||||
);
|
||||
await deleteStore(row.id);
|
||||
ElMessage.success("门店已删除");
|
||||
|
||||
if (stores.value.length === 1 && query.page > 1) {
|
||||
query.page -= 1;
|
||||
}
|
||||
fetchStores();
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
@@ -262,7 +282,7 @@ onMounted(fetchStores);
|
||||
<h1>门店管理</h1>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManageStores"
|
||||
v-if="canCreateStore"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openCreateDialog"
|
||||
@@ -274,14 +294,14 @@ onMounted(fetchStores);
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span>总门店</span>
|
||||
<strong>{{ stores.length }}</strong>
|
||||
<strong>{{ pagination.total }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>启用门店</span>
|
||||
<span>当前页启用</span>
|
||||
<strong>{{ activeCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>停用门店</span>
|
||||
<span>当前页停用</span>
|
||||
<strong>{{ inactiveCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
@@ -356,13 +376,14 @@ onMounted(fetchStores);
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageStores"
|
||||
v-if="canOperateStore"
|
||||
label="操作"
|
||||
width="260"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="canUpdateStore"
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
@@ -371,6 +392,7 @@ onMounted(fetchStores);
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canUpdateStore"
|
||||
link
|
||||
:type="row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||
:icon="row.status === 'ACTIVE' ? CircleClose : CircleCheck"
|
||||
@@ -379,6 +401,7 @@ onMounted(fetchStores);
|
||||
{{ row.status === "ACTIVE" ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteStore"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@@ -389,6 +412,22 @@ onMounted(fetchStores);
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-row">
|
||||
<span
|
||||
>共 {{ pagination.total }} 条,{{ pagination.totalPages }} 页</span
|
||||
>
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.pageSize"
|
||||
background
|
||||
layout="sizes, prev, pager, next"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -453,9 +492,9 @@ onMounted(fetchStores);
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
@@ -540,6 +579,16 @@ onMounted(fetchStores);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.primary-text {
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
@@ -561,8 +610,8 @@ onMounted(fetchStores);
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
@@ -580,6 +629,11 @@ onMounted(fetchStores);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user