fix: enforce platform role menu access

This commit is contained in:
2026-07-27 12:48:35 +08:00
parent cd58e1f6b6
commit 160172bf10
33 changed files with 481 additions and 107 deletions

View File

@@ -3,7 +3,7 @@ import { request } from './http';
/** 平台登录和当前账号资料的接口模型。 */
export type LoginData = { username: string; password: string };
export type LoginReply = { access_token: string; token_type: string; identity: string; display_name: string; role_code: string };
export type Profile = { identity: string; username: string; display_name: string; avatar: string; role_code: string };
export type Profile = { identity: string; username: string; display_name: string; avatar: string; role_code: string; menu_codes: string[] };
export const authApi = {
login: (data: LoginData) => request<LoginReply>('/auth/login', { method: 'POST', body: JSON.stringify(data) }),

View File

@@ -11,4 +11,16 @@ export const platformApi = {
listRole: () => resourceApi.list<PlatformRole>('/platform/platform_role'),
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform/platform_role', data),
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/platform/platform_menu'),
listRoleMenuIdentities: (identity: string) =>
request<{ menu_identities: string[] }>(
`/platform/platform_role/${identity}/menu`,
),
replaceRoleMenus: (identity: string, menuIdentities: string[]) =>
request<{ updated: boolean }>(
`/platform/platform_role/${identity}/menu`,
{
method: 'PUT',
body: JSON.stringify({ menu_identities: menuIdentities }),
},
),
};

View File

@@ -1,10 +1,10 @@
import type { ResourceField } from './resources';
export type ResourceFormValue = string | number | boolean | undefined;
export type ResourceFormValue = string | number | boolean | string[] | undefined;
export type ResourceFormMode = 'create' | 'edit';
export function isMissingField(value: ResourceFormValue | null): boolean {
return value === '' || value === null || value === undefined;
return value === '' || value === null || value === undefined || (Array.isArray(value) && value.length === 0);
}
export function isResourceFieldRequired(

View File

@@ -4,6 +4,8 @@ export type ResourceFieldType =
| 'text'
| 'password'
| 'identity'
| 'role-code'
| 'menu-identities'
| 'number'
| 'boolean'
| 'date'
@@ -105,6 +107,7 @@ const labels: Record<string, string> = {
category: '分类',
priority: '优先级',
platform_role_code: '平台角色',
menu_identities: '菜单权限',
data_scope: '数据范围',
menu_code: '菜单编码',
icon: '图标',
@@ -181,6 +184,8 @@ const jsonFields = new Set([
]);
const textareaFields = new Set(['body', 'content', 'reason', 'opinion']);
const fieldType = (key: string): ResourceFieldType => {
if (key === 'platform_role_code') return 'role-code';
if (key === 'menu_identities') return 'menu-identities';
if (key.endsWith('_identity')) return 'identity';
if (key === 'password') return 'password';
if (numberFields.has(key)) return 'number';
@@ -541,14 +546,23 @@ export const resources: ResourceUiDefinition[] = [
'password!',
'display_name',
'avatar',
'platform_role_code',
'platform_role_code!',
'phone',
]),
define('platform_role', '/platform/platform_role', 'writable', 'list', [
'role_code!',
'name!',
'data_scope!',
]),
define(
'platform_role',
'/platform/platform_role',
'writable',
'list',
['role_code!', 'name!', 'data_scope!'],
[
action(
'分配菜单',
'/platform/platform_role/:identity/menu',
['menu_identities!'],
),
],
),
define('platform_menu', '/platform/platform_menu', 'writable', 'tree', [
'parent_identity',
'menu_code!',

View File

@@ -7,19 +7,24 @@ export default function usePermission() {
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
return (
!route.meta?.requiresAuth ||
!route.meta?.roles ||
route.meta?.roles?.includes('*') ||
route.meta?.roles?.includes(userStore.role)
userStore.role === 'root' ||
(!route.meta?.menuCode && !route.meta?.roles) ||
Boolean(
route.meta?.menuCode &&
userStore.menuCodes.includes(route.meta.menuCode),
) ||
Boolean(route.meta?.roles?.includes(userStore.role))
);
},
findFirstPermissionRoute(_routers: any, role = 'admin') {
findFirstPermissionRoute(_routers: any, role = userStore.role) {
const cloneRouters = [..._routers];
while (cloneRouters.length) {
const firstElement = cloneRouters.shift();
if (
firstElement?.meta?.roles?.find((el: string[]) => {
return el.includes('*') || el.includes(role);
})
role === 'root' ||
(firstElement?.meta?.menuCode &&
userStore.menuCodes.includes(firstElement.meta.menuCode)) ||
firstElement?.meta?.roles?.includes(role)
)
return { name: firstElement.name };
if (firstElement?.children) {

View File

@@ -4,9 +4,9 @@ const routes: AppRouteRecordRaw[] = [{
path: '/audit', name: 'audit', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.audit', requiresAuth: true, icon: 'icon-apps', order: 22 },
children: [
{ path: 'aud-operation-log', name: 'audit-aud-operation-log', component: () => import('@/views/audit/aud_operation_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.aud_operation_log', requiresAuth: true, roles: ['*'] } },
{ path: 'aud-export-log', name: 'audit-aud-export-log', component: () => import('@/views/audit/aud_export_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.aud_export_log', requiresAuth: true, roles: ['*'] } },
{ path: 'aud-approval', name: 'audit-aud-approval', component: () => import('@/views/audit/aud_approval/ListPage.vue'), meta: { locale: 'menu.platform.audit.aud_approval', requiresAuth: true, roles: ['*'] } }
{ path: 'aud-operation-log', name: 'audit-aud-operation-log', component: () => import('@/views/audit/aud_operation_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.aud_operation_log', requiresAuth: true, menuCode: 'audit' } },
{ path: 'aud-export-log', name: 'audit-aud-export-log', component: () => import('@/views/audit/aud_export_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.aud_export_log', requiresAuth: true, menuCode: 'audit' } },
{ path: 'aud-approval', name: 'audit-aud-approval', component: () => import('@/views/audit/aud_approval/ListPage.vue'), meta: { locale: 'menu.platform.audit.aud_approval', requiresAuth: true, menuCode: 'audit' } }
],
}];
export default routes;

View File

@@ -4,7 +4,7 @@ const routes: AppRouteRecordRaw[] = [{
path: '/content', name: 'content', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.content', requiresAuth: true, icon: 'icon-apps', order: 17 },
children: [
{ path: 'cnt-content', name: 'content-cnt-content', component: () => import('@/views/content/cnt_content/ListPage.vue'), meta: { locale: 'menu.platform.content.cnt_content', requiresAuth: true, roles: ['*'] } }
{ path: 'cnt-content', name: 'content-cnt-content', component: () => import('@/views/content/cnt_content/ListPage.vue'), meta: { locale: 'menu.platform.content.cnt_content', requiresAuth: true, menuCode: 'content' } }
],
}];
export default routes;

View File

@@ -4,7 +4,7 @@ const routes: AppRouteRecordRaw[] = [{
path: '/customer_service', name: 'customer_service', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.customer_service', requiresAuth: true, icon: 'icon-apps', order: 19 },
children: [
{ path: 'cs-ticket', name: 'customer_service-cs-ticket', component: () => import('@/views/customer_service/cs_ticket/ListPage.vue'), meta: { locale: 'menu.platform.customer_service.cs_ticket', requiresAuth: true, roles: ['*'] } }
{ path: 'cs-ticket', name: 'customer_service-cs-ticket', component: () => import('@/views/customer_service/cs_ticket/ListPage.vue'), meta: { locale: 'menu.platform.customer_service.cs_ticket', requiresAuth: true, menuCode: 'customer_service' } }
],
}];
export default routes;

View File

@@ -4,11 +4,11 @@ const routes: AppRouteRecordRaw[] = [{
path: '/delivery', name: 'delivery', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.delivery', requiresAuth: true, icon: 'icon-apps', order: 11 },
children: [
{ path: 'delivery-basic', name: 'delivery-delivery-basic', component: () => import('@/views/delivery/delivery_basic/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_basic', requiresAuth: true, roles: ['*'] } },
{ path: 'delivery-account', name: 'delivery-delivery-account', component: () => import('@/views/delivery/delivery_account/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_account', requiresAuth: true, roles: ['*'] } },
{ path: 'delivery-task', name: 'delivery-delivery-task', component: () => import('@/views/delivery/delivery_task/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_task', requiresAuth: true, roles: ['*'] } },
{ path: 'delivery-track', name: 'delivery-delivery-track', component: () => import('@/views/delivery/delivery_track/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_track', requiresAuth: true, roles: ['*'] } },
{ path: 'delivery-track-point', name: 'delivery-delivery-track-point', component: () => import('@/views/delivery/delivery_track_point/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_track_point', requiresAuth: true, roles: ['*'] } }
{ path: 'delivery-basic', name: 'delivery-delivery-basic', component: () => import('@/views/delivery/delivery_basic/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_basic', requiresAuth: true, menuCode: 'delivery' } },
{ path: 'delivery-account', name: 'delivery-delivery-account', component: () => import('@/views/delivery/delivery_account/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_account', requiresAuth: true, menuCode: 'delivery' } },
{ path: 'delivery-task', name: 'delivery-delivery-task', component: () => import('@/views/delivery/delivery_task/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_task', requiresAuth: true, menuCode: 'delivery' } },
{ path: 'delivery-track', name: 'delivery-delivery-track', component: () => import('@/views/delivery/delivery_track/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_track', requiresAuth: true, menuCode: 'delivery' } },
{ path: 'delivery-track-point', name: 'delivery-delivery-track-point', component: () => import('@/views/delivery/delivery_track_point/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_track_point', requiresAuth: true, menuCode: 'delivery' } }
],
}];
export default routes;

View File

@@ -4,9 +4,9 @@ const routes: AppRouteRecordRaw[] = [{
path: '/device', name: 'device', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.device', requiresAuth: true, icon: 'icon-apps', order: 14 },
children: [
{ path: 'dev-smart-cylinder-valve', name: 'device-dev-smart-cylinder-valve', component: () => import('@/views/device/dev_smart_cylinder_valve/ListPage.vue'), meta: { locale: 'menu.platform.device.dev_smart_cylinder_valve', requiresAuth: true, roles: ['*'] } },
{ path: 'dev-device-binding', name: 'device-dev-device-binding', component: () => import('@/views/device/dev_device_binding/ListPage.vue'), meta: { locale: 'menu.platform.device.dev_device_binding', requiresAuth: true, roles: ['*'] } },
{ path: 'dev-telemetry', name: 'device-dev-telemetry', component: () => import('@/views/device/dev_telemetry/ListPage.vue'), meta: { locale: 'menu.platform.device.dev_telemetry', requiresAuth: true, roles: ['*'] } }
{ path: 'dev-smart-cylinder-valve', name: 'device-dev-smart-cylinder-valve', component: () => import('@/views/device/dev_smart_cylinder_valve/ListPage.vue'), meta: { locale: 'menu.platform.device.dev_smart_cylinder_valve', requiresAuth: true, menuCode: 'device' } },
{ path: 'dev-device-binding', name: 'device-dev-device-binding', component: () => import('@/views/device/dev_device_binding/ListPage.vue'), meta: { locale: 'menu.platform.device.dev_device_binding', requiresAuth: true, menuCode: 'device' } },
{ path: 'dev-telemetry', name: 'device-dev-telemetry', component: () => import('@/views/device/dev_telemetry/ListPage.vue'), meta: { locale: 'menu.platform.device.dev_telemetry', requiresAuth: true, menuCode: 'device' } }
],
}];
export default routes;

View File

@@ -1,13 +1,13 @@
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const routes: AppRouteRecordRaw[] = [{ path: '/ec', name: 'ec', component: DEFAULT_LAYOUT, meta: { locale: 'menu.platform.ec', requiresAuth: true, icon: 'icon-apps', order: 16 }, children: [
{ path: 'ec-category', name: 'ec-ec-category', component: () => import('@/views/ec/ec_category/TreePage.vue'), meta: { locale: 'menu.platform.ec.ec_category', requiresAuth: true, roles: ['*'] } },
{ path: 'ec-product', name: 'ec-ec-product', component: () => import('@/views/ec/ec_product/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_product', requiresAuth: true, roles: ['*'] } },
{ path: 'ec-product-attribute', name: 'ec-ec-product-attribute', component: () => import('@/views/ec/ec_product_attribute/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_product_attribute', requiresAuth: true, roles: ['*'] } },
{ path: 'ec-product-image', name: 'ec-ec-product-image', component: () => import('@/views/ec/ec_product_image/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_product_image', requiresAuth: true, roles: ['*'] } },
{ path: 'ec-cart', name: 'ec-ec-cart', component: () => import('@/views/ec/ec_cart/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_cart', requiresAuth: true, roles: ['*'] } },
{ path: 'ec-order', name: 'ec-ec-order', component: () => import('@/views/ec/ec_order/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_order', requiresAuth: true, roles: ['*'] } },
{ path: 'ec-order-item', name: 'ec-ec-order-item', component: () => import('@/views/ec/ec_order_item/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_order_item', requiresAuth: true, roles: ['*'] } },
{ path: 'ec-review', name: 'ec-ec-review', component: () => import('@/views/ec/ec_review/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_review', requiresAuth: true, roles: ['*'] } }
{ path: 'ec-category', name: 'ec-ec-category', component: () => import('@/views/ec/ec_category/TreePage.vue'), meta: { locale: 'menu.platform.ec.ec_category', requiresAuth: true, menuCode: 'ec' } },
{ path: 'ec-product', name: 'ec-ec-product', component: () => import('@/views/ec/ec_product/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_product', requiresAuth: true, menuCode: 'ec' } },
{ path: 'ec-product-attribute', name: 'ec-ec-product-attribute', component: () => import('@/views/ec/ec_product_attribute/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_product_attribute', requiresAuth: true, menuCode: 'ec' } },
{ path: 'ec-product-image', name: 'ec-ec-product-image', component: () => import('@/views/ec/ec_product_image/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_product_image', requiresAuth: true, menuCode: 'ec' } },
{ path: 'ec-cart', name: 'ec-ec-cart', component: () => import('@/views/ec/ec_cart/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_cart', requiresAuth: true, menuCode: 'ec' } },
{ path: 'ec-order', name: 'ec-ec-order', component: () => import('@/views/ec/ec_order/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_order', requiresAuth: true, menuCode: 'ec' } },
{ path: 'ec-order-item', name: 'ec-ec-order-item', component: () => import('@/views/ec/ec_order_item/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_order_item', requiresAuth: true, menuCode: 'ec' } },
{ path: 'ec-review', name: 'ec-ec-review', component: () => import('@/views/ec/ec_review/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_review', requiresAuth: true, menuCode: 'ec' } }
] }];
export default routes;

View File

@@ -4,9 +4,9 @@ const routes: AppRouteRecordRaw[] = [{
path: '/finance', name: 'finance', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.finance', requiresAuth: true, icon: 'icon-apps', order: 16 },
children: [
{ path: 'fin-payment', name: 'finance-fin-payment', component: () => import('@/views/finance/fin_payment/ListPage.vue'), meta: { locale: 'menu.platform.finance.fin_payment', requiresAuth: true, roles: ['*'] } },
{ path: 'fin-settlement', name: 'finance-fin-settlement', component: () => import('@/views/finance/fin_settlement/ListPage.vue'), meta: { locale: 'menu.platform.finance.fin_settlement', requiresAuth: true, roles: ['*'] } },
{ path: 'fin-reconciliation', name: 'finance-fin-reconciliation', component: () => import('@/views/finance/fin_reconciliation/ListPage.vue'), meta: { locale: 'menu.platform.finance.fin_reconciliation', requiresAuth: true, roles: ['*'] } }
{ path: 'fin-payment', name: 'finance-fin-payment', component: () => import('@/views/finance/fin_payment/ListPage.vue'), meta: { locale: 'menu.platform.finance.fin_payment', requiresAuth: true, menuCode: 'finance' } },
{ path: 'fin-settlement', name: 'finance-fin-settlement', component: () => import('@/views/finance/fin_settlement/ListPage.vue'), meta: { locale: 'menu.platform.finance.fin_settlement', requiresAuth: true, menuCode: 'finance' } },
{ path: 'fin-reconciliation', name: 'finance-fin-reconciliation', component: () => import('@/views/finance/fin_reconciliation/ListPage.vue'), meta: { locale: 'menu.platform.finance.fin_reconciliation', requiresAuth: true, menuCode: 'finance' } }
],
}];
export default routes;

View File

@@ -4,8 +4,8 @@ const routes: AppRouteRecordRaw[] = [{
path: '/gas', name: 'gas', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.gas', requiresAuth: true, icon: 'icon-apps', order: 10 },
children: [
{ path: 'gas-basic', name: 'gas-gas-basic', component: () => import('@/views/gas/gas_basic/ListPage.vue'), meta: { locale: 'menu.platform.gas.gas_basic', requiresAuth: true, roles: ['*'] } },
{ path: 'gas-account', name: 'gas-gas-account', component: () => import('@/views/gas/gas_account/ListPage.vue'), meta: { locale: 'menu.platform.gas.gas_account', requiresAuth: true, roles: ['*'] } }
{ path: 'gas-basic', name: 'gas-gas-basic', component: () => import('@/views/gas/gas_basic/ListPage.vue'), meta: { locale: 'menu.platform.gas.gas_basic', requiresAuth: true, menuCode: 'gas' } },
{ path: 'gas-account', name: 'gas-gas-account', component: () => import('@/views/gas/gas_account/ListPage.vue'), meta: { locale: 'menu.platform.gas.gas_account', requiresAuth: true, menuCode: 'gas' } }
],
}];
export default routes;

View File

@@ -4,7 +4,7 @@ const routes: AppRouteRecordRaw[] = [{
path: '/notification', name: 'notification', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.notification', requiresAuth: true, icon: 'icon-apps', order: 18 },
children: [
{ path: 'ntf-template', name: 'notification-ntf-template', component: () => import('@/views/notification/ntf_template/ListPage.vue'), meta: { locale: 'menu.platform.notification.ntf_template', requiresAuth: true, roles: ['*'] } }
{ path: 'ntf-template', name: 'notification-ntf-template', component: () => import('@/views/notification/ntf_template/ListPage.vue'), meta: { locale: 'menu.platform.notification.ntf_template', requiresAuth: true, menuCode: 'notification' } }
],
}];
export default routes;

View File

@@ -15,11 +15,11 @@ import walletRoutes from './wallet';
import reportRoutes from './report';
import auditRoutes from './audit';
const platformRoutes: AppRouteRecordRaw[] = [
{ path: '/dashboard', name: 'dashboard', component: DEFAULT_LAYOUT, meta: { locale: 'menu.dashboard', requiresAuth: true, icon: 'icon-dashboard', order: 0 }, children: [{ path: 'overview', name: 'DashboardOverview', component: () => import('@/views/dashboard/DashboardPage.vue'), meta: { locale: 'menu.platform.dashboard', requiresAuth: true, roles: ['*'] } }] },
{ path: '/dashboard', name: 'dashboard', component: DEFAULT_LAYOUT, meta: { locale: 'menu.dashboard', requiresAuth: true, icon: 'icon-dashboard', order: 0 }, children: [{ path: 'overview', name: 'DashboardOverview', component: () => import('@/views/dashboard/DashboardPage.vue'), meta: { locale: 'menu.platform.dashboard', requiresAuth: true, menuCode: 'dashboard' } }] },
{ path: '/platform', name: 'platform', component: DEFAULT_LAYOUT, meta: { locale: 'menu.platform.config', requiresAuth: true, icon: 'icon-settings', order: 30 }, children: [
{ path: 'platfrom-account', name: 'platform-platfrom-account', component: () => import('@/views/platform/platfrom_account/ListPage.vue'), meta: { locale: 'menu.platform.platform.platfrom_account', requiresAuth: true, roles: ['*'] } },
{ path: 'platform-role', name: 'platform-platform-role', component: () => import('@/views/platform/platform_role/ListPage.vue'), meta: { locale: 'menu.platform.platform.platform_role', requiresAuth: true, roles: ['*'] } },
{ path: 'platform-menu', name: 'platform-platform-menu', component: () => import('@/views/platform/platform_menu/TreePage.vue'), meta: { locale: 'menu.platform.platform.platform_menu', requiresAuth: true, roles: ['*'] } },
{ path: 'platfrom-account', name: 'platform-platfrom-account', component: () => import('@/views/platform/platfrom_account/ListPage.vue'), meta: { locale: 'menu.platform.platform.platfrom_account', requiresAuth: true, menuCode: 'platform' } },
{ path: 'platform-role', name: 'platform-platform-role', component: () => import('@/views/platform/platform_role/ListPage.vue'), meta: { locale: 'menu.platform.platform.platform_role', requiresAuth: true, menuCode: 'platform' } },
{ path: 'platform-menu', name: 'platform-platform-menu', component: () => import('@/views/platform/platform_menu/TreePage.vue'), meta: { locale: 'menu.platform.platform.platform_menu', requiresAuth: true, menuCode: 'platform' } },
] },
];
export default [...platformRoutes, ...gasRoutes, ...deliveryRoutes, ...staffRoutes, ...userRoutes, ...deviceRoutes, ...safetyRoutes, ...ecRoutes, ...financeRoutes, ...contentRoutes, ...notificationRoutes, ...customerServiceRoutes, ...walletRoutes, ...reportRoutes, ...auditRoutes];

View File

@@ -4,9 +4,9 @@ const routes: AppRouteRecordRaw[] = [{
path: '/report', name: 'report', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.report', requiresAuth: true, icon: 'icon-apps', order: 21 },
children: [
{ path: 'report', name: 'report-report', component: () => import('@/views/report/report/ListPage.vue'), meta: { locale: 'menu.platform.report.report', requiresAuth: true, roles: ['*'] } },
{ path: 'report-item', name: 'report-report-item', component: () => import('@/views/report/report_item/ListPage.vue'), meta: { locale: 'menu.platform.report.report_item', requiresAuth: true, roles: ['*'] } },
{ path: 'report-metric-snapshot', name: 'report-report-metric-snapshot', component: () => import('@/views/report/report_metric_snapshot/ListPage.vue'), meta: { locale: 'menu.platform.report.report_metric_snapshot', requiresAuth: true, roles: ['*'] } }
{ path: 'report', name: 'report-report', component: () => import('@/views/report/report/ListPage.vue'), meta: { locale: 'menu.platform.report.report', requiresAuth: true, menuCode: 'report' } },
{ path: 'report-item', name: 'report-report-item', component: () => import('@/views/report/report_item/ListPage.vue'), meta: { locale: 'menu.platform.report.report_item', requiresAuth: true, menuCode: 'report' } },
{ path: 'report-metric-snapshot', name: 'report-report-metric-snapshot', component: () => import('@/views/report/report_metric_snapshot/ListPage.vue'), meta: { locale: 'menu.platform.report.report_metric_snapshot', requiresAuth: true, menuCode: 'report' } }
],
}];
export default routes;

View File

@@ -1,8 +1,8 @@
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const routes: AppRouteRecordRaw[] = [{ path: '/safety', name: 'safety', component: DEFAULT_LAYOUT, meta: { locale: 'menu.platform.safety', requiresAuth: true, icon: 'icon-apps', order: 15 }, children: [
{ path: 'saf-rule', name: 'safety-saf-rule', component: () => import('@/views/safety/saf_rule/ListPage.vue'), meta: { locale: 'menu.platform.safety.saf_rule', requiresAuth: true, roles: ['*'] } },
{ path: 'saf-event', name: 'safety-saf-event', component: () => import('@/views/safety/saf_event/ListPage.vue'), meta: { locale: 'menu.platform.safety.saf_event', requiresAuth: true, roles: ['*'] } },
{ path: 'saf-inspection', name: 'safety-saf-inspection', component: () => import('@/views/safety/saf_inspection/ListPage.vue'), meta: { locale: 'menu.platform.safety.saf_inspection', requiresAuth: true, roles: ['*'] } }
{ path: 'saf-rule', name: 'safety-saf-rule', component: () => import('@/views/safety/saf_rule/ListPage.vue'), meta: { locale: 'menu.platform.safety.saf_rule', requiresAuth: true, menuCode: 'safety' } },
{ path: 'saf-event', name: 'safety-saf-event', component: () => import('@/views/safety/saf_event/ListPage.vue'), meta: { locale: 'menu.platform.safety.saf_event', requiresAuth: true, menuCode: 'safety' } },
{ path: 'saf-inspection', name: 'safety-saf-inspection', component: () => import('@/views/safety/saf_inspection/ListPage.vue'), meta: { locale: 'menu.platform.safety.saf_inspection', requiresAuth: true, menuCode: 'safety' } }
] }];
export default routes;

View File

@@ -4,8 +4,8 @@ const routes: AppRouteRecordRaw[] = [{
path: '/staff', name: 'staff', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.staff', requiresAuth: true, icon: 'icon-apps', order: 12 },
children: [
{ path: 'account', name: 'staff-staff-account', component: () => import('@/views/staff/staff_account/ListPage.vue'), meta: { locale: 'menu.platform.staff.staff_account', requiresAuth: true, roles: ['*'] } },
{ path: 'credential', name: 'staff-staff-credential', component: () => import('@/views/staff/staff_credential/ListPage.vue'), meta: { locale: 'menu.platform.staff.staff_credential', requiresAuth: true, roles: ['*'] } }
{ path: 'account', name: 'staff-staff-account', component: () => import('@/views/staff/staff_account/ListPage.vue'), meta: { locale: 'menu.platform.staff.staff_account', requiresAuth: true, menuCode: 'staff' } },
{ path: 'credential', name: 'staff-staff-credential', component: () => import('@/views/staff/staff_credential/ListPage.vue'), meta: { locale: 'menu.platform.staff.staff_credential', requiresAuth: true, menuCode: 'staff' } }
],
}];
export default routes;

View File

@@ -4,9 +4,9 @@ const routes: AppRouteRecordRaw[] = [{
path: '/user', name: 'user', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.user', requiresAuth: true, icon: 'icon-apps', order: 13 },
children: [
{ path: 'account', name: 'user-user-account', component: () => import('@/views/user/user_account/ListPage.vue'), meta: { locale: 'menu.platform.user.user_account', requiresAuth: true, roles: ['*'] } },
{ path: 'address', name: 'user-user-address', component: () => import('@/views/user/user_address/ListPage.vue'), meta: { locale: 'menu.platform.user.user_address', requiresAuth: true, roles: ['*'] } },
{ path: 'service-relation', name: 'user-user-service-relation', component: () => import('@/views/user/user_service_relation/ListPage.vue'), meta: { locale: 'menu.platform.user.user_service_relation', requiresAuth: true, roles: ['*'] } }
{ path: 'account', name: 'user-user-account', component: () => import('@/views/user/user_account/ListPage.vue'), meta: { locale: 'menu.platform.user.user_account', requiresAuth: true, menuCode: 'user' } },
{ path: 'address', name: 'user-user-address', component: () => import('@/views/user/user_address/ListPage.vue'), meta: { locale: 'menu.platform.user.user_address', requiresAuth: true, menuCode: 'user' } },
{ path: 'service-relation', name: 'user-user-service-relation', component: () => import('@/views/user/user_service_relation/ListPage.vue'), meta: { locale: 'menu.platform.user.user_service_relation', requiresAuth: true, menuCode: 'user' } }
],
}];
export default routes;

View File

@@ -4,10 +4,10 @@ const routes: AppRouteRecordRaw[] = [{
path: '/wallet', name: 'wallet', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.wallet', requiresAuth: true, icon: 'icon-apps', order: 20 },
children: [
{ path: 'wallet', name: 'wallet-wallet', component: () => import('@/views/wallet/wallet/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet', requiresAuth: true, roles: ['*'] } },
{ path: 'wallet-ledger', name: 'wallet-wallet-ledger', component: () => import('@/views/wallet/wallet_ledger/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet_ledger', requiresAuth: true, roles: ['*'] } },
{ path: 'wallet-recharge', name: 'wallet-wallet-recharge', component: () => import('@/views/wallet/wallet_recharge/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet_recharge', requiresAuth: true, roles: ['*'] } },
{ path: 'wallet-withdrawal', name: 'wallet-wallet-withdrawal', component: () => import('@/views/wallet/wallet_withdrawal/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet_withdrawal', requiresAuth: true, roles: ['*'] } }
{ path: 'wallet', name: 'wallet-wallet', component: () => import('@/views/wallet/wallet/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet', requiresAuth: true, menuCode: 'wallet' } },
{ path: 'wallet-ledger', name: 'wallet-wallet-ledger', component: () => import('@/views/wallet/wallet_ledger/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet_ledger', requiresAuth: true, menuCode: 'wallet' } },
{ path: 'wallet-recharge', name: 'wallet-wallet-recharge', component: () => import('@/views/wallet/wallet_recharge/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet_recharge', requiresAuth: true, menuCode: 'wallet' } },
{ path: 'wallet-withdrawal', name: 'wallet-wallet-withdrawal', component: () => import('@/views/wallet/wallet_withdrawal/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet_withdrawal', requiresAuth: true, menuCode: 'wallet' } }
],
}];
export default routes;

View File

@@ -3,6 +3,7 @@ import 'vue-router';
declare module 'vue-router' {
interface RouteMeta {
roles?: string[]; // Controls roles that have access to the page
menuCode?: string; // Server-assigned menu domain required by this route
requiresAuth: boolean; // Whether login is required to access the current page (every route must declare)
icon?: string; // The icon show in the side menu
locale?: string; // The locale name show in side menu and breadcrumb

View File

@@ -24,6 +24,7 @@ const useUserStore = defineStore('user', {
accountId: undefined,
certification: undefined,
role: '',
menuCodes: [],
}),
getters: {
@@ -55,7 +56,13 @@ const useUserStore = defineStore('user', {
// Get user's information
async info() {
const profile = await authApi.profile();
this.setInfo({ name: profile.display_name || profile.username, avatar: profile.avatar, accountId: profile.identity, role: '*' });
this.setInfo({
name: profile.display_name || profile.username,
avatar: profile.avatar,
accountId: profile.identity,
role: profile.role_code,
menuCodes: profile.menu_codes,
});
},
// Login

View File

@@ -1,4 +1,4 @@
export type RoleType = '' | '*' | 'admin' | 'user';
export type RoleType = string;
export interface UserState {
name?: string;
avatar?: string;
@@ -16,4 +16,5 @@ export interface UserState {
accountId?: string;
certification?: number;
role: RoleType;
menuCodes: string[];
}

View File

@@ -39,6 +39,9 @@
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
<a-textarea v-else-if="field.type === 'json' || field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
<a-select v-else-if="field.type === 'role-code'" v-model="form[field.key]">
<a-option v-for="role in roleOptions" :key="role.role_code" :value="role.role_code">{{ role.name }}</a-option>
</a-select>
<a-input v-else v-model="form[field.key]" :placeholder="`请输入${field.label}`" />
</a-form-item>
</a-form>
@@ -59,6 +62,9 @@
<a-input-number v-if="field.type === 'number'" v-model="actionForm[field.key]" />
<a-switch v-else-if="field.type === 'boolean'" v-model="actionForm[field.key]" />
<a-textarea v-else-if="field.type === 'json' || field.type === 'textarea'" v-model="actionForm[field.key]" />
<a-select v-else-if="field.type === 'menu-identities'" v-model="actionForm[field.key]" multiple allow-search>
<a-option v-for="menu in menuOptions" :key="menu.identity" :value="menu.identity">{{ menu.name }}</a-option>
</a-select>
<a-input v-else v-model="actionForm[field.key]" />
</a-form-item>
</a-form>
@@ -69,6 +75,7 @@
import { Message, Modal } from '@arco-design/web-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import { platformApi, type PlatformMenu, type PlatformRole } from '@/api/platform';
import {
buildResourcePayload,
isMissingField,
@@ -92,6 +99,8 @@ const detail = ref<Row>({});
const actionVisible = ref(false);
const activeAction = ref<DetailAction>();
const actionForm = reactive<Record<string, any>>({});
const menuOptions = ref<PlatformMenu[]>([]);
const roleOptions = ref<PlatformRole[]>([]);
const canCreate = computed(() => props.definition.mode !== 'readonly');
const canEdit = computed(() => props.definition.mode === 'writable');
const canArchive = computed(() => props.definition.mode === 'writable');
@@ -166,9 +175,23 @@ async function openDetail(row: Row) {
}
}
function openDetailAction(action: DetailAction) {
async function openDetailAction(action: DetailAction) {
activeAction.value = action;
for (const field of action.fields) actionForm[field.key] = undefined;
if (action.fields.some((field) => field.type === 'menu-identities')) {
try {
const identity = String(detail.value.identity);
const [menus, assigned] = await Promise.all([
platformApi.listMenu(),
platformApi.listRoleMenuIdentities(identity),
]);
menuOptions.value = menus.list;
actionForm.menu_identities = assigned.menu_identities;
} catch (error) {
Message.error((error as Error).message);
return;
}
}
actionVisible.value = true;
}
@@ -188,10 +211,17 @@ async function submitDetailAction() {
...action.payload,
...buildResourcePayload(action.fields, actionForm),
};
await resourceApi.create(
action.resource.replace(':identity', String(detail.value.identity)),
payload,
);
if (action.fields.some((field) => field.type === 'menu-identities')) {
await platformApi.replaceRoleMenus(
String(detail.value.identity),
payload.menu_identities as string[],
);
} else {
await resourceApi.create(
action.resource.replace(':identity', String(detail.value.identity)),
payload,
);
}
Message.success('操作成功');
actionVisible.value = false;
await openDetail(detail.value);
@@ -256,7 +286,15 @@ async function changePage(next: number) {
await load();
}
onMounted(load);
onMounted(async () => {
await load();
if (props.definition.fields.some((field) => field.type === 'role-code')) {
const result = await platformApi.listRole();
roleOptions.value = result.list.filter(
(role) => !role.is_system && role.status === 'enabled',
);
}
});
</script>
<style scoped lang="less">