%P$NlNy~(lyM;g
zS2ge;6tm=-hnF{Uq_Fk9)L1z8R8rIZfljKr6}5qt^i`*-7n5fBtGVW3+5P&KesERK
zR$O71nOKqg^64Oxc~Z|H=y8x7avB|{tnBO%>$uc4(c
z&!TRpzAm%$UsIb<`EpOU!)-2H@rRH1T}UD`bib
zPPs`A{9Z0Y-&JE*m0Ps`r8MQ$4wCls!Gy|ru&vv-r~xFc7#dM78uRh|1OKPa)$RTXa?Z~dONYNWO4e$ykS`vuF3
z14rogex-bGVw#0=?Zwo47MrS91&+TQxp4Bbkzf39bWZ(6cn4Jus!c6$2>bp`KbJ^T
zwvfy(>!Q&)8d|^5R$Fz)$nqRF=GSxZY?1RgA{okl08i!g&BSs^aH(g5kGi)sGd+V?
zdX#N{Rh^bKUGm%juyM}jSFHnr3zw9y;{2AuquAWXGmo^m=&QNYi(XzSFKzvMYabye
zWnuZvYp?(QlaHJM;3Aq2ez8{P#{pJs8m3(pkyw*cWp!m6hbQfBM`eCQ&RP}#iuHX(
z@p%8^b#R>cBgZtBnXx1TdNaKW>V6nk(v4N!3U60o)ga$njmJG*2jy+!`nB8#W*=8R
z+xEtb1+L0luF+WDK34biaci>_r{#6#3pJK=kJXh9%KPTk(`^h)T~amaciMi
zXKdvbpU4G^=R7T-_T0?b$JHzWo|*ffguQfrA%XQF%pEs~EibyxGKJk*D`w~nfP7lx
zPk`cDhL~{6OjPY#qd{o2?j}nALVLNxt=OeTrQxbcqkTWOD)x_@U%?>
zV(~23)lppKEfYz{MayaqOwjM<%zK!LWPgMFaPGQSJDCXU*(OSHXZ^U-H4(a#4RQvS
zTMt22Uzfx((|R0GWW<;IIAmXItSETEoOTYLGZG6EE{%{Ey~j*R|CU(hyA`a)+8}%a
zEav9DnHAl8wN&et`I3$g%j#am`S~O`CWbStHNE8;;a!pqJ4)SvcXCaUm-Bu_%J7a%
zmnvIgYFQU}WVvnOH>YzivdZ%USl#I2$N+t~69gv*vyi9&bTxAhZ7))!Eann=g8qrA
z3(J4@{APdS-M&2PGP(=0=lY+%_*GZGO#f%VA83}C3fg!a%>8<9VSi>7TwRgZvunH7
zU*27H8fp9;WD2dvlc-K@sq!G~*}INd`g@IM+Kd=POs`I{!hjbh!0ZF1gHo;>SgqeU
z6^X53=^{-hBkOzr^T8V=
z^-0O_R=-}6YnXgRI63-U%c`n>
zy}PG1b&os(CzXI}RNK5P~bN&P@uEc>d*QrIoew-aRPj
zdmiW9wSgzq)aI6Rf4f`Nu5gQdA8oaSNF`}UOJ){tI@{KoOk*9s%7Q6*Ljf^u92Ows}F=QS@F~-wkiFn
z9Y#ZXHcZTS`vh@O3)y!&4N^WRHw{j4yhb3MFV<^@L&HbW)Kt-+}D%F6N0e{RMY
z6+^&yAtlalvP0im7G?t|&AlguCyV;0gkaFC08YCzbq_waj7;%HZT~YAXtspEofDTZ
zj_2N9SP+SS350*G>sQ-nTD^Lvp$6D(q20OA{lgcY?>%Et?{7tojU2fB+B1#cB&wN8
z26~X|*Zn3XI5z)@YP--J5G>6^_=fJ+qRE!n{DUHKV~Yau988tc
z_h6};z8>VN1{OwjJnW<2jYUo4N@cVh-*lR0F7Jc$C?QoZXy(%1*_hAbG76^UeV^p_
zFFg*+Z|Fc(6yK+^pZ-!5_TK7L2ZUky5RjPvaZS_e-j_yntj$7<=R!IV&B(%GKR}^<
zv;oUr;s9#PPmdxyBtW?;SiKeNp0J0iuQ-x%>G3T6F9$(6LK1gSz3o>lE?*nP#GavA
z$K0Kb^+Hc5qdD=l7IU5V&pyrTC+!-^U=
z?fmyco$vkZi{|NA26oes*M@`QbMSjf>ja8y^`i;V34;@vT5uI_O2no^+plfZu{GrU
z2H;M*^~5rN{%hLE;EmO^;a9m9-STk&s7sWSDa$37H-e6YOr8oj3w4ZK@gA5$=1xb^
znqJh3hZ(pkiqoktD@yI2+*?E-q-
z*!cEUaW}_NRk8EC68LM;bp$KY&qO_QYfOc4P#ZR5gecZkUV8jSj73xRb0R5L&2hDz
z>LX3(Wz&tj;Ar^d-QTh_VmiB$?vK>JJ;lRVgS=EIU(m7B963p=`Z3Q
zt9AV_t&CVZ)Eud(XUaw2^HbpAcCbM8Ible_ZuT?Z!^ZhOAVbI)gWWd(VZ`20E=
zL1Jpx?bkL}I3b+(&QQ@h#|A{@-F%v$!cy20Latxj9~R#?aS`jHwLsW&BdQ$kZrFzT
z?tSVBO+e){x0gd`Nf(<>P(Lhr{G%saYqMQ?I%4Tj_M>Mh{kDeYx^h5{&=JDwvCom^
zJzCgKwg`5+gy=tA_w1y(qc5VF51Tc}jCJ)>A2tojJBijVE9+SSUaN&1-CV&^Ksk~k
zDU|GV-al+t1`{xnhS!a>x360J1ZQuf!AURnAa{IA)A(DXA}xvn=hIw~v#$Y9ic7X4
zI96S&VxJU=Fo#qjwm#dOOFC1Z<#&rQ
zd%8;SCpC|FV{n~J>F)K9t~$~kj$WuP36VW@q{Fbdkqq?(-Fc=w_Aq)4CtBZyr-
zrJ%hr*Cx^DZWf=jXsxqe@CH=L$0-%GN~qr1vYQmWD`jAJ+TA1<6KyZut&lB0w9|7D
zdSUnLnRqe7T0`+`2Vn%Q52}3=K?I@42gs|@c#X0bYxS1tIORWdt3<|F(GMhq5h#9C
zp&9$URy!YOZFY^s7#NCCek-vn-8$KQPj$Ka5i_gq+3I@_&>?SZ*X->1LY<7?xMsG
zAH*y86(fw3bOc%-7bwrcDjP~8UN^yKE%B1bd^A#3YWwVqHCeAP*Dc|21aoSGlI;;$*1oZ6GrwUL
z1|fJ=jkafet~3=G=$gbg&%VIqdbpiNN`BwN6VwaP
z_!h+b>E)Ghzkv=&MdO$K-b67FZX?U0G2!J*DKFMe=nGJyJD|0(L2
z*vwistQA*lIv1rU+9UOxxofj7Uj%aE0jD~^#;eVoqjH08jEQo3=UP*+jksVEre6t4
zet_D#U{0l1LY^~M?V;xfa_&+<1yGBd5%jw*lD2y=7tS>dIU5&7*}=0ab^U+`gYrz#
zSZ}&zBlk|T>Ho&aN$HoC#iR{jqxa$M1k;6{Tmh5Tk!s*YsKb&rQb$wBV+a4XdKb`gg(q!
z?S-jy6(i2B3`zabt=fLW2IXPes7|3~KVS4wCsUvVcT^{In{y6*4HV$RE$F?U^j>f_
zzTQ5jM?N^J+(Di8!sQjzu)Mdgc3@~eM!UB>0lP&QApYH`Ioc&?j>?mX+R4LEaL16i
zUt|W^2zC;9lcc;Mj!)7!)~2Ar{UK~OU4%A)+dv?Dyfla|kaif4{uz)r=umnw2uK9l
zPvC}(M$O}IMwM?vZfYD)y*xqP2@QRY(qC>nx*!q^C3eh|J(WC{wIh-K)B{@>6n%wAO;>;(}b
zc;BR>$4;69sdW?;N*2Rsk(g7e5(*h9VRG)_htRsk!y@T>BOiMg5lP`V4_AvIvoyXy
z!1NSaQRC_bY!rjHXz3Nm9M_wvxMOR)?k9zr!Wa}dCsCuiBz
z-l)vhtZl%DoI}6qhC)5>dXaYArAKurMO-LCQG7L=`!OHaMB)G?4I@eeZ*(xJ1t*DS
zNc|!R>Kg|0GIW;Kf$@q&173uBpLNEkVrEhIvv^Ff|#MApstRL60S8w$TvMBqEGe`2%^zreP(_AB909im3DP`!jj?
zDf=NJ5!r}H8fJICZ_X+jCNjqN$`HO1St4GVsxVB6AT)SM)l|+YKNSW0=o!)iy2Xmo
zK0`+~18SZ^HZV~86ZgfTcccH5J
zL)pY+_(THN&KLL(Ni@A#xQ#+oZ>&X5sy0Mo*&XHCI1T0Wkm+>j1b%Dk$wKqJS{X$m
z2iZTp02z)_AF`mczodqV{EjfB|9X+8seUx$0Smr6R(y|KoSC&F
zo2r+?Ku|NV>zFHgqOxLV#%JRKEfg{x`=0zu$-WNTjaH2jX
zE=)pL06HGt`JpfU<_f-~>YbU&d!(1=`J(>9p>aHt8Au&eKv0_;dW9s596N&k2
zoJfV3G9lZT5InYIZiaBj%|SF^tRT5%BisLV
z$o)ls4JZI#572URXu2;nNGwgy^2#n0GK!h9M>pR=JFl$MWcNM;dpKW;;<6V?sN^K=4AEE%dk=p}kD
z2bXaDuq+|F3rduP0lVeSS*kli<-Z;_{Pf6w72{LetAzfeD%DGzG|dqw9mBY=TT2h4
zgud#)?xNQlzL)dIa1*}8#8+Ko^G4&mF>KxjHV@Y>?T}$KinL+qnMpiyRma#gCngR7
zw%f?2v4wzz>oqoVX%`Luy_gcJrLcki5Sspj(dHgWG~d}#k!)cr!!#5&6esV6mmIKf
zLs#lB)kc*2b+6#;FCl~J`xNwUxqz`f1D&Uxfs^S~JM40rJBj?TBf0{T!h~9{~5iv7w;NXzj?@DKPu6=@IV#5WQtrKrGa_uKuf>2
z02LRpk$RDO@GAcMKy4hoog@BTkl@dqSunym^m2gmOwS{Wn6tk!rPaWnUd0pP12tmM
z1$A7MzW{aQbc5T|p~xBZ{)=ckZnoNt<<5>J#VD*&TDEMmuhFFuX4>D3J&UB>7!iAa
zMHubiZ3}E4^$OL%OAPX#?!oRqQ$_G-PWJwmLjlJ}4WjbJ*k)&VGG-~gw894*0OBmY
ziy?OO4Fl-9qGNtzl7GfeAE#m2@c&NI9uNDfh*wCtk5RJlgD=>D75%foiUptOqYkVB
zL$|*f!gp&20{to0!x_WQ9$i3zo`&Z-|9>NZZ6OVt77dXLc4juRPTzd>>*-)J^@Puj
zz||JZ|1+exc6ylB@NX>EW9`m*sHC#tVTvHmI*{3r%kUYd0mf
z#%R#|*jSjr5`spB^4cQwE6&Kfh2~=TiZixjEMvH4U7&Y+B!*M5+@PZApB2KOyjG4O
zhrvL*yQ7jJJ1b9p0Xhpk5R~6sFmN!^DbYkES1&`Tkc9SfO@F>dIc{Mj$LS82acNI-
zsU68H{Aiu+Qh6{f&L2?Ei=Cmna=nhYF=@}Jj44MF>bmFkjX@|s4T8zot)lk
z_Cd4S(^C?TK@G0$BN$9?&?pOPH~nKq(O=A$iiYC)Gqkey`X9PL7gL@5{xe;B1QTTO
zeJCD>I{bIv!#Ec%3>4uv(^c1r1_<*2{NU{17t<4daKikN-sliaYhyS4^^dc~(#FDW
zG=lWTe?Eh8NN4>|0kn^C@<7+~IsdHzB>$|Tk%5sc;|%qBG1dn<{?9^special_event
\ No newline at end of file
diff --git a/src/assets/status/403.svg b/src/assets/status/403.svg
new file mode 100644
index 0000000..97a10b0
--- /dev/null
+++ b/src/assets/status/403.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/status/404.svg b/src/assets/status/404.svg
new file mode 100644
index 0000000..137faca
--- /dev/null
+++ b/src/assets/status/404.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/status/500.svg b/src/assets/status/500.svg
new file mode 100644
index 0000000..138aeac
--- /dev/null
+++ b/src/assets/status/500.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/back_top.svg b/src/assets/svg/back_top.svg
new file mode 100644
index 0000000..3c75ca8
--- /dev/null
+++ b/src/assets/svg/back_top.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/dark.svg b/src/assets/svg/dark.svg
new file mode 100644
index 0000000..421d28c
--- /dev/null
+++ b/src/assets/svg/dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/day.svg b/src/assets/svg/day.svg
new file mode 100644
index 0000000..debccce
--- /dev/null
+++ b/src/assets/svg/day.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/enter_outlined.svg b/src/assets/svg/enter_outlined.svg
new file mode 100644
index 0000000..ad3f939
--- /dev/null
+++ b/src/assets/svg/enter_outlined.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/exit_screen.svg b/src/assets/svg/exit_screen.svg
new file mode 100644
index 0000000..d14a678
--- /dev/null
+++ b/src/assets/svg/exit_screen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/full_screen.svg b/src/assets/svg/full_screen.svg
new file mode 100644
index 0000000..ebb1111
--- /dev/null
+++ b/src/assets/svg/full_screen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/keyboard_esc.svg b/src/assets/svg/keyboard_esc.svg
new file mode 100644
index 0000000..8008fdf
--- /dev/null
+++ b/src/assets/svg/keyboard_esc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/ReAuth/index.ts b/src/components/ReAuth/index.ts
new file mode 100644
index 0000000..975ed2c
--- /dev/null
+++ b/src/components/ReAuth/index.ts
@@ -0,0 +1,5 @@
+import auth from "./src/auth";
+
+const Auth = auth;
+
+export { Auth };
diff --git a/src/components/ReAuth/src/auth.tsx b/src/components/ReAuth/src/auth.tsx
new file mode 100644
index 0000000..d2cf9b3
--- /dev/null
+++ b/src/components/ReAuth/src/auth.tsx
@@ -0,0 +1,20 @@
+import { defineComponent, Fragment } from "vue";
+import { hasAuth } from "@/router/utils";
+
+export default defineComponent({
+ name: "Auth",
+ props: {
+ value: {
+ type: undefined,
+ default: []
+ }
+ },
+ setup(props, { slots }) {
+ return () => {
+ if (!slots) return null;
+ return hasAuth(props.value) ? (
+ {slots.default?.()}
+ ) : null;
+ };
+ }
+});
diff --git a/src/components/ReIcon/index.ts b/src/components/ReIcon/index.ts
new file mode 100644
index 0000000..5179723
--- /dev/null
+++ b/src/components/ReIcon/index.ts
@@ -0,0 +1,12 @@
+import iconifyIconOffline from "./src/iconifyIconOffline";
+import iconifyIconOnline from "./src/iconifyIconOnline";
+import fontIcon from "./src/iconfont";
+
+/** 离线图标组件 */
+const IconifyIconOffline = iconifyIconOffline;
+/** 在线图标组件 */
+const IconifyIconOnline = iconifyIconOnline;
+/** iconfont组件 */
+const FontIcon = fontIcon;
+
+export { IconifyIconOffline, IconifyIconOnline, FontIcon };
diff --git a/src/components/ReIcon/src/hooks.ts b/src/components/ReIcon/src/hooks.ts
new file mode 100644
index 0000000..65c4134
--- /dev/null
+++ b/src/components/ReIcon/src/hooks.ts
@@ -0,0 +1,49 @@
+import { iconType } from "./types";
+import { h, defineComponent, Component } from "vue";
+import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
+
+/**
+ * 支持fontawesome4、5+、iconfont、remixicon、element-plus的icons、自定义svg
+ * @param icon 必传 图标
+ * @param attrs 可选 iconType 属性
+ * @returns Component
+ */
+export function useRenderIcon(icon: any, attrs?: iconType): Component {
+ // iconfont
+ const ifReg = /^IF-/;
+ // typeof icon === "function" 属于SVG
+ if (ifReg.test(icon)) {
+ // iconfont
+ const name = icon.split(ifReg)[1];
+ const iconName = name.slice(
+ 0,
+ name.indexOf(" ") == -1 ? name.length : name.indexOf(" ")
+ );
+ const iconType = name.slice(name.indexOf(" ") + 1, name.length);
+ return defineComponent({
+ name: "FontIcon",
+ render() {
+ return h(FontIcon, {
+ icon: iconName,
+ iconType,
+ ...attrs
+ });
+ }
+ });
+ } else if (typeof icon === "function" || typeof icon?.render === "function") {
+ // svg
+ return icon;
+ } else {
+ return defineComponent({
+ name: "Icon",
+ render() {
+ const IconifyIcon =
+ attrs && attrs["online"] ? IconifyIconOnline : IconifyIconOffline;
+ return h(IconifyIcon, {
+ icon: icon,
+ ...attrs
+ });
+ }
+ });
+ }
+}
diff --git a/src/components/ReIcon/src/iconfont.ts b/src/components/ReIcon/src/iconfont.ts
new file mode 100644
index 0000000..c110451
--- /dev/null
+++ b/src/components/ReIcon/src/iconfont.ts
@@ -0,0 +1,48 @@
+import { h, defineComponent } from "vue";
+
+// 封装iconfont组件,默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code)
+export default defineComponent({
+ name: "FontIcon",
+ props: {
+ icon: {
+ type: String,
+ default: ""
+ }
+ },
+ render() {
+ const attrs = this.$attrs;
+ if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") {
+ return h(
+ "i",
+ {
+ class: "iconfont",
+ ...attrs
+ },
+ this.icon
+ );
+ } else if (
+ Object.keys(attrs).includes("svg") ||
+ attrs?.iconType === "svg"
+ ) {
+ return h(
+ "svg",
+ {
+ class: "icon-svg",
+ "aria-hidden": true
+ },
+ {
+ default: () => [
+ h("use", {
+ "xlink:href": `#${this.icon}`
+ })
+ ]
+ }
+ );
+ } else {
+ return h("i", {
+ class: `iconfont ${this.icon}`,
+ ...attrs
+ });
+ }
+ }
+});
diff --git a/src/components/ReIcon/src/iconifyIconOffline.ts b/src/components/ReIcon/src/iconifyIconOffline.ts
new file mode 100644
index 0000000..b55f25b
--- /dev/null
+++ b/src/components/ReIcon/src/iconifyIconOffline.ts
@@ -0,0 +1,88 @@
+import { h, defineComponent } from "vue";
+import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
+
+// element-plus icon
+import Check from "@iconify-icons/ep/check";
+import HomeFilled from "@iconify-icons/ep/home-filled";
+import Lollipop from "@iconify-icons/ep/lollipop";
+import RefreshRight from "@iconify-icons/ep/refresh-right";
+import Close from "@iconify-icons/ep/close";
+import CloseBold from "@iconify-icons/ep/close-bold";
+import Bell from "@iconify-icons/ep/bell";
+import Search from "@iconify-icons/ep/search";
+addIcon("check", Check);
+addIcon("home-filled", HomeFilled);
+addIcon("lollipop", Lollipop);
+addIcon("refresh-right", RefreshRight);
+addIcon("close", Close);
+addIcon("close-bold", CloseBold);
+addIcon("bell", Bell);
+addIcon("search", Search);
+
+// remixicon
+import ArrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
+import ArrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
+import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
+import InformationLine from "@iconify-icons/ri/information-line";
+import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
+import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
+import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
+import User from "@iconify-icons/ri/user-3-fill";
+import Lock from "@iconify-icons/ri/lock-fill";
+import MenuUnfold from "@iconify-icons/ri/menu-unfold-fill";
+import MenuFold from "@iconify-icons/ri/menu-fold-fill";
+import Setting from "@iconify-icons/ri/settings-3-line";
+import ArrowDown from "@iconify-icons/ri/arrow-down-s-line";
+import CloseLeftTags from "@iconify-icons/ri/text-direction-r";
+import CloseRightTags from "@iconify-icons/ri/text-direction-l";
+import CloseOtherTags from "@iconify-icons/ri/text-spacing";
+import CloseAllTags from "@iconify-icons/ri/subtract-line";
+import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
+import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
+addIcon("arrow-right-s-line", ArrowRightSLine);
+addIcon("arrow-left-s-line", ArrowLeftSLine);
+addIcon("logout-circle-r-line", LogoutCircleRLine);
+addIcon("information-line", InformationLine);
+addIcon("arrow-up-line", ArrowUpLine);
+addIcon("arrow-down-line", ArrowDownLine);
+addIcon("bookmark-2-line", Bookmark2Line);
+addIcon("user", User);
+addIcon("lock", Lock);
+addIcon("menu-unfold", MenuUnfold);
+addIcon("menu-fold", MenuFold);
+addIcon("setting", Setting);
+addIcon("arrow-down", ArrowDown);
+addIcon("close-left-tags", CloseLeftTags);
+addIcon("close-right-tags", CloseRightTags);
+addIcon("close-other-tags", CloseOtherTags);
+addIcon("close-all-tags", CloseAllTags);
+addIcon("fullscreen", Fullscreen);
+addIcon("exit-fullscreen", ExitFullscreen);
+
+// Iconify Icon在Vue里离线使用(用于内网环境)https://docs.iconify.design/icon-components/vue/offline.html
+export default defineComponent({
+ name: "IconifyIconOffline",
+ components: { IconifyIcon },
+ props: {
+ icon: {
+ type: String,
+ default: ""
+ }
+ },
+ render() {
+ const attrs = this.$attrs;
+ return h(
+ IconifyIcon,
+ {
+ icon: `${this.icon}`,
+ style: attrs?.style
+ ? Object.assign(attrs.style, { outline: "none" })
+ : { outline: "none" },
+ ...attrs
+ },
+ {
+ default: () => []
+ }
+ );
+ }
+});
diff --git a/src/components/ReIcon/src/iconifyIconOnline.ts b/src/components/ReIcon/src/iconifyIconOnline.ts
new file mode 100644
index 0000000..a5f5822
--- /dev/null
+++ b/src/components/ReIcon/src/iconifyIconOnline.ts
@@ -0,0 +1,30 @@
+import { h, defineComponent } from "vue";
+import { Icon as IconifyIcon } from "@iconify/vue";
+
+// Iconify Icon在Vue里在线使用(用于外网环境)
+export default defineComponent({
+ name: "IconifyIconOnline",
+ components: { IconifyIcon },
+ props: {
+ icon: {
+ type: String,
+ default: ""
+ }
+ },
+ render() {
+ const attrs = this.$attrs;
+ return h(
+ IconifyIcon,
+ {
+ icon: `${this.icon}`,
+ style: attrs?.style
+ ? Object.assign(attrs.style, { outline: "none" })
+ : { outline: "none" },
+ ...attrs
+ },
+ {
+ default: () => []
+ }
+ );
+ }
+});
diff --git a/src/components/ReIcon/src/types.ts b/src/components/ReIcon/src/types.ts
new file mode 100644
index 0000000..7e3ffab
--- /dev/null
+++ b/src/components/ReIcon/src/types.ts
@@ -0,0 +1,20 @@
+export interface iconType {
+ // iconify (https://docs.iconify.design/icon-components/vue/#properties)
+ inline?: boolean;
+ width?: string | number;
+ height?: string | number;
+ horizontalFlip?: boolean;
+ verticalFlip?: boolean;
+ flip?: string;
+ rotate?: number | string;
+ color?: string;
+ horizontalAlign?: boolean;
+ verticalAlign?: boolean;
+ align?: string;
+ online?: boolean;
+ onLoad?: Function;
+ includes?: Function;
+
+ // all icon
+ style?: object;
+}
diff --git a/src/config/index.ts b/src/config/index.ts
new file mode 100644
index 0000000..8365018
--- /dev/null
+++ b/src/config/index.ts
@@ -0,0 +1,56 @@
+import { App } from "vue";
+import axios from "axios";
+import { loadEnv } from "@build/index";
+
+let config: object = {};
+const { VITE_PUBLIC_PATH } = loadEnv();
+
+const setConfig = (cfg?: unknown) => {
+ config = Object.assign(config, cfg);
+};
+
+const getConfig = (key?: string): ServerConfigs => {
+ if (typeof key === "string") {
+ const arr = key.split(".");
+ if (arr && arr.length) {
+ let data = config;
+ arr.forEach(v => {
+ if (data && typeof data[v] !== "undefined") {
+ data = data[v];
+ } else {
+ data = null;
+ }
+ });
+ return data;
+ }
+ }
+ return config;
+};
+
+/** 获取项目动态全局配置 */
+export const getServerConfig = async (app: App): Promise => {
+ app.config.globalProperties.$config = getConfig();
+ return axios({
+ baseURL: "",
+ method: "get",
+ url: `${VITE_PUBLIC_PATH}serverConfig.json`
+ })
+ .then(({ data: config }) => {
+ let $config = app.config.globalProperties.$config;
+ // 自动注入项目配置
+ if (app && $config && typeof config === "object") {
+ $config = Object.assign($config, config);
+ app.config.globalProperties.$config = $config;
+ // 设置全局配置
+ setConfig($config);
+ }
+ // 设置全局baseURL
+ app.config.globalProperties.$baseUrl = $config.baseURL;
+ return $config;
+ })
+ .catch(() => {
+ throw "请在public文件夹下添加serverConfig.json配置文件";
+ });
+};
+
+export { getConfig, setConfig };
diff --git a/src/directives/auth/index.ts b/src/directives/auth/index.ts
new file mode 100644
index 0000000..69118d6
--- /dev/null
+++ b/src/directives/auth/index.ts
@@ -0,0 +1,13 @@
+import { hasAuth } from "@/router/utils";
+import { Directive, type DirectiveBinding } from "vue";
+
+export const auth: Directive = {
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
+ const { value } = binding;
+ if (value) {
+ !hasAuth(value) && el.parentNode.removeChild(el);
+ } else {
+ throw new Error("need auths! Like v-auth=\"['btn.add','btn.edit']\"");
+ }
+ }
+};
diff --git a/src/directives/elResizeDetector/index.ts b/src/directives/elResizeDetector/index.ts
new file mode 100644
index 0000000..af089be
--- /dev/null
+++ b/src/directives/elResizeDetector/index.ts
@@ -0,0 +1,27 @@
+import { Directive, type DirectiveBinding, type VNode } from "vue";
+import elementResizeDetectorMaker from "element-resize-detector";
+import type { Erd } from "element-resize-detector";
+import { emitter } from "@/utils/mitt";
+
+const erd: Erd = elementResizeDetectorMaker({
+ strategy: "scroll"
+});
+
+export const resize: Directive = {
+ mounted(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) {
+ erd.listenTo(el, elem => {
+ const width = elem.offsetWidth;
+ const height = elem.offsetHeight;
+ if (binding?.instance) {
+ emitter.emit("resize", { detail: { width, height } });
+ } else {
+ vnode.el.dispatchEvent(
+ new CustomEvent("resize", { detail: { width, height } })
+ );
+ }
+ });
+ },
+ unmounted(el: HTMLElement) {
+ erd.uninstall(el);
+ }
+};
diff --git a/src/directives/index.ts b/src/directives/index.ts
new file mode 100644
index 0000000..d6d6592
--- /dev/null
+++ b/src/directives/index.ts
@@ -0,0 +1,2 @@
+export * from "./auth";
+export * from "./elResizeDetector";
diff --git a/src/layout/components/appMain.vue b/src/layout/components/appMain.vue
new file mode 100644
index 0000000..e9b5450
--- /dev/null
+++ b/src/layout/components/appMain.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/navbar.vue b/src/layout/components/navbar.vue
new file mode 100644
index 0000000..300f6ed
--- /dev/null
+++ b/src/layout/components/navbar.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/notice/data.ts b/src/layout/components/notice/data.ts
new file mode 100644
index 0000000..14c7560
--- /dev/null
+++ b/src/layout/components/notice/data.ts
@@ -0,0 +1,146 @@
+export interface ListItem {
+ avatar: string;
+ title: string;
+ datetime: string;
+ type: string;
+ description: string;
+ status?: "" | "success" | "warning" | "info" | "danger";
+ extra?: string;
+}
+
+export interface TabItem {
+ key: string;
+ name: string;
+ list: ListItem[];
+}
+
+export const noticesData: TabItem[] = [
+ {
+ key: "1",
+ name: "通知",
+ list: [
+ {
+ avatar:
+ "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
+ title: "你收到了 12 份新周报",
+ datetime: "一年前",
+ description: "",
+ type: "1"
+ },
+ {
+ avatar:
+ "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
+ title: "你推荐的 前端高手 已通过第三轮面试",
+ datetime: "一年前",
+ description: "",
+ type: "1"
+ },
+ {
+ avatar:
+ "https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
+ title: "这种模板可以区分多种通知类型",
+ datetime: "一年前",
+ description: "",
+ type: "1"
+ },
+ {
+ avatar:
+ "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
+ title:
+ "展示标题内容超过一行后的处理方式,如果内容超过1行将自动截断并支持tooltip显示完整标题。",
+ datetime: "一年前",
+ description: "",
+ type: "1"
+ },
+ {
+ avatar:
+ "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
+ title: "左侧图标用于区分不同的类型",
+ datetime: "一年前",
+ description: "",
+ type: "1"
+ },
+ {
+ avatar:
+ "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
+ title: "左侧图标用于区分不同的类型",
+ datetime: "一年前",
+ description: "",
+ type: "1"
+ }
+ ]
+ },
+ {
+ key: "2",
+ name: "消息",
+ list: [
+ {
+ avatar:
+ "https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
+ title: "李白 评论了你",
+ description: "长风破浪会有时,直挂云帆济沧海",
+ datetime: "一年前",
+ type: "2"
+ },
+ {
+ avatar:
+ "https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
+ title: "李白 回复了你",
+ description: "行路难,行路难,多歧路,今安在。",
+ datetime: "一年前",
+ type: "2"
+ },
+ {
+ avatar:
+ "https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
+ title: "标题",
+ description:
+ "请将鼠标移动到此处,以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2,超过2行的描述内容将被省略并且可以通过tooltip查看完整内容",
+ datetime: "一年前",
+ type: "2"
+ }
+ ]
+ },
+ {
+ key: "3",
+ name: "代办",
+ list: [
+ {
+ avatar: "",
+ title: "任务名称",
+ description: "任务需要在 2022-11-16 20:00 前启动",
+ datetime: "",
+ extra: "未开始",
+ status: "info",
+ type: "3"
+ },
+ {
+ avatar: "",
+ title: "第三方紧急代码变更",
+ description:
+ "一拳提交于 2022-11-16,需在 2022-11-18 前完成代码变更任务",
+ datetime: "",
+ extra: "马上到期",
+ status: "danger",
+ type: "3"
+ },
+ {
+ avatar: "",
+ title: "信息安全考试",
+ description: "指派小仙于 2022-12-12 前完成更新并发布",
+ datetime: "",
+ extra: "已耗时 8 天",
+ status: "warning",
+ type: "3"
+ },
+ {
+ avatar: "",
+ title: "vue-pure-admin 版本发布",
+ description: "vue-pure-admin 版本发布",
+ datetime: "",
+ extra: "进行中",
+ type: "3"
+ }
+ ]
+ }
+];
diff --git a/src/layout/components/notice/index.vue b/src/layout/components/notice/index.vue
new file mode 100644
index 0000000..fbce59d
--- /dev/null
+++ b/src/layout/components/notice/index.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/notice/noticeItem.vue b/src/layout/components/notice/noticeItem.vue
new file mode 100644
index 0000000..fe6de2f
--- /dev/null
+++ b/src/layout/components/notice/noticeItem.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+ {{ props.noticeItem.title }}
+
+
+
+
+
+
+
+ {{ props.noticeItem.description }}
+
+
+
+ {{ props.noticeItem.datetime }}
+
+
+
+
+
+
+
diff --git a/src/layout/components/notice/noticeList.vue b/src/layout/components/notice/noticeList.vue
new file mode 100644
index 0000000..109cd1a
--- /dev/null
+++ b/src/layout/components/notice/noticeList.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/panel/index.vue b/src/layout/components/panel/index.vue
new file mode 100644
index 0000000..2a53f61
--- /dev/null
+++ b/src/layout/components/panel/index.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/screenfull/index.vue b/src/layout/components/screenfull/index.vue
new file mode 100644
index 0000000..978ba50
--- /dev/null
+++ b/src/layout/components/screenfull/index.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/layout/components/search/components/SearchFooter.vue b/src/layout/components/search/components/SearchFooter.vue
new file mode 100644
index 0000000..051ca5f
--- /dev/null
+++ b/src/layout/components/search/components/SearchFooter.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
diff --git a/src/layout/components/search/components/SearchModal.vue b/src/layout/components/search/components/SearchModal.vue
new file mode 100644
index 0000000..4ff03c3
--- /dev/null
+++ b/src/layout/components/search/components/SearchModal.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/search/components/SearchResult.vue b/src/layout/components/search/components/SearchResult.vue
new file mode 100644
index 0000000..493a4a3
--- /dev/null
+++ b/src/layout/components/search/components/SearchResult.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+ {{ item.meta?.title }}
+
+
+
+
+
+
+
diff --git a/src/layout/components/search/components/index.ts b/src/layout/components/search/components/index.ts
new file mode 100644
index 0000000..6e895d9
--- /dev/null
+++ b/src/layout/components/search/components/index.ts
@@ -0,0 +1,3 @@
+import SearchModal from "./SearchModal.vue";
+
+export { SearchModal };
diff --git a/src/layout/components/search/index.vue b/src/layout/components/search/index.vue
new file mode 100644
index 0000000..01f3923
--- /dev/null
+++ b/src/layout/components/search/index.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/setting/index.vue b/src/layout/components/setting/index.vue
new file mode 100644
index 0000000..f30d7c9
--- /dev/null
+++ b/src/layout/components/setting/index.vue
@@ -0,0 +1,523 @@
+
+
+
+
+ 主题
+
+
+ 导航栏模式
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 主题色
+
+
+ 界面显示
+
+
+ 灰色模式
+
+
+
+ 色弱模式
+
+
+
+ 隐藏标签页
+
+
+
+ 侧边栏Logo
+
+
+
+ 标签页持久化
+
+
+
+
+ 标签风格
+
+ 卡片
+ 灵动
+
+
+
+
+
+
+
+ 清空缓存并返回登录页
+
+
+
+
+
+
+
diff --git a/src/layout/components/sidebar/breadCrumb.vue b/src/layout/components/sidebar/breadCrumb.vue
new file mode 100644
index 0000000..52c0228
--- /dev/null
+++ b/src/layout/components/sidebar/breadCrumb.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+ {{ item.meta.title }}
+
+
+
+
+
diff --git a/src/layout/components/sidebar/horizontal.vue b/src/layout/components/sidebar/horizontal.vue
new file mode 100644
index 0000000..ba966e8
--- /dev/null
+++ b/src/layout/components/sidebar/horizontal.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
diff --git a/src/layout/components/sidebar/leftCollapse.vue b/src/layout/components/sidebar/leftCollapse.vue
new file mode 100644
index 0000000..aaa73c6
--- /dev/null
+++ b/src/layout/components/sidebar/leftCollapse.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/sidebar/logo.vue b/src/layout/components/sidebar/logo.vue
new file mode 100644
index 0000000..6f8b475
--- /dev/null
+++ b/src/layout/components/sidebar/logo.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
diff --git a/src/layout/components/sidebar/mixNav.vue b/src/layout/components/sidebar/mixNav.vue
new file mode 100644
index 0000000..6bd9585
--- /dev/null
+++ b/src/layout/components/sidebar/mixNav.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
diff --git a/src/layout/components/sidebar/sidebarItem.vue b/src/layout/components/sidebar/sidebarItem.vue
new file mode 100644
index 0000000..afaf86b
--- /dev/null
+++ b/src/layout/components/sidebar/sidebarItem.vue
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+ {{ onlyOneChild.meta.title }}
+
+
+
+
+ {{ onlyOneChild.meta.title }}
+
+
+
+
+
+ {{ onlyOneChild.meta.title }}
+
+
+
+ {{ onlyOneChild.meta.title }}
+
+
+ {{ onlyOneChild.meta.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ props.item.meta.title }}
+
+
+
+ {{ props.item.meta.title }}
+
+
+
+ {{ props.item.meta.title }}
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/sidebar/topCollapse.vue b/src/layout/components/sidebar/topCollapse.vue
new file mode 100644
index 0000000..4a33c08
--- /dev/null
+++ b/src/layout/components/sidebar/topCollapse.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/src/layout/components/sidebar/vertical.vue b/src/layout/components/sidebar/vertical.vue
new file mode 100644
index 0000000..ee3351c
--- /dev/null
+++ b/src/layout/components/sidebar/vertical.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+ menuSelect(indexPath, routers)"
+ >
+
+
+
+
+
+
diff --git a/src/layout/components/tag/index.scss b/src/layout/components/tag/index.scss
new file mode 100644
index 0000000..76c0043
--- /dev/null
+++ b/src/layout/components/tag/index.scss
@@ -0,0 +1,296 @@
+@keyframes scheduleInWidth {
+ from {
+ width: 0;
+ }
+
+ to {
+ width: 100%;
+ }
+}
+
+@keyframes scheduleOutWidth {
+ from {
+ width: 100%;
+ }
+
+ to {
+ width: 0;
+ }
+}
+
+@keyframes rotate {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes close {
+ from {
+ transform: translate(-50%, -50%);
+ }
+
+ to {
+ transform: translate(0, -50%);
+ }
+}
+
+.tags-view {
+ width: 100%;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ color: var(--el-text-color-primary);
+ background: #fff;
+ position: relative;
+ box-shadow: 0 0 1px #888;
+
+ .scroll-item {
+ border-radius: 3px 3px 0 0;
+ padding: 0 6px;
+ box-shadow: 0 0 1px #888;
+ position: relative;
+ margin-right: 4px;
+ height: 28px;
+ display: inline-block;
+ line-height: 28px;
+ transition: all 0.4s;
+ cursor: pointer;
+
+ .el-icon-close {
+ font-size: 10px;
+ color: var(--el-color-primary);
+ cursor: pointer;
+ position: absolute;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ transition: font-size 0.2s;
+
+ &:hover {
+ border-radius: 50%;
+ color: #fff;
+ background: #b4bccc;
+ font-size: 13px;
+ }
+ }
+
+ &.is-closable:not(:first-child) {
+ &:hover {
+ padding-right: 18px;
+
+ &:not(.is-active) {
+ .el-icon-close {
+ animation: close 200ms ease-in forwards;
+ }
+ }
+ }
+ }
+ }
+
+ a {
+ text-decoration: none;
+ color: var(--el-text-color-primary);
+ padding: 0 4px;
+ }
+
+ .scroll-container {
+ flex: 1;
+ overflow: hidden;
+ padding: 5px 0;
+ white-space: nowrap;
+ position: relative;
+
+ .tab {
+ position: relative;
+ float: left;
+ list-style: none;
+ overflow: visible;
+ white-space: nowrap;
+ transition: transform 0.5s ease-in-out;
+
+ .scroll-item {
+ transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+ &:nth-child(1) {
+ margin-left: 5px;
+ }
+ }
+ }
+ }
+
+ /* 右键菜单 */
+ .contextmenu {
+ margin: 0;
+ background: #fff;
+ position: absolute;
+ list-style-type: none;
+ padding: 5px 0;
+ border-radius: 4px;
+ color: var(--el-text-color-primary);
+ font-weight: normal;
+ font-size: 13px;
+ white-space: nowrap;
+ outline: 0;
+ box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
+
+ li {
+ width: 100%;
+ margin: 0;
+ padding: 7px 12px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+
+ &:hover {
+ // background: var(--el-color-primary-light-9);
+ color: var(--el-color-primary);
+ }
+
+ svg {
+ display: block;
+ margin-right: 0.5em;
+ }
+ }
+ }
+}
+
+.el-dropdown-menu {
+ li {
+ width: 100%;
+ margin: 0;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+
+ svg {
+ display: block;
+ margin-right: 0.5em;
+ }
+ }
+}
+
+.el-dropdown-menu__item:not(.is-disabled):hover {
+ color: #606266;
+ background: #f0f0f0;
+}
+
+:deep(.el-dropdown-menu__item) i {
+ margin-right: 10px;
+}
+
+:deep(.el-dropdown-menu__item--divided) {
+ margin: 1px 0;
+}
+.el-dropdown-menu__item--divided::before {
+ margin: 0;
+}
+
+.el-dropdown-menu__item.is-disabled {
+ cursor: not-allowed;
+}
+
+.scroll-item.is-active {
+ // background-color: var(--el-color-primary-light-9);
+ position: relative;
+ color: #fff;
+
+ &:not(:first-child) {
+ padding-right: 18px;
+ }
+
+ .el-icon-close {
+ transform: translate(0, -50%);
+ }
+
+ a {
+ color: var(--el-color-primary) !important;
+ }
+}
+
+.arrow-left,
+.arrow-right,
+.arrow-down {
+ width: 40px;
+ height: 38px;
+ color: var(--el-text-color-primary);
+ position: relative;
+
+ svg {
+ width: 20px;
+ height: 20px;
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, 50%);
+ }
+}
+
+.arrow-left {
+ box-shadow: 5px 0 5px -6px #ccc;
+
+ &:hover {
+ cursor: w-resize;
+ }
+}
+
+.arrow-right {
+ box-shadow: -5px 0 5px -6px #ccc;
+ border-right: 0.5px solid #ccc;
+
+ &:hover {
+ cursor: e-resize;
+ }
+}
+
+/* 卡片模式下鼠标移入显示蓝色边框 */
+.card-in {
+ color: var(--el-color-primary);
+
+ a {
+ color: var(--el-color-primary);
+ }
+}
+
+/* 卡片模式下鼠标移出隐藏蓝色边框 */
+.card-out {
+ border: none;
+ color: #666;
+
+ a {
+ color: #666;
+ }
+}
+
+/* 灵动模式 */
+.schedule-active {
+ width: 100%;
+ height: 2px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ background: var(--el-color-primary);
+}
+
+/* 灵动模式下鼠标移入显示蓝色进度条 */
+.schedule-in {
+ width: 100%;
+ height: 2px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ background: var(--el-color-primary);
+ animation: scheduleInWidth 400ms ease-in;
+}
+
+/* 灵动模式下鼠标移出隐藏蓝色进度条 */
+.schedule-out {
+ width: 0;
+ height: 2px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ background: var(--el-color-primary);
+ animation: scheduleOutWidth 400ms ease-in;
+}
diff --git a/src/layout/components/tag/index.vue b/src/layout/components/tag/index.vue
new file mode 100644
index 0000000..2380ea7
--- /dev/null
+++ b/src/layout/components/tag/index.vue
@@ -0,0 +1,596 @@
+
+
+
+
+
+
+
diff --git a/src/layout/frameView.vue b/src/layout/frameView.vue
new file mode 100644
index 0000000..5e4f4f4
--- /dev/null
+++ b/src/layout/frameView.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/hooks/useBoolean.ts b/src/layout/hooks/useBoolean.ts
new file mode 100644
index 0000000..1d14031
--- /dev/null
+++ b/src/layout/hooks/useBoolean.ts
@@ -0,0 +1,26 @@
+import { ref } from "vue";
+
+export function useBoolean(initValue = false) {
+ const bool = ref(initValue);
+
+ function setBool(value: boolean) {
+ bool.value = value;
+ }
+ function setTrue() {
+ setBool(true);
+ }
+ function setFalse() {
+ setBool(false);
+ }
+ function toggle() {
+ setBool(!bool.value);
+ }
+
+ return {
+ bool,
+ setBool,
+ setTrue,
+ setFalse,
+ toggle
+ };
+}
diff --git a/src/layout/hooks/useDataThemeChange.ts b/src/layout/hooks/useDataThemeChange.ts
new file mode 100644
index 0000000..10a0ce0
--- /dev/null
+++ b/src/layout/hooks/useDataThemeChange.ts
@@ -0,0 +1,116 @@
+import { ref } from "vue";
+import { getConfig } from "@/config";
+import { find } from "lodash-unified";
+import { useLayout } from "./useLayout";
+import { themeColorsType } from "../types";
+import { TinyColor } from "@ctrl/tinycolor";
+import { useGlobal } from "@pureadmin/utils";
+import { useEpThemeStoreHook } from "@/store/modules/epTheme";
+import {
+ darken,
+ lighten,
+ toggleTheme
+} from "@pureadmin/theme/dist/browser-utils";
+
+export function useDataThemeChange() {
+ const { layoutTheme, layout } = useLayout();
+ const themeColors = ref>([
+ /* 道奇蓝(默认) */
+ { color: "#1b2a47", themeColor: "default" },
+ /* 亮白色 */
+ { color: "#ffffff", themeColor: "light" },
+ /* 猩红色 */
+ { color: "#f5222d", themeColor: "dusk" },
+ /* 橙红色 */
+ { color: "#fa541c", themeColor: "volcano" },
+ /* 金色 */
+ { color: "#fadb14", themeColor: "yellow" },
+ /* 绿宝石 */
+ { color: "#13c2c2", themeColor: "mingQing" },
+ /* 酸橙绿 */
+ { color: "#52c41a", themeColor: "auroraGreen" },
+ /* 深粉色 */
+ { color: "#eb2f96", themeColor: "pink" },
+ /* 深紫罗兰色 */
+ { color: "#722ed1", themeColor: "saucePurple" }
+ ]);
+
+ const { $storage } = useGlobal();
+ const dataTheme = ref($storage?.layout?.darkMode);
+ const body = document.documentElement as HTMLElement;
+
+ /** 设置导航主题色 */
+ function setLayoutThemeColor(theme = "default") {
+ layoutTheme.value.theme = theme;
+ toggleTheme({
+ scopeName: `layout-theme-${theme}`
+ });
+ $storage.layout = {
+ layout: layout.value,
+ theme,
+ darkMode: dataTheme.value,
+ sidebarStatus: $storage.layout?.sidebarStatus,
+ epThemeColor: $storage.layout?.epThemeColor
+ };
+
+ if (theme === "default" || theme === "light") {
+ setEpThemeColor(getConfig().EpThemeColor);
+ } else {
+ const colors = find(themeColors.value, { themeColor: theme });
+ setEpThemeColor(colors.color);
+ }
+ }
+
+ /**
+ * @description 自动计算hover和active颜色
+ * @see {@link https://element-plus.org/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2}
+ */
+ const shadeBgColor = (color: string): string => {
+ return new TinyColor(color).shade(10).toString();
+ };
+
+ /** 设置ep主题色 */
+ const setEpThemeColor = (color: string) => {
+ useEpThemeStoreHook().setEpThemeColor(color);
+ body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
+ document.documentElement.style.setProperty("--el-color-primary", color);
+ for (let i = 1; i <= 9; i++) {
+ document.documentElement.style.setProperty(
+ `--el-color-primary-light-${i}`,
+ lighten(color, i / 10)
+ );
+ }
+ for (let i = 1; i <= 2; i++) {
+ document.documentElement.style.setProperty(
+ `--el-color-primary-dark-${i}`,
+ darken(color, i / 10)
+ );
+ }
+ };
+
+ /** 日间、夜间主题切换 */
+ function dataThemeChange() {
+ /* 如果当前是light夜间主题,默认切换到default主题 */
+ if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
+ setLayoutThemeColor("default");
+ } else {
+ setLayoutThemeColor(useEpThemeStoreHook().epTheme);
+ }
+
+ if (dataTheme.value) {
+ document.documentElement.classList.add("dark");
+ } else {
+ document.documentElement.classList.remove("dark");
+ }
+ }
+
+ return {
+ body,
+ dataTheme,
+ layoutTheme,
+ themeColors,
+ dataThemeChange,
+ setEpThemeColor,
+ setLayoutThemeColor
+ };
+}
diff --git a/src/layout/hooks/useLayout.ts b/src/layout/hooks/useLayout.ts
new file mode 100644
index 0000000..37736cf
--- /dev/null
+++ b/src/layout/hooks/useLayout.ts
@@ -0,0 +1,54 @@
+import { computed } from "vue";
+import { routerArrays } from "../types";
+import { useGlobal } from "@pureadmin/utils";
+import { useMultiTagsStore } from "@/store/modules/multiTags";
+
+export function useLayout() {
+ const { $storage, $config } = useGlobal();
+
+ const initStorage = () => {
+ /** 路由 */
+ if (
+ useMultiTagsStore().multiTagsCache &&
+ (!$storage.tags || $storage.tags.length === 0)
+ ) {
+ $storage.tags = routerArrays;
+ }
+ /** 导航 */
+ if (!$storage.layout) {
+ $storage.layout = {
+ layout: $config?.Layout ?? "vertical",
+ theme: $config?.Theme ?? "default",
+ darkMode: $config?.DarkMode ?? false,
+ sidebarStatus: $config?.SidebarStatus ?? true,
+ epThemeColor: $config?.EpThemeColor ?? "#409EFF"
+ };
+ }
+ /** 灰色模式、色弱模式、隐藏标签页 */
+ if (!$storage.configure) {
+ $storage.configure = {
+ grey: $config?.Grey ?? false,
+ weak: $config?.Weak ?? false,
+ hideTabs: $config?.HideTabs ?? false,
+ showLogo: $config?.ShowLogo ?? true,
+ showModel: $config?.ShowModel ?? "smart",
+ multiTagsCache: $config?.MultiTagsCache ?? false
+ };
+ }
+ };
+
+ /** 清空缓存后从serverConfig.json读取默认配置并赋值到storage中 */
+ const layout = computed(() => {
+ return $storage?.layout.layout;
+ });
+
+ const layoutTheme = computed(() => {
+ return $storage.layout;
+ });
+
+ return {
+ layout,
+ layoutTheme,
+ initStorage
+ };
+}
diff --git a/src/layout/hooks/useNav.ts b/src/layout/hooks/useNav.ts
new file mode 100644
index 0000000..74583e0
--- /dev/null
+++ b/src/layout/hooks/useNav.ts
@@ -0,0 +1,137 @@
+import { computed } from "vue";
+import { getConfig } from "@/config";
+import { emitter } from "@/utils/mitt";
+import { routeMetaType } from "../types";
+import { useGlobal } from "@pureadmin/utils";
+import { useRouter, useRoute } from "vue-router";
+import { router, remainingPaths } from "@/router";
+import { useAppStoreHook } from "@/store/modules/app";
+import { useUserStoreHook } from "@/store/modules/user";
+
+const errorInfo = "当前路由配置不正确,请检查配置";
+
+export function useNav() {
+ const route = useRoute();
+ const pureApp = useAppStoreHook();
+ const routers = useRouter().options.routes;
+
+ /** 用户名 */
+ const username = computed(() => {
+ return useUserStoreHook()?.username;
+ });
+
+ const avatarsStyle = computed(() => {
+ return username.value ? { marginRight: "10px" } : "";
+ });
+
+ const isCollapse = computed(() => {
+ return !pureApp.getSidebarStatus;
+ });
+
+ const device = computed(() => {
+ return pureApp.getDevice;
+ });
+
+ const { $storage, $config } = useGlobal();
+ const layout = computed(() => {
+ return $storage?.layout?.layout;
+ });
+
+ const title = computed(() => {
+ return $config.Title;
+ });
+
+ /** 动态title */
+ function changeTitle(meta: routeMetaType) {
+ const Title = getConfig().Title;
+ if (Title) document.title = `${meta.title} | ${Title}`;
+ else document.title = meta.title;
+ }
+
+ /** 退出登录 */
+ function logout() {
+ useUserStoreHook().logOut();
+ }
+
+ function backHome() {
+ router.push("/welcome");
+ }
+
+ function onPanel() {
+ emitter.emit("openPanel");
+ }
+
+ function toggleSideBar() {
+ pureApp.toggleSideBar();
+ }
+
+ function handleResize(menuRef) {
+ menuRef?.handleResize();
+ }
+
+ function resolvePath(route) {
+ if (!route.children) return console.error(errorInfo);
+ const httpReg = /^http(s?):\/\//;
+ const routeChildPath = route.children[0]?.path;
+ if (httpReg.test(routeChildPath)) {
+ return route.path + "/" + routeChildPath;
+ } else {
+ return routeChildPath;
+ }
+ }
+
+ function menuSelect(indexPath: string, routers): void {
+ if (isRemaining(indexPath)) return;
+ let parentPath = "";
+ const parentPathIndex = indexPath.lastIndexOf("/");
+ if (parentPathIndex > 0) {
+ parentPath = indexPath.slice(0, parentPathIndex);
+ }
+ /** 找到当前路由的信息 */
+ function findCurrentRoute(indexPath: string, routes) {
+ if (!routes) return console.error(errorInfo);
+ return routes.map(item => {
+ if (item.path === indexPath) {
+ if (item.redirect) {
+ findCurrentRoute(item.redirect, item.children);
+ } else {
+ /** 切换左侧菜单 通知标签页 */
+ emitter.emit("changLayoutRoute", {
+ indexPath,
+ parentPath
+ });
+ }
+ } else {
+ if (item.children) findCurrentRoute(indexPath, item.children);
+ }
+ });
+ }
+ findCurrentRoute(indexPath, routers);
+ }
+
+ /** 判断路径是否参与菜单 */
+ function isRemaining(path: string): boolean {
+ return remainingPaths.includes(path);
+ }
+
+ return {
+ route,
+ title,
+ device,
+ layout,
+ logout,
+ routers,
+ $storage,
+ backHome,
+ onPanel,
+ changeTitle,
+ toggleSideBar,
+ menuSelect,
+ handleResize,
+ resolvePath,
+ isCollapse,
+ pureApp,
+ username,
+ avatarsStyle
+ };
+}
diff --git a/src/layout/hooks/useTag.ts b/src/layout/hooks/useTag.ts
new file mode 100644
index 0000000..ad64ffd
--- /dev/null
+++ b/src/layout/hooks/useTag.ts
@@ -0,0 +1,232 @@
+import {
+ ref,
+ unref,
+ watch,
+ computed,
+ reactive,
+ onMounted,
+ CSSProperties,
+ getCurrentInstance
+} from "vue";
+import { tagsViewsType } from "../types";
+import { isEqual } from "lodash-unified";
+import type { StorageConfigs } from "/#/index";
+import { useEventListener } from "@vueuse/core";
+import { useRoute, useRouter } from "vue-router";
+import { useSettingStoreHook } from "@/store/modules/settings";
+import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
+import { storageLocal, toggleClass, hasClass } from "@pureadmin/utils";
+
+export function useTags() {
+ const route = useRoute();
+ const router = useRouter();
+ const instance = getCurrentInstance();
+ const pureSetting = useSettingStoreHook();
+
+ const buttonTop = ref(0);
+ const buttonLeft = ref(0);
+ const translateX = ref(0);
+ const visible = ref(false);
+ const activeIndex = ref(-1);
+ // 当前右键选中的路由信息
+ const currentSelect = ref({});
+
+ /** 显示模式,默认灵动模式 */
+ const showModel = ref(
+ storageLocal.getItem("responsive-configure")?.showModel ||
+ "smart"
+ );
+ /** 是否隐藏标签页,默认显示 */
+ const showTags =
+ ref(
+ storageLocal.getItem("responsive-configure").hideTabs
+ ) ?? ref("false");
+ const multiTags: any = computed(() => {
+ return useMultiTagsStoreHook().multiTags;
+ });
+
+ const tagsViews = reactive>([
+ {
+ icon: "refresh-right",
+ text: "重新加载",
+ divided: false,
+ disabled: false,
+ show: true
+ },
+ {
+ icon: "close",
+ text: "关闭当前标签页",
+ divided: false,
+ disabled: multiTags.value.length > 1 ? false : true,
+ show: true
+ },
+ {
+ icon: "close-left-tags",
+ text: "关闭左侧标签页",
+ divided: true,
+ disabled: multiTags.value.length > 1 ? false : true,
+ show: true
+ },
+ {
+ icon: "close-right-tags",
+ text: "关闭右侧标签页",
+ divided: false,
+ disabled: multiTags.value.length > 1 ? false : true,
+ show: true
+ },
+ {
+ icon: "close-other-tags",
+ text: "关闭其他标签页",
+ divided: true,
+ disabled: multiTags.value.length > 2 ? false : true,
+ show: true
+ },
+ {
+ icon: "close-all-tags",
+ text: "关闭全部标签页",
+ divided: false,
+ disabled: multiTags.value.length > 1 ? false : true,
+ show: true
+ },
+ {
+ icon: "fullscreen",
+ text: "整体页面全屏",
+ divided: true,
+ disabled: false,
+ show: true
+ },
+ {
+ icon: "fullscreen",
+ text: "内容区全屏",
+ divided: false,
+ disabled: false,
+ show: true
+ }
+ ]);
+
+ function conditionHandle(item, previous, next) {
+ if (
+ Object.keys(route.query).length === 0 &&
+ Object.keys(route.params).length === 0
+ ) {
+ return route.path === item.path ? previous : next;
+ } else if (Object.keys(route.query).length > 0) {
+ return isEqual(route.query, item.query) ? previous : next;
+ } else {
+ return isEqual(route.params, item.params) ? previous : next;
+ }
+ }
+
+ const iconIsActive = computed(() => {
+ return (item, index) => {
+ if (index === 0) return;
+ return conditionHandle(item, true, false);
+ };
+ });
+
+ const linkIsActive = computed(() => {
+ return item => {
+ return conditionHandle(item, "is-active", "");
+ };
+ });
+
+ const scheduleIsActive = computed(() => {
+ return item => {
+ return conditionHandle(item, "schedule-active", "");
+ };
+ });
+
+ const getTabStyle = computed((): CSSProperties => {
+ return {
+ transform: `translateX(${translateX.value}px)`
+ };
+ });
+
+ const getContextMenuStyle = computed((): CSSProperties => {
+ return { left: buttonLeft.value + "px", top: buttonTop.value + "px" };
+ });
+
+ const closeMenu = () => {
+ visible.value = false;
+ };
+
+ /** 鼠标移入添加激活样式 */
+ function onMouseenter(index) {
+ if (index) activeIndex.value = index;
+ if (unref(showModel) === "smart") {
+ if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
+ return;
+ toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]);
+ toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]);
+ } else {
+ if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
+ toggleClass(true, "card-in", instance.refs["dynamic" + index][0]);
+ toggleClass(false, "card-out", instance.refs["dynamic" + index][0]);
+ }
+ }
+
+ /** 鼠标移出恢复默认样式 */
+ function onMouseleave(index) {
+ activeIndex.value = -1;
+ if (unref(showModel) === "smart") {
+ if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
+ return;
+ toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]);
+ toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]);
+ } else {
+ if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
+ toggleClass(false, "card-in", instance.refs["dynamic" + index][0]);
+ toggleClass(true, "card-out", instance.refs["dynamic" + index][0]);
+ }
+ }
+
+ function onContentFullScreen() {
+ pureSetting.hiddenSideBar
+ ? pureSetting.changeSetting({ key: "hiddenSideBar", value: false })
+ : pureSetting.changeSetting({ key: "hiddenSideBar", value: true });
+ }
+
+ onMounted(() => {
+ if (!showModel.value) {
+ const configure = storageLocal.getItem(
+ "responsive-configure"
+ );
+ configure.showModel = "card";
+ storageLocal.setItem("responsive-configure", configure);
+ }
+ });
+
+ watch(
+ () => visible.value,
+ () => {
+ useEventListener(document, "click", closeMenu);
+ }
+ );
+
+ return {
+ route,
+ router,
+ visible,
+ showTags,
+ instance,
+ multiTags,
+ showModel,
+ tagsViews,
+ buttonTop,
+ buttonLeft,
+ translateX,
+ pureSetting,
+ activeIndex,
+ getTabStyle,
+ iconIsActive,
+ linkIsActive,
+ currentSelect,
+ scheduleIsActive,
+ getContextMenuStyle,
+ closeMenu,
+ onMounted,
+ onMouseenter,
+ onMouseleave,
+ onContentFullScreen
+ };
+}
diff --git a/src/layout/index.vue b/src/layout/index.vue
new file mode 100644
index 0000000..7c97b6c
--- /dev/null
+++ b/src/layout/index.vue
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
diff --git a/src/layout/redirect.vue b/src/layout/redirect.vue
new file mode 100644
index 0000000..6e16339
--- /dev/null
+++ b/src/layout/redirect.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/src/layout/theme/index.ts b/src/layout/theme/index.ts
new file mode 100644
index 0000000..c24bb1b
--- /dev/null
+++ b/src/layout/theme/index.ts
@@ -0,0 +1,159 @@
+/**
+ * @description ⚠️:此文件仅供主题插件使用,请不要在此文件中导出别的工具函数(仅在页面加载前运行)
+ */
+
+import { EpThemeColor } from "../../../public/serverConfig.json";
+
+type MultipleScopeVarsItem = {
+ scopeName: string;
+ varsContent: string;
+};
+
+/** 将vxe默认主题色和ep默认主题色保持一致 */
+const vxeColor = EpThemeColor;
+/** 预设主题色 */
+const themeColors = {
+ default: {
+ vxeColor,
+ subMenuActiveText: "#fff",
+ menuBg: "#001529",
+ menuHover: "#4091f7",
+ subMenuBg: "#0f0303",
+ subMenuActiveBg: "#4091f7",
+ navTextColor: "#fff",
+ menuText: "rgb(254 254 254 / 65%)",
+ sidebarLogo: "#002140",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#4091f7"
+ },
+ light: {
+ vxeColor,
+ subMenuActiveText: "#409eff",
+ menuBg: "#fff",
+ menuHover: "#e0ebf6",
+ subMenuBg: "#fff",
+ subMenuActiveBg: "#e0ebf6",
+ navTextColor: "#7a80b4",
+ menuText: "#7a80b4",
+ sidebarLogo: "#fff",
+ menuTitleHover: "#000",
+ menuActiveBefore: "#4091f7"
+ },
+ dusk: {
+ vxeColor: "#f5222d",
+ subMenuActiveText: "#fff",
+ menuBg: "#2a0608",
+ menuHover: "#e13c39",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#e13c39",
+ navTextColor: "#red",
+ menuText: "rgb(254 254 254 / 65.1%)",
+ sidebarLogo: "#42090c",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#e13c39"
+ },
+ volcano: {
+ vxeColor: "#fa541c",
+ subMenuActiveText: "#fff",
+ menuBg: "#2b0e05",
+ menuHover: "#e85f33",
+ subMenuBg: "#0f0603",
+ subMenuActiveBg: "#e85f33",
+ navTextColor: "#fff",
+ menuText: "rgb(254 254 254 / 65%)",
+ sidebarLogo: "#441708",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#e85f33"
+ },
+ yellow: {
+ vxeColor: "#fadb14",
+ subMenuActiveText: "#d25f00",
+ menuBg: "#2b2503",
+ menuHover: "#f6da4d",
+ subMenuBg: "#0f0603",
+ subMenuActiveBg: "#f6da4d",
+ navTextColor: "#fff",
+ menuText: "rgb(254 254 254 / 65%)",
+ sidebarLogo: "#443b05",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#f6da4d"
+ },
+ mingQing: {
+ vxeColor: "#13c2c2",
+ subMenuActiveText: "#fff",
+ menuBg: "#032121",
+ menuHover: "#59bfc1",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#59bfc1",
+ navTextColor: "#7a80b4",
+ menuText: "#7a80b4",
+ sidebarLogo: "#053434",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#59bfc1"
+ },
+ auroraGreen: {
+ vxeColor: "#52c41a",
+ subMenuActiveText: "#fff",
+ menuBg: "#0b1e15",
+ menuHover: "#60ac80",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#60ac80",
+ navTextColor: "#7a80b4",
+ menuText: "#7a80b4",
+ sidebarLogo: "#112f21",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#60ac80"
+ },
+ pink: {
+ vxeColor: "#eb2f96",
+ subMenuActiveText: "#fff",
+ menuBg: "#28081a",
+ menuHover: "#d84493",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#d84493",
+ navTextColor: "#7a80b4",
+ menuText: "#7a80b4",
+ sidebarLogo: "#3f0d29",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#d84493"
+ },
+ saucePurple: {
+ vxeColor: "#722ed1",
+ subMenuActiveText: "#fff",
+ menuBg: "#130824",
+ menuHover: "#693ac9",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#693ac9",
+ navTextColor: "#7a80b4",
+ menuText: "#7a80b4",
+ sidebarLogo: "#1f0c38",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#693ac9"
+ }
+};
+
+/**
+ * @description 将预设主题色处理成主题插件所需格式
+ */
+export const genScssMultipleScopeVars = (): MultipleScopeVarsItem[] => {
+ const result = [] as MultipleScopeVarsItem[];
+ Object.keys(themeColors).forEach(key => {
+ result.push({
+ scopeName: `layout-theme-${key}`,
+ varsContent: `
+ $vxe-primary-color: ${themeColors[key].vxeColor} !default;
+ $subMenuActiveText: ${themeColors[key].subMenuActiveText} !default;
+ $menuBg: ${themeColors[key].menuBg} !default;
+ $menuHover: ${themeColors[key].menuHover} !default;
+ $subMenuBg: ${themeColors[key].subMenuBg} !default;
+ $subMenuActiveBg: ${themeColors[key].subMenuActiveBg} !default;
+ $navTextColor: ${themeColors[key].navTextColor} !default;
+ $menuText: ${themeColors[key].menuText} !default;
+ $sidebarLogo: ${themeColors[key].sidebarLogo} !default;
+ $menuTitleHover: ${themeColors[key].menuTitleHover} !default;
+ $menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;
+ `
+ } as MultipleScopeVarsItem);
+ });
+ return result;
+};
diff --git a/src/layout/types.ts b/src/layout/types.ts
new file mode 100644
index 0000000..ff79230
--- /dev/null
+++ b/src/layout/types.ts
@@ -0,0 +1,87 @@
+export const routerArrays: Array = [
+ {
+ path: "/welcome",
+ parentPath: "/",
+ meta: {
+ title: "首页",
+ icon: "home-filled"
+ }
+ }
+];
+
+export type routeMetaType = {
+ title?: string;
+ icon?: string;
+ showLink?: boolean;
+ savedPosition?: boolean;
+ auths?: Array;
+};
+
+export type RouteConfigs = {
+ path?: string;
+ parentPath?: string;
+ query?: object;
+ params?: object;
+ meta?: routeMetaType;
+ children?: RouteConfigs[];
+ name?: string;
+};
+
+export type multiTagsType = {
+ tags: Array;
+};
+
+export type tagsViewsType = {
+ icon: string;
+ text: string;
+ divided: boolean;
+ disabled: boolean;
+ show: boolean;
+};
+
+export interface setType {
+ sidebar: {
+ opened: boolean;
+ withoutAnimation: boolean;
+ isClickCollapse: boolean;
+ };
+ device: string;
+ fixedHeader: boolean;
+ classes: {
+ hideSidebar: boolean;
+ openSidebar: boolean;
+ withoutAnimation: boolean;
+ mobile: boolean;
+ };
+ hideTabs: boolean;
+}
+
+export type childrenType = {
+ path?: string;
+ noShowingChildren?: boolean;
+ children?: childrenType[];
+ value: unknown;
+ meta?: {
+ icon?: string;
+ title?: string;
+ showParent?: boolean;
+ extraIcon?: {
+ svg?: boolean;
+ name?: string;
+ };
+ };
+ showTooltip?: boolean;
+ parentId?: number;
+ pathList?: number[];
+};
+
+export type themeColorsType = {
+ color: string;
+ themeColor: string;
+};
+
+export interface scrollbarDomType extends HTMLElement {
+ wrap?: {
+ offsetWidth: number;
+ };
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..3ea7939
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,61 @@
+import App from "./App.vue";
+import router from "./router";
+import { setupStore } from "@/store";
+import ElementPlus from "element-plus";
+import { getServerConfig } from "./config";
+import { createApp, Directive } from "vue";
+import { MotionPlugin } from "@vueuse/motion";
+// import { useEcharts } from "@/plugins/echarts";
+// import { useTable } from "@/plugins/vxe-table";
+import { injectResponsiveStorage } from "@/utils/responsive";
+
+// import Table from "@pureadmin/table";
+// import PureDescriptions from "@pureadmin/descriptions";
+
+import "animate.css";
+// 引入重置样式
+import "./style/reset.scss";
+// 导入公共样式
+import "./style/index.scss";
+import "element-plus/dist/index.css";
+import "@pureadmin/components/dist/index.css";
+import "@pureadmin/components/dist/theme.css";
+import "@pureadmin/components/dist/dark.scss";
+// 导入字体图标
+import "./assets/iconfont/iconfont.js";
+import "./assets/iconfont/iconfont.css";
+
+const app = createApp(App);
+
+// 自定义指令
+import * as directives from "@/directives";
+Object.keys(directives).forEach(key => {
+ app.directive(key, (directives as { [key: string]: Directive })[key]);
+});
+
+// 全局注册`@iconify/vue`图标库
+import {
+ IconifyIconOffline,
+ IconifyIconOnline,
+ FontIcon
+} from "./components/ReIcon";
+app.component("IconifyIconOffline", IconifyIconOffline);
+app.component("IconifyIconOnline", IconifyIconOnline);
+app.component("FontIcon", FontIcon);
+
+// 全局注册按钮级别权限组件
+import { Auth } from "@/components/ReAuth";
+app.component("Auth", Auth);
+
+getServerConfig(app).then(async config => {
+ app.use(router);
+ await router.isReady();
+ injectResponsiveStorage(app, config);
+ setupStore(app);
+ app.use(MotionPlugin).use(ElementPlus);
+ // .use(useEcharts);
+ // .use(Table);
+ // .use(PureDescriptions);
+ // .use(useTable);
+ app.mount("#app");
+});
diff --git a/src/mockProdServer.ts b/src/mockProdServer.ts
new file mode 100644
index 0000000..96d7f90
--- /dev/null
+++ b/src/mockProdServer.ts
@@ -0,0 +1,14 @@
+import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";
+
+const modules: Record = import.meta.glob("../mock/*.ts", {
+ eager: true
+});
+const mockModules = [];
+
+Object.keys(modules).forEach(key => {
+ mockModules.push(...modules[key].default);
+});
+
+export function setupProdMockServer() {
+ createProdMockServer(mockModules);
+}
diff --git a/src/plugins/echarts/index.ts b/src/plugins/echarts/index.ts
new file mode 100644
index 0000000..beb8d77
--- /dev/null
+++ b/src/plugins/echarts/index.ts
@@ -0,0 +1,41 @@
+import type { App } from "vue";
+import * as echarts from "echarts/core";
+import { SVGRenderer } from "echarts/renderers";
+import { PieChart, BarChart, LineChart } from "echarts/charts";
+import {
+ GridComponent,
+ TitleComponent,
+ LegendComponent,
+ GraphicComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ DataZoomComponent,
+ VisualMapComponent
+} from "echarts/components";
+
+const { use } = echarts;
+
+use([
+ PieChart,
+ BarChart,
+ LineChart,
+ SVGRenderer,
+ GridComponent,
+ TitleComponent,
+ LegendComponent,
+ GraphicComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ DataZoomComponent,
+ VisualMapComponent
+]);
+
+/**
+ * @description 按需引入echarts
+ * @see {@link https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6}
+ */
+export function useEcharts(app: App) {
+ app.config.globalProperties.$echarts = echarts;
+}
+
+export default echarts;
diff --git a/src/plugins/element-plus/index.ts b/src/plugins/element-plus/index.ts
new file mode 100644
index 0000000..83a1716
--- /dev/null
+++ b/src/plugins/element-plus/index.ts
@@ -0,0 +1,141 @@
+import { App, Component } from "vue";
+import {
+ ElTag,
+ ElAffix,
+ ElSkeleton,
+ ElBreadcrumb,
+ ElBreadcrumbItem,
+ ElScrollbar,
+ ElSubMenu,
+ ElButton,
+ ElCol,
+ ElRow,
+ ElSpace,
+ ElDivider,
+ ElCard,
+ ElDropdown,
+ ElDialog,
+ ElMenu,
+ ElMenuItem,
+ ElDropdownItem,
+ ElDropdownMenu,
+ ElIcon,
+ ElInput,
+ ElForm,
+ ElFormItem,
+ ElPopover,
+ ElPopper,
+ ElTooltip,
+ ElDrawer,
+ ElPagination,
+ ElAlert,
+ ElRadio,
+ ElRadioButton,
+ ElRadioGroup,
+ ElDescriptions,
+ ElDescriptionsItem,
+ ElBacktop,
+ ElSwitch,
+ ElBadge,
+ ElTabs,
+ ElTabPane,
+ ElAvatar,
+ ElEmpty,
+ ElCollapse,
+ ElCollapseItem,
+ ElTable,
+ ElTableColumn,
+ ElLink,
+ ElColorPicker,
+ ElSelect,
+ ElOption,
+ ElTimeline,
+ ElTimelineItem,
+ ElResult,
+ ElSteps,
+ ElStep,
+ ElTree,
+ ElTreeV2,
+ ElPopconfirm,
+ ElCheckbox,
+ ElCheckboxGroup,
+ // 指令
+ ElLoading,
+ ElInfiniteScroll
+} from "element-plus";
+
+// Directives
+const plugins = [ElLoading, ElInfiniteScroll];
+
+const components = [
+ ElTag,
+ ElAffix,
+ ElSkeleton,
+ ElBreadcrumb,
+ ElBreadcrumbItem,
+ ElScrollbar,
+ ElSubMenu,
+ ElButton,
+ ElCol,
+ ElRow,
+ ElSpace,
+ ElDivider,
+ ElCard,
+ ElDropdown,
+ ElDialog,
+ ElMenu,
+ ElMenuItem,
+ ElDropdownItem,
+ ElDropdownMenu,
+ ElIcon,
+ ElInput,
+ ElForm,
+ ElFormItem,
+ ElPopover,
+ ElPopper,
+ ElTooltip,
+ ElDrawer,
+ ElPagination,
+ ElAlert,
+ ElRadio,
+ ElRadioButton,
+ ElRadioGroup,
+ ElDescriptions,
+ ElDescriptionsItem,
+ ElBacktop,
+ ElSwitch,
+ ElBadge,
+ ElTabs,
+ ElTabPane,
+ ElAvatar,
+ ElEmpty,
+ ElCollapse,
+ ElCollapseItem,
+ ElTree,
+ ElTreeV2,
+ ElPopconfirm,
+ ElCheckbox,
+ ElCheckboxGroup,
+ ElTable,
+ ElTableColumn,
+ ElLink,
+ ElColorPicker,
+ ElSelect,
+ ElOption,
+ ElTimeline,
+ ElTimelineItem,
+ ElResult,
+ ElSteps,
+ ElStep
+];
+
+export function useElementPlus(app: App) {
+ // 注册组件
+ components.forEach((component: Component) => {
+ app.component(component.name, component);
+ });
+ // 注册指令
+ plugins.forEach(plugin => {
+ app.use(plugin);
+ });
+}
diff --git a/src/plugins/vxe-table/index.scss b/src/plugins/vxe-table/index.scss
new file mode 100644
index 0000000..f34fd0c
--- /dev/null
+++ b/src/plugins/vxe-table/index.scss
@@ -0,0 +1,6 @@
+@import "vxe-table/styles/variable.scss";
+@import "vxe-table/styles/modules.scss";
+
+i {
+ border-color: initial;
+}
diff --git a/src/plugins/vxe-table/index.ts b/src/plugins/vxe-table/index.ts
new file mode 100644
index 0000000..1197677
--- /dev/null
+++ b/src/plugins/vxe-table/index.ts
@@ -0,0 +1,98 @@
+import "xe-utils";
+import "./index.scss";
+import { App } from "vue";
+import "font-awesome/css/font-awesome.min.css";
+
+import {
+ // 核心
+ VXETable,
+ // 表格功能
+ Icon,
+ Filter,
+ Edit,
+ Menu,
+ Export,
+ Keyboard,
+ Validator,
+ // 可选组件
+ Column,
+ Colgroup,
+ Grid,
+ Tooltip,
+ Toolbar,
+ Pager,
+ Form,
+ FormItem,
+ FormGather,
+ Checkbox,
+ CheckboxGroup,
+ Radio,
+ RadioGroup,
+ RadioButton,
+ Switch,
+ Input,
+ Select,
+ Optgroup,
+ Option,
+ Textarea,
+ Button,
+ Modal,
+ List,
+ Pulldown,
+ // 表格
+ Table
+} from "vxe-table";
+
+// 全局默认参数
+VXETable.setup({
+ size: "medium",
+ version: 0,
+ zIndex: 1002,
+ table: {
+ // 自动监听父元素的变化去重新计算表格
+ autoResize: true,
+ // 鼠标移到行是否要高亮显示
+ highlightHoverRow: true
+ },
+ input: {
+ clearable: true
+ }
+});
+
+export function useTable(app: App) {
+ app
+ .use(Icon)
+ .use(Filter)
+ .use(Edit)
+ .use(Menu)
+ .use(Export)
+ .use(Keyboard)
+ .use(Validator)
+ // 可选组件
+ .use(Column)
+ .use(Colgroup)
+ .use(Grid)
+ .use(Tooltip)
+ .use(Toolbar)
+ .use(Pager)
+ .use(Form)
+ .use(FormItem)
+ .use(FormGather)
+ .use(Checkbox)
+ .use(CheckboxGroup)
+ .use(Radio)
+ .use(RadioGroup)
+ .use(RadioButton)
+ .use(Switch)
+ .use(Input)
+ .use(Select)
+ .use(Optgroup)
+ .use(Option)
+ .use(Textarea)
+ .use(Button)
+ .use(Modal)
+ .use(List)
+ .use(Pulldown)
+ // 安装表格
+ .use(Table);
+}
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..83159b9
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,170 @@
+import { getConfig } from "@/config";
+import { toRouteType } from "./types";
+import NProgress from "@/utils/progress";
+import { findIndex } from "lodash-unified";
+import { sessionKey, type DataInfo } from "@/utils/auth";
+import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
+import { usePermissionStoreHook } from "@/store/modules/permission";
+import {
+ Router,
+ createRouter,
+ RouteRecordRaw,
+ RouteComponent
+} from "vue-router";
+import {
+ ascending,
+ initRouter,
+ isOneOfArray,
+ getHistoryMode,
+ findRouteByPath,
+ handleAliveRoute,
+ formatTwoStageRoutes,
+ formatFlatteningRoutes
+} from "./utils";
+import {
+ buildHierarchyTree,
+ openLink,
+ isUrl,
+ storageSession
+} from "@pureadmin/utils";
+
+import homeRouter from "./modules/home";
+import errorRouter from "./modules/error";
+import remainingRouter from "./modules/remaining";
+
+/** 原始静态路由(未做任何处理) */
+const routes = [homeRouter, errorRouter];
+
+/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
+export const constantRoutes: Array = formatTwoStageRoutes(
+ formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
+);
+
+/** 用于渲染菜单,保持原始层级 */
+export const constantMenus: Array = ascending(routes).concat(
+ ...remainingRouter
+);
+
+/** 不参与菜单的路由 */
+export const remainingPaths = Object.keys(remainingRouter).map(v => {
+ return remainingRouter[v].path;
+});
+
+/** 创建路由实例 */
+export const router: Router = createRouter({
+ history: getHistoryMode(),
+ routes: constantRoutes.concat(...(remainingRouter as any)),
+ strict: true,
+ scrollBehavior(to, from, savedPosition) {
+ return new Promise(resolve => {
+ if (savedPosition) {
+ return savedPosition;
+ } else {
+ if (from.meta.saveSrollTop) {
+ const top: number =
+ document.documentElement.scrollTop || document.body.scrollTop;
+ resolve({ left: 0, top });
+ }
+ }
+ });
+ }
+});
+
+/** 重置路由 */
+export function resetRouter() {
+ router.getRoutes().forEach(route => {
+ const { name, meta } = route;
+ if (name && router.hasRoute(name) && meta?.backstage) {
+ router.removeRoute(name);
+ router.options.routes = formatTwoStageRoutes(
+ formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
+ );
+ }
+ });
+ usePermissionStoreHook().clearAllCachePage();
+}
+
+/** 路由白名单 */
+const whiteList = ["/login"];
+
+router.beforeEach((to: toRouteType, _from, next) => {
+ if (to.meta?.keepAlive) {
+ const newMatched = to.matched;
+ handleAliveRoute(newMatched, "add");
+ // 页面整体刷新和点击标签页刷新
+ if (_from.name === undefined || _from.name === "Redirect") {
+ handleAliveRoute(newMatched);
+ }
+ }
+ const userInfo = storageSession.getItem>(sessionKey);
+ NProgress.start();
+ const externalLink = isUrl(to?.name as string);
+ if (!externalLink) {
+ to.matched.some(item => {
+ if (!item.meta.title) return "";
+ const Title = getConfig().Title;
+ if (Title) document.title = `${item.meta.title} | ${Title}`;
+ else document.title = item.meta.title as string;
+ });
+ }
+ if (userInfo) {
+ // 无权限跳转403页面
+ if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
+ next({ path: "/error/403" });
+ }
+ if (_from?.name) {
+ // name为超链接
+ if (externalLink) {
+ openLink(to?.name as string);
+ NProgress.done();
+ } else {
+ next();
+ }
+ } else {
+ // 刷新
+ if (
+ usePermissionStoreHook().wholeMenus.length === 0 &&
+ to.path !== "/login"
+ )
+ initRouter().then((router: Router) => {
+ if (!useMultiTagsStoreHook().getMultiTagsCache) {
+ const { path } = to;
+ const index = findIndex(remainingRouter, v => {
+ return v.path == path;
+ });
+ const routes: any =
+ index === -1
+ ? router.options.routes[0].children
+ : router.options.routes;
+ const route = findRouteByPath(path, routes);
+ // query、params模式路由传参数的标签页不在此处处理
+ if (route && route.meta?.title) {
+ useMultiTagsStoreHook().handleTags("push", {
+ path: route.path,
+ name: route.name,
+ meta: route.meta
+ });
+ }
+ }
+ router.push(to.fullPath);
+ });
+ next();
+ }
+ } else {
+ if (to.path !== "/login") {
+ if (whiteList.indexOf(to.path) !== -1) {
+ next();
+ } else {
+ next({ path: "/login" });
+ }
+ } else {
+ next();
+ }
+ }
+});
+
+router.afterEach(() => {
+ NProgress.done();
+});
+
+export default router;
diff --git a/src/router/modules/error.ts b/src/router/modules/error.ts
new file mode 100644
index 0000000..c9d9b3b
--- /dev/null
+++ b/src/router/modules/error.ts
@@ -0,0 +1,39 @@
+import type { RouteConfigsTable } from "/#/index";
+
+const errorRouter: RouteConfigsTable = {
+ path: "/error",
+ redirect: "/error/403",
+ meta: {
+ icon: "information-line",
+ title: "异常页面",
+ rank: 9
+ },
+ children: [
+ {
+ path: "/error/403",
+ name: "403",
+ component: () => import("@/views/error/403.vue"),
+ meta: {
+ title: "403"
+ }
+ },
+ {
+ path: "/error/404",
+ name: "404",
+ component: () => import("@/views/error/404.vue"),
+ meta: {
+ title: "404"
+ }
+ },
+ {
+ path: "/error/500",
+ name: "500",
+ component: () => import("@/views/error/500.vue"),
+ meta: {
+ title: "500"
+ }
+ }
+ ]
+};
+
+export default errorRouter;
diff --git a/src/router/modules/home.ts b/src/router/modules/home.ts
new file mode 100644
index 0000000..5a25c93
--- /dev/null
+++ b/src/router/modules/home.ts
@@ -0,0 +1,26 @@
+import type { RouteConfigsTable } from "/#/index";
+const Layout = () => import("@/layout/index.vue");
+
+const homeRouter: RouteConfigsTable = {
+ path: "/",
+ name: "Home",
+ component: Layout,
+ redirect: "/welcome",
+ meta: {
+ icon: "home-filled",
+ title: "首页",
+ rank: 0
+ },
+ children: [
+ {
+ path: "/welcome",
+ name: "Welcome",
+ component: () => import("@/views/welcome/index.vue"),
+ meta: {
+ title: "首页"
+ }
+ }
+ ]
+};
+
+export default homeRouter;
diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts
new file mode 100644
index 0000000..332ebde
--- /dev/null
+++ b/src/router/modules/remaining.ts
@@ -0,0 +1,34 @@
+import type { RouteConfigsTable } from "/#/index";
+const Layout = () => import("@/layout/index.vue");
+
+const remainingRouter: Array = [
+ {
+ path: "/login",
+ name: "Login",
+ component: () => import("@/views/login/index.vue"),
+ meta: {
+ title: "登录",
+ showLink: false,
+ rank: 101
+ }
+ },
+ {
+ path: "/redirect",
+ component: Layout,
+ meta: {
+ icon: "home-filled",
+ title: "首页",
+ showLink: false,
+ rank: 104
+ },
+ children: [
+ {
+ path: "/redirect/:path(.*)",
+ name: "Redirect",
+ component: () => import("@/layout/redirect.vue")
+ }
+ ]
+ }
+];
+
+export default remainingRouter;
diff --git a/src/router/types.ts b/src/router/types.ts
new file mode 100644
index 0000000..139bf40
--- /dev/null
+++ b/src/router/types.ts
@@ -0,0 +1,9 @@
+import { RouteLocationNormalized } from "vue-router";
+
+export interface toRouteType extends RouteLocationNormalized {
+ meta: {
+ roles: Array;
+ keepAlive?: boolean;
+ dynamicLevel?: string;
+ };
+}
diff --git a/src/router/utils.ts b/src/router/utils.ts
new file mode 100644
index 0000000..562a383
--- /dev/null
+++ b/src/router/utils.ts
@@ -0,0 +1,348 @@
+import {
+ RouterHistory,
+ RouteRecordRaw,
+ RouteComponent,
+ createWebHistory,
+ createWebHashHistory,
+ RouteRecordNormalized
+} from "vue-router";
+import { router } from "./index";
+import { isProxy, toRaw } from "vue";
+import { loadEnv } from "../../build";
+import { useTimeoutFn } from "@vueuse/core";
+import { RouteConfigs } from "@/layout/types";
+import {
+ isString,
+ storageSession,
+ buildHierarchyTree,
+ isIncludeAllChildren
+} from "@pureadmin/utils";
+import { cloneDeep, intersection } from "lodash-unified";
+import { sessionKey, type DataInfo } from "@/utils/auth";
+import { usePermissionStoreHook } from "@/store/modules/permission";
+const IFrame = () => import("@/layout/frameView.vue");
+// https://cn.vitejs.dev/guide/features.html#glob-import
+const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
+
+// 动态路由
+import { getAsyncRoutes } from "@/api/routes";
+
+/** 按照路由中meta下的rank等级升序来排序路由 */
+function ascending(arr: any[]) {
+ arr.forEach(v => {
+ if (v?.meta?.rank === null) v.meta.rank = undefined;
+ if (v?.meta?.rank === 0) {
+ if (v.name !== "Home" && v.path !== "/") {
+ console.warn("rank only the home page can be 0");
+ }
+ }
+ });
+ return arr.sort(
+ (a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
+ return a?.meta?.rank - b?.meta?.rank;
+ }
+ );
+}
+
+/** 过滤meta中showLink为false的菜单 */
+function filterTree(data: RouteComponent[]) {
+ const newTree = cloneDeep(data).filter(
+ (v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false
+ );
+ newTree.forEach(
+ (v: { children }) => v.children && (v.children = filterTree(v.children))
+ );
+ return newTree;
+}
+
+/** 过滤children长度为0的的目录,当目录下没有菜单时,会过滤此目录,目录没有赋予roles权限,当目录下只要有一个菜单有显示权限,那么此目录就会显示 */
+function filterChildrenTree(data: RouteComponent[]) {
+ const newTree = cloneDeep(data).filter((v: any) => v?.children?.length !== 0);
+ newTree.forEach(
+ (v: { children }) => v.children && (v.children = filterTree(v.children))
+ );
+ return newTree;
+}
+
+/** 判断两个数组彼此是否存在相同值 */
+function isOneOfArray(a: Array, b: Array) {
+ return Array.isArray(a) && Array.isArray(b)
+ ? intersection(a, b).length > 0
+ ? true
+ : false
+ : true;
+}
+
+/** 从sessionStorage里取出当前登陆用户的角色roles,过滤无权限的菜单 */
+function filterNoPermissionTree(data: RouteComponent[]) {
+ const currentRoles =
+ storageSession.getItem>(sessionKey).roles ?? [];
+ const newTree = cloneDeep(data).filter((v: any) =>
+ isOneOfArray(v.meta?.roles, currentRoles)
+ );
+ newTree.forEach(
+ (v: any) => v.children && (v.children = filterNoPermissionTree(v.children))
+ );
+ return filterChildrenTree(newTree);
+}
+
+/** 批量删除缓存路由(keepalive) */
+function delAliveRoutes(delAliveRouteList: Array) {
+ delAliveRouteList.forEach(route => {
+ usePermissionStoreHook().cacheOperate({
+ mode: "delete",
+ name: route?.name
+ });
+ });
+}
+
+/** 通过path获取父级路径 */
+function getParentPaths(path: string, routes: RouteRecordRaw[]) {
+ // 深度遍历查找
+ function dfs(routes: RouteRecordRaw[], path: string, parents: string[]) {
+ for (let i = 0; i < routes.length; i++) {
+ const item = routes[i];
+ // 找到path则返回父级path
+ if (item.path === path) return parents;
+ // children不存在或为空则不递归
+ if (!item.children || !item.children.length) continue;
+ // 往下查找时将当前path入栈
+ parents.push(item.path);
+
+ if (dfs(item.children, path, parents).length) return parents;
+ // 深度遍历查找未找到时当前path 出栈
+ parents.pop();
+ }
+ // 未找到时返回空数组
+ return [];
+ }
+
+ return dfs(routes, path, []);
+}
+
+/** 查找对应path的路由信息 */
+function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
+ let res = routes.find((item: { path: string }) => item.path == path);
+ if (res) {
+ return isProxy(res) ? toRaw(res) : res;
+ } else {
+ for (let i = 0; i < routes.length; i++) {
+ if (
+ routes[i].children instanceof Array &&
+ routes[i].children.length > 0
+ ) {
+ res = findRouteByPath(path, routes[i].children);
+ if (res) {
+ return isProxy(res) ? toRaw(res) : res;
+ }
+ }
+ }
+ return null;
+ }
+}
+
+function addPathMatch() {
+ if (!router.hasRoute("pathMatch")) {
+ router.addRoute({
+ path: "/:pathMatch(.*)",
+ name: "pathMatch",
+ redirect: "/error/404"
+ });
+ }
+}
+
+/** 初始化路由 */
+function initRouter() {
+ return new Promise(resolve => {
+ getAsyncRoutes().then(({ data }) => {
+ if (data.length === 0) {
+ usePermissionStoreHook().handleWholeMenus(data);
+ resolve(router);
+ } else {
+ formatFlatteningRoutes(addAsyncRoutes(data)).map(
+ (v: RouteRecordRaw) => {
+ // 防止重复添加路由
+ if (
+ router.options.routes[0].children.findIndex(
+ value => value.path === v.path
+ ) !== -1
+ ) {
+ return;
+ } else {
+ // 切记将路由push到routes后还需要使用addRoute,这样路由才能正常跳转
+ router.options.routes[0].children.push(v);
+ // 最终路由进行升序
+ ascending(router.options.routes[0].children);
+ if (!router.hasRoute(v?.name)) router.addRoute(v);
+ const flattenRouters: any = router
+ .getRoutes()
+ .find(n => n.path === "/");
+ router.addRoute(flattenRouters);
+ }
+ resolve(router);
+ }
+ );
+ usePermissionStoreHook().handleWholeMenus(data);
+ }
+ addPathMatch();
+ });
+ });
+}
+
+/**
+ * 将多级嵌套路由处理成一维数组
+ * @param routesList 传入路由
+ * @returns 返回处理后的一维路由
+ */
+function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
+ if (routesList.length === 0) return routesList;
+ let hierarchyList = buildHierarchyTree(routesList);
+ for (let i = 0; i < hierarchyList.length; i++) {
+ if (hierarchyList[i].children) {
+ hierarchyList = hierarchyList
+ .slice(0, i + 1)
+ .concat(hierarchyList[i].children, hierarchyList.slice(i + 1));
+ }
+ }
+ return hierarchyList;
+}
+
+/**
+ * 一维数组处理成多级嵌套数组(三级及以上的路由全部拍成二级,keep-alive 只支持到二级缓存)
+ * https://github.com/xiaoxian521/vue-pure-admin/issues/67
+ * @param routesList 处理后的一维路由菜单数组
+ * @returns 返回将一维数组重新处理成规定路由的格式
+ */
+function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
+ if (routesList.length === 0) return routesList;
+ const newRoutesList: RouteRecordRaw[] = [];
+ routesList.forEach((v: RouteRecordRaw) => {
+ if (v.path === "/") {
+ newRoutesList.push({
+ component: v.component,
+ name: v.name,
+ path: v.path,
+ redirect: v.redirect,
+ meta: v.meta,
+ children: []
+ });
+ } else {
+ newRoutesList[0].children.push({ ...v });
+ }
+ });
+ return newRoutesList;
+}
+
+/** 处理缓存路由(添加、删除、刷新) */
+function handleAliveRoute(matched: RouteRecordNormalized[], mode?: string) {
+ switch (mode) {
+ case "add":
+ matched.forEach(v => {
+ usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
+ });
+ break;
+ case "delete":
+ usePermissionStoreHook().cacheOperate({
+ mode: "delete",
+ name: matched[matched.length - 1].name
+ });
+ break;
+ default:
+ usePermissionStoreHook().cacheOperate({
+ mode: "delete",
+ name: matched[matched.length - 1].name
+ });
+ useTimeoutFn(() => {
+ matched.forEach(v => {
+ usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
+ });
+ }, 100);
+ }
+}
+
+/** 过滤后端传来的动态路由 重新生成规范路由 */
+function addAsyncRoutes(arrRoutes: Array) {
+ if (!arrRoutes || !arrRoutes.length) return;
+ const modulesRoutesKeys = Object.keys(modulesRoutes);
+ arrRoutes.forEach((v: RouteRecordRaw) => {
+ // 将backstage属性加入meta,标识此路由为后端返回路由
+ v.meta.backstage = true;
+ // 父级的redirect属性取值:如果子级存在且父级的redirect属性不存在,默认取第一个子级的path;如果子级存在且父级的redirect属性存在,取存在的redirect属性,会覆盖默认值
+ if (v?.children && v.children.length && !v.redirect)
+ v.redirect = v.children[0].path;
+ // 父级的name属性取值:如果子级存在且父级的name属性不存在,默认取第一个子级的name;如果子级存在且父级的name属性存在,取存在的name属性,会覆盖默认值(注意:测试中发现父级的name不能和子级name重复,如果重复会造成重定向无效(跳转404),所以这里给父级的name起名的时候后面会自动加上`Parent`,避免重复)
+ if (v?.children && v.children.length && !v.name)
+ v.name = (v.children[0].name as string) + "Parent";
+ if (v.meta?.frameSrc) {
+ v.component = IFrame;
+ } else {
+ // 对后端传component组件路径和不传做兼容(如果后端传component组件路径,那么path可以随便写,如果不传,component组件路径会跟path保持一致)
+ const index = v?.component
+ ? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any))
+ : modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
+ v.component = modulesRoutes[modulesRoutesKeys[index]];
+ }
+ if (v?.children && v.children.length) {
+ addAsyncRoutes(v.children);
+ }
+ });
+ return arrRoutes;
+}
+
+/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
+function getHistoryMode(): RouterHistory {
+ const routerHistory = loadEnv().VITE_ROUTER_HISTORY;
+ // len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
+ const historyMode = routerHistory.split(",");
+ const leftMode = historyMode[0];
+ const rightMode = historyMode[1];
+ // no param
+ if (historyMode.length === 1) {
+ if (leftMode === "hash") {
+ return createWebHashHistory("");
+ } else if (leftMode === "h5") {
+ return createWebHistory("");
+ }
+ } //has param
+ else if (historyMode.length === 2) {
+ if (leftMode === "hash") {
+ return createWebHashHistory(rightMode);
+ } else if (leftMode === "h5") {
+ return createWebHistory(rightMode);
+ }
+ }
+}
+
+/** 获取当前页面按钮级别的权限 */
+function getAuths(): Array {
+ return router.currentRoute.value.meta.auths as Array;
+}
+
+/** 是否有按钮级别的权限 */
+function hasAuth(value: string | Array): boolean {
+ if (!value) return false;
+ /** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */
+ const metaAuths = getAuths();
+ const isAuths = isString(value)
+ ? metaAuths.includes(value)
+ : isIncludeAllChildren(value, metaAuths);
+ return isAuths ? true : false;
+}
+
+export {
+ hasAuth,
+ getAuths,
+ ascending,
+ filterTree,
+ initRouter,
+ isOneOfArray,
+ getHistoryMode,
+ addAsyncRoutes,
+ delAliveRoutes,
+ getParentPaths,
+ findRouteByPath,
+ handleAliveRoute,
+ formatTwoStageRoutes,
+ formatFlatteningRoutes,
+ filterNoPermissionTree
+};
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 0000000..a8dc752
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,9 @@
+import type { App } from "vue";
+import { createPinia } from "pinia";
+const store = createPinia();
+
+export function setupStore(app: App) {
+ app.use(store);
+}
+
+export { store };
diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts
new file mode 100644
index 0000000..b185c56
--- /dev/null
+++ b/src/store/modules/app.ts
@@ -0,0 +1,65 @@
+import { store } from "@/store";
+import { appType } from "./types";
+import { defineStore } from "pinia";
+import { getConfig } from "@/config";
+import type { StorageConfigs } from "/#/index";
+import { deviceDetection, storageLocal } from "@pureadmin/utils";
+
+export const useAppStore = defineStore({
+ id: "pure-app",
+ state: (): appType => ({
+ sidebar: {
+ opened:
+ storageLocal.getItem("responsive-layout")
+ ?.sidebarStatus ?? getConfig().SidebarStatus,
+ withoutAnimation: false,
+ isClickCollapse: false
+ },
+ // 这里的layout用于监听容器拖拉后恢复对应的导航模式
+ layout:
+ storageLocal.getItem("responsive-layout")?.layout ??
+ getConfig().Layout,
+ device: deviceDetection() ? "mobile" : "desktop"
+ }),
+ getters: {
+ getSidebarStatus() {
+ return this.sidebar.opened;
+ },
+ getDevice() {
+ return this.device;
+ }
+ },
+ actions: {
+ TOGGLE_SIDEBAR(opened?: boolean, resize?: string) {
+ const layout = storageLocal.getItem("responsive-layout");
+ if (opened && resize) {
+ this.sidebar.withoutAnimation = true;
+ this.sidebar.opened = true;
+ layout.sidebarStatus = true;
+ } else if (!opened && resize) {
+ this.sidebar.withoutAnimation = true;
+ this.sidebar.opened = false;
+ layout.sidebarStatus = false;
+ } else if (!opened && !resize) {
+ this.sidebar.withoutAnimation = false;
+ this.sidebar.opened = !this.sidebar.opened;
+ this.sidebar.isClickCollapse = !this.sidebar.opened;
+ layout.sidebarStatus = this.sidebar.opened;
+ }
+ storageLocal.setItem("responsive-layout", layout);
+ },
+ async toggleSideBar(opened?: boolean, resize?: string) {
+ await this.TOGGLE_SIDEBAR(opened, resize);
+ },
+ toggleDevice(device: string) {
+ this.device = device;
+ },
+ setLayout(layout) {
+ this.layout = layout;
+ }
+ }
+});
+
+export function useAppStoreHook() {
+ return useAppStore(store);
+}
diff --git a/src/store/modules/epTheme.ts b/src/store/modules/epTheme.ts
new file mode 100644
index 0000000..0df6311
--- /dev/null
+++ b/src/store/modules/epTheme.ts
@@ -0,0 +1,46 @@
+import { store } from "@/store";
+import { defineStore } from "pinia";
+import { getConfig } from "@/config";
+import type { StorageConfigs } from "/#/index";
+import { storageLocal } from "@pureadmin/utils";
+
+export const useEpThemeStore = defineStore({
+ id: "pure-epTheme",
+ state: () => ({
+ epThemeColor:
+ storageLocal.getItem("responsive-layout")?.epThemeColor ??
+ getConfig().EpThemeColor,
+ epTheme:
+ storageLocal.getItem("responsive-layout")?.theme ??
+ getConfig().Theme
+ }),
+ getters: {
+ getEpThemeColor() {
+ return this.epThemeColor;
+ },
+ /** 用于mix导航模式下hamburger-svg的fill属性 */
+ fill() {
+ if (this.epTheme === "light") {
+ return "#409eff";
+ } else if (this.epTheme === "yellow") {
+ return "#d25f00";
+ } else {
+ return "#fff";
+ }
+ }
+ },
+ actions: {
+ setEpThemeColor(newColor: string): void {
+ const layout = storageLocal.getItem("responsive-layout");
+ this.epTheme = layout?.theme;
+ this.epThemeColor = newColor;
+ if (!layout) return;
+ layout.epThemeColor = newColor;
+ storageLocal.setItem("responsive-layout", layout);
+ }
+ }
+});
+
+export function useEpThemeStoreHook() {
+ return useEpThemeStore(store);
+}
diff --git a/src/store/modules/multiTags.ts b/src/store/modules/multiTags.ts
new file mode 100644
index 0000000..8086ed6
--- /dev/null
+++ b/src/store/modules/multiTags.ts
@@ -0,0 +1,112 @@
+import { defineStore } from "pinia";
+import { store } from "@/store";
+import { isEqual } from "lodash-unified";
+import type { StorageConfigs } from "/#/index";
+import { routerArrays } from "@/layout/types";
+import { multiType, positionType } from "./types";
+import { isUrl, storageLocal } from "@pureadmin/utils";
+
+export const useMultiTagsStore = defineStore({
+ id: "pure-multiTags",
+ state: () => ({
+ // 存储标签页信息(路由信息)
+ multiTags: storageLocal.getItem("responsive-configure")
+ .multiTagsCache
+ ? storageLocal.getItem("responsive-tags")
+ : [...routerArrays],
+ multiTagsCache: storageLocal.getItem("responsive-configure")
+ .multiTagsCache
+ }),
+ getters: {
+ getMultiTagsCache() {
+ return this.multiTagsCache;
+ }
+ },
+ actions: {
+ multiTagsCacheChange(multiTagsCache: boolean) {
+ this.multiTagsCache = multiTagsCache;
+ if (multiTagsCache) {
+ storageLocal.setItem("responsive-tags", this.multiTags);
+ } else {
+ storageLocal.removeItem("responsive-tags");
+ }
+ },
+ tagsCache(multiTags) {
+ this.getMultiTagsCache &&
+ storageLocal.setItem("responsive-tags", multiTags);
+ },
+ handleTags(
+ mode: string,
+ value?: T | multiType,
+ position?: positionType
+ ): T {
+ switch (mode) {
+ case "equal":
+ this.multiTags = value;
+ this.tagsCache(this.multiTags);
+ break;
+ case "push":
+ {
+ const tagVal = value as multiType;
+ // 不添加到标签页
+ if (tagVal?.meta?.hiddenTag) return;
+ // 如果是外链无需添加信息到标签页
+ if (isUrl(tagVal?.name)) return;
+ // 如果title为空拒绝添加空信息到标签页
+ if (tagVal?.meta?.title.length === 0) return;
+ const tagPath = tagVal.path;
+ // 判断tag是否已存在
+ const tagHasExits = this.multiTags.some(tag => {
+ return tag.path === tagPath;
+ });
+
+ // 判断tag中的query键值是否相等
+ const tagQueryHasExits = this.multiTags.some(tag => {
+ return isEqual(tag?.query, tagVal?.query);
+ });
+
+ // 判断tag中的params键值是否相等
+ const tagParamsHasExits = this.multiTags.some(tag => {
+ return isEqual(tag?.params, tagVal?.params);
+ });
+
+ if (tagHasExits && tagQueryHasExits && tagParamsHasExits) return;
+
+ // 动态路由可打开的最大数量
+ const dynamicLevel = tagVal?.meta?.dynamicLevel ?? -1;
+ if (dynamicLevel > 0) {
+ if (
+ this.multiTags.filter(e => e?.path === tagPath).length >=
+ dynamicLevel
+ ) {
+ // 如果当前已打开的动态路由数大于dynamicLevel,替换第一个动态路由标签
+ const index = this.multiTags.findIndex(
+ item => item?.path === tagPath
+ );
+ index !== -1 && this.multiTags.splice(index, 1);
+ }
+ }
+ this.multiTags.push(value);
+ this.tagsCache(this.multiTags);
+ }
+ break;
+ case "splice":
+ if (!position) {
+ const index = this.multiTags.findIndex(v => v.path === value);
+ if (index === -1) return;
+ this.multiTags.splice(index, 1);
+ } else {
+ this.multiTags.splice(position?.startIndex, position?.length);
+ }
+ this.tagsCache(this.multiTags);
+ return this.multiTags;
+ case "slice":
+ return this.multiTags.slice(-1);
+ }
+ }
+ }
+});
+
+export function useMultiTagsStoreHook() {
+ return useMultiTagsStore(store);
+}
diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts
new file mode 100644
index 0000000..76b1ac0
--- /dev/null
+++ b/src/store/modules/permission.ts
@@ -0,0 +1,47 @@
+import { defineStore } from "pinia";
+import { store } from "@/store";
+import { cacheType } from "./types";
+import { constantMenus } from "@/router";
+import { ascending, filterTree, filterNoPermissionTree } from "@/router/utils";
+
+export const usePermissionStore = defineStore({
+ id: "pure-permission",
+ state: () => ({
+ // 静态路由生成的菜单
+ constantMenus,
+ // 整体路由生成的菜单(静态、动态)
+ wholeMenus: [],
+ // 缓存页面keepAlive
+ cachePageList: []
+ }),
+ actions: {
+ /** 组装整体路由生成的菜单 */
+ handleWholeMenus(routes: any[]) {
+ this.wholeMenus = filterNoPermissionTree(
+ filterTree(ascending(this.constantMenus.concat(routes)))
+ );
+ },
+ cacheOperate({ mode, name }: cacheType) {
+ switch (mode) {
+ case "add":
+ this.cachePageList.push(name);
+ this.cachePageList = [...new Set(this.cachePageList)];
+ break;
+ case "delete":
+ // eslint-disable-next-line no-case-declarations
+ const delIndex = this.cachePageList.findIndex(v => v === name);
+ delIndex !== -1 && this.cachePageList.splice(delIndex, 1);
+ break;
+ }
+ },
+ /** 清空缓存页面 */
+ clearAllCachePage() {
+ this.wholeMenus = [];
+ this.cachePageList = [];
+ }
+ }
+});
+
+export function usePermissionStoreHook() {
+ return usePermissionStore(store);
+}
diff --git a/src/store/modules/settings.ts b/src/store/modules/settings.ts
new file mode 100644
index 0000000..3cb6c34
--- /dev/null
+++ b/src/store/modules/settings.ts
@@ -0,0 +1,39 @@
+import { defineStore } from "pinia";
+import { store } from "@/store";
+import { setType } from "./types";
+import { getConfig } from "@/config";
+
+export const useSettingStore = defineStore({
+ id: "pure-setting",
+ state: (): setType => ({
+ title: getConfig().Title,
+ fixedHeader: getConfig().FixedHeader,
+ hiddenSideBar: getConfig().HiddenSideBar
+ }),
+ getters: {
+ getTitle() {
+ return this.title;
+ },
+ getFixedHeader() {
+ return this.fixedHeader;
+ },
+ getHiddenSideBar() {
+ return this.HiddenSideBar;
+ }
+ },
+ actions: {
+ CHANGE_SETTING({ key, value }) {
+ // eslint-disable-next-line no-prototype-builtins
+ if (this.hasOwnProperty(key)) {
+ this[key] = value;
+ }
+ },
+ changeSetting(data) {
+ this.CHANGE_SETTING(data);
+ }
+ }
+});
+
+export function useSettingStoreHook() {
+ return useSettingStore(store);
+}
diff --git a/src/store/modules/types.ts b/src/store/modules/types.ts
new file mode 100644
index 0000000..22fa404
--- /dev/null
+++ b/src/store/modules/types.ts
@@ -0,0 +1,44 @@
+import { RouteRecordName } from "vue-router";
+
+export type cacheType = {
+ mode: string;
+ name?: RouteRecordName;
+};
+
+export type positionType = {
+ startIndex?: number;
+ length?: number;
+};
+
+export type appType = {
+ sidebar: {
+ opened: boolean;
+ withoutAnimation: boolean;
+ // 判断是否手动点击Collapse
+ isClickCollapse: boolean;
+ };
+ layout: string;
+ device: string;
+};
+
+export type multiType = {
+ path: string;
+ parentPath: string;
+ name: string;
+ meta: any;
+ query?: object;
+ params?: object;
+};
+
+export type setType = {
+ title: string;
+ fixedHeader: boolean;
+ hiddenSideBar: boolean;
+};
+
+export type userType = {
+ username?: string;
+ roles?: Array;
+ verifyCode?: string;
+ currentPage?: number;
+};
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
new file mode 100644
index 0000000..7ec9d71
--- /dev/null
+++ b/src/store/modules/user.ts
@@ -0,0 +1,86 @@
+import { defineStore } from "pinia";
+import { store } from "@/store";
+import { userType } from "./types";
+import { routerArrays } from "@/layout/types";
+import { router, resetRouter } from "@/router";
+import { storageSession } from "@pureadmin/utils";
+import { getLogin, refreshTokenApi } from "@/api/user";
+import { UserResult, RefreshTokenResult } from "@/api/user";
+import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
+import { type DataInfo, setToken, removeToken, sessionKey } from "@/utils/auth";
+
+export const useUserStore = defineStore({
+ id: "pure-user",
+ state: (): userType => ({
+ // 用户名
+ username:
+ storageSession.getItem>(sessionKey)?.username ?? "",
+ // 页面级别权限
+ roles: storageSession.getItem>(sessionKey)?.roles ?? [],
+ // 前端生成的验证码(按实际需求替换)
+ verifyCode: "",
+ // 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码)
+ currentPage: 0
+ }),
+ actions: {
+ /** 存储用户名 */
+ SET_USERNAME(username: string) {
+ this.username = username;
+ },
+ /** 存储角色 */
+ SET_ROLES(roles: Array) {
+ this.roles = roles;
+ },
+ /** 存储前端生成的验证码 */
+ SET_VERIFYCODE(verifyCode: string) {
+ this.verifyCode = verifyCode;
+ },
+ /** 存储登录页面显示哪个组件 */
+ SET_CURRENTPAGE(value: number) {
+ this.currentPage = value;
+ },
+ /** 登入 */
+ async loginByUsername(data) {
+ return new Promise((resolve, reject) => {
+ getLogin(data)
+ .then(data => {
+ if (data) {
+ setToken(data.data);
+ resolve(data);
+ }
+ })
+ .catch(error => {
+ reject(error);
+ });
+ });
+ },
+ /** 前端登出(不调用接口) */
+ logOut() {
+ this.username = "";
+ this.roles = [];
+ removeToken();
+ router.push("/login");
+ useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
+ resetRouter();
+ },
+ /** 刷新`token` */
+ async handRefreshToken(data) {
+ return new Promise((resolve, reject) => {
+ refreshTokenApi(data)
+ .then(data => {
+ if (data) {
+ setToken(data.data);
+ resolve(data);
+ }
+ })
+ .catch(error => {
+ reject(error);
+ });
+ });
+ }
+ }
+});
+
+export function useUserStoreHook() {
+ return useUserStore(store);
+}
diff --git a/src/style/dark.scss b/src/style/dark.scss
new file mode 100644
index 0000000..3d4b05e
--- /dev/null
+++ b/src/style/dark.scss
@@ -0,0 +1,163 @@
+@import "element-plus/theme-chalk/src/dark/css-vars.scss";
+
+/* 暗黑模式适配 */
+html.dark {
+ /* 自定义深色背景颜色 */
+ // --el-bg-color: #020409;
+ $border-style: #303030;
+ $color-white: #fff;
+
+ .navbar,
+ .tags-view,
+ .contextmenu,
+ .sidebar-container,
+ .horizontal-header,
+ .sidebar-logo-container,
+ .horizontal-header .el-sub-menu__title,
+ .horizontal-header .submenu-title-noDropdown {
+ background: var(--el-bg-color) !important;
+ }
+
+ .app-main {
+ background: #020409 !important;
+ }
+
+ .frame {
+ filter: invert(0.9) hue-rotate(180deg);
+ }
+
+ .ant-tabs {
+ background: var(--el-bg-color);
+ color: $color-white;
+ }
+
+ /* 标签页 */
+ .tags-view {
+ .arrow-left,
+ .arrow-right {
+ box-shadow: none;
+ border-right: 1px solid $border-style;
+ }
+
+ .arrow-right {
+ border-left: 1px solid $border-style;
+ }
+ }
+
+ /* vxe-table */
+ .vxe-table--header-wrapper,
+ .vxe-table--body-wrapper {
+ color: var(--el-text-color-primary);
+ background: var(--el-bg-color) !important;
+ }
+
+ .vxe-table--render-default.border--full .vxe-header--column,
+ .vxe-table--render-default.border--full .vxe-body--column,
+ .vxe-table--render-default.border--full .vxe-footer--column {
+ background-image: linear-gradient(
+ var(--el-border-color-lighter),
+ var(--el-border-color-lighter)
+ ),
+ linear-gradient(
+ var(--el-border-color-lighter),
+ var(--el-border-color-lighter)
+ );
+ }
+
+ /* 表头 */
+ .vxe-table--header-wrapper {
+ background: #262727 !important;
+ }
+
+ .vxe-table--render-wrapper,
+ .vxe-table--main-wrapper {
+ border: none;
+ }
+
+ .vxe-pager.is--perfect,
+ .vxe-table--render-default .vxe-table--border-line {
+ border: 1px solid var(--el-border-color-lighter);
+ }
+
+ .vxe-table--header-border-line {
+ border-bottom: 1px solid var(--el-border-color-lighter) !important;
+ }
+
+ .vxe-body--row.row--hover,
+ .vxe-pager {
+ background-color: #262727;
+ }
+
+ .vxe-input--inner,
+ .vxe-pager .vxe-pager--jump-prev,
+ .vxe-pager .vxe-pager--prev-btn,
+ .vxe-pager .vxe-pager--next-btn,
+ .vxe-pager .vxe-pager--jump-next,
+ .vxe-pager .vxe-pager--num-btn,
+ .vxe-pager .vxe-pager--jump .vxe-pager--goto {
+ background-color: transparent;
+ color: var(--el-text-color-primary);
+ // outline: none !important;
+ }
+
+ .vxe-select-option--wrapper {
+ background: var(--el-bg-color) !important;
+ }
+
+ .vxe-select-option:not(.is--disabled).is--hover {
+ background: var(--el-color-primary-light-6) !important;
+ }
+
+ .vxe-modal--wrapper.type--modal .vxe-modal--box,
+ .vxe-modal--wrapper.type--alert .vxe-modal--box,
+ .vxe-modal--wrapper.type--confirm .vxe-modal--box,
+ .vxe-form {
+ background: var(--el-bg-color) !important;
+ }
+
+ .vxe-modal--box,
+ .vxe-modal--header {
+ border: none;
+ background: var(--el-bg-color) !important;
+ }
+
+ .vxe-modal--title,
+ .vxe-button--content,
+ .vxe-modal--header-title {
+ color: var(--el-text-color-primary);
+ }
+
+ .vxe-button.type--button.size--medium:hover {
+ background: var(--el-color-primary) !important;
+ }
+
+ .vxe-button {
+ background-color: transparent;
+ }
+
+ /* 项目配置面板 */
+ .right-panel-items {
+ .el-divider__text {
+ --el-bg-color: var(--el-bg-color);
+ }
+ .el-divider--horizontal {
+ border-top: none;
+ }
+ }
+
+ /* element-plus */
+ .el-table__cell {
+ background: var(--el-bg-color);
+ }
+ .el-card {
+ --el-card-bg-color: var(--el-bg-color);
+ // border: none !important;
+ }
+ .el-backtop {
+ --el-backtop-bg-color: var(--el-color-primary-light-9);
+ --el-backtop-hover-bg-color: var(--el-color-primary);
+ }
+ .el-dropdown-menu__item:not(.is-disabled):hover {
+ background: transparent;
+ }
+}
diff --git a/src/style/element-plus.scss b/src/style/element-plus.scss
new file mode 100644
index 0000000..a24c30f
--- /dev/null
+++ b/src/style/element-plus.scss
@@ -0,0 +1,64 @@
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+ font-weight: 400 !important;
+}
+
+.el-upload {
+ input[type="file"] {
+ display: none !important;
+ }
+}
+
+.el-upload__input {
+ display: none;
+}
+
+.upload-container {
+ .el-upload {
+ width: 100%;
+
+ .el-upload-dragger {
+ width: 100%;
+ height: 200px;
+ }
+ }
+}
+
+.el-dropdown-menu {
+ padding: 0 !important;
+}
+
+.el-range-separator {
+ box-sizing: content-box;
+}
+
+.is-dark {
+ z-index: 9999 !important;
+}
+
+/* 重置 el-button 中 icon 的 margin */
+.reset-margin [class*="el-icon"] + span {
+ margin-left: 2px !important;
+}
+
+/* 自定义 popover 的类名 */
+.pure-popper {
+ padding: 0 !important;
+}
+
+/* nprogress 适配 element-plus 的主题色 */
+#nprogress {
+ & .bar {
+ background-color: var(--el-color-primary) !important;
+ }
+
+ & .peg {
+ box-shadow: 0 0 10px var(--el-color-primary),
+ 0 0 5px var(--el-color-primary) !important;
+ }
+
+ & .spinner-icon {
+ border-top-color: var(--el-color-primary);
+ border-left-color: var(--el-color-primary);
+ }
+}
diff --git a/src/style/index.scss b/src/style/index.scss
new file mode 100644
index 0000000..00995d9
--- /dev/null
+++ b/src/style/index.scss
@@ -0,0 +1,26 @@
+@import "./mixin.scss";
+@import "./transition.scss";
+@import "./element-plus.scss";
+@import "./sidebar.scss";
+@import "./dark.scss";
+@import "./tailwind.css";
+
+/* 自定义全局 CssVar */
+:root {
+ --pure-transition-duration: 0.016s;
+}
+
+/* 灰色模式 */
+.html-grey {
+ filter: grayscale(100%);
+}
+
+/* 色弱模式 */
+.html-weakness {
+ filter: invert(80%);
+}
+
+/* 重置 vxe-table 中 pager 样式 */
+.vxe-pager .vxe-pager--num-btn:not(.is--disabled).is--active {
+ color: #fff !important;
+}
diff --git a/src/style/login.css b/src/style/login.css
new file mode 100644
index 0000000..781826c
--- /dev/null
+++ b/src/style/login.css
@@ -0,0 +1,90 @@
+.wave {
+ position: fixed;
+ height: 100%;
+ left: 0;
+ bottom: 0;
+ z-index: -1;
+}
+
+.login-container {
+ width: 100vw;
+ height: 100vh;
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ grid-gap: 18rem;
+ padding: 0 2rem;
+}
+
+.img {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+}
+
+.img img {
+ width: 500px;
+}
+
+.login-box {
+ display: flex;
+ align-items: center;
+ text-align: center;
+}
+
+.login-form {
+ width: 360px;
+}
+
+.avatar {
+ width: 350px;
+ height: 80px;
+}
+
+.login-form h2 {
+ text-transform: uppercase;
+ margin: 15px 0;
+ color: #999;
+ font: bold 200% Consolas, Monaco, monospace;
+}
+
+@media screen and (max-width: 1180px) {
+ .login-container {
+ grid-gap: 9rem;
+ }
+
+ .login-form {
+ width: 290px;
+ }
+
+ .login-form h2 {
+ font-size: 2.4rem;
+ margin: 8px 0;
+ }
+
+ .img img {
+ width: 360px;
+ }
+
+ .avatar {
+ width: 280px;
+ height: 80px;
+ }
+}
+
+@media screen and (max-width: 968px) {
+ .wave {
+ display: none;
+ }
+
+ .img {
+ display: none;
+ }
+
+ .login-container {
+ grid-template-columns: 1fr;
+ }
+
+ .login-box {
+ justify-content: center;
+ }
+}
diff --git a/src/style/mixin.scss b/src/style/mixin.scss
new file mode 100644
index 0000000..fb8e37d
--- /dev/null
+++ b/src/style/mixin.scss
@@ -0,0 +1,28 @@
+@mixin clearfix {
+ &::after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+}
+
+@mixin relative {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+@mixin scrollBar {
+ &::-webkit-scrollbar-track-piece {
+ background: #d3dce6;
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #99a9bf;
+ border-radius: 20px;
+ }
+}
diff --git a/src/style/reset.scss b/src/style/reset.scss
new file mode 100644
index 0000000..2382a76
--- /dev/null
+++ b/src/style/reset.scss
@@ -0,0 +1,277 @@
+*,
+::before,
+::after {
+ box-sizing: border-box;
+ border-width: 0;
+ border-style: solid;
+ border-color: currentColor;
+}
+
+#app {
+ width: 100%;
+ height: 100%;
+}
+
+html {
+ line-height: 1.5;
+ -webkit-text-size-adjust: 100%;
+ -moz-tab-size: 4;
+ tab-size: 4;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ line-height: inherit;
+ width: 100%;
+ height: 100%;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizelegibility;
+ font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
+ "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
+}
+
+hr {
+ height: 0;
+ color: inherit;
+ border-top-width: 1px;
+}
+
+abbr:where([title]) {
+ text-decoration: underline dotted;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-size: inherit;
+ font-weight: inherit;
+}
+
+a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+code,
+kbd,
+samp,
+pre {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
+ "Liberation Mono", "Courier New", monospace;
+ font-size: 1em;
+}
+
+small {
+ font-size: 80%;
+}
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+table {
+ text-indent: 0;
+ border-color: inherit;
+ border-collapse: collapse;
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit;
+ font-size: 100%;
+ line-height: inherit;
+ color: inherit;
+ margin: 0;
+ padding: 0;
+}
+
+button,
+select {
+ text-transform: none;
+}
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+ background-image: none;
+}
+
+:-moz-focusring {
+ outline: auto;
+}
+
+:-moz-ui-invalid {
+ box-shadow: none;
+}
+
+progress {
+ vertical-align: baseline;
+}
+
+::-webkit-inner-spin-button,
+::-webkit-outer-spin-button {
+ height: auto;
+}
+
+[type="search"] {
+ -webkit-appearance: textfield;
+ outline-offset: -2px;
+}
+
+::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button;
+ font: inherit;
+}
+
+summary {
+ display: list-item;
+}
+
+blockquote,
+dl,
+dd,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+hr,
+figure,
+p,
+pre {
+ margin: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+}
+
+legend {
+ padding: 0;
+}
+
+ol,
+ul,
+menu {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+textarea {
+ resize: vertical;
+}
+
+input::placeholder,
+textarea::placeholder {
+ opacity: 1;
+ color: #9ca3af;
+}
+
+button,
+[role="button"] {
+ cursor: pointer;
+}
+
+:disabled {
+ cursor: default;
+}
+
+img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+ display: block;
+ vertical-align: middle;
+}
+
+img,
+video {
+ max-width: 100%;
+ height: auto;
+}
+
+[hidden] {
+ display: none;
+}
+
+.dark {
+ color-scheme: dark;
+}
+
+label {
+ font-weight: 700;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: inherit;
+}
+
+a:focus,
+a:active {
+ outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+ cursor: pointer;
+ color: inherit;
+ text-decoration: none;
+}
+
+div:focus {
+ outline: none;
+}
+
+.clearfix {
+ &::after {
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: " ";
+ clear: both;
+ height: 0;
+ }
+}
diff --git a/src/style/sidebar.scss b/src/style/sidebar.scss
new file mode 100644
index 0000000..e833e8f
--- /dev/null
+++ b/src/style/sidebar.scss
@@ -0,0 +1,647 @@
+/* $sideBarWidth: vertical 模式下主体内容距离网页文档左侧的距离 */
+@mixin merge-style($sideBarWidth) {
+ $menuActiveText: #7a80b4;
+
+ @media screen and (min-width: 150px) and (max-width: 420px) {
+ .app-main-nofixed-header {
+ overflow-y: hidden;
+ }
+ }
+ @media screen and (min-width: 420px) {
+ .app-main-nofixed-header {
+ overflow: hidden;
+ }
+ }
+
+ .sub-menu-icon {
+ vertical-align: middle;
+ font-size: 18px;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ margin-right: 5px;
+ }
+
+ .set-icon {
+ height: 48px;
+ width: 40px;
+ display: flex;
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .main-container {
+ height: 100vh;
+ min-height: 100%;
+ /* main-content 属性动画 */
+ transition: margin-left var(--pure-transition-duration);
+ margin-left: $sideBarWidth;
+ position: relative;
+ background: #f0f2f5;
+
+ .el-scrollbar__wrap {
+ overflow: auto;
+ height: 100%;
+ }
+ }
+
+ .fixed-header {
+ position: fixed;
+ top: 0;
+ right: 0;
+ z-index: 998;
+ width: calc(100% - 210px);
+ /* fixed-header 属性左上角动画 */
+ transition: width var(--pure-transition-duration);
+ }
+
+ .main-hidden {
+ margin-left: 0 !important;
+
+ .fixed-header {
+ width: 100% !important;
+
+ + .app-main {
+ padding-top: 37px !important;
+ }
+ }
+ }
+
+ .el-popper.is-light {
+ border: none !important;
+ }
+
+ .sidebar-container {
+ /* 展开动画 */
+ transition: width var(--pure-transition-duration);
+ width: $sideBarWidth !important;
+ background: $menuBg;
+ height: 100%;
+ position: fixed;
+ font-size: 0;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1001;
+ overflow: hidden;
+ box-shadow: 0 0 1px #888;
+
+ .scrollbar-wrapper {
+ overflow-x: hidden !important;
+ }
+
+ .el-scrollbar__bar.is-vertical {
+ right: 0;
+ }
+
+ .el-scrollbar {
+ height: calc(100% - 44px);
+ }
+
+ &.has-logo {
+ .el-scrollbar.pc {
+ /* logo: 48px、leftCollapse: 40px、leftCollapse-shadow: 4px */
+ height: calc(100% - 92px);
+ }
+ .el-scrollbar.mobile {
+ height: 100%;
+ }
+ }
+
+ .is-horizontal {
+ display: none;
+ }
+
+ a {
+ display: inline-block;
+ display: flex;
+ padding-left: 10px;
+ flex-wrap: wrap;
+ width: 100%;
+ }
+
+ .el-menu {
+ border: none;
+ height: 100%;
+ background-color: transparent !important;
+ }
+
+ .el-menu-item,
+ .el-sub-menu__title {
+ height: 50px;
+ color: $menuText;
+ background-color: transparent !important;
+
+ &:hover {
+ color: $menuTitleHover !important;
+ }
+
+ div,
+ span {
+ height: 50px;
+ line-height: 50px;
+ }
+ }
+
+ .submenu-title-noDropdown,
+ .el-sub-menu__title {
+ &:hover {
+ background-color: transparent;
+ }
+ }
+
+ .is-active > .el-sub-menu__title,
+ .is-active.submenu-title-noDropdown {
+ color: $subMenuActiveText !important;
+
+ i {
+ color: $subMenuActiveText !important;
+ }
+ }
+
+ .is-active {
+ transition: color 0.3s;
+ color: $subMenuActiveText !important;
+ }
+
+ .el-menu .el-menu--inline .el-sub-menu__title,
+ & .el-sub-menu .el-menu-item {
+ font-size: 12px;
+ min-width: $sideBarWidth !important;
+ background-color: $subMenuBg !important;
+ }
+
+ /* 无子集的激活菜单背景 */
+ .is-active.submenu-title-noDropdown.outer-most {
+ background: $subMenuActiveBg !important;
+ }
+
+ /* 有子集的激活菜单背景 */
+ .is-active.nest-menu {
+ background: $subMenuActiveBg !important;
+ }
+ }
+
+ .horizontal-header {
+ display: flex;
+ justify-content: space-around;
+ background: $menuBg;
+ width: 100%;
+ height: 48px;
+ align-items: center;
+
+ .horizontal-header-left {
+ display: flex;
+ height: 100%;
+ width: auto;
+ min-width: 200px;
+ align-items: center;
+ padding-left: 10px;
+ cursor: pointer;
+ transition: all 0.125s ease;
+
+ i {
+ font-size: 30px;
+ color: #1890ff;
+ margin-right: 4px;
+ }
+
+ h4 {
+ font-size: 16px;
+ font-weight: 700;
+ color: $subMenuActiveText;
+ transition: all 0.5s;
+ }
+ }
+
+ .horizontal-header-menu {
+ height: 100%;
+ min-width: 0;
+ flex: 1;
+ align-items: center;
+ }
+
+ .horizontal-header-right {
+ display: flex;
+ min-width: 340px;
+ align-items: center;
+ color: $subMenuActiveText;
+ justify-content: flex-end;
+
+ /* 搜索 */
+ .search-container,
+ /* 告警 */
+ .dropdown-badge,
+ /* 全屏 */
+ .screen-full,
+ /* 登录名 */
+ .el-dropdown-link,
+ /* 设置 */
+ .set-icon {
+ &:hover {
+ background: $menuHover;
+ }
+ }
+
+ .dropdown-badge {
+ height: 48px;
+ color: $subMenuActiveText;
+ }
+
+ .el-dropdown-link {
+ height: 48px;
+ padding: 10px;
+ display: flex;
+ cursor: pointer;
+ align-items: center;
+ justify-content: space-around;
+ color: $subMenuActiveText;
+
+ p {
+ font-size: 14px;
+ }
+
+ img {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ }
+ }
+ }
+
+ .el-menu {
+ border: none;
+ height: 100%;
+ width: 100% !important;
+ background-color: transparent;
+ }
+
+ .el-menu-item,
+ .el-sub-menu__title {
+ color: $menuText;
+
+ &:hover {
+ color: $menuTitleHover !important;
+ }
+ }
+
+ .submenu-title-noDropdown,
+ .el-sub-menu__title {
+ height: 48px;
+ line-height: 48px;
+ background: $menuBg;
+
+ svg {
+ position: static !important;
+ }
+ }
+
+ .is-active > .el-sub-menu__title,
+ .is-active.submenu-title-noDropdown {
+ color: $subMenuActiveText !important;
+
+ i {
+ color: $subMenuActiveText !important;
+ }
+ }
+
+ .is-active {
+ transition: color 0.3s;
+ color: $subMenuActiveText !important;
+ }
+ }
+
+ /* vertical 菜单折叠 */
+ .el-menu--vertical {
+ .el-menu--popup {
+ background-color: $subMenuBg !important;
+
+ .el-menu-item {
+ span {
+ font-size: 12px;
+ }
+ }
+ }
+
+ & > .el-menu {
+ i {
+ margin-right: 20px;
+ }
+ }
+
+ .is-active > .el-sub-menu__title,
+ .is-active.submenu-title-noDropdown {
+ color: $subMenuActiveText !important;
+
+ i {
+ color: $subMenuActiveText !important;
+ }
+ }
+
+ /* 子菜单中还有子菜单 */
+ .el-menu .el-sub-menu__title {
+ font-size: 12px;
+ min-width: $sideBarWidth !important;
+ background-color: $subMenuBg !important;
+ }
+
+ .el-menu-item,
+ .el-sub-menu__title {
+ height: 50px;
+ line-height: 50px;
+ color: $menuText;
+ background-color: $subMenuBg;
+
+ &:hover {
+ color: $menuTitleHover !important;
+ }
+ }
+
+ .is-active {
+ transition: color 0.3s;
+ color: $subMenuActiveText !important;
+ }
+
+ .el-menu-item.is-active.nest-menu {
+ background: $subMenuActiveBg !important;
+ }
+
+ .el-menu-item,
+ .el-sub-menu {
+ // i {
+ // width: 20px;
+ // text-align: center;
+ // font-size: 16px;
+ // }
+
+ // i.fa {
+ // margin-right: 5px;
+ // font-size: 16px;
+ // }
+ .el-menu-tooltip__trigger {
+ width: 54px;
+ padding: 18px !important;
+ }
+ }
+ }
+
+ /* horizontal 菜单 */
+ .el-menu--horizontal {
+ & > .el-sub-menu .el-sub-menu__icon-arrow {
+ position: static !important;
+ margin-top: 0;
+ }
+
+ .el-menu--popup {
+ background-color: $subMenuBg !important;
+
+ .el-menu-item {
+ color: $menuText;
+ background-color: $subMenuBg;
+
+ span {
+ font-size: 12px;
+ }
+ }
+
+ .el-sub-menu__title {
+ color: $menuText;
+ }
+ }
+
+ /* 无子菜单时激活 border-bottom */
+ .router-link-exact-active > .submenu-title-noDropdown {
+ height: 60px;
+ border-bottom: 2px solid var(--el-menu-active-color);
+ }
+
+ /* 子菜单中还有子菜单 */
+ .el-menu .el-sub-menu__title {
+ font-size: 12px;
+ min-width: $sideBarWidth !important;
+ background-color: $subMenuBg !important;
+
+ &:hover {
+ color: $menuTitleHover !important;
+ }
+ }
+
+ .is-active > .el-sub-menu__title,
+ .is-active.submenu-title-noDropdown {
+ color: $subMenuActiveText !important;
+
+ i {
+ color: $subMenuActiveText !important;
+ }
+ }
+
+ .nest-menu .el-sub-menu > .el-sub-menu__title,
+ .el-menu-item {
+ &:hover {
+ color: $menuTitleHover !important;
+ }
+ }
+
+ /* 有子集的激活菜单背景 */
+ .is-active.nest-menu {
+ background: $subMenuActiveBg !important;
+ }
+
+ .el-menu-item.is-active {
+ transition: color 0.3s;
+ color: $subMenuActiveText !important;
+ }
+ }
+
+ .el-menu--collapse .el-menu .el-sub-menu {
+ min-width: $sideBarWidth !important;
+ }
+
+ /* 有子菜单 */
+ .el-menu--collapse
+ .is-active.outer-most.el-sub-menu
+ > .el-sub-menu__title::before,
+ /* 无子菜单 */
+ .el-menu--collapse .is-active.submenu-title-noDropdown.outer-most::before {
+ position: absolute;
+ top: 0;
+ left: 2px;
+ width: 2px;
+ height: 100%;
+ background-color: $menuActiveBefore;
+ content: "";
+ clear: both;
+ transition: all 0.125s ease-in-out;
+ transform: translateY(0);
+ }
+
+ .el-menu--collapse .outer-most.el-sub-menu > .el-sub-menu__title::before,
+ .el-menu--collapse .submenu-title-noDropdown.outer-most::before {
+ content: "";
+ display: block;
+ position: absolute;
+ height: 0;
+ width: 3px;
+ transform: translateY(-50%);
+ top: 50%;
+ }
+
+ /* 手机端 */
+ .mobile {
+ .fixed-header {
+ width: 100% !important;
+ transition: width var(--pure-transition-duration);
+ }
+
+ .main-container {
+ margin-left: 0 !important;
+ }
+
+ .sidebar-container {
+ transition: transform var(--pure-transition-duration);
+ width: $sideBarWidth;
+ }
+
+ &.hideSidebar {
+ .sidebar-container {
+ pointer-events: none;
+ transition-duration: 0.3s;
+ transform: translate3d(-$sideBarWidth, 0, 0);
+ }
+ }
+ }
+}
+
+body[layout="vertical"] {
+ $sideBarWidth: 210px;
+ @include merge-style($sideBarWidth);
+
+ .el-menu--collapse {
+ width: 54px;
+ }
+
+ .sidebar-logo-container {
+ background: $sidebarLogo;
+ }
+
+ .hideSidebar {
+ .fixed-header {
+ width: calc(100% - 54px);
+ transition: width var(--pure-transition-duration);
+ }
+
+ .sidebar-container {
+ transition: width 0.125s;
+ width: 54px !important;
+
+ .is-active.submenu-title-noDropdown.outer-most {
+ background: transparent !important;
+ }
+ }
+
+ .main-container {
+ margin-left: 54px;
+ }
+
+ /* 菜单折叠 */
+ .el-menu--collapse {
+ .el-sub-menu {
+ & > .el-sub-menu__title {
+ & > span {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ }
+ }
+
+ .submenu-title-noDropdown {
+ background: transparent !important;
+ }
+
+ .el-sub-menu__title {
+ padding: 0 18px !important;
+ }
+ }
+
+ .sub-menu-icon {
+ margin-right: 0;
+ }
+ }
+
+ /* 搜索 */
+ .search-container,
+ /* 告警 */
+ .dropdown-badge,
+ /* 全屏 */
+ .screen-full,
+ /* 登录名 */
+ .el-dropdown-link,
+ /* 设置 */
+ .set-icon {
+ &:hover {
+ background: #f6f6f6;
+ }
+ }
+}
+
+body[layout="horizontal"] {
+ $sideBarWidth: 0;
+ @include merge-style($sideBarWidth);
+
+ .fixed-header {
+ width: 100%;
+ transition: none !important;
+ }
+}
+
+body[layout="mix"] {
+ $sideBarWidth: 210px;
+ @include merge-style($sideBarWidth);
+
+ .el-menu--collapse {
+ width: 54px;
+ }
+
+ .el-menu {
+ --el-menu-hover-bg-color: transparent !important;
+ }
+
+ .hideSidebar {
+ .fixed-header {
+ width: calc(100% - 54px);
+ transition: width var(--pure-transition-duration);
+ }
+
+ .sidebar-container {
+ transition: width 0.125s;
+ width: 54px !important;
+
+ .is-active.submenu-title-noDropdown.outer-most {
+ background: transparent !important;
+ }
+ }
+
+ .main-container {
+ margin-left: 54px;
+ }
+
+ /* 菜单折叠 */
+ .el-menu--collapse {
+ .el-sub-menu {
+ & > .el-sub-menu__title {
+ & > span {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/style/tailwind.css b/src/style/tailwind.css
new file mode 100644
index 0000000..3e48b68
--- /dev/null
+++ b/src/style/tailwind.css
@@ -0,0 +1,21 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer components {
+ .flex-c {
+ @apply flex justify-center items-center;
+ }
+
+ .flex-ac {
+ @apply flex justify-around items-center;
+ }
+
+ .flex-bc {
+ @apply flex justify-between items-center;
+ }
+
+ .navbar-bg-hover {
+ @apply dark:text-white dark:hover:!bg-[#242424];
+ }
+}
diff --git a/src/style/transition.scss b/src/style/transition.scss
new file mode 100644
index 0000000..453c1a7
--- /dev/null
+++ b/src/style/transition.scss
@@ -0,0 +1,55 @@
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+ opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+ transition: all 0.5s;
+}
+
+.fade-transform-enter-from {
+ opacity: 0;
+ transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+ opacity: 0;
+ transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+ transition: all 0.5s;
+}
+
+.breadcrumb-enter-from,
+.breadcrumb-leave-active {
+ opacity: 0;
+ transform: translateX(20px);
+}
+
+.breadcrumb-leave-active {
+ position: absolute;
+}
+
+/**
+ * @description 重置el-menu的展开收起动画时长
+ * @see {@link https://github.com/element-plus/element-plus/issues/4509#issuecomment-980165001}
+ */
+.outer-most .el-collapse-transition-leave-active,
+.outer-most .el-collapse-transition-enter-active {
+ transition: 0.12s all ease-in-out !important;
+}
+
+.horizontal-collapse-transition {
+ transition: var(--pure-transition-duration) all !important;
+}
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
new file mode 100644
index 0000000..4aa28cc
--- /dev/null
+++ b/src/utils/auth.ts
@@ -0,0 +1,72 @@
+import Cookies from "js-cookie";
+import { storageSession } from "@pureadmin/utils";
+import { useUserStoreHook } from "@/store/modules/user";
+
+export interface DataInfo {
+ /** token */
+ accessToken: string;
+ /** `accessToken`的过期时间(时间戳) */
+ expires: T;
+ /** 用于调用刷新accessToken的接口时所需的token */
+ refreshToken: string;
+ /** 用户名 */
+ username?: string;
+ /** 当前登陆用户的角色 */
+ roles?: Array;
+}
+
+export const sessionKey = "user-info";
+export const TokenKey = "authorized-token";
+
+/** 获取`token` */
+export function getToken(): DataInfo {
+ // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
+ return Cookies.get(TokenKey)
+ ? JSON.parse(Cookies.get(TokenKey))
+ : storageSession.getItem(sessionKey);
+}
+
+/**
+ * @description 设置`token`以及一些必要信息并采用无感刷新`token`方案
+ * 无感刷新:后端返回`accessToken`(访问接口使用的`token`)、`refreshToken`(用于调用刷新`accessToken`的接口时所需的`token`,`refreshToken`的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间)
+ * 将`accessToken`、`expires`这两条信息放在key值为authorized-token的cookie里(过期自动销毁)
+ * 将`username`、`roles`、`refreshToken`、`expires`这四条信息放在key值为`user-info`的sessionStorage里(浏览器关闭自动销毁)
+ */
+export function setToken(data: DataInfo) {
+ let expires = 0;
+ const { accessToken, refreshToken } = data;
+ expires = new Date(data.expires).getTime();
+ const cookieString = JSON.stringify({ accessToken, expires });
+
+ expires > 0
+ ? Cookies.set(TokenKey, cookieString, {
+ expires: (expires - Date.now()) / 86400000
+ })
+ : Cookies.set(TokenKey, cookieString);
+
+ function setSessionKey(username: string, roles: Array) {
+ useUserStoreHook().SET_USERNAME(username);
+ useUserStoreHook().SET_ROLES(roles);
+ storageSession.setItem(sessionKey, {
+ refreshToken,
+ expires,
+ username,
+ roles
+ });
+ }
+
+ if (data.username && data.roles) {
+ const { username, roles } = data;
+ setSessionKey(username, roles);
+ } else {
+ const { username, roles } =
+ storageSession.getItem>(sessionKey);
+ setSessionKey(username, roles);
+ }
+}
+
+/** 删除`token`以及key值为`user-info`的session信息 */
+export function removeToken() {
+ Cookies.remove(TokenKey);
+ sessionStorage.removeItem(sessionKey);
+}
diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts
new file mode 100644
index 0000000..98b7f36
--- /dev/null
+++ b/src/utils/http/index.ts
@@ -0,0 +1,178 @@
+import Axios, {
+ AxiosInstance,
+ AxiosRequestConfig,
+ CustomParamsSerializer
+} from "axios";
+import {
+ PureHttpError,
+ RequestMethods,
+ PureHttpResponse,
+ PureHttpRequestConfig
+} from "./types.d";
+import { stringify } from "qs";
+import NProgress from "../progress";
+// import { loadEnv } from "@build/index";
+import { getToken } from "@/utils/auth";
+import { useUserStoreHook } from "@/store/modules/user";
+
+// 加载环境变量 VITE_PROXY_DOMAIN(开发环境) VITE_PROXY_DOMAIN_REAL(打包后的线上环境)
+// const { VITE_PROXY_DOMAIN, VITE_PROXY_DOMAIN_REAL } = loadEnv();
+
+// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
+const defaultConfig: AxiosRequestConfig = {
+ // baseURL:
+ // process.env.NODE_ENV === "production"
+ // ? VITE_PROXY_DOMAIN_REAL
+ // : VITE_PROXY_DOMAIN,
+ // 当前使用mock模拟请求,将baseURL制空,如果你的环境用到了http请求,请删除下面的baseURL启用上面的baseURL,并将第14行、19行代码注释取消
+ baseURL: "",
+ timeout: 10000,
+ headers: {
+ Accept: "application/json, text/plain, */*",
+ "Content-Type": "application/json",
+ "X-Requested-With": "XMLHttpRequest"
+ },
+ // 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
+ paramsSerializer: {
+ serialize: stringify as unknown as CustomParamsSerializer
+ }
+};
+
+class PureHttp {
+ constructor() {
+ this.httpInterceptorsRequest();
+ this.httpInterceptorsResponse();
+ }
+ /** 初始化配置对象 */
+ private static initConfig: PureHttpRequestConfig = {};
+
+ /** 保存当前Axios实例对象 */
+ private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
+
+ /** 请求拦截 */
+ private httpInterceptorsRequest(): void {
+ PureHttp.axiosInstance.interceptors.request.use(
+ async (config: PureHttpRequestConfig) => {
+ const $config = config;
+ // 开启进度条动画
+ NProgress.start();
+ // 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
+ if (typeof config.beforeRequestCallback === "function") {
+ config.beforeRequestCallback($config);
+ return $config;
+ }
+ if (PureHttp.initConfig.beforeRequestCallback) {
+ PureHttp.initConfig.beforeRequestCallback($config);
+ return $config;
+ }
+ /** 请求白名单,放置一些不需要token的接口(通过设置请求白名单,防止token过期后再请求造成的死循环问题) */
+ const whiteList = ["/refreshToken", "/login"];
+ return whiteList.some(v => config.url.indexOf(v) > -1)
+ ? config
+ : new Promise(resolve => {
+ const data = getToken();
+ if (data) {
+ const now = new Date().getTime();
+ const expired = parseInt(data.expires) - now <= 0;
+ if (expired) {
+ // token过期刷新
+ useUserStoreHook()
+ .handRefreshToken({ refreshToken: data.refreshToken })
+ .then(res => {
+ config.headers["Authorization"] =
+ "Bearer " + res.data.accessToken;
+ resolve($config);
+ });
+ } else {
+ config.headers["Authorization"] =
+ "Bearer " + data.accessToken;
+ resolve($config);
+ }
+ } else {
+ resolve($config);
+ }
+ });
+ },
+ error => {
+ return Promise.reject(error);
+ }
+ );
+ }
+
+ /** 响应拦截 */
+ private httpInterceptorsResponse(): void {
+ const instance = PureHttp.axiosInstance;
+ instance.interceptors.response.use(
+ (response: PureHttpResponse) => {
+ const $config = response.config;
+ // 关闭进度条动画
+ NProgress.done();
+ // 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
+ if (typeof $config.beforeResponseCallback === "function") {
+ $config.beforeResponseCallback(response);
+ return response.data;
+ }
+ if (PureHttp.initConfig.beforeResponseCallback) {
+ PureHttp.initConfig.beforeResponseCallback(response);
+ return response.data;
+ }
+ return response.data;
+ },
+ (error: PureHttpError) => {
+ const $error = error;
+ $error.isCancelRequest = Axios.isCancel($error);
+ // 关闭进度条动画
+ NProgress.done();
+ // 所有的响应异常 区分来源为取消请求/非取消请求
+ return Promise.reject($error);
+ }
+ );
+ }
+
+ /** 通用请求工具函数 */
+ public request(
+ method: RequestMethods,
+ url: string,
+ param?: AxiosRequestConfig,
+ axiosConfig?: PureHttpRequestConfig
+ ): Promise {
+ const config = {
+ method,
+ url,
+ ...param,
+ ...axiosConfig
+ } as PureHttpRequestConfig;
+
+ // 单独处理自定义请求/响应回掉
+ return new Promise((resolve, reject) => {
+ PureHttp.axiosInstance
+ .request(config)
+ .then((response: undefined) => {
+ resolve(response);
+ })
+ .catch(error => {
+ reject(error);
+ });
+ });
+ }
+
+ /** 单独抽离的post工具函数 */
+ public post(
+ url: string,
+ params?: T,
+ config?: PureHttpRequestConfig
+ ): Promise {
+ return this.request
("post", url, params, config);
+ }
+
+ /** 单独抽离的get工具函数 */
+ public get(
+ url: string,
+ params?: T,
+ config?: PureHttpRequestConfig
+ ): Promise {
+ return this.request
("get", url, params, config);
+ }
+}
+
+export const http = new PureHttp();
diff --git a/src/utils/http/types.d.ts b/src/utils/http/types.d.ts
new file mode 100644
index 0000000..197b152
--- /dev/null
+++ b/src/utils/http/types.d.ts
@@ -0,0 +1,47 @@
+import Axios, {
+ Method,
+ AxiosError,
+ AxiosResponse,
+ AxiosRequestConfig
+} from "axios";
+
+export type resultType = {
+ accessToken?: string;
+};
+
+export type RequestMethods = Extract<
+ Method,
+ "get" | "post" | "put" | "delete" | "patch" | "option" | "head"
+>;
+
+export interface PureHttpError extends AxiosError {
+ isCancelRequest?: boolean;
+}
+
+export interface PureHttpResponse extends AxiosResponse {
+ config: PureHttpRequestConfig;
+}
+
+export interface PureHttpRequestConfig extends AxiosRequestConfig {
+ beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
+ beforeResponseCallback?: (response: PureHttpResponse) => void;
+}
+
+export default class PureHttp {
+ request(
+ method: RequestMethods,
+ url: string,
+ param?: AxiosRequestConfig,
+ axiosConfig?: PureHttpRequestConfig
+ ): Promise;
+ post(
+ url: string,
+ params?: T,
+ config?: PureHttpRequestConfig
+ ): Promise;
+ get(
+ url: string,
+ params?: T,
+ config?: PureHttpRequestConfig
+ ): Promise;
+}
diff --git a/src/utils/mitt.ts b/src/utils/mitt.ts
new file mode 100644
index 0000000..0022e8d
--- /dev/null
+++ b/src/utils/mitt.ts
@@ -0,0 +1,21 @@
+import type { Emitter } from "mitt";
+import mitt from "mitt";
+
+type Events = {
+ resize: {
+ detail: {
+ width: number;
+ height: number;
+ };
+ };
+ openPanel: string;
+ tagViewsChange: string;
+ tagViewsShowModel: string;
+ logoChange: boolean;
+ changLayoutRoute: {
+ indexPath: string;
+ parentPath: string;
+ };
+};
+
+export const emitter: Emitter = mitt();
diff --git a/src/utils/print.ts b/src/utils/print.ts
new file mode 100644
index 0000000..c828f7c
--- /dev/null
+++ b/src/utils/print.ts
@@ -0,0 +1,225 @@
+interface PrintFunction {
+ extendOptions: Function;
+ getStyle: Function;
+ setDomHeight: Function;
+ toPrint: Function;
+}
+
+const Print = function (dom, options?: object): PrintFunction {
+ options = options || {};
+ // @ts-expect-error
+ if (!(this instanceof Print)) return new Print(dom, options);
+ this.conf = {
+ styleStr: "",
+ // Elements that need to dynamically get and set the height
+ setDomHeightArr: [],
+ // Echart dom List
+ echartDomArr: [],
+ // Callback before printing
+ printBeforeFn: null,
+ // Callback after printing
+ printDoneCallBack: null
+ };
+ for (const key in this.conf) {
+ // eslint-disable-next-line no-prototype-builtins
+ if (key && options.hasOwnProperty(key)) {
+ this.conf[key] = options[key];
+ }
+ }
+ if (typeof dom === "string") {
+ this.dom = document.querySelector(dom);
+ } else {
+ this.dom = this.isDOM(dom) ? dom : dom.$el;
+ }
+ if (this.conf.setDomHeightArr && this.conf.setDomHeightArr.length) {
+ this.setDomHeight(this.conf.setDomHeightArr);
+ }
+ this.init();
+};
+
+Print.prototype = {
+ /**
+ * init
+ */
+ init: function (): void {
+ const content = this.getStyle() + this.getHtml();
+ this.writeIframe(content);
+ },
+ /**
+ * Configuration property extension
+ * @param {Object} obj
+ * @param {Object} obj2
+ */
+ extendOptions: function (obj, obj2: T): T {
+ for (const k in obj2) {
+ obj[k] = obj2[k];
+ }
+ return obj;
+ },
+ /**
+ Copy all styles of the original page
+ */
+ getStyle: function (): string {
+ let str = "";
+ const styles: NodeListOf = document.querySelectorAll("style,link");
+ for (let i = 0; i < styles.length; i++) {
+ str += styles[i].outerHTML;
+ }
+ str += ``;
+ return str;
+ },
+ // form assignment
+ getHtml: function (): Element {
+ const inputs = document.querySelectorAll("input");
+ const selects = document.querySelectorAll("select");
+ const textareas = document.querySelectorAll("textarea");
+ for (let k = 0; k < inputs.length; k++) {
+ if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
+ if (inputs[k].checked == true) {
+ inputs[k].setAttribute("checked", "checked");
+ } else {
+ inputs[k].removeAttribute("checked");
+ }
+ } else if (inputs[k].type == "text") {
+ inputs[k].setAttribute("value", inputs[k].value);
+ } else {
+ inputs[k].setAttribute("value", inputs[k].value);
+ }
+ }
+
+ for (let k2 = 0; k2 < textareas.length; k2++) {
+ if (textareas[k2].type == "textarea") {
+ textareas[k2].innerHTML = textareas[k2].value;
+ }
+ }
+
+ for (let k3 = 0; k3 < selects.length; k3++) {
+ if (selects[k3].type == "select-one") {
+ const child = selects[k3].children;
+ for (const i in child) {
+ if (child[i].tagName == "OPTION") {
+ if ((child[i] as any).selected == true) {
+ child[i].setAttribute("selected", "selected");
+ } else {
+ child[i].removeAttribute("selected");
+ }
+ }
+ }
+ }
+ }
+
+ return this.dom.outerHTML;
+ },
+ /**
+ create iframe
+ */
+ writeIframe: function (content) {
+ let w: Document | Window;
+ let doc: Document;
+ const iframe: HTMLIFrameElement = document.createElement("iframe");
+ const f: HTMLIFrameElement = document.body.appendChild(iframe);
+ iframe.id = "myIframe";
+ iframe.setAttribute(
+ "style",
+ "position:absolute;width:0;height:0;top:-10px;left:-10px;"
+ );
+ // eslint-disable-next-line prefer-const
+ w = f.contentWindow || f.contentDocument;
+ // eslint-disable-next-line prefer-const
+ doc = f.contentDocument || f.contentWindow.document;
+ doc.open();
+ doc.write(content);
+ doc.close();
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const _this = this;
+ iframe.onload = function (): void {
+ // Before popping, callback
+ if (_this.conf.printBeforeFn) {
+ _this.conf.printBeforeFn({ doc });
+ }
+
+ _this.drawEchartImg(doc).then(() => {
+ _this.toPrint(w);
+ setTimeout(function () {
+ document.body.removeChild(iframe);
+ // After popup, callback
+ if (_this.conf.printDoneCallBack) {
+ _this.conf.printDoneCallBack();
+ }
+ }, 100);
+ });
+ };
+ },
+ /**
+ * echarts printing
+ * @param {Object} doc iframe window
+ */
+ drawEchartImg(doc): Promise {
+ return new Promise(resolve => {
+ if (this.conf.echartDomArr && this.conf.echartDomArr.length > 0) {
+ this.conf.echartDomArr.forEach(e => {
+ const dom = doc.querySelector("#" + e.$el.id);
+ const img = new Image();
+ const w = dom.offsetWidth + "px";
+ const H = dom.offsetHeight + "px";
+
+ img.style.width = w;
+ img.style.height = H;
+ img.src = e.imgSrc;
+ dom.innerHTML = "";
+ dom.appendChild(img);
+ });
+ }
+ resolve();
+ });
+ },
+ /**
+ Print
+ */
+ toPrint: function (frameWindow): void {
+ try {
+ setTimeout(function () {
+ frameWindow.focus();
+ try {
+ if (!frameWindow.document.execCommand("print", false, null)) {
+ frameWindow.print();
+ }
+ } catch (e) {
+ frameWindow.print();
+ }
+ frameWindow.close();
+ }, 10);
+ } catch (err) {
+ console.error(err);
+ }
+ },
+ isDOM:
+ typeof HTMLElement === "object"
+ ? function (obj) {
+ return obj instanceof HTMLElement;
+ }
+ : function (obj) {
+ return (
+ obj &&
+ typeof obj === "object" &&
+ obj.nodeType === 1 &&
+ typeof obj.nodeName === "string"
+ );
+ },
+ /**
+ * Set the height of the specified dom element by getting the existing height of the dom element and setting
+ * @param {Array} arr
+ */
+ setDomHeight(arr) {
+ if (arr && arr.length) {
+ arr.forEach(name => {
+ const domArr = document.querySelectorAll(name);
+ domArr.forEach(dom => {
+ dom.style.height = dom.offsetHeight + "px";
+ });
+ });
+ }
+ }
+};
+
+export default Print;
diff --git a/src/utils/progress/index.ts b/src/utils/progress/index.ts
new file mode 100644
index 0000000..d309862
--- /dev/null
+++ b/src/utils/progress/index.ts
@@ -0,0 +1,17 @@
+import NProgress from "nprogress";
+import "nprogress/nprogress.css";
+
+NProgress.configure({
+ // 动画方式
+ easing: "ease",
+ // 递增进度条的速度
+ speed: 500,
+ // 是否显示加载ico
+ showSpinner: false,
+ // 自动递增间隔
+ trickleSpeed: 200,
+ // 初始化时的最小百分比
+ minimum: 0.3
+});
+
+export default NProgress;
diff --git a/src/utils/propTypes.ts b/src/utils/propTypes.ts
new file mode 100644
index 0000000..403b78b
--- /dev/null
+++ b/src/utils/propTypes.ts
@@ -0,0 +1,34 @@
+import { CSSProperties, VNodeChild } from "vue";
+import { createTypes, VueTypeValidableDef, VueTypesInterface } from "vue-types";
+
+export type VueNode = VNodeChild | JSX.Element;
+
+type PropTypes = VueTypesInterface & {
+ readonly style: VueTypeValidableDef;
+ readonly VNodeChild: VueTypeValidableDef;
+};
+
+const propTypes = createTypes({
+ func: undefined,
+ bool: undefined,
+ string: undefined,
+ number: undefined,
+ object: undefined,
+ integer: undefined
+}) as PropTypes;
+
+propTypes.extend([
+ {
+ name: "style",
+ getter: true,
+ type: [String, Object],
+ default: undefined
+ },
+ {
+ name: "VNodeChild",
+ getter: true,
+ type: undefined
+ }
+]);
+
+export { propTypes };
diff --git a/src/utils/responsive.ts b/src/utils/responsive.ts
new file mode 100644
index 0000000..3e2d55f
--- /dev/null
+++ b/src/utils/responsive.ts
@@ -0,0 +1,37 @@
+// 响应式storage
+import { App } from "vue";
+import Storage from "responsive-storage";
+import { routerArrays } from "@/layout/types";
+
+const nameSpace = "responsive-";
+
+export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
+ const configObj = Object.assign(
+ {
+ // layout模式以及主题
+ layout: Storage.getData("layout", nameSpace) ?? {
+ layout: config.Layout ?? "vertical",
+ theme: config.Theme ?? "default",
+ darkMode: config.DarkMode ?? false,
+ sidebarStatus: config.SidebarStatus ?? true,
+ epThemeColor: config.EpThemeColor ?? "#409EFF"
+ },
+ configure: Storage.getData("configure", nameSpace) ?? {
+ grey: config.Grey ?? false,
+ weak: config.Weak ?? false,
+ hideTabs: config.HideTabs ?? false,
+ showLogo: config.ShowLogo ?? true,
+ showModel: config.ShowModel ?? "smart",
+ multiTagsCache: config.MultiTagsCache ?? false
+ }
+ },
+ config.MultiTagsCache
+ ? {
+ // 默认显示首页tag
+ tags: Storage.getData("tags", nameSpace) ?? routerArrays
+ }
+ : {}
+ );
+
+ app.use(Storage, { nameSpace, memory: configObj });
+};
diff --git a/src/views/error/403.vue b/src/views/error/403.vue
new file mode 100644
index 0000000..2a763ac
--- /dev/null
+++ b/src/views/error/403.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+ 403
+
+
+ 抱歉,你无权访问该页面
+
+
+ 返回首页
+
+
+
+
diff --git a/src/views/error/404.vue b/src/views/error/404.vue
new file mode 100644
index 0000000..1c20ed7
--- /dev/null
+++ b/src/views/error/404.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+ 404
+
+
+ 抱歉,你访问的页面不存在
+
+
+ 返回首页
+
+
+
+
diff --git a/src/views/error/500.vue b/src/views/error/500.vue
new file mode 100644
index 0000000..f21944e
--- /dev/null
+++ b/src/views/error/500.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+ 500
+
+
+ 抱歉,服务器出错了
+
+
+ 返回首页
+
+
+
+
diff --git a/src/views/login/index.vue b/src/views/login/index.vue
new file mode 100644
index 0000000..ca4799e
--- /dev/null
+++ b/src/views/login/index.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/login/utils/motion.ts b/src/views/login/utils/motion.ts
new file mode 100644
index 0000000..2b1182c
--- /dev/null
+++ b/src/views/login/utils/motion.ts
@@ -0,0 +1,40 @@
+import { h, defineComponent, withDirectives, resolveDirective } from "vue";
+
+/** 封装@vueuse/motion动画库中的自定义指令v-motion */
+export default defineComponent({
+ name: "Motion",
+ props: {
+ delay: {
+ type: Number,
+ default: 50
+ }
+ },
+ render() {
+ const { delay } = this;
+ const motion = resolveDirective("motion");
+ return withDirectives(
+ h(
+ "div",
+ {},
+ {
+ default: () => [this.$slots.default()]
+ }
+ ),
+ [
+ [
+ motion,
+ {
+ initial: { opacity: 0, y: 100 },
+ enter: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ delay
+ }
+ }
+ }
+ ]
+ ]
+ );
+ }
+});
diff --git a/src/views/login/utils/rule.ts b/src/views/login/utils/rule.ts
new file mode 100644
index 0000000..6b73d5a
--- /dev/null
+++ b/src/views/login/utils/rule.ts
@@ -0,0 +1,28 @@
+import { reactive } from "vue";
+import type { FormRules } from "element-plus";
+
+/** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */
+export const REGEXP_PWD =
+ /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
+
+/** 登录校验 */
+const loginRules = reactive({
+ password: [
+ {
+ validator: (rule, value, callback) => {
+ if (value === "") {
+ callback(new Error("请输入密码"));
+ } else if (!REGEXP_PWD.test(value)) {
+ callback(
+ new Error("密码格式应为8-18位数字、字母、符号的任意两种组合")
+ );
+ } else {
+ callback();
+ }
+ },
+ trigger: "blur"
+ }
+ ]
+});
+
+export { loginRules };
diff --git a/src/views/login/utils/static.ts b/src/views/login/utils/static.ts
new file mode 100644
index 0000000..18268d8
--- /dev/null
+++ b/src/views/login/utils/static.ts
@@ -0,0 +1,5 @@
+import bg from "@/assets/login/bg.png";
+import avatar from "@/assets/login/avatar.svg?component";
+import illustration from "@/assets/login/illustration.svg?component";
+
+export { bg, avatar, illustration };
diff --git a/src/views/permission/button/index.vue b/src/views/permission/button/index.vue
new file mode 100644
index 0000000..656b52e
--- /dev/null
+++ b/src/views/permission/button/index.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+ 当前拥有的code列表:{{ getAuths() }}
+
+
+
+
+
+
+
+ 拥有code:'btn_add' 权限可见
+
+
+ 拥有code:['btn_edit'] 权限可见
+
+
+
+ 拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+
+
+
+
+
+
+
+
+
+ 拥有code:'btn_add' 权限可见
+
+
+ 拥有code:['btn_edit'] 权限可见
+
+
+ 拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+
+
+
+
+
+
+
+
+ 拥有code:'btn_add' 权限可见
+
+
+ 拥有code:['btn_edit'] 权限可见
+
+
+ 拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+
+
+
+
diff --git a/src/views/permission/page/index.vue b/src/views/permission/page/index.vue
new file mode 100644
index 0000000..7dc49a3
--- /dev/null
+++ b/src/views/permission/page/index.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+ 模拟后台根据不同角色返回对应路由(具体参考完整版pure-admin代码)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/welcome/index.vue b/src/views/welcome/index.vue
new file mode 100644
index 0000000..8db10d2
--- /dev/null
+++ b/src/views/welcome/index.vue
@@ -0,0 +1,9 @@
+
+
+
+ Pure-Admin-Thin(非国际化版本)
+
diff --git a/stylelint.config.js b/stylelint.config.js
new file mode 100644
index 0000000..67f8c07
--- /dev/null
+++ b/stylelint.config.js
@@ -0,0 +1,92 @@
+module.exports = {
+ root: true,
+ plugins: ["stylelint-order"],
+ customSyntax: "postcss-html",
+ extends: ["stylelint-config-standard", "stylelint-config-prettier"],
+ rules: {
+ "selector-class-pattern": null,
+ "selector-pseudo-class-no-unknown": [
+ true,
+ {
+ ignorePseudoClasses: ["global"]
+ }
+ ],
+ "selector-pseudo-element-no-unknown": [
+ true,
+ {
+ ignorePseudoElements: ["v-deep"]
+ }
+ ],
+ "at-rule-no-unknown": [
+ true,
+ {
+ ignoreAtRules: [
+ "tailwind",
+ "apply",
+ "variants",
+ "responsive",
+ "screen",
+ "function",
+ "if",
+ "each",
+ "include",
+ "mixin"
+ ]
+ }
+ ],
+ "no-empty-source": null,
+ "named-grid-areas-no-invalid": null,
+ "unicode-bom": "never",
+ "no-descending-specificity": null,
+ "font-family-no-missing-generic-family-keyword": null,
+ "declaration-colon-space-after": "always-single-line",
+ "declaration-colon-space-before": "never",
+ "rule-empty-line-before": [
+ "always",
+ {
+ ignore: ["after-comment", "first-nested"]
+ }
+ ],
+ "unit-no-unknown": [true, { ignoreUnits: ["rpx"] }],
+ "order/order": [
+ [
+ "dollar-variables",
+ "custom-properties",
+ "at-rules",
+ "declarations",
+ {
+ type: "at-rule",
+ name: "supports"
+ },
+ {
+ type: "at-rule",
+ name: "media"
+ },
+ "rules"
+ ],
+ { severity: "warning" }
+ ]
+ },
+ ignoreFiles: ["**/*.js", "**/*.jsx", "**/*.tsx", "**/*.ts", "**/*.json"],
+ overrides: [
+ {
+ files: ["*.vue", "**/*.vue", "*.html", "**/*.html"],
+ extends: ["stylelint-config-recommended", "stylelint-config-html"],
+ rules: {
+ "keyframes-name-pattern": null,
+ "selector-pseudo-class-no-unknown": [
+ true,
+ {
+ ignorePseudoClasses: ["deep", "global"]
+ }
+ ],
+ "selector-pseudo-element-no-unknown": [
+ true,
+ {
+ ignorePseudoElements: ["v-deep", "v-global", "v-slotted"]
+ }
+ ]
+ }
+ }
+ ]
+};
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..11bb37f
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,20 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: "class",
+ corePlugins: {
+ preflight: false
+ },
+ content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
+ theme: {
+ extend: {
+ colors: {
+ bg_color: "var(--el-bg-color)",
+ primary: "var(--el-color-primary)",
+ primary_light_9: "var(--el-color-primary-light-9)",
+ text_color_primary: "var(--el-text-color-primary)",
+ text_color_regular: "var(--el-text-color-regular)",
+ text_color_disabled: "var(--el-text-color-disabled)"
+ }
+ }
+ }
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..0e3a5c8
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "Node",
+ "strict": false,
+ "jsx": "preserve",
+ "importHelpers": true,
+ "experimentalDecorators": true,
+ "strictFunctionTypes": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "sourceMap": true,
+ "baseUrl": ".",
+ "allowJs": false,
+ "resolveJsonModule": true,
+ "lib": ["dom", "esnext"],
+ "incremental": true,
+ "paths": {
+ "@/*": ["src/*"],
+ "@build/*": ["build/*"],
+ "/#/*": ["types/*"]
+ },
+ "types": [
+ "node",
+ "vite/client",
+ "element-plus/global",
+ "@pureadmin/table/volar",
+ "unplugin-vue-define-options",
+ "@pureadmin/descriptions/volar"
+ ],
+ "typeRoots": ["./node_modules/@types/", "./types"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "src/**/*.vue",
+ "types/*.d.ts",
+ "mock/*.ts",
+ "vite.config.ts"
+ ],
+ "exclude": ["node_modules", "dist", "**/*.js", "index.html"]
+}
diff --git a/types/global.d.ts b/types/global.d.ts
new file mode 100644
index 0000000..60acb4d
--- /dev/null
+++ b/types/global.d.ts
@@ -0,0 +1,149 @@
+import type {
+ ComponentRenderProxy,
+ VNode,
+ ComponentPublicInstance,
+ FunctionalComponent,
+ PropType as VuePropType
+} from "vue";
+import type { ECharts } from "echarts";
+import { type ResponsiveStorage } from "./index";
+
+// GlobalComponents for Volar
+declare module "vue" {
+ export interface GlobalComponents {
+ IconifyIconOffline: typeof import("../src/components/ReIcon")["IconifyIconOffline"];
+ IconifyIconOnline: typeof import("../src/components/ReIcon")["IconifyIconOnline"];
+ FontIcon: typeof import("../src/components/ReIcon")["FontIcon"];
+ Auth: typeof import("../src/components/ReAuth")["Auth"];
+ }
+}
+
+declare global {
+ const __APP_INFO__: {
+ pkg: {
+ name: string;
+ version: string;
+ dependencies: Recordable;
+ devDependencies: Recordable;
+ };
+ lastBuildTime: string;
+ };
+ interface Window {
+ // Global vue app instance
+ __APP__: App;
+ webkitCancelAnimationFrame: (handle: number) => void;
+ mozCancelAnimationFrame: (handle: number) => void;
+ oCancelAnimationFrame: (handle: number) => void;
+ msCancelAnimationFrame: (handle: number) => void;
+ webkitRequestAnimationFrame: (callback: FrameRequestCallback) => number;
+ mozRequestAnimationFrame: (callback: FrameRequestCallback) => number;
+ oRequestAnimationFrame: (callback: FrameRequestCallback) => number;
+ msRequestAnimationFrame: (callback: FrameRequestCallback) => number;
+ }
+
+ // vue
+ type PropType = VuePropType;
+
+ type Writable = {
+ -readonly [P in keyof T]: T[P];
+ };
+
+ type Nullable = T | null;
+ type NonNullable = T extends null | undefined ? never : T;
+ type Recordable = Record;
+ type ReadonlyRecordable = {
+ readonly [key: string]: T;
+ };
+ type Indexable = {
+ [key: string]: T;
+ };
+ type DeepPartial = {
+ [P in keyof T]?: DeepPartial;
+ };
+ type TimeoutHandle = ReturnType;
+ type IntervalHandle = ReturnType;
+
+ interface ChangeEvent extends Event {
+ target: HTMLInputElement;
+ }
+
+ interface WheelEvent {
+ path?: EventTarget[];
+ }
+ interface ImportMetaEnv extends ViteEnv {
+ __: unknown;
+ }
+
+ type ViteCompression =
+ | "none"
+ | "gzip"
+ | "brotli"
+ | "both"
+ | "gzip-clear"
+ | "brotli-clear"
+ | "both-clear";
+
+ declare interface ViteEnv {
+ VITE_PORT: number;
+ VITE_PUBLIC_PATH: string;
+ VITE_PROXY_DOMAIN: string;
+ VITE_PROXY_DOMAIN_REAL: string;
+ VITE_ROUTER_HISTORY: string;
+ VITE_LEGACY: boolean;
+ VITE_CDN: boolean;
+ VITE_COMPRESSION: ViteCompression;
+ }
+
+ declare interface ServerConfigs {
+ Version?: string;
+ Title?: string;
+ FixedHeader?: boolean;
+ HiddenSideBar?: boolean;
+ MultiTagsCache?: boolean;
+ KeepAlive?: boolean;
+ Layout?: string;
+ Theme?: string;
+ DarkMode?: boolean;
+ Grey?: boolean;
+ Weak?: boolean;
+ HideTabs?: boolean;
+ SidebarStatus?: boolean;
+ EpThemeColor?: string;
+ ShowLogo?: boolean;
+ ShowModel?: string;
+ MapConfigure?: {
+ amapKey?: string;
+ options: {
+ resizeEnable?: boolean;
+ center?: number[];
+ zoom?: number;
+ };
+ };
+ }
+
+ declare interface GlobalPropertiesApi {
+ $echarts: ECharts;
+ $storage: ResponsiveStorage;
+ $config: ServerConfigs;
+ }
+
+ function parseInt(s: string | number, radix?: number): number;
+
+ function parseFloat(string: string | number): number;
+
+ namespace JSX {
+ // tslint:disable no-empty-interface
+ type Element = VNode;
+ // tslint:disable no-empty-interface
+ type ElementClass = ComponentRenderProxy;
+ interface ElementAttributesProperty {
+ $props: any;
+ }
+ interface IntrinsicElements {
+ [elem: string]: any;
+ }
+ interface IntrinsicAttributes {
+ [elem: string]: any;
+ }
+ }
+}
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 0000000..215d923
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,33 @@
+declare interface Fn {
+ (...arg: T[]): R;
+}
+
+declare interface PromiseFn {
+ (...arg: T[]): Promise;
+}
+
+declare type RefType = T | null;
+
+declare type LabelValueOptions = {
+ label: string;
+ value: any;
+}[];
+
+declare type EmitType = (event: string, ...args: any[]) => void;
+
+declare type TargetContext = "_self" | "_blank";
+
+declare interface ComponentElRef {
+ $el: T;
+}
+
+declare type ComponentRef =
+ ComponentElRef | null;
+
+declare type ElRef = Nullable;
+
+declare type ForDataType = {
+ [P in T]?: ForDataType;
+};
+
+declare type AnyFunction = (...args: any[]) => T;
diff --git a/types/index.ts b/types/index.ts
new file mode 100644
index 0000000..914094c
--- /dev/null
+++ b/types/index.ts
@@ -0,0 +1,129 @@
+import { type RouteComponent } from "vue-router";
+
+export interface StorageConfigs {
+ version?: string;
+ title?: string;
+ fixedHeader?: boolean;
+ hiddenSideBar?: boolean;
+ multiTagsCache?: boolean;
+ keepAlive?: boolean;
+ layout?: string;
+ theme?: string;
+ darkMode?: boolean;
+ grey?: boolean;
+ weak?: boolean;
+ hideTabs?: boolean;
+ sidebarStatus?: boolean;
+ epThemeColor?: string;
+ showLogo?: boolean;
+ showModel?: string;
+ mapConfigure?: {
+ amapKey?: string;
+ options: {
+ resizeEnable?: boolean;
+ center?: number[];
+ zoom?: number;
+ };
+ };
+ username?: string;
+}
+
+export interface ResponsiveStorage {
+ layout: {
+ layout?: string;
+ theme?: string;
+ darkMode?: boolean;
+ sidebarStatus?: boolean;
+ epThemeColor?: string;
+ };
+ configure: {
+ grey?: boolean;
+ weak?: boolean;
+ hideTabs?: boolean;
+ showLogo?: boolean;
+ showModel?: string;
+ multiTagsCache?: boolean;
+ };
+ tags?: Array;
+}
+
+export interface RouteChildrenConfigsTable {
+ /** 子路由地址 `必填` */
+ path: string;
+ /** 路由名字(对应不要重复,和当前组件的`name`保持一致)`必填` */
+ name?: string;
+ /** 路由重定向 `可选` */
+ redirect?: string;
+ /** 按需加载组件 `可选` */
+ component?: RouteComponent;
+ meta?: {
+ /** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加) `必填` */
+ title: string;
+ /** 菜单图标 `可选` */
+ icon?: string;
+ /** 菜单名称右侧的额外图标,支持`fontawesome`、`iconfont`、`element-plus-icon` `可选` */
+ extraIcon?: {
+ svg?: boolean;
+ name?: string;
+ };
+ /** 是否在菜单中显示(默认`true`)`可选` */
+ showLink?: boolean;
+ /** 是否显示父级菜单 `可选` */
+ showParent?: boolean;
+ /** 页面级别权限设置 `可选` */
+ roles?: Array;
+ /** 按钮级别权限设置 `可选` */
+ auths?: Array;
+ /** 路由组件缓存(开启 `true`、关闭 `false`)`可选` */
+ keepAlive?: boolean;
+ /** 内嵌的`iframe`链接 `可选` */
+ frameSrc?: string;
+ /** `iframe`页是否开启首次加载动画(默认`true`)`可选` */
+ frameLoading?: boolean;
+ /** 页面加载动画(有两种形式,一种直接采用vue内置的`transitions`动画,另一种是使用`animate.css`写进、离场动画)`可选` */
+ transition?: {
+ /**
+ * @description 当前路由动画效果
+ * @see {@link https://next.router.vuejs.org/guide/advanced/transitions.html#transitions}
+ */
+ name?: string;
+ /** 进场动画 */
+ enterTransition?: string;
+ /** 离场动画 */
+ leaveTransition?: string;
+ };
+ // 是否不添加信息到标签页,(默认`false`)
+ hiddenTag?: boolean;
+ /** 动态路由可打开的最大数量 `可选` */
+ dynamicLevel?: number;
+ };
+ /** 子路由配置项 */
+ children?: Array;
+}
+
+/**
+ * @description 完整路由配置表
+ * @see {@link https://yiming_chang.gitee.io/pure-admin-doc/pages/782b6e/#%E4%B8%80-%E9%85%8D%E7%BD%AE%E9%A1%B9}
+ */
+export interface RouteConfigsTable {
+ /** 路由地址 `必填` */
+ path: string;
+ /** 路由名字(保持唯一)`可选` */
+ name?: string;
+ /** `Layout`组件 `可选` */
+ component?: RouteComponent;
+ /** 路由重定向 `可选` */
+ redirect?: string;
+ meta?: {
+ /** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加)`必填` */
+ title: string;
+ /** 菜单图标 `可选` */
+ icon?: string;
+ /** 是否在菜单中显示(默认`true`)`可选` */
+ showLink?: boolean;
+ /** 菜单升序排序,值越高排的越后(只针对顶级路由)`可选` */
+ rank?: number;
+ };
+ /** 子路由配置项 */
+ children?: Array;
+}
diff --git a/types/shims-tsx.d.ts b/types/shims-tsx.d.ts
new file mode 100644
index 0000000..4d21788
--- /dev/null
+++ b/types/shims-tsx.d.ts
@@ -0,0 +1,16 @@
+import Vue, { VNode } from "vue";
+
+declare module "*.tsx" {
+ import Vue from "compatible-vue";
+ export default Vue;
+}
+
+declare global {
+ namespace JSX {
+ interface Element extends VNode {}
+ interface ElementClass extends Vue {}
+ interface IntrinsicElements {
+ [elem: string]: any;
+ }
+ }
+}
diff --git a/types/shims-vue.d.ts b/types/shims-vue.d.ts
new file mode 100644
index 0000000..755d737
--- /dev/null
+++ b/types/shims-vue.d.ts
@@ -0,0 +1,15 @@
+declare module "*.vue" {
+ import { DefineComponent } from "vue";
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
+
+declare module "*.scss" {
+ const scss: Record;
+ export default scss;
+}
+
+declare module "vuedraggable/src/vuedraggable";
+declare module "@pureadmin/components";
+declare module "@pureadmin/theme";
+declare module "@pureadmin/theme/dist/browser-utils";
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..c10236c
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,90 @@
+import dayjs from "dayjs";
+import { resolve } from "path";
+import pkg from "./package.json";
+import { warpperEnv, regExps } from "./build";
+import { getPluginsList } from "./build/plugins";
+import { UserConfigExport, ConfigEnv, loadEnv } from "vite";
+
+/** 当前执行node命令时文件夹的地址(工作目录) */
+const root: string = process.cwd();
+
+/** 路径查找 */
+const pathResolve = (dir: string): string => {
+ return resolve(__dirname, ".", dir);
+};
+
+/** 设置别名 */
+const alias: Record = {
+ "@": pathResolve("src"),
+ "@build": pathResolve("build")
+};
+
+const { dependencies, devDependencies, name, version } = pkg;
+const __APP_INFO__ = {
+ pkg: { dependencies, devDependencies, name, version },
+ lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")
+};
+
+export default ({ command, mode }: ConfigEnv): UserConfigExport => {
+ const {
+ VITE_CDN,
+ VITE_PORT,
+ VITE_LEGACY,
+ VITE_COMPRESSION,
+ VITE_PUBLIC_PATH,
+ VITE_PROXY_DOMAIN,
+ VITE_PROXY_DOMAIN_REAL
+ } = warpperEnv(loadEnv(mode, root));
+ return {
+ base: VITE_PUBLIC_PATH,
+ root,
+ resolve: {
+ alias
+ },
+ // 服务端渲染
+ server: {
+ // 是否开启 https
+ https: false,
+ // 端口号
+ port: VITE_PORT,
+ host: "0.0.0.0",
+ // 本地跨域代理
+ proxy:
+ VITE_PROXY_DOMAIN_REAL.length > 0
+ ? {
+ [VITE_PROXY_DOMAIN]: {
+ target: VITE_PROXY_DOMAIN_REAL,
+ // ws: true,
+ changeOrigin: true,
+ rewrite: (path: string) => regExps(path, VITE_PROXY_DOMAIN)
+ }
+ }
+ : null
+ },
+ plugins: getPluginsList(command, VITE_LEGACY, VITE_CDN, VITE_COMPRESSION),
+ optimizeDeps: {
+ include: ["pinia", "lodash-es", "@vueuse/core", "dayjs"],
+ exclude: ["@pureadmin/theme/dist/browser-utils"]
+ },
+ build: {
+ sourcemap: false,
+ // 消除打包大小超过500kb警告
+ chunkSizeWarningLimit: 4000,
+ rollupOptions: {
+ input: {
+ index: pathResolve("index.html")
+ },
+ // 静态资源分类打包
+ output: {
+ chunkFileNames: "static/js/[name]-[hash].js",
+ entryFileNames: "static/js/[name]-[hash].js",
+ assetFileNames: "static/[ext]/[name]-[hash].[ext]"
+ }
+ }
+ },
+ define: {
+ __INTLIFY_PROD_DEVTOOLS__: false,
+ __APP_INFO__: JSON.stringify(__APP_INFO__)
+ }
+ };
+};