feat: implement gas and delivery admin systems

This commit is contained in:
david
2026-07-30 14:16:58 +08:00
parent 1093385f95
commit f5ecc0d973
252 changed files with 49385 additions and 363 deletions

View File

@@ -0,0 +1,16 @@
import { appExternalRoutes, appRoutes } from '../routes';
const mixinRoutes = [...appRoutes, ...appExternalRoutes];
const appClientMenus = mixinRoutes.map((el) => {
const { name, path, meta, redirect, children } = el;
return {
name,
path,
meta,
redirect,
children,
};
});
export default appClientMenus;

View File

@@ -0,0 +1,18 @@
export const WHITE_LIST = [
{ name: 'notFound', children: [] },
{ name: 'login', children: [] },
];
export const NOT_FOUND = {
name: 'notFound',
};
export const REDIRECT_ROUTE_NAME = 'Redirect';
export const DEFAULT_ROUTE_NAME = 'Workplace';
export const DEFAULT_ROUTE = {
title: 'menu.dashboard.workplace',
name: DEFAULT_ROUTE_NAME,
fullPath: '/dashboard/workplace',
};

View File

@@ -0,0 +1,17 @@
import type { Router } from 'vue-router';
import { setRouteEmitter } from '@/utils/route-listener';
import setupPermissionGuard from './permission';
import setupUserLoginInfoGuard from './userLoginInfo';
function setupPageGuard(router: Router) {
router.beforeEach(async (to) => {
// emit route change
setRouteEmitter(to);
});
}
export default function createRouteGuard(router: Router) {
setupPageGuard(router);
setupUserLoginInfoGuard(router);
setupPermissionGuard(router);
}

View File

@@ -0,0 +1,53 @@
import NProgress from 'nprogress'; // progress bar
import type { RouteRecordNormalized, Router } from 'vue-router';
import usePermission from '@/hooks/permission';
import { useAppStore, useUserStore } from '@/store';
import { NOT_FOUND, WHITE_LIST } from '../constants';
import { appRoutes } from '../routes';
export default function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const appStore = useAppStore();
const userStore = useUserStore();
const Permission = usePermission();
const permissionsAllow = Permission.accessRouter(to);
if (appStore.menuFromServer) {
// 针对来自服务端的菜单配置进行处理
// Handle routing configuration from the server
// 根据需要自行完善来源于服务端的菜单配置的permission逻辑
// Refine the permission logic from the server's menu configuration as needed
if (
!appStore.appAsyncMenus.length &&
!WHITE_LIST.find((el) => el.name === to.name)
) {
await appStore.fetchServerMenuConfig();
}
const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST];
let exist = false;
while (serverMenuConfig.length && !exist) {
const element = serverMenuConfig.shift();
if (element?.name === to.name) exist = true;
if (element?.children) {
serverMenuConfig.push(
...(element.children as unknown as RouteRecordNormalized[]),
);
}
}
if (exist && permissionsAllow) {
next();
} else next(NOT_FOUND);
} else if (permissionsAllow) {
next();
} else {
const destination =
Permission.findFirstPermissionRoute(appRoutes, userStore.role) ||
NOT_FOUND;
next(destination);
}
NProgress.done();
});
}

View File

@@ -0,0 +1,44 @@
import NProgress from 'nprogress'; // progress bar
import type { LocationQueryRaw, Router } from 'vue-router';
import { useUserStore } from '@/store';
import { isLogin } from '@/utils/auth';
export default function setupUserLoginInfoGuard(router: Router) {
router.beforeEach(async (to, _from, next) => {
NProgress.start();
const userStore = useUserStore();
// 登录页不需要校验旧令牌,避免后端未启动或令牌过期时重复请求用户资料。
if (to.name === 'login') {
next();
return;
}
if (isLogin()) {
if (userStore.role) {
next();
} else {
try {
await userStore.info();
next();
} catch {
await userStore.logout();
next({
name: 'login',
query: {
redirect: to.name,
...to.query,
} as LocationQueryRaw,
});
}
}
} else {
next({
name: 'login',
query: {
redirect: to.name,
...to.query,
} as LocationQueryRaw,
});
}
});
}

View File

@@ -0,0 +1,37 @@
import NProgress from 'nprogress'; // progress bar
import { createRouter, createWebHistory } from 'vue-router';
import 'nprogress/nprogress.css';
import createRouteGuard from './guard';
import { appRoutes } from './routes';
import { NOT_FOUND_ROUTE, REDIRECT_MAIN } from './routes/base';
NProgress.configure({ showSpinner: false }); // NProgress Configuration
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
redirect: 'login',
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
meta: {
requiresAuth: false,
},
},
...appRoutes,
REDIRECT_MAIN,
NOT_FOUND_ROUTE,
],
scrollBehavior() {
return { top: 0 };
},
});
createRouteGuard(router);
export default router;

View File

@@ -0,0 +1,31 @@
import type { RouteRecordRaw } from 'vue-router';
import { REDIRECT_ROUTE_NAME } from '@/router/constants';
export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue');
export const REDIRECT_MAIN: RouteRecordRaw = {
path: '/redirect',
name: 'redirectWrapper',
component: DEFAULT_LAYOUT,
meta: {
requiresAuth: true,
hideInMenu: true,
},
children: [
{
path: '/redirect/:path',
name: REDIRECT_ROUTE_NAME,
component: () => import('@/views/redirect/index.vue'),
meta: {
requiresAuth: true,
hideInMenu: true,
},
},
],
};
export const NOT_FOUND_ROUTE: RouteRecordRaw = {
path: '/:pathMatch(.*)*',
name: 'notFound',
component: () => import('@/views/not-found/index.vue'),
};

View File

@@ -0,0 +1,10 @@
export default {
path: 'https://arco.design',
name: 'arcoWebsite',
meta: {
locale: 'menu.arcoWebsite',
icon: 'icon-link',
requiresAuth: true,
order: 8,
},
};

View File

@@ -0,0 +1,10 @@
export default {
path: 'https://arco.design/vue/docs/pro/faq',
name: 'faq',
meta: {
locale: 'menu.faq',
icon: 'icon-question-circle',
requiresAuth: true,
order: 9,
},
};

View File

@@ -0,0 +1,8 @@
import type { RouteRecordNormalized } from 'vue-router';
import platformRoutes from './modules/platform';
/** 配送点后台仅注册已实现的业务路由,菜单能力由后端返回。 */
export const appRoutes: RouteRecordNormalized[] = platformRoutes as unknown as RouteRecordNormalized[];
/** 本期没有外部菜单。 */
export const appExternalRoutes: RouteRecordNormalized[] = [];

View File

@@ -0,0 +1,55 @@
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const resourcePage = () => import('@/views/shared/ResourcePage.vue');
const child = (domain: string, path: string, title: string, resource: string, menuCode: string, meta: Record<string, unknown> = {}): AppRouteRecordRaw => ({
path, name: `${domain}-${path}`, component: resourcePage,
meta: { title, resource, requiresAuth: true, menuCode, ...meta },
});
const group = (path: string, name: string, title: string, icon: string, order: number, children: AppRouteRecordRaw[]): AppRouteRecordRaw => ({
path: `/${path}`, name, component: DEFAULT_LAYOUT, redirect: `/${path}/${children[0].path}`,
meta: { title, requiresAuth: true, icon, order, menuCode: name }, children,
});
const routes: AppRouteRecordRaw[] = [
{ path: '/dashboard', name: 'dashboard', component: DEFAULT_LAYOUT, redirect: '/dashboard/overview', meta: { title: '数据概述', requiresAuth: true, icon: 'icon-dashboard', order: 10, menuCode: 'dashboard' }, children: [
{ path: 'overview', name: 'dashboard-overview', component: () => import('@/views/dashboard/DashboardPage.vue'), meta: { title: '运营概览', requiresAuth: true, menuCode: 'dashboard_overview' } },
{ path: 'reports', name: 'dashboard-reports', component: () => import('@/views/dashboard/ReportPage.vue'), meta: { title: '订单报表', requiresAuth: true, menuCode: 'dashboard_reports' } },
] },
group('profile', 'profile', '配送点资料', 'icon-storage', 20, [
child('profile', 'basic', '本点资料', '/delivery_profile', 'delivery_profile'),
]),
group('staff', 'staff', '配送人员管理', 'icon-user-group', 30, [
child('staff', 'add', '新增配送人员', '/staff_account', 'staff_add', { createMode: true }),
child('staff', 'delivery', '配送人员列表', '/staff_account', 'staff_delivery'),
child('staff', 'credentials', '人员资质', '/staff_credential', 'staff', { hideInMenu: true, activeMenu: 'staff-delivery' }),
]),
group('user', 'user', '用户管理', 'icon-user', 40, [
child('user', 'accounts', '用户账户', '/user_account', 'user_account'),
child('user', 'addresses', '用户地址', '/user_address', 'user_account', { hideInMenu: true, activeMenu: 'user-accounts' }),
]),
group('contract', 'contract', '合同管理', 'icon-file', 50, [
child('contract', 'contracts', '配送合同', '/gasorder_contract', 'gasorder_contract'),
child('contract', 'products', '合同气瓶', '/gasorder_contract_product', 'gasorder_contract', { hideInMenu: true, activeMenu: 'contract-contracts' }),
child('contract', 'revisions', '合同修订记录', '/gasorder_contract_revision', 'gasorder_contract', { hideInMenu: true, activeMenu: 'contract-contracts' }),
child('contract', 'candidates', '合同可选气瓶', '/product_info', 'gasorder_contract', { hideInMenu: true, activeMenu: 'contract-contracts' }),
]),
group('gasorder', 'gasorder', '配送订单', 'icon-list', 60, [
child('gasorder', 'create', '创建订单', '/gasorder_basic', 'gasorder_create', { createMode: true }),
child('gasorder', 'orders', '订单列表', '/gasorder_basic', 'gasorder_basic'),
]),
group('finance', 'finance', '财务管理', 'icon-bar-chart', 70, [
child('finance', 'wallet', '钱包', '/wallet_basic', 'wallet_basic'),
child('finance', 'banks', '银行卡', '/wallet_bank', 'wallet_bank'),
child('finance', 'payments', '支付记录', '/wallet_payment', 'wallet_payment'),
child('finance', 'records', '钱包流水', '/wallet_record', 'wallet_record'),
child('finance', 'refunds', '退款记录', '/wallet_refund', 'wallet_refund'),
child('finance', 'recharge', '钱包充值', '/wallet_recharge', 'wallet_recharge'),
child('finance', 'withdrawals', '提现申请', '/wallet_apply_cash', 'wallet_apply_cash'),
child('finance', 'settlements', '结算结果', '/fin_settlement', 'fin_settlement'),
]),
{ path: '/invitation', name: 'invitation', component: DEFAULT_LAYOUT, redirect: '/invitation/qrcode', meta: { title: '邀请注册', requiresAuth: true, icon: 'icon-qrcode', order: 80, menuCode: 'invitation' }, children: [
{ path: 'qrcode', name: 'invitation-qrcode', component: () => import('@/views/invitation/QrcodePage.vue'), meta: { title: '邀请二维码', requiresAuth: true, menuCode: 'invitation_qrcode' } },
] },
];
export default routes;

View File

@@ -0,0 +1,20 @@
import type { defineComponent } from 'vue';
import type { NavigationGuard, RouteMeta } from 'vue-router';
export type Component<T = any> =
| ReturnType<typeof defineComponent>
| (() => Promise<typeof import('*.vue')>)
| (() => Promise<T>);
export interface AppRouteRecordRaw {
path: string;
name?: string | symbol;
meta?: RouteMeta;
redirect?: string;
component: Component | string;
children?: AppRouteRecordRaw[];
alias?: string | string[];
props?: Record<string, any>;
beforeEnter?: NavigationGuard | NavigationGuard[];
fullPath?: string;
}

View File

@@ -0,0 +1,19 @@
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
staffType?: 'installer' | 'delivery' | 'operations';
createMode?: boolean;
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
hideInMenu?: boolean; // If true, it is not displayed in the side menu
hideChildrenInMenu?: boolean; // if set true, the children are not displayed in the side menu
activeMenu?: string; // if set name, the menu will be highlighted according to the name you set
order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is
noAffix?: boolean; // if set true, the tag will not affix in the tab-bar
ignoreCache?: boolean; // if set true, the page will not be cached
}
}