feat: 升级前端框架,适配手机端

This commit is contained in:
xiaojunnuo
2025-03-06 21:11:07 +08:00
659 changed files with 37406 additions and 873 deletions
@@ -1,437 +0,0 @@
import { defineStore } from "pinia";
import { cloneDeep, get, uniq } from "lodash-es";
import router from "/src/router";
import { frameworkRoutes } from "/src/router/resolve";
// @ts-ignore
import { LocalStorage } from "/src/utils/util.storage";
import { useUserStore } from "/src/store/modules/user";
const OPENED_CACHE_KEY = "TABS_OPENED";
interface PageState {
// 可以在多页 tab 模式下显示的页面
pool: Array<any>;
// 当前显示的多页面列表
opened: Array<any>;
// 已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
openedLoaded: boolean;
// 当前页面
current: string;
// 需要缓存的页面 name
keepAlive: Array<any>;
inited: boolean;
}
// 判定是否需要缓存
const isKeepAlive = (data: any) => get(data, "meta.cache", false);
export const usePageStore = defineStore({
id: "app.page",
state: (): PageState => ({
// 可以在多页 tab 模式下显示的页面
pool: [],
// 当前显示的多页面列表
opened: [
{
name: "index",
fullPath: "/index",
meta: {
title: "首页",
auth: false
}
}
],
// 已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
openedLoaded: false,
// 当前页面
current: "",
// 需要缓存的页面 name
keepAlive: [],
inited: false
}),
getters: {
// @ts-ignore
getOpened(): any {
// @ts-ignore
return this.opened;
},
getCurrent(): string {
return this.current;
}
},
actions: {
/**
* @description 确认已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
* @param {Object} context
*/
async isLoaded() {
if (this.openedLoaded) {
return true;
}
return new Promise((resolve) => {
const timer = setInterval(() => {
if (this.openedLoaded) {
resolve(clearInterval(timer));
}
}, 10);
});
},
/**
* @class opened
* @description 从持久化数据载入标签页列表
* @param {Object} context
*/
async openedLoad() {
// store 赋值
const value = LocalStorage.get(this.getStorageKey());
if (value == null) {
return;
}
// 在处理函数中进行数据优化 过滤掉现在已经失效的页签或者已经改变了信息的页签
// 以 fullPath 字段为准
// 如果页面过多的话可能需要优化算法
// valid 有效列表 1, 1, 0, 1 => 有效, 有效, 失效, 有效
const valid: Array<number> = [];
// 处理数据
this.opened = value
.map((opened: any) => {
// 忽略首页
if (opened.fullPath === "/index") {
valid.push(1);
return opened;
}
// 尝试在所有的支持多标签页的页面里找到 name 匹配的页面
const find = this.pool.find((item) => item.name === opened.name);
// 记录有效或无效信息
valid.push(find ? 1 : 0);
// 返回合并后的数据 新的覆盖旧的
// 新的数据中一般不会携带 params 和 query, 所以旧的参数会留存
return Object.assign({}, opened, find);
})
.filter((opened: any, index: any) => valid[index] === 1);
// 标记已经加载多标签页数据 https://github.com/d2-projects/d2-admin/issues/201
this.openedLoaded = true;
// 根据 opened 数据生成缓存设置
this.keepAliveRefresh();
},
getStorageKey() {
const userStore = useUserStore();
const userId = userStore.getUserInfo?.id ?? "anonymous";
return OPENED_CACHE_KEY + ":" + userId;
},
/**
* 将 opened 属性赋值并持久化 在这之前请先确保已经更新了 state.opened
* @param {Object} context
*/
async opened2db() {
// 设置数据
LocalStorage.set(this.getStorageKey(), this.opened);
},
/**
* @class opened
* @description 更新页面列表上的某一项
* @param {Object} context
* @param {Object} payload { index, params, query, fullPath } 路由信息
*/
async openedUpdate({ index, params, query, fullPath }: any) {
// 更新页面列表某一项
const page = this.opened[index];
page.params = params || page.params;
page.query = query || page.query;
page.fullPath = fullPath || page.fullPath;
this.opened.splice(index, 1, page);
// 持久化
await this.opened2db();
},
/**
* @class opened
* @description 重排页面列表上的某一项
* @param {Object} context
* @param {Object} payload { oldIndex, newIndex } 位置信息
*/
async openedSort({ oldIndex, newIndex }: any) {
// 重排页面列表某一项
const page = this.opened[oldIndex];
this.opened.splice(oldIndex, 1);
this.opened.splice(newIndex, 0, page);
// 持久化
await this.opened2db();
},
/**
* @class opened
* @description 新增一个 tag (打开一个页面)
* @param {Object} context
* @param {Object} payload new tag info
*/
async add({ tag, params, query, fullPath }: any) {
// 设置新的 tag 在新打开一个以前没打开过的页面时使用
const newTag = tag;
newTag.params = params || newTag.params;
newTag.query = query || newTag.query;
newTag.fullPath = fullPath || newTag.fullPath;
// 添加进当前显示的页面数组
this.opened.push(newTag);
// 如果这个页面需要缓存 将其添加到缓存设置
if (isKeepAlive(newTag)) {
this.keepAlivePush(tag.name);
}
// 持久化
await this.opened2db();
},
/**
* @class current
* @description 打开一个新的页面
* @param {Object} context
* @param {Object} payload 从路由钩子的 to 对象上获取 { name, params, query, fullPath, meta } 路由信息
*/
async open({ name, params, query, fullPath, meta }: any) {
// 已经打开的页面
const opened = this.opened;
// 判断此页面是否已经打开 并且记录位置
let pageOpendIndex = 0;
const pageOpend = opened.find((page, index) => {
const same = page.fullPath === fullPath;
pageOpendIndex = same ? index : pageOpendIndex;
return same;
});
if (pageOpend) {
// 页面以前打开过
await this.openedUpdate({
index: pageOpendIndex,
params,
query,
fullPath
});
} else {
// 页面以前没有打开过
const page = this.pool.find((t) => t.name === name);
// 如果这里没有找到 page 代表这个路由虽然在框架内 但是不参与标签页显示
if (page) {
this.add({
tag: Object.assign({}, page),
params,
query,
fullPath
});
}
}
// 如果这个页面需要缓存 将其添加到缓存设置
if (isKeepAlive({ meta })) {
this.keepAlivePush(name);
}
// 设置当前的页面
this.currentSet(fullPath);
},
/**
* @class opened
* @description 关闭一个 tag (关闭一个页面)
* @param {Object} context
* @param {Object} payload { tagName: 要关闭的标签名字 }
*/
async close({ tagName }: any) {
// 预定下个新页面
let newPage = {};
const isCurrent = this.current === tagName;
// 如果关闭的页面就是当前显示的页面
if (isCurrent) {
// 去找一个新的页面
const len = this.opened.length;
for (let i = 0; i < len; i++) {
if (this.opened[i].fullPath === tagName) {
newPage = i < len - 1 ? this.opened[i + 1] : this.opened[i - 1];
break;
}
}
}
// 找到这个页面在已经打开的数据里是第几个
const index = this.opened.findIndex((page) => page.fullPath === tagName);
if (index >= 0) {
// 如果这个页面是缓存的页面 将其在缓存设置中删除
this.keepAliveRemove(this.opened[index].name);
// 更新数据 删除关闭的页面
this.opened.splice(index, 1);
}
// 持久化
await this.opened2db();
// 决定最后停留的页面
if (isCurrent) {
// @ts-ignore
const { name = "index", params = {}, query = {} } = newPage;
const routerObj = { name, params, query };
await router.push(routerObj);
}
},
/**
* @class opened
* @description 关闭当前标签左边的标签
* @param opts
*/
async closeLeft(opts = {}) {
await this.closeByCondition({
condition({ i, currentIndex }: any) {
return i >= currentIndex;
},
...opts
});
},
/**
* @class opened
* @description 关闭当前标签右边的标签
* @param opts
*/
async closeRight(opts = {}) {
await this.closeByCondition({
condition({ i, currentIndex }: any) {
return currentIndex >= i;
},
...opts
});
},
/**
* @class opened
* @description 关闭当前标签右边的标签
* @param opts
*/
async closeByCondition(opts = {}) {
// @ts-ignore
const { pageSelect, condition } = opts;
const pageAim = pageSelect || this.current;
let currentIndex = 0;
this.opened.forEach((page, index) => {
if (page.fullPath === pageAim) currentIndex = index;
});
// 删除打开的页面 并在缓存设置中删除
for (let i = this.opened.length - 1; i >= 0; i--) {
if (this.opened[i].name === "index" || condition({ i, currentIndex })) {
continue;
}
this.keepAliveRemove(this.opened[i].name);
this.opened.splice(i, 1);
}
// 持久化
await this.opened2db();
// 设置当前的页面
this.current = pageAim;
// @ts-ignore
if (router.currentRoute.fullPath !== pageAim) await router.push(pageAim);
},
/**
* @class opened
* @description 关闭当前激活之外的 tag
* @param opts
*/
async closeOther(opts = {}) {
await this.closeByCondition({
condition({ i, currentIndex }: any) {
return currentIndex === i;
},
...opts
});
},
/**
* @class opened
* @description 关闭所有 tag
* @param {Object} context
*/
async closeAll() {
// 删除打开的页面 并在缓存设置中删除
for (let i = this.opened.length - 1; i >= 0; i--) {
if (this.opened[i].name === "index") {
continue;
}
this.keepAliveRemove(this.opened[i].name);
this.opened.splice(i, 1);
}
// 持久化
await this.opened2db();
// 关闭所有的标签页后需要判断一次现在是不是在首页
// @ts-ignore
if (router.currentRoute.name !== "index") {
await router.push({ name: "index" });
}
},
/**
* @class keepAlive
* @description 从已经打开的页面记录中更新需要缓存的页面记录
* @param {Object} state state
*/
keepAliveRefresh() {
this.keepAlive = this.opened.filter((item) => isKeepAlive(item)).map((e) => e.name);
console.log("keepalive", this.keepAlive);
},
/**
* @description 删除一个页面的缓存设置
* @param {Object} state state
* @param {String} name name
*/
keepAliveRemove(name: string) {
const list = cloneDeep(this.keepAlive);
const index = list.findIndex((item) => item === name);
if (index !== -1) {
list.splice(index, 1);
this.keepAlive = list;
}
},
/**
* @description 增加一个页面的缓存设置
* @param {Object} state state
* @param {String} name name
*/
keepAlivePush(name: string) {
const keep = cloneDeep(this.keepAlive);
keep.push(name);
this.keepAlive = uniq(keep);
},
/**
* @description 清空页面缓存设置
* @param {Object} state state
*/
keepAliveClean() {
this.keepAlive = [];
},
/**
* @class current
* @description 设置当前激活的页面 fullPath
* @param {Object} state state
* @param {String} fullPath new fullPath
*/
currentSet(fullPath: string) {
this.current = fullPath;
},
/**
* @class pool
* @description 保存 pool (候选池)
* @param {Object} state state
* @param {Array} routes routes
*/
async init(routes?: any) {
if (this.inited) {
return;
}
this.inited = true;
if (routes == null) {
//不能用全部的routes,只能是framework内的
routes = frameworkRoutes;
}
const pool: any = [];
const push = function (routes: any) {
routes.forEach((route: any) => {
if (route.children && route.children.length > 0) {
push(route.children);
} else {
if (!route.hidden) {
const { meta, name, path } = route;
// @ts-ignore
pool.push({ meta, name, path });
}
}
});
};
push(routes);
this.pool = pool;
await this.openedLoad();
}
}
});
@@ -1,148 +0,0 @@
import { defineStore } from "pinia";
// @ts-ignore
import { frameworkMenus, headerMenus, filterMenus, findMenus } from "/src/router/resolve";
import * as _ from "lodash-es";
import { mitter } from "/src/utils/util.mitt";
//监听注销事件
mitter.on("app.logout", () => {
const resourceStore = useResourceStore();
resourceStore.clear();
});
//
// mitter.on("app.login", () => {
// const resourceStore = useResourceStore();
// resourceStore.clear();
// resourceStore.init();
// });
interface ResourceState {
topMenus: Array<any>;
authedTopMenus: Array<any>;
headerMenus: Array<any>;
asideMenus: Array<any>;
fixedAsideMenus: Array<any>;
inited: boolean;
currentTopMenu?: string;
currentAsidePath?: string;
}
export const useResourceStore = defineStore({
id: "app.resource",
//@ts-ignore
state: (): ResourceState => ({
// user info
topMenus: [],
authedTopMenus: [],
headerMenus: [],
fixedAsideMenus: [],
inited: false,
currentTopMenu: undefined,
currentAsidePath: undefined
}),
getters: {
getAsideMenus() {
let topMenu = this.currentTopMenu;
if (!topMenu && this.authedTopMenus.length > 0) {
topMenu = this.authedTopMenus[0];
}
let asideMenus = topMenu?.children || [];
asideMenus = [...this.fixedAsideMenus, ...asideMenus];
return asideMenus;
},
getHeaderMenus() {
return this.headerMenus;
},
getFrameworkMenus() {
return this.authedTopMenus;
// const menus = _.cloneDeep(this.authedTopMenus);
// for (const menu of menus) {
// delete menu.children;
// }
// return menus;
}
} as any,
actions: {
clear() {
this.inited = false;
this.currentTopMenu = undefined;
},
/**
* 初始化资源
*/
init() {
if (this.inited) {
return;
}
this.inited = true;
const allMenus = _.cloneDeep(frameworkMenus[0].children);
this.topMenus = filterMenus(allMenus, (item: any) => {
return item?.meta?.showOnHeader !== false;
});
this.fixedAsideMenus = findMenus(allMenus, (item: any) => {
return item?.meta?.fixedAside === true;
});
this.headerMenus = headerMenus;
},
setCurrentTopMenu(topMenu?: any) {
if (this.topMenus.length === 0) {
return;
}
if (topMenu == null) {
topMenu = this.topMenus[0];
}
this.currentTopMenu = topMenu;
},
setCurrentTopMenuByCurrentRoute(matched: any) {
const menuHeader = this.authedTopMenus;
if (matched?.length <= 1) {
return;
}
function findFromTree(tree: any, find: any) {
tree = tree || [];
const results: Array<any> = [];
for (const item of tree) {
if (find(item)) {
results.push(item);
return results;
}
if (item.children && item.children.length > 0) {
const found: any = findFromTree(item.children, find);
if (found) {
results.push(item);
return results.concat(found);
}
}
}
}
const matchedPath = matched[1].path;
const _side = findFromTree(menuHeader, (menu: any) => menu.path === matchedPath);
if (_side?.length > 0) {
if (this.currentAsidePath === _side[0]) {
return;
}
this.currentAsidePath = _side[0];
this.setCurrentTopMenu(_side[0]);
}
},
filterByPermission(permissions: any) {
this.authedTopMenus = this.filterChildrenByPermission(this.topMenus, permissions);
},
filterChildrenByPermission(list: any, permissions: any) {
const menus = list.filter((item: any) => {
if (item?.meta?.permission) {
return permissions.includes(item.meta.permission);
}
return true;
});
for (const menu of menus) {
if (menu.children && menu.children.length > 0) {
menu.children = this.filterChildrenByPermission(menu.children, permissions);
}
}
return menus;
}
}
});
@@ -1,5 +1,5 @@
import { defineStore } from "pinia";
import { Modal, notification, theme } from "ant-design-vue";
import { Modal, notification } from "ant-design-vue";
import * as _ from "lodash-es";
// @ts-ignore
import { LocalStorage } from "/src/utils/util.storage";
@@ -9,20 +9,9 @@ import { HeaderMenus, PlusInfo, SiteEnv, SiteInfo, SuiteSetting, SysInstallInfo,
import { useUserStore } from "/@/store/modules/user";
import { mitter } from "/@/utils/util.mitt";
import { env } from "/@/utils/util.env";
import { preferences } from "/@/vben/preferences";
export type ThemeToken = {
token: {
colorPrimary?: string;
};
algorithm: any;
};
export type ThemeConfig = {
colorPrimary: string;
mode: string;
};
export interface SettingState {
themeConfig?: ThemeConfig;
themeToken: ThemeToken;
sysPublic?: SysPublicSetting;
installInfo?: {
siteId: string;
@@ -40,11 +29,6 @@ export interface SettingState {
suiteSetting?: SuiteSetting;
}
const defaultThemeConfig = {
colorPrimary: "#1890ff",
mode: "light"
};
const SETTING_THEME_KEY = "SETTING_THEME";
const defaultSiteInfo: SiteInfo = {
title: env.TITLE || "Certd",
slogan: env.SLOGAN || "让你的证书永不过期",
@@ -56,11 +40,6 @@ const defaultSiteInfo: SiteInfo = {
export const useSettingStore = defineStore({
id: "app.setting",
state: (): SettingState => ({
themeConfig: null,
themeToken: {
token: {},
algorithm: theme.defaultAlgorithm
},
plusInfo: {
isPlus: false,
vipType: "free",
@@ -93,9 +72,6 @@ export const useSettingStore = defineStore({
inited: false
}),
getters: {
getThemeConfig(): any {
return this.themeConfig || _.merge({}, defaultThemeConfig, LocalStorage.get(SETTING_THEME_KEY) || {});
},
getSysPublic(): SysPublicSetting {
return this.sysPublic;
},
@@ -162,6 +138,10 @@ export const useSettingStore = defineStore({
}
}
this.siteInfo = _.merge({}, defaultSiteInfo, siteInfo);
if (this.siteInfo.logo) {
preferences.logo.source = this.siteInfo.logo;
}
},
async checkUrlBound() {
const userStore = useUserStore();
@@ -207,44 +187,7 @@ export const useSettingStore = defineStore({
}
}
},
persistThemeConfig() {
LocalStorage.set(SETTING_THEME_KEY, this.getThemeConfig);
},
async setThemeConfig(themeConfig?: ThemeConfig) {
this.themeConfig = _.merge({}, this.themeConfig, themeConfig);
this.persistThemeConfig();
this.setPrimaryColor(this.themeConfig.colorPrimary);
this.setDarkMode(this.themeConfig.mode);
},
setPrimaryColor(color: any) {
this.themeConfig.colorPrimary = color;
_.set(this.themeToken, "token.colorPrimary", color);
this.persistThemeConfig();
},
setDarkMode(mode: string) {
this.themeConfig.mode = mode;
if (mode === "dark") {
this.themeToken.algorithm = theme.darkAlgorithm;
// const defaultSeed = theme.defaultSeed;
// const mapToken = theme.darkAlgorithm(defaultSeed);
// less.modifyVars(mapToken);
// less.modifyVars({
// "@colorPrimaryBg": "#111a2c",
// colorPrimaryBg: "#111a2c"
// });
// less.refreshStyles();
} else {
this.themeToken.algorithm = theme.defaultAlgorithm;
// const defaultSeed = theme.defaultSeed;
// const mapToken = theme.defaultAlgorithm(defaultSeed);
// less.modifyVars(mapToken);
}
this.persistThemeConfig();
},
async init() {
await this.setThemeConfig(this.getThemeConfig);
await this.loadSysSettings();
},
async initOnce() {
@@ -11,6 +11,7 @@ import { message, Modal, notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { mitter } from "/src/utils/util.mitt";
import { resetAllStores, useAccessStore } from "/@/vben/stores";
interface UserState {
userInfo: Nullable<UserInfoRes>;
@@ -39,8 +40,10 @@ export const useUserStore = defineStore({
}
},
actions: {
setToken(info: string, expire: number) {
this.token = info;
setToken(token: string, expire: number) {
this.token = token;
const accessStore = useAccessStore();
accessStore.setAccessToken(token);
LocalStorage.set(TOKEN_KEY, this.token, expire);
},
setUserInfo(info: UserInfoRes) {
@@ -92,11 +95,10 @@ export const useUserStore = defineStore({
},
async onLoginSuccess(loginData: any) {
await this.getUserInfoAction();
const userInfo = await this.getUserInfoAction();
mitter.emit("app.login", { userInfo, token: loginData });
// await this.getUserInfoAction();
// const userInfo = await this.getUserInfoAction();
mitter.emit("app.login", { token: loginData });
await router.replace("/");
return userInfo;
},
/**
@@ -104,6 +106,7 @@ export const useUserStore = defineStore({
*/
logout(goLogin = true) {
this.resetState();
resetAllStores();
goLogin && router.push("/login");
mitter.emit("app.logout");
},