init: 初始系统模板

This commit is contained in:
TsMask
2023-09-05 14:38:23 +08:00
parent a5bc16ae4f
commit 1075c8ae4f
130 changed files with 22531 additions and 1 deletions

31
src/store/modules/app.ts Normal file
View File

@@ -0,0 +1,31 @@
import { defineStore } from 'pinia';
/**应用参数类型 */
type AppStore = {
/**应用名称 */
appName: string;
/**应用标识 */
appCode: string;
/**应用版本 */
appVersion: string;
};
const useAppStore = defineStore('app', {
state: (): AppStore => ({
appName: import.meta.env.VITE_APP_NAME,
appCode: import.meta.env.VITE_APP_CODE,
appVersion: import.meta.env.VITE_APP_VERSION,
}),
actions: {
/**设置网页标题 */
setTitle(title?: string) {
if (title) {
document.title = `${title} - ${this.appName}`;
} else {
document.title = this.appName;
}
},
},
});
export default useAppStore;

63
src/store/modules/dict.ts Normal file
View File

@@ -0,0 +1,63 @@
import { defineStore } from 'pinia';
import { getDictDataType } from '@/api/system/dict/data';
/**字典参数类型 */
type DictStore = {
/**字典数据 */
dicts: Map<string, DictType[]>;
};
const useDictStore = defineStore('dict', {
state: (): DictStore => ({
dicts: new Map(),
}),
actions: {
/**清空字典 */
clearDict() {
this.dicts.clear();
},
/**删除字典 */
removeDict(key: string) {
if (!key) return;
return this.dicts.delete(key);
},
/**
* 处理字典数据对象用于回显标签
* @param data 字典数据项
* @returns
*/
parseDataDict(data: Record<string, any>) {
return [
{
label: data.dictLabel,
value: data.dictValue,
elTagType: data.tagType,
elTagClass: data.tagClass,
},
];
},
/**获取字典 */
async getDict(key: string) {
if (!key) return [];
let disct = this.dicts.get(key);
if (disct === undefined || disct.length === 0) {
const res = await getDictDataType(key);
if (res.code === 200 && Array.isArray(res.data)) {
const dictData: DictType[] = res.data.map(d => ({
label: d.dictLabel,
value: d.dictValue,
elTagType: d.tagType,
elTagClass: d.tagClass,
}));
this.dicts.set(key, dictData);
disct = dictData;
} else {
disct = [];
}
}
return disct;
},
},
});
export default useDictStore;

View File

@@ -0,0 +1,91 @@
import { CACHE_LOCAL_PROCONFIG } from '@/constants/cache-keys-constants';
import { localGetJSON, localSetJSON } from '@/utils/cache-local-utils';
import { defineStore } from 'pinia';
/**布局参数类型 */
type LayoutStore = {
/**布局设置抽屉显示 */
visible: boolean;
/**布局配置 */
proConfig: {
/**导航布局 */
layout: 'side' | 'top' | 'mix';
/**导航菜单主题色 */
navTheme: 'dark' | 'light';
/**顶部导航主题仅导航布局为mix时生效 */
headerTheme: 'dark' | 'light';
/**固定顶部栏 */
fixedHeader: boolean;
/**固定菜单栏 */
fixSiderbar: boolean;
/**自动分割菜单 */
splitMenus: boolean;
/**内容区域-顶栏 */
headerRender: any | boolean | undefined;
/**内容区域-页脚 */
footerRender: any | boolean | undefined;
/**内容区域-菜单头 */
menuHeaderRender: any | boolean | undefined;
/**内容区域-导航标签项 */
tabRender: any | boolean | undefined;
};
/**水印内容 */
waterMarkContent: string;
};
/**判断是否关闭内容区域 */
const proRender = (render: any) => (render === false ? false : undefined);
/**本地缓存-布局配置设置 */
const proConfigLocal: LayoutStore['proConfig'] = localGetJSON(
CACHE_LOCAL_PROCONFIG
) || {
layout: 'side',
headerTheme: 'light',
navTheme: 'light',
fixSiderbar: true,
fixedHeader: true,
splitMenus: true,
};
const useLayoutStore = defineStore('layout', {
state: (): LayoutStore => ({
visible: false,
proConfig: {
layout: proConfigLocal.layout,
navTheme: proConfigLocal.navTheme,
headerTheme: proConfigLocal.headerTheme,
fixedHeader: Boolean(proConfigLocal.fixedHeader),
fixSiderbar: Boolean(proConfigLocal.fixSiderbar),
splitMenus: Boolean(proConfigLocal.splitMenus),
headerRender: proRender(proConfigLocal.headerRender),
footerRender: proRender(proConfigLocal.footerRender),
menuHeaderRender: proRender(proConfigLocal.menuHeaderRender),
tabRender: proRender(proConfigLocal.tabRender),
},
waterMarkContent: import.meta.env.VITE_APP_NAME,
}),
actions: {
/**改变显示状态 */
changeVisibleLayoutSetting() {
this.visible = !this.visible;
},
/**修改水印文字 */
changeWaterMark(text: string) {
this.waterMarkContent = text;
},
/**修改布局设置 */
changeConf(key: string, value: boolean | string | number | undefined) {
if (Reflect.has(this.proConfig, key)) {
// 同时修改mix混合菜单的导航主题
if (key === 'navTheme') {
Reflect.set(this.proConfig, 'headerTheme', value);
}
Reflect.set(this.proConfig, key, value);
localSetJSON(CACHE_LOCAL_PROCONFIG, this.proConfig);
}
},
},
});
export default useLayoutStore;

152
src/store/modules/router.ts Normal file
View File

@@ -0,0 +1,152 @@
import { defineStore } from 'pinia';
import {
RouteComponent,
RouteLocationRaw,
RouteMeta,
RouteRecordRaw,
} from 'vue-router';
import { getRouters } from '@/api/router';
import BasicLayout from '@/layouts/BasicLayout.vue';
import BlankLayout from '@/layouts/BlankLayout.vue';
import LinkLayout from '@/layouts/LinkLayout.vue';
import {
MENU_COMPONENT_LAYOUT_BASIC,
MENU_COMPONENT_LAYOUT_BLANK,
MENU_COMPONENT_LAYOUT_LINK,
} from '@/constants/menu-constants';
/**路由构建参数类型 */
type RouterStore = {
/**初始的根路由数据 */
rootRouterData: RouteRecordRaw[];
/**动态路由数据 */
buildRouterData: RouteRecordRaw[];
};
const useRouterStore = defineStore('router', {
state: (): RouterStore => ({
rootRouterData: [],
buildRouterData: [],
}),
actions: {
/**
* 记录初始根节点菜单数据
* @param data 初始数据
* @returns 初始数据
*/
setRootRouterData(data: RouteRecordRaw[]) {
if (this.rootRouterData.length <= 0) {
this.rootRouterData = data;
}
return this.rootRouterData;
},
/**
* 动态路由列表数据生成
* @returns 生成的路由菜单
*/
async generateRoutes() {
const res = await getRouters();
if (res.code === 200 && Array.isArray(res.data)) {
const buildRoutes = buildRouters(res.data.concat());
this.buildRouterData = buildRoutes;
return buildRoutes;
}
return [];
},
},
});
/**异步路由类型 */
type RecordRaws = {
path: string;
name: string;
meta: RouteMeta;
redirect: RouteLocationRaw;
component: string;
children: RecordRaws[];
};
/**
* 构建动态路由
*
* 遍历后台配置的路由菜单,转换为组件路由菜单
*
* @param recordRaws 异步路由列表
* @returns 可添加的路由列表
*/
function buildRouters(recordRaws: RecordRaws[]): RouteRecordRaw[] {
const routers: RouteRecordRaw[] = [];
for (const item of recordRaws) {
// 路由页面组件
let component: RouteComponent = {};
if (item.component) {
const comp = item.component;
if (comp === MENU_COMPONENT_LAYOUT_BASIC) {
component = BasicLayout;
} else if (comp === MENU_COMPONENT_LAYOUT_BLANK) {
component = BlankLayout;
} else if (comp === MENU_COMPONENT_LAYOUT_LINK) {
component = LinkLayout;
} else {
// 指定页面视图,一般用于显示子菜单
component = findView(comp);
}
}
// 有子菜单进行递归
let children: RouteRecordRaw[] = [];
if (item.children && item.children.length > 0) {
children = buildRouters(item.children);
}
// 对元数据特殊参数进行处理
let metaIcon = (item.meta?.icon as string) || '';
if (!metaIcon.startsWith('icon-')) {
metaIcon = '';
}
item.meta = Object.assign(item.meta, {
icon: metaIcon,
});
// 构建路由
const router: RouteRecordRaw = {
path: item.path,
name: item.name,
meta: item.meta,
redirect: item.redirect,
component: component,
children: children,
};
routers.push(router);
}
return routers;
}
/**匹配views里面所有的.vue或.tsx文件 */
const views = import.meta.glob('./../../views/**/*.{vue,tsx}');
/**
* 查找页面模块
*
* 查找 `/views/system/menu/index.vue` 或 `/views/system/menu/index.tsx`
*
* 参数值为 `system/menu/index`
*
* @param dirName 组件路径
* @returns 路由懒加载函数
*/
function findView(dirName: string) {
for (const dir in views) {
let viewDirName = '';
const component = dir.match(/views\/(.+)\.(vue|tsx)/);
if (component && component.length === 3) {
viewDirName = component[1];
}
if (viewDirName === dirName) {
return () => views[dir]();
}
}
return () => import('@/views/error/404.vue');
}
export default useRouterStore;

189
src/store/modules/tabs.ts Normal file
View File

@@ -0,0 +1,189 @@
import { defineStore } from 'pinia';
import type { LocationQuery, RouteLocationNormalizedLoaded } from 'vue-router';
/**导航标签栏类型 */
type TabsStore = {
/**标签列表 */
tabs: TabType[];
/**激活标签项 */
activePath: string;
/**缓存页面路由名称 */
caches: Set<string>;
};
/**标签信息类型 */
type TabType = {
path: string;
query: LocationQuery;
name: string;
title: string;
icon?: any;
cache?: boolean;
};
const useTabsStore = defineStore('tabs', {
state: (): TabsStore => ({
tabs: [],
activePath: '',
caches: new Set(),
}),
getters: {
/**获取导航标签栏列表 */
getTabs(state) {
return state.tabs;
},
/**获取缓存页面名 */
getCaches(state) {
return [...state.caches];
},
},
actions: {
/**清空标签项和缓存项列表 */
clear() {
this.tabs = [];
this.caches.clear();
},
/**
* 删除标签项
* @param path 当期标签路由地址
* @returns 布尔 true/false
*/
remove(path: string) {
if (!path) return false;
const tabIndex = this.tabs.findIndex(tab => tab.path === path);
if (tabIndex === -1) return false;
// 同名称标签只剩一个时,才移除缓存
const name = this.tabs[tabIndex].name;
const tabs = this.tabs.filter(tab => tab.name === name);
if (tabs.length <= 1) {
this.cacheDelete(name);
}
this.tabs.splice(tabIndex, 1);
return true;
},
/**
* 添加标签项
* @param tab 标签信息对象
* @param index 插入指定位置,默认加到最后
* @returns 布尔 true/false
*/
add(tab: TabType, index?: number) {
const { path, query, name, title, icon, cache } = tab;
// 是否缓存
if (cache) {
this.cacheAdd(name);
}
// 获取没有才添加
let tabIndex = this.tabs.findIndex(tab => tab.path === path);
if (tabIndex >= 0) return false;
const idx = index ? index : this.tabs.length;
this.tabs.splice(idx, 0, { path, query, name, title, icon });
return true;
},
/**添加缓存项
* @param name 路由名称
* @returns 布尔 true/false
*/
cacheAdd(name: string) {
if (!name) return;
const has = this.caches.has(name);
if (has) return;
this.caches.add(name);
},
/**
* 删除缓存项
* @param name 路由名称
* @returns 布尔 true/false
*/
cacheDelete(name: string) {
if (!name) return false;
const has = this.caches.has(name);
if (!has) return false;
return this.caches.delete(name);
},
/**
* 打开标签
*
* 动态参数会开新标签,这是考虑多信息查看才没用同一个标签打开。
* @param raw 跳转的路由信息
* @returns 无
*/
tabOpen(raw: RouteLocationNormalizedLoaded) {
// 刷新是重定向不记录
if (raw.path.startsWith('/redirect')) return;
// 标签缓存使用路由名称
const name = (raw.name && raw.name.toString()) || '-';
// 新增到当期标签后面打开,获取当期标签下标
const tabIndex = this.tabs.findIndex(tab => tab.path === this.activePath);
this.add(
{
path: raw.path,
query: raw.query,
name: name,
title: raw.meta.title || '-',
icon: raw.meta.icon || '#',
cache: Boolean(raw.meta.cache),
},
tabIndex + 1
);
// 设置激活项
this.activePath = raw.path;
},
/**
* 关闭标签
* @param path 当期标签路由地址
* @returns 新跳转push路由参数
*/
tabClose(path: string) {
if (!path) return null;
// 获取当前项和最后项下标
const tabIndex = this.tabs.findIndex(tab => tab.path === path);
if (tabIndex === -1) return null;
const lastIndex = this.tabs.length - 1;
let to = null;
// 只有一项默认跳首页
if (lastIndex === 0) {
to = {
path: '/index',
query: {},
};
}
// 关闭当期标签,操作第一项跳后一项
else if (path === this.activePath && tabIndex === 0) {
const tab = this.tabs[tabIndex + 1];
to = {
path: tab.path,
query: tab.query,
};
}
// 关闭当期标签,默认跳前一项
else if (path === this.activePath && tabIndex <= lastIndex) {
const tab = this.tabs[tabIndex - 1];
to = {
path: tab.path,
query: tab.query,
};
}
// 移除标签
this.remove(path);
return to;
},
/**
* 跳转标签
* @param path 当期标签路由地址
* @returns 新跳转push路由参数
*/
tabGoto(path: string) {
if (!path) return null;
const tab = this.tabs.find(tab => tab.path === path);
if (!tab) return null;
return {
path: tab.path,
query: tab.query,
};
},
},
});
export default useTabsStore;

171
src/store/modules/user.ts Normal file
View File

@@ -0,0 +1,171 @@
import defaultAvatar from '@/assets/images/default_avatar.png';
import useLayoutStore from './layout';
import { login, logout, getInfo } from '@/api/login';
import { getToken, setToken, removeToken } from '@/plugins/auth-token';
import { defineStore } from 'pinia';
import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants';
import { validHttp } from '@/utils/regular-utils';
/**用户信息类型 */
type UserInfo = {
/**授权凭证 */
token: string;
/**登录账号 */
userName: string;
/**用户角色 字符串数组 */
roles: string[];
/**用户权限 字符串数组 */
permissions: string[];
/**用户头像 */
avatar: string;
/**用户昵称 */
nickName: string;
/**用户手机号 */
phonenumber: string;
/**用户邮箱 */
email: string;
/**用户性别 */
sex: string | undefined;
};
/**
* 格式解析头像地址
* @param avatar 头像路径
* @returns url地址
*/
function parseAvatar(avatar: string): string {
if (!avatar) {
return defaultAvatar;
}
if (validHttp(avatar)) {
return avatar;
}
const baseApi = import.meta.env.VITE_API_BASE_URL;
return `${baseApi}${avatar}`;
}
const useUserStore = defineStore('user', {
state: (): UserInfo => ({
token: getToken(),
userName: '',
roles: [],
permissions: [],
avatar: '',
nickName: '',
phonenumber: '',
email: '',
sex: undefined,
}),
getters: {
/**
* 获取正确头像地址
* @param state 内部属性不用传入
* @returns 头像地址url
*/
getAvatar(state) {
return parseAvatar(state.avatar);
},
/**
* 获取基础信息属性
* @param state 内部属性不用传入
* @returns 基础信息
*/
getBaseInfo(state) {
return {
nickName: state.nickName,
phonenumber: state.phonenumber,
email: state.email,
sex: state.sex,
};
},
},
actions: {
/**
* 更新基础信息属性
* @param data 变更信息
*/
setBaseInfo(data: Record<string, any>) {
this.nickName = data.nickName;
this.phonenumber = data.phonenumber;
this.email = data.email;
this.sex = data.sex;
},
/**
* 更新头像
* @param avatar 上传后的地址
*/
setAvatar(avatar: string) {
this.avatar = avatar;
},
/**
* 获取正确头像地址
* @param avatar
*/
fnAvatar(avatar: string) {
return parseAvatar(avatar);
},
// 登录
async fnLogin(loginBody: Record<string, string>) {
const res = await login(loginBody);
if (res.code === 200 && res.data) {
const token = res.data[TOKEN_RESPONSE_FIELD];
setToken(token);
this.token = token;
}
return res;
},
// 获取用户信息
async fnGetInfo() {
const res = await getInfo();
if (res.code === 200 && res.data) {
const { user, roles, permissions } = res.data;
// 登录账号
this.userName = user.userName;
// 用户头像
this.avatar = user.avatar;
// 基础信息
this.nickName = user.nickName;
this.phonenumber = user.phonenumber;
this.email = user.email;
this.sex = user.sex;
// 验证返回的roles是否是一个非空数组
if (Array.isArray(roles) && roles.length > 0) {
this.roles = roles;
this.permissions = permissions;
} else {
this.roles = ['ROLE_DEFAULT'];
this.permissions = [];
}
// 水印文字信息=用户昵称 手机号
let waterMarkContent = this.nickName;
if (this.phonenumber) {
waterMarkContent = `${this.nickName} ${this.phonenumber}`;
}
useLayoutStore().changeWaterMark(waterMarkContent);
}
// 网络错误时退出登录状态
if (res.code === 500) {
removeToken();
window.location.reload();
}
return res;
},
// 退出系统
async fnLogOut() {
try {
await logout();
} catch (error) {
throw error;
} finally {
this.token = '';
this.roles = [];
this.permissions = [];
removeToken();
}
},
},
});
export default useUserStore;