)KvfE??(DNCiz9FPwJg`#HygM1P~NSgOE@(Q71{+rGmgG9U%5y
zIwl^!9CtoR)MduGRXipg5su1Z_uDrM{H6hZNfoKmsJ|*y*V+^mER;BJs&ls
zM6%Nk+E=3qHwi_JAV!5To@`)nb{
zsA+^mD=C|3T7;Z+F`1xgi6nKJPzY9HSdC$1Hk~4!SVm=8ZCwOl1OEeF{+E)-*){1hrT95R!!&qv>0wyEq9ZGIdIX#3`YoyyZM8
z)>U=RLljdj(at>2v`5vE@3E{_I1C+3Iw5>-c9xXQs+;7Oi18?4KTI#^QT4Uft{!hQ
zgyLJoJCX0=w~E#owzu)U*!ha_#5Tf+yoybV77ooUoVjHd^&D>4#%D_(X9Y6@*`KE4
z+VFGd3oo48!bg022*Hg=1OGhnkazI5ZSUFa){P%$eUkn`%7Z`nXsApW#1h;`Y+&C-
zM-m*gDNU?HW6-53ONe*zC3tl@4tl?1hnt(7P21Ukl_=?DG#m9b_GSNLpP~l=`)MIP6N9DS*(^#>U_Ss2F=#MHiXvnDFaw$N;8#uQx;|TKh(mm
zxca!ds+8K4E>l|J3h?@tf&YC{%hwNR>NBmF`^|rH-SVkT(`gh;3G6-XNm{Fo$$tKo
zjlYyWUK-^$52>D9HvjQ+fsZD(j7qHeeAJ9MGj;{?;I*e@#zFFYIZ6rA`+mbWXs)acuD7-~ik2LzSW7%GeEiKzd3
z#!N#1=ve&;*K!Ue`Mlfr7eQOZvz98MAkPHN2n`D$7=Ajn(rKe0{P|&W=a+Afp&$jQ
zz1w!|8Tg;6FnWSxYxegI89g@f_pFs-Ze0AQNYd|h#E*+zTlrID(`sp4Fz(ZUgoUto
z!Ap3^y@`){>Hl8{H*mj#j7z{P_;{D!H6HhYD~|D~Ii0)PE@a?bI)=Xkjp~k&RrUw-
zDyGDHlf0l`wuW#YWfTUlcZz*TpD2a!cd09l4;$A=$@7
zpb`=30hgt|^dyWWQSjMb5;(k_J6x&i3|qIpekZSzssfTV++Gt86H`gN-A3fJG&3#D
zMB2gdN)2OiSGalQW#!QGeo9UbhU=|SDqPviP!T)(9DtJq$BB%eFb^hI)^61ClyJAgedYmXJkIqsbId!;N
zUH7+LP(z`JL*k2Fr)1CsuJL?*JE|&17q}pm=2=Q^d$D#Nb;w+C4JQI3BrUUsr1z{j
zg*A-to>eE^uoNoo^vrwP|L^
yMK>0RIu*}lIU1d93(1#3g?ar_ee*7YMnW&~cDquE8s$#N*sa)dr=eE>0000mD!%Oi
literal 0
HcmV?d00001
diff --git a/sop-admin/sop-admin-frontend/src/assets/login/avatar.svg b/sop-admin/sop-admin-frontend/src/assets/login/avatar.svg
new file mode 100644
index 00000000..a63d2b1a
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/login/avatar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/login/illustration.svg b/sop-admin/sop-admin-frontend/src/assets/login/illustration.svg
new file mode 100644
index 00000000..b58ffd08
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/login/illustration.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/logo.svg b/sop-admin/sop-admin-frontend/src/assets/logo.svg
deleted file mode 100644
index 3215438f..00000000
--- a/sop-admin/sop-admin-frontend/src/assets/logo.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
diff --git a/sop-admin/sop-admin-frontend/src/assets/status/403.svg b/sop-admin/sop-admin-frontend/src/assets/status/403.svg
new file mode 100644
index 00000000..ba3ce293
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/status/403.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/status/404.svg b/sop-admin/sop-admin-frontend/src/assets/status/404.svg
new file mode 100644
index 00000000..aacb7402
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/status/404.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/status/500.svg b/sop-admin/sop-admin-frontend/src/assets/status/500.svg
new file mode 100644
index 00000000..ea23a378
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/status/500.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/style/breakpoint.less b/sop-admin/sop-admin-frontend/src/assets/style/breakpoint.less
deleted file mode 100644
index 584f21e5..00000000
--- a/sop-admin/sop-admin-frontend/src/assets/style/breakpoint.less
+++ /dev/null
@@ -1,19 +0,0 @@
-// ==============breakpoint============
-
-// Extra small screen / phone
-@screen-xs: 480px;
-
-// Small screen / tablet
-@screen-sm: 576px;
-
-// Medium screen / desktop
-@screen-md: 768px;
-
-// Large screen / wide desktop
-@screen-lg: 992px;
-
-// Extra large screen / full hd
-@screen-xl: 1200px;
-
-// Extra extra large screen / large desktop
-@screen-xxl: 1600px;
diff --git a/sop-admin/sop-admin-frontend/src/assets/style/global.less b/sop-admin/sop-admin-frontend/src/assets/style/global.less
deleted file mode 100644
index f90b206f..00000000
--- a/sop-admin/sop-admin-frontend/src/assets/style/global.less
+++ /dev/null
@@ -1,94 +0,0 @@
-* {
- box-sizing: border-box;
-}
-
-html,
-body {
- width: 100%;
- height: 100%;
- margin: 0;
- padding: 0;
- font-size: 14px;
- background-color: var(--color-bg-1);
- -moz-osx-font-smoothing: grayscale;
- -webkit-font-smoothing: antialiased;
-}
-
-.echarts-tooltip-diy {
- background: linear-gradient(
- 304.17deg,
- rgba(253, 254, 255, 0.6) -6.04%,
- rgba(244, 247, 252, 0.6) 85.2%
- ) !important;
- border: none !important;
- backdrop-filter: blur(10px) !important;
- /* Note: backdrop-filter has minimal browser support */
-
- border-radius: 6px !important;
- .content-panel {
- display: flex;
- justify-content: space-between;
- padding: 0 9px;
- background: rgba(255, 255, 255, 0.8);
- width: 164px;
- height: 32px;
- line-height: 32px;
- box-shadow: 6px 0px 20px rgba(34, 87, 188, 0.1);
- border-radius: 4px;
- margin-bottom: 4px;
- }
- .tooltip-title {
- margin: 0 0 10px 0;
- }
- p {
- margin: 0;
- }
- .tooltip-title,
- .tooltip-value {
- font-size: 13px;
- line-height: 15px;
- display: flex;
- align-items: center;
- text-align: right;
- color: #1d2129;
- font-weight: bold;
- }
- .tooltip-item-icon {
- display: inline-block;
- margin-right: 8px;
- width: 10px;
- height: 10px;
- border-radius: 50%;
- }
-}
-
-.general-card {
- border-radius: 4px;
- border: none;
- & > .arco-card-header {
- height: auto;
- padding: 20px;
- border: none;
- }
- & > .arco-card-body {
- padding: 0 20px 20px 20px;
- }
-}
-
-.split-line {
- border-color: rgb(var(--gray-2));
-}
-
-.arco-table-cell {
- .circle {
- display: inline-block;
- margin-right: 4px;
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background-color: rgb(var(--blue-6));
- &.pass {
- background-color: rgb(var(--green-6));
- }
- }
-}
diff --git a/sop-admin/sop-admin-frontend/src/assets/svg/back_top.svg b/sop-admin/sop-admin-frontend/src/assets/svg/back_top.svg
new file mode 100644
index 00000000..f8e6aa02
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/svg/back_top.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/svg/dark.svg b/sop-admin/sop-admin-frontend/src/assets/svg/dark.svg
new file mode 100644
index 00000000..b5c4d2d5
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/svg/dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/svg/day.svg b/sop-admin/sop-admin-frontend/src/assets/svg/day.svg
new file mode 100644
index 00000000..b7600345
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/svg/day.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/svg/enter_outlined.svg b/sop-admin/sop-admin-frontend/src/assets/svg/enter_outlined.svg
new file mode 100644
index 00000000..45e0bafe
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/svg/enter_outlined.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/svg/exit_screen.svg b/sop-admin/sop-admin-frontend/src/assets/svg/exit_screen.svg
new file mode 100644
index 00000000..007c0b63
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/svg/exit_screen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/svg/full_screen.svg b/sop-admin/sop-admin-frontend/src/assets/svg/full_screen.svg
new file mode 100644
index 00000000..fff93a5d
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/svg/full_screen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/svg/globalization.svg b/sop-admin/sop-admin-frontend/src/assets/svg/globalization.svg
new file mode 100644
index 00000000..5f6bce6b
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/svg/globalization.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/svg/keyboard_esc.svg b/sop-admin/sop-admin-frontend/src/assets/svg/keyboard_esc.svg
new file mode 100644
index 00000000..bd671654
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/svg/keyboard_esc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/svg/system.svg b/sop-admin/sop-admin-frontend/src/assets/svg/system.svg
new file mode 100644
index 00000000..9ad39a56
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/svg/system.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/table-bar/collapse.svg b/sop-admin/sop-admin-frontend/src/assets/table-bar/collapse.svg
new file mode 100644
index 00000000..0823ae63
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/table-bar/collapse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/table-bar/drag.svg b/sop-admin/sop-admin-frontend/src/assets/table-bar/drag.svg
new file mode 100644
index 00000000..8ac32a7b
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/table-bar/drag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/table-bar/expand.svg b/sop-admin/sop-admin-frontend/src/assets/table-bar/expand.svg
new file mode 100644
index 00000000..bb41c350
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/table-bar/expand.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/table-bar/refresh.svg b/sop-admin/sop-admin-frontend/src/assets/table-bar/refresh.svg
new file mode 100644
index 00000000..140288cd
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/table-bar/refresh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/table-bar/settings.svg b/sop-admin/sop-admin-frontend/src/assets/table-bar/settings.svg
new file mode 100644
index 00000000..4ecd0779
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/assets/table-bar/settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/assets/user.jpg b/sop-admin/sop-admin-frontend/src/assets/user.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..a2973ace3367cf7181b470e2814db5a9c06a4533
GIT binary patch
literal 3694
zcmV-!4w3OvNk&Fy4gdgGMM6+kP&go34gdfUJpi2nDxd(M06vjGnMtLiq9G@=4G6Fj
z31%=YJ_J#Mqzynh+5qkiBt9n)6vG`w5
z59Yt%zw-M=|Ci+3>i=N>Ew5u=yB^RVw5UFe3RiK5ppxFI5hZzrgl1&wg-n3c))Y%AQ
zztZ0A)fc1!SiK%ISX~$|)-hs;X=zAAAD@Z*gWVgzm)%g1+`$}ptJ-~!RpF(M+D9~|
zyzKiw5rIMAM4W$##xF)FI@aAEmGoC*qlTH*Y#7}@iW_duD(>JV*{zEr22N+LV{FP<1qq}&(tt4p3;yxmOQ!w+`1j={ETfnRl
zb*POc9Om`L-8@f+Gk(-A2)n%p1tC~&oQ?yQ38vTpU!MQ)ux#BxhN6FS+Ip3+kZ
zo)vP~0RH)Kb6xxU_v?P7m=(NjXJ{Qq)1;v4mM+fSM86gKRbTlS-Q*o$`VsB;freYXMC&Fq|O+`H_g4W7j#I156=zk-lyK(W;g4Pr^fJEUsrmG@~M<
z8YJKtA)lr1dC%-VDU&Z@=;T7n$wm_N*ypE#R{lf>W{F^Cc+%9AvK*ifnV3I72I=~y
zrX$rW(zQAbZygSnMX7Y;U5>deXIh31`1g-ozrKYzNYUp04RlM>U>Ij}<*ZZho!<05
zYmb%3O)I4=oaZb8Dw2p_yb?=pr8GA){5J)#o6zoeg0xoY9UXILJq>qq^Hr+Y5_Yag
zId(Fll_vTlz>?58^}uZksfi)kv{%Pol8y;#(Z3Qjz(#GqGs97bk8XnB4BC(unp=DF
zwb@#fdhkZHt?382*X9i4H3(
zP%33VLUkL7<7cB&pEAN|2jiP_KpfK{BwK(ZQ;sRM)qUC5H(1&2goq|LPtRZv0uO}(WVRdjlr->H4e|s$?c(t=
z0Y+*WC>B3@x61{)IcDz!K7qehBk6sfXnf(5`f*hK;TvIM#Isf=jLMsYL;sj$x!2Y8
z{cbjG*LdoeU*lwo%hL0ALneG>2>`vmeQ(IQ^#t3emMwCnq#_gAJ@P0XY1%L2vwB1J
z^X|s=t%IgV-(Qy%kXc93u`9Dk&2Ls3i4mGtR(DUGY_(Zm{2##iZJ{CzQ@OnEfM{Qo
ze#f+W8o~d;onaf71-BcWY!b=oo`-r;Nq_}|8ehGiG5*utgI7MMek4CM1hxkxXrlORW=5OfHu(6?{I)XQg87b!kL2!Z
zukGavgW)KJ+F`?&ZR$T>*<|}e=^$Y)*j|G&9bU3GMX6GxjN?uRKFarnJ1Ii3(q-FM*cM~QMOv{tbAPN}BsP9Y?3ec^(|(8D>aYFf8<+-HrcvmwDl-?u-Vp
zJ|3KvuNMo~Fj-xr9OYGmPg6QI~cU=+S4PG9EoZLla
zf~s<=?i7=OIk8oX2%pFnZz4v}0d_Yy_WSVi&tWnP*o6YH(vSfzlb$(ZTy2RS$T+)f
z(5r!}zZSDj4mr^-J1`2Yp{${_`5%})c>iJ5iW)1!>1mFAFGx
z;B;)cd(7#6vu0zB3Qiz&wE9A&QQy8+YwetHC?0#?zR%)|rIuC^P(^NcLGFLLWvrwT
ziSba^I7L^ceM_vWH_7yL!}bLW3DNWJjqs&HWJa>c6e!xK__eysks1GnQI
z=f_v}Bj_px0`6cV_hwtJ5)QfC)>1IDGb$J>Vy9^nQaj04!pnQu2D>_Ozw$GD}C-N_mEcRS!<`}D?FWA;{I4@mjmo4iX`{bVhu`N5_P7$
z$*T<)7`5sHvXa}65d`e%prY8yx~xO-i9`QIz@F?0o2dtu#R1kNto!5Rr&u4`(XmBy
zEWzm3Sj90g7xrfDLt|ZF4$qYzc0Sq}*PSHT6JDm}trLU{TeU&6#Fqam6qK9*P
z=N#HV3O9LNdF|4Kk}aJkxHdJT#X4I;oHW$da#uo4OO6Srw>$?F!``-RN^mw_Yxw1o
zxo`|bmxWItG&jIDM3j3qsV{jks(QdvNM(AEOv@?eVx=4w8)k6rnBFTQh?@{8W=HvQ
zk>-?8)CHGVg;;uXr{F^8={SY<_^v;05dcUlv@?V@0}i&WN8?
z@E5~EBta9?8mDw9)qrV$3OL(`wg?`sH$8rc1(LIi5)6y=vR_*$r<4r|;t)!_qCNEp
zF}2c#-d4enAZYOlHcS0MkH5V_;>cd~ETx8-fJK#EXWSaQg!I}N`C=~yR^_y3!@Duh
z(oAswXyBc3&2Q>c31feA8*p;L0a89nQ9)??NeirTT=;C8ExeBxwgMzTBU5Pr#G_C}
zGE9$H@qU}U)DgzCagb1$xUof9d-id5Bs6=`!!BN#>C#n?d6;ksZSVCO-%~UvJ_O
z7K93jU4giQlw`I7QvtbdUa&p-I;*l@#2YW?%Ku7!?Gt*r8+9vL{`Ga=mN)Kn1Dh}1
zFFK$=E^uzUi^92o+^tUHv9BwVxin&)&?V)_I#1gC?F^CYC}ZI^4rPOaD>`V-8W`mJ
zj)FqAei|k^JBZFZDI5_r$N{6To8A>=DE(nmG4eZ
z0Oco+JGokWUkHtBtHHGj|L+S#dTpwVov{G}q#pQd^(dy7@qE`=a28_{C^L8vH
zbFj1Kh!|RsnqvSfIg*Y=_P5~OclqFwGl`?fv>G@;%KOeL5AjlIPzeTbrk;ru(H1L0
zd;Isk3}O=|1XHS9xWfK3LT8g=)b>X@PjwP=3)n^4FB>`%!VQ>9+IHe-L
z!`KUQU$KxlEY<-h#RJ_gB9-EV)mBh34C%_DsmSnql4?BtvY8Hl^pqD(!_cnWV@K}M
zYr8t?Tlh}AngVOKjWx9G3uu{pb{O&yFosG{7Fnp+`9aBx?GId7yL=665}>Y
zbn=*9Gkfa|B$iYPJv{zfO!NZx9NZ9*AXlhxm=t>4pfj{%eUw~!G)t0L4-lj)}T~z
zuzBX_ {
+ if (!slots) return null;
+ return hasAuth(props.value) ? (
+ {slots.default?.()}
+ ) : null;
+ };
+ }
+});
diff --git a/sop-admin/sop-admin-frontend/src/components/ReCol/index.ts b/sop-admin/sop-admin-frontend/src/components/ReCol/index.ts
new file mode 100644
index 00000000..7a6c9374
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReCol/index.ts
@@ -0,0 +1,29 @@
+import { ElCol } from "element-plus";
+import { h, defineComponent } from "vue";
+
+// 封装element-plus的el-col组件
+export default defineComponent({
+ name: "ReCol",
+ props: {
+ value: {
+ type: Number,
+ default: 24
+ }
+ },
+ render() {
+ const attrs = this.$attrs;
+ const val = this.value;
+ return h(
+ ElCol,
+ {
+ xs: val,
+ sm: val,
+ md: val,
+ lg: val,
+ xl: val,
+ ...attrs
+ },
+ { default: () => this.$slots.default() }
+ );
+ }
+});
diff --git a/sop-admin/sop-admin-frontend/src/components/ReDialog/index.ts b/sop-admin/sop-admin-frontend/src/components/ReDialog/index.ts
new file mode 100644
index 00000000..b471764b
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReDialog/index.ts
@@ -0,0 +1,69 @@
+import { ref } from "vue";
+import reDialog from "./index.vue";
+import { useTimeoutFn } from "@vueuse/core";
+import { withInstall } from "@pureadmin/utils";
+import type {
+ EventType,
+ ArgsType,
+ DialogProps,
+ ButtonProps,
+ DialogOptions
+} from "./type";
+
+const dialogStore = ref>([]);
+
+/** 打开弹框 */
+const addDialog = (options: DialogOptions) => {
+ const open = () =>
+ dialogStore.value.push(Object.assign(options, { visible: true }));
+ if (options?.openDelay) {
+ useTimeoutFn(() => {
+ open();
+ }, options.openDelay);
+ } else {
+ open();
+ }
+};
+
+/** 关闭弹框 */
+const closeDialog = (options: DialogOptions, index: number, args?: any) => {
+ dialogStore.value[index].visible = false;
+ options.closeCallBack && options.closeCallBack({ options, index, args });
+
+ const closeDelay = options?.closeDelay ?? 200;
+ useTimeoutFn(() => {
+ dialogStore.value.splice(index, 1);
+ }, closeDelay);
+};
+
+/**
+ * @description 更改弹框自身属性值
+ * @param value 属性值
+ * @param key 属性,默认`title`
+ * @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`)
+ */
+const updateDialog = (value: any, key = "title", index = 0) => {
+ dialogStore.value[index][key] = value;
+};
+
+/** 关闭所有弹框 */
+const closeAllDialog = () => {
+ dialogStore.value = [];
+};
+
+/** 千万别忘了在下面这三处引入并注册下,放心注册,不使用`addDialog`调用就不会被挂载
+ * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
+ * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L12
+ * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22
+ */
+const ReDialog = withInstall(reDialog);
+
+export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
+export {
+ ReDialog,
+ dialogStore,
+ addDialog,
+ closeDialog,
+ updateDialog,
+ closeAllDialog
+};
diff --git a/sop-admin/sop-admin-frontend/src/components/ReDialog/index.vue b/sop-admin/sop-admin-frontend/src/components/ReDialog/index.vue
new file mode 100644
index 00000000..23a0106e
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReDialog/index.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+ {{ options?.title }}
+ {
+ fullscreen = !fullscreen;
+ eventsCallBack(
+ 'fullscreenCallBack',
+ { ...options, fullscreen },
+ index,
+ true
+ );
+ }
+ "
+ >
+
+
+
+
+
+ handleClose(options, index, args)"
+ />
+
+
+
+
+
+
+
+
+
+ {{ btn?.label }}
+
+
+
+ {{ btn?.label }}
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/components/ReDialog/type.ts b/sop-admin/sop-admin-frontend/src/components/ReDialog/type.ts
new file mode 100644
index 00000000..7efbe201
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReDialog/type.ts
@@ -0,0 +1,275 @@
+import type { CSSProperties, VNode, Component } from "vue";
+
+type DoneFn = (cancel?: boolean) => void;
+type EventType =
+ | "open"
+ | "close"
+ | "openAutoFocus"
+ | "closeAutoFocus"
+ | "fullscreenCallBack";
+type ArgsType = {
+ /** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
+ command: "cancel" | "sure" | "close";
+};
+type ButtonType =
+ | "primary"
+ | "success"
+ | "warning"
+ | "danger"
+ | "info"
+ | "text";
+
+/** https://element-plus.org/zh-CN/component/dialog.html#attributes */
+type DialogProps = {
+ /** `Dialog` 的显示与隐藏 */
+ visible?: boolean;
+ /** `Dialog` 的标题 */
+ title?: string;
+ /** `Dialog` 的宽度,默认 `50%` */
+ width?: string | number;
+ /** 是否为全屏 `Dialog`(会一直处于全屏状态,除非弹框关闭),默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
+ fullscreen?: boolean;
+ /** 是否显示全屏操作图标,默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
+ fullscreenIcon?: boolean;
+ /** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */
+ top?: string;
+ /** 是否需要遮罩层,默认 `true` */
+ modal?: boolean;
+ /** `Dialog` 自身是否插入至 `body` 元素上。嵌套的 `Dialog` 必须指定该属性并赋值为 `true`,默认 `false` */
+ appendToBody?: boolean;
+ /** 是否在 `Dialog` 出现时将 `body` 滚动锁定,默认 `true` */
+ lockScroll?: boolean;
+ /** `Dialog` 的自定义类名 */
+ class?: string;
+ /** `Dialog` 的自定义样式 */
+ style?: CSSProperties;
+ /** `Dialog` 打开的延时时间,单位毫秒,默认 `0` */
+ openDelay?: number;
+ /** `Dialog` 关闭的延时时间,单位毫秒,默认 `0` */
+ closeDelay?: number;
+ /** 是否可以通过点击 `modal` 关闭 `Dialog`,默认 `true` */
+ closeOnClickModal?: boolean;
+ /** 是否可以通过按下 `ESC` 关闭 `Dialog`,默认 `true` */
+ closeOnPressEscape?: boolean;
+ /** 是否显示关闭按钮,默认 `true` */
+ showClose?: boolean;
+ /** 关闭前的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
+ beforeClose?: (done: DoneFn) => void;
+ /** 为 `Dialog` 启用可拖拽功能,默认 `false` */
+ draggable?: boolean;
+ /** 是否让 `Dialog` 的 `header` 和 `footer` 部分居中排列,默认 `false` */
+ center?: boolean;
+ /** 是否水平垂直对齐对话框,默认 `false` */
+ alignCenter?: boolean;
+ /** 当关闭 `Dialog` 时,销毁其中的元素,默认 `false` */
+ destroyOnClose?: boolean;
+};
+
+//element-plus.org/zh-CN/component/popconfirm.html#attributes
+type Popconfirm = {
+ /** 标题 */
+ title?: string;
+ /** 确定按钮文字 */
+ confirmButtonText?: string;
+ /** 取消按钮文字 */
+ cancelButtonText?: string;
+ /** 确定按钮类型,默认 `primary` */
+ confirmButtonType?: ButtonType;
+ /** 取消按钮类型,默认 `text` */
+ cancelButtonType?: ButtonType;
+ /** 自定义图标,默认 `QuestionFilled` */
+ icon?: string | Component;
+ /** `Icon` 颜色,默认 `#f90` */
+ iconColor?: string;
+ /** 是否隐藏 `Icon`,默认 `false` */
+ hideIcon?: boolean;
+ /** 关闭时的延迟,默认 `200` */
+ hideAfter?: number;
+ /** 是否将 `popover` 的下拉列表插入至 `body` 元素,默认 `true` */
+ teleported?: boolean;
+ /** 当 `popover` 组件长时间不触发且 `persistent` 属性设置为 `false` 时, `popover` 将会被删除,默认 `false` */
+ persistent?: boolean;
+ /** 弹层宽度,最小宽度 `150px`,默认 `150` */
+ width?: string | number;
+};
+
+type BtnClickDialog = {
+ options?: DialogOptions;
+ index?: number;
+};
+type BtnClickButton = {
+ btn?: ButtonProps;
+ index?: number;
+};
+/** https://element-plus.org/zh-CN/component/button.html#button-attributes */
+type ButtonProps = {
+ /** 按钮文字 */
+ label: string;
+ /** 按钮尺寸 */
+ size?: "large" | "default" | "small";
+ /** 按钮类型 */
+ type?: "primary" | "success" | "warning" | "danger" | "info";
+ /** 是否为朴素按钮,默认 `false` */
+ plain?: boolean;
+ /** 是否为文字按钮,默认 `false` */
+ text?: boolean;
+ /** 是否显示文字按钮背景颜色,默认 `false` */
+ bg?: boolean;
+ /** 是否为链接按钮,默认 `false` */
+ link?: boolean;
+ /** 是否为圆角按钮,默认 `false` */
+ round?: boolean;
+ /** 是否为圆形按钮,默认 `false` */
+ circle?: boolean;
+ /** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */
+ popconfirm?: Popconfirm;
+ /** 是否为加载中状态,默认 `false` */
+ loading?: boolean;
+ /** 自定义加载中状态图标组件 */
+ loadingIcon?: string | Component;
+ /** 按钮是否为禁用状态,默认 `false` */
+ disabled?: boolean;
+ /** 图标组件 */
+ icon?: string | Component;
+ /** 是否开启原生 `autofocus` 属性,默认 `false` */
+ autofocus?: boolean;
+ /** 原生 `type` 属性,默认 `button` */
+ nativeType?: "button" | "submit" | "reset";
+ /** 自动在两个中文字符之间插入空格 */
+ autoInsertSpace?: boolean;
+ /** 自定义按钮颜色, 并自动计算 `hover` 和 `active` 触发后的颜色 */
+ color?: string;
+ /** `dark` 模式, 意味着自动设置 `color` 为 `dark` 模式的颜色,默认 `false` */
+ dark?: boolean;
+ /** 自定义元素标签 */
+ tag?: string | Component;
+ /** 点击按钮后触发的回调 */
+ btnClick?: ({
+ dialog,
+ button
+ }: {
+ /** 当前 `Dialog` 信息 */
+ dialog: BtnClickDialog;
+ /** 当前 `button` 信息 */
+ button: BtnClickButton;
+ }) => void;
+};
+
+interface DialogOptions extends DialogProps {
+ /** 内容区组件的 `props`,可通过 `defineProps` 接收 */
+ props?: any;
+ /** 是否隐藏 `Dialog` 按钮操作区的内容 */
+ hideFooter?: boolean;
+ /** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */
+ popconfirm?: Popconfirm;
+ /** 点击确定按钮后是否开启 `loading` 加载动画 */
+ sureBtnLoading?: boolean;
+ /**
+ * @description 自定义对话框标题的内容渲染器
+ * @see {@link https://element-plus.org/zh-CN/component/dialog.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%A4%B4%E9%83%A8}
+ */
+ headerRenderer?: ({
+ close,
+ titleId,
+ titleClass
+ }: {
+ close: Function;
+ titleId: string;
+ titleClass: string;
+ }) => VNode | Component;
+ /** 自定义内容渲染器 */
+ contentRenderer?: ({
+ options,
+ index
+ }: {
+ options: DialogOptions;
+ index: number;
+ }) => VNode | Component;
+ /** 自定义按钮操作区的内容渲染器,会覆盖`footerButtons`以及默认的 `取消` 和 `确定` 按钮 */
+ footerRenderer?: ({
+ options,
+ index
+ }: {
+ options: DialogOptions;
+ index: number;
+ }) => VNode | Component;
+ /** 自定义底部按钮操作 */
+ footerButtons?: Array;
+ /** `Dialog` 打开后的回调 */
+ open?: ({
+ options,
+ index
+ }: {
+ options: DialogOptions;
+ index: number;
+ }) => void;
+ /** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或空白页或按下了esc键关闭页面时才会触发) */
+ close?: ({
+ options,
+ index
+ }: {
+ options: DialogOptions;
+ index: number;
+ }) => void;
+ /** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
+ closeCallBack?: ({
+ options,
+ index,
+ args
+ }: {
+ options: DialogOptions;
+ index: number;
+ args: any;
+ }) => void;
+ /** 点击全屏按钮时的回调 */
+ fullscreenCallBack?: ({
+ options,
+ index
+ }: {
+ options: DialogOptions;
+ index: number;
+ }) => void;
+ /** 输入焦点聚焦在 `Dialog` 内容时的回调 */
+ openAutoFocus?: ({
+ options,
+ index
+ }: {
+ options: DialogOptions;
+ index: number;
+ }) => void;
+ /** 输入焦点从 `Dialog` 内容失焦时的回调 */
+ closeAutoFocus?: ({
+ options,
+ index
+ }: {
+ options: DialogOptions;
+ index: number;
+ }) => void;
+ /** 点击底部取消按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
+ beforeCancel?: (
+ done: Function,
+ {
+ options,
+ index
+ }: {
+ options: DialogOptions;
+ index: number;
+ }
+ ) => void;
+ /** 点击底部确定按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
+ beforeSure?: (
+ done: Function,
+ {
+ options,
+ index,
+ closeLoading
+ }: {
+ options: DialogOptions;
+ index: number;
+ /** 关闭确定按钮的 `loading` 加载动画 */
+ closeLoading: Function;
+ }
+ ) => void;
+}
+
+export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
diff --git a/sop-admin/sop-admin-frontend/src/components/ReIcon/index.ts b/sop-admin/sop-admin-frontend/src/components/ReIcon/index.ts
new file mode 100644
index 00000000..86efe721
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/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/sop-admin/sop-admin-frontend/src/components/ReIcon/src/hooks.ts b/sop-admin/sop-admin-frontend/src/components/ReIcon/src/hooks.ts
new file mode 100644
index 00000000..5a377dac
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReIcon/src/hooks.ts
@@ -0,0 +1,61 @@
+import type { iconType } from "./types";
+import { h, defineComponent, type Component } from "vue";
+import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
+
+/**
+ * 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标
+ * @see 点击查看文档图标篇 {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/}
+ * @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 attrs ? h(icon, { ...attrs }) : icon;
+ } else if (typeof icon === "object") {
+ return defineComponent({
+ name: "OfflineIcon",
+ render() {
+ return h(IconifyIconOffline, {
+ icon: icon,
+ ...attrs
+ });
+ }
+ });
+ } else {
+ // 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
+ return defineComponent({
+ name: "Icon",
+ render() {
+ const IconifyIcon =
+ icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline;
+ return h(IconifyIcon, {
+ icon: icon,
+ ...attrs
+ });
+ }
+ });
+ }
+}
diff --git a/sop-admin/sop-admin-frontend/src/components/ReIcon/src/iconfont.ts b/sop-admin/sop-admin-frontend/src/components/ReIcon/src/iconfont.ts
new file mode 100644
index 00000000..c1104519
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/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/sop-admin/sop-admin-frontend/src/components/ReIcon/src/iconifyIconOffline.ts b/sop-admin/sop-admin-frontend/src/components/ReIcon/src/iconifyIconOffline.ts
new file mode 100644
index 00000000..b47aa99a
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReIcon/src/iconifyIconOffline.ts
@@ -0,0 +1,30 @@
+import { h, defineComponent } from "vue";
+import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
+
+// Iconify Icon在Vue里本地使用(用于内网环境)
+export default defineComponent({
+ name: "IconifyIconOffline",
+ components: { IconifyIcon },
+ props: {
+ icon: {
+ default: null
+ }
+ },
+ render() {
+ if (typeof this.icon === "object") addIcon(this.icon, this.icon);
+ 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/sop-admin/sop-admin-frontend/src/components/ReIcon/src/iconifyIconOnline.ts b/sop-admin/sop-admin-frontend/src/components/ReIcon/src/iconifyIconOnline.ts
new file mode 100644
index 00000000..a5f5822d
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/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/sop-admin/sop-admin-frontend/src/components/ReIcon/src/offlineIcon.ts b/sop-admin/sop-admin-frontend/src/components/ReIcon/src/offlineIcon.ts
new file mode 100644
index 00000000..fc5f9120
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReIcon/src/offlineIcon.ts
@@ -0,0 +1,14 @@
+// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
+import { addIcon } from "@iconify/vue/dist/offline";
+
+// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标
+// @iconify-icons/ep
+import Lollipop from "@iconify-icons/ep/lollipop";
+import HomeFilled from "@iconify-icons/ep/home-filled";
+addIcon("ep:lollipop", Lollipop);
+addIcon("ep:home-filled", HomeFilled);
+// @iconify-icons/ri
+import Search from "@iconify-icons/ri/search-line";
+import InformationLine from "@iconify-icons/ri/information-line";
+addIcon("ri:search-line", Search);
+addIcon("ri:information-line", InformationLine);
diff --git a/sop-admin/sop-admin-frontend/src/components/ReIcon/src/types.ts b/sop-admin/sop-admin-frontend/src/components/ReIcon/src/types.ts
new file mode 100644
index 00000000..000bdc59
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/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;
+ onLoad?: Function;
+ includes?: Function;
+ // svg 需要什么SVG属性自行添加
+ fill?: string;
+ // all icon
+ style?: object;
+}
diff --git a/sop-admin/sop-admin-frontend/src/components/RePerms/index.ts b/sop-admin/sop-admin-frontend/src/components/RePerms/index.ts
new file mode 100644
index 00000000..3701c3c1
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/RePerms/index.ts
@@ -0,0 +1,5 @@
+import perms from "./src/perms";
+
+const Perms = perms;
+
+export { Perms };
diff --git a/sop-admin/sop-admin-frontend/src/components/RePerms/src/perms.tsx b/sop-admin/sop-admin-frontend/src/components/RePerms/src/perms.tsx
new file mode 100644
index 00000000..da01bc16
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/RePerms/src/perms.tsx
@@ -0,0 +1,20 @@
+import { defineComponent, Fragment } from "vue";
+import { hasPerms } from "@/utils/auth";
+
+export default defineComponent({
+ name: "Perms",
+ props: {
+ value: {
+ type: undefined,
+ default: []
+ }
+ },
+ setup(props, { slots }) {
+ return () => {
+ if (!slots) return null;
+ return hasPerms(props.value) ? (
+ {slots.default?.()}
+ ) : null;
+ };
+ }
+});
diff --git a/sop-admin/sop-admin-frontend/src/components/RePureTableBar/index.ts b/sop-admin/sop-admin-frontend/src/components/RePureTableBar/index.ts
new file mode 100644
index 00000000..31b8a16e
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/RePureTableBar/index.ts
@@ -0,0 +1,5 @@
+import pureTableBar from "./src/bar";
+import { withInstall } from "@pureadmin/utils";
+
+/** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */
+export const PureTableBar = withInstall(pureTableBar);
diff --git a/sop-admin/sop-admin-frontend/src/components/RePureTableBar/src/bar.tsx b/sop-admin/sop-admin-frontend/src/components/RePureTableBar/src/bar.tsx
new file mode 100644
index 00000000..5367c6b3
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/RePureTableBar/src/bar.tsx
@@ -0,0 +1,393 @@
+import Sortable from "sortablejs";
+import { transformI18n } from "@/plugins/i18n";
+import { useEpThemeStoreHook } from "@/store/modules/epTheme";
+import {
+ type PropType,
+ ref,
+ unref,
+ computed,
+ nextTick,
+ defineComponent,
+ getCurrentInstance
+} from "vue";
+import {
+ delay,
+ cloneDeep,
+ isBoolean,
+ isFunction,
+ getKeyList
+} from "@pureadmin/utils";
+
+import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
+import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
+import DragIcon from "@/assets/table-bar/drag.svg?component";
+import ExpandIcon from "@/assets/table-bar/expand.svg?component";
+import RefreshIcon from "@/assets/table-bar/refresh.svg?component";
+import SettingIcon from "@/assets/table-bar/settings.svg?component";
+import CollapseIcon from "@/assets/table-bar/collapse.svg?component";
+
+const props = {
+ /** 头部最左边的标题 */
+ title: {
+ type: String,
+ default: "列表"
+ },
+ /** 对于树形表格,如果想启用展开和折叠功能,传入当前表格的ref即可 */
+ tableRef: {
+ type: Object as PropType
+ },
+ /** 需要展示的列 */
+ columns: {
+ type: Array as PropType,
+ default: () => []
+ },
+ isExpandAll: {
+ type: Boolean,
+ default: true
+ },
+ tableKey: {
+ type: [String, Number] as PropType,
+ default: "0"
+ }
+};
+
+export default defineComponent({
+ name: "PureTableBar",
+ props,
+ emits: ["refresh"],
+ setup(props, { emit, slots, attrs }) {
+ const size = ref("default");
+ const loading = ref(false);
+ const checkAll = ref(true);
+ const isFullscreen = ref(false);
+ const isIndeterminate = ref(false);
+ const instance = getCurrentInstance()!;
+ const isExpandAll = ref(props.isExpandAll);
+ const filterColumns = cloneDeep(props?.columns).filter(column =>
+ isBoolean(column?.hide)
+ ? !column.hide
+ : !(isFunction(column?.hide) && column?.hide())
+ );
+ let checkColumnList = getKeyList(cloneDeep(props?.columns), "label");
+ const checkedColumns = ref(getKeyList(cloneDeep(filterColumns), "label"));
+ const dynamicColumns = ref(cloneDeep(props?.columns));
+
+ const getDropdownItemStyle = computed(() => {
+ return s => {
+ return {
+ background:
+ s === size.value ? useEpThemeStoreHook().epThemeColor : "",
+ color: s === size.value ? "#fff" : "var(--el-text-color-primary)"
+ };
+ };
+ });
+
+ const iconClass = computed(() => {
+ return [
+ "text-black",
+ "dark:text-white",
+ "duration-100",
+ "hover:!text-primary",
+ "cursor-pointer",
+ "outline-none"
+ ];
+ });
+
+ const topClass = computed(() => {
+ return [
+ "flex",
+ "justify-between",
+ "pt-[3px]",
+ "px-[11px]",
+ "border-b-[1px]",
+ "border-solid",
+ "border-[#dcdfe6]",
+ "dark:border-[#303030]"
+ ];
+ });
+
+ function onReFresh() {
+ loading.value = true;
+ emit("refresh");
+ delay(500).then(() => (loading.value = false));
+ }
+
+ function onExpand() {
+ isExpandAll.value = !isExpandAll.value;
+ toggleRowExpansionAll(props.tableRef.data, isExpandAll.value);
+ }
+
+ function toggleRowExpansionAll(data, isExpansion) {
+ data.forEach(item => {
+ props.tableRef.toggleRowExpansion(item, isExpansion);
+ if (item.children !== undefined && item.children !== null) {
+ toggleRowExpansionAll(item.children, isExpansion);
+ }
+ });
+ }
+
+ function handleCheckAllChange(val: boolean) {
+ checkedColumns.value = val ? checkColumnList : [];
+ isIndeterminate.value = false;
+ dynamicColumns.value.map(column =>
+ val ? (column.hide = false) : (column.hide = true)
+ );
+ }
+
+ function handleCheckedColumnsChange(value: string[]) {
+ checkedColumns.value = value;
+ const checkedCount = value.length;
+ checkAll.value = checkedCount === checkColumnList.length;
+ isIndeterminate.value =
+ checkedCount > 0 && checkedCount < checkColumnList.length;
+ }
+
+ function handleCheckColumnListChange(val: boolean, label: string) {
+ dynamicColumns.value.filter(
+ item => transformI18n(item.label) === transformI18n(label)
+ )[0].hide = !val;
+ }
+
+ async function onReset() {
+ checkAll.value = true;
+ isIndeterminate.value = false;
+ dynamicColumns.value = cloneDeep(props?.columns);
+ checkColumnList = [];
+ checkColumnList = await getKeyList(cloneDeep(props?.columns), "label");
+ checkedColumns.value = getKeyList(cloneDeep(filterColumns), "label");
+ }
+
+ const dropdown = {
+ dropdown: () => (
+
+ (size.value = "large")}
+ >
+ 宽松
+
+ (size.value = "default")}
+ >
+ 默认
+
+ (size.value = "small")}
+ >
+ 紧凑
+
+
+ )
+ };
+
+ /** 列展示拖拽排序 */
+ const rowDrop = (event: { preventDefault: () => void }) => {
+ event.preventDefault();
+ nextTick(() => {
+ const wrapper: HTMLElement = (
+ instance?.proxy?.$refs[`GroupRef${unref(props.tableKey)}`] as any
+ ).$el.firstElementChild;
+ Sortable.create(wrapper, {
+ animation: 300,
+ handle: ".drag-btn",
+ onEnd: ({ newIndex, oldIndex, item }) => {
+ const targetThElem = item;
+ const wrapperElem = targetThElem.parentNode as HTMLElement;
+ const oldColumn = dynamicColumns.value[oldIndex];
+ const newColumn = dynamicColumns.value[newIndex];
+ if (oldColumn?.fixed || newColumn?.fixed) {
+ // 当前列存在fixed属性 则不可拖拽
+ const oldThElem = wrapperElem.children[oldIndex] as HTMLElement;
+ if (newIndex > oldIndex) {
+ wrapperElem.insertBefore(targetThElem, oldThElem);
+ } else {
+ wrapperElem.insertBefore(
+ targetThElem,
+ oldThElem ? oldThElem.nextElementSibling : oldThElem
+ );
+ }
+ return;
+ }
+ const currentRow = dynamicColumns.value.splice(oldIndex, 1)[0];
+ dynamicColumns.value.splice(newIndex, 0, currentRow);
+ }
+ });
+ });
+ };
+
+ const isFixedColumn = (label: string) => {
+ return dynamicColumns.value.filter(
+ item => transformI18n(item.label) === transformI18n(label)
+ )[0].fixed
+ ? true
+ : false;
+ };
+
+ const rendTippyProps = (content: string) => {
+ // https://vue-tippy.netlify.app/props
+ return {
+ content,
+ offset: [0, 18],
+ duration: [300, 0],
+ followCursor: true,
+ hideOnClick: "toggle"
+ };
+ };
+
+ const reference = {
+ reference: () => (
+
+ )
+ };
+
+ return () => (
+ <>
+
+
+ {slots?.title ? (
+ slots.title()
+ ) : (
+
{props.title}
+ )}
+
+ {slots?.buttons ? (
+
{slots.buttons()}
+ ) : null}
+ {props.tableRef?.size ? (
+ <>
+
onExpand()}
+ />
+
+ >
+ ) : null}
+ onReFresh()}
+ />
+
+
+
+
+
+
+
+
+ handleCheckAllChange(value)}
+ />
+ onReset()}>
+ 重置
+
+
+
+
+
+ handleCheckedColumnsChange(value)}
+ >
+
+ {checkColumnList.map((item, index) => {
+ return (
+
+ void;
+ }) => rowDrop(event)}
+ />
+
+ handleCheckColumnListChange(value, item)
+ }
+ >
+
+ {transformI18n(item)}
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ (isFullscreen.value = !isFullscreen.value)}
+ />
+
+
+ {slots.default({
+ size: size.value,
+ dynamicColumns: dynamicColumns.value
+ })}
+
+ >
+ );
+ }
+});
diff --git a/sop-admin/sop-admin-frontend/src/components/ReSegmented/index.ts b/sop-admin/sop-admin-frontend/src/components/ReSegmented/index.ts
new file mode 100644
index 00000000..de4253c4
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReSegmented/index.ts
@@ -0,0 +1,8 @@
+import reSegmented from "./src/index";
+import { withInstall } from "@pureadmin/utils";
+
+/** 分段控制器组件 */
+export const ReSegmented = withInstall(reSegmented);
+
+export default ReSegmented;
+export type { OptionsType } from "./src/type";
diff --git a/sop-admin/sop-admin-frontend/src/components/ReSegmented/src/index.css b/sop-admin/sop-admin-frontend/src/components/ReSegmented/src/index.css
new file mode 100644
index 00000000..503bbe43
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReSegmented/src/index.css
@@ -0,0 +1,157 @@
+.pure-segmented {
+ --pure-control-padding-horizontal: 12px;
+ --pure-control-padding-horizontal-sm: 8px;
+ --pure-segmented-track-padding: 2px;
+ --pure-segmented-line-width: 1px;
+
+ --pure-segmented-border-radius-small: 4px;
+ --pure-segmented-border-radius-base: 6px;
+ --pure-segmented-border-radius-large: 8px;
+
+ box-sizing: border-box;
+ display: inline-block;
+ padding: var(--pure-segmented-track-padding);
+ font-size: var(--el-font-size-base);
+ color: rgba(0, 0, 0, 0.65);
+ background-color: rgb(0 0 0 / 4%);
+ border-radius: var(--pure-segmented-border-radius-base);
+}
+
+.pure-segmented-block {
+ display: flex;
+}
+
+.pure-segmented-block .pure-segmented-item {
+ flex: 1;
+ min-width: 0;
+}
+
+.pure-segmented-block .pure-segmented-item > .pure-segmented-item-label > span {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+/* small */
+.pure-segmented.pure-segmented--small {
+ border-radius: var(--pure-segmented-border-radius-small);
+}
+.pure-segmented.pure-segmented--small .pure-segmented-item {
+ border-radius: var(--el-border-radius-small);
+}
+.pure-segmented.pure-segmented--small .pure-segmented-item > div {
+ min-height: calc(
+ var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2
+ );
+ line-height: calc(
+ var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2
+ );
+ padding: 0
+ calc(
+ var(--pure-control-padding-horizontal-sm) -
+ var(--pure-segmented-line-width)
+ );
+}
+
+/* large */
+.pure-segmented.pure-segmented--large {
+ border-radius: var(--pure-segmented-border-radius-large);
+}
+.pure-segmented.pure-segmented--large .pure-segmented-item {
+ border-radius: calc(
+ var(--el-border-radius-base) + var(--el-border-radius-small)
+ );
+}
+.pure-segmented.pure-segmented--large .pure-segmented-item > div {
+ min-height: calc(
+ var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2
+ );
+ line-height: calc(
+ var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2
+ );
+ padding: 0
+ calc(
+ var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width)
+ );
+ font-size: var(--el-font-size-medium);
+}
+
+/* default */
+.pure-segmented-item {
+ position: relative;
+ text-align: center;
+ cursor: pointer;
+ border-radius: var(--el-border-radius-base);
+ transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+.pure-segmented .pure-segmented-item > div {
+ min-height: calc(
+ var(--el-component-size) - var(--pure-segmented-track-padding) * 2
+ );
+ line-height: calc(
+ var(--el-component-size) - var(--pure-segmented-track-padding) * 2
+ );
+ padding: 0
+ calc(
+ var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width)
+ );
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ transition: 0.1s;
+}
+
+.pure-segmented-group {
+ position: relative;
+ display: flex;
+ align-items: stretch;
+ justify-items: flex-start;
+ width: 100%;
+}
+
+.pure-segmented-item-selected {
+ position: absolute;
+ top: 0;
+ left: 0;
+ box-sizing: border-box;
+ display: none;
+ width: 0;
+ height: 100%;
+ padding: 4px 0;
+ background-color: #fff;
+ border-radius: 4px;
+ box-shadow:
+ 0 2px 8px -2px rgb(0 0 0 / 5%),
+ 0 1px 4px -1px rgb(0 0 0 / 7%),
+ 0 0 1px rgb(0 0 0 / 7%);
+ transition:
+ transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
+ width 0.5s cubic-bezier(0.645, 0.045, 0.355, 1);
+ will-change: transform, width;
+}
+
+.pure-segmented-item > input {
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.pure-segmented-item-label {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.pure-segmented-item-icon svg {
+ width: 16px;
+ height: 16px;
+}
+
+.pure-segmented-item-disabled {
+ color: rgba(0, 0, 0, 0.25);
+ cursor: not-allowed;
+}
diff --git a/sop-admin/sop-admin-frontend/src/components/ReSegmented/src/index.tsx b/sop-admin/sop-admin-frontend/src/components/ReSegmented/src/index.tsx
new file mode 100644
index 00000000..39580ed8
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReSegmented/src/index.tsx
@@ -0,0 +1,216 @@
+import "./index.css";
+import type { OptionsType } from "./type";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import {
+ useDark,
+ isNumber,
+ isFunction,
+ useResizeObserver
+} from "@pureadmin/utils";
+import {
+ type PropType,
+ h,
+ ref,
+ toRef,
+ watch,
+ nextTick,
+ defineComponent,
+ getCurrentInstance
+} from "vue";
+
+const props = {
+ options: {
+ type: Array,
+ default: () => []
+ },
+ /** 默认选中,按照第一个索引为 `0` 的模式,可选(`modelValue`只有传`number`类型时才为响应式) */
+ modelValue: {
+ type: undefined,
+ require: false,
+ default: "0"
+ },
+ /** 将宽度调整为父元素宽度 */
+ block: {
+ type: Boolean,
+ default: false
+ },
+ /** 控件尺寸 */
+ size: {
+ type: String as PropType<"small" | "default" | "large">
+ },
+ /** 是否全局禁用,默认 `false` */
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ /** 当内容发生变化时,设置 `resize` 可使其自适应容器位置 */
+ resize: {
+ type: Boolean,
+ default: false
+ }
+};
+
+export default defineComponent({
+ name: "ReSegmented",
+ props,
+ emits: ["change", "update:modelValue"],
+ setup(props, { emit }) {
+ const width = ref(0);
+ const translateX = ref(0);
+ const { isDark } = useDark();
+ const initStatus = ref(false);
+ const curMouseActive = ref(-1);
+ const segmentedItembg = ref("");
+ const instance = getCurrentInstance()!;
+ const curIndex = isNumber(props.modelValue)
+ ? toRef(props, "modelValue")
+ : ref(0);
+
+ function handleChange({ option, index }, event: Event) {
+ if (props.disabled || option.disabled) return;
+ event.preventDefault();
+ isNumber(props.modelValue)
+ ? emit("update:modelValue", index)
+ : (curIndex.value = index);
+ segmentedItembg.value = "";
+ emit("change", { index, option });
+ }
+
+ function handleMouseenter({ option, index }, event: Event) {
+ if (props.disabled) return;
+ event.preventDefault();
+ curMouseActive.value = index;
+ if (option.disabled || curIndex.value === index) {
+ segmentedItembg.value = "";
+ } else {
+ segmentedItembg.value = isDark.value
+ ? "#1f1f1f"
+ : "rgba(0, 0, 0, 0.06)";
+ }
+ }
+
+ function handleMouseleave(_, event: Event) {
+ if (props.disabled) return;
+ event.preventDefault();
+ curMouseActive.value = -1;
+ }
+
+ function handleInit(index = curIndex.value) {
+ nextTick(() => {
+ const curLabelRef = instance?.proxy?.$refs[`labelRef${index}`] as ElRef;
+ if (!curLabelRef) return;
+ width.value = curLabelRef.clientWidth;
+ translateX.value = curLabelRef.offsetLeft;
+ initStatus.value = true;
+ });
+ }
+
+ function handleResizeInit() {
+ useResizeObserver(".pure-segmented", () => {
+ nextTick(() => {
+ handleInit(curIndex.value);
+ });
+ });
+ }
+
+ (props.block || props.resize) && handleResizeInit();
+
+ watch(
+ () => curIndex.value,
+ index => {
+ nextTick(() => {
+ handleInit(index);
+ });
+ },
+ {
+ immediate: true
+ }
+ );
+
+ watch(() => props.size, handleResizeInit, {
+ immediate: true
+ });
+
+ const rendLabel = () => {
+ return props.options.map((option, index) => {
+ return (
+
+ );
+ });
+ };
+
+ return () => (
+
+ );
+ }
+});
diff --git a/sop-admin/sop-admin-frontend/src/components/ReSegmented/src/type.ts b/sop-admin/sop-admin-frontend/src/components/ReSegmented/src/type.ts
new file mode 100644
index 00000000..205e34dc
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReSegmented/src/type.ts
@@ -0,0 +1,20 @@
+import type { VNode, Component } from "vue";
+import type { iconType } from "@/components/ReIcon/src/types.ts";
+
+export interface OptionsType {
+ /** 文字 */
+ label?: string | (() => VNode | Component);
+ /**
+ * @description 图标,采用平台内置的 `useRenderIcon` 函数渲染
+ * @see {@link 用法参考 https://pure-admin.github.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
+ */
+ icon?: string | Component;
+ /** 图标属性、样式配置 */
+ iconAttrs?: iconType;
+ /** 值 */
+ value?: any;
+ /** 是否禁用 */
+ disabled?: boolean;
+ /** `tooltip` 提示 */
+ tip?: string;
+}
diff --git a/sop-admin/sop-admin-frontend/src/components/ReText/index.ts b/sop-admin/sop-admin-frontend/src/components/ReText/index.ts
new file mode 100644
index 00000000..62135660
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReText/index.ts
@@ -0,0 +1,7 @@
+import reText from "./src/index.vue";
+import { withInstall } from "@pureadmin/utils";
+
+/** 支持`Tooltip`提示的文本省略组件 */
+export const ReText = withInstall(reText);
+
+export default ReText;
diff --git a/sop-admin/sop-admin-frontend/src/components/ReText/src/index.vue b/sop-admin/sop-admin-frontend/src/components/ReText/src/index.vue
new file mode 100644
index 00000000..ecaebdbb
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/components/ReText/src/index.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/components/breadcrumb/index.vue b/sop-admin/sop-admin-frontend/src/components/breadcrumb/index.vue
deleted file mode 100644
index e4c9f139..00000000
--- a/sop-admin/sop-admin-frontend/src/components/breadcrumb/index.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
- {{ $t(item) }}
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/chart/index.vue b/sop-admin/sop-admin-frontend/src/components/chart/index.vue
deleted file mode 100644
index 417c4c8b..00000000
--- a/sop-admin/sop-admin-frontend/src/components/chart/index.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/footer/index.vue b/sop-admin/sop-admin-frontend/src/components/footer/index.vue
deleted file mode 100644
index 9a250ccc..00000000
--- a/sop-admin/sop-admin-frontend/src/components/footer/index.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/global-setting/block.vue b/sop-admin/sop-admin-frontend/src/components/global-setting/block.vue
deleted file mode 100644
index be43e8cc..00000000
--- a/sop-admin/sop-admin-frontend/src/components/global-setting/block.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
{{ title }}
-
- {{ $t(option.name) }}
-
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/global-setting/form-wrapper.vue b/sop-admin/sop-admin-frontend/src/components/global-setting/form-wrapper.vue
deleted file mode 100644
index fe3bbbe1..00000000
--- a/sop-admin/sop-admin-frontend/src/components/global-setting/form-wrapper.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/global-setting/index.vue b/sop-admin/sop-admin-frontend/src/components/global-setting/index.vue
deleted file mode 100644
index 7874fbec..00000000
--- a/sop-admin/sop-admin-frontend/src/components/global-setting/index.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
- {{ $t('settings.title') }}
-
-
- {{ $t('settings.alertContent') }}
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/index.ts b/sop-admin/sop-admin-frontend/src/components/index.ts
deleted file mode 100644
index f418972f..00000000
--- a/sop-admin/sop-admin-frontend/src/components/index.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { App } from 'vue';
-import { use } from 'echarts/core';
-import { CanvasRenderer } from 'echarts/renderers';
-import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
-import {
- GridComponent,
- TooltipComponent,
- LegendComponent,
- DataZoomComponent,
- GraphicComponent,
-} from 'echarts/components';
-import Chart from './chart/index.vue';
-import Breadcrumb from './breadcrumb/index.vue';
-
-// Manually introduce ECharts modules to reduce packing size
-
-use([
- CanvasRenderer,
- BarChart,
- LineChart,
- PieChart,
- RadarChart,
- GridComponent,
- TooltipComponent,
- LegendComponent,
- DataZoomComponent,
- GraphicComponent,
-]);
-
-export default {
- install(Vue: App) {
- Vue.component('Chart', Chart);
- Vue.component('Breadcrumb', Breadcrumb);
- },
-};
diff --git a/sop-admin/sop-admin-frontend/src/components/menu/index.vue b/sop-admin/sop-admin-frontend/src/components/menu/index.vue
deleted file mode 100644
index 0702897e..00000000
--- a/sop-admin/sop-admin-frontend/src/components/menu/index.vue
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/menu/use-menu-tree.ts b/sop-admin/sop-admin-frontend/src/components/menu/use-menu-tree.ts
deleted file mode 100644
index ed548c5a..00000000
--- a/sop-admin/sop-admin-frontend/src/components/menu/use-menu-tree.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { computed } from 'vue';
-import { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
-import usePermission from '@/hooks/permission';
-import { useAppStore } from '@/store';
-import appClientMenus from '@/router/app-menus';
-import { cloneDeep } from 'lodash';
-
-export default function useMenuTree() {
- const permission = usePermission();
- const appStore = useAppStore();
- const appRoute = computed(() => {
- if (appStore.menuFromServer) {
- return appStore.appAsyncMenus;
- }
- return appClientMenus;
- });
- const menuTree = computed(() => {
- const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
- copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
- return (a.meta.order || 0) - (b.meta.order || 0);
- });
- function travel(_routes: RouteRecordRaw[], layer: number) {
- if (!_routes) return null;
-
- const collector: any = _routes.map((element) => {
- // no access
- if (!permission.accessRouter(element)) {
- return null;
- }
-
- // leaf node
- if (element.meta?.hideChildrenInMenu || !element.children) {
- element.children = [];
- return element;
- }
-
- // route filter hideInMenu true
- element.children = element.children.filter(
- (x) => x.meta?.hideInMenu !== true
- );
-
- // Associated child node
- const subItem = travel(element.children, layer + 1);
-
- if (subItem.length) {
- element.children = subItem;
- return element;
- }
- // the else logic
- if (layer > 1) {
- element.children = subItem;
- return element;
- }
-
- if (element.meta?.hideInMenu === false) {
- return element;
- }
-
- return null;
- });
- return collector.filter(Boolean);
- }
- return travel(copyRouter, 0);
- });
-
- return {
- menuTree,
- };
-}
diff --git a/sop-admin/sop-admin-frontend/src/components/message-box/index.vue b/sop-admin/sop-admin-frontend/src/components/message-box/index.vue
deleted file mode 100644
index c0a24f73..00000000
--- a/sop-admin/sop-admin-frontend/src/components/message-box/index.vue
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
-
- {{ item.title }}{{ formatUnreadLength(item.key) }}
-
-
- {{ $t('messageBox.noContent') }}
-
-
-
-
-
- {{ $t('messageBox.tab.button') }}
-
-
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/message-box/list.vue b/sop-admin/sop-admin-frontend/src/components/message-box/list.vue
deleted file mode 100644
index b0c64885..00000000
--- a/sop-admin/sop-admin-frontend/src/components/message-box/list.vue
+++ /dev/null
@@ -1,149 +0,0 @@
-
-
-
-
- 未开始
- 已开通
- 进行中
- 即将到期
-
-
-
-
-
-
-
-
-
-
-
- {{ item.title }}
-
- {{ item.subTitle }}
-
-
-
-
-
-
{{ item.content }}
-
- {{ item.time }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/message-box/locale/en-US.ts b/sop-admin/sop-admin-frontend/src/components/message-box/locale/en-US.ts
deleted file mode 100644
index 8a0f68e0..00000000
--- a/sop-admin/sop-admin-frontend/src/components/message-box/locale/en-US.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export default {
- 'messageBox.tab.title.message': 'Message',
- 'messageBox.tab.title.notice': 'Notice',
- 'messageBox.tab.title.todo': 'Todo',
- 'messageBox.tab.button': 'empty',
- 'messageBox.allRead': 'All Read',
- 'messageBox.viewMore': 'View More',
- 'messageBox.noContent': 'No Content',
- 'messageBox.switchRoles': 'Switch Roles',
- 'messageBox.userCenter': 'User Center',
- 'messageBox.userSettings': 'User Settings',
- 'messageBox.logout': 'Logout',
-};
diff --git a/sop-admin/sop-admin-frontend/src/components/message-box/locale/zh-CN.ts b/sop-admin/sop-admin-frontend/src/components/message-box/locale/zh-CN.ts
deleted file mode 100644
index cf3e6303..00000000
--- a/sop-admin/sop-admin-frontend/src/components/message-box/locale/zh-CN.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export default {
- 'messageBox.tab.title.message': '消息',
- 'messageBox.tab.title.notice': '通知',
- 'messageBox.tab.title.todo': '待办',
- 'messageBox.tab.button': '清空',
- 'messageBox.allRead': '全部已读',
- 'messageBox.viewMore': '查看更多',
- 'messageBox.noContent': '暂无内容',
- 'messageBox.switchRoles': '切换角色',
- 'messageBox.userCenter': '用户中心',
- 'messageBox.userSettings': '用户设置',
- 'messageBox.logout': '登出登录',
-};
diff --git a/sop-admin/sop-admin-frontend/src/components/navbar/index.vue b/sop-admin/sop-admin-frontend/src/components/navbar/index.vue
deleted file mode 100644
index e37b6978..00000000
--- a/sop-admin/sop-admin-frontend/src/components/navbar/index.vue
+++ /dev/null
@@ -1,323 +0,0 @@
-
-
-
-
-
-
- Arco Pro
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ item.label }}
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- {{ $t('messageBox.switchRoles') }}
-
-
-
-
-
-
-
- {{ $t('messageBox.userCenter') }}
-
-
-
-
-
-
-
- {{ $t('messageBox.userSettings') }}
-
-
-
-
-
-
-
- {{ $t('messageBox.logout') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/tab-bar/index.vue b/sop-admin/sop-admin-frontend/src/components/tab-bar/index.vue
deleted file mode 100644
index 609ba2a4..00000000
--- a/sop-admin/sop-admin-frontend/src/components/tab-bar/index.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/components/tab-bar/readme.md b/sop-admin/sop-admin-frontend/src/components/tab-bar/readme.md
deleted file mode 100644
index cea5c934..00000000
--- a/sop-admin/sop-admin-frontend/src/components/tab-bar/readme.md
+++ /dev/null
@@ -1,12 +0,0 @@
-## 组件说明
-
-该组件非官方最终设计规范,以单独组件存在。
-
-同时仅仅提供最基本的功能,后续进行优化及更改。
-
-
-## Component description
-
-The component unofficial final design specification exists as a separate component.
-
-At the same time, only the most basic functions are provided, and subsequent optimizations and changes will be made.
\ No newline at end of file
diff --git a/sop-admin/sop-admin-frontend/src/components/tab-bar/tab-item.vue b/sop-admin/sop-admin-frontend/src/components/tab-bar/tab-item.vue
deleted file mode 100644
index bc2014d2..00000000
--- a/sop-admin/sop-admin-frontend/src/components/tab-bar/tab-item.vue
+++ /dev/null
@@ -1,200 +0,0 @@
-
-
-
-
- {{ $t(itemData.title) }}
-
-
-
-
-
-
-
-
- 重新加载
-
-
-
- 关闭当前标签页
-
-
-
- 关闭左侧标签页
-
-
-
- 关闭右侧标签页
-
-
-
- 关闭其它标签页
-
-
-
- 关闭全部标签页
-
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/config/index.ts b/sop-admin/sop-admin-frontend/src/config/index.ts
new file mode 100644
index 00000000..c81d1c4d
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/config/index.ts
@@ -0,0 +1,55 @@
+import axios from "axios";
+import type { App } from "vue";
+
+let config: object = {};
+const { VITE_PUBLIC_PATH } = import.meta.env;
+
+const setConfig = (cfg?: unknown) => {
+ config = Object.assign(config, cfg);
+};
+
+const getConfig = (key?: string): PlatformConfigs => {
+ 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 getPlatformConfig = async (app: App): Promise => {
+ app.config.globalProperties.$config = getConfig();
+ return axios({
+ method: "get",
+ url: `${VITE_PUBLIC_PATH}platform-config.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);
+ }
+ return $config;
+ })
+ .catch(() => {
+ throw "请在public文件夹下添加platform-config.json配置文件";
+ });
+};
+
+/** 本地响应式存储的命名空间 */
+const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace;
+
+export { getConfig, setConfig, responsiveStorageNameSpace };
diff --git a/sop-admin/sop-admin-frontend/src/config/settings.json b/sop-admin/sop-admin-frontend/src/config/settings.json
deleted file mode 100644
index ef20b23c..00000000
--- a/sop-admin/sop-admin-frontend/src/config/settings.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "theme": "light",
- "colorWeak": false,
- "navbar": true,
- "menu": true,
- "topMenu": false,
- "hideMenu": false,
- "menuCollapse": false,
- "footer": true,
- "themeColor": "#165DFF",
- "menuWidth": 220,
- "globalSettings": false,
- "device": "desktop",
- "tabBar": false,
- "menuFromServer": false,
- "serverMenu": []
-}
diff --git a/sop-admin/sop-admin-frontend/src/directive/index.ts b/sop-admin/sop-admin-frontend/src/directive/index.ts
deleted file mode 100644
index 85c567f8..00000000
--- a/sop-admin/sop-admin-frontend/src/directive/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { App } from 'vue';
-import permission from './permission';
-
-export default {
- install(Vue: App) {
- Vue.directive('permission', permission);
- },
-};
diff --git a/sop-admin/sop-admin-frontend/src/directive/permission/index.ts b/sop-admin/sop-admin-frontend/src/directive/permission/index.ts
deleted file mode 100644
index 4968e984..00000000
--- a/sop-admin/sop-admin-frontend/src/directive/permission/index.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { DirectiveBinding } from 'vue';
-import { useUserStore } from '@/store';
-
-function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
- const { value } = binding;
- const userStore = useUserStore();
- const { role } = userStore;
-
- if (Array.isArray(value)) {
- if (value.length > 0) {
- const permissionValues = value;
-
- const hasPermission = permissionValues.includes(role);
- if (!hasPermission && el.parentNode) {
- el.parentNode.removeChild(el);
- }
- }
- } else {
- throw new Error(`need roles! Like v-permission="['admin','user']"`);
- }
-}
-
-export default {
- mounted(el: HTMLElement, binding: DirectiveBinding) {
- checkPermission(el, binding);
- },
- updated(el: HTMLElement, binding: DirectiveBinding) {
- checkPermission(el, binding);
- },
-};
diff --git a/sop-admin/sop-admin-frontend/src/directives/auth/index.ts b/sop-admin/sop-admin-frontend/src/directives/auth/index.ts
new file mode 100644
index 00000000..2fc64904
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/directives/auth/index.ts
@@ -0,0 +1,15 @@
+import { hasAuth } from "@/router/utils";
+import type { Directive, 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(
+ "[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\""
+ );
+ }
+ }
+};
diff --git a/sop-admin/sop-admin-frontend/src/directives/copy/index.ts b/sop-admin/sop-admin-frontend/src/directives/copy/index.ts
new file mode 100644
index 00000000..b71fa190
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/directives/copy/index.ts
@@ -0,0 +1,33 @@
+import { message } from "@/utils/message";
+import { useEventListener } from "@vueuse/core";
+import { copyTextToClipboard } from "@pureadmin/utils";
+import type { Directive, DirectiveBinding } from "vue";
+
+export interface CopyEl extends HTMLElement {
+ copyValue: string;
+}
+
+/** 文本复制指令(默认双击复制) */
+export const copy: Directive = {
+ mounted(el: CopyEl, binding: DirectiveBinding) {
+ const { value } = binding;
+ if (value) {
+ el.copyValue = value;
+ const arg = binding.arg ?? "dblclick";
+ // Register using addEventListener on mounted, and removeEventListener automatically on unmounted
+ useEventListener(el, arg, () => {
+ const success = copyTextToClipboard(el.copyValue);
+ success
+ ? message("复制成功", { type: "success" })
+ : message("复制失败", { type: "error" });
+ });
+ } else {
+ throw new Error(
+ '[Directive: copy]: need value! Like v-copy="modelValue"'
+ );
+ }
+ },
+ updated(el: CopyEl, binding: DirectiveBinding) {
+ el.copyValue = binding.value;
+ }
+};
diff --git a/sop-admin/sop-admin-frontend/src/directives/index.ts b/sop-admin/sop-admin-frontend/src/directives/index.ts
new file mode 100644
index 00000000..d01fe714
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/directives/index.ts
@@ -0,0 +1,6 @@
+export * from "./auth";
+export * from "./copy";
+export * from "./longpress";
+export * from "./optimize";
+export * from "./perms";
+export * from "./ripple";
diff --git a/sop-admin/sop-admin-frontend/src/directives/longpress/index.ts b/sop-admin/sop-admin-frontend/src/directives/longpress/index.ts
new file mode 100644
index 00000000..4eec6a22
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/directives/longpress/index.ts
@@ -0,0 +1,63 @@
+import { useEventListener } from "@vueuse/core";
+import type { Directive, DirectiveBinding } from "vue";
+import { subBefore, subAfter, isFunction } from "@pureadmin/utils";
+
+export const longpress: Directive = {
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
+ const cb = binding.value;
+ if (cb && isFunction(cb)) {
+ let timer = null;
+ let interTimer = null;
+ let num = 500;
+ let interNum = null;
+ const isInter = binding?.arg?.includes(":") ?? false;
+
+ if (isInter) {
+ num = Number(subBefore(binding.arg, ":"));
+ interNum = Number(subAfter(binding.arg, ":"));
+ } else if (binding.arg) {
+ num = Number(binding.arg);
+ }
+
+ const clear = () => {
+ if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ if (interTimer) {
+ clearInterval(interTimer);
+ interTimer = null;
+ }
+ };
+
+ const onDownInter = (ev: PointerEvent) => {
+ ev.preventDefault();
+ if (interTimer === null) {
+ interTimer = setInterval(() => cb(), interNum);
+ }
+ };
+
+ const onDown = (ev: PointerEvent) => {
+ clear();
+ ev.preventDefault();
+ if (timer === null) {
+ timer = isInter
+ ? setTimeout(() => {
+ cb();
+ onDownInter(ev);
+ }, num)
+ : setTimeout(() => cb(), num);
+ }
+ };
+
+ // Register using addEventListener on mounted, and removeEventListener automatically on unmounted
+ useEventListener(el, "pointerdown", onDown);
+ useEventListener(el, "pointerup", clear);
+ useEventListener(el, "pointerleave", clear);
+ } else {
+ throw new Error(
+ '[Directive: longpress]: need callback and callback must be a function! Like v-longpress="callback"'
+ );
+ }
+ }
+};
diff --git a/sop-admin/sop-admin-frontend/src/directives/optimize/index.ts b/sop-admin/sop-admin-frontend/src/directives/optimize/index.ts
new file mode 100644
index 00000000..7b92538d
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/directives/optimize/index.ts
@@ -0,0 +1,68 @@
+import {
+ isArray,
+ throttle,
+ debounce,
+ isObject,
+ isFunction
+} from "@pureadmin/utils";
+import { useEventListener } from "@vueuse/core";
+import type { Directive, DirectiveBinding } from "vue";
+
+export interface OptimizeOptions {
+ /** 事件名 */
+ event: string;
+ /** 事件触发的方法 */
+ fn: (...params: any) => any;
+ /** 是否立即执行 */
+ immediate?: boolean;
+ /** 防抖或节流的延迟时间(防抖默认:`200`毫秒、节流默认:`1000`毫秒) */
+ timeout?: number;
+ /** 传递的参数 */
+ params?: any;
+}
+
+/** 防抖(v-optimize或v-optimize:debounce)、节流(v-optimize:throttle)指令 */
+export const optimize: Directive = {
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
+ const { value } = binding;
+ const optimizeType = binding.arg ?? "debounce";
+ const type = ["debounce", "throttle"].find(t => t === optimizeType);
+ if (type) {
+ if (value && value.event && isFunction(value.fn)) {
+ let params = value?.params;
+ if (params) {
+ if (isArray(params) || isObject(params)) {
+ params = isObject(params) ? Array.of(params) : params;
+ } else {
+ throw new Error(
+ "[Directive: optimize]: `params` must be an array or object"
+ );
+ }
+ }
+ // Register using addEventListener on mounted, and removeEventListener automatically on unmounted
+ useEventListener(
+ el,
+ value.event,
+ type === "debounce"
+ ? debounce(
+ params ? () => value.fn(...params) : value.fn,
+ value?.timeout ?? 200,
+ value?.immediate ?? false
+ )
+ : throttle(
+ params ? () => value.fn(...params) : value.fn,
+ value?.timeout ?? 1000
+ )
+ );
+ } else {
+ throw new Error(
+ "[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function"
+ );
+ }
+ } else {
+ throw new Error(
+ "[Directive: optimize]: only `debounce` and `throttle` are supported"
+ );
+ }
+ }
+};
diff --git a/sop-admin/sop-admin-frontend/src/directives/perms/index.ts b/sop-admin/sop-admin-frontend/src/directives/perms/index.ts
new file mode 100644
index 00000000..073c918b
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/directives/perms/index.ts
@@ -0,0 +1,15 @@
+import { hasPerms } from "@/utils/auth";
+import type { Directive, DirectiveBinding } from "vue";
+
+export const perms: Directive = {
+ mounted(el: HTMLElement, binding: DirectiveBinding>) {
+ const { value } = binding;
+ if (value) {
+ !hasPerms(value) && el.parentNode?.removeChild(el);
+ } else {
+ throw new Error(
+ "[Directive: perms]: need perms! Like v-perms=\"['btn.add','btn.edit']\""
+ );
+ }
+ }
+};
diff --git a/sop-admin/sop-admin-frontend/src/directives/ripple/index.scss b/sop-admin/sop-admin-frontend/src/directives/ripple/index.scss
new file mode 100644
index 00000000..061c82c9
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/directives/ripple/index.scss
@@ -0,0 +1,48 @@
+/* stylelint-disable-next-line scss/dollar-variable-colon-space-after */
+$ripple-animation-transition-in:
+ transform 0.4s cubic-bezier(0, 0, 0.2, 1),
+ opacity 0.2s cubic-bezier(0, 0, 0.2, 1) !default;
+$ripple-animation-transition-out: opacity 0.5s cubic-bezier(0, 0, 0.2, 1) !default;
+$ripple-animation-visible-opacity: 0.25 !default;
+
+.v-ripple {
+ &__container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ pointer-events: none;
+ border-radius: inherit;
+ contain: strict;
+ }
+
+ &__animation {
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+ pointer-events: none;
+ background: currentcolor;
+ border-radius: 50%;
+ opacity: 0;
+ will-change: transform, opacity;
+
+ &--enter {
+ opacity: 0;
+ transition: none;
+ }
+
+ &--in {
+ opacity: $ripple-animation-visible-opacity;
+ transition: $ripple-animation-transition-in;
+ }
+
+ &--out {
+ opacity: 0;
+ transition: $ripple-animation-transition-out;
+ }
+ }
+}
diff --git a/sop-admin/sop-admin-frontend/src/directives/ripple/index.ts b/sop-admin/sop-admin-frontend/src/directives/ripple/index.ts
new file mode 100644
index 00000000..3fd94d9c
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/directives/ripple/index.ts
@@ -0,0 +1,229 @@
+import "./index.scss";
+import { isObject } from "@pureadmin/utils";
+import type { Directive, DirectiveBinding } from "vue";
+
+export interface RippleOptions {
+ /** 自定义`ripple`颜色,支持`tailwindcss` */
+ class?: string;
+ /** 是否从中心扩散 */
+ center?: boolean;
+ circle?: boolean;
+}
+
+export interface RippleDirectiveBinding
+ extends Omit {
+ value?: boolean | { class: string };
+ modifiers: {
+ center?: boolean;
+ circle?: boolean;
+ };
+}
+
+function transform(el: HTMLElement, value: string) {
+ el.style.transform = value;
+ el.style.webkitTransform = value;
+}
+
+const calculate = (
+ e: PointerEvent,
+ el: HTMLElement,
+ value: RippleOptions = {}
+) => {
+ const offset = el.getBoundingClientRect();
+
+ // 获取点击位置距离 el 的垂直和水平距离
+ let localX = e.clientX - offset.left;
+ let localY = e.clientY - offset.top;
+
+ let radius = 0;
+ let scale = 0.3;
+ // 计算点击位置到 el 顶点最远距离,即为圆的最大半径(勾股定理)
+ if (el._ripple?.circle) {
+ scale = 0.15;
+ radius = el.clientWidth / 2;
+ radius = value.center
+ ? radius
+ : radius + Math.sqrt((localX - radius) ** 2 + (localY - radius) ** 2) / 4;
+ } else {
+ radius = Math.sqrt(el.clientWidth ** 2 + el.clientHeight ** 2) / 2;
+ }
+
+ // 中心点坐标
+ const centerX = `${(el.clientWidth - radius * 2) / 2}px`;
+ const centerY = `${(el.clientHeight - radius * 2) / 2}px`;
+
+ // 点击位置坐标
+ const x = value.center ? centerX : `${localX - radius}px`;
+ const y = value.center ? centerY : `${localY - radius}px`;
+
+ return { radius, scale, x, y, centerX, centerY };
+};
+
+const ripples = {
+ show(e: PointerEvent, el: HTMLElement, value: RippleOptions = {}) {
+ if (!el?._ripple?.enabled) {
+ return;
+ }
+
+ // 创建 ripple 元素和 ripple 父元素
+ const container = document.createElement("span");
+ const animation = document.createElement("span");
+
+ container.appendChild(animation);
+ container.className = "v-ripple__container";
+
+ if (value.class) {
+ container.className += ` ${value.class}`;
+ }
+
+ const { radius, scale, x, y, centerX, centerY } = calculate(e, el, value);
+
+ // ripple 圆大小
+ const size = `${radius * 2}px`;
+
+ animation.className = "v-ripple__animation";
+ animation.style.width = size;
+ animation.style.height = size;
+
+ el.appendChild(container);
+
+ // 获取目标元素样式表
+ const computed = window.getComputedStyle(el);
+ // 防止 position 被覆盖导致 ripple 位置有问题
+ if (computed && computed.position === "static") {
+ el.style.position = "relative";
+ el.dataset.previousPosition = "static";
+ }
+
+ animation.classList.add("v-ripple__animation--enter");
+ animation.classList.add("v-ripple__animation--visible");
+ transform(
+ animation,
+ `translate(${x}, ${y}) scale3d(${scale},${scale},${scale})`
+ );
+ animation.dataset.activated = String(performance.now());
+
+ setTimeout(() => {
+ animation.classList.remove("v-ripple__animation--enter");
+ animation.classList.add("v-ripple__animation--in");
+ transform(animation, `translate(${centerX}, ${centerY}) scale3d(1,1,1)`);
+ }, 0);
+ },
+
+ hide(el: HTMLElement | null) {
+ if (!el?._ripple?.enabled) return;
+
+ const ripples = el.getElementsByClassName("v-ripple__animation");
+
+ if (ripples.length === 0) return;
+ const animation = ripples[ripples.length - 1] as HTMLElement;
+
+ if (animation.dataset.isHiding) return;
+ else animation.dataset.isHiding = "true";
+
+ const diff = performance.now() - Number(animation.dataset.activated);
+ const delay = Math.max(250 - diff, 0);
+
+ setTimeout(() => {
+ animation.classList.remove("v-ripple__animation--in");
+ animation.classList.add("v-ripple__animation--out");
+
+ setTimeout(() => {
+ const ripples = el.getElementsByClassName("v-ripple__animation");
+ if (ripples.length === 1 && el.dataset.previousPosition) {
+ el.style.position = el.dataset.previousPosition;
+ delete el.dataset.previousPosition;
+ }
+
+ if (animation.parentNode?.parentNode === el)
+ el.removeChild(animation.parentNode);
+ }, 300);
+ }, delay);
+ }
+};
+
+function isRippleEnabled(value: any): value is true {
+ return typeof value === "undefined" || !!value;
+}
+
+function rippleShow(e: PointerEvent) {
+ const value: RippleOptions = {};
+ const element = e.currentTarget as HTMLElement | undefined;
+
+ if (!element?._ripple || element._ripple.touched) return;
+
+ value.center = element._ripple.centered;
+ if (element._ripple.class) {
+ value.class = element._ripple.class;
+ }
+
+ ripples.show(e, element, value);
+}
+
+function rippleHide(e: Event) {
+ const element = e.currentTarget as HTMLElement | null;
+ if (!element?._ripple) return;
+
+ window.setTimeout(() => {
+ if (element._ripple) {
+ element._ripple.touched = false;
+ }
+ });
+ ripples.hide(element);
+}
+
+function updateRipple(
+ el: HTMLElement,
+ binding: RippleDirectiveBinding,
+ wasEnabled: boolean
+) {
+ const { value, modifiers } = binding;
+ const enabled = isRippleEnabled(value);
+ if (!enabled) {
+ ripples.hide(el);
+ }
+
+ el._ripple = el._ripple ?? {};
+ el._ripple.enabled = enabled;
+ el._ripple.centered = modifiers.center;
+ el._ripple.circle = modifiers.circle;
+ if (isObject(value) && value.class) {
+ el._ripple.class = value.class;
+ }
+
+ if (enabled && !wasEnabled) {
+ el.addEventListener("pointerdown", rippleShow);
+ el.addEventListener("pointerup", rippleHide);
+ } else if (!enabled && wasEnabled) {
+ removeListeners(el);
+ }
+}
+
+function removeListeners(el: HTMLElement) {
+ el.removeEventListener("pointerdown", rippleShow);
+ el.removeEventListener("pointerup", rippleHide);
+}
+
+function mounted(el: HTMLElement, binding: RippleDirectiveBinding) {
+ updateRipple(el, binding, false);
+}
+
+function unmounted(el: HTMLElement) {
+ delete el._ripple;
+ removeListeners(el);
+}
+
+function updated(el: HTMLElement, binding: RippleDirectiveBinding) {
+ if (binding.value === binding.oldValue) {
+ return;
+ }
+
+ const wasEnabled = isRippleEnabled(binding.oldValue);
+ updateRipple(el, binding, wasEnabled);
+}
+
+export const Ripple: Directive = {
+ mounted,
+ unmounted,
+ updated
+};
diff --git a/sop-admin/sop-admin-frontend/src/env.d.ts b/sop-admin/sop-admin-frontend/src/env.d.ts
deleted file mode 100644
index 6b7fc7d7..00000000
--- a/sop-admin/sop-admin-frontend/src/env.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-///
-
-declare module '*.vue' {
- import { DefineComponent } from 'vue';
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
- const component: DefineComponent<{}, {}, any>;
- export default component;
-}
-interface ImportMetaEnv {
- readonly VITE_API_BASE_URL: string;
-}
diff --git a/sop-admin/sop-admin-frontend/src/hooks/chart-option.ts b/sop-admin/sop-admin-frontend/src/hooks/chart-option.ts
deleted file mode 100644
index 13c39260..00000000
--- a/sop-admin/sop-admin-frontend/src/hooks/chart-option.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { computed } from 'vue';
-import { EChartsOption } from 'echarts';
-import { useAppStore } from '@/store';
-
-// for code hints
-// import { SeriesOption } from 'echarts';
-// Because there are so many configuration items, this provides a relatively convenient code hint.
-// When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, TypeScript does not report errors, and code writing is convenient.
-interface optionsFn {
- (isDark: boolean): EChartsOption;
-}
-
-export default function useChartOption(sourceOption: optionsFn) {
- const appStore = useAppStore();
- const isDark = computed(() => {
- return appStore.theme === 'dark';
- });
- // echarts support https://echarts.apache.org/zh/theme-builder.html
- // It's not used here
- // TODO echarts themes
- const chartOption = computed(() => {
- return sourceOption(isDark.value);
- });
- return {
- chartOption,
- };
-}
diff --git a/sop-admin/sop-admin-frontend/src/hooks/loading.ts b/sop-admin/sop-admin-frontend/src/hooks/loading.ts
deleted file mode 100644
index a6f2768a..00000000
--- a/sop-admin/sop-admin-frontend/src/hooks/loading.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ref } from 'vue';
-
-export default function useLoading(initValue = false) {
- const loading = ref(initValue);
- const setLoading = (value: boolean) => {
- loading.value = value;
- };
- const toggle = () => {
- loading.value = !loading.value;
- };
- return {
- loading,
- setLoading,
- toggle,
- };
-}
diff --git a/sop-admin/sop-admin-frontend/src/hooks/locale.ts b/sop-admin/sop-admin-frontend/src/hooks/locale.ts
deleted file mode 100644
index afe4bf76..00000000
--- a/sop-admin/sop-admin-frontend/src/hooks/locale.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { computed } from 'vue';
-import { useI18n } from 'vue-i18n';
-import { Message } from '@arco-design/web-vue';
-
-export default function useLocale() {
- const i18 = useI18n();
- const currentLocale = computed(() => {
- return i18.locale.value;
- });
- const changeLocale = (value: string) => {
- if (i18.locale.value === value) {
- return;
- }
- i18.locale.value = value;
- localStorage.setItem('arco-locale', value);
- Message.success(i18.t('navbar.action.locale'));
- };
- return {
- currentLocale,
- changeLocale,
- };
-}
diff --git a/sop-admin/sop-admin-frontend/src/hooks/permission.ts b/sop-admin/sop-admin-frontend/src/hooks/permission.ts
deleted file mode 100644
index de8cdc4f..00000000
--- a/sop-admin/sop-admin-frontend/src/hooks/permission.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
-import { useUserStore } from '@/store';
-
-export default function usePermission() {
- const userStore = useUserStore();
- return {
- accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
- return (
- !route.meta?.requiresAuth ||
- !route.meta?.roles ||
- route.meta?.roles?.includes('*') ||
- route.meta?.roles?.includes(userStore.role)
- );
- },
- findFirstPermissionRoute(_routers: any, role = 'admin') {
- const cloneRouters = [..._routers];
- while (cloneRouters.length) {
- const firstElement = cloneRouters.shift();
- if (
- firstElement?.meta?.roles?.find((el: string[]) => {
- return el.includes('*') || el.includes(role);
- })
- )
- return { name: firstElement.name };
- if (firstElement?.children) {
- cloneRouters.push(...firstElement.children);
- }
- }
- return null;
- },
- // You can add any rules you want
- };
-}
diff --git a/sop-admin/sop-admin-frontend/src/hooks/request.ts b/sop-admin/sop-admin-frontend/src/hooks/request.ts
deleted file mode 100644
index 7b853362..00000000
--- a/sop-admin/sop-admin-frontend/src/hooks/request.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { ref, UnwrapRef } from 'vue';
-import { AxiosResponse } from 'axios';
-import { HttpResponse } from '@/api/interceptor';
-import useLoading from './loading';
-
-// use to fetch list
-// Don't use async function. It doesn't work in async function.
-// Use the bind function to add parameters
-// example: useRequest(api.bind(null, {}))
-
-export default function useRequest(
- api: () => Promise>,
- defaultValue = [] as unknown as T,
- isLoading = true
-) {
- const { loading, setLoading } = useLoading(isLoading);
- const response = ref(defaultValue);
- api()
- .then((res) => {
- response.value = res.data as unknown as UnwrapRef;
- })
- .finally(() => {
- setLoading(false);
- });
- return { loading, response };
-}
diff --git a/sop-admin/sop-admin-frontend/src/hooks/responsive.ts b/sop-admin/sop-admin-frontend/src/hooks/responsive.ts
deleted file mode 100644
index eefee8e2..00000000
--- a/sop-admin/sop-admin-frontend/src/hooks/responsive.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue';
-import { useDebounceFn } from '@vueuse/core';
-import { useAppStore } from '@/store';
-import { addEventListen, removeEventListen } from '@/utils/event';
-
-const WIDTH = 992; // https://arco.design/vue/component/grid#responsivevalue
-
-function queryDevice() {
- const rect = document.body.getBoundingClientRect();
- return rect.width - 1 < WIDTH;
-}
-
-export default function useResponsive(immediate?: boolean) {
- const appStore = useAppStore();
- function resizeHandler() {
- if (!document.hidden) {
- const isMobile = queryDevice();
- appStore.toggleDevice(isMobile ? 'mobile' : 'desktop');
- appStore.toggleMenu(isMobile);
- }
- }
- const debounceFn = useDebounceFn(resizeHandler, 100);
- onMounted(() => {
- if (immediate) debounceFn();
- });
- onBeforeMount(() => {
- addEventListen(window, 'resize', debounceFn);
- });
- onBeforeUnmount(() => {
- removeEventListen(window, 'resize', debounceFn);
- });
-}
diff --git a/sop-admin/sop-admin-frontend/src/hooks/themes.ts b/sop-admin/sop-admin-frontend/src/hooks/themes.ts
deleted file mode 100644
index 7ce85b78..00000000
--- a/sop-admin/sop-admin-frontend/src/hooks/themes.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { computed } from 'vue';
-import { useAppStore } from '@/store';
-
-export default function useThemes() {
- const appStore = useAppStore();
- const isDark = computed(() => {
- return appStore.theme === 'dark';
- });
- return {
- isDark,
- };
-}
diff --git a/sop-admin/sop-admin-frontend/src/hooks/user.ts b/sop-admin/sop-admin-frontend/src/hooks/user.ts
deleted file mode 100644
index 107358cb..00000000
--- a/sop-admin/sop-admin-frontend/src/hooks/user.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useRouter } from 'vue-router';
-import { Message } from '@arco-design/web-vue';
-
-import { useUserStore } from '@/store';
-
-export default function useUser() {
- const router = useRouter();
- const userStore = useUserStore();
- const logout = async (logoutTo?: string) => {
- await userStore.logout();
- const currentRoute = router.currentRoute.value;
- Message.success('登出成功');
- router.push({
- name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login',
- query: {
- ...router.currentRoute.value.query,
- redirect: currentRoute.name as string,
- },
- });
- };
- return {
- logout,
- };
-}
diff --git a/sop-admin/sop-admin-frontend/src/hooks/visible.ts b/sop-admin/sop-admin-frontend/src/hooks/visible.ts
deleted file mode 100644
index 211f90da..00000000
--- a/sop-admin/sop-admin-frontend/src/hooks/visible.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ref } from 'vue';
-
-export default function useVisible(initValue = false) {
- const visible = ref(initValue);
- const setVisible = (value: boolean) => {
- visible.value = value;
- };
- const toggle = () => {
- visible.value = !visible.value;
- };
- return {
- visible,
- setVisible,
- toggle,
- };
-}
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-content/index.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-content/index.vue
new file mode 100644
index 00000000..5810d665
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-content/index.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-footer/index.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-footer/index.vue
new file mode 100644
index 00000000..77631343
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-footer/index.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-frame/index.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-frame/index.vue
new file mode 100644
index 00000000..b2bb9d51
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-frame/index.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-navbar/index.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-navbar/index.vue
new file mode 100644
index 00000000..e9030edf
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-navbar/index.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/components/NoticeItem.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/components/NoticeItem.vue
new file mode 100644
index 00000000..823d9cd8
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/components/NoticeItem.vue
@@ -0,0 +1,177 @@
+
+
+
+
+
+
+
+
+
+ {{ noticeItem.title }}
+
+
+
+
+
+
+
+ {{ noticeItem.description }}
+
+
+
+ {{ noticeItem.datetime }}
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/components/NoticeList.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/components/NoticeList.vue
new file mode 100644
index 00000000..7bc9922c
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/components/NoticeList.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/data.ts b/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/data.ts
new file mode 100644
index 00000000..bd49f5e5
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/data.ts
@@ -0,0 +1,99 @@
+import { $t } from "@/plugins/i18n";
+
+export interface ListItem {
+ avatar: string;
+ title: string;
+ datetime: string;
+ type: string;
+ description: string;
+ status?: "primary" | "success" | "warning" | "info" | "danger";
+ extra?: string;
+}
+
+export interface TabItem {
+ key: string;
+ name: string;
+ list: ListItem[];
+ emptyText: string;
+}
+
+export const noticesData: TabItem[] = [
+ {
+ key: "1",
+ name: $t("status.pureNotify"),
+ list: [],
+ emptyText: $t("status.pureNoNotify")
+ },
+ {
+ key: "2",
+ name: $t("status.pureMessage"),
+ list: [
+ {
+ avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile1.svg",
+ title: "小铭 评论了你",
+ description: "诚在于心,信在于行,诚信在于心行合一。",
+ datetime: "今天",
+ type: "2"
+ },
+ {
+ avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile2.svg",
+ title: "李白 回复了你",
+ description: "长风破浪会有时,直挂云帆济沧海。",
+ datetime: "昨天",
+ type: "2"
+ },
+ {
+ avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile5.svg",
+ title: "标题",
+ description:
+ "请将鼠标移动到此处,以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2,超过2行的描述内容将被省略并且可以通过tooltip查看完整内容",
+ datetime: "时间",
+ type: "2"
+ }
+ ],
+ emptyText: $t("status.pureNoMessage")
+ },
+ {
+ key: "3",
+ name: $t("status.pureTodo"),
+ list: [
+ {
+ avatar: "",
+ title: "第三方紧急代码变更",
+ description:
+ "小林提交于 2024-05-10,需在 2024-05-11 前完成代码变更任务",
+ datetime: "",
+ extra: "马上到期",
+ status: "danger",
+ type: "3"
+ },
+ {
+ avatar: "",
+ title: "版本发布",
+ description: "指派小铭于 2024-06-18 前完成更新并发布",
+ datetime: "",
+ extra: "已耗时 8 天",
+ status: "warning",
+ type: "3"
+ },
+ {
+ avatar: "",
+ title: "新功能开发",
+ description: "开发多租户管理",
+ datetime: "",
+ extra: "进行中",
+ type: "3"
+ },
+ {
+ avatar: "",
+ title: "任务名称",
+ description: "任务需要在 2030-10-30 10:00 前启动",
+ datetime: "",
+ extra: "未开始",
+ status: "info",
+ type: "3"
+ }
+ ],
+ emptyText: $t("status.pureNoTodo")
+ }
+];
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/index.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/index.vue
new file mode 100644
index 00000000..d85cf0f7
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-notice/index.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-panel/index.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-panel/index.vue
new file mode 100644
index 00000000..fb4fb20a
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-panel/index.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+ {{ t("panel.pureSystemSet") }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("panel.pureClearCache") }}
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchFooter.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchFooter.vue
new file mode 100644
index 00000000..d8350d0f
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchFooter.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchHistory.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchHistory.vue
new file mode 100644
index 00000000..87d5488c
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchHistory.vue
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+ {{ t("search.pureHistory") }}
+
+
+
+
+
+
+
+ {{
+ `${t("search.pureCollect")}${collectList.length > 1 ? t("search.pureDragSort") : ""}`
+ }}
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchHistoryItem.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchHistoryItem.vue
new file mode 100644
index 00000000..0ee9f194
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchHistoryItem.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+ {{ transformI18n(item.meta?.title) }}
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchModal.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchModal.vue
new file mode 100644
index 00000000..48fb9cf7
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchModal.vue
@@ -0,0 +1,338 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchResult.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchResult.vue
new file mode 100644
index 00000000..12507b15
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/components/SearchResult.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+ {{ transformI18n(item.meta?.title) }}
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-search/index.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/index.vue
new file mode 100644
index 00000000..123d6a66
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/index.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-search/types.ts b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/types.ts
new file mode 100644
index 00000000..a39adbd4
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-search/types.ts
@@ -0,0 +1,20 @@
+interface optionsItem {
+ path: string;
+ type: "history" | "collect";
+ meta: {
+ icon?: string;
+ title?: string;
+ };
+}
+
+interface dragItem {
+ oldIndex: number;
+ newIndex: number;
+}
+
+interface Props {
+ value: string;
+ options: Array;
+}
+
+export type { optionsItem, dragItem, Props };
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-setting/index.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-setting/index.vue
new file mode 100644
index 00000000..18aacf25
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-setting/index.vue
@@ -0,0 +1,642 @@
+
+
+
+
+
+
{{ t("panel.pureOverallStyle") }}
+
{
+ theme.index === 1 && theme.index !== 2
+ ? (dataTheme = true)
+ : (dataTheme = false);
+ overallStyle = theme.option.theme;
+ dataThemeChange(theme.option.theme);
+ theme.index === 2 && watchSystemThemeChange();
+ }
+ "
+ />
+
+ {{ t("panel.pureThemeColor") }}
+
+
+ {{ t("panel.pureLayoutModel") }}
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+ {{ t("panel.pureStretch") }}
+
+ setStretch(value)"
+ />
+
+
+
+ {{ t("panel.pureTagsStyle") }}
+
+
+
+ {{ t("panel.pureInterfaceDisplay") }}
+
+
+ -
+ {{ t("panel.pureGreyModel") }}
+
+
+ -
+ {{ t("panel.pureWeakModel") }}
+
+
+ -
+ {{ t("panel.pureHiddenTags") }}
+
+
+ -
+ {{ t("panel.pureHiddenFooter") }}
+
+
+ -
+ Logo
+
+
+ -
+
+ {{ t("panel.pureMultiTagsCache") }}
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/NavHorizontal.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/NavHorizontal.vue
new file mode 100644
index 00000000..e2e81d06
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/NavHorizontal.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/NavMix.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/NavMix.vue
new file mode 100644
index 00000000..fc50ede5
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/NavMix.vue
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/NavVertical.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/NavVertical.vue
new file mode 100644
index 00000000..0e9fa129
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/NavVertical.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue
new file mode 100644
index 00000000..417b8394
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+ {{ transformI18n(item.meta.title) }}
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue
new file mode 100644
index 00000000..447fa500
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue
new file mode 100644
index 00000000..7cad16e6
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue
new file mode 100644
index 00000000..4d38bd0c
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarItem.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarItem.vue
new file mode 100644
index 00000000..cffa6771
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarItem.vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+ {{ transformI18n(onlyOneChild.meta.title) }}
+
+
+
+
+
+ {{ transformI18n(onlyOneChild.meta.title) }}
+
+
+
+
+
+
+
+
+
+
+ {{ transformI18n(item.meta.title) }}
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue
new file mode 100644
index 00000000..c007d3b8
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue
new file mode 100644
index 00000000..8911c122
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarLogo.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarLogo.vue
new file mode 100644
index 00000000..0441f52f
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarLogo.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue
new file mode 100644
index 00000000..c2f1b5ad
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-tag/components/TagChrome.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-tag/components/TagChrome.vue
new file mode 100644
index 00000000..137365b4
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-tag/components/TagChrome.vue
@@ -0,0 +1,33 @@
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-tag/index.scss b/sop-admin/sop-admin-frontend/src/layout/components/lay-tag/index.scss
new file mode 100644
index 00000000..b8812169
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-tag/index.scss
@@ -0,0 +1,371 @@
+@keyframes schedule-in-width {
+ from {
+ width: 0;
+ }
+
+ to {
+ width: 100%;
+ }
+}
+
+@keyframes schedule-out-width {
+ from {
+ width: 100%;
+ }
+
+ to {
+ width: 0;
+ }
+}
+
+.tags-view {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ font-size: 14px;
+ color: var(--el-text-color-primary);
+ background: #fff;
+ box-shadow: 0 0 1px #888;
+
+ .scroll-item {
+ position: relative;
+ display: inline-block;
+ height: 34px;
+ padding-left: 6px;
+ line-height: 34px;
+ cursor: pointer;
+ transition: all 0.4s;
+
+ &:not(:first-child) {
+ padding-right: 24px;
+ }
+
+ &.chrome-item {
+ padding-right: 0;
+ padding-left: 0;
+ margin-right: -18px;
+ box-shadow: none;
+ }
+
+ .el-icon-close {
+ position: absolute;
+ top: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ color: var(--el-color-primary);
+ cursor: pointer;
+ border-radius: 4px;
+ transition:
+ background-color 0.12s,
+ color 0.12s;
+ transform: translate(0, -50%);
+
+ &:hover {
+ color: rgb(0 0 0 / 88%) !important;
+ background-color: rgb(0 0 0 / 6%);
+ }
+ }
+ }
+
+ .tag-title {
+ padding: 0 4px;
+ color: var(--el-text-color-primary);
+ text-decoration: none;
+ }
+
+ .scroll-container {
+ position: relative;
+ flex: 1;
+ overflow: hidden;
+ white-space: nowrap;
+
+ &.chrome-scroll-container {
+ padding-top: 4px;
+
+ .fixed-tag {
+ padding: 0 !important;
+ }
+ }
+
+ .tab {
+ position: relative;
+ float: left;
+ overflow: visible;
+ white-space: nowrap;
+ list-style: none;
+
+ .scroll-item {
+ transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+ &:nth-child(1) {
+ padding: 0 12px;
+ }
+
+ &.chrome-item {
+ &:nth-child(1) {
+ padding: 0;
+ }
+ }
+ }
+
+ .fixed-tag {
+ padding: 0 12px;
+ }
+ }
+ }
+
+ /* 右键菜单 */
+ .contextmenu {
+ position: absolute;
+ padding: 5px 0;
+ margin: 0;
+ font-size: 13px;
+ font-weight: normal;
+ color: var(--el-text-color-primary);
+ white-space: nowrap;
+ list-style-type: none;
+ background: #fff;
+ border-radius: 4px;
+ outline: 0;
+ box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
+
+ li {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ padding: 7px 12px;
+ margin: 0;
+ cursor: pointer;
+
+ &:hover {
+ color: var(--el-color-primary);
+ }
+
+ svg {
+ display: block;
+ margin-right: 0.5em;
+ }
+ }
+ }
+}
+
+.el-dropdown-menu {
+ li {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ margin: 0;
+ cursor: pointer;
+
+ 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 {
+ position: relative;
+ color: #fff;
+ box-shadow: 0 0 0.7px #888;
+
+ .chrome-tab {
+ z-index: 10;
+ }
+
+ .chrome-tab__bg {
+ color: var(--el-color-primary-light-9) !important;
+ }
+
+ .tag-title {
+ color: var(--el-color-primary) !important;
+ }
+
+ .chrome-close-btn {
+ color: var(--el-color-primary);
+
+ &:hover {
+ background-color: var(--el-color-primary);
+ }
+ }
+
+ .chrome-tab-divider {
+ opacity: 0;
+ }
+}
+
+.arrow-left,
+.arrow-right,
+.arrow-down {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 34px;
+ color: var(--el-text-color-primary);
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+}
+
+.arrow-left {
+ box-shadow: 5px 0 5px -6px #ccc;
+
+ &:hover {
+ cursor: w-resize;
+ }
+}
+
+.arrow-right {
+ border-right: 0.5px solid #ccc;
+ box-shadow: -5px 0 5px -6px #ccc;
+
+ &:hover {
+ cursor: e-resize;
+ }
+}
+
+/* 卡片模式下鼠标移入显示蓝色边框 */
+.card-in {
+ color: var(--el-color-primary);
+
+ .tag-title {
+ color: var(--el-color-primary);
+ }
+}
+
+/* 卡片模式下鼠标移出隐藏蓝色边框 */
+.card-out {
+ color: #666;
+ border: none;
+
+ .tag-title {
+ color: #666;
+ }
+}
+
+/* 灵动模式 */
+.schedule-active {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ background: var(--el-color-primary);
+}
+
+/* 灵动模式下鼠标移入显示蓝色进度条 */
+.schedule-in {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ background: var(--el-color-primary);
+ animation: schedule-in-width 200ms ease-in;
+}
+
+/* 灵动模式下鼠标移出隐藏蓝色进度条 */
+.schedule-out {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 0;
+ height: 2px;
+ background: var(--el-color-primary);
+ animation: schedule-out-width 200ms ease-in;
+}
+
+/* 谷歌风格的页签 */
+.chrome-tab {
+ position: relative;
+ display: inline-flex;
+ gap: 16px;
+ align-items: center;
+ justify-content: center;
+ padding: 0 24px;
+ white-space: nowrap;
+ cursor: pointer;
+
+ .tag-title {
+ padding: 0;
+ }
+
+ .chrome-tab-divider {
+ position: absolute;
+ right: 7px;
+ width: 1px;
+ height: 14px;
+ background-color: #2b2d2f;
+ }
+
+ &:hover {
+ z-index: 10;
+
+ .chrome-tab__bg {
+ color: #dee1e6;
+ }
+
+ .tag-title {
+ color: #1f1f1f;
+ }
+
+ .chrome-tab-divider {
+ opacity: 0;
+ }
+ }
+
+ .chrome-tab__bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: -10;
+ width: 100%;
+ height: 100%;
+ color: transparent;
+ pointer-events: none;
+ }
+
+ .chrome-close-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ color: #666;
+ border-radius: 50%;
+
+ &:hover {
+ color: white;
+ background-color: #b1b3b8;
+ }
+ }
+}
diff --git a/sop-admin/sop-admin-frontend/src/layout/components/lay-tag/index.vue b/sop-admin/sop-admin-frontend/src/layout/components/lay-tag/index.vue
new file mode 100644
index 00000000..87f234fd
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/components/lay-tag/index.vue
@@ -0,0 +1,685 @@
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/default-layout.vue b/sop-admin/sop-admin-frontend/src/layout/default-layout.vue
deleted file mode 100644
index 6e570677..00000000
--- a/sop-admin/sop-admin-frontend/src/layout/default-layout.vue
+++ /dev/null
@@ -1,178 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/layout/frame.vue b/sop-admin/sop-admin-frontend/src/layout/frame.vue
new file mode 100644
index 00000000..4243b57d
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/frame.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/hooks/useBoolean.ts b/sop-admin/sop-admin-frontend/src/layout/hooks/useBoolean.ts
new file mode 100644
index 00000000..1d140317
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/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/sop-admin/sop-admin-frontend/src/layout/hooks/useDataThemeChange.ts b/sop-admin/sop-admin-frontend/src/layout/hooks/useDataThemeChange.ts
new file mode 100644
index 00000000..80db6dd1
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/hooks/useDataThemeChange.ts
@@ -0,0 +1,145 @@
+import { ref } from "vue";
+import { getConfig } from "@/config";
+import { useLayout } from "./useLayout";
+import { removeToken } from "@/utils/auth";
+import { routerArrays } from "@/layout/types";
+import { router, resetRouter } from "@/router";
+import type { themeColorsType } from "../types";
+import { useAppStoreHook } from "@/store/modules/app";
+import { useGlobal, storageLocal } from "@pureadmin/utils";
+import { useEpThemeStoreHook } from "@/store/modules/epTheme";
+import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
+import {
+ darken,
+ lighten,
+ toggleTheme
+} from "@pureadmin/theme/dist/browser-utils";
+
+export function useDataThemeChange() {
+ const { layoutTheme, layout } = useLayout();
+ const themeColors = ref>([
+ /* 亮白色 */
+ { color: "#ffffff", themeColor: "light" },
+ /* 道奇蓝 */
+ { color: "#1b2a47", themeColor: "default" },
+ /* 深紫罗兰色 */
+ { color: "#722ed1", themeColor: "saucePurple" },
+ /* 深粉色 */
+ { color: "#eb2f96", themeColor: "pink" },
+ /* 猩红色 */
+ { color: "#f5222d", themeColor: "dusk" },
+ /* 橙红色 */
+ { color: "#fa541c", themeColor: "volcano" },
+ /* 绿宝石 */
+ { color: "#13c2c2", themeColor: "mingQing" },
+ /* 酸橙绿 */
+ { color: "#52c41a", themeColor: "auroraGreen" }
+ ]);
+
+ const { $storage } = useGlobal();
+ const dataTheme = ref($storage?.layout?.darkMode);
+ const overallStyle = ref($storage?.layout?.overallStyle);
+ const body = document.documentElement as HTMLElement;
+
+ function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
+ const targetEl = target || document.body;
+ let { className } = targetEl;
+ className = className.replace(clsName, "").trim();
+ targetEl.className = flag ? `${className} ${clsName}` : className;
+ }
+
+ /** 设置导航主题色 */
+ function setLayoutThemeColor(
+ theme = getConfig().Theme ?? "light",
+ isClick = true
+ ) {
+ layoutTheme.value.theme = theme;
+ toggleTheme({
+ scopeName: `layout-theme-${theme}`
+ });
+ // 如果非isClick,保留之前的themeColor
+ const storageThemeColor = $storage.layout.themeColor;
+ $storage.layout = {
+ layout: layout.value,
+ theme,
+ darkMode: dataTheme.value,
+ sidebarStatus: $storage.layout?.sidebarStatus,
+ epThemeColor: $storage.layout?.epThemeColor,
+ themeColor: isClick ? theme : storageThemeColor,
+ overallStyle: overallStyle.value
+ };
+
+ if (theme === "default" || theme === "light") {
+ setEpThemeColor(getConfig().EpThemeColor);
+ } else {
+ const colors = themeColors.value.find(v => v.themeColor === theme);
+ setEpThemeColor(colors.color);
+ }
+ }
+
+ function setPropertyPrimary(mode: string, i: number, color: string) {
+ document.documentElement.style.setProperty(
+ `--el-color-primary-${mode}-${i}`,
+ dataTheme.value ? darken(color, i / 10) : lighten(color, i / 10)
+ );
+ }
+
+ /** 设置 `element-plus` 主题色 */
+ const setEpThemeColor = (color: string) => {
+ useEpThemeStoreHook().setEpThemeColor(color);
+ document.documentElement.style.setProperty("--el-color-primary", color);
+ for (let i = 1; i <= 2; i++) {
+ setPropertyPrimary("dark", i, color);
+ }
+ for (let i = 1; i <= 9; i++) {
+ setPropertyPrimary("light", i, color);
+ }
+ };
+
+ /** 浅色、深色整体风格切换 */
+ function dataThemeChange(overall?: string) {
+ overallStyle.value = overall;
+ if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
+ setLayoutThemeColor("default", false);
+ } else {
+ setLayoutThemeColor(useEpThemeStoreHook().epTheme, false);
+ }
+
+ if (dataTheme.value) {
+ document.documentElement.classList.add("dark");
+ } else {
+ if ($storage.layout.themeColor === "light") {
+ setLayoutThemeColor("light", false);
+ }
+ document.documentElement.classList.remove("dark");
+ }
+ }
+
+ /** 清空缓存并返回登录页 */
+ function onReset() {
+ removeToken();
+ storageLocal().clear();
+ const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
+ useAppStoreHook().setLayout(Layout);
+ setEpThemeColor(EpThemeColor);
+ useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
+ toggleClass(Grey, "html-grey", document.querySelector("html"));
+ toggleClass(Weak, "html-weakness", document.querySelector("html"));
+ router.push("/login");
+ useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
+ resetRouter();
+ }
+
+ return {
+ body,
+ dataTheme,
+ overallStyle,
+ layoutTheme,
+ themeColors,
+ onReset,
+ toggleClass,
+ dataThemeChange,
+ setEpThemeColor,
+ setLayoutThemeColor
+ };
+}
diff --git a/sop-admin/sop-admin-frontend/src/layout/hooks/useLayout.ts b/sop-admin/sop-admin-frontend/src/layout/hooks/useLayout.ts
new file mode 100644
index 00000000..5fb0235d
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/hooks/useLayout.ts
@@ -0,0 +1,64 @@
+import { computed } from "vue";
+import { useI18n } from "vue-i18n";
+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.locale) {
+ $storage.locale = { locale: $config?.Locale ?? "zh" };
+ useI18n().locale.value = $config?.Locale ?? "zh";
+ }
+ /** 导航 */
+ if (!$storage.layout) {
+ $storage.layout = {
+ layout: $config?.Layout ?? "vertical",
+ theme: $config?.Theme ?? "light",
+ darkMode: $config?.DarkMode ?? false,
+ sidebarStatus: $config?.SidebarStatus ?? true,
+ epThemeColor: $config?.EpThemeColor ?? "#409EFF",
+ themeColor: $config?.Theme ?? "light",
+ overallStyle: $config?.OverallStyle ?? "light"
+ };
+ }
+ /** 灰色模式、色弱模式、隐藏标签页 */
+ if (!$storage.configure) {
+ $storage.configure = {
+ grey: $config?.Grey ?? false,
+ weak: $config?.Weak ?? false,
+ hideTabs: $config?.HideTabs ?? false,
+ hideFooter: $config.HideFooter ?? true,
+ showLogo: $config?.ShowLogo ?? true,
+ showModel: $config?.ShowModel ?? "smart",
+ multiTagsCache: $config?.MultiTagsCache ?? false,
+ stretch: $config?.Stretch ?? false
+ };
+ }
+ };
+
+ /** 清空缓存后从platform-config.json读取默认配置并赋值到storage中 */
+ const layout = computed(() => {
+ return $storage?.layout.layout;
+ });
+
+ const layoutTheme = computed(() => {
+ return $storage.layout;
+ });
+
+ return {
+ layout,
+ layoutTheme,
+ initStorage
+ };
+}
diff --git a/sop-admin/sop-admin-frontend/src/layout/hooks/useMultiFrame.ts b/sop-admin/sop-admin-frontend/src/layout/hooks/useMultiFrame.ts
new file mode 100644
index 00000000..73a779d2
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/hooks/useMultiFrame.ts
@@ -0,0 +1,25 @@
+const MAP = new Map();
+
+export const useMultiFrame = () => {
+ function setMap(path, Comp) {
+ MAP.set(path, Comp);
+ }
+
+ function getMap(path?) {
+ if (path) {
+ return MAP.get(path);
+ }
+ return [...MAP.entries()];
+ }
+
+ function delMap(path) {
+ MAP.delete(path);
+ }
+
+ return {
+ setMap,
+ getMap,
+ delMap,
+ MAP
+ };
+};
diff --git a/sop-admin/sop-admin-frontend/src/layout/hooks/useNav.ts b/sop-admin/sop-admin-frontend/src/layout/hooks/useNav.ts
new file mode 100644
index 00000000..3d1341aa
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/hooks/useNav.ts
@@ -0,0 +1,175 @@
+import { storeToRefs } from "pinia";
+import { getConfig } from "@/config";
+import { useRouter } from "vue-router";
+import { emitter } from "@/utils/mitt";
+import Avatar from "@/assets/user.jpg";
+import { getTopMenu } from "@/router/utils";
+import { useFullscreen } from "@vueuse/core";
+import type { routeMetaType } from "../types";
+import { transformI18n } from "@/plugins/i18n";
+import { router, remainingPaths } from "@/router";
+import { computed, type CSSProperties } from "vue";
+import { useAppStoreHook } from "@/store/modules/app";
+import { useUserStoreHook } from "@/store/modules/user";
+import { useGlobal, isAllEmpty } from "@pureadmin/utils";
+import { useEpThemeStoreHook } from "@/store/modules/epTheme";
+import { usePermissionStoreHook } from "@/store/modules/permission";
+import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
+import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
+
+const errorInfo =
+ "The current routing configuration is incorrect, please check the configuration";
+
+export function useNav() {
+ const pureApp = useAppStoreHook();
+ const routers = useRouter().options.routes;
+ const { isFullscreen, toggle } = useFullscreen();
+ const { wholeMenus } = storeToRefs(usePermissionStoreHook());
+ /** 平台`layout`中所有`el-tooltip`的`effect`配置,默认`light` */
+ const tooltipEffect = getConfig()?.TooltipEffect ?? "light";
+
+ const getDivStyle = computed((): CSSProperties => {
+ return {
+ width: "100%",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ overflow: "hidden"
+ };
+ });
+
+ /** 头像(如果头像为空则使用 src/assets/user.jpg ) */
+ const userAvatar = computed(() => {
+ return isAllEmpty(useUserStoreHook()?.avatar)
+ ? Avatar
+ : useUserStoreHook()?.avatar;
+ });
+
+ /** 昵称(如果昵称为空则显示用户名) */
+ const username = computed(() => {
+ return isAllEmpty(useUserStoreHook()?.nickname)
+ ? useUserStoreHook()?.username
+ : useUserStoreHook()?.nickname;
+ });
+
+ /** 设置国际化选中后的样式 */
+ const getDropdownItemStyle = computed(() => {
+ return (locale, t) => {
+ return {
+ background: locale === t ? useEpThemeStoreHook().epThemeColor : "",
+ color: locale === t ? "#f4f4f5" : "#000"
+ };
+ };
+ });
+
+ const getDropdownItemClass = computed(() => {
+ return (locale, t) => {
+ return locale === t ? "" : "dark:hover:!text-primary";
+ };
+ });
+
+ 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 = `${transformI18n(meta.title)} | ${Title}`;
+ else document.title = transformI18n(meta.title);
+ }
+
+ /** 退出登录 */
+ function logout() {
+ useUserStoreHook().logOut();
+ }
+
+ function backTopMenu() {
+ router.push(getTopMenu()?.path);
+ }
+
+ 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) {
+ if (wholeMenus.value.length === 0 || isRemaining(indexPath)) return;
+ emitter.emit("changLayoutRoute", indexPath);
+ }
+
+ /** 判断路径是否参与菜单 */
+ function isRemaining(path: string) {
+ return remainingPaths.includes(path);
+ }
+
+ /** 获取`logo` */
+ function getLogo() {
+ return new URL("/logo.svg", import.meta.url).href;
+ }
+
+ return {
+ title,
+ device,
+ layout,
+ logout,
+ routers,
+ $storage,
+ isFullscreen,
+ Fullscreen,
+ ExitFullscreen,
+ toggle,
+ backTopMenu,
+ onPanel,
+ getDivStyle,
+ changeTitle,
+ toggleSideBar,
+ menuSelect,
+ handleResize,
+ resolvePath,
+ getLogo,
+ isCollapse,
+ pureApp,
+ username,
+ userAvatar,
+ avatarsStyle,
+ tooltipEffect,
+ getDropdownItemStyle,
+ getDropdownItemClass
+ };
+}
diff --git a/sop-admin/sop-admin-frontend/src/layout/hooks/useTag.ts b/sop-admin/sop-admin-frontend/src/layout/hooks/useTag.ts
new file mode 100644
index 00000000..aff70f1c
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/hooks/useTag.ts
@@ -0,0 +1,248 @@
+import {
+ ref,
+ unref,
+ computed,
+ reactive,
+ onMounted,
+ type CSSProperties,
+ getCurrentInstance
+} from "vue";
+import type { tagsViewsType } from "../types";
+import { useRoute, useRouter } from "vue-router";
+import { transformI18n, $t } from "@/plugins/i18n";
+import { responsiveStorageNameSpace } from "@/config";
+import { useSettingStoreHook } from "@/store/modules/settings";
+import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
+import {
+ isEqual,
+ isBoolean,
+ storageLocal,
+ toggleClass,
+ hasClass
+} from "@pureadmin/utils";
+
+import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
+import CloseAllTags from "@iconify-icons/ri/subtract-line";
+import CloseOtherTags from "@iconify-icons/ri/text-spacing";
+import CloseRightTags from "@iconify-icons/ri/text-direction-l";
+import CloseLeftTags from "@iconify-icons/ri/text-direction-r";
+import RefreshRight from "@iconify-icons/ep/refresh-right";
+import Close from "@iconify-icons/ep/close";
+
+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 isScrolling = ref(false);
+
+ /** 显示模式,默认灵动模式 */
+ const showModel = ref(
+ storageLocal().getItem(
+ `${responsiveStorageNameSpace()}configure`
+ )?.showModel || "smart"
+ );
+ /** 是否隐藏标签页,默认显示 */
+ const showTags =
+ ref(
+ storageLocal().getItem(
+ `${responsiveStorageNameSpace()}configure`
+ ).hideTabs
+ ) ?? ref("false");
+ const multiTags: any = computed(() => {
+ return useMultiTagsStoreHook().multiTags;
+ });
+
+ const tagsViews = reactive>([
+ {
+ icon: RefreshRight,
+ text: $t("buttons.pureReload"),
+ divided: false,
+ disabled: false,
+ show: true
+ },
+ {
+ icon: Close,
+ text: $t("buttons.pureCloseCurrentTab"),
+ divided: false,
+ disabled: multiTags.value.length > 1 ? false : true,
+ show: true
+ },
+ {
+ icon: CloseLeftTags,
+ text: $t("buttons.pureCloseLeftTabs"),
+ divided: true,
+ disabled: multiTags.value.length > 1 ? false : true,
+ show: true
+ },
+ {
+ icon: CloseRightTags,
+ text: $t("buttons.pureCloseRightTabs"),
+ divided: false,
+ disabled: multiTags.value.length > 1 ? false : true,
+ show: true
+ },
+ {
+ icon: CloseOtherTags,
+ text: $t("buttons.pureCloseOtherTabs"),
+ divided: true,
+ disabled: multiTags.value.length > 2 ? false : true,
+ show: true
+ },
+ {
+ icon: CloseAllTags,
+ text: $t("buttons.pureCloseAllTabs"),
+ divided: false,
+ disabled: multiTags.value.length > 1 ? false : true,
+ show: true
+ },
+ {
+ icon: Fullscreen,
+ text: $t("buttons.pureContentFullScreen"),
+ divided: true,
+ disabled: false,
+ show: true
+ }
+ ]);
+
+ function conditionHandle(item, previous, next) {
+ if (isBoolean(route?.meta?.showLink) && route?.meta?.showLink === false) {
+ if (Object.keys(route.query).length > 0) {
+ return isEqual(route.query, item.query) ? previous : next;
+ } else {
+ return isEqual(route.params, item.params) ? previous : next;
+ }
+ } else {
+ return route.path === item.path ? previous : next;
+ }
+ }
+
+ const isFixedTag = computed(() => {
+ return item => {
+ return isBoolean(item?.meta?.fixedTag) && item?.meta?.fixedTag === true;
+ };
+ });
+
+ 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)`,
+ transition: isScrolling.value ? "none" : "transform 0.5s ease-in-out"
+ };
+ });
+
+ 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], "is-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], "is-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(
+ `${responsiveStorageNameSpace()}configure`
+ );
+ configure.showModel = "card";
+ storageLocal().setItem(
+ `${responsiveStorageNameSpace()}configure`,
+ configure
+ );
+ }
+ });
+
+ return {
+ Close,
+ route,
+ router,
+ visible,
+ showTags,
+ instance,
+ multiTags,
+ showModel,
+ tagsViews,
+ buttonTop,
+ buttonLeft,
+ translateX,
+ isFixedTag,
+ pureSetting,
+ activeIndex,
+ getTabStyle,
+ isScrolling,
+ iconIsActive,
+ linkIsActive,
+ currentSelect,
+ scheduleIsActive,
+ getContextMenuStyle,
+ $t,
+ closeMenu,
+ onMounted,
+ onMouseenter,
+ onMouseleave,
+ transformI18n,
+ onContentFullScreen
+ };
+}
diff --git a/sop-admin/sop-admin-frontend/src/layout/hooks/useTranslationLang.ts b/sop-admin/sop-admin-frontend/src/layout/hooks/useTranslationLang.ts
new file mode 100644
index 00000000..ebad1615
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/hooks/useTranslationLang.ts
@@ -0,0 +1,41 @@
+import { useNav } from "./useNav";
+import { useI18n } from "vue-i18n";
+import { useRoute } from "vue-router";
+import { watch, onBeforeMount, type Ref } from "vue";
+
+export function useTranslationLang(ref?: Ref) {
+ const { $storage, changeTitle, handleResize } = useNav();
+ const { locale, t } = useI18n();
+ const route = useRoute();
+
+ function translationCh() {
+ $storage.locale = { locale: "zh" };
+ locale.value = "zh";
+ ref && handleResize(ref.value);
+ }
+
+ function translationEn() {
+ $storage.locale = { locale: "en" };
+ locale.value = "en";
+ ref && handleResize(ref.value);
+ }
+
+ watch(
+ () => locale.value,
+ () => {
+ changeTitle(route.meta);
+ }
+ );
+
+ onBeforeMount(() => {
+ locale.value = $storage.locale?.locale ?? "zh";
+ });
+
+ return {
+ t,
+ route,
+ locale,
+ translationCh,
+ translationEn
+ };
+}
diff --git a/sop-admin/sop-admin-frontend/src/layout/index.vue b/sop-admin/sop-admin-frontend/src/layout/index.vue
new file mode 100644
index 00000000..5570ecd3
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/index.vue
@@ -0,0 +1,237 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/page-layout.vue b/sop-admin/sop-admin-frontend/src/layout/page-layout.vue
deleted file mode 100644
index 5f60b365..00000000
--- a/sop-admin/sop-admin-frontend/src/layout/page-layout.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sop-admin/sop-admin-frontend/src/layout/redirect.vue b/sop-admin/sop-admin-frontend/src/layout/redirect.vue
new file mode 100644
index 00000000..6e163393
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/redirect.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/sop-admin/sop-admin-frontend/src/layout/theme/index.ts b/sop-admin/sop-admin-frontend/src/layout/theme/index.ts
new file mode 100644
index 00000000..f7b4d470
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/theme/index.ts
@@ -0,0 +1,129 @@
+/**
+ * @description ⚠️:此文件仅供主题插件使用,请不要在此文件中导出别的工具函数(仅在页面加载前运行)
+ */
+
+import type { multipleScopeVarsOptions } from "@pureadmin/theme";
+
+/** 预设主题色 */
+const themeColors = {
+ /* 亮白色 */
+ light: {
+ subMenuActiveText: "#000000d9",
+ menuBg: "#fff",
+ menuHover: "#f6f6f6",
+ subMenuBg: "#fff",
+ subMenuActiveBg: "#e0ebf6",
+ menuText: "rgb(0 0 0 / 60%)",
+ sidebarLogo: "#fff",
+ menuTitleHover: "#000",
+ menuActiveBefore: "#4091f7"
+ },
+ /* 道奇蓝 */
+ default: {
+ subMenuActiveText: "#fff",
+ menuBg: "#001529",
+ menuHover: "rgb(64 145 247 / 15%)",
+ subMenuBg: "#0f0303",
+ subMenuActiveBg: "#4091f7",
+ menuText: "rgb(254 254 254 / 65%)",
+ sidebarLogo: "#002140",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#4091f7"
+ },
+ /* 深紫罗兰色 */
+ saucePurple: {
+ subMenuActiveText: "#fff",
+ menuBg: "#130824",
+ menuHover: "rgb(105 58 201 / 15%)",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#693ac9",
+ menuText: "#7a80b4",
+ sidebarLogo: "#1f0c38",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#693ac9"
+ },
+ /* 深粉色 */
+ pink: {
+ subMenuActiveText: "#fff",
+ menuBg: "#28081a",
+ menuHover: "rgb(216 68 147 / 15%)",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#d84493",
+ menuText: "#7a80b4",
+ sidebarLogo: "#3f0d29",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#d84493"
+ },
+ /* 猩红色 */
+ dusk: {
+ subMenuActiveText: "#fff",
+ menuBg: "#2a0608",
+ menuHover: "rgb(225 60 57 / 15%)",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#e13c39",
+ menuText: "rgb(254 254 254 / 65.1%)",
+ sidebarLogo: "#42090c",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#e13c39"
+ },
+ /* 橙红色 */
+ volcano: {
+ subMenuActiveText: "#fff",
+ menuBg: "#2b0e05",
+ menuHover: "rgb(232 95 51 / 15%)",
+ subMenuBg: "#0f0603",
+ subMenuActiveBg: "#e85f33",
+ menuText: "rgb(254 254 254 / 65%)",
+ sidebarLogo: "#441708",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#e85f33"
+ },
+ /* 绿宝石 */
+ mingQing: {
+ subMenuActiveText: "#fff",
+ menuBg: "#032121",
+ menuHover: "rgb(89 191 193 / 15%)",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#59bfc1",
+ menuText: "#7a80b4",
+ sidebarLogo: "#053434",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#59bfc1"
+ },
+ /* 酸橙绿 */
+ auroraGreen: {
+ subMenuActiveText: "#fff",
+ menuBg: "#0b1e15",
+ menuHover: "rgb(96 172 128 / 15%)",
+ subMenuBg: "#000",
+ subMenuActiveBg: "#60ac80",
+ menuText: "#7a80b4",
+ sidebarLogo: "#112f21",
+ menuTitleHover: "#fff",
+ menuActiveBefore: "#60ac80"
+ }
+};
+
+/**
+ * @description 将预设主题色处理成主题插件所需格式
+ */
+export const genScssMultipleScopeVars = (): multipleScopeVarsOptions[] => {
+ const result = [] as multipleScopeVarsOptions[];
+ Object.keys(themeColors).forEach(key => {
+ result.push({
+ scopeName: `layout-theme-${key}`,
+ varsContent: `
+ $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;
+ $menuText: ${themeColors[key].menuText} !default;
+ $sidebarLogo: ${themeColors[key].sidebarLogo} !default;
+ $menuTitleHover: ${themeColors[key].menuTitleHover} !default;
+ $menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;
+ `
+ } as multipleScopeVarsOptions);
+ });
+ return result;
+};
diff --git a/sop-admin/sop-admin-frontend/src/layout/types.ts b/sop-admin/sop-admin-frontend/src/layout/types.ts
new file mode 100644
index 00000000..1d25a0d9
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/layout/types.ts
@@ -0,0 +1,92 @@
+import type { IconifyIcon } from "@iconify/vue";
+const { VITE_HIDE_HOME } = import.meta.env;
+
+export const routerArrays: Array =
+ VITE_HIDE_HOME === "false"
+ ? [
+ {
+ path: "/welcome",
+ meta: {
+ title: "menus.pureHome",
+ icon: "ep:home-filled"
+ }
+ }
+ ]
+ : [];
+
+export type routeMetaType = {
+ title?: string;
+ icon?: string | IconifyIcon;
+ showLink?: boolean;
+ savedPosition?: boolean;
+ auths?: Array;
+};
+
+export type RouteConfigs = {
+ path?: string;
+ query?: object;
+ params?: object;
+ meta?: routeMetaType;
+ children?: RouteConfigs[];
+ name?: string;
+};
+
+export type multiTagsType = {
+ tags: Array;
+};
+
+export type tagsViewsType = {
+ icon: string | IconifyIcon;
+ 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 menuType = {
+ id?: number;
+ name?: string;
+ path?: string;
+ noShowingChildren?: boolean;
+ children?: menuType[];
+ value: unknown;
+ meta?: {
+ icon?: string;
+ title?: string;
+ rank?: number;
+ showParent?: boolean;
+ extraIcon?: string;
+ };
+ showTooltip?: boolean;
+ parentId?: number;
+ pathList?: number[];
+ redirect?: string;
+};
+
+export type themeColorsType = {
+ color: string;
+ themeColor: string;
+};
+
+export interface scrollbarDomType extends HTMLElement {
+ wrap?: {
+ offsetWidth: number;
+ };
+}
diff --git a/sop-admin/sop-admin-frontend/src/locale/en-US.ts b/sop-admin/sop-admin-frontend/src/locale/en-US.ts
deleted file mode 100644
index 89e9705f..00000000
--- a/sop-admin/sop-admin-frontend/src/locale/en-US.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import localeMessageBox from '@/components/message-box/locale/en-US';
-import localeLogin from '@/views/login/locale/en-US';
-
-import localeWorkplace from '@/views/dashboard/workplace/locale/en-US';
-
-import localeSettings from './en-US/settings';
-
-export default {
- 'menu.dashboard': 'Dashboard',
- 'menu.server.dashboard': 'Dashboard-Server',
- 'menu.server.workplace': 'Workplace-Server',
- 'menu.server.monitor': 'Monitor-Server',
- 'menu.list': 'List',
- 'menu.result': 'Result',
- 'menu.exception': 'Exception',
- 'menu.form': 'Form',
- 'menu.profile': 'Profile',
- 'menu.visualization': 'Data Visualization',
- 'menu.user': 'User Center',
- 'menu.arcoWebsite': 'Arco Design',
- 'menu.faq': 'FAQ',
- 'navbar.docs': 'Docs',
- 'navbar.action.locale': 'Switch to English',
- ...localeSettings,
- ...localeMessageBox,
- ...localeLogin,
- ...localeWorkplace,
-};
diff --git a/sop-admin/sop-admin-frontend/src/locale/en-US/settings.ts b/sop-admin/sop-admin-frontend/src/locale/en-US/settings.ts
deleted file mode 100644
index f929e50f..00000000
--- a/sop-admin/sop-admin-frontend/src/locale/en-US/settings.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-export default {
- 'settings.title': 'Settings',
- 'settings.themeColor': 'Theme Color',
- 'settings.content': 'Content Setting',
- 'settings.search': 'Search',
- 'settings.language': 'Language',
- 'settings.navbar': 'Navbar',
- 'settings.menuWidth': 'Menu Width (px)',
- 'settings.navbar.theme.toLight': 'Click to use light mode',
- 'settings.navbar.theme.toDark': 'Click to use dark mode',
- 'settings.navbar.screen.toFull': 'Click to switch to full screen mode',
- 'settings.navbar.screen.toExit': 'Click to exit the full screen mode',
- 'settings.navbar.alerts': 'alerts',
- 'settings.menu': 'Menu',
- 'settings.topMenu': 'Top Menu',
- 'settings.tabBar': 'Tab Bar',
- 'settings.footer': 'Footer',
- 'settings.otherSettings': 'Other Settings',
- 'settings.colorWeak': 'Color Weak',
- 'settings.alertContent':
- 'After the configuration is only temporarily effective, if you want to really affect the project, click the "Copy Settings" button below and replace the configuration in settings.json.',
- 'settings.copySettings': 'Copy Settings',
- 'settings.copySettings.message':
- 'Copy succeeded, please paste to file src/settings.json.',
- 'settings.close': 'Close',
- 'settings.color.tooltip':
- '10 gradient colors generated according to the theme color',
- 'settings.menuFromServer': 'Menu From Server',
-};
diff --git a/sop-admin/sop-admin-frontend/src/locale/index.ts b/sop-admin/sop-admin-frontend/src/locale/index.ts
deleted file mode 100644
index 68349ce0..00000000
--- a/sop-admin/sop-admin-frontend/src/locale/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { createI18n } from 'vue-i18n';
-import en from './en-US';
-import cn from './zh-CN';
-
-export const LOCALE_OPTIONS = [
- { label: '中文', value: 'zh-CN' },
- { label: 'English', value: 'en-US' },
-];
-const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN';
-
-const i18n = createI18n({
- locale: defaultLocale,
- fallbackLocale: 'en-US',
- legacy: false,
- allowComposition: true,
- messages: {
- 'en-US': en,
- 'zh-CN': cn,
- },
-});
-
-export default i18n;
diff --git a/sop-admin/sop-admin-frontend/src/locale/zh-CN.ts b/sop-admin/sop-admin-frontend/src/locale/zh-CN.ts
deleted file mode 100644
index 65d564da..00000000
--- a/sop-admin/sop-admin-frontend/src/locale/zh-CN.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import localeMessageBox from '@/components/message-box/locale/zh-CN';
-import localeLogin from '@/views/login/locale/zh-CN';
-
-import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN';
-
-import localeSettings from './zh-CN/settings';
-
-export default {
- 'menu.dashboard': '仪表盘',
- 'menu.server.dashboard': '仪表盘-服务端',
- 'menu.server.workplace': '工作台-服务端',
- 'menu.server.monitor': '实时监控-服务端',
- 'menu.list': '列表页',
- 'menu.result': '结果页',
- 'menu.exception': '异常页',
- 'menu.form': '表单页',
- 'menu.profile': '详情页',
- 'menu.visualization': '数据可视化',
- 'menu.user': '个人中心',
- 'menu.arcoWebsite': 'Arco Design',
- 'menu.faq': '常见问题',
- 'navbar.docs': '文档中心',
- 'navbar.action.locale': '切换为中文',
- ...localeSettings,
- ...localeMessageBox,
- ...localeLogin,
- ...localeWorkplace,
-};
diff --git a/sop-admin/sop-admin-frontend/src/locale/zh-CN/settings.ts b/sop-admin/sop-admin-frontend/src/locale/zh-CN/settings.ts
deleted file mode 100644
index e42021db..00000000
--- a/sop-admin/sop-admin-frontend/src/locale/zh-CN/settings.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-export default {
- 'settings.title': '页面配置',
- 'settings.themeColor': '主题色',
- 'settings.content': '内容区域',
- 'settings.search': '搜索',
- 'settings.language': '语言',
- 'settings.navbar': '导航栏',
- 'settings.menuWidth': '菜单宽度 (px)',
- 'settings.navbar.theme.toLight': '点击切换为亮色模式',
- 'settings.navbar.theme.toDark': '点击切换为暗黑模式',
- 'settings.navbar.screen.toFull': '点击切换全屏模式',
- 'settings.navbar.screen.toExit': '点击退出全屏模式',
- 'settings.navbar.alerts': '消息通知',
- 'settings.menu': '菜单栏',
- 'settings.topMenu': '顶部菜单栏',
- 'settings.tabBar': '多页签',
- 'settings.footer': '底部',
- 'settings.otherSettings': '其他设置',
- 'settings.colorWeak': '色弱模式',
- 'settings.alertContent':
- '配置之后仅是临时生效,要想真正作用于项目,点击下方的 "复制配置" 按钮,将配置替换到 settings.json 中即可。',
- 'settings.copySettings': '复制配置',
- 'settings.copySettings.message':
- '复制成功,请粘贴到 src/settings.json 文件中',
- 'settings.close': '关闭',
- 'settings.color.tooltip':
- '根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)',
- 'settings.menuFromServer': '菜单来源于后台',
-};
diff --git a/sop-admin/sop-admin-frontend/src/main.ts b/sop-admin/sop-admin-frontend/src/main.ts
index 9b34d2e1..e0182261 100644
--- a/sop-admin/sop-admin-frontend/src/main.ts
+++ b/sop-admin/sop-admin-frontend/src/main.ts
@@ -1,28 +1,77 @@
-import { createApp } from 'vue';
-import ArcoVue from '@arco-design/web-vue';
-import ArcoVueIcon from '@arco-design/web-vue/es/icon';
-import globalComponents from '@/components';
-import router from './router';
-import store from './store';
-import i18n from './locale';
-import directive from './directive';
-import './mock';
-import App from './App.vue';
-// Styles are imported via arco-plugin. See config/plugin/arcoStyleImport.ts in the directory for details
-// 样式通过 arco-plugin 插件导入。详见目录文件 config/plugin/arcoStyleImport.ts
-// https://arco.design/docs/designlab/use-theme-package
-import '@/assets/style/global.less';
-import '@/api/interceptor';
+import App from "./App.vue";
+import router from "./router";
+import { setupStore } from "@/store";
+import { useI18n } from "@/plugins/i18n";
+import { getPlatformConfig } from "./config";
+import { MotionPlugin } from "@vueuse/motion";
+// import { useEcharts } from "@/plugins/echarts";
+import { createApp, type Directive } from "vue";
+// import { useElementPlus } from "@/plugins/elementPlus";
+import { injectResponsiveStorage } from "@/utils/responsive";
+
+import Table from "@pureadmin/table";
+// import PureDescriptions from "@pureadmin/descriptions";
+
+// 引入重置样式
+import "./style/reset.scss";
+// 导入公共样式
+import "./style/index.scss";
+// 一定要在main.ts中导入tailwind.css,防止vite每次hmr都会请求src/style/index.scss整体css文件导致热更新慢的问题
+import "./style/tailwind.css";
+import "element-plus/dist/index.css";
+// 导入字体图标
+import "./assets/iconfont/iconfont.js";
+import "./assets/iconfont/iconfont.css";
const app = createApp(App);
-app.use(ArcoVue, {});
-app.use(ArcoVueIcon);
+// 自定义指令
+import * as directives from "@/directives";
+Object.keys(directives).forEach(key => {
+ app.directive(key, (directives as { [key: string]: Directive })[key]);
+});
-app.use(router);
-app.use(store);
-app.use(i18n);
-app.use(globalComponents);
-app.use(directive);
+// 全局注册@iconify/vue图标库
+import {
+ IconifyIconOffline,
+ IconifyIconOnline,
+ FontIcon
+} from "./components/ReIcon";
+app.component("IconifyIconOffline", IconifyIconOffline);
+app.component("IconifyIconOnline", IconifyIconOnline);
+app.component("FontIcon", FontIcon);
-app.mount('#app');
+// 全局注册按钮级别权限组件
+import { Auth } from "@/components/ReAuth";
+import { Perms } from "@/components/RePerms";
+app.component("Auth", Auth);
+app.component("Perms", Perms);
+
+// 全局注册vue-tippy
+import "tippy.js/dist/tippy.css";
+import "tippy.js/themes/light.css";
+import VueTippy from "vue-tippy";
+app.use(VueTippy);
+
+// 导入plus-pro-components 及其样式
+import PlusProComponents from "plus-pro-components";
+import "plus-pro-components/index.css";
+
+// 全局引入ElementPlus
+import ElementPlus from "element-plus";
+
+getPlatformConfig(app).then(async config => {
+ setupStore(app);
+ app.use(router);
+ await router.isReady();
+ injectResponsiveStorage(app, config);
+ app
+ .use(MotionPlugin)
+ .use(useI18n)
+ .use(ElementPlus)
+ .use(PlusProComponents)
+ .use(Table);
+ // .use(PureDescriptions)
+ // .use(useEcharts);
+ app.mount("#app");
+});
diff --git a/sop-admin/sop-admin-frontend/src/mock/index.ts b/sop-admin/sop-admin-frontend/src/mock/index.ts
deleted file mode 100644
index cf1af17e..00000000
--- a/sop-admin/sop-admin-frontend/src/mock/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import Mock from 'mockjs';
-
-import './user';
-import './message-box';
-
-import '@/views/dashboard/workplace/mock';
-
-Mock.setup({
- timeout: '600-1000',
-});
diff --git a/sop-admin/sop-admin-frontend/src/mock/message-box.ts b/sop-admin/sop-admin-frontend/src/mock/message-box.ts
deleted file mode 100644
index 6449e68f..00000000
--- a/sop-admin/sop-admin-frontend/src/mock/message-box.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import Mock from 'mockjs';
-import setupMock, { successResponseWrap } from '@/utils/setup-mock';
-
-const haveReadIds: number[] = [];
-const getMessageList = () => {
- return [
- {
- id: 1,
- type: 'message',
- title: '郑曦月',
- subTitle: '的私信',
- avatar:
- '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp',
- content: '审批请求已发送,请查收',
- time: '今天 12:30:01',
- },
- {
- id: 2,
- type: 'message',
- title: '宁波',
- subTitle: '的回复',
- avatar:
- '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
- content: '此处 bug 已经修复',
- time: '今天 12:30:01',
- },
- {
- id: 3,
- type: 'message',
- title: '宁波',
- subTitle: '的回复',
- avatar:
- '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
- content: '此处 bug 已经修复',
- time: '今天 12:20:01',
- },
- {
- id: 4,
- type: 'notice',
- title: '续费通知',
- subTitle: '',
- avatar: '',
- content: '您的产品使用期限即将截止,如需继续使用产品请前往购…',
- time: '今天 12:20:01',
- messageType: 3,
- },
- {
- id: 5,
- type: 'notice',
- title: '规则开通成功',
- subTitle: '',
- avatar: '',
- content: '内容屏蔽规则于 2021-12-01 开通成功并生效',
- time: '今天 12:20:01',
- messageType: 1,
- },
- {
- id: 6,
- type: 'todo',
- title: '质检队列变更',
- subTitle: '',
- avatar: '',
- content: '内容质检队列于 2021-12-01 19:50:23 进行变更,请重新…',
- time: '今天 12:20:01',
- messageType: 0,
- },
- ].map((item) => ({
- ...item,
- status: haveReadIds.indexOf(item.id) === -1 ? 0 : 1,
- }));
-};
-
-setupMock({
- setup: () => {
- Mock.mock(new RegExp('/api/message/list'), () => {
- return successResponseWrap(getMessageList());
- });
-
- Mock.mock(new RegExp('/api/message/read'), (params: { body: string }) => {
- const { ids } = JSON.parse(params.body);
- haveReadIds.push(...(ids || []));
- return successResponseWrap(true);
- });
- },
-});
diff --git a/sop-admin/sop-admin-frontend/src/mock/user.ts b/sop-admin/sop-admin-frontend/src/mock/user.ts
deleted file mode 100644
index 02920d13..00000000
--- a/sop-admin/sop-admin-frontend/src/mock/user.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import Mock from 'mockjs';
-import setupMock, {
- successResponseWrap,
- failResponseWrap,
-} from '@/utils/setup-mock';
-
-import { MockParams } from '@/types/mock';
-import { isLogin } from '@/utils/auth';
-
-setupMock({
- setup() {
- // Mock.XHR.prototype.withCredentials = true;
-
- // 用户信息
- Mock.mock(new RegExp('/api/user/info'), () => {
- if (isLogin()) {
- const role = window.localStorage.getItem('userRole') || 'admin';
- return successResponseWrap({
- name: 'admin',
- avatar:
- '//lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
- email: 'wangliqun@email.com',
- job: 'frontend',
- jobName: '前端艺术家',
- organization: 'Frontend',
- organizationName: '前端',
- location: 'beijing',
- locationName: '北京',
- introduction: '人潇洒,性温存',
- personalWebsite: 'https://www.arco.design',
- phone: '150****0000',
- registrationDate: '2013-05-10 12:10:00',
- accountId: '15012312300',
- certification: 1,
- role,
- });
- }
- return failResponseWrap(null, '未登录', 50008);
- });
-
- // 登录
- Mock.mock(new RegExp('/api/user/login'), (params: MockParams) => {
- const { username, password } = JSON.parse(params.body);
- if (!username) {
- return failResponseWrap(null, '用户名不能为空', 50000);
- }
- if (!password) {
- return failResponseWrap(null, '密码不能为空', 50000);
- }
- if (username === 'admin' && password === 'admin') {
- window.localStorage.setItem('userRole', 'admin');
- return successResponseWrap({
- token: '12345',
- });
- }
- if (username === 'user' && password === 'user') {
- window.localStorage.setItem('userRole', 'user');
- return successResponseWrap({
- token: '54321',
- });
- }
- return failResponseWrap(null, '账号或者密码错误', 50000);
- });
-
- // 登出
- Mock.mock(new RegExp('/api/user/logout'), () => {
- return successResponseWrap(null);
- });
-
- // 用户的服务端菜单
- Mock.mock(new RegExp('/api/user/menu'), () => {
- const menuList = [
- {
- path: '/dashboard',
- name: 'dashboard',
- meta: {
- locale: 'menu.server.dashboard',
- requiresAuth: true,
- icon: 'icon-dashboard',
- order: 1,
- },
- children: [
- {
- path: 'workplace',
- name: 'Workplace',
- meta: {
- locale: 'menu.server.workplace',
- requiresAuth: true,
- },
- },
- {
- path: 'https://arco.design',
- name: 'arcoWebsite',
- meta: {
- locale: 'menu.arcoWebsite',
- requiresAuth: true,
- },
- },
- ],
- },
- ];
- return successResponseWrap(menuList);
- });
- },
-});
diff --git a/sop-admin/sop-admin-frontend/src/plugins/echarts.ts b/sop-admin/sop-admin-frontend/src/plugins/echarts.ts
new file mode 100644
index 00000000..cb62d966
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/plugins/echarts.ts
@@ -0,0 +1,44 @@
+import type { App } from "vue";
+import * as echarts from "echarts/core";
+import { PieChart, BarChart, LineChart } from "echarts/charts";
+import { CanvasRenderer, SVGRenderer } from "echarts/renderers";
+import {
+ GridComponent,
+ TitleComponent,
+ PolarComponent,
+ LegendComponent,
+ GraphicComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ DataZoomComponent,
+ VisualMapComponent
+} from "echarts/components";
+
+const { use } = echarts;
+
+use([
+ PieChart,
+ BarChart,
+ LineChart,
+ CanvasRenderer,
+ SVGRenderer,
+ GridComponent,
+ TitleComponent,
+ PolarComponent,
+ LegendComponent,
+ GraphicComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ DataZoomComponent,
+ VisualMapComponent
+]);
+
+/**
+ * @description 按需引入echarts,具体看 https://echarts.apache.org/handbook/zh/basics/import/#%E5%9C%A8-typescript-%E4%B8%AD%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5
+ * @see 温馨提示:必须将 `$echarts` 添加到全局 `globalProperties` ,具体看 https://pure-admin-utils.netlify.app/hooks/useECharts/useECharts#%E4%BD%BF%E7%94%A8%E5%89%8D%E6%8F%90
+ */
+export function useEcharts(app: App) {
+ app.config.globalProperties.$echarts = echarts;
+}
+
+export default echarts;
diff --git a/sop-admin/sop-admin-frontend/src/plugins/elementPlus.ts b/sop-admin/sop-admin-frontend/src/plugins/elementPlus.ts
new file mode 100644
index 00000000..8363187c
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/plugins/elementPlus.ts
@@ -0,0 +1,248 @@
+// 按需引入element-plus(该方法稳定且明确。当然也支持:https://element-plus.org/zh-CN/guide/quickstart.html#%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5)
+import type { App, Component } from "vue";
+import {
+ /**
+ * 为了方便演示平台将 element-plus 导出的所有组件引入,实际使用中如果你没用到哪个组件,将其注释掉就行
+ * 导出来源:https://github.com/element-plus/element-plus/blob/dev/packages/element-plus/component.ts#L111-L211
+ * */
+ ElAffix,
+ ElAlert,
+ ElAutocomplete,
+ ElAutoResizer,
+ ElAvatar,
+ ElAnchor,
+ ElAnchorLink,
+ ElBacktop,
+ ElBadge,
+ ElBreadcrumb,
+ ElBreadcrumbItem,
+ ElButton,
+ ElButtonGroup,
+ ElCalendar,
+ ElCard,
+ ElCarousel,
+ ElCarouselItem,
+ ElCascader,
+ ElCascaderPanel,
+ ElCheckTag,
+ ElCheckbox,
+ ElCheckboxButton,
+ ElCheckboxGroup,
+ ElCol,
+ ElCollapse,
+ ElCollapseItem,
+ ElCollapseTransition,
+ ElColorPicker,
+ ElConfigProvider,
+ ElContainer,
+ ElAside,
+ ElFooter,
+ ElHeader,
+ ElMain,
+ ElDatePicker,
+ ElDescriptions,
+ ElDescriptionsItem,
+ ElDialog,
+ ElDivider,
+ ElDrawer,
+ ElDropdown,
+ ElDropdownItem,
+ ElDropdownMenu,
+ ElEmpty,
+ ElForm,
+ ElFormItem,
+ ElIcon,
+ ElImage,
+ ElImageViewer,
+ ElInput,
+ ElInputNumber,
+ ElLink,
+ ElMenu,
+ ElMenuItem,
+ ElMenuItemGroup,
+ ElSubMenu,
+ ElPageHeader,
+ ElPagination,
+ ElPopconfirm,
+ ElPopover,
+ ElPopper,
+ ElProgress,
+ ElRadio,
+ ElRadioButton,
+ ElRadioGroup,
+ ElRate,
+ ElResult,
+ ElRow,
+ ElScrollbar,
+ ElSelect,
+ ElOption,
+ ElOptionGroup,
+ ElSelectV2,
+ ElSkeleton,
+ ElSkeletonItem,
+ ElSlider,
+ ElSpace,
+ ElStatistic,
+ ElCountdown,
+ ElSteps,
+ ElStep,
+ ElSwitch,
+ ElTable,
+ ElTableColumn,
+ ElTableV2,
+ ElTabs,
+ ElTabPane,
+ ElTag,
+ ElText,
+ ElTimePicker,
+ ElTimeSelect,
+ ElTimeline,
+ ElTimelineItem,
+ ElTooltip,
+ ElTransfer,
+ ElTree,
+ ElTreeSelect,
+ ElTreeV2,
+ ElUpload,
+ ElWatermark,
+ ElTour,
+ ElTourStep,
+ ElSegmented,
+ /**
+ * 为了方便演示平台将 element-plus 导出的所有插件引入,实际使用中如果你没用到哪个插件,将其注释掉就行
+ * 导出来源:https://github.com/element-plus/element-plus/blob/dev/packages/element-plus/plugin.ts#L11-L16
+ * */
+ ElLoading, // v-loading 指令
+ ElInfiniteScroll, // v-infinite-scroll 指令
+ ElPopoverDirective, // v-popover 指令
+ ElMessage, // $message 全局属性对象globalProperties
+ ElMessageBox, // $msgbox、$alert、$confirm、$prompt 全局属性对象globalProperties
+ ElNotification // $notify 全局属性对象globalProperties
+} from "element-plus";
+
+const components = [
+ ElAffix,
+ ElAlert,
+ ElAutocomplete,
+ ElAutoResizer,
+ ElAvatar,
+ ElAnchor,
+ ElAnchorLink,
+ ElBacktop,
+ ElBadge,
+ ElBreadcrumb,
+ ElBreadcrumbItem,
+ ElButton,
+ ElButtonGroup,
+ ElCalendar,
+ ElCard,
+ ElCarousel,
+ ElCarouselItem,
+ ElCascader,
+ ElCascaderPanel,
+ ElCheckTag,
+ ElCheckbox,
+ ElCheckboxButton,
+ ElCheckboxGroup,
+ ElCol,
+ ElCollapse,
+ ElCollapseItem,
+ ElCollapseTransition,
+ ElColorPicker,
+ ElConfigProvider,
+ ElContainer,
+ ElAside,
+ ElFooter,
+ ElHeader,
+ ElMain,
+ ElDatePicker,
+ ElDescriptions,
+ ElDescriptionsItem,
+ ElDialog,
+ ElDivider,
+ ElDrawer,
+ ElDropdown,
+ ElDropdownItem,
+ ElDropdownMenu,
+ ElEmpty,
+ ElForm,
+ ElFormItem,
+ ElIcon,
+ ElImage,
+ ElImageViewer,
+ ElInput,
+ ElInputNumber,
+ ElLink,
+ ElMenu,
+ ElMenuItem,
+ ElMenuItemGroup,
+ ElSubMenu,
+ ElPageHeader,
+ ElPagination,
+ ElPopconfirm,
+ ElPopover,
+ ElPopper,
+ ElProgress,
+ ElRadio,
+ ElRadioButton,
+ ElRadioGroup,
+ ElRate,
+ ElResult,
+ ElRow,
+ ElScrollbar,
+ ElSelect,
+ ElOption,
+ ElOptionGroup,
+ ElSelectV2,
+ ElSkeleton,
+ ElSkeletonItem,
+ ElSlider,
+ ElSpace,
+ ElStatistic,
+ ElCountdown,
+ ElSteps,
+ ElStep,
+ ElSwitch,
+ ElTable,
+ ElTableColumn,
+ ElTableV2,
+ ElTabs,
+ ElTabPane,
+ ElTag,
+ ElText,
+ ElTimePicker,
+ ElTimeSelect,
+ ElTimeline,
+ ElTimelineItem,
+ ElTooltip,
+ ElTransfer,
+ ElTree,
+ ElTreeSelect,
+ ElTreeV2,
+ ElUpload,
+ ElWatermark,
+ ElTour,
+ ElTourStep,
+ ElSegmented
+];
+
+const plugins = [
+ ElLoading,
+ ElInfiniteScroll,
+ ElPopoverDirective,
+ ElMessage,
+ ElMessageBox,
+ ElNotification
+];
+
+/** 按需引入`element-plus` */
+export function useElementPlus(app: App) {
+ // 全局注册组件
+ components.forEach((component: Component) => {
+ app.component(component.name, component);
+ });
+ // 全局注册插件
+ plugins.forEach(plugin => {
+ app.use(plugin);
+ });
+}
diff --git a/sop-admin/sop-admin-frontend/src/plugins/i18n.ts b/sop-admin/sop-admin-frontend/src/plugins/i18n.ts
new file mode 100644
index 00000000..0e0ae1e1
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/plugins/i18n.ts
@@ -0,0 +1,116 @@
+// 多组件库的国际化和本地项目国际化兼容
+import { type I18n, createI18n } from "vue-i18n";
+import type { App, WritableComputedRef } from "vue";
+import { responsiveStorageNameSpace } from "@/config";
+import { storageLocal, isObject } from "@pureadmin/utils";
+
+// element-plus国际化
+import enLocale from "element-plus/es/locale/lang/en";
+import zhLocale from "element-plus/es/locale/lang/zh-cn";
+
+const siphonI18n = (function () {
+ // 仅初始化一次国际化配置
+ let cache = Object.fromEntries(
+ Object.entries(
+ import.meta.glob("../../locales/*.y(a)?ml", { eager: true })
+ ).map(([key, value]: any) => {
+ const matched = key.match(/([A-Za-z0-9-_]+)\./i)[1];
+ return [matched, value.default];
+ })
+ );
+ return (prefix = "zh-CN") => {
+ return cache[prefix];
+ };
+})();
+
+export const localesConfigs = {
+ zh: {
+ ...siphonI18n("zh-CN"),
+ ...zhLocale
+ },
+ en: {
+ ...siphonI18n("en"),
+ ...enLocale
+ }
+};
+
+/** 获取对象中所有嵌套对象的key键,并将它们用点号分割组成字符串 */
+function getObjectKeys(obj) {
+ const stack = [];
+ const keys: Set = new Set();
+
+ stack.push({ obj, key: "" });
+
+ while (stack.length > 0) {
+ const { obj, key } = stack.pop();
+
+ for (const k in obj) {
+ const newKey = key ? `${key}.${k}` : k;
+
+ if (obj[k] && isObject(obj[k])) {
+ stack.push({ obj: obj[k], key: newKey });
+ } else {
+ keys.add(newKey);
+ }
+ }
+ }
+
+ return keys;
+}
+
+/** 将展开的key缓存 */
+const keysCache: Map> = new Map();
+const flatI18n = (prefix = "zh-CN") => {
+ let cache = keysCache.get(prefix);
+ if (!cache) {
+ cache = getObjectKeys(siphonI18n(prefix));
+ keysCache.set(prefix, cache);
+ }
+ return cache;
+};
+
+/**
+ * 国际化转换工具函数(自动读取根目录locales文件夹下文件进行国际化匹配)
+ * @param message message
+ * @returns 转化后的message
+ */
+export function transformI18n(message: any = "") {
+ if (!message) {
+ return "";
+ }
+
+ // 处理存储动态路由的title,格式 {zh:"",en:""}
+ if (typeof message === "object") {
+ const locale: string | WritableComputedRef | any =
+ i18n.global.locale;
+ return message[locale?.value];
+ }
+
+ const key = message.match(/(\S*)\./)?.input;
+
+ if (key && flatI18n("zh-CN").has(key)) {
+ return i18n.global.t.call(i18n.global.locale, message);
+ } else if (!key && Object.hasOwn(siphonI18n("zh-CN"), message)) {
+ // 兼容非嵌套形式的国际化写法
+ return i18n.global.t.call(i18n.global.locale, message);
+ } else {
+ return message;
+ }
+}
+
+/** 此函数只是配合i18n Ally插件来进行国际化智能提示,并无实际意义(只对提示起作用),如果不需要国际化可删除 */
+export const $t = (key: string) => key;
+
+export const i18n: I18n = createI18n({
+ legacy: false,
+ locale:
+ storageLocal().getItem(
+ `${responsiveStorageNameSpace()}locale`
+ )?.locale ?? "zh",
+ fallbackLocale: "en",
+ messages: localesConfigs
+});
+
+export function useI18n(app: App) {
+ app.use(i18n);
+}
diff --git a/sop-admin/sop-admin-frontend/src/router/app-menus/index.ts b/sop-admin/sop-admin-frontend/src/router/app-menus/index.ts
deleted file mode 100644
index 996bfa36..00000000
--- a/sop-admin/sop-admin-frontend/src/router/app-menus/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { appRoutes, appExternalRoutes } from '../routes';
-
-const mixinRoutes = [
- ...appRoutes,
- // 隐藏内置路由
- // ...appExternalRoutes
-];
-
-const appClientMenus = mixinRoutes.map((el) => {
- const { name, path, meta, redirect, children } = el;
- return {
- name,
- path,
- meta,
- redirect,
- children,
- };
-});
-
-export default appClientMenus;
diff --git a/sop-admin/sop-admin-frontend/src/router/constants.ts b/sop-admin/sop-admin-frontend/src/router/constants.ts
deleted file mode 100644
index 7daef7f7..00000000
--- a/sop-admin/sop-admin-frontend/src/router/constants.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export const WHITE_LIST = [
- { name: 'notFound', children: [] },
- { name: 'login', children: [] },
-];
-
-export const NOT_FOUND = {
- name: 'notFound',
-};
-
-export const REDIRECT_ROUTE_NAME = 'Redirect';
-
-export const DEFAULT_ROUTE_NAME = 'Workplace';
-
-export const DEFAULT_ROUTE = {
- title: 'menu.dashboard.workplace',
- name: DEFAULT_ROUTE_NAME,
- fullPath: '/dashboard/workplace',
-};
diff --git a/sop-admin/sop-admin-frontend/src/router/guard/index.ts b/sop-admin/sop-admin-frontend/src/router/guard/index.ts
deleted file mode 100644
index bedc28d2..00000000
--- a/sop-admin/sop-admin-frontend/src/router/guard/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Router } from 'vue-router';
-import { setRouteEmitter } from '@/utils/route-listener';
-import setupUserLoginInfoGuard from './userLoginInfo';
-import setupPermissionGuard from './permission';
-
-function setupPageGuard(router: Router) {
- router.beforeEach(async (to) => {
- // emit route change
- setRouteEmitter(to);
- });
-}
-
-export default function createRouteGuard(router: Router) {
- setupPageGuard(router);
- setupUserLoginInfoGuard(router);
- setupPermissionGuard(router);
-}
diff --git a/sop-admin/sop-admin-frontend/src/router/guard/permission.ts b/sop-admin/sop-admin-frontend/src/router/guard/permission.ts
deleted file mode 100644
index ad3724e9..00000000
--- a/sop-admin/sop-admin-frontend/src/router/guard/permission.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import type { Router, RouteRecordNormalized } from 'vue-router';
-import NProgress from 'nprogress'; // progress bar
-
-import usePermission from '@/hooks/permission';
-import { useUserStore, useAppStore } from '@/store';
-import { appRoutes } from '../routes';
-import { WHITE_LIST, NOT_FOUND } from '../constants';
-
-export default function setupPermissionGuard(router: Router) {
- router.beforeEach(async (to, from, next) => {
- const appStore = useAppStore();
- const userStore = useUserStore();
- const Permission = usePermission();
- const permissionsAllow = Permission.accessRouter(to);
- if (appStore.menuFromServer) {
- // 针对来自服务端的菜单配置进行处理
- // Handle routing configuration from the server
-
- // 根据需要自行完善来源于服务端的菜单配置的permission逻辑
- // Refine the permission logic from the server's menu configuration as needed
- if (
- !appStore.appAsyncMenus.length &&
- !WHITE_LIST.find((el) => el.name === to.name)
- ) {
- await appStore.fetchServerMenuConfig();
- }
- const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST];
-
- let exist = false;
- while (serverMenuConfig.length && !exist) {
- const element = serverMenuConfig.shift();
- if (element?.name === to.name) exist = true;
-
- if (element?.children) {
- serverMenuConfig.push(
- ...(element.children as unknown as RouteRecordNormalized[])
- );
- }
- }
- if (exist && permissionsAllow) {
- next();
- } else next(NOT_FOUND);
- } else {
- // eslint-disable-next-line no-lonely-if
- if (permissionsAllow) next();
- else {
- const destination =
- Permission.findFirstPermissionRoute(appRoutes, userStore.role) ||
- NOT_FOUND;
- next(destination);
- }
- }
- NProgress.done();
- });
-}
diff --git a/sop-admin/sop-admin-frontend/src/router/guard/userLoginInfo.ts b/sop-admin/sop-admin-frontend/src/router/guard/userLoginInfo.ts
deleted file mode 100644
index 7a068950..00000000
--- a/sop-admin/sop-admin-frontend/src/router/guard/userLoginInfo.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import type { Router, LocationQueryRaw } from 'vue-router';
-import NProgress from 'nprogress'; // progress bar
-
-import { useUserStore } from '@/store';
-import { isLogin } from '@/utils/auth';
-
-export default function setupUserLoginInfoGuard(router: Router) {
- router.beforeEach(async (to, from, next) => {
- NProgress.start();
- const userStore = useUserStore();
- if (isLogin()) {
- if (userStore.role) {
- next();
- } else {
- try {
- await userStore.info();
- next();
- } catch (error) {
- await userStore.logout();
- next({
- name: 'login',
- query: {
- redirect: to.name,
- ...to.query,
- } as LocationQueryRaw,
- });
- }
- }
- } else {
- if (to.name === 'login') {
- next();
- return;
- }
- next({
- name: 'login',
- query: {
- redirect: to.name,
- ...to.query,
- } as LocationQueryRaw,
- });
- }
- });
-}
diff --git a/sop-admin/sop-admin-frontend/src/router/index.ts b/sop-admin/sop-admin-frontend/src/router/index.ts
index e230a4b8..6f6e2832 100644
--- a/sop-admin/sop-admin-frontend/src/router/index.ts
+++ b/sop-admin/sop-admin-frontend/src/router/index.ts
@@ -1,37 +1,208 @@
-import { createRouter, createWebHistory } from 'vue-router';
-import NProgress from 'nprogress'; // progress bar
-import 'nprogress/nprogress.css';
+// import "@/utils/sso";
+import Cookies from "js-cookie";
+import { getConfig } from "@/config";
+import NProgress from "@/utils/progress";
+import { transformI18n } from "@/plugins/i18n";
+import { buildHierarchyTree } from "@/utils/tree";
+import remainingRouter from "./modules/remaining";
+import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
+import { usePermissionStoreHook } from "@/store/modules/permission";
+import { isUrl, openLink, storageLocal, isAllEmpty } from "@pureadmin/utils";
+import {
+ ascending,
+ getTopMenu,
+ initRouter,
+ isOneOfArray,
+ getHistoryMode,
+ findRouteByPath,
+ handleAliveRoute,
+ formatTwoStageRoutes,
+ formatFlatteningRoutes
+} from "./utils";
+import {
+ type Router,
+ createRouter,
+ type RouteRecordRaw,
+ type RouteComponent
+} from "vue-router";
+import {
+ type DataInfo,
+ userKey,
+ removeToken,
+ multipleTabsKey
+} from "@/utils/auth";
-import { appRoutes } from './routes';
-import { REDIRECT_MAIN, NOT_FOUND_ROUTE } from './routes/base';
-import createRouteGuard from './guard';
+/** 自动导入全部静态路由,无需再手动引入!匹配 src/router/modules 目录(任何嵌套级别)中具有 .ts 扩展名的所有文件,除了 remaining.ts 文件
+ * 如何匹配所有文件请看:https://github.com/mrmlnc/fast-glob#basic-syntax
+ * 如何排除文件请看:https://cn.vitejs.dev/guide/features.html#negative-patterns
+ */
+const modules: Record = import.meta.glob(
+ ["./modules/**/*.ts", "!./modules/**/remaining.ts"],
+ {
+ eager: true
+ }
+);
-NProgress.configure({ showSpinner: false }); // NProgress Configuration
+/** 原始静态路由(未做任何处理) */
+const routes = [];
-const router = createRouter({
- history: createWebHistory(),
- routes: [
- {
- path: '/',
- redirect: 'login',
- },
- {
- path: '/login',
- name: 'login',
- component: () => import('@/views/login/index.vue'),
- meta: {
- requiresAuth: false,
- },
- },
- ...appRoutes,
- REDIRECT_MAIN,
- NOT_FOUND_ROUTE,
- ],
- scrollBehavior() {
- return { top: 0 };
- },
+Object.keys(modules).forEach(key => {
+ routes.push(modules[key].default);
});
-createRouteGuard(router);
+/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
+export const constantRoutes: Array = formatTwoStageRoutes(
+ formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
+);
+
+/** 用于渲染菜单,保持原始层级 */
+export const constantMenus: Array = ascending(
+ routes.flat(Infinity)
+).concat(...remainingRouter);
+
+/** 不参与菜单的路由 */
+export const remainingPaths = Object.keys(remainingRouter).map(v => {
+ return remainingRouter[v].path;
+});
+
+/** 创建路由实例 */
+export const router: Router = createRouter({
+ history: getHistoryMode(import.meta.env.VITE_ROUTER_HISTORY),
+ 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.flat(Infinity)))
+ )
+ );
+ }
+ });
+ usePermissionStoreHook().clearAllCachePage();
+}
+
+/** 路由白名单 */
+const whiteList = ["/login"];
+
+const { VITE_HIDE_HOME } = import.meta.env;
+
+router.beforeEach((to: ToRouteType, _from, next) => {
+ if (to.meta?.keepAlive) {
+ handleAliveRoute(to, "add");
+ // 页面整体刷新和点击标签页刷新
+ if (_from.name === undefined || _from.name === "Redirect") {
+ handleAliveRoute(to);
+ }
+ }
+ const userInfo = storageLocal().getItem>(userKey);
+ 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 = `${transformI18n(item.meta.title)} | ${Title}`;
+ else document.title = transformI18n(item.meta.title);
+ });
+ }
+ /** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */
+ function toCorrectRoute() {
+ whiteList.includes(to.fullPath) ? next(_from.fullPath) : next();
+ }
+ if (Cookies.get(multipleTabsKey) && userInfo) {
+ // 无权限跳转403页面
+ if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
+ next({ path: "/error/403" });
+ }
+ // 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面
+ if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") {
+ next({ path: "/error/404" });
+ }
+ if (_from?.name) {
+ // name为超链接
+ if (externalLink) {
+ openLink(to?.name as string);
+ NProgress.done();
+ } else {
+ toCorrectRoute();
+ }
+ } else {
+ // 刷新
+ if (
+ usePermissionStoreHook().wholeMenus.length === 0 &&
+ to.path !== "/login"
+ ) {
+ initRouter().then((router: Router) => {
+ if (!useMultiTagsStoreHook().getMultiTagsCache) {
+ const { path } = to;
+ const route = findRouteByPath(
+ path,
+ router.options.routes[0].children
+ );
+ getTopMenu(true);
+ // query、params模式路由传参数的标签页不在此处处理
+ if (route && route.meta?.title) {
+ if (isAllEmpty(route.parentId) && route.meta?.backstage) {
+ // 此处为动态顶级路由(目录)
+ const { path, name, meta } = route.children[0];
+ useMultiTagsStoreHook().handleTags("push", {
+ path,
+ name,
+ meta
+ });
+ } else {
+ const { path, name, meta } = route;
+ useMultiTagsStoreHook().handleTags("push", {
+ path,
+ name,
+ meta
+ });
+ }
+ }
+ }
+ // 确保动态路由完全加入路由列表并且不影响静态路由(注意:动态路由刷新时router.beforeEach可能会触发两次,第一次触发动态路由还未完全添加,第二次动态路由才完全添加到路由列表,如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断,这样就只会触发一次)
+ if (isAllEmpty(to.name)) router.push(to.fullPath);
+ });
+ }
+ toCorrectRoute();
+ }
+ } else {
+ if (to.path !== "/login") {
+ if (whiteList.indexOf(to.path) !== -1) {
+ next();
+ } else {
+ removeToken();
+ next({ path: "/login" });
+ }
+ } else {
+ next();
+ }
+ }
+});
+
+router.afterEach(() => {
+ NProgress.done();
+});
export default router;
diff --git a/sop-admin/sop-admin-frontend/src/router/modules/error.ts b/sop-admin/sop-admin-frontend/src/router/modules/error.ts
new file mode 100644
index 00000000..bc880664
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/router/modules/error.ts
@@ -0,0 +1,38 @@
+import { $t } from "@/plugins/i18n";
+
+export default {
+ path: "/error",
+ redirect: "/error/403",
+ meta: {
+ icon: "ri:information-line",
+ // showLink: false,
+ title: $t("menus.pureAbnormal"),
+ rank: 9
+ },
+ children: [
+ {
+ path: "/error/403",
+ name: "403",
+ component: () => import("@/views/error/403.vue"),
+ meta: {
+ title: $t("menus.pureFourZeroOne")
+ }
+ },
+ {
+ path: "/error/404",
+ name: "404",
+ component: () => import("@/views/error/404.vue"),
+ meta: {
+ title: $t("menus.pureFourZeroFour")
+ }
+ },
+ {
+ path: "/error/500",
+ name: "500",
+ component: () => import("@/views/error/500.vue"),
+ meta: {
+ title: $t("menus.pureFive")
+ }
+ }
+ ]
+} satisfies RouteConfigsTable;
diff --git a/sop-admin/sop-admin-frontend/src/router/modules/home.ts b/sop-admin/sop-admin-frontend/src/router/modules/home.ts
new file mode 100644
index 00000000..87243c07
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/router/modules/home.ts
@@ -0,0 +1,26 @@
+import { $t } from "@/plugins/i18n";
+const { VITE_HIDE_HOME } = import.meta.env;
+const Layout = () => import("@/layout/index.vue");
+
+export default {
+ path: "/",
+ name: "Home",
+ component: Layout,
+ redirect: "/welcome",
+ meta: {
+ icon: "ep:home-filled",
+ title: $t("menus.pureHome"),
+ rank: 0
+ },
+ children: [
+ {
+ path: "/welcome",
+ name: "Welcome",
+ component: () => import("@/views/welcome/index.vue"),
+ meta: {
+ title: $t("menus.pureHome"),
+ showLink: VITE_HIDE_HOME === "true" ? false : true
+ }
+ }
+ ]
+} satisfies RouteConfigsTable;
diff --git a/sop-admin/sop-admin-frontend/src/router/modules/remaining.ts b/sop-admin/sop-admin-frontend/src/router/modules/remaining.ts
new file mode 100644
index 00000000..4f5ac479
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/router/modules/remaining.ts
@@ -0,0 +1,31 @@
+import { $t } from "@/plugins/i18n";
+const Layout = () => import("@/layout/index.vue");
+
+export default [
+ {
+ path: "/login",
+ name: "Login",
+ component: () => import("@/views/login/index.vue"),
+ meta: {
+ title: $t("menus.pureLogin"),
+ showLink: false,
+ rank: 101
+ }
+ },
+ {
+ path: "/redirect",
+ component: Layout,
+ meta: {
+ title: $t("status.pureLoad"),
+ showLink: false,
+ rank: 102
+ },
+ children: [
+ {
+ path: "/redirect/:path(.*)",
+ name: "Redirect",
+ component: () => import("@/layout/redirect.vue")
+ }
+ ]
+ }
+] satisfies Array;
diff --git a/sop-admin/sop-admin-frontend/src/router/routes/base.ts b/sop-admin/sop-admin-frontend/src/router/routes/base.ts
deleted file mode 100644
index 4ee02d18..00000000
--- a/sop-admin/sop-admin-frontend/src/router/routes/base.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { RouteRecordRaw } from 'vue-router';
-import { REDIRECT_ROUTE_NAME } from '@/router/constants';
-
-export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue');
-
-export const REDIRECT_MAIN: RouteRecordRaw = {
- path: '/redirect',
- name: 'redirectWrapper',
- component: DEFAULT_LAYOUT,
- meta: {
- requiresAuth: true,
- hideInMenu: true,
- },
- children: [
- {
- path: '/redirect/:path',
- name: REDIRECT_ROUTE_NAME,
- component: () => import('@/views/redirect/index.vue'),
- meta: {
- requiresAuth: true,
- hideInMenu: true,
- },
- },
- ],
-};
-
-export const NOT_FOUND_ROUTE: RouteRecordRaw = {
- path: '/:pathMatch(.*)*',
- name: 'notFound',
- component: () => import('@/views/not-found/index.vue'),
-};
diff --git a/sop-admin/sop-admin-frontend/src/router/routes/externalModules/arco.ts b/sop-admin/sop-admin-frontend/src/router/routes/externalModules/arco.ts
deleted file mode 100644
index d9a76eb6..00000000
--- a/sop-admin/sop-admin-frontend/src/router/routes/externalModules/arco.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export default {
- path: 'https://arco.design',
- name: 'arcoWebsite',
- meta: {
- locale: 'menu.arcoWebsite',
- icon: 'icon-link',
- requiresAuth: true,
- order: 8,
- },
-};
diff --git a/sop-admin/sop-admin-frontend/src/router/routes/externalModules/faq.ts b/sop-admin/sop-admin-frontend/src/router/routes/externalModules/faq.ts
deleted file mode 100644
index 232b81de..00000000
--- a/sop-admin/sop-admin-frontend/src/router/routes/externalModules/faq.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export default {
- path: 'https://arco.design/vue/docs/pro/faq',
- name: 'faq',
- meta: {
- locale: 'menu.faq',
- icon: 'icon-question-circle',
- requiresAuth: true,
- order: 9,
- },
-};
diff --git a/sop-admin/sop-admin-frontend/src/router/routes/index.ts b/sop-admin/sop-admin-frontend/src/router/routes/index.ts
deleted file mode 100644
index c60f6955..00000000
--- a/sop-admin/sop-admin-frontend/src/router/routes/index.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { RouteRecordNormalized } from 'vue-router';
-
-const modules = import.meta.glob('./modules/*.ts', { eager: true });
-const externalModules = import.meta.glob('./externalModules/*.ts', {
- eager: true,
-});
-
-function formatModules(_modules: any, result: RouteRecordNormalized[]) {
- Object.keys(_modules).forEach((key) => {
- const defaultModule = _modules[key].default;
- if (!defaultModule) return;
- const moduleList = Array.isArray(defaultModule)
- ? [...defaultModule]
- : [defaultModule];
- result.push(...moduleList);
- });
- return result;
-}
-
-export const appRoutes: RouteRecordNormalized[] = formatModules(modules, []);
-
-export const appExternalRoutes: RouteRecordNormalized[] = formatModules(
- externalModules,
- []
-);
diff --git a/sop-admin/sop-admin-frontend/src/router/routes/modules/dashboard.ts b/sop-admin/sop-admin-frontend/src/router/routes/modules/dashboard.ts
deleted file mode 100644
index 27ffba0a..00000000
--- a/sop-admin/sop-admin-frontend/src/router/routes/modules/dashboard.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { DEFAULT_LAYOUT } from '../base';
-import { AppRouteRecordRaw } from '../types';
-
-const DASHBOARD: AppRouteRecordRaw = {
- path: '/dashboard',
- name: 'dashboard',
- component: DEFAULT_LAYOUT,
- meta: {
- locale: 'menu.dashboard',
- requiresAuth: true,
- icon: 'icon-dashboard',
- order: 0,
- },
- children: [
- {
- path: 'workplace',
- name: 'Workplace',
- component: () => import('@/views/dashboard/workplace/index.vue'),
- meta: {
- locale: 'menu.dashboard.workplace',
- requiresAuth: true,
- roles: ['*'],
- },
- },
- ],
-};
-
-export default DASHBOARD;
diff --git a/sop-admin/sop-admin-frontend/src/router/routes/types.ts b/sop-admin/sop-admin-frontend/src/router/routes/types.ts
deleted file mode 100644
index 6b8e8d77..00000000
--- a/sop-admin/sop-admin-frontend/src/router/routes/types.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { defineComponent } from 'vue';
-import type { RouteMeta, NavigationGuard } from 'vue-router';
-
-export type Component =
- | ReturnType
- | (() => Promise)
- | (() => Promise);
-
-export interface AppRouteRecordRaw {
- path: string;
- name?: string | symbol;
- meta?: RouteMeta;
- redirect?: string;
- component: Component | string;
- children?: AppRouteRecordRaw[];
- alias?: string | string[];
- props?: Record;
- beforeEnter?: NavigationGuard | NavigationGuard[];
- fullPath?: string;
-}
diff --git a/sop-admin/sop-admin-frontend/src/router/typings.d.ts b/sop-admin/sop-admin-frontend/src/router/typings.d.ts
deleted file mode 100644
index 5ccaa700..00000000
--- a/sop-admin/sop-admin-frontend/src/router/typings.d.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import 'vue-router';
-
-declare module 'vue-router' {
- interface RouteMeta {
- roles?: string[]; // Controls roles that have access to the page
- requiresAuth: boolean; // Whether login is required to access the current page (every route must declare)
- icon?: string; // The icon show in the side menu
- locale?: string; // The locale name show in side menu and breadcrumb
- hideInMenu?: boolean; // If true, it is not displayed in the side menu
- hideChildrenInMenu?: boolean; // if set true, the children are not displayed in the side menu
- activeMenu?: string; // if set name, the menu will be highlighted according to the name you set
- order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is
- noAffix?: boolean; // if set true, the tag will not affix in the tab-bar
- ignoreCache?: boolean; // if set true, the page will not be cached
- }
-}
diff --git a/sop-admin/sop-admin-frontend/src/router/utils.ts b/sop-admin/sop-admin-frontend/src/router/utils.ts
new file mode 100644
index 00000000..dd6df9aa
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/router/utils.ts
@@ -0,0 +1,408 @@
+import {
+ type RouterHistory,
+ type RouteRecordRaw,
+ type RouteComponent,
+ createWebHistory,
+ createWebHashHistory
+} from "vue-router";
+import { router } from "./index";
+import { isProxy, toRaw } from "vue";
+import { useTimeoutFn } from "@vueuse/core";
+import {
+ isString,
+ cloneDeep,
+ isAllEmpty,
+ intersection,
+ storageLocal,
+ isIncludeAllChildren
+} from "@pureadmin/utils";
+import { getConfig } from "@/config";
+import { buildHierarchyTree } from "@/utils/tree";
+import { userKey, type DataInfo } from "@/utils/auth";
+import { type menuType, routerArrays } from "@/layout/types";
+import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
+import { usePermissionStoreHook } from "@/store/modules/permission";
+const IFrame = () => import("@/layout/frame.vue");
+// https://cn.vitejs.dev/guide/features.html#glob-import
+const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
+
+// 动态路由
+import { getAsyncRoutes } from "@/api/routes";
+
+function handRank(routeInfo: any) {
+ const { name, path, parentId, meta } = routeInfo;
+ return isAllEmpty(parentId)
+ ? isAllEmpty(meta?.rank) ||
+ (meta?.rank === 0 && name !== "Home" && path !== "/")
+ ? true
+ : false
+ : false;
+}
+
+/** 按照路由中meta下的rank等级升序来排序路由 */
+function ascending(arr: any[]) {
+ arr.forEach((v, index) => {
+ // 当rank不存在时,根据顺序自动创建,首页路由永远在第一位
+ if (handRank(v)) v.meta.rank = index + 2;
+ });
+ 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;
+}
+
+/** 从localStorage里取出当前登录用户的角色roles,过滤无权限的菜单 */
+function filterNoPermissionTree(data: RouteComponent[]) {
+ const currentRoles =
+ storageLocal().getItem>(userKey)?.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);
+}
+
+/** 通过指定 `key` 获取父级路径集合,默认 `key` 为 `path` */
+function getParentPaths(value: string, routes: RouteRecordRaw[], key = "path") {
+ // 深度遍历查找
+ function dfs(routes: RouteRecordRaw[], value: string, parents: string[]) {
+ for (let i = 0; i < routes.length; i++) {
+ const item = routes[i];
+ // 返回父级path
+ if (item[key] === value) return parents;
+ // children不存在或为空则不递归
+ if (!item.children || !item.children.length) continue;
+ // 往下查找时将当前path入栈
+ parents.push(item.path);
+
+ if (dfs(item.children, value, parents).length) return parents;
+ // 深度遍历查找未找到时当前path 出栈
+ parents.pop();
+ }
+ // 未找到时返回空数组
+ return [];
+ }
+
+ return dfs(routes, value, []);
+}
+
+/** 查找对应 `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 handleAsyncRoutes(routeList) {
+ if (routeList.length === 0) {
+ usePermissionStoreHook().handleWholeMenus(routeList);
+ } else {
+ formatFlatteningRoutes(addAsyncRoutes(routeList)).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);
+ }
+ }
+ );
+ usePermissionStoreHook().handleWholeMenus(routeList);
+ }
+ if (!useMultiTagsStoreHook().getMultiTagsCache) {
+ useMultiTagsStoreHook().handleTags("equal", [
+ ...routerArrays,
+ ...usePermissionStoreHook().flatteningRoutes.filter(
+ v => v?.meta?.fixedTag
+ )
+ ]);
+ }
+ addPathMatch();
+}
+
+/** 初始化路由(`new Promise` 写法防止在异步请求中造成无限循环)*/
+function initRouter() {
+ if (getConfig()?.CachingAsyncRoutes) {
+ // 开启动态路由缓存本地localStorage
+ const key = "async-routes";
+ const asyncRouteList = storageLocal().getItem(key) as any;
+ if (asyncRouteList && asyncRouteList?.length > 0) {
+ return new Promise(resolve => {
+ handleAsyncRoutes(asyncRouteList);
+ resolve(router);
+ });
+ } else {
+ return new Promise(resolve => {
+ getAsyncRoutes().then(({ data }) => {
+ handleAsyncRoutes(cloneDeep(data));
+ storageLocal().setItem(key, data);
+ resolve(router);
+ });
+ });
+ }
+ } else {
+ return new Promise(resolve => {
+ getAsyncRoutes().then(({ data }) => {
+ handleAsyncRoutes(cloneDeep(data));
+ resolve(router);
+ });
+ });
+ }
+}
+
+/**
+ * 将多级嵌套路由处理成一维数组
+ * @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/pure-admin/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({ name }: ToRouteType, mode?: string) {
+ switch (mode) {
+ case "add":
+ usePermissionStoreHook().cacheOperate({
+ mode: "add",
+ name
+ });
+ break;
+ case "delete":
+ usePermissionStoreHook().cacheOperate({
+ mode: "delete",
+ name
+ });
+ break;
+ case "refresh":
+ usePermissionStoreHook().cacheOperate({
+ mode: "refresh",
+ name
+ });
+ break;
+ default:
+ usePermissionStoreHook().cacheOperate({
+ mode: "delete",
+ name
+ });
+ useTimeoutFn(() => {
+ usePermissionStoreHook().cacheOperate({
+ mode: "add",
+ 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): RouterHistory {
+ // 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;
+}
+
+/** 是否有按钮级别的权限(根据路由`meta`中的`auths`字段进行判断)*/
+function hasAuth(value: string | Array): boolean {
+ if (!value) return false;
+ /** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */
+ const metaAuths = getAuths();
+ if (!metaAuths) return false;
+ const isAuths = isString(value)
+ ? metaAuths.includes(value)
+ : isIncludeAllChildren(value, metaAuths);
+ return isAuths ? true : false;
+}
+
+function handleTopMenu(route) {
+ if (route?.children && route.children.length > 1) {
+ if (route.redirect) {
+ return route.children.filter(cur => cur.path === route.redirect)[0];
+ } else {
+ return route.children[0];
+ }
+ } else {
+ return route;
+ }
+}
+
+/** 获取所有菜单中的第一个菜单(顶级菜单)*/
+function getTopMenu(tag = false): menuType {
+ const topMenu = handleTopMenu(
+ usePermissionStoreHook().wholeMenus[0]?.children[0]
+ );
+ tag && useMultiTagsStoreHook().handleTags("push", topMenu);
+ return topMenu;
+}
+
+export {
+ hasAuth,
+ getAuths,
+ ascending,
+ filterTree,
+ initRouter,
+ getTopMenu,
+ addPathMatch,
+ isOneOfArray,
+ getHistoryMode,
+ addAsyncRoutes,
+ getParentPaths,
+ findRouteByPath,
+ handleAliveRoute,
+ formatTwoStageRoutes,
+ formatFlatteningRoutes,
+ filterNoPermissionTree
+};
diff --git a/sop-admin/sop-admin-frontend/src/store/index.ts b/sop-admin/sop-admin-frontend/src/store/index.ts
index 3d3035f7..a8dc7527 100644
--- a/sop-admin/sop-admin-frontend/src/store/index.ts
+++ b/sop-admin/sop-admin-frontend/src/store/index.ts
@@ -1,9 +1,9 @@
-import { createPinia } from 'pinia';
-import useAppStore from './modules/app';
-import useUserStore from './modules/user';
-import useTabBarStore from './modules/tab-bar';
+import type { App } from "vue";
+import { createPinia } from "pinia";
+const store = createPinia();
-const pinia = createPinia();
+export function setupStore(app: App) {
+ app.use(store);
+}
-export { useAppStore, useUserStore, useTabBarStore };
-export default pinia;
+export { store };
diff --git a/sop-admin/sop-admin-frontend/src/store/modules/app.ts b/sop-admin/sop-admin-frontend/src/store/modules/app.ts
new file mode 100644
index 00000000..2644aefb
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/store/modules/app.ts
@@ -0,0 +1,89 @@
+import { defineStore } from "pinia";
+import {
+ type appType,
+ store,
+ getConfig,
+ storageLocal,
+ deviceDetection,
+ responsiveStorageNameSpace
+} from "../utils";
+
+export const useAppStore = defineStore({
+ id: "pure-app",
+ state: (): appType => ({
+ sidebar: {
+ opened:
+ storageLocal().getItem