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,9 @@
import { createPinia } from 'pinia';
import useAppStore from './modules/app';
import useTabBarStore from './modules/tab-bar';
import useUserStore from './modules/user';
const pinia = createPinia();
export { useAppStore, useTabBarStore, useUserStore };
export default pinia;

View File

@@ -0,0 +1,129 @@
import { Notification } from '@arco-design/web-vue';
import { defineStore } from 'pinia';
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
import { platformApi, type PlatformMenu } from '@/api/platform';
import defaultSettings from '@/config/settings.json';
import appClientMenus from '@/router/app-menus';
import type { AppState } from './types';
function indexClientMenus(
routes: RouteRecordRaw[],
index = new Map<string, RouteRecordRaw>(),
) {
for (const route of routes) {
const menuCode = route.meta?.menuCode;
if (menuCode && route.meta?.hideInMenu !== true && !index.has(menuCode)) {
index.set(menuCode, route);
}
if (route.children) indexClientMenus(route.children, index);
}
return index;
}
function buildServerMenuTree(menus: PlatformMenu[]): RouteRecordRaw[] {
const clientMenus = indexClientMenus(
appClientMenus as unknown as RouteRecordRaw[],
);
const nodes = new Map<string, RouteRecordRaw>();
for (const menu of [...menus].sort((a, b) => a.sort_no - b.sort_no)) {
const clientRoute = clientMenus.get(menu.identity);
if (!clientRoute) continue;
nodes.set(menu.identity, {
...clientRoute,
path: menu.path,
meta: {
...clientRoute.meta,
title: menu.name,
icon: menu.icon || clientRoute.meta?.icon,
order: menu.sort_no,
menuCode: menu.identity,
requiresAuth: clientRoute.meta?.requiresAuth ?? true,
},
children: [],
});
}
const roots: RouteRecordRaw[] = [];
for (const menu of menus) {
const node = nodes.get(menu.identity);
if (!node) continue;
if (menu.parent_identity) {
const parent = nodes.get(menu.parent_identity);
if (parent) parent.children?.push(node);
} else {
roots.push(node);
}
}
return roots;
}
const useAppStore = defineStore('app', {
state: (): AppState => ({ ...defaultSettings }),
getters: {
appCurrentSetting(state: AppState): AppState {
return { ...state };
},
appDevice(state: AppState) {
return state.device;
},
appAsyncMenus(state: AppState): RouteRecordNormalized[] {
return state.serverMenu as unknown as RouteRecordNormalized[];
},
},
actions: {
// Update app settings
updateSettings(partial: Partial<AppState>) {
// @ts-expect-error-next-line
this.$patch(partial);
},
// Change theme color
toggleTheme(dark: boolean) {
if (dark) {
this.theme = 'dark';
document.body.setAttribute('arco-theme', 'dark');
} else {
this.theme = 'light';
document.body.removeAttribute('arco-theme');
}
},
toggleDevice(device: string) {
this.device = device;
},
toggleMenu(value: boolean) {
this.hideMenu = value;
},
async fetchServerMenuConfig() {
try {
Notification.info({
id: 'menuNotice', // Keep the instance id the same
content: 'loading',
closable: true,
});
const response = await platformApi.listMenu();
this.serverMenu = buildServerMenuTree(
response.list,
) as unknown as RouteRecordNormalized[];
Notification.success({
id: 'menuNotice',
content: 'success',
closable: true,
});
} catch {
Notification.error({
id: 'menuNotice',
content: 'error',
closable: true,
});
}
},
clearServerMenu() {
this.serverMenu = [];
},
},
});
export default useAppStore;

View File

@@ -0,0 +1,19 @@
import type { RouteRecordNormalized } from 'vue-router';
export interface AppState {
theme: string;
colorWeak: boolean;
navbar: boolean;
menu: boolean;
topMenu: boolean;
hideMenu: boolean;
menuCollapse: boolean;
footer: boolean;
themeColor: string;
menuWidth: number;
device: string;
tabBar: boolean;
menuFromServer: boolean;
serverMenu: RouteRecordNormalized[];
[key: string]: unknown;
}

View File

@@ -0,0 +1,75 @@
import { defineStore } from 'pinia';
import type { RouteLocationNormalized } from 'vue-router';
import {
DEFAULT_ROUTE,
DEFAULT_ROUTE_NAME,
REDIRECT_ROUTE_NAME,
} from '@/router/constants';
import { isString } from '@/utils/is';
import type { TabBarState, TagProps } from './types';
const formatTag = (route: RouteLocationNormalized): TagProps => {
const { name, meta, fullPath, query } = route;
return {
title: meta.locale || '',
name: String(name),
fullPath,
query,
ignoreCache: meta.ignoreCache,
};
};
const BAN_LIST = [REDIRECT_ROUTE_NAME];
const useAppStore = defineStore('tabBar', {
state: (): TabBarState => ({
cacheTabList: new Set([DEFAULT_ROUTE_NAME]),
tagList: [DEFAULT_ROUTE],
}),
getters: {
getTabList(): TagProps[] {
return this.tagList;
},
getCacheList(): string[] {
return Array.from(this.cacheTabList);
},
},
actions: {
updateTabList(route: RouteLocationNormalized) {
if (BAN_LIST.includes(route.name as string)) return;
this.tagList.push(formatTag(route));
if (!route.meta.ignoreCache) {
this.cacheTabList.add(route.name as string);
}
},
deleteTag(idx: number, tag: TagProps) {
this.tagList.splice(idx, 1);
this.cacheTabList.delete(tag.name);
},
addCache(name: string) {
if (isString(name) && name !== '') this.cacheTabList.add(name);
},
deleteCache(tag: TagProps) {
this.cacheTabList.delete(tag.name);
},
freshTabList(tags: TagProps[]) {
this.tagList = tags;
this.cacheTabList.clear();
// 要先判断ignoreCache
for (const name of this.tagList
.filter((el) => !el.ignoreCache)
.map((el) => el.name)) {
this.cacheTabList.add(name);
}
},
resetTabList() {
this.tagList = [DEFAULT_ROUTE];
this.cacheTabList.clear();
this.cacheTabList.add(DEFAULT_ROUTE_NAME);
},
},
});
export default useAppStore;

View File

@@ -0,0 +1,12 @@
export interface TagProps {
title: string;
name: string;
fullPath: string;
query?: any;
ignoreCache?: boolean;
}
export interface TabBarState {
tagList: TagProps[];
cacheTabList: Set<string>;
}

View File

@@ -0,0 +1,100 @@
import { defineStore } from 'pinia';
import { authApi, type LoginData } from '@/api/auth';
import { resolveAvatarUrl } from '@/constants/avatar';
import { clearToken, setToken } from '@/utils/auth';
import { removeRouteListener } from '@/utils/route-listener';
import useAppStore from '../app';
import type { UserState } from './types';
const useUserStore = defineStore('user', {
state: (): UserState => ({
name: undefined,
avatar: undefined,
job: undefined,
organization: undefined,
location: undefined,
email: undefined,
introduction: undefined,
personalWebsite: undefined,
jobName: undefined,
organizationName: undefined,
locationName: undefined,
phone: undefined,
registrationDate: undefined,
accountId: undefined,
certification: undefined,
role: '',
menuCodes: [],
}),
getters: {
userInfo(state: UserState): UserState {
return { ...state };
},
},
actions: {
switchRoles() {
return new Promise((resolve) => {
this.role = this.role === 'user' ? 'admin' : 'user';
resolve(this.role);
});
},
// Set user's information
setInfo(partial: Partial<UserState>) {
if (partial.avatar !== undefined) {
partial.avatar = resolveAvatarUrl(partial.avatar);
}
this.$patch(partial);
},
// Reset user's information
resetInfo() {
this.$reset();
},
// Get user's information
async info() {
const profile = await authApi.profile();
const appStore = useAppStore();
this.setInfo({
name: profile.display_name || profile.username,
avatar: profile.avatar,
accountId: profile.identity,
role: profile.role_code,
menuCodes: profile.menu_codes,
});
if (appStore.menuFromServer) {
await appStore.fetchServerMenuConfig();
}
},
// Login
async login(loginForm: LoginData) {
try {
const res = await authApi.login(loginForm);
setToken(res.access_token);
} catch (err) {
clearToken();
throw err;
}
},
logoutCallBack() {
const appStore = useAppStore();
this.resetInfo();
clearToken();
removeRouteListener();
appStore.clearServerMenu();
},
// Logout
async logout() {
try {
await Promise.resolve();
} finally {
this.logoutCallBack();
}
},
},
});
export default useUserStore;

View File

@@ -0,0 +1,20 @@
export type RoleType = string;
export interface UserState {
name?: string;
avatar?: string;
job?: string;
organization?: string;
location?: string;
email?: string;
introduction?: string;
personalWebsite?: string;
jobName?: string;
organizationName?: string;
locationName?: string;
phone?: string;
registrationDate?: string;
accountId?: string;
certification?: number;
role: RoleType;
menuCodes: string[];
}