Подготовить HY2XS к production-сборке

This commit is contained in:
2026-04-25 23:13:12 +05:00
commit 84a4e94567
277 changed files with 26513 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
import type { App } from "vue";
import { createPinia } from "pinia";
const store = createPinia();
// 全局注册 store
export function setupStore(app: App<Element>) {
app.use(store);
}
export { store };
@@ -0,0 +1,91 @@
import { defineStore } from "pinia";
import { getAccountInfoApi, loginApi } from "@/api/account";
import { resetRouter } from "@/router";
import { store } from "@/store";
import { AccountInfo, AccountLoginDto } from "@/api/account/types";
import { useStorage } from "@vueuse/core";
export const useAccountStore = defineStore("account", () => {
// state
const token = useStorage("accessToken", "");
const id = ref(0);
const username = ref("");
const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
/**
* 登录
*
* @returns
*/
function login(accountLoginDto: AccountLoginDto) {
return new Promise<void>((resolve, reject) => {
loginApi(accountLoginDto)
.then((response) => {
const { tokenType, accessToken } = response.data;
token.value = tokenType + " " + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
resolve();
})
.catch((error) => {
reject(error);
});
});
}
// 查询当前
function getAccountInfo() {
return new Promise<AccountInfo>((resolve, reject) => {
getAccountInfoApi()
.then(({ data }) => {
if (!data) {
return reject("Verification failed, please Login again.");
}
if (!data.roles || data.roles.length <= 0) {
reject("getAccountInfoApi: roles must be a non-null array!");
}
id.value = data.id;
username.value = data.username;
roles.value = data.roles;
resolve(data);
})
.catch((error) => {
reject(error);
});
});
}
// 注销
function logout() {
return new Promise<void>((resolve, reject) => {
resetRouter();
resetToken();
resolve();
});
}
// 重置
function resetToken() {
token.value = "";
id.value = 0;
username.value = "";
roles.value = [];
}
return {
token,
id,
username,
roles,
login,
getAccountInfo,
logout,
resetToken,
};
});
// 非setup
export function useAccountStoreHook() {
return useAccountStore(store);
}
+80
View File
@@ -0,0 +1,80 @@
import { defineStore } from "pinia";
import { useStorage } from "@vueuse/core";
import defaultSettings from "@/settings";
import en from "element-plus/es/locale/lang/en";
import ru from "element-plus/es/locale/lang/ru";
// setup
export const useAppStore = defineStore("app", () => {
// state
const device = useStorage("device", "desktop");
const size = useStorage<any>("size", defaultSettings.size);
const language = useStorage("language", defaultSettings.language);
const sidebarStatus = useStorage("sidebarStatus", "closed");
const sidebar = reactive({
opened: sidebarStatus.value !== "closed",
withoutAnimation: false,
});
/**
* 根据语言标识读取对应的语言包
*/
const locale = computed(() => {
return language?.value == "en" ? en : ru;
});
// actions
function toggleSidebar(withoutAnimation: boolean) {
sidebar.opened = !sidebar.opened;
sidebar.withoutAnimation = withoutAnimation;
if (sidebar.opened) {
sidebarStatus.value = "opened";
} else {
sidebarStatus.value = "closed";
}
}
function closeSideBar(withoutAnimation: boolean) {
sidebar.opened = false;
sidebar.withoutAnimation = withoutAnimation;
sidebarStatus.value = "closed";
}
function openSideBar(withoutAnimation: boolean) {
sidebar.opened = true;
sidebar.withoutAnimation = withoutAnimation;
sidebarStatus.value = "opened";
}
function toggleDevice(val: string) {
device.value = val;
}
function changeSize(val: string) {
size.value = val;
}
/**
* 切换语言
*
* @param val
*/
function changeLanguage(val: string) {
language.value = val;
}
return {
device,
sidebar,
language,
locale,
size,
toggleDevice,
changeSize,
changeLanguage,
toggleSidebar,
closeSideBar,
openSideBar,
};
});
@@ -0,0 +1,97 @@
import { RouteRecordRaw } from "vue-router";
import { defineStore } from "pinia";
import { asyncRoutes, constantRoutes } from "@/router";
import { store } from "@/store";
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layout/index.vue");
/**
* Use meta.role to determine if the current user has permission
*
* @param roles 用户角色集合
* @param route 路由
* @returns
*/
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
if (route.meta && route.meta.roles) {
// 角色【超级管理员】拥有所有权限,忽略校验
if (roles.includes("admin")) {
return true;
}
return roles.some((role) => {
if (route.meta?.roles !== undefined) {
return (route.meta.roles as string[]).includes(role);
}
});
}
return false;
};
/**
* 递归过滤有权限的异步(动态)路由
*
* @param routes 接口返回的异步(动态)路由
* @param roles 用户角色集合
* @returns 返回用户有权限的异步(动态)路由
*/
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
const asyncRoutes: RouteRecordRaw[] = [];
routes.forEach((route) => {
const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
// 判断用户(角色)是否有该路由的访问权限
if (hasPermission(roles, tmpRoute)) {
if (tmpRoute.component?.toString() == "Layout") {
tmpRoute.component = Layout;
} else {
const component = modules[`../../views/${tmpRoute.component}.vue`];
if (component) {
tmpRoute.component = component;
} else {
tmpRoute.component = modules[`../../views/error-page/404.vue`];
}
}
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, roles);
}
asyncRoutes.push(tmpRoute);
}
});
return asyncRoutes;
};
// setup
export const usePermissionStore = defineStore("permission", () => {
// state
const routes = ref<RouteRecordRaw[]>([]);
// actions
function setRoutes(newRoutes: RouteRecordRaw[]) {
routes.value = constantRoutes.concat(newRoutes);
}
/**
* 生成动态路由
*
* @param roles 用户角色集合
* @returns
*/
function generateRoutes(roles: string[]) {
// 根据角色获取有访问权限的路由
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
setRoutes(accessedRoutes);
return accessedRoutes;
}
return { routes, setRoutes, generateRoutes };
});
// 非setup
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
@@ -0,0 +1,56 @@
import { defineStore } from "pinia";
import defaultSettings from "@/settings";
import { useStorage } from "@vueuse/core";
export const useSettingsStore = defineStore("setting", () => {
// state
const tagsView = useStorage<boolean>("tagsView", defaultSettings.tagsView);
const showSettings = ref<boolean>(defaultSettings.showSettings);
const fixedHeader = ref<boolean>(defaultSettings.fixedHeader);
const sidebarLogo = ref<boolean>(defaultSettings.sidebarLogo);
const layout = useStorage<string>("layout", defaultSettings.layout);
const themeColor = useStorage<string>(
"themeColor",
defaultSettings.themeColor
);
// actions
function changeSetting(param: { key: string; value: any }) {
const { key, value } = param;
switch (key) {
case "showSettings":
showSettings.value = value;
break;
case "fixedHeader":
fixedHeader.value = value;
break;
case "tagsView":
tagsView.value = value;
break;
case "sidevarLogo":
sidebarLogo.value = value;
break;
case "layout":
layout.value = value;
break;
case "themeColor":
themeColor.value = value;
break;
default:
break;
}
}
return {
showSettings,
tagsView,
fixedHeader,
sidebarLogo,
layout,
themeColor,
changeSetting,
};
});
+218
View File
@@ -0,0 +1,218 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { RouteLocationNormalized } from "vue-router";
export interface TagView extends Partial<RouteLocationNormalized> {
title?: string;
}
// setup
export const useTagsViewStore = defineStore("tagsView", () => {
// state
const visitedViews = ref<TagView[]>([]);
const cachedViews = ref<string[]>([]);
// actions
function addVisitedView(view: TagView) {
if (visitedViews.value.some((v) => v.path === view.path)) return;
if (view.meta && view.meta.affix) {
visitedViews.value.unshift(
Object.assign({}, view, {
title: view.meta?.title || "no-name",
})
);
} else {
visitedViews.value.push(
Object.assign({}, view, {
title: view.meta?.title || "no-name",
})
);
}
}
function addCachedView(view: TagView) {
const viewName = view.name as string;
if (cachedViews.value.includes(viewName)) return;
if (view.meta?.keepAlive) {
cachedViews.value.push(viewName);
}
}
function delVisitedView(view: TagView) {
return new Promise((resolve) => {
for (const [i, v] of visitedViews.value.entries()) {
if (v.path === view.path) {
visitedViews.value.splice(i, 1);
break;
}
}
resolve([...visitedViews.value]);
});
}
function delCachedView(view: TagView) {
const viewName = view.name as string;
return new Promise((resolve) => {
const index = cachedViews.value.indexOf(viewName);
index > -1 && cachedViews.value.splice(index, 1);
resolve([...cachedViews.value]);
});
}
function delOtherVisitedViews(view: TagView) {
return new Promise((resolve) => {
visitedViews.value = visitedViews.value.filter((v) => {
return v.meta?.affix || v.path === view.path;
});
resolve([...visitedViews.value]);
});
}
function delOtherCachedViews(view: TagView) {
const viewName = view.name as string;
return new Promise((resolve) => {
const index = cachedViews.value.indexOf(viewName);
if (index > -1) {
cachedViews.value = cachedViews.value.slice(index, index + 1);
} else {
// if index = -1, there is no cached tags
cachedViews.value = [];
}
resolve([...cachedViews.value]);
});
}
function updateVisitedView(view: TagView) {
for (let v of visitedViews.value) {
if (v.path === view.path) {
v = Object.assign(v, view);
break;
}
}
}
function addView(view: TagView) {
addVisitedView(view);
addCachedView(view);
}
function delView(view: TagView) {
return new Promise((resolve) => {
delVisitedView(view);
delCachedView(view);
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
}
function delOtherViews(view: TagView) {
return new Promise((resolve) => {
delOtherVisitedViews(view);
delOtherCachedViews(view);
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
}
function delLeftViews(view: TagView) {
return new Promise((resolve) => {
const currIndex = visitedViews.value.findIndex(
(v) => v.path === view.path
);
if (currIndex === -1) {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true 固定tag,例如“首页”
if (index >= currIndex || (item.meta && item.meta.affix)) {
return true;
}
const cacheIndex = cachedViews.value.indexOf(item.name as string);
if (cacheIndex > -1) {
cachedViews.value.splice(cacheIndex, 1);
}
return false;
});
resolve({
visitedViews: [...visitedViews.value],
});
});
}
function delRightViews(view: TagView) {
return new Promise((resolve) => {
const currIndex = visitedViews.value.findIndex(
(v) => v.path === view.path
);
if (currIndex === -1) {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true 固定tag,例如“首页”
if (index <= currIndex || (item.meta && item.meta.affix)) {
return true;
}
const cacheIndex = cachedViews.value.indexOf(item.name as string);
if (cacheIndex > -1) {
cachedViews.value.splice(cacheIndex, 1);
}
return false;
});
resolve({
visitedViews: [...visitedViews.value],
});
});
}
function delAllViews() {
return new Promise((resolve) => {
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
visitedViews.value = affixTags;
cachedViews.value = [];
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
}
function delAllVisitedViews() {
return new Promise((resolve) => {
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
visitedViews.value = affixTags;
resolve([...visitedViews.value]);
});
}
function delAllCachedViews() {
return new Promise((resolve) => {
cachedViews.value = [];
resolve([...cachedViews.value]);
});
}
return {
visitedViews,
cachedViews,
addVisitedView,
addCachedView,
delVisitedView,
delCachedView,
delOtherVisitedViews,
delOtherCachedViews,
updateVisitedView,
addView,
delView,
delOtherViews,
delLeftViews,
delRightViews,
delAllViews,
delAllVisitedViews,
delAllCachedViews,
};
});