+HeWXH6-L#qhZxcVmiFz9p&pDJ<;RV*FA?^hX@CB(BV&c!|;#X*_(yk
zjjM{N8gdHU*oQH&3c
z&X@;}J1=5{RtrheTGm9Z1dyWl1Z&Rl`+HDH!=(K&utoqQqki
z3b5TYH-Xk*0StG-8M$N7FHRQ^wb$0}IL@17y;(O4OzkX#wwZ=69}qUkT|SL3IFttN
z5GcPIEd%nSN31!=aNIMCVBH9t7p`DtM>6C$lGq{_@m+HhzD)76+u|P@*NI-q(8LBT
zk~e-EBrM{T$`5ac(HA(sX)Kws?1>WnBbFHVowEd#u^Y0q5NX4EE=H!4(y@EKM
z&rNKfb=qz?zwatL@|H|^9DD@x+0NR=EE0f~a`=*~N*eakxEv~Jr7!r1YbpvNEY
zn=IPaz#FMnTjPryzrK7F`uZ1wa$ne#ZtW6?9Np#HFuhOT03qDdg$fTX0{0$?Zquac
zD8@i1dn=nZ4qF;!Uy`1v8}b9_W~^pmvPNIhyeHTu5)|V})1<{Emb4p!rVDAycXGbo
zWBs-x<7k6nFa1f!o~$9jH*!9Mhck=Wi{JlDL)1wm0tlz|e3{%)t-0H*NuQTKZcvdx
zU0ZPggFa69jfnel@Py|Ee1YCv?4R=lK7?;ioBMLU+<0U-@-%7ec(vIR&1sydLGe3BYS4ZUVidx%X!V
zfSV7xL7-6os11ws*SCGcVY>$-1OsWS52x6(afE_WUs&cCp5y1%~g1w2){O~2;P(?^-SlbK#wKin|cDF}x1<+@}BQb#j|9ykVA
z7*{Ee_Cp)Z%_UI@t_{ZLH=Nw_2i@9;IulC}1hY7Qz^aS;)aV(H8g{XmwWVZ<=P
z(^uCbPmaD(R>VNkWV*g=`C2eF&Mv@{$lw%U$URk^+K%7LE9_&)umvwhc{edJk)4&B
z_K1*t^b)E~Q6=R4h;cyw>Rme0xnW1E?PH&pJ;^cj#r|6Jo>%rvz&EP>{dzmbF<>wg
zf)HsOz6hyfz4+J4UDm$@i@O?R4nR;#n_=cb!zG
zTin~bs65e6Dn`M@gc%(e6&eh!7MW?8FP4ni7urq8{MBqAi|>EPZ!brE+r3`C*%2K>
zCVq2
zVrY3t9ArFi2d{KitnnWD)vifEV;zw_$LnIScI!M>XO9;igtBT$+$Z>b{heaX&VKS?
zgE)590$T^A>dna!qu{vBSOC|Fxz;%1Y)q8P{6l!W?^!O*+Ke8qIW`o%yvX1m_NiN1
zvThrRSfUYvd?>Ejj5Ge}Y0Y(~InS1N(1jl|NzZ
z%Jmow*y9yRqLksd{W80Va`rvD1I^>Xte;=U>XlW@560`##4vSCa_gU-+;SJgkH@nR
zAbR0_=|g{yPkBrTgmy<(4L96JWUcHH#7m>p8I%=vt6!zwbSp8aL!j~
z@$2&f!>M?>BHp$yGg-R|MmDtGAeHpsNYvI(F>%Y={}T9m&?6u;CIK^pXlCdP^1@bj
zZXuruOJGDq&E7y}!#>&J_FW+37IH~vhW5~C0b4n>)I(zKUgo<=@bkbrjvJ@$rW!k*|eIPI0LqbQd!#)slAs_MmHP{kgkj6^~Ecf)qm>+2Ay&5n(+
z+3RZmaW&Wja&e6r@f`6%H9{oZ-QoK5n=251PJebh{OiYqBs$p}M17Jak~!?6Z$4M$QDEqqp-@|wnpI2@jYQ#
zp;JbjQ)171!_9zY6@BJ4%wgI7Ga$)_SPCn9kz?GRk90=EimdooD$dyNX}zzD5^2s~
zO85+k8{T{a`g`IBYR*o?68NmbF!8$F+?ULMG`WA#ae9
z7^_F&q@kGn=JvY4{d&Lu&sj0>xQHWF^RV&J(
zhz5K0nFeSsr2^keq1bSkp)zVxVG0iin=t3ZxiFnb)7||$pXp8<@hm(o;Rq#X@d1Cx
z$f`(b-fMfwe#0*bDoi|Yu}}o+h+sV!O^FBlZ^a6cJWn};Iks}>yFHPYFn}%^eIJ~96Ea@^dmx&x;
zb%n*%tB4QiEQHirP)`Gsa4agLBNTzZw6<{t&FC&~T>@tuMABfKAuV~`yGGW4NWHhx!s*18e<5|rq4BqM}0MfIPPFv*aR?ySSfW36ZTFT>$
zZQQLm+^8Db3M@Dcg%}nMc;mwXyST9>CEJ-(vZS6nHl}W1{!ldqMFwHk=a+9+Z;nZR
z#F#1}DY$PPNtG-ABA9g=hN-(RPal`0YALq5f3rwCpt2|ld1v_K}ue=1-PDNxQg(c$M}`|Z%>=vTrk>63?V
zJ%4@1?E|ilURlX%W@#?Oi_V2V7CPs!px#OC=)<;81Yfy|_wy+?Pq?{5~`9G(`;a`}70V9IHA&?0F
z@(2-W(AMi~q
fBJH_)KH#e)Wdg=|qgDU?4uAql$xBv=8wCA7hwhrO
literal 0
HcmV?d00001
diff --git a/frontend/src/assets/styles/tailMain.css b/frontend/src/assets/styles/tailMain.css
index 855175a..56c5d07 100644
--- a/frontend/src/assets/styles/tailMain.css
+++ b/frontend/src/assets/styles/tailMain.css
@@ -3,6 +3,7 @@
@tailwind utilities;
+
/* 申明字体为东方大楷 */
@font-face {
font-family: 'DongFangDaKai';
@@ -16,4 +17,3 @@
font-family: "MFBanHei"; /* Project id 1513211 */
src: url('@/assets/font/MFBanHei.ttf?t=1643094287456') format('truetype');
}
-
diff --git a/frontend/src/constants/storeKey.ts b/frontend/src/constants/storeKey.ts
index a477600..412b47f 100644
--- a/frontend/src/constants/storeKey.ts
+++ b/frontend/src/constants/storeKey.ts
@@ -5,6 +5,9 @@
// 用户信息
export const USER_INFO = 'userInfo'
+// 用户信息
+export const TABS_INFO = 'tabsInfo'
+
// WEB端布局配置
export const STORE_CONFIG = 'storeConfig'
diff --git a/frontend/src/layouts/CnHeader/index.vue b/frontend/src/layouts/CnHeader/index.vue
new file mode 100644
index 0000000..21a79c8
--- /dev/null
+++ b/frontend/src/layouts/CnHeader/index.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
diff --git a/frontend/src/layouts/index.vue b/frontend/src/layouts/index.vue
new file mode 100644
index 0000000..bff62b2
--- /dev/null
+++ b/frontend/src/layouts/index.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 9562623..4faec90 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -1,9 +1,9 @@
import { createApp } from 'vue'
import App from './App.vue'
-// element-plus
-import ElementPlus from 'element-plus'
-import 'element-plus/dist/index.css'
+// element-plus 全局引入图标
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+
// 使用pinia
import pinia from '@/stores'
// 导入路由
@@ -18,11 +18,16 @@ import registerGlobComp from '@/components'
//创建实例
const app = createApp(App)
const setupAll = async () => {
+ //全局注册图标
+ for(const [key, component] of Object.entries(ElementPlusIconsVue)){
+ app.component(key, component)
+ }
+
app
.use(Router) // 使用路由
- .use(ElementPlus) // 使用ele-plus组件
.use(pinia) // 使用pinia
.use(registerGlobComp) // 使用全局自定义组件
+ .use(ElementPlusIconsVue) // 使用element-plus图标
//待路由初始化完毕后,挂载app
diff --git a/frontend/src/router/routerMap.ts b/frontend/src/router/routerMap.ts
index 558b8fd..9a6786e 100644
--- a/frontend/src/router/routerMap.ts
+++ b/frontend/src/router/routerMap.ts
@@ -10,6 +10,18 @@ const constantRouterMap = [
name: 'login',
component: Login,
},
+ {
+ path: '/home',
+ name: 'home',
+ component: import("@/layouts/index.vue"),
+ children: [
+ {
+ path: '/home',
+ name: 'home',
+ component: () => import("@/views/Home.vue"),
+ },
+ ],
+ },
]
export default constantRouterMap
diff --git a/frontend/src/stores/interface/index.ts b/frontend/src/stores/interface/index.ts
new file mode 100644
index 0000000..d6b8ef9
--- /dev/null
+++ b/frontend/src/stores/interface/index.ts
@@ -0,0 +1,62 @@
+export type LayoutType = "vertical" | "classic" | "transverse" | "columns";
+
+export type AssemblySizeType = "large" | "default" | "small";
+
+export type LanguageType = "zh" | "en" | null;
+
+/* GlobalState */
+export interface GlobalState {
+ layout: LayoutType;
+ assemblySize: AssemblySizeType;
+ language: LanguageType;
+ maximize: boolean;
+ primary: string;
+ isDark: boolean;
+ isGrey: boolean;
+ isWeak: boolean;
+ asideInverted: boolean;
+ headerInverted: boolean;
+ isCollapse: boolean;
+ accordion: boolean;
+ watermark: boolean;
+ breadcrumb: boolean;
+ breadcrumbIcon: boolean;
+ tabs: boolean;
+ tabsIcon: boolean;
+ footer: boolean;
+}
+
+/* UserState */
+export interface UserState {
+ token: string;
+ userInfo: { name: string };
+}
+
+/* tabsMenuProps */
+export interface TabsMenuProps {
+ icon: string;
+ title: string;
+ path: string;
+ name: string;
+ close: boolean;
+ isKeepAlive: boolean;
+}
+
+/* TabsState */
+export interface TabsState {
+ tabsMenuList: TabsMenuProps[];
+}
+
+/* AuthState */
+export interface AuthState {
+ routeName: string;
+ authButtonList: {
+ [key: string]: string[];
+ };
+ authMenuList: Menu.MenuOptions[];
+}
+
+/* KeepAliveState */
+export interface KeepAliveState {
+ keepAliveName: string[];
+}
diff --git a/frontend/src/stores/user/index.ts b/frontend/src/stores/modules/user/index.ts
similarity index 100%
rename from frontend/src/stores/user/index.ts
rename to frontend/src/stores/modules/user/index.ts
diff --git a/frontend/src/theme/index.scss b/frontend/src/theme/index.scss
new file mode 100644
index 0000000..22a98cf
--- /dev/null
+++ b/frontend/src/theme/index.scss
@@ -0,0 +1,22 @@
+@forward "element-plus/theme-chalk/src/common/var.scss" with (
+ $colors: (
+ "primary": (//主题色
+ "base": #003078,
+ ),
+ "success": (//成功色
+ "base": #67C23A,
+ ),
+ "warning": (//警告色
+ "base": #E6A23C,
+ ),
+ "danger": (//危险色
+ "base": #F56C6C,
+ ),
+ "error": (//错误色
+ "base": #FF4949,
+ ),
+ "info": (//信息色
+ "base": #409EFF,
+ ),
+ ),
+);
diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts
index 7bd63d6..f7b0abe 100644
--- a/frontend/src/types/global.d.ts
+++ b/frontend/src/types/global.d.ts
@@ -31,3 +31,78 @@ interface ApiResponse {
}
type ApiPromise = Promise>
+
+/* Menu */
+declare namespace Menu {
+ interface MenuOptions {
+ path: string;
+ name: string;
+ component?: string | (() => Promise);
+ redirect?: string;
+ meta: MetaProps;
+ children?: MenuOptions[];
+ }
+ interface MetaProps {
+ icon: string;
+ title: string;
+ activeMenu?: string;
+ isLink?: string;
+ isHide: boolean;
+ isFull: boolean;
+ isAffix: boolean;
+ isKeepAlive: boolean;
+ }
+}
+
+/* FileType */
+declare namespace File {
+ type ImageMimeType =
+ | "image/apng"
+ | "image/bmp"
+ | "image/gif"
+ | "image/jpeg"
+ | "image/pjpeg"
+ | "image/png"
+ | "image/svg+xml"
+ | "image/tiff"
+ | "image/webp"
+ | "image/x-icon";
+
+ type ExcelMimeType = "application/vnd.ms-excel" | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+}
+
+/* Vite */
+declare type Recordable = Record;
+
+declare interface ViteEnv {
+ VITE_USER_NODE_ENV: "development" | "production" | "test";
+ VITE_GLOB_APP_TITLE: string;
+ VITE_PORT: number;
+ VITE_OPEN: boolean;
+ VITE_REPORT: boolean;
+ VITE_ROUTER_MODE: "hash" | "history";
+ VITE_BUILD_COMPRESS: "gzip" | "brotli" | "gzip,brotli" | "none";
+ VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
+ VITE_DROP_CONSOLE: boolean;
+ VITE_PWA: boolean;
+ VITE_DEVTOOLS: boolean;
+ VITE_PUBLIC_PATH: string;
+ VITE_API_URL: string;
+ VITE_PROXY: [string, string][];
+}
+
+interface ImportMetaEnv extends ViteEnv {
+ __: unknown;
+}
+
+/* __APP_INFO__ */
+declare const __APP_INFO__: {
+ pkg: {
+ name: string;
+ version: string;
+ dependencies: Recordable;
+ devDependencies: Recordable;
+ };
+ lastBuildTime: string;
+};
+
diff --git a/frontend/src/types/utils.d.ts b/frontend/src/types/utils.d.ts
new file mode 100644
index 0000000..8476216
--- /dev/null
+++ b/frontend/src/types/utils.d.ts
@@ -0,0 +1,17 @@
+type ObjToKeyValUnion = {
+ [K in keyof T]: { key: K; value: T[K] };
+}[keyof T];
+
+type ObjToKeyValArray = {
+ [K in keyof T]: [K, T[K]];
+}[keyof T];
+
+type ObjToSelectedValueUnion = {
+ [K in keyof T]: T[K];
+}[keyof T];
+
+type Optional = Omit & Partial>;
+
+type GetOptional = {
+ [P in keyof T as T[P] extends Required[P] ? never : P]: T[P];
+};
diff --git a/frontend/src/types/window.d.ts b/frontend/src/types/window.d.ts
new file mode 100644
index 0000000..1582726
--- /dev/null
+++ b/frontend/src/types/window.d.ts
@@ -0,0 +1,8 @@
+declare global {
+ interface Navigator {
+ msSaveOrOpenBlob: (blob: Blob, fileName: string) => void;
+ browserLanguage: string;
+ }
+}
+
+export {};
diff --git a/frontend/src/utils/http/index.ts b/frontend/src/utils/http/index.ts
index c8bcf59..2e969c9 100644
--- a/frontend/src/utils/http/index.ts
+++ b/frontend/src/utils/http/index.ts
@@ -3,7 +3,7 @@ import axios from 'axios'
import { ElLoading, ElNotification, type LoadingOptions } from 'element-plus'
import { refreshToken } from '@/api/user'
import router from '@/router/index'
-import { useUserInfoStore } from '@/stores/user'
+import { useUserInfoStore } from '@/stores/modules/user'
window.requests = []
window.tokenRefreshing = false
diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts
new file mode 100644
index 0000000..e1eb997
--- /dev/null
+++ b/frontend/src/utils/index.ts
@@ -0,0 +1,288 @@
+import { isArray } from "@/utils/is";
+
+const mode = import.meta.env.VITE_ROUTER_MODE;
+
+/**
+ * @description 获取localStorage
+ * @param {String} key Storage名称
+ * @returns {String}
+ */
+export function localGet(key: string) {
+ const value = window.localStorage.getItem(key);
+ try {
+ return JSON.parse(window.localStorage.getItem(key) as string);
+ } catch (error) {
+ return value;
+ }
+}
+
+/**
+ * @description 存储localStorage
+ * @param {String} key Storage名称
+ * @param {*} value Storage值
+ * @returns {void}
+ */
+export function localSet(key: string, value: any) {
+ window.localStorage.setItem(key, JSON.stringify(value));
+}
+
+/**
+ * @description 清除localStorage
+ * @param {String} key Storage名称
+ * @returns {void}
+ */
+export function localRemove(key: string) {
+ window.localStorage.removeItem(key);
+}
+
+/**
+ * @description 清除所有localStorage
+ * @returns {void}
+ */
+export function localClear() {
+ window.localStorage.clear();
+}
+
+/**
+ * @description 判断数据类型
+ * @param {*} val 需要判断类型的数据
+ * @returns {String}
+ */
+export function isType(val: any) {
+ if (val === null) return "null";
+ if (typeof val !== "object") return typeof val;
+ else return Object.prototype.toString.call(val).slice(8, -1).toLocaleLowerCase();
+}
+
+/**
+ * @description 生成唯一 uuid
+ * @returns {String}
+ */
+export function generateUUID() {
+ let uuid = "";
+ for (let i = 0; i < 32; i++) {
+ let random = (Math.random() * 16) | 0;
+ if (i === 8 || i === 12 || i === 16 || i === 20) uuid += "-";
+ uuid += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16);
+ }
+ return uuid;
+}
+
+/**
+ * 判断两个对象是否相同
+ * @param {Object} a 要比较的对象一
+ * @param {Object} b 要比较的对象二
+ * @returns {Boolean} 相同返回 true,反之 false
+ */
+export function isObjectValueEqual(a: { [key: string]: any }, b: { [key: string]: any }) {
+ if (!a || !b) return false;
+ let aProps = Object.getOwnPropertyNames(a);
+ let bProps = Object.getOwnPropertyNames(b);
+ if (aProps.length != bProps.length) return false;
+ for (let i = 0; i < aProps.length; i++) {
+ let propName = aProps[i];
+ let propA = a[propName];
+ let propB = b[propName];
+ if (!b.hasOwnProperty(propName)) return false;
+ if (propA instanceof Object) {
+ if (!isObjectValueEqual(propA, propB)) return false;
+ } else if (propA !== propB) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * @description 生成随机数
+ * @param {Number} min 最小值
+ * @param {Number} max 最大值
+ * @returns {Number}
+ */
+export function randomNum(min: number, max: number): number {
+ let num = Math.floor(Math.random() * (min - max) + max);
+ return num;
+}
+
+/**
+ * @description 获取当前时间对应的提示语
+ * @returns {String}
+ */
+export function getTimeState() {
+ let timeNow = new Date();
+ let hours = timeNow.getHours();
+ if (hours >= 6 && hours <= 10) return `早上好 ⛅`;
+ if (hours >= 10 && hours <= 14) return `中午好 🌞`;
+ if (hours >= 14 && hours <= 18) return `下午好 🌞`;
+ if (hours >= 18 && hours <= 24) return `晚上好 🌛`;
+ if (hours >= 0 && hours <= 6) return `凌晨好 🌛`;
+}
+
+/**
+ * @description 获取浏览器默认语言
+ * @returns {String}
+ */
+export function getBrowserLang() {
+ let browserLang = navigator.language ? navigator.language : navigator.browserLanguage;
+ let defaultBrowserLang = "";
+ if (["cn", "zh", "zh-cn"].includes(browserLang.toLowerCase())) {
+ defaultBrowserLang = "zh";
+ } else {
+ defaultBrowserLang = "en";
+ }
+ return defaultBrowserLang;
+}
+
+/**
+ * @description 获取不同路由模式所对应的 url + params
+ * @returns {String}
+ */
+export function getUrlWithParams() {
+ const url = {
+ hash: location.hash.substring(1),
+ history: location.pathname + location.search
+ };
+ return url[mode];
+}
+
+/**
+ * @description 使用递归扁平化菜单,方便添加动态路由
+ * @param {Array} menuList 菜单列表
+ * @returns {Array}
+ */
+export function getFlatMenuList(menuList: Menu.MenuOptions[]): Menu.MenuOptions[] {
+ let newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList));
+ return newMenuList.flatMap(item => [item, ...(item.children ? getFlatMenuList(item.children) : [])]);
+}
+
+/**
+ * @description 使用递归过滤出需要渲染在左侧菜单的列表 (需剔除 isHide == true 的菜单)
+ * @param {Array} menuList 菜单列表
+ * @returns {Array}
+ * */
+export function getShowMenuList(menuList: Menu.MenuOptions[]) {
+ let newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList));
+ return newMenuList.filter(item => {
+ item.children?.length && (item.children = getShowMenuList(item.children));
+ return !item.meta?.isHide;
+ });
+}
+
+/**
+ * @description 使用递归找出所有面包屑存储到 pinia/vuex 中
+ * @param {Array} menuList 菜单列表
+ * @param {Array} parent 父级菜单
+ * @param {Object} result 处理后的结果
+ * @returns {Object}
+ */
+export const getAllBreadcrumbList = (menuList: Menu.MenuOptions[], parent = [], result: { [key: string]: any } = {}) => {
+ for (const item of menuList) {
+ result[item.path] = [...parent, item];
+ if (item.children) getAllBreadcrumbList(item.children, result[item.path], result);
+ }
+ return result;
+};
+
+/**
+ * @description 使用递归处理路由菜单 path,生成一维数组 (第一版本地路由鉴权会用到,该函数暂未使用)
+ * @param {Array} menuList 所有菜单列表
+ * @param {Array} menuPathArr 菜单地址的一维数组 ['**','**']
+ * @returns {Array}
+ */
+export function getMenuListPath(menuList: Menu.MenuOptions[], menuPathArr: string[] = []): string[] {
+ for (const item of menuList) {
+ if (typeof item === "object" && item.path) menuPathArr.push(item.path);
+ if (item.children?.length) getMenuListPath(item.children, menuPathArr);
+ }
+ return menuPathArr;
+}
+
+/**
+ * @description 递归查询当前 path 所对应的菜单对象 (该函数暂未使用)
+ * @param {Array} menuList 菜单列表
+ * @param {String} path 当前访问地址
+ * @returns {Object | null}
+ */
+export function findMenuByPath(menuList: Menu.MenuOptions[], path: string): Menu.MenuOptions | null {
+ for (const item of menuList) {
+ if (item.path === path) return item;
+ if (item.children) {
+ const res = findMenuByPath(item.children, path);
+ if (res) return res;
+ }
+ }
+ return null;
+}
+
+/**
+ * @description 使用递归过滤需要缓存的菜单 name (该函数暂未使用)
+ * @param {Array} menuList 所有菜单列表
+ * @param {Array} keepAliveNameArr 缓存的菜单 name ['**','**']
+ * @returns {Array}
+ * */
+export function getKeepAliveRouterName(menuList: Menu.MenuOptions[], keepAliveNameArr: string[] = []) {
+ menuList.forEach(item => {
+ item.meta.isKeepAlive && item.name && keepAliveNameArr.push(item.name);
+ item.children?.length && getKeepAliveRouterName(item.children, keepAliveNameArr);
+ });
+ return keepAliveNameArr;
+}
+
+/**
+ * @description 格式化表格单元格默认值 (el-table-column)
+ * @param {Number} row 行
+ * @param {Number} col 列
+ * @param {*} callValue 当前单元格值
+ * @returns {String}
+ * */
+export function formatTableColumn(row: number, col: number, callValue: any) {
+ // 如果当前值为数组,使用 / 拼接(根据需求自定义)
+ if (isArray(callValue)) return callValue.length ? callValue.join(" / ") : "--";
+ return callValue ?? "--";
+}
+
+/**
+ * @description 处理 ProTable 值为数组 || 无数据
+ * @param {*} callValue 需要处理的值
+ * @returns {String}
+ * */
+export function formatValue(callValue: any) {
+ // 如果当前值为数组,使用 / 拼接(根据需求自定义)
+ if (isArray(callValue)) return callValue.length ? callValue.join(" / ") : "--";
+ return callValue ?? "--";
+}
+
+/**
+ * @description 处理 prop 为多级嵌套的情况,返回的数据 (列如: prop: user.name)
+ * @param {Object} row 当前行数据
+ * @param {String} prop 当前 prop
+ * @returns {*}
+ * */
+export function handleRowAccordingToProp(row: { [key: string]: any }, prop: string) {
+ if (!prop.includes(".")) return row[prop] ?? "--";
+ prop.split(".").forEach(item => (row = row[item] ?? "--"));
+ return row;
+}
+
+/**
+ * @description 处理 prop,当 prop 为多级嵌套时 ==> 返回最后一级 prop
+ * @param {String} prop 当前 prop
+ * @returns {String}
+ * */
+export function handleProp(prop: string) {
+ const propArr = prop.split(".");
+ if (propArr.length == 1) return prop;
+ return propArr[propArr.length - 1];
+}
+
+
+/**
+ * @description 递归查找 callValue 对应的 enum 值
+ * */
+export function findItemNested(enumData: any, callValue: any, value: string, children: string) {
+ return enumData.reduce((accumulator: any, current: any) => {
+ if (accumulator) return accumulator;
+ if (current[value] === callValue) return current;
+ if (current[children]) return findItemNested(current[children], callValue, value, children);
+ }, null);
+}
diff --git a/frontend/src/utils/is/index.ts b/frontend/src/utils/is/index.ts
new file mode 100644
index 0000000..5678491
--- /dev/null
+++ b/frontend/src/utils/is/index.ts
@@ -0,0 +1,125 @@
+/**
+ * @description: 判断值是否未某个类型
+ */
+export function is(val: unknown, type: string) {
+ return Object.prototype.toString.call(val) === `[object ${type}]`;
+}
+
+/**
+ * @description: 是否为函数
+ */
+export function isFunction(val: unknown): val is T {
+ return is(val, "Function");
+}
+
+/**
+ * @description: 是否已定义
+ */
+export const isDef = (val?: T): val is T => {
+ return typeof val !== "undefined";
+};
+
+/**
+ * @description: 是否未定义
+ */
+export const isUnDef = (val?: T): val is T => {
+ return !isDef(val);
+};
+
+/**
+ * @description: 是否为对象
+ */
+export const isObject = (val: any): val is Record => {
+ return val !== null && is(val, "Object");
+};
+
+/**
+ * @description: 是否为时间
+ */
+export function isDate(val: unknown): val is Date {
+ return is(val, "Date");
+}
+
+/**
+ * @description: 是否为数值
+ */
+export function isNumber(val: unknown): val is number {
+ return is(val, "Number");
+}
+
+/**
+ * @description: 是否为AsyncFunction
+ */
+export function isAsyncFunction(val: unknown): val is Promise {
+ return is(val, "AsyncFunction");
+}
+
+/**
+ * @description: 是否为promise
+ */
+export function isPromise(val: unknown): val is Promise {
+ return is(val, "Promise") && isObject(val) && isFunction(val.then) && isFunction(val.catch);
+}
+
+/**
+ * @description: 是否为字符串
+ */
+export function isString(val: unknown): val is string {
+ return is(val, "String");
+}
+
+/**
+ * @description: 是否为boolean类型
+ */
+export function isBoolean(val: unknown): val is boolean {
+ return is(val, "Boolean");
+}
+
+/**
+ * @description: 是否为数组
+ */
+export function isArray(val: any): val is Array {
+ return val && Array.isArray(val);
+}
+
+/**
+ * @description: 是否客户端
+ */
+export const isClient = () => {
+ return typeof window !== "undefined";
+};
+
+/**
+ * @description: 是否为浏览器
+ */
+export const isWindow = (val: any): val is Window => {
+ return typeof window !== "undefined" && is(val, "Window");
+};
+
+/**
+ * @description: 是否为 element 元素
+ */
+export const isElement = (val: unknown): val is Element => {
+ return isObject(val) && !!val.tagName;
+};
+
+/**
+ * @description: 是否为 null
+ */
+export function isNull(val: unknown): val is null {
+ return val === null;
+}
+
+/**
+ * @description: 是否为 null || undefined
+ */
+export function isNullOrUnDef(val: unknown): val is null | undefined {
+ return isUnDef(val) || isNull(val);
+}
+
+/**
+ * @description: 是否为 16 进制颜色
+ */
+export const isHexColor = (str: string) => {
+ return /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(str);
+};
diff --git a/frontend/src/utils/logout/index.ts b/frontend/src/utils/logout/index.ts
index ddd2c64..256342a 100644
--- a/frontend/src/utils/logout/index.ts
+++ b/frontend/src/utils/logout/index.ts
@@ -1,4 +1,4 @@
-import { useUserInfoStore } from '@/stores/user'
+import { useUserInfoStore } from '@/stores/modules/user'
import { USER_INFO } from '@/constants/storeKey'
export function clearUserInfo() {
diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue
index b3fe48b..f42c6c4 100644
--- a/frontend/src/views/Login.vue
+++ b/frontend/src/views/Login.vue
@@ -38,7 +38,7 @@ import { ref, onMounted } from "vue";
const router = useRouter();
import { useRouter } from "vue-router";
import { Avatar, Lock } from "@element-plus/icons-vue";
-import { useUserInfoStore } from "@/stores/user";
+import { useUserInfoStore } from "@/stores/modules/user";
const userInfoStore = useUserInfoStore();
@@ -61,12 +61,12 @@ const handleLogin = () => {
loginLoading.value = true;
if (LoginForm.value.loginUserName && LoginForm.value.loginUserPassword) {
setTimeout(() => {
- // router.push({
- // path: "/admin",
- // query: {
- // name: LoginForm.value.loginUserName,
- // },
- // });
+ router.push({
+ path: "/home",
+ query: {
+ name: LoginForm.value.loginUserName,
+ },
+ });
userInfoStore.dataFill({ loginName: LoginForm.value.loginUserName });
console.log(userInfoStore.loginName)
}, 1500);
diff --git a/frontend/src/views/dashboard/index.vue b/frontend/src/views/dashboard/index.vue
new file mode 100644
index 0000000..9928094
--- /dev/null
+++ b/frontend/src/views/dashboard/index.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index fd178d6..200c659 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -3,6 +3,9 @@ import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import vue from '@vitejs/plugin-vue'
import path from 'path'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig((config) => {
return {
@@ -13,6 +16,16 @@ export default defineConfig((config) => {
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]',
}),
+ AutoImport({
+ resolvers: [ElementPlusResolver({
+ importStyle: "sass",
+ })]
+ }),
+ Components({
+ resolvers: [ElementPlusResolver({
+ importStyle: "sass",
+ })]
+ }),
],
// 基础配置
base: './',
@@ -30,6 +43,10 @@ export default defineConfig((config) => {
},
javascriptEnabled: true,
},
+ scss: {
+ // 引入index.scss覆盖文件
+ additionalData: `@use "@/theme/index.scss" as *;`,
+ }
},
},
build: {