|>}j&b_*KM>Vl
zdym6}h{tr}=NcpxN#2RAkgb;`4Wj*i4cHpz-3Js(soCObCdqxlM;`C{18+Hamx
zDV8*i!sa%ZaeK8t9*v6`s;N#1aa>@mPl4=&9*Tt_7{5EqMLI7q)~2+T_*aLA^|
z*FNmn!XzuxryDz8M`g1zG|cdj((Xj6_uCuVM#Td?@~ZUMTXJ|e?)emPa49^P@
z1;3WoZqy6<)u&7=lV2J;D>c%4%y7>XNG6=~6^|}q-+5YG{f9YP*tOf7M7Eb=$_mfM-5VZIeX}*n=Z;!vbGSQG0IZh>4LqUBT&QndQlj9S
z?nU}9X%2Wn4D|XR4c4jo#3k8sLW}l~N#y1NY;M|ylW$Yo3=xy-59%T?XQ@Uiw&WV#-(()Na3htG
z>UEJF-0E!>8E
zsq0UOySPt|OjqL?IEP7v6-_hf^*kZDCd%rASSdQi+1;5J9KO<22Omme2a4A*sX)Wz
z!xZ^9R;Cj13g%40(Q)Hea4{vZBJd32?$6173M_qk_d1;KLBdz0@ydo`E9Iu)=Ci5l
zCi{YbF;Td&neFWE^WxweDpp*JyEApIl+W|Q`75LI?S=j^pC}cCCv2s3Uz%qGNXoxm
z0&!con9^9;W}$|Z;-(oHXL}z+sC#H=sgsJo!6(Y+wr0sJDU6oO|S|1NuwK}Dgk5$qMY6y!#}s*Gz-Kv5*|>LL-$0&
z{JvUjV}>R@dR$hjIK3JFFaS-NMGSMB(1de
zTz$L}R);Jp5&Qw92Fg|`!a{>1XH%|D{pP|wg9OsI*vY&_oL8VFLw3$H##hU+%QEde
z$lFyVz?@&KL|r*6yOoj^_92n4beccY*WBf!hKmx;zpk(QQBB`vuTCx==Tts72P=nI
zv=<#YLw)45skiE7=HXl<`*2SrBSZSvaVE+ow(f*fD4A}Op<9&TC9XF}C*z#v
zUmNgGl=#zB&b=>k%5*RO29zw}Hefze>5-<8A+=1DnXen@H+OkvPtAxqc9BH-Kpfm#
zpv?n6Wq?b_#zv+l6IGEbTu10nk~6Vivsj
zK%Hj@qz(mREVnkSGo?$vSq)A_XIJS|Qa4f#za}br;%c4vb>@x}Yy3B4*7NrSTqV`{
zz~zBnQ(tCY!>lgF|H%XQS@k_MRQl$?G
z9Gs>a7)f|Kj2~lW15PMW;&k6?q6{Pu{~4=6lCrK3_|1bBUxGw2>04<3lX)Wzj4K*>
z3W(7A&+I)G8?a%ov_8rk1rtuj9rY}L7^_Cdr~XkTp8)7P<~PJKVEQWgICr#uHbI-s
z7kM_yWoM}d;?Q=@(BLfaKEl`uuwOOMi>v)}y7&sddaNViEHa9
z3+K^*^)ts;WN@#Iy%*VO4rRKkxO42jz$g>)(kcP`9d7TJCyWwbf&!;KV*>~O*!CK1
z&T~ldQsb`|$o8*f#rL7nP5k;h@=#R
z;E83qHIaY+Wu9t-us4Hk?rp$so8z_GpxNEbX$RQi3*W)Fq^-OZ_=g8AJG31Go4crI
zL7IM3kL@jpS;t&+!D+CL|4hn|X8ZG|QNdWn4{E>b6d5O-=Lw8%lnm*<$Z{<|=Kx{*
zuQCHD$vat%3O5`VdSK7csfvhO;AwxATyd7%744C57o1LO|EIw57a7uT&FyZT$77kuX9f(46$P@z;qW;!Xb8
z_&?irK3a#nSNl+aC{TVUxcv`oj?vF~=XM8!s$4AWR?X$FMUT=tNg#C58f6GRWoOl-AZ7cjt
zD>R!0Q!stv!4%MSTO64HE>4m54qe9lNZp;kQ
z1l#OIc=As4=0Bc|w9lq9Jr6*>@}rM?UC_6aUK8G|&7uac`WPUs8t_J%^MCf5=#FXC
zX)QEc?1O8pt{fg*MM#M?Jkd1d@)j8(Ho2f<2Sx8k^xcR@jNQG~^;g77O#83woZH68
zp9kgX4RwOD!IO
z*2&1PuMc!WWm=f`Bg4^)NSmI|CEYqY_O*y(F`+@HiZ|!I?E8eZ&|x2g4bOK8yD~=x
z4QWPt#O|hNNWTKR5hKq=Xp#8gN|a#
zr&UmmuYFrc#tFR50!2*?erO*D?5cc%LXm$PX*BNcEj;8{3!CS6|BDj`Vm@d~F1`L@{c535pIbhPF4OD&lLe85m}Mga#W_u?yaQT%=1B
zCfHpD*$doQ?%k83SJ`3`M1GxFg-tJJ{4%UWeWWu!#A4Yf!r@=a*)`f{Xk)`k+8;@2
zRHxRfRQdx&@2a(#)f{74-GD4xWvlrok6xylS=?(*do7;?j
zZ%Rp_+YxCRV}cu}f2guI`NW$cP386zqA6Igi68dEHvFkQ7ZVdTc0
z6c;Hv!;SqDM@AGnp-tNKMfj3s+Y27%zvna*sXk8SP;XQcM83jQB{oiy_vov8ERk}+gq
z+aq`p+ZpR7d3NbVTJ;&tZ|34d`c59slHZ4rvJnEVa6V1)Od7H$3iR(@-|cbkHkZBA
zkTN)wrqDy-7J}PAS}fFne6-uVRbSW4UGkrBJyvvSnwU}^DNvOn*9~)Kz_^uNel|nx
z`A9PPeFo!B=fei$aWsI^ThVBfz=0t!5=G~!~MZB&c`{|Y3s4l
zJ-L-2!ox5NK?&Ec<5fTs)OhxX7JDM%Ick=OP;(
zwvLXL$?2^4-+MZzw#<~aB$k+qnkI)BknNFvPIM$;k3;ja8z0=Ey&DY3PmWH|8@p>g
z`CSTr1_xt8!XKeTuCl(>!_3B!YGLUu!ZjAs)3LLy8QtGQ+n^|$J%VG%O_GXYjh@^B
zEmlAoapV@RGk~>ni1u?d9OJW>W;^Bd=o~
zbazz`tFXf))l5o;_sv()#B;vCJ3HSRs>;qZCp}^cP~2Rf6dj^s?a5W1Kk7@YdCBfV
zhEA)0xlb7FqZltd4W9+|EwAsFcT!<EBiq_~p#AzYJqJkU-~fRjGj-(Ch|om+J2olH@hJ(8wUR
z-AcVF9mCAnkt+uOmYMk7FaCg;f^s_L9bt6Y2wrg78A2U`f0*L&;$-yiU$kr0o?~MA
z%M1E=o1Xr6ZGLQOq+IK#MFrU7HZj5pF#oidx!+475npA6`ao1X&I0MZ$F(r@9n
z#S8vWKSx){IA0CHiVz*SNT;1OFrz~@$;q1yBCFK&RcD|&$#=tQ?wH&(z>Z5RHVuQ{S(PN@2xTcL
zBQ93OxJqiXD&dx8<`JL_uxiwyr>_Rjs7}dCM)o+RZRv8u`ScU2Q~p|{ZM|)laYpT>
zjK_ILxkx&~18J*M;FsZnSSLxQAtVdTIXmN%m99-stWD3&xZKIJ=Mh(5${j
zwALnBFN+eX!{0AnCgH5^;Bg`-j~?YGhgKrQmgnfO5jY+isLVwo9AXp*nqq?#o3p@r
zxB9*qNQ+1U_N6gs!-32RxoJWK)2Hbt*=V!N4E_;1c**Lvz}a@oJ1nUl-ku<(;zzbW
zx%prIlojnOi7~vg^PedV*cgTw<9=DSXoW3+bHV1&
znu5jXs2BB5xz~o~dr8L-rJ=oZi?F%L@wg=&ABY5F!}S;fcRhCOIir6{CZXkb5kb?(
zkW@{VYHYwf`+*qlUd19tW;o~AZcn#i!#p9sikb+Dx>y011pw|I8V`&X=(*TBg_bg?m}66Gf5?Kc}MX_Atk({FEKXUHOD(K1ypz7M9uE7a-zu8rgk}|^
zzqdV?9NZ8?6)IehDgN43y2`5N7r=DY68|`O&Zp;~UE?zkmi~Rmd+dY?;arUK|!eNyKC)r3uworc6Pf7EOEzN|UwcN?aei{QUd6
z@Xk!r9{t#79<;)?i0Ki_K0N@IjONo;jrAa1a2xu$lDpj;iwuY-3yiE3q@Ht$uMTO%
z|Dwmrv%k5B%cxLK4@S!4>&fUG3;czT1s87_(1%a@3HqvZA!D-NHwCc=&47xitCT7^
zBNJ#V2kpEIBp+PP`mb9`^A7<7y
zk1Lb29?Q!sI%mc0EDUcx`-ZlH5EO0U=4Um
zf$HtZ+-`kJd|z2BG0xSApTOV&k7s8YcXdKPysan>nlp(nrOvqso~3p`yb;-ds7
z&QTG;^R5pijA*%i7*ZP}OPF5&tv<5i$VP9Mn^t|3Q1Qgc&~|X=cCPldp>s+kR?-Mn
zZdo_$P`YNI&di}r&nPI63EV*T;IfKW*!QsG=z9R4dn7aOMUa`l_R6Mb$ef2B;UekL
zZ$4nex_B57Q+iRY5KmFN2AWs9GV|;PPjx=6{C71T1wJ3j7kf*`!6~YN-OQL-`TXY8
z78g&NFi^HgrX$`i-X52|2YxyGfCtip3W(G1mtU@%@e}`aa|Dn5cE{b-o?}dJ0clUm
zxMI|oz}rj*gm`(L0c}4@F+0ZajlJRr>Mi*)gl4j&Z+N`0Yicx`8d80R`WN+H6>rK!
zEZlz3+R!$sRoGQCD|+KJ$uUiUgSD%9=A`FC?!g6b3XlPIXqb{7wk$rVBk>{I6|ZVd
z#y)Q18r8^6Q@HA+ZS`(HJuG1sSWC@8DbTS;ZGRz%zmfYOnr@93OvwrQO5
zf^V+TPyN7i_V8Rrqn;${Dw+9KqJ$#iKgPgiq5-l&*0%P^FY9xOy!7cbS5`JFHuldY!zi8O2sdM30|xnC3WY9>bXId{IO{--E^U`b_B
z*z&7EedO*(R|SbKcblFDX2$OQ-nqZ`;5lTU^jNEe^5WpIH!8E4rd|L^Jcbb`J#;u|
zuT+%xu4Cf0O1<@YIZhT(lL_-Yw*7YU%O9&txiBPWK
zKFT%;T}cXI71gUUu}oQ=4i*=DAEWF1t)U|TXm!)<)s
zD1y_!RHXRBicvX_`pCl4l2Mk#^ls}j|CUX2lSB0=f|$rYl=pFw+MNT_w57hQRiZ3s
zfMoWA`iO70UFEGB(F#V%SGG2WlO?(C^7n6?x(Vl`g=ft)#4I)C{z!WIqbj}@D&fTM
z{z|Ekn(Mc1!Y0oaU`Qk6J1=mq>Ayic2Lkg*Lk~kbdH3f?pk@;KtNhFf_(MfHU05!2
zZQ6J?doTUceV-c!uU>)&w_McdRF4rseLc5cCL%_9+V_%Gd54aPQtz4Z5}gMemYy$F
zZ{n|Y2KdldeQTHt`~K4mltrb3#Vsr!V=n%K&1oh-2=91x^1iPk9rhjc$1T{}GSQAiH02(#?=KzA^A~
z)lmnYe=pF7fYKT@SW>`oR)6l_*`72jTRr)6()Q5=)BgMg05urpXS_<)~JI2Vu$R9xPNBhXD@U;S^b?nN(%;?{O=h7
zKX^8@_}u@n=-cgfL_Q90SEsq`|3?Aaq90zs>@mYpT)c7s6ve?z=k~2s>BySE-K%k+
z^Mv`1Pl}Qt7Ed48CsooOp9_g+AFks-}Rz0AEV;a5LeZ&Ae?r0Y2CZL5thtj_yzQR-SBBiK7lZqPKw%SyFos
z#{r{?q%a>eH|!`MEgvCnu;ZL0yT$|CxCO%}rGZM9wDzxcq_r4z@=R2JKzvw#p!E(z
zcTiz*-{U{qSI8kxLK<}NpfU3{{%#MoB%yAU?UHuLL}~01=!zsrhqx$)kyFz|2+Fre
zuj||j_A12~D$Bef{=wZgb;Pm1Sqf58t47H;x%n&om=503t{QC!h!|2bCX?jd??*PEBGbOkI
z3K8Zml&sBLKC^$`YF*b^=Tz8PXU5(>fgSe{$cRd5$kS1+Xh)gs8}xzimSGl`QCWeS
ze<6X!qye+h&FA7aK{P>rpg6hTp+(FAYX1A0dFG^0w9YscYbC}>Gp`6u!f16jz9vM$
z+g}j)E17TJ-y!V+G2@;gA4O6SdNee{jBlYV6^BuLj
z)9iBb$+EJ(nYo#`(rG_XIbr?1AZ_O;J)F;3p;u^Bj;+`J`tKN<<*t
zy}!`2GEwte|Aan}1-Ul)3T+q}EuW@<=|`J7&GE8?o%emZIpHezG-2lgxmfGA?I*Yp
z%8?r#gQWhl2;#<~wRU(%+%sEI??z_JKX@>mknuwJB`w
zx7`et*56+%dBR&Iw{};V@*4Sc>_R0Y0p7Wdz_>5{fWlurOr>?b0(wy{x(M0+Pf`-q
zmBan3@(TNZ$SMEaqYG;naQ~4Qr>c1J4D8V+r*duNsY^tC4Tk+ZVODkNFHSxbP{5aC
zj6{YxFI(n`O_S5qpOP1)84#t#E8z1l*qnOQTN^bCeEoW#s2!VuP_q>pFw}qLP-iG)D$Ln
zSLbgQ{U?A}Z~t7$7a%Bm3FSMd1vtPLxx7zmw6&25gO)-{KV@_!X6U?wN$)ii1lu0}
znJk8%2~d~ifH&6e@ahr?V=pPL{tgn5!(vd7-6Qo}8;J*as&72OUR-;mR;9f^RX*{>
z#k3hxlh5BT6;v;>fxR%#$eTL_PvhdLk7e2NB}WzY)6ijkx>(1gQ06;?9KlzUM7ikf
z;WUK@c+UR#5#%zcbgBp_d-*EnjJ;cBG$namcx<05|R}Du`g7VW9P;I
zP?kpZzFqn}m77OHR;F+GxG_dj{MEA3!<#wDIma4&h9po*uR3cGIrUtBIKp`l1Tzo7
zK7X$HIMi#!;6|L9xFgTCH$HZi=tC~e+k3%)scK;33l#zCda9l1^tvscmixwQSL3oX
z$QQLk`_E%S!E46?3aeulG;(uT&!b3?;Zfk)Bg@Z+>NY8Jmu77v(z%O2z@lOBL#lKak?mb*EhuWLawSI{Kjj6?YX^loJGp-jCe
z2?PRH>-v|7kp3&d&P*qn@_>f&OvYK~lXHjb3N%bJu-Zw-<{S1k{>$Y-Ok?-T;e3iD
zFRfjEzG`xPikuCyr|q)6BF=A;N}!`^>9Z^gNtng9r;N=#;YB51{<7UyHg~@iHg|w3
zzJBOI8)#%PNHMlzYjN2rwO16FKh3;vx9jrUitV=SI}Dg>l)}7po+96TAtjp@O{XPg
zWfox6N`^Y{d^}=_~oWn**naVvi=9mToI^}1m0XTdH0x~#Jc^M06S#R
z@oE2e>R?_d!F+@1*5I41UvXzJ4iDLJalfevmVla(uo?_h+y*%dJ;wQpCOx;OBV29A
z`q=kd*P_1`?drEj&_R3>QyYIxHs@YJgZU__9ATUDU{8YPwK`+N%PWA7U+eylckHW|
zcU
zKenmpy6U<9g~Yy>QU7e{(R2@U?FcOkQUq?5%}r0I_=j>dWa>z1PT6Z`zyzX~t@%rR
z@r{j*i@dpXR)avhGkZ3(5Ajnl(NGZ)EGpDibUUE`wkUCz9^;rxQ)-1urys+I-2axr
z7vncJpF3#K{g!?ozR?wnwmYA=_($A=yR{;WmaNQ;Gr~LmbP%APOV8mSyE<{NNlv2(
zZaesd;_JsN8|eT#D+Y||m06~IGEPi*YZJcUiw!BOd!*3PH1k}~+a`_nn#HF1suvG2
z)Fwl8y6gW+gu%*ore1;^)R~whsC4@>utm?~H2kMOZ9R$ccJ0-1#?4RR0EJ;cHX+<;JwWW*c@*TnD_IgNdSz^sjeAx
z0<)pk80UDu?l%Z>aVAexUCi#7OO8uWCVfJZwp6Vn&rZx}M`yn`G;n2KN_{S+55-Kd
zV*q2qx*^sh`y3%Er2q4`Kkj@%VBpKQXZsq#Jh|F(I%ak*eO&q$xaYAmXZvRFazq3j
z0Wf8N71PynLUDe>c^$TUmHGLF`S~8G-?GD>Z|&FE5cFPuPgXc`avpVYeldjfKlpe?
zvxRa0yd}`JAn?KG6>2$UJ|Wwm5kj7)6Zlub_#>nU>h7+V%sa?jlT}i>f4K*kuSH(E
zR}(tQD{!8ZKRM$^72s#w8C^f~B!9t6Z+z9~CrKPw;YkUL_*Q@{iBAKGFm?H}r#_
zCWuYEdE{f&Zefv(pw8c4n*$C}yT+|)f5TVH-|IY3C|ldt0V5+LzkYq%`=jVR`ux`~
z4ryyHP{3QU8vPkQm}{$pKL@=2J;Zxy@lGwkDMt4
zqQQAI4fea!%f_ntANU#i|Fj&2tH*>1gvOOSmHbI{swp18{h01#;{8UbU{
z-9f5yY~g^YnHoWtptlI#LbeyZzuJ>0{>x_$I=%QHgJm2a*C0EfRvI@WxP+sl=bhaJ
zLt3mdrFnt9m86Gl(d7l!Sa3GqS?Cda
zLGMH4zZ-2dY4^-q0Y=tAkY+gh#FSR5UB@*
zR_Dkp1Nw&U?+l1O6Tb5fo;Zqf$5cI5J!{MYFx@!34eWE3N%51>^!^S5THW?k6;n<
zap#Nnh;if{=A@WgUS6y8Bc|)}(n;ru@>_v2W)6%V6aL!HPxw%8nm(INk1|crIgGUYe{*f9
z8Z;kIO!<4ys&HVCH@M=4^dpg0yM|UA;r2;qG<5(cn`zTAlM~}mcz~tCWCFGO<$V9?
z^O(WzeDEq%ErL!%L6i6335+s8OMAqin(zW$ew7`!x%te67Ku@sKU(5J=PNFkBEhK_
zo3=HRtgQ<_Eal7~L{T9u@#WfFFz0uQ54|B#$hYs&ga3^{lOkTzC54y848`ntS>55O
zPVoS?bJ#p+7HdF(V|E*p{$U$KtjGE@DceuSrchih45%5$(m>Lvo;2bg7s4dFfi31Q6fGxgAmp~5-3AbGmua)uD4@VlsvAON!MDT8egoItrgNj
zo-*M*xYyK>k;w8siu`3Fzb7LCbX`T$M__AaOf+OKDEY+~?lsKMG=;LjA9g;GBdGTT
ze@$ulL=DxJq-+UtCvD1p%KQeh{hKmya|SlY*!Bb^6>GHD1BGsN3X>bWk@gDAS<Y!TdXru}WLw9mN0ke3CmMFz*9CkJI-HNO7Y7m@H$hp{bLlU1U&wL-DN6$a~sp4F#89deC_vy%q^})c0Oi+Ky=*X)x3IP++
zlEMuw{|pc*U`p4Fs-sGbDbIDoC6?O*cctheMw0Ir3j?qHUi|Gp5Md#<
z{?xZugxb#0ksUC9?C%p|dnH=LJjWCeINB}DTw(g6Bjm8j#ILvjxs-NQrWsNHjz!IH
zxF-#>EA0BChew0_ftkbm#WhlHl?ajQI{tqn?z0q4M2PZ*sMSQqc%F*
zZnr_$^(W9Y1OmnsXu*orr&KliO1W~?rubd{&zkyWOq%ebk=}(-DF_J
zYSZ&F1Rvdxx?hvB{$$(0tlR88c~|S+HwynwR`ZP3YO{UPLwlcOjJ*$I<9gNNZrYz7
z$~5|~fmuKw5DesTc$ZDdVF-@04)DN=zcS8K0JvS3@?O{#;KbjDe3F^B9X3CWs81PF
zdmt%TD&wq$Y|r`?Q!{iWxykG{GHilsEfSzV+#zyDIT3{UR%kQwT~n^NRdvw54XC0l
zzeTps-?<|VSIYthU!Dz(R37weurTL2OyG@-$R2Ch+!ZO4|FK7ZGgt9t3Xn)hJXctA
z92;A_VgLq=r~^S&LouqL!+fA>ILVsqW;h8&rfog4Ppd99Vji~n`4E!V@g8~{V#9R6
zB4aR)lAe|HGn0`)GgyYiq#NDsY4EJjqus0UM63CTVeZ3`{j>!Oy7>aC&s~E_q!NP*ShhTLce>Ou*5b^%%nm;yG+zE
zHPJ`~i8YR|p=4xat3T>dJ(D4e2%R%$V&S+Xtg4r!Qk0?;)H=JZxOj1Ku`gqLyY<~m
ze-XCXZO!%l=JmH?MB~xdHO`7rO(tAxQV4vyQxsfcq?QpOjw(cJO9qIn5=0H0dJP!~
zsoZO&Zdbao=!qpqOEjtl8cA^`6iXKqSg+>_?OmqdUz|+a3Z)IK$&rTdajfBD(Y#Dz
zsE~-`W}L45gt_$^>YHMBo01UfPxs&E+k}vWv$#&7f0ZIETl|H7NinuESE$xTIX1?Y
zdcAIbNg6u)KtC~y)SoYh=Rt|<+Jj{Gu5MyfbFVQ*{LM9h@9&}6r`$edlu
zng6@1@{?o7S7FDxZm8+{3C06}PqIpeu21qm-Vk02c^^5GhH0B|XKwL0zhrk?XI-6E
z^Fij((hF03p6~Y*X3fbVzkIw^{XQ1mVu!n6#WMuzkzodm2W@etkg&2V-P+%;*JoQ4WB+04$1=G#DwQPX!eBDcWBEqP
zN&gjS@PrUFSWgvtk#CQkbiM=|CYlpCuC-wt%6O2Lt9z<>lkq_Nl|{SYQYp6KIx$j1
zmt7q`Ta1F1FLiNG6!$Us6BE&5i(hWn8^^eFI`FHcyKzlge7k*f~Hjp~&4(4OR4-!YEXCin(#&D0aj^T&glZzIb$C@03GvA>_q+j42t$%*Oo
z`-j^GP||BKL8r=L-7Csr-Eg9^i{Ah2;jW6rpVx|d_};_FtgO3(E`Ah~HUV+e@|b93
zoMmTE&rVNy>M$Z3Js76dOOK$8O#yex4gA=ijqhGgXXSq6;jaBY>lRJHAKo90dRu7i
zdkD?o<@?Xdg;Citf2CJ43-ufs=1H>h)v3#2O6>lgeN=>&P&*OSkDpneOJ_t_zN98-
zDkVKl<+`@pef@lI#|g_|hrwC*u*M+&*QMBm?@a=jONZ-z7Fc~3h9LsaXgs;p|ySMdjoT3OfLgspkw|8{y92NLr97`2UGg)
zf)*FI#vPyRIhgbLuco=fT8Z4Uy1Cc!s@~m0L7i{k$nSh4e2pr6^T@mN4foLBfc>Ke
zze{m^st7cz;xmE%ejSAfCB!B2^x~bq1U~RsgGN55MUz~Dm7)Awd7~0^l7~sSK>UMF
zm3jF2s5n1GBd%>#)uhfu?zMCylE_kl&rxj2%LlDDxMd}XhP90%hVt6&cly`>9wSq_
zYlQ5;xl6w_^wNo>j`03G!2LEyy5y|8pzS?DH|-^5GK1b~B8mQ>a-IX-vcQ;YRO3p6
zDs3#}hqR%7&>u8^ra$-+UEf%!PSvVs*gghc`rRS@LCkytwxI@!5&o>teL>}+Cfd>MT38HLmOL~lW
zKl66yXB-^O4t4s8RQ90VBMd#3(5-|cSEJa>+jEkrW^`3Mf{^##*{j%w$Wv*p?Qm@9
z7y94=dzkXHNHgzvNNd;P(WTNi@vH9IcH3OMt@qy;m9Jg5t(5x|h~Zv(-1D%jQmT6L
zQSgimRHHm9HaQT&bT)ii9#N%bPqdIfOfNe1N~wSkFbEt#rV}@ydFkUv09vm_(46m8slLO{
zN7a^V7uqtDuRYb>`H&i1d}>n~Tj?RoZzj)$b8YmLVig60yBCl2>^5%+5lzl|`*Fo<
zL_`dNAT~gJsYdZt6u;@K=qsZt>XDX})aLh9M%A(RlIri_fdOb^G1ZYsW&g;Zf{McJ
zfe9%S^Y(dBwwFRw1=V%~-JGVsps0nFga
zkD$I=EjOtnnxoNvYT?Z>TkjjnG=YFw9n>~`9Y#=G7!qSML39g(=MjHNUuVM15KujM5|FTC|xj^*YJL`8Kz=q;z;0
z`DLwSZc?X3yUmSEm3I#<_$-U>`j&h%>aFHNb6M`{4Hz{US+rZh2R0l#WmwAeTyvYr
zFQHb$T<9AOv2cN<$*o+~<-hHf2z*F>i!@ETMz7S*NxDenITCC5r;q1@bF{a|u1`KT
zKI<-_sGKkRfww8gTk&Q;@?K`9Y>$#y@8p>?owgg(I5gwI8nJILyR6atJr}y!R4sj4
z0vha$xnDcSwekH-b8P*gcT=a3wtJ0ktLX5pgEMESEsd}1TB^~Xr1aDuUR!@u{!T>I
zu`d%WQ7+xKR}0}O*29vgiJj){+ZBZxs=xb+Jh?$k4f_4>TSKh(5ZA`awM{m3`DHI$
z@E%|2=N$WhC{v({{;vQ91N!{C_aFsaeb@V5rdj}Ag~8WX
zy%g}PmZX5^N=gCO9|0fC0Pt*Vwbb{yqour8@s{!+ZKsGJE^MlJ{qYLyQbu%3p!a-c
zsA?owVb-=*B$#T&@b$Fr>hO$YqG?x(ko9CTp-p3N0Ga`D6!3>8&Xw*A!CeILEC>rPH&=*hiZxfZXUc!g*xPr5
z{HoDwMsFs8O(|j+aOV$&OH;rNB3u58?|N^WN49JlM7AtTxKk=z7TJ=^i)kok<0zS}|=}(X#3o(S0EO4xED_!4s{7oAa?%9!K
zsw-WCyTuVP9zr)eP!xVIF<#qH-*`X*U_}UbWT36FD;>m}DI#Jx9ty<6BA^aRfxuWo
zIsy+LKYG-f6!4LOWGd624wCJ%q9{h=lAaJjPCyw{I37zh)YmsAlgT%a7M}usbRdZk
z*N~`9N+S*$DL#iXV$mdur9uFu4gBXy07sWsz
fAPR8K>DT`OHwh56Fb7(<00000NkvXXu0mjfI-;Xb
literal 0
HcmV?d00001
diff --git a/picx/src/common/api/index.ts b/picx/src/common/api/index.ts
new file mode 100644
index 0000000..50005a2
--- /dev/null
+++ b/picx/src/common/api/index.ts
@@ -0,0 +1,82 @@
+import { computed } from 'vue'
+import axios from '@/utils/axios'
+import { store } from '@/store'
+import { getFileSuffix, isImage } from '@/utils/file-handle-helper'
+import structureImageObject from '@/utils/image-helper'
+
+const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
+
+/**
+ * 获取指定路径(path)下的目录列表
+ * @param path 路径
+ */
+export const getDirListByPath = (path: string = '') => {
+ return new Promise((resolve) => {
+ axios
+ .get(
+ `/repos/${userConfigInfo.owner}/${userConfigInfo.selectedRepos}/contents/${path}`,
+ {
+ params: {
+ ref: userConfigInfo.selectedBranch
+ }
+ }
+ )
+ .then((res: any) => {
+ if (res && res.status === 200 && res.data.length > 0) {
+ resolve(
+ res.data
+ .filter((v: any) => v.type === 'dir')
+ .map((x: any) => ({
+ value: x.name,
+ label: x.name
+ }))
+ )
+ } else {
+ resolve(null)
+ }
+ })
+ .catch(() => {
+ resolve(null)
+ })
+ })
+}
+
+/**
+ * 获取指定路径(path)下的目录和图片
+ * @param path
+ */
+export const getContentByReposPath = (path: string = '') => {
+ return new Promise((resolve) => {
+ axios
+ .get(
+ `/repos/${userConfigInfo.owner}/${userConfigInfo.selectedRepos}/contents/${path}`,
+ {
+ params: {
+ ref: userConfigInfo.selectedBranch
+ }
+ }
+ )
+ .then((res: any) => {
+ if (res && res.status === 200 && res.data.length > 0) {
+ res.data
+ .filter((v: any) => v.type === 'dir')
+ .forEach((x: any) => store.dispatch('DIR_IMAGE_LIST_ADD_DIR', x.path))
+
+ setTimeout(() => {
+ res.data
+ .filter((v: any) => v.type === 'file' && isImage(getFileSuffix(v.name)))
+ .forEach((x: any) =>
+ store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', structureImageObject(x, path))
+ )
+ }, 100)
+
+ resolve(true)
+ } else {
+ resolve(null)
+ }
+ })
+ .catch(() => {
+ resolve(null)
+ })
+ })
+}
diff --git a/picx/src/common/model/delete.model.ts b/picx/src/common/model/delete.model.ts
new file mode 100644
index 0000000..2a79dd6
--- /dev/null
+++ b/picx/src/common/model/delete.model.ts
@@ -0,0 +1,6 @@
+/* eslint-disable import/prefer-default-export */
+export enum deleteStatusEnum {
+ deleted = 'deleted',
+ allDeleted = 'allDeleted',
+ deleteFail = 'deleteFail'
+}
diff --git a/picx/src/common/model/dir.model.ts b/picx/src/common/model/dir.model.ts
new file mode 100644
index 0000000..40fac92
--- /dev/null
+++ b/picx/src/common/model/dir.model.ts
@@ -0,0 +1,11 @@
+export interface DirModel {
+ value: string
+ label: string
+}
+
+export enum DirModeEnum {
+ autoDir = 'autoDir',
+ newDir = 'newDir',
+ rootDir = 'rootDir',
+ reposDir = 'reposDir'
+}
diff --git a/picx/src/common/model/external-link.model.ts b/picx/src/common/model/external-link.model.ts
new file mode 100644
index 0000000..82e6c4c
--- /dev/null
+++ b/picx/src/common/model/external-link.model.ts
@@ -0,0 +1,8 @@
+enum ExternalLinkType {
+ staticaly = 'staticaly',
+ jsdelivr = 'jsdelivr',
+ github = 'github',
+ cloudflare = 'cloudflare'
+}
+
+export default ExternalLinkType
diff --git a/picx/src/common/model/storage.model.ts b/picx/src/common/model/storage.model.ts
new file mode 100644
index 0000000..7f7a94b
--- /dev/null
+++ b/picx/src/common/model/storage.model.ts
@@ -0,0 +1,6 @@
+const PICX_PREFIX = 'PICX_'
+
+export const PICX_CONFIG = `${PICX_PREFIX}CONFIG`
+export const PICX_UPLOADED = `${PICX_PREFIX}UPLOADED`
+export const PICX_MANAGEMENT = `${PICX_PREFIX}MANAGEMENT_MULTI`
+export const PICX_SETTINGS = `${PICX_PREFIX}SETTINGS`
diff --git a/picx/src/common/model/upload.model.ts b/picx/src/common/model/upload.model.ts
new file mode 100644
index 0000000..2b91a39
--- /dev/null
+++ b/picx/src/common/model/upload.model.ts
@@ -0,0 +1,68 @@
+export enum UploadStatusEnum {
+ // eslint-disable-next-line no-unused-vars
+ uploaded = 'uploaded',
+ // eslint-disable-next-line no-unused-vars
+ allUploaded = 'allUploaded',
+ // eslint-disable-next-line no-unused-vars
+ uploadFail = 'uploadFail'
+}
+
+export interface UploadedImageModel {
+ type: string
+ uuid: string
+ sha: string
+ dir: string
+ path: string
+ name: string
+ size: any
+ deleting: boolean
+ is_transform_md: boolean
+ checked: boolean
+ github_url: string
+ jsdelivr_cdn_url: string
+ staticaly_cdn_url: string
+ cloudflare_cdn_url: string
+}
+
+export interface ToUploadImageModel {
+ uuid: string
+
+ uploadStatus: {
+ progress: number
+ uploading: boolean
+ }
+
+ imgData: {
+ base64Content: string
+ base64Url: string
+ }
+
+ fileInfo: {
+ compressedSize?: number | undefined
+ originSize?: number | undefined
+ size: number | undefined
+ lastModified: number | undefined
+ }
+
+ filename: {
+ name: string
+ hash: string
+ suffix: string
+ prefixName: string
+ now: string
+ initName: string
+ newName: string
+ isHashRename: boolean
+ isRename: boolean
+ isPrefix: boolean
+ }
+
+ externalLink: {
+ github: string
+ jsdelivr: string
+ staticaly: string
+ cloudflare: string
+ }
+
+ uploadedImg?: UploadedImageModel
+}
diff --git a/picx/src/common/model/user-config-info.model.ts b/picx/src/common/model/user-config-info.model.ts
new file mode 100644
index 0000000..f11ed55
--- /dev/null
+++ b/picx/src/common/model/user-config-info.model.ts
@@ -0,0 +1,35 @@
+import { DirModeEnum, DirModel } from './dir.model'
+
+export interface ReposModel {
+ value: string
+ label: string
+ desc?: string
+}
+
+export interface BranchModel {
+ value: string
+ label: string
+}
+
+export enum BranchModeEnum {
+ newBranch = 'newBranch',
+ reposBranch = 'reposBranch'
+}
+
+export interface UserConfigInfoModel {
+ token: string
+ owner: string
+ email: string
+ name: string
+ avatarUrl: string
+ selectedRepos: string
+ reposList: ReposModel[]
+ selectedBranch: string
+ branchMode: BranchModeEnum
+ branchList: BranchModel[]
+ dirMode: DirModeEnum
+ selectedDir: string
+ selectedDirList: string[]
+ dirList: DirModel[]
+ loggingStatus: boolean
+}
diff --git a/picx/src/common/model/user-settings.model.ts b/picx/src/common/model/user-settings.model.ts
new file mode 100644
index 0000000..5d44ef4
--- /dev/null
+++ b/picx/src/common/model/user-settings.model.ts
@@ -0,0 +1,15 @@
+import { CompressEncoderMap } from '../../utils/compress'
+import ExternalLinkType from '@/common/model/external-link.model'
+
+export interface UserSettingsModel {
+ defaultHash: boolean
+ defaultMarkdown: boolean
+ defaultPrefix: boolean
+ prefixName: string
+ themeMode: 'auto' | 'light' | 'dark'
+ autoLightThemeTime: string[]
+ isCompress: boolean
+ compressEncoder: CompressEncoderMap
+ elementPlusSize: 'large' | 'default' | 'small'
+ externalLinkType: ExternalLinkType
+}
diff --git a/picx/src/common/model/vite-config.model.ts b/picx/src/common/model/vite-config.model.ts
new file mode 100644
index 0000000..19121e8
--- /dev/null
+++ b/picx/src/common/model/vite-config.model.ts
@@ -0,0 +1,11 @@
+export declare type Recordable = Record
+
+export declare interface ViteEnv {
+ VITE_PORT?: number
+ VITE_USE_PWA?: boolean
+ VITE_PUBLIC_PATH?: string
+ VITE_GLOB_APP_TITLE?: string
+ VITE_GLOB_APP_SHORT_NAME?: string
+ VITE_OPEN_BROWSER?: boolean
+ VITE_CORS?: boolean
+}
diff --git a/picx/src/components/copy-external-link/copy-external-link.styl b/picx/src/components/copy-external-link/copy-external-link.styl
new file mode 100644
index 0000000..f51c446
--- /dev/null
+++ b/picx/src/components/copy-external-link/copy-external-link.styl
@@ -0,0 +1,62 @@
+@import "../../style/base.styl"
+
+.copy-external-link-box {
+ position relative
+ width 100%
+ height 100%
+ box-sizing border-box
+ display flex
+ justify-content space-between
+ align-items flex-end
+ padding 2rem
+
+ .markdown-icon-box {
+ width 26rem
+ height 20rem
+ position relative
+ cursor pointer
+
+ .markdown-icon {
+ path {
+ fill var(--markdown-icon-color)
+ }
+
+ &.active {
+ path {
+ fill var(--markdown-icon-active-color)
+ }
+ }
+ }
+ }
+
+
+ .btn-box {
+ position relative
+ box-sizing border-box
+ height 100%
+ display flex
+ justify-content space-between
+
+ .btn-item {
+ height 20rem
+ box-sizing border-box
+ border-radius 5rem
+ font-size 12rem
+ cursor pointer
+ transition all 0.3s ease
+ }
+
+ .copy-url {
+ box-sizing border-box
+ border 1rem solid var(--default-text-color)
+ color var(--default-text-color)
+ padding 1rem 2rem
+ margin-right 4rem
+
+ &:hover {
+ background var(--default-text-color)
+ color var(--background-color)
+ }
+ }
+ }
+}
diff --git a/picx/src/components/copy-external-link/copy-external-link.vue b/picx/src/components/copy-external-link/copy-external-link.vue
new file mode 100644
index 0000000..edb4cfa
--- /dev/null
+++ b/picx/src/components/copy-external-link/copy-external-link.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+ Staticaly
+
+
+
+
+ Cloudflare
+
+
+
+
+
+
+
+
+
diff --git a/picx/src/components/folder-card/folder-card.styl b/picx/src/components/folder-card/folder-card.styl
new file mode 100644
index 0000000..b61ac4b
--- /dev/null
+++ b/picx/src/components/folder-card/folder-card.styl
@@ -0,0 +1,47 @@
+@import '../../style/base.styl'
+
+.folder-card {
+ position relative
+ width 110rem
+ height 106rem
+ display flex
+ align-items center
+ flex-direction column
+ justify-content flex-start
+ cursor pointer
+ box-sizing border-box
+ padding 3rem
+ user-select none
+
+ &:hover {
+ background var(--second-background-color)
+ }
+
+ .icon {
+ display flex
+ align-items center
+ justify-content center
+ width 50rem
+ height 50rem
+
+ svg {
+ width 100%
+ height 100%
+ }
+ }
+
+
+ .text {
+ width 90%
+ font-size 14rem
+ margin-top 5rem
+ text-align center
+ overflow hidden
+ text-overflow ellipsis
+ display -webkit-box
+ -webkit-box-orient vertical
+ -webkit-line-clamp 2
+ word-wrap break-word
+ word-break break-all
+ }
+}
diff --git a/picx/src/components/folder-card/folder-card.vue b/picx/src/components/folder-card/folder-card.vue
new file mode 100644
index 0000000..f332c32
--- /dev/null
+++ b/picx/src/components/folder-card/folder-card.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
{{ folderObj.dir }}
+
双击返回
+
+
+
+
+
+
diff --git a/picx/src/components/header-content/header-content.styl b/picx/src/components/header-content/header-content.styl
new file mode 100644
index 0000000..825efab
--- /dev/null
+++ b/picx/src/components/header-content/header-content.styl
@@ -0,0 +1,92 @@
+@import "../../style/base.styl"
+
+.header {
+ width 100%
+ height 100%
+ background var(--background-color)
+ padding 0 20rem
+ box-sizing border-box
+ display flex
+ justify-content space-between
+ align-items center
+
+ .header-left {
+ height 100%
+ display flex
+ justify-content flex-start
+
+ .brand {
+ height 100%
+ display flex
+ justify-content flex-start
+ align-items center
+ cursor pointer
+
+ .logo {
+ width 46rem
+ height 46rem
+ margin-right 10rem
+
+ img {
+ width 100%
+ }
+
+ }
+
+
+ .title {
+ font-size 36rem
+ font-weight bold
+ }
+ }
+
+
+ .website-count {
+ box-sizing border-box
+ display flex
+ align-items flex-end
+ font-size 14rem
+ margin-left 10rem
+ padding-bottom 12rem
+ cursor pointer
+
+ +picx-mobile() {
+ display none
+ }
+ }
+ }
+
+
+ .header-right {
+ .user-info {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+
+ .username {
+ font-size: 16rem;
+ }
+
+ .avatar {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 38rem;
+ height: 38rem;
+ color: var(--default-text-color);
+ border-radius: 50%;
+ border: 1rem solid var(--default-text-color);
+ margin-left: 10rem;
+ padding: 1rem;
+ box-sizing: border-box;
+
+ img {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ }
+ }
+
+ }
+ }
+}
diff --git a/picx/src/components/header-content/header-content.vue b/picx/src/components/header-content/header-content.vue
new file mode 100644
index 0000000..79dcd9c
--- /dev/null
+++ b/picx/src/components/header-content/header-content.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
diff --git a/picx/src/components/image-card/image-card.styl b/picx/src/components/image-card/image-card.styl
new file mode 100644
index 0000000..1db2ce8
--- /dev/null
+++ b/picx/src/components/image-card/image-card.styl
@@ -0,0 +1,142 @@
+@import "../../style/base.styl"
+
+$infoBoxHeight = 56rem
+
+.image-card {
+ position relative
+ width 100%
+ height 100%
+ box-shadow 1rem 2rem 3rem var(--shadow-color)
+ box-sizing border-box
+ padding-bottom $infoBoxHeight
+ user-select none
+
+ &.checked, &:hover {
+ box-shadow 0 0 10rem #666
+ }
+
+ &.listing {
+ display flex
+ justify-content flex-start
+ align-items center
+ padding 5rem
+ border-radius $box-border-radius
+
+ .image-box {
+ height 45rem
+ width 45rem
+ }
+
+ .info-box {
+ position relative
+ width 80%
+ }
+
+ :deep(.el-loading-mask) {
+ .el-loading-spinner {
+ margin-top -25rem
+
+ .circular {
+ height 24rem
+ width 24rem
+ }
+
+ .el-loading-text {
+ margin 0
+ }
+ }
+
+ }
+ }
+
+
+ .image-box {
+ position relative
+ width 100%
+ height 100%
+
+ img {
+ width 100%
+ height 100%
+ object-fit cover
+ cursor pointer
+ }
+ }
+
+
+ .info-box {
+ width 100%
+ height $infoBoxHeight
+ position absolute
+ bottom 0
+ left 0
+
+ .image-info {
+ width 100%
+ height 100%
+ padding 5rem
+ color var(--default-text-color)
+ box-sizing border-box
+ display flex
+ flex-direction column
+ justify-content space-between
+
+ .rename-input {
+ height 20rem
+ display flex
+ margin-bottom 4rem
+ }
+
+ .filename {
+ height 16rem
+ overflow hidden
+ text-overflow ellipsis
+ white-space nowrap
+ font-size 14rem
+ margin-bottom 6rem
+ }
+ }
+ }
+
+
+ .operation-box {
+ position absolute
+ top 10rem
+ right 8rem
+ width calc(100% - 16rem)
+ display flex
+ justify-content space-between
+
+ .operation-left {
+ .picked-btn {
+ i {
+ font-weight bold
+ }
+ }
+ }
+
+ .operation-right {
+ display flex
+ }
+
+ .operation-btn {
+ width 32rem
+ height 32rem
+ border-radius 50%
+ box-shadow 0 0 6rem #555
+ cursor pointer
+ background var(--background-color)
+ display flex
+ justify-content center
+ align-items center
+ margin-right 8rem
+ font-size 18rem
+
+ &:last-child {
+ margin-right 0
+ }
+ }
+ }
+
+
+}
diff --git a/picx/src/components/image-card/image-card.vue b/picx/src/components/image-card/image-card.vue
new file mode 100644
index 0000000..4df449a
--- /dev/null
+++ b/picx/src/components/image-card/image-card.vue
@@ -0,0 +1,287 @@
+
+
+
+
![]()
+
+
+
+
+
{{ imageObj.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+ 重命名
+
+
+ 属性
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/picx/src/components/image-selector/image-selector.styl b/picx/src/components/image-selector/image-selector.styl
new file mode 100644
index 0000000..2a3a18c
--- /dev/null
+++ b/picx/src/components/image-selector/image-selector.styl
@@ -0,0 +1,52 @@
+@import "../../style/base.styl"
+
+.selector-wrapper {
+ padding 4rem 12rem
+ width 100%
+ box-sizing border-box
+ display flex
+ justify-content space-between
+ align-items center
+ border-bottom 1rem solid var(--third-background-color)
+
+
+ .selector-left-box {
+ display flex
+ align-items center
+
+ :deep(.el-checkbox) {
+ font-weight unset
+ }
+
+ :deep(.el-checkbox__label ) {
+ line-height unset
+ }
+
+ .cancel-select-btn {
+ color #576b95
+ cursor pointer
+ }
+
+ div.item {
+ margin-left 8rem
+ }
+ }
+
+ .selector-right-box {
+ .btn-icon {
+ cursor: pointer;
+ font-size: 22rem;
+ margin-left: 10rem;
+ }
+
+ }
+}
+
+.temp-batch-externalink {
+ opacity 0
+ position absolute
+ left -9999rem
+ top -9999rem
+ width 0
+ height 0
+}
diff --git a/picx/src/components/image-selector/image-selector.vue b/picx/src/components/image-selector/image-selector.vue
new file mode 100644
index 0000000..45162f3
--- /dev/null
+++ b/picx/src/components/image-selector/image-selector.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
已选择 {{ getImageCardCheckedNum }} 张图片
+
取消选择
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/picx/src/components/image-viewer/image-viewer.styl b/picx/src/components/image-viewer/image-viewer.styl
new file mode 100644
index 0000000..7f88182
--- /dev/null
+++ b/picx/src/components/image-viewer/image-viewer.styl
@@ -0,0 +1,75 @@
+$transition-duration = 0.3s
+$transition-delay = 0s
+
+.image-viewer {
+ position fixed
+ left 0
+ top 0
+ width 100%
+ height 100%
+ display flex
+ align-items center
+ justify-content center
+ background rgba(0, 0, 0, 0)
+ visibility hidden
+ z-index 1000
+ padding 6%
+ box-sizing border-box
+ transition-property visibility, background
+ transition-delay $transition-delay, $transition-delay
+ transition-duration $transition-duration, $transition-duration
+ transition-timing-function ease, ease
+
+ &.active {
+ background rgba(0, 0, 0, 0.6)
+ visibility visible
+
+ .image-box {
+ transform scale(1)
+ padding 2rem
+
+ .image-info {
+ display block
+ }
+ }
+
+
+ }
+
+
+ .image-box {
+ position relative;
+ width 60%
+ height 100%
+ display flex
+ flex-direction column
+ justify-content center
+ align-items center
+ transform scale(0)
+ transition-property transform
+ transition-delay $transition-delay
+ transition-duration $transition-duration
+ transition-timing-function ease
+
+
+ @media (max-width: 1200px) {
+ width 80%
+ }
+
+ .img {
+ cursor zoom-out
+ max-width 100%
+ max-height 100%
+ }
+
+ .image-info {
+ display none
+ padding 10rem
+
+ .item {
+ margin 0 6rem
+ }
+ }
+
+ }
+}
diff --git a/picx/src/components/image-viewer/image-viewer.vue b/picx/src/components/image-viewer/image-viewer.vue
new file mode 100644
index 0000000..a2512ca
--- /dev/null
+++ b/picx/src/components/image-viewer/image-viewer.vue
@@ -0,0 +1,44 @@
+
+
+
+
![]()
+
+
+ 图片名:{{ imageViewer.imgInfo.name }}
+
+
+ 图片大小:{{ parseFileSize(imageViewer.imgInfo.size) }}
+
+
+ 最后修改时间:{{ formatLastModified(imageViewer.imgInfo.lastModified) }}
+
+
+
+
+
+
+
+
+
diff --git a/picx/src/components/main-container/main-container.styl b/picx/src/components/main-container/main-container.styl
new file mode 100644
index 0000000..523c84e
--- /dev/null
+++ b/picx/src/components/main-container/main-container.styl
@@ -0,0 +1,73 @@
+@import "../../style/base.styl"
+
+$top-height = 60rem
+$left-side-width = 80rem
+
+.main-container {
+ position absolute
+ box-sizing border-box
+ width 100%
+ height 100%
+ background var(--second-background-color)
+ padding-top $top-height
+ font-size 15rem
+
+
+
+ .top {
+ position absolute
+ top 0
+ left 0
+ box-sizing border-box
+ width 100%
+ height $top-height
+ }
+
+
+ .bottom {
+ position relative
+ box-sizing border-box
+ width 100%
+ height 100%
+ padding-top $component-interval
+
+
+ .container {
+ position relative
+ box-sizing border-box
+ width 100%
+ height 100%
+ padding-left $left-side-width
+
+
+ .left {
+ position absolute
+ box-sizing border-box
+ width $left-side-width
+ height 100%
+ top 0
+ left 0
+ }
+
+
+ .right {
+ position relative
+ width 100%
+ height 100%
+ box-sizing border-box
+ padding 0 $component-interval 0 $component-interval
+
+
+ .content {
+ position relative
+ box-sizing border-box
+ width 100%
+ height 100%
+ }
+ }
+
+ }
+
+
+ }
+}
diff --git a/picx/src/components/main-container/main-container.vue b/picx/src/components/main-container/main-container.vue
new file mode 100644
index 0000000..088f3f4
--- /dev/null
+++ b/picx/src/components/main-container/main-container.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/picx/src/components/nav-content/nav-content.styl b/picx/src/components/nav-content/nav-content.styl
new file mode 100644
index 0000000..6cbcac4
--- /dev/null
+++ b/picx/src/components/nav-content/nav-content.styl
@@ -0,0 +1,40 @@
+@import "../../style/base.styl"
+
+.nav {
+ position relative
+ width 100%
+ height 100%
+ box-sizing border-box
+ background var(--background-color)
+
+ ul.nav-list {
+ padding 0
+ margin 0
+
+ li.nav-item {
+ box-sizing border-box
+ width 100%
+ height 76rem
+ cursor pointer
+
+ &.active {
+ font-weight bold
+ background var(--second-background-color)
+ }
+
+ .nav-content {
+ display flex
+ flex-direction column
+ justify-content center
+ align-items center
+
+ .nav-name {
+ margin-top 5rem
+ font-size 12rem
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/picx/src/components/nav-content/nav-content.vue b/picx/src/components/nav-content/nav-content.vue
new file mode 100644
index 0000000..10dc70e
--- /dev/null
+++ b/picx/src/components/nav-content/nav-content.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
diff --git a/picx/src/components/selected-info-bar/selected-info-bar.styl b/picx/src/components/selected-info-bar/selected-info-bar.styl
new file mode 100644
index 0000000..eb3f16e
--- /dev/null
+++ b/picx/src/components/selected-info-bar/selected-info-bar.styl
@@ -0,0 +1,16 @@
+.selected-info-bar-box {
+ height 100%
+ display flex
+ align-items center
+ justify-content flex-start
+ font-size 12rem
+ box-sizing border-box
+
+ .info-item {
+ margin-right 8rem
+
+ &:last-child {
+ margin-right 0
+ }
+ }
+}
diff --git a/picx/src/components/selected-info-bar/selected-info-bar.vue b/picx/src/components/selected-info-bar/selected-info-bar.vue
new file mode 100644
index 0000000..0fbaabb
--- /dev/null
+++ b/picx/src/components/selected-info-bar/selected-info-bar.vue
@@ -0,0 +1,35 @@
+
+
+
+ 仓库:
+
+ {{ userConfigInfo.selectedRepos }}
+
+
+
+ 分支:
+
+ {{ userConfigInfo.selectedBranch }}
+
+
+
+ 目录:
+
+ {{ userConfigInfo.selectedDir }}
+
+
+
+
+
+
+
+
diff --git a/picx/src/components/site-count/site-count.vue b/picx/src/components/site-count/site-count.vue
new file mode 100644
index 0000000..685b6ac
--- /dev/null
+++ b/picx/src/components/site-count/site-count.vue
@@ -0,0 +1,58 @@
+
+
+ 超过
+
+
+ 次被使用
+
+
+
+
+
diff --git a/picx/src/components/to-upload-image-card/to-upload-image-card.styl b/picx/src/components/to-upload-image-card/to-upload-image-card.styl
new file mode 100644
index 0000000..10c8d52
--- /dev/null
+++ b/picx/src/components/to-upload-image-card/to-upload-image-card.styl
@@ -0,0 +1,211 @@
+@import "../../style/base.styl"
+
+$info-item-height = 68rem
+$info-item-border = 1rem
+$info-item-padding = 5rem
+$compressed-file-background-color = #228eff
+$image-width = $info-item-height - ($info-item-border * 2)
+
+.to-upload-image-list-card {
+ position relative
+ width 100%
+ box-sizing border-box
+ margin-top 6rem
+
+ .header {
+ width 100%
+ height 30rem
+ box-sizing border-box
+ font-size 12rem
+ display flex
+ align-items center
+ justify-content space-between
+ padding-bottom 6rem
+ }
+
+ .body {
+ width 100%
+ height 100%
+ max-height 170rem
+ overflow-y auto
+ box-sizing border-box
+ padding 10rem
+ border 1rem solid var(--border-color)
+ margin-top 10rem
+
+ &::-webkit-scrollbar {
+ width 5rem
+ }
+
+ &::-webkit-scrollbar-thumb {
+ border-radius 2rem
+ }
+
+ .image-uploading-info-box {
+ position relative
+ width 100%
+ box-sizing border-box
+ padding 0
+ margin 0
+
+ .image-uploading-info-item {
+ position relative
+ box-sizing border-box
+ width 100%
+ height $info-item-height
+ border $info-item-border solid var(--border-color)
+ border-radius 5rem
+ margin-bottom 10rem
+ overflow hidden
+ font-size 15rem
+ padding-left $image-width
+ transition all 0.3s ease
+
+ &.disable {
+ pointer-events none
+ cursor not-allowed
+ }
+
+ &:last-child {
+ margin-bottom 0
+ }
+
+ &:hover {
+ box-shadow 0 0 5rem var(--shadow-hover-color)
+ }
+
+ .left-image-box {
+ position absolute
+ top 0
+ left 0
+ width $image-width
+ height 100%
+ box-sizing border-box
+ margin-right 5rem
+
+ img {
+ object-fit cover
+ width 100%
+ height 100%
+ overflow hidden
+ cursor pointer
+ border-top-left-radius 5rem
+ border-bottom-left-radius 5rem
+ }
+ }
+
+
+ .right-operation-box {
+ position relative
+ width 100%
+ height 100%
+ box-sizing border-box
+ padding $info-item-padding 20rem $info-item-padding $info-item-padding
+
+ .top, .bottom {
+ width 100%
+ height 50%
+ box-sizing border-box
+ padding 0 5rem
+ }
+
+ .top {
+ display flex
+ justify-content space-between
+
+ .image-name,
+ .image-info {
+ display flex
+ align-items center
+ box-sizing border-box
+ height 100%
+ }
+
+ .image-name {
+ font-size 13rem
+ overflow hidden
+ text-overflow ellipsis
+ white-space nowrap
+ }
+
+ .image-info {
+ font-size 12rem
+
+ .item {
+ padding 1rem 4rem
+ background var(--third-background-color)
+ border-radius 2rem
+ margin-left 10rem
+
+ &.compressed {
+ color $compressed-file-background-color
+ }
+ }
+ }
+ }
+
+
+ .bottom {
+ display flex
+ align-items center
+
+ &.rename-operation {
+
+ .el-checkbox {
+ margin-right 20rem
+ }
+
+ .rename-input {
+ input {
+ height 23rem
+ line-height 23rem
+ }
+ }
+ }
+ }
+ }
+
+
+ .upload-status-box {
+ box-sizing border-box
+ color #fff
+ position absolute
+ right -17rem
+ top -7rem
+ width 46rem
+ height 26rem
+ text-align center
+ transform rotate(45deg)
+ box-shadow 0 1rem 1rem var(--border-color)
+
+ &.wait-upload {
+ background var(--await-upload-color)
+ }
+
+ &.uploading {
+ background var(--uploading-color)
+ }
+
+ &.uploaded {
+ background var(--uploaded-color)
+ }
+
+ i {
+ font-size 12rem
+ margin-top 12rem
+ transform rotate(-45deg)
+ }
+ }
+
+ .remove-to-upload-image {
+ position absolute
+ bottom 5rem
+ right 5rem
+ cursor pointer
+ }
+
+ }
+ }
+
+ }
+}
diff --git a/picx/src/components/to-upload-image-card/to-upload-image-card.vue b/picx/src/components/to-upload-image-card/to-upload-image-card.vue
new file mode 100644
index 0000000..99d6614
--- /dev/null
+++ b/picx/src/components/to-upload-image-card/to-upload-image-card.vue
@@ -0,0 +1,300 @@
+
+
+
+
+
+
+ -
+
+
![]()
+
+
+
+
+
+ {{ imgItem.filename.now }}
+
+
+
+
+ {{ getFileSize(imgItem.fileInfo.originSize) }}
+
+
+
+
+ {{ getFileSize(imgItem.fileInfo.size) }}
+
+
+
+ {{ formatLastModified(imgItem.fileInfo.lastModified) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/picx/src/components/tutorials-step/step1.vue b/picx/src/components/tutorials-step/step1.vue
new file mode 100644
index 0000000..af8e1ac
--- /dev/null
+++ b/picx/src/components/tutorials-step/step1.vue
@@ -0,0 +1,40 @@
+
+
+
+ 创建一个用来存储图片的
+ GitHub 仓库
+
+

+
+
+
+
+
+
diff --git a/picx/src/components/tutorials-step/step2.vue b/picx/src/components/tutorials-step/step2.vue
new file mode 100644
index 0000000..924264b
--- /dev/null
+++ b/picx/src/components/tutorials-step/step2.vue
@@ -0,0 +1,54 @@
+
+
+
+ 创建一个有 repo 权限的
+ GitHub Token
+
+

+
然后点击 Generate token 按钮,即可生成一个token,如下图:
+

+
+ 新生成的 Token 只会显示一次,请妥善保存!如有遗失,重新生成即可。
+
+
+
+
+
+
+
diff --git a/picx/src/components/tutorials-step/step3.vue b/picx/src/components/tutorials-step/step3.vue
new file mode 100644
index 0000000..ff9acd5
--- /dev/null
+++ b/picx/src/components/tutorials-step/step3.vue
@@ -0,0 +1,62 @@
+
+
+
进行图床配置(绑定 GitHub Token、存储图片的仓库和目录)
+
1、填写 Token,自动获取该用户下的仓库
+

+
+
2、在仓库的下拉列表中,选择一个作为图床的仓库
+

+
+
3、选择一种目录方式(目录即仓库里存放图片的文件夹)
+

+
+
+
+ - 新建目录:需手动输入一个新目录。
+ - 根目录:图片将直接存储在仓库根目录下。
+ - 自动目录:自动生成日期格式 YYYYMMDD 的目录。例如:20200909
+ - 选择仓库目录:自动获取仓库下所有目录,选择一个即可。
+
+
+
+
+
+
diff --git a/picx/src/components/upload-area/upload-area.styl b/picx/src/components/upload-area/upload-area.styl
new file mode 100644
index 0000000..54c2900
--- /dev/null
+++ b/picx/src/components/upload-area/upload-area.styl
@@ -0,0 +1,57 @@
+@import "../../style/base.styl"
+
+.upload-area {
+ position: relative;
+ width: 100%;
+ height: 300rem;
+ border: 4rem dashed var(--third-text-color)
+ box-sizing border-box
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 999;
+
+ &.focus {
+ border-color: var(--upload-area-focus-color);
+ }
+
+ &:hover {
+ border-color: var(--upload-area-focus-color);
+ }
+
+ label {
+ display: block;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 1000;
+ cursor: pointer;
+ }
+
+ input[type="file"] {
+ position: absolute;
+ left: -9999rem;
+ top: -9999rem;
+ }
+
+ .tips {
+ text-align: center;
+ color: #aaa;
+
+ .icon {
+ font-size: 100rem;
+ }
+
+ .text {
+ cursor: default;
+ font-size: 20rem;
+ }
+ }
+
+ img {
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+ }
+
+}
diff --git a/picx/src/components/upload-area/upload-area.vue b/picx/src/components/upload-area/upload-area.vue
new file mode 100644
index 0000000..2169abf
--- /dev/null
+++ b/picx/src/components/upload-area/upload-area.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
![Pictures to be uploaded]()
+
+
+
+
+
+
diff --git a/picx/src/main.ts b/picx/src/main.ts
new file mode 100644
index 0000000..aac61c5
--- /dev/null
+++ b/picx/src/main.ts
@@ -0,0 +1,21 @@
+import { createApp } from 'vue'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import router from '@/router/index'
+import { key, store } from '@/store'
+import App from './App.vue'
+import 'element-plus/theme-chalk/dark/css-vars.css'
+
+if (import.meta.env.MODE === 'production') {
+ // @ts-ignore
+ import('@/utils/register-sw.ts')
+}
+
+const app = createApp(App)
+
+// import element-plus icons
+// eslint-disable-next-line no-restricted-syntax
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+ app.component(key, component)
+}
+// @ts-ignore
+app.use(router).use(store, key).mount('#app')
diff --git a/picx/src/plugins/index.ts b/picx/src/plugins/index.ts
new file mode 100644
index 0000000..b9848df
--- /dev/null
+++ b/picx/src/plugins/index.ts
@@ -0,0 +1,32 @@
+import type { Plugin } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+import { ViteEnv } from '@/common/model/vite-config.model'
+
+import configPWAPlugin from './pwa'
+
+export default function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
+ const vitePlugins: (Plugin | Plugin[])[] = [vue()]
+
+ // On-demand import style for Element Plus
+ vitePlugins.push(
+ AutoImport({
+ resolvers: [ElementPlusResolver()]
+ }),
+ Components({
+ resolvers: [ElementPlusResolver()]
+ })
+ )
+
+ // production env
+ if (isBuild) {
+ // add plugin vite-plugin-pwa
+ if (viteEnv.VITE_USE_PWA) {
+ vitePlugins.push(configPWAPlugin(viteEnv))
+ }
+ }
+
+ return vitePlugins
+}
diff --git a/picx/src/plugins/pwa.ts b/picx/src/plugins/pwa.ts
new file mode 100644
index 0000000..9d89758
--- /dev/null
+++ b/picx/src/plugins/pwa.ts
@@ -0,0 +1,24 @@
+/**
+ * Zero config PWA for Vite
+ * Plugin: vite-plugin-pwa
+ * https://github.com/antfu/vite-plugin-pwa
+ */
+import { VitePWA } from 'vite-plugin-pwa'
+import { ViteEnv } from '@/common/model/vite-config.model'
+
+export default function configPWAPlugin(env: ViteEnv) {
+ return VitePWA({
+ registerType: 'autoUpdate',
+ manifest: {
+ name: env.VITE_GLOB_APP_TITLE,
+ short_name: env.VITE_GLOB_APP_SHORT_NAME,
+ icons: [
+ {
+ src: './logo@192x192.png',
+ sizes: '192x192',
+ type: 'image/png'
+ }
+ ]
+ }
+ })
+}
diff --git a/picx/src/router/index.ts b/picx/src/router/index.ts
new file mode 100644
index 0000000..ef83833
--- /dev/null
+++ b/picx/src/router/index.ts
@@ -0,0 +1,82 @@
+import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
+import config from '@/views/config/config.vue'
+import upload from '@/views/upload/upload.vue'
+import management from '@/views/management/management.vue'
+import tutorials from '@/views/tutorials/tutorials.vue'
+import settings from '@/views/settings/settings.vue'
+import { store } from '@/store'
+
+const titleSuffix = ` | PicX 图床神器`
+
+const routes: Array = [
+ {
+ path: '/',
+ name: 'index',
+ redirect: {
+ name: 'upload'
+ }
+ },
+ {
+ path: '/config',
+ name: 'config',
+ component: config,
+ meta: {
+ title: `图床配置${titleSuffix}`
+ }
+ },
+ {
+ path: '/upload',
+ name: 'upload',
+ component: upload,
+ meta: {
+ title: `图片上传${titleSuffix}`
+ }
+ },
+ {
+ path: '/management',
+ name: 'Management',
+ component: management,
+ meta: {
+ title: `图床管理${titleSuffix}`
+ }
+ },
+ {
+ path: '/tutorials',
+ name: 'tutorials',
+ component: tutorials,
+ meta: {
+ title: `使用教程${titleSuffix}`
+ }
+ },
+ {
+ path: '/about',
+ name: 'about',
+ component: () => import('@/views/about/about.vue'),
+ meta: {
+ title: `帮助反馈${titleSuffix}`
+ }
+ },
+ {
+ path: '/settings',
+ name: 'settings',
+ component: settings,
+ meta: {
+ title: `我的设置${titleSuffix}`
+ }
+ }
+]
+
+const router = createRouter({
+ history: createWebHashHistory(),
+ routes
+})
+
+router.beforeEach((to, from, next) => {
+ if (to.meta.title) (window).document.title = to.meta.title
+ if (from.path === '/management') {
+ store.dispatch('USER_CONFIG_INFO_RESET')
+ }
+ next()
+})
+
+export default router
diff --git a/picx/src/shims-vue.d.ts b/picx/src/shims-vue.d.ts
new file mode 100644
index 0000000..d53ab3d
--- /dev/null
+++ b/picx/src/shims-vue.d.ts
@@ -0,0 +1,6 @@
+declare module '*.vue' {
+ import { DefineComponent } from 'vue'
+
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/picx/src/store/index.ts b/picx/src/store/index.ts
new file mode 100644
index 0000000..5209066
--- /dev/null
+++ b/picx/src/store/index.ts
@@ -0,0 +1,57 @@
+import { InjectionKey } from 'vue'
+import { createStore, Store, useStore as baseUseStore } from 'vuex'
+import RootStateTypes, { AllStateTypes } from './types'
+import dirImageListModule from './modules/dir-image-list'
+import toUploadImageModule from './modules/to-upload-image'
+import uploadedImageListModule from './modules/uploaded-image-list'
+import userConfigInfoModule from './modules/user-config-info'
+import imageViewerModule from './modules/image-viewer'
+import imageCardModule from './modules/image-card'
+import uploadAreaActiveModule from './modules/upload-area-active'
+import uploadSettingsModule from './modules/upload-settings'
+import userSettingsModule from './modules/user-settings'
+
+// Create a new store instance.
+export const store = createStore({
+ modules: {
+ dirImageListModule,
+ toUploadImageModule,
+ uploadedImageListModule,
+ userConfigInfoModule,
+ imageViewerModule,
+ imageCardModule,
+ uploadAreaActiveModule,
+ uploadSettingsModule,
+ userSettingsModule
+ },
+
+ state: {
+ rootName: 'root'
+ },
+
+ mutations: {},
+
+ actions: {
+ // 退出登录(删除 localStorage 和 sessionStorage 数据,清空 state 的值)
+ LOGOUT({ dispatch, commit }) {
+ dispatch('DIR_IMAGE_LOGOUT')
+ dispatch('TO_UPLOAD_IMAGE_LOGOUT')
+ dispatch('UPLOADED_LIST_LOGOUT')
+ dispatch('USER_CONFIG_INFO_LOGOUT')
+ commit('IMAGE_VIEWER_LOGOUT')
+ commit('UPLOAD_AREA_ACTIVE_LOGOUT')
+ commit('UPLOAD_SETTINGS_LOGOUT')
+ dispatch('USER_SETTINGS_LOGOUT')
+ localStorage.clear()
+ sessionStorage.clear()
+ }
+ },
+
+ getters: {}
+})
+
+export const key: InjectionKey> = Symbol('vuex-store')
+
+export function useStore() {
+ return baseUseStore(key)
+}
diff --git a/picx/src/store/modules/dir-image-list/index.ts b/picx/src/store/modules/dir-image-list/index.ts
new file mode 100644
index 0000000..d482d6c
--- /dev/null
+++ b/picx/src/store/modules/dir-image-list/index.ts
@@ -0,0 +1,260 @@
+import { Module } from 'vuex'
+import { PICX_MANAGEMENT } from '@/common/model/storage.model'
+import DirImageListStateTypes, { DirObject } from './types'
+import RootStateTypes from '../../types'
+import {
+ createDirObject,
+ getUpLevelDirList,
+ getUpOneLevelDir
+} from '@/store/modules/dir-image-list/utils'
+import { UploadedImageModel } from '@/common/model/upload.model'
+import { getDirContent } from '@/views/management/management.util'
+
+const initDirObject = () => {
+ const dirObj = localStorage.getItem(PICX_MANAGEMENT)
+ return dirObj ? JSON.parse(dirObj) : createDirObject('/', '/')
+}
+
+const dirImageListModule: Module = {
+ state: {
+ name: 'dirImageListModule',
+ dirObject: initDirObject()
+ },
+
+ mutations: {},
+
+ actions: {
+ // 图床管理 - 增加目录
+ DIR_IMAGE_LIST_ADD_DIR({ state, dispatch }, dirPath: string) {
+ if (dirPath === '/') {
+ return
+ }
+
+ const findAssign = (dirObj: DirObject, dir: string, dirPath: string) => {
+ if (dirObj) {
+ if (!dirObj.childrenDirs.some((v: DirObject) => v.dir === dir)) {
+ dirObj.childrenDirs.push(createDirObject(dir, dirPath))
+ }
+ const temp = dirObj.childrenDirs.find((x: DirObject) => x.dir === dir)
+ return temp || createDirObject(dir, dirPath)
+ }
+ return createDirObject(dir, dirPath)
+ }
+
+ const dirList: string[] = dirPath.split('/')
+ let dirPathC = ''
+ let tempDirObj: DirObject = state.dirObject
+
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0, len = dirList.length; i < len; i++) {
+ const dirName = dirList[i]
+ dirPathC += `${i > 0 ? '/' : ''}${dirName}`
+ tempDirObj = findAssign(tempDirObj, dirName, dirPathC)
+
+ if (i === 0) {
+ dispatch('USER_CONFIG_INFO_ADD_DIR', dirName)
+ }
+ }
+
+ dispatch('DIR_IMAGE_LIST_PERSIST')
+ },
+
+ // 图床管理 - 删除目录
+ DIR_IMAGE_LIST_REMOVE_DIR({ state, dispatch }, dirPath: string) {
+ if (dirPath === '/') {
+ return
+ }
+
+ const rmDir = (dirObj: DirObject, dir: string, isRm: boolean) => {
+ if (dir === '/') {
+ return state.dirObject
+ }
+
+ const temp = dirObj.childrenDirs.find((v) => v.dir === dir)
+ if (!temp) {
+ return dirObj
+ }
+
+ if (isRm) {
+ const rmIndex = dirObj.childrenDirs.findIndex((v: any) => v.dir === dir)
+ if (rmIndex !== -1) {
+ dirObj.childrenDirs.splice(rmIndex, 1)
+ }
+ }
+
+ return temp
+ }
+
+ const dirList = dirPath.split('/')
+
+ let tempDirObj = state.dirObject
+ dirList.forEach((d, i) => {
+ tempDirObj = rmDir(tempDirObj, d, i === dirList.length - 1)
+
+ // 删除在用户配置信息模块里的目录项
+ if (i === 0) {
+ dispatch('USER_CONFIG_INFO_REMOVE_DIR', d)
+ }
+ })
+
+ dispatch('DIR_IMAGE_LIST_PERSIST')
+ },
+
+ // 图床管理 - 增加图片
+ DIR_IMAGE_LIST_ADD_IMAGE({ state, dispatch }, item: UploadedImageModel) {
+ const addImg = (
+ dirObj: DirObject,
+ dir: string,
+ Img: UploadedImageModel,
+ isAdd: boolean = false
+ ) => {
+ if (!dirObj) {
+ return state.dirObject
+ }
+
+ const temp = dirObj.childrenDirs?.find((x: DirObject) => x.dir === dir)
+ if (!temp) {
+ return state.dirObject
+ }
+
+ if (isAdd && !temp.imageList.some((v) => v.name === Img.name)) {
+ temp.imageList.push(Img)
+ }
+
+ return temp
+ }
+
+ let tempDirObj: DirObject = state.dirObject
+
+ if (item.dir === '/') {
+ if (!tempDirObj.imageList.some((v) => v.name === item.name)) {
+ tempDirObj.imageList.push(item)
+ }
+ } else {
+ const dirList: string[] = item.dir.split('/')
+ dirList.forEach((dir, i) => {
+ tempDirObj = addImg(tempDirObj, dir, item, i === dirList.length - 1)
+ })
+ }
+ dispatch('DIR_IMAGE_LIST_PERSIST')
+ },
+
+ // 图床管理 - 删除图片(即删除指定目录里的指定图片)
+ DIR_IMAGE_LIST_REMOVE({ state, dispatch }, item: any) {
+ // 删除
+ const rm = (list: UploadedImageModel[], uuid: string) => {
+ if (list.length) {
+ const rmIndex = list.findIndex((v: any) => v.uuid === uuid)
+ if (rmIndex !== -1) {
+ list.splice(rmIndex, 1)
+ }
+ }
+ }
+
+ // 删除图片
+ const rmImg = (
+ dirObj: DirObject,
+ dir: string,
+ img: UploadedImageModel,
+ isRm: boolean
+ ) => {
+ if (!dirObj) {
+ return state.dirObject
+ }
+
+ const temp = dirObj.childrenDirs.find((x: DirObject) => x.dir === dir)
+ if (!temp) {
+ return state.dirObject
+ }
+
+ if (temp.dir === dir && isRm) {
+ rm(temp.imageList, img.uuid)
+ }
+
+ return temp
+ }
+
+ const { dir, uuid } = item
+
+ if (dir === '/') {
+ rm(state.dirObject.imageList, uuid)
+ dispatch('DIR_IMAGE_LIST_PERSIST')
+ return
+ }
+
+ const dirList: string[] = dir.split('/')
+ let tempDirObj: DirObject = state.dirObject
+
+ dirList.forEach((d, i) => {
+ tempDirObj = rmImg(tempDirObj, d, item, i === dirList.length - 1)
+ if (!tempDirObj.imageList.length && !tempDirObj.childrenDirs.length) {
+ const dirPathList = getUpLevelDirList(tempDirObj.dirPath)
+
+ // 循环遍历判断上一级目录的内容是否为空,为空则删除,依次往上查找,直到根目录
+ dirPathList.forEach((dp) => {
+ const dpc = getDirContent(dp, state.dirObject)
+ if (dpc && !dpc.imageList.length && !dpc.childrenDirs.length) {
+ const { dirPath } = getUpOneLevelDir(dp)
+ dispatch('SET_USER_CONFIG_INFO', { selectedDir: dirPath })
+ dispatch('DIR_IMAGE_LIST_REMOVE_DIR', dp)
+ }
+ })
+ }
+ })
+ },
+
+ // 图床管理 - 初始化指定目录(即删除指定目录的子目录列表和图片列表) -- OK
+ DIR_IMAGE_LIST_INIT_DIR({ state, dispatch }, dirPath: string) {
+ let tempDirObj = state.dirObject
+
+ if (dirPath === '/') {
+ tempDirObj.imageList = []
+ tempDirObj.childrenDirs = []
+ dispatch('DIR_IMAGE_LIST_PERSIST')
+ return
+ }
+
+ const initDirObject = (dirObj: DirObject, dir: string, isInit: boolean) => {
+ if (!dirObj) {
+ return state.dirObject
+ }
+
+ const temp = dirObj.childrenDirs?.find((x: DirObject) => x.dir === dir)
+ if (!temp) {
+ return state.dirObject
+ }
+
+ if (isInit) {
+ temp.imageList = []
+ temp.childrenDirs = []
+ }
+
+ return temp
+ }
+
+ const dirList = dirPath.split('/')
+
+ dirList.forEach((d, i) => {
+ tempDirObj = initDirObject(tempDirObj, d, i === dirList.length - 1)
+ })
+
+ dispatch('DIR_IMAGE_LIST_PERSIST')
+ },
+
+ // 图床管理 - 持久化存储 -- OK
+ DIR_IMAGE_LIST_PERSIST({ state }) {
+ localStorage.setItem(PICX_MANAGEMENT, JSON.stringify(state.dirObject))
+ },
+
+ // 图床管理 - 退出登录
+ DIR_IMAGE_LOGOUT({ state }) {
+ state.dirObject = createDirObject('/', '/')
+ }
+ },
+
+ getters: {
+ getDirObject: (state: any) => state.dirObject
+ }
+}
+
+export default dirImageListModule
diff --git a/picx/src/store/modules/dir-image-list/types.ts b/picx/src/store/modules/dir-image-list/types.ts
new file mode 100644
index 0000000..fdda470
--- /dev/null
+++ b/picx/src/store/modules/dir-image-list/types.ts
@@ -0,0 +1,14 @@
+import { UploadedImageModel } from '@/common/model/upload.model'
+
+export interface DirObject {
+ type: 'dir'
+ dir: string
+ dirPath: string
+ childrenDirs: DirObject[]
+ imageList: UploadedImageModel[]
+}
+
+export default interface DirImageListStateTypes {
+ name: string
+ dirObject: DirObject
+}
diff --git a/picx/src/store/modules/dir-image-list/utils.ts b/picx/src/store/modules/dir-image-list/utils.ts
new file mode 100644
index 0000000..3d4694d
--- /dev/null
+++ b/picx/src/store/modules/dir-image-list/utils.ts
@@ -0,0 +1,73 @@
+import { DirObject } from '@/store/modules/dir-image-list/types'
+
+/**
+ * 构造一个新的目录对象
+ * @param dir
+ * @param dirPath
+ */
+export const createDirObject = (dir: string, dirPath: string): DirObject => {
+ return {
+ type: 'dir',
+ dir,
+ dirPath,
+ childrenDirs: [],
+ imageList: []
+ }
+}
+
+/**
+ * 获取上一级目录
+ * @param dirPath
+ */
+export const getUpOneLevelDir = (dirPath: string) => {
+ if (dirPath === '/') {
+ return {
+ currentDir: '/',
+ dirPath: '/'
+ }
+ }
+
+ const dirList = dirPath.split('/')
+
+ if (dirList.length === 1) {
+ return {
+ currentDir: '/',
+ dirPath: '/'
+ }
+ }
+
+ if (dirList.length > 1) {
+ dirList.length -= 1
+ return {
+ currentDir: dirList[dirList.length - 1],
+ dirPath: dirList.join('/')
+ }
+ }
+
+ return {
+ currentDir: '/',
+ dirPath: '/'
+ }
+}
+
+/**
+ * 获取上级目录列表
+ * @param dirPath
+ */
+export const getUpLevelDirList = (dirPath: string) => {
+ if (dirPath === '/') {
+ return []
+ }
+
+ const dirList = dirPath.split('/')
+
+ const tempL: string[] = []
+ let tempP = ''
+
+ dirList.forEach((d, i) => {
+ tempP += `${i > 0 ? '/' : ''}${d}`
+ tempL.unshift(tempP)
+ })
+
+ return tempL
+}
diff --git a/picx/src/store/modules/image-card/index.ts b/picx/src/store/modules/image-card/index.ts
new file mode 100644
index 0000000..32c4e42
--- /dev/null
+++ b/picx/src/store/modules/image-card/index.ts
@@ -0,0 +1,41 @@
+import { Module } from 'vuex'
+import { ImageCardStateTypes } from './types'
+import RootStateTypes from '../../types'
+import { UploadedImageModel } from '@/common/model/upload.model'
+
+const imageCardModule: Module = {
+ state: {
+ imgCardArr: []
+ },
+ mutations: {
+ IMAGE_CARD(state: ImageCardStateTypes, { imageObj }) {
+ const { uuid, checked } = imageObj
+ if (checked) {
+ state.imgCardArr.forEach((item) => {
+ if (item.uuid === uuid) {
+ // eslint-disable-next-line no-param-reassign
+ item.checked = true
+ }
+ })
+ }
+ },
+ REPLACE_IMAGE_CARD(state: ImageCardStateTypes, { checkedImgArr }) {
+ if (checkedImgArr.length > 0) {
+ state.imgCardArr = checkedImgArr
+ } else {
+ state.imgCardArr = []
+ }
+ }
+ },
+ actions: {},
+ getters: {
+ getImageCardArr: (state: ImageCardStateTypes) => state.imgCardArr,
+ getImageCardCheckedArr: (state: ImageCardStateTypes) => {
+ return state.imgCardArr.filter((item: UploadedImageModel) => {
+ return item.checked
+ })
+ }
+ }
+}
+
+export default imageCardModule
diff --git a/picx/src/store/modules/image-card/types.ts b/picx/src/store/modules/image-card/types.ts
new file mode 100644
index 0000000..b750664
--- /dev/null
+++ b/picx/src/store/modules/image-card/types.ts
@@ -0,0 +1,5 @@
+import { UploadedImageModel } from '@/common/model/upload.model'
+
+export interface ImageCardStateTypes {
+ imgCardArr: UploadedImageModel[]
+}
diff --git a/picx/src/store/modules/image-viewer/index.ts b/picx/src/store/modules/image-viewer/index.ts
new file mode 100644
index 0000000..07a2bf3
--- /dev/null
+++ b/picx/src/store/modules/image-viewer/index.ts
@@ -0,0 +1,29 @@
+import { Module } from 'vuex'
+import ImageViewerStateTypes from './types'
+import RootStateTypes from '../../types'
+
+const imageViewerModule: Module = {
+ state: {
+ imageViewer: {
+ imgInfo: null,
+ isShow: false
+ }
+ },
+ mutations: {
+ IMAGE_VIEWER(state: ImageViewerStateTypes, { imgInfo, isShow }) {
+ state.imageViewer.imgInfo = imgInfo
+ state.imageViewer.isShow = isShow
+ },
+
+ IMAGE_VIEWER_LOGOUT(state: ImageViewerStateTypes) {
+ state.imageViewer.isShow = false
+ state.imageViewer.imgInfo = null
+ }
+ },
+ actions: {},
+ getters: {
+ getImageViewer: (state: ImageViewerStateTypes) => state.imageViewer
+ }
+}
+
+export default imageViewerModule
diff --git a/picx/src/store/modules/image-viewer/types.ts b/picx/src/store/modules/image-viewer/types.ts
new file mode 100644
index 0000000..fbbcf07
--- /dev/null
+++ b/picx/src/store/modules/image-viewer/types.ts
@@ -0,0 +1,12 @@
+export interface ImgInfo {
+ name: string
+ size: number
+ lastModified: number
+ url: string
+}
+export default interface ImageViewerStateTypes {
+ imageViewer: {
+ imgInfo: ImgInfo | null
+ isShow: boolean
+ }
+}
diff --git a/picx/src/store/modules/to-upload-image/index.ts b/picx/src/store/modules/to-upload-image/index.ts
new file mode 100644
index 0000000..e7f6c1c
--- /dev/null
+++ b/picx/src/store/modules/to-upload-image/index.ts
@@ -0,0 +1,92 @@
+import { ToUploadImageModel } from '@/common/model/upload.model'
+import { Module } from 'vuex'
+import ToUploadImageStateTypes from '@/store/modules/to-upload-image/types'
+import RootStateTypes from '@/store/types'
+
+const toUploadImageModule: Module = {
+ state: {
+ curImgBase64Url: '',
+ curImgUuid: '',
+ list: [],
+ uploadedNumber: 0
+ },
+
+ actions: {
+ // 要上传的图片列表 - 增加图片项
+ TO_UPLOAD_IMAGE_LIST_ADD({ state }, item: ToUploadImageModel) {
+ state.list.unshift(item)
+ },
+
+ // 要上传的图片列表 - 设置当前图片的 Base64Url
+ TO_UPLOAD_IMAGE_SET_CURRENT({ state }, { uuid, base64Url }) {
+ state.curImgUuid = uuid
+ state.curImgBase64Url = base64Url
+ },
+
+ // 要上传的图片列表 - 上传完成的图片数量 +1
+ TO_UPLOAD_IMAGE_UPLOADED({ state }) {
+ state.uploadedNumber += 1
+ },
+
+ // 要上传的图片列表 - 删除图片项
+ TO_UPLOAD_IMAGE_LIST_REMOVE({ state }, uuid: string) {
+ if (state.list.length > 0) {
+ const rmIndex = state.list.findIndex((v: ToUploadImageModel) => v.uuid === uuid)
+ if (rmIndex !== -1) {
+ state.list.splice(rmIndex, 1)
+ }
+ if (state.list.length === 0) {
+ state.curImgBase64Url = ''
+ state.uploadedNumber = 0
+ } else if (state.curImgUuid === uuid) {
+ const cur = state.list[0]
+ state.curImgBase64Url = cur.imgData.base64Url
+ state.curImgUuid = cur.uuid
+ }
+ }
+ },
+
+ // 要上传的图片列表 - 上传失败时,在列表中移除已上传的图片
+ TO_UPLOAD_IMAGE_LIST_FAIL({ state }) {
+ if (state.list.length > 0) {
+ const temp: ToUploadImageModel[] = state.list.filter(
+ (v: ToUploadImageModel) => v.uploadStatus.progress !== 100
+ )
+ if (temp.length > 0) {
+ state.list = temp
+ state.uploadedNumber = 0
+ state.curImgBase64Url = temp[0].imgData.base64Url
+ }
+ }
+ },
+
+ // 要上传的图片列表 - 清空 Url
+ TO_UPLOAD_IMAGE_CLEAN_URL({ state }) {
+ state.curImgBase64Url = ''
+ },
+
+ // 要上传的图片列表 - 清空 List
+ TO_UPLOAD_IMAGE_CLEAN_LIST({ state }) {
+ state.list = []
+ },
+
+ // 要上传的图片列表 - 清空上传完成数量
+ TO_UPLOAD_IMAGE_CLEAN_UPLOADED_NUMBER({ state }) {
+ state.uploadedNumber = 0
+ },
+
+ // 要上传的图片列表 - 退出登录
+ TO_UPLOAD_IMAGE_LOGOUT({ state }) {
+ state.curImgBase64Url = ''
+ state.list = []
+ state.uploadedNumber = 0
+ }
+ },
+
+ getters: {
+ getToUploadImageList: (state: ToUploadImageStateTypes) => state.list,
+ getToUploadImage: (state: ToUploadImageStateTypes) => state
+ }
+}
+
+export default toUploadImageModule
diff --git a/picx/src/store/modules/to-upload-image/types.ts b/picx/src/store/modules/to-upload-image/types.ts
new file mode 100644
index 0000000..0e7cf40
--- /dev/null
+++ b/picx/src/store/modules/to-upload-image/types.ts
@@ -0,0 +1,6 @@
+export default interface ToUploadImageStateTypes {
+ curImgBase64Url: string
+ curImgUuid: string
+ list: any[]
+ uploadedNumber: number
+}
diff --git a/picx/src/store/modules/upload-area-active/index.ts b/picx/src/store/modules/upload-area-active/index.ts
new file mode 100644
index 0000000..0e22bbf
--- /dev/null
+++ b/picx/src/store/modules/upload-area-active/index.ts
@@ -0,0 +1,25 @@
+import { Module } from 'vuex'
+import UploadAreaActiveStateTypes from './types'
+import RootStateTypes from '../../types'
+
+const uploadAreaActiveModule: Module = {
+ state: {
+ uploadAreaActive: false
+ },
+ mutations: {
+ // 修改上传区域激活状态
+ CHANGE_UPLOAD_AREA_ACTIVE(state: UploadAreaActiveStateTypes, isActive: boolean) {
+ state.uploadAreaActive = isActive
+ },
+
+ UPLOAD_AREA_ACTIVE_LOGOUT(state: UploadAreaActiveStateTypes) {
+ state.uploadAreaActive = false
+ }
+ },
+ actions: {},
+ getters: {
+ getUploadAreaActive: (state: UploadAreaActiveStateTypes) => state.uploadAreaActive
+ }
+}
+
+export default uploadAreaActiveModule
diff --git a/picx/src/store/modules/upload-area-active/types.ts b/picx/src/store/modules/upload-area-active/types.ts
new file mode 100644
index 0000000..4e8f4f3
--- /dev/null
+++ b/picx/src/store/modules/upload-area-active/types.ts
@@ -0,0 +1,3 @@
+export default interface UploadAreaActiveStateTypes {
+ uploadAreaActive: boolean
+}
diff --git a/picx/src/store/modules/upload-settings/index.ts b/picx/src/store/modules/upload-settings/index.ts
new file mode 100644
index 0000000..3455fad
--- /dev/null
+++ b/picx/src/store/modules/upload-settings/index.ts
@@ -0,0 +1,24 @@
+import { Module } from 'vuex'
+import UploadAreaActiveStateTypes from './types'
+import RootStateTypes from '../../types'
+
+const uploadSettingsModule: Module = {
+ state: {
+ uploadSettings: {
+ isSetMaxSize: true,
+ imageMaxSize: 30 * 1024
+ }
+ },
+ mutations: {
+ UPLOAD_SETTINGS_LOGOUT(state: UploadAreaActiveStateTypes) {
+ state.uploadSettings.isSetMaxSize = true
+ state.uploadSettings.imageMaxSize = 50 * 1024
+ }
+ },
+ actions: {},
+ getters: {
+ getUploadSettings: (state) => state.uploadSettings
+ }
+}
+
+export default uploadSettingsModule
diff --git a/picx/src/store/modules/upload-settings/types.ts b/picx/src/store/modules/upload-settings/types.ts
new file mode 100644
index 0000000..5e1ff9d
--- /dev/null
+++ b/picx/src/store/modules/upload-settings/types.ts
@@ -0,0 +1,6 @@
+export default interface UploadSettingsStateTypes {
+ uploadSettings: {
+ isSetMaxSize: boolean
+ imageMaxSize: number
+ }
+}
diff --git a/picx/src/store/modules/uploaded-image-list/index.ts b/picx/src/store/modules/uploaded-image-list/index.ts
new file mode 100644
index 0000000..ea96a74
--- /dev/null
+++ b/picx/src/store/modules/uploaded-image-list/index.ts
@@ -0,0 +1,53 @@
+import { Module } from 'vuex'
+import UploadedImageListStateTypes from '@/store/modules/uploaded-image-list/types'
+import RootStateTypes from '@/store/types'
+import { PICX_UPLOADED } from '@/common/model/storage.model'
+import { UploadedImageModel } from '@/common/model/upload.model'
+
+const initUploadedImageList = (): UploadedImageModel[] => {
+ const imageList: string | null = sessionStorage.getItem(PICX_UPLOADED)
+ return imageList ? JSON.parse(imageList) : []
+}
+
+const uploadedImageListModule: Module = {
+ state: {
+ uploadedImageList: initUploadedImageList()
+ },
+
+ mutations: {},
+
+ actions: {
+ // 上传完成的图片列表 - 增加
+ UPLOADED_LIST_ADD({ state, dispatch }, item: UploadedImageModel) {
+ state.uploadedImageList.unshift(item)
+ dispatch('UPLOADED_LIST_PERSIST')
+ },
+
+ // 上传完成的图片列表 - 删除
+ UPLOADED_LIST_REMOVE({ state, dispatch }, uuid: string) {
+ if (state.uploadedImageList.length > 0) {
+ const rmIndex = state.uploadedImageList.findIndex((v) => v.uuid === uuid)
+ if (rmIndex !== -1) {
+ state.uploadedImageList.splice(rmIndex, 1)
+ dispatch('UPLOADED_LIST_PERSIST')
+ }
+ }
+ },
+
+ // 上传完成的图片列表 - 持久化
+ UPLOADED_LIST_PERSIST({ state }) {
+ sessionStorage.setItem(PICX_UPLOADED, JSON.stringify(state.uploadedImageList))
+ },
+
+ // 上传完成的图片列表 - 退出登录
+ UPLOADED_LIST_LOGOUT({ state }) {
+ state.uploadedImageList = []
+ }
+ },
+
+ getters: {
+ getUploadedImageList: (state: any) => state.uploadedImageList
+ }
+}
+
+export default uploadedImageListModule
diff --git a/picx/src/store/modules/uploaded-image-list/types.ts b/picx/src/store/modules/uploaded-image-list/types.ts
new file mode 100644
index 0000000..6048dd1
--- /dev/null
+++ b/picx/src/store/modules/uploaded-image-list/types.ts
@@ -0,0 +1,5 @@
+import { UploadedImageModel } from '@/common/model/upload.model'
+
+export default interface UploadedImageListStateTypes {
+ uploadedImageList: UploadedImageModel[]
+}
diff --git a/picx/src/store/modules/user-config-info/index.ts b/picx/src/store/modules/user-config-info/index.ts
new file mode 100644
index 0000000..6f73e99
--- /dev/null
+++ b/picx/src/store/modules/user-config-info/index.ts
@@ -0,0 +1,155 @@
+import { Module } from 'vuex'
+import {
+ BranchModeEnum,
+ UserConfigInfoModel
+} from '@/common/model/user-config-info.model'
+import { PICX_CONFIG } from '@/common/model/storage.model'
+import { deepAssignObject, cleanObject } from '@/utils/object-helper'
+import UserConfigInfoStateTypes from '@/store/modules/user-config-info/types'
+import RootStateTypes from '@/store/types'
+import { DirModeEnum } from '@/common/model/dir.model'
+import TimeHelper from '@/utils/time-helper'
+
+const initUserConfigInfo = (): UserConfigInfoModel => {
+ const initConfig: UserConfigInfoModel = {
+ token: '',
+ owner: '',
+ email: '',
+ name: '',
+ avatarUrl: '',
+ selectedRepos: '',
+ reposList: [],
+ branchMode: BranchModeEnum.reposBranch,
+ branchList: [],
+ selectedBranch: '',
+ selectedDir: '',
+ dirMode: DirModeEnum.reposDir,
+ dirList: [],
+ loggingStatus: false,
+ selectedDirList: []
+ }
+
+ const LSConfig: string | null = localStorage.getItem(PICX_CONFIG)
+
+ if (LSConfig) {
+ // Assign: oldConfig -> initConfig
+ deepAssignObject(initConfig, JSON.parse(LSConfig))
+
+ if (initConfig.selectedBranch && !initConfig.branchList.length) {
+ initConfig.branchList = [
+ {
+ value: initConfig.selectedBranch,
+ label: initConfig.selectedBranch
+ }
+ ]
+ }
+
+ if (initConfig.dirMode === DirModeEnum.autoDir) {
+ initConfig.selectedDir = TimeHelper.getYyyyMmDd()
+ }
+
+ return initConfig
+ }
+
+ return initConfig
+}
+
+const userConfigInfoUpdate = (state: UserConfigInfoStateTypes): void => {
+ const { selectedDir, selectedBranch, dirMode } = state.userConfigInfo
+ if (dirMode === 'newDir') {
+ const strList = selectedDir.split('')
+ let count = 0
+ let newStr = ''
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < strList.length; i++) {
+ if (strList[i] === ' ' || strList[i] === '.' || strList[i] === '、') {
+ strList[i] = '-'
+ }
+ if (strList[i] === '/') {
+ count += 1
+ }
+ if (count >= 3) {
+ break
+ }
+ newStr += strList[i]
+ }
+ state.userConfigInfo.selectedDir = newStr
+ }
+ state.userConfigInfo.selectedBranch = selectedBranch.replace(/\s+/g, '-')
+}
+
+const userConfigInfoModule: Module = {
+ state: {
+ userConfigInfo: initUserConfigInfo()
+ },
+
+ actions: {
+ // 持久化状态获取
+ USER_CONFIG_INFO_RESET({ state }) {
+ state.userConfigInfo = initUserConfigInfo()
+ },
+ // 设置用户配置信息
+ SET_USER_CONFIG_INFO(
+ { state, dispatch },
+ configInfo: UserConfigInfoStateTypes,
+ needPersist: boolean = true
+ ) {
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key in configInfo) {
+ // eslint-disable-next-line no-prototype-builtins
+ if (state.userConfigInfo.hasOwnProperty(key)) {
+ // @ts-ignore
+ state.userConfigInfo[key] = configInfo[key]
+ } else if (key === 'needPersist') {
+ // eslint-disable-next-line
+ needPersist = false
+ }
+ }
+ if (!needPersist) return
+ dispatch('USER_CONFIG_INFO_PERSIST')
+ },
+
+ // 用户配置信息 - 增加目录
+ USER_CONFIG_INFO_ADD_DIR({ state, dispatch }, dir: string) {
+ if (!state.userConfigInfo.dirList.some((v: any) => v.value === dir)) {
+ state.userConfigInfo.dirList.push({ label: dir, value: dir })
+ dispatch('USER_CONFIG_INFO_PERSIST')
+ }
+ },
+
+ // 用户配置信息 - 删除目录列表的某个目录
+ USER_CONFIG_INFO_REMOVE_DIR({ state, dispatch }, dir: string) {
+ const { dirList } = state.userConfigInfo
+ if (dirList.some((v: any) => v.value === dir)) {
+ const rmIndex = dirList.findIndex((v: any) => v.value === dir)
+ dirList.splice(rmIndex, 1)
+ dispatch('USER_CONFIG_INFO_PERSIST')
+ }
+ },
+
+ // 持久化用户配置信息
+ USER_CONFIG_INFO_PERSIST({ state }) {
+ userConfigInfoUpdate(state)
+ localStorage.setItem(PICX_CONFIG, JSON.stringify(state.userConfigInfo))
+ },
+
+ // 修改 userConfigInfo 但无需持久化 (目前提供图床管理页面使用)
+ USER_CONFIG_INFO_NOT_PERSIST({ state }) {
+ userConfigInfoUpdate(state)
+ },
+
+ // 退出登录
+ USER_CONFIG_INFO_LOGOUT({ state }) {
+ cleanObject(state.userConfigInfo)
+ }
+ },
+
+ getters: {
+ getUserLoggingStatus: (state: UserConfigInfoStateTypes): boolean =>
+ state.userConfigInfo.loggingStatus,
+ getUserConfigInfo: (state: UserConfigInfoStateTypes): UserConfigInfoModel =>
+ state.userConfigInfo
+ }
+}
+
+export default userConfigInfoModule
diff --git a/picx/src/store/modules/user-config-info/types.ts b/picx/src/store/modules/user-config-info/types.ts
new file mode 100644
index 0000000..e54818e
--- /dev/null
+++ b/picx/src/store/modules/user-config-info/types.ts
@@ -0,0 +1,5 @@
+import { UserConfigInfoModel } from '@/common/model/user-config-info.model'
+
+export default interface UserConfigInfoStateTypes {
+ userConfigInfo: UserConfigInfoModel
+}
diff --git a/picx/src/store/modules/user-settings/index.ts b/picx/src/store/modules/user-settings/index.ts
new file mode 100644
index 0000000..6607590
--- /dev/null
+++ b/picx/src/store/modules/user-settings/index.ts
@@ -0,0 +1,67 @@
+import { Module } from 'vuex'
+import { PICX_SETTINGS } from '@/common/model/storage.model'
+import { deepAssignObject } from '@/utils/object-helper'
+import UserConfigInfoStateTypes from '@/store/modules/user-config-info/types'
+import RootStateTypes from '@/store/types'
+import { CompressEncoderMap } from '@/utils/compress'
+import { UserSettingsModel } from '@/common/model/user-settings.model'
+import UserSettingsStateTypes from '@/store/modules/user-settings/types'
+import { getLocalItem } from '@/utils/common-utils'
+import ExternalLinkType from '@/common/model/external-link.model'
+
+const initSettings: UserSettingsModel = {
+ defaultHash: true,
+ defaultMarkdown: false,
+ defaultPrefix: false,
+ prefixName: '',
+ isCompress: true,
+ compressEncoder: CompressEncoderMap.webP,
+ themeMode: 'light',
+ autoLightThemeTime: ['08:00', '19:00'],
+ elementPlusSize: 'default',
+ externalLinkType: ExternalLinkType.staticaly
+}
+
+const initUserSettings = (): UserSettingsModel => {
+ const LSSettings = getLocalItem(PICX_SETTINGS)
+ if (LSSettings) {
+ deepAssignObject(initSettings, LSSettings)
+ }
+ return initSettings
+}
+
+const userSettingsModule: Module = {
+ state: {
+ userSettings: initUserSettings()
+ },
+
+ actions: {
+ // 设置
+ SET_USER_SETTINGS({ state }, configInfo: UserConfigInfoStateTypes) {
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key in configInfo) {
+ // eslint-disable-next-line no-prototype-builtins
+ if (state.userSettings.hasOwnProperty(key)) {
+ // @ts-ignore
+ state.userSettings[key] = configInfo[key]
+ }
+ }
+ },
+
+ // 持久化
+ USER_SETTINGS_PERSIST({ state }) {
+ localStorage.setItem(PICX_SETTINGS, JSON.stringify(state.userSettings))
+ },
+
+ // 退出登录
+ USER_SETTINGS_LOGOUT({ state }) {
+ state.userSettings = initSettings
+ }
+ },
+
+ getters: {
+ getUserSettings: (state): UserSettingsModel => state.userSettings
+ }
+}
+
+export default userSettingsModule
diff --git a/picx/src/store/modules/user-settings/types.ts b/picx/src/store/modules/user-settings/types.ts
new file mode 100644
index 0000000..596b418
--- /dev/null
+++ b/picx/src/store/modules/user-settings/types.ts
@@ -0,0 +1,5 @@
+import { UserSettingsModel } from '@/common/model/user-settings.model'
+
+export default interface UserSettingsStateTypes {
+ userSettings: UserSettingsModel
+}
diff --git a/picx/src/store/types.ts b/picx/src/store/types.ts
new file mode 100644
index 0000000..dca2872
--- /dev/null
+++ b/picx/src/store/types.ts
@@ -0,0 +1,21 @@
+import DirImageListStateTypes from './modules/dir-image-list/types'
+import ToUploadImageStateTypes from './modules/to-upload-image/types'
+import UploadedImageListStateTypes from './modules/uploaded-image-list/types'
+import UserConfigInfoStateTypes from './modules/user-config-info/types'
+import ImageViewerStateTypes from './modules/image-viewer/types'
+import UploadAreaActiveStateTypes from './modules/upload-area-active/types'
+import UploadSettingsStateTypes from './modules/upload-settings/types'
+
+export default interface RootStateTypes {
+ rootName: string
+}
+
+export interface AllStateTypes extends RootStateTypes {
+ dirImageListModule: DirImageListStateTypes
+ toUploadImageModule: ToUploadImageStateTypes
+ uploadedImageListModule: UploadedImageListStateTypes
+ userConfigInfoModule: UserConfigInfoStateTypes
+ imageViewerModule: ImageViewerStateTypes
+ uploadAreaActiveModule: UploadAreaActiveStateTypes
+ uploadSettingsModule: UploadSettingsStateTypes
+}
diff --git a/picx/src/style/base.styl b/picx/src/style/base.styl
new file mode 100644
index 0000000..acce335
--- /dev/null
+++ b/picx/src/style/base.styl
@@ -0,0 +1,98 @@
+@import './theme.styl'
+@import './variables.styl'
+
+$component-interval = 16rem
+$box-border-radius = 6rem
+$content-max-width = 888rem
+$scrollbar-size = 8rem
+
+:root {
+ font-size 1px
+
+ +picx-tablet() {
+ font-size 0.9px
+ }
+
+ +picx-mobile() {
+ font-size 0.8px
+ }
+}
+
+html, body {
+ position relative
+ padding 0
+ margin 0
+ width 100%
+ height 100%
+ color var(--default-text-color)
+}
+
+a {
+ text-decoration none
+ font-size 1.5rem
+ color var(--default-text-color)
+}
+
+a:link {
+ color var(--default-text-color)
+ text-decoration none
+}
+
+ul, ol, li {
+ list-style none
+}
+
+* {
+ &::-webkit-scrollbar {
+ height $scrollbar-size
+ width $scrollbar-size
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background var(--scrollbar-color)
+ border-radius $box-border-radius
+ }
+
+ &::-webkit-scrollbar-track {
+ background transparent
+ }
+}
+
+
+.flex-center {
+ display flex
+ justify-content center
+ align-items center
+}
+
+
+.flex-start {
+ display flex
+ justify-content flex-start
+ align-items center
+}
+
+
+.page-container {
+ width 100%
+ height 100%
+ box-sizing border-box
+ padding 30rem
+ background var(--background-color)
+ border-top-left-radius $box-border-radius
+ border-top-right-radius $box-border-radius
+ overflow-y auto
+}
+
+.clear {
+ &::after {
+ content ''
+ display block
+ clear both
+ visibility hidden
+ overflow hidden
+ height 0
+ }
+}
+
+
diff --git a/picx/src/style/theme.styl b/picx/src/style/theme.styl
new file mode 100644
index 0000000..5f3e64c
--- /dev/null
+++ b/picx/src/style/theme.styl
@@ -0,0 +1,25 @@
+@import './variables.styl'
+
+:root {
+ root-color('light')
+}
+
+@media (prefers-color-scheme light) {
+ :root {
+ root-color('light')
+ }
+}
+
+@media (prefers-color-scheme dark) {
+ :root {
+ root-color('dark')
+ }
+}
+
+.light {
+ root-color('light')
+}
+
+.dark {
+ root-color('dark')
+}
diff --git a/picx/src/style/variables.styl b/picx/src/style/variables.styl
new file mode 100644
index 0000000..f43d8b4
--- /dev/null
+++ b/picx/src/style/variables.styl
@@ -0,0 +1,110 @@
+// ========================================================================================
+// media query
+// ========================================================================================
+$media-max-width-tablet = 800px; // media query max width (tablet)
+$media-max-width-mobile = 500px; // media query max width (mobile)
+
+picx-tablet()
+ @media (max-width: $media-max-width-tablet)
+ { block }
+
+picx-mobile()
+ @media (max-width: $media-max-width-mobile)
+ { block }
+
+
+// ========================================================================================
+// z-index
+// ========================================================================================
+$z-index-1 = 1001;
+$z-index-2 = 1002;
+$z-index-3 = 1003;
+$z-index-4 = 1004;
+$z-index-5 = 1005;
+$z-index-6 = 1006;
+$z-index-7 = 1007;
+$z-index-8 = 1008;
+$z-index-9 = 1009;
+
+
+// ========================================================================================
+// light mode color
+// ========================================================================================
+$primary-color = #0066CC;
+$background-color = #fff;
+$second-background-color = darken($background-color, 5%);
+$third-background-color = darken($background-color, 10%);
+$default-text-color = #50505c;
+$first-text-color = darken($default-text-color, 10%);
+$second-text-color = darken($default-text-color, 5%);
+$third-text-color = lighten($default-text-color, 30%);
+$fourth-text-color = lighten($default-text-color, 90%);
+$border-color = darken($background-color, 30%);
+$selection-color = lighten($primary-color, 10%);
+$shadow-color = rgba(0, 0, 0, 0.2);
+$shadow-hover-color = rgba(0, 0, 0, 0.28);
+$scrollbar-color = darken($background-color, 20%);
+$scroll-bar-bg-color = darken($background-color, 30%);
+$upload-area-focus-color = #0066CC;
+$await-upload-color = #E6A23C;
+$uploading-color = #409EFF;
+$uploaded-color = #67C23A;
+$markdown-icon-color = #808080;
+$markdown-icon-active-color = darken($markdown-icon-color, 30%);
+
+
+
+// ========================================================================================
+// dark mode color
+// ========================================================================================
+$dark-primary-color = #0066CC;
+$dark-background-color = #2a2a2f;
+$dark-second-background-color = darken($dark-background-color, 10%);
+$dark-third-background-color = darken($dark-background-color, 15%);
+$dark-default-text-color = #bebec6;
+$dark-first-text-color = lighten($dark-default-text-color, 30%);
+$dark-second-text-color = lighten($dark-default-text-color, 20%);
+$dark-third-text-color = darken($dark-default-text-color, 20%);
+$dark-fourth-text-color = darken($dark-default-text-color, 80%);
+$dark-border-color = lighten($dark-background-color, 20%);
+$dark-selection-color = $selection-color;
+$dark-shadow-color = rgba(128, 128, 128, 0.2);
+$dark-shadow-hover-color = rgba(128, 128, 128, 0.28);
+$dark-scrollbar-color = darken($dark-background-color, 20%);
+$dark-scroll-bar-bg-color = lighten($dark-background-color, 30%);
+$dark-upload-area-focus-color = #1070d0;
+$dark-await-upload-color = #c08327;
+$dark-uploading-color = #287dd5;
+$dark-uploaded-color = #55b626;
+$dark-markdown-icon-color = #aaa;
+$dark-markdown-icon-active-color = lighten($dark-markdown-icon-color, 30%);
+
+
+
+// ========================================================================
+// light/dark mode color
+// ========================================================================
+root-color(mode) {
+ --background-color: mode == 'light' ? $background-color : $dark-background-color;
+ --second-background-color: mode == 'light' ? $second-background-color : $dark-second-background-color;
+ --third-background-color: mode == 'light' ? $third-background-color : $dark-third-background-color;
+ --primary-color: mode == 'light' ? $primary-color : $dark-primary-color;
+ --first-text-color: mode == 'light' ? $first-text-color : $dark-first-text-color;
+ --second-text-color: mode == 'light' ? $second-text-color : $dark-second-text-color;
+ --third-text-color: mode == 'light' ? $third-text-color : $dark-third-text-color;
+ --fourth-text-color: mode == 'light' ? $fourth-text-color : $dark-fourth-text-color;
+ --default-text-color: mode == 'light' ? $default-text-color : $dark-default-text-color;
+ --border-color: mode == 'light' ? $border-color : $dark-border-color;
+ --selection-color: mode == 'light' ? $selection-color : $dark-selection-color;
+ --shadow-color: mode == 'light' ? $shadow-color : $dark-shadow-color;
+ --shadow-hover-color: mode == 'light' ? $shadow-hover-color : $dark-shadow-hover-color;
+ --scrollbar-color: mode == 'light' ? $scrollbar-color : $dark-scrollbar-color;
+ --scroll-bar-bg-color: mode == 'light' ? $scroll-bar-bg-color : $dark-scroll-bar-bg-color;
+ --upload-area-focus-color : mode == 'light' ? $upload-area-focus-color : $dark-upload-area-focus-color;
+ --await-upload-color : mode == 'light' ? $await-upload-color : $dark-await-upload-color;
+ --uploading-color : mode == 'light' ? $uploading-color : $dark-uploading-color;
+ --uploaded-color : mode == 'light' ? $uploaded-color : $dark-uploaded-color;
+ --markdown-icon-color : mode == 'light' ? $markdown-icon-color : $dark-markdown-icon-color;
+ --markdown-icon-active-color : mode == 'light' ? $markdown-icon-active-color : $dark-markdown-icon-active-color;
+}
+
diff --git a/picx/src/utils/axios.ts b/picx/src/utils/axios.ts
new file mode 100644
index 0000000..37c0b38
--- /dev/null
+++ b/picx/src/utils/axios.ts
@@ -0,0 +1,51 @@
+import Axios from 'axios'
+import { PICX_CONFIG } from '@/common/model/storage.model'
+
+const baseURL = 'https://api.github.com'
+
+const axios = Axios.create({
+ baseURL,
+ timeout: 300000 // request timeout 请求超时 5m
+})
+
+axios.defaults.headers['Content-Type'] = 'application/json'
+
+// 发起请求之前的拦截器(前置拦截)
+axios.interceptors.request.use(
+ (config) => {
+ const userConfig = localStorage.getItem(PICX_CONFIG)
+
+ if (userConfig) {
+ const { token } = JSON.parse(userConfig)
+ if (token) {
+ config.headers.Authorization = `token ${token}`
+ }
+ }
+
+ return config
+ },
+ (error) => {
+ return Promise.reject(error)
+ }
+)
+
+// 响应拦截器
+axios.interceptors.response.use(
+ (response) => {
+ return response
+ },
+ (error) => {
+ if (error.response && error.response.data) {
+ const code = error.response.status
+ const msg = error.response.data.message
+ ElMessage.error(`Code: ${code}, Message: ${msg}`)
+ console.error(`[PicX Error]`, error.response)
+ } else {
+ ElMessage.error(`${error}`)
+ }
+
+ return error.response
+ }
+)
+
+export default axios
diff --git a/picx/src/utils/common-utils.ts b/picx/src/utils/common-utils.ts
new file mode 100644
index 0000000..d22db32
--- /dev/null
+++ b/picx/src/utils/common-utils.ts
@@ -0,0 +1,27 @@
+/**
+ * Get JavaScript basic data types
+ * @param data
+ * @returns {string} array | string | number ...
+ */
+export const getType = (data: string) => {
+ const type = Object.prototype.toString.call(data).split(' ')[1]
+ return type.substring(0, type.length - 1).toLowerCase()
+}
+
+/**
+ * Gets a string(uuid) that is not repeated
+ * @returns uuid {string}
+ */
+export const getUuid = () => {
+ return Number(Math.random().toString().substr(2, 5) + Date.now()).toString(36)
+}
+
+/**
+ * get localStorage value
+ * @param key
+ * @returns {*}
+ */
+export const getLocalItem = (key: string) => {
+ const temp = window.localStorage.getItem(key)
+ return temp ? JSON.parse(temp) : null
+}
diff --git a/picx/src/utils/compress.ts b/picx/src/utils/compress.ts
new file mode 100644
index 0000000..e058abb
--- /dev/null
+++ b/picx/src/utils/compress.ts
@@ -0,0 +1,26 @@
+import Compress from '@yireen/squoosh-browser'
+import {
+ defaultPreprocessorState,
+ defaultProcessorState,
+ encoderMap,
+ EncoderState
+} from '@yireen/squoosh-browser/dist/client/lazy-app/feature-meta'
+
+export enum CompressEncoderMap {
+ mozJPEG = 'mozJPEG',
+ avif = 'avif',
+ webP = 'webP'
+}
+
+export const compress = async (file: File, encoder: CompressEncoderMap) => {
+ const compress = new Compress(file, {
+ encoderState: {
+ type: encoder,
+ options: encoderMap[encoder].meta.defaultOptions
+ } as EncoderState,
+ processorState: defaultProcessorState,
+ preprocessorState: defaultPreprocessorState
+ })
+
+ return compress.process()
+}
diff --git a/picx/src/utils/create-to-upload-image.ts b/picx/src/utils/create-to-upload-image.ts
new file mode 100644
index 0000000..547c489
--- /dev/null
+++ b/picx/src/utils/create-to-upload-image.ts
@@ -0,0 +1,42 @@
+import { ToUploadImageModel } from '../common/model/upload.model'
+
+export default (): ToUploadImageModel => {
+ return {
+ uuid: '',
+
+ uploadStatus: {
+ progress: 0,
+ uploading: false
+ },
+
+ imgData: {
+ base64Content: '',
+ base64Url: ''
+ },
+
+ fileInfo: {
+ size: 0,
+ lastModified: 0
+ },
+
+ filename: {
+ name: '',
+ hash: '',
+ suffix: '',
+ prefixName: '',
+ now: '',
+ initName: '',
+ newName: 'xxx',
+ isHashRename: true,
+ isRename: false,
+ isPrefix: false
+ },
+
+ externalLink: {
+ github: '',
+ jsdelivr: '',
+ staticaly: '',
+ cloudflare: ''
+ }
+ }
+}
diff --git a/picx/src/utils/delete-image-card.ts b/picx/src/utils/delete-image-card.ts
new file mode 100644
index 0000000..a90852a
--- /dev/null
+++ b/picx/src/utils/delete-image-card.ts
@@ -0,0 +1,64 @@
+import { UploadedImageModel } from '../common/model/upload.model'
+import { UserConfigInfoModel } from '../common/model/user-config-info.model'
+import axios from '@/utils/axios'
+import { deleteStatusEnum } from '../common/model/delete.model'
+import { store } from '@/store'
+
+let deleteIndex = 0
+
+export async function deleteSingleImage(
+ imageObj: UploadedImageModel,
+ userConfigInfo: UserConfigInfoModel
+): Promise {
+ // eslint-disable-next-line no-param-reassign
+ imageObj.deleting = true
+ const { owner, selectedRepos } = userConfigInfo
+ return new Promise((resolve, reject) => {
+ axios
+ .delete(`/repos/${owner}/${selectedRepos}/contents/${imageObj.path}`, {
+ data: {
+ owner,
+ repo: selectedRepos,
+ path: imageObj.path,
+ message: 'delete picture via PicX(https://github.com/XPoet/picx)',
+ sha: imageObj.sha
+ }
+ })
+ .then((res) => {
+ if (res && res.status === 200) {
+ // eslint-disable-next-line no-param-reassign
+ imageObj.deleting = false
+ store.dispatch('UPLOADED_LIST_REMOVE', imageObj.uuid)
+ store.dispatch('DIR_IMAGE_LIST_REMOVE', imageObj)
+ resolve(true)
+ } else {
+ // eslint-disable-next-line no-param-reassign
+ imageObj.deleting = false
+ resolve(false)
+ }
+ })
+ .catch((err) => {
+ reject(err)
+ })
+ })
+}
+
+export async function delelteBatchImage(
+ imgCardArr: Array,
+ userConfigInfo: UserConfigInfoModel
+) {
+ if (deleteIndex >= imgCardArr.length) {
+ return deleteStatusEnum.deleted
+ }
+ if (await deleteSingleImage(imgCardArr[deleteIndex], userConfigInfo)) {
+ if (deleteIndex < imgCardArr.length) {
+ deleteIndex += 1
+ if (await delelteBatchImage(imgCardArr, userConfigInfo)) {
+ deleteIndex = 0
+ return deleteStatusEnum.allDeleted
+ }
+ }
+ return deleteStatusEnum.deleted
+ }
+ return deleteStatusEnum.deleteFail
+}
diff --git a/picx/src/utils/env.ts b/picx/src/utils/env.ts
new file mode 100644
index 0000000..99919a5
--- /dev/null
+++ b/picx/src/utils/env.ts
@@ -0,0 +1,23 @@
+/* eslint-disable no-restricted-syntax */
+import { Recordable, ViteEnv } from '@/common/model/vite-config.model'
+
+// Read all environment variable configuration files to process.env
+export default function wrapperEnv(envConf: Recordable): ViteEnv {
+ const ret: any = {}
+
+ for (const envName of Object.keys(envConf)) {
+ let realName = envConf[envName].replace(/\\n/g, '\n')
+ if (realName === 'true') {
+ realName = true
+ } else if (realName === 'false') {
+ realName = false
+ }
+
+ if (envName === 'VITE_PORT') {
+ realName = Number(realName)
+ }
+ ret[envName] = realName
+ process.env[envName] = realName
+ }
+ return ret
+}
diff --git a/picx/src/utils/external-link-handler.ts b/picx/src/utils/external-link-handler.ts
new file mode 100644
index 0000000..e8f4a3e
--- /dev/null
+++ b/picx/src/utils/external-link-handler.ts
@@ -0,0 +1,178 @@
+import ExternalLinkType from '@/common/model/external-link.model'
+import { UserConfigInfoModel } from '@/common/model/user-config-info.model'
+import { getFilename } from '@/utils/file-handle-helper'
+import { UploadedImageModel } from '@/common/model/upload.model'
+
+/**
+ * 创建承载图片外链文本的 DOM 元素
+ */
+export const createExternalLinkDom = () => {
+ let externalLinkDom: any = document.querySelector('.temp-external-link-txt')
+ if (!externalLinkDom) {
+ externalLinkDom = document.createElement('textarea')
+ externalLinkDom.setAttribute('class', 'temp-external-link-txt')
+ externalLinkDom.style.position = 'absolute'
+ externalLinkDom.style.top = '-99999rem'
+ externalLinkDom.style.left = '-99999rem'
+ document.body.appendChild(externalLinkDom)
+ }
+ return externalLinkDom
+}
+
+/**
+ * 生成图片外链
+ * @param type
+ * @param content
+ * @param config
+ */
+export const generateExternalLink = (
+ type: ExternalLinkType,
+ content: any,
+ config: UserConfigInfoModel
+): string => {
+ const staticalyLink: string = `https://cdn.staticaly.com/gh/${config.owner}/${config.selectedRepos}@${config.selectedBranch}/${content.path}`
+ const cloudflareLink: string = `https://git.poker/${config.owner}/${config.selectedRepos}/blob/${config.selectedBranch}/${content.path}?raw=true`
+ const jsdelivrLink: string = `https://cdn.jsdelivr.net/gh/${config.owner}/${config.selectedRepos}@${config.selectedBranch}/${content.path}`
+ const githubLink: string = decodeURI(content.download_url)
+
+ // eslint-disable-next-line default-case
+ switch (type) {
+ case ExternalLinkType.staticaly:
+ return staticalyLink
+
+ case ExternalLinkType.cloudflare:
+ return cloudflareLink
+
+ case ExternalLinkType.jsdelivr:
+ return jsdelivrLink
+
+ case ExternalLinkType.github:
+ return githubLink
+
+ default:
+ return githubLink
+ }
+}
+
+/**
+ * 图片外链转换为 Markdown 格式
+ * @param name 图片名
+ * @param url 图片外链
+ */
+export const formatMarkdown = (name: string, url: string): string => {
+ return ``
+}
+
+/**
+ * 复制图片外链
+ * @param img 图片对象
+ * @param type CDN 类型
+ */
+export const copyExternalLink = (img: UploadedImageModel, type: ExternalLinkType) => {
+ let externalLink = ''
+ let successInfo = ''
+ const { name, is_transform_md: isMD } = img
+
+ switch (type) {
+ case ExternalLinkType.jsdelivr:
+ if (isMD) {
+ externalLink = formatMarkdown(name, img.jsdelivr_cdn_url)
+ successInfo = 'Markdown 格式的 jsDelivr CDN'
+ } else {
+ externalLink = img.jsdelivr_cdn_url
+ successInfo = 'jsDelivr CDN'
+ }
+ break
+
+ case ExternalLinkType.staticaly:
+ if (isMD) {
+ externalLink = formatMarkdown(name, img.staticaly_cdn_url)
+ successInfo = 'Markdown 格式的 Staticaly CDN'
+ } else {
+ externalLink = img.staticaly_cdn_url
+ successInfo = 'Staticaly CDN'
+ }
+ break
+
+ case ExternalLinkType.cloudflare:
+ if (isMD) {
+ externalLink = formatMarkdown(name, img.cloudflare_cdn_url)
+ successInfo = 'Markdown 格式的 Cloudflare CDN'
+ } else {
+ externalLink = img.cloudflare_cdn_url
+ successInfo = 'Cloudflare CDN'
+ }
+ break
+
+ default:
+ if (isMD) {
+ externalLink = formatMarkdown(name, img.github_url)
+ successInfo = 'Markdown 格式的 GitHub'
+ } else {
+ externalLink = img.github_url
+ successInfo = 'GitHub'
+ }
+ }
+
+ const externalLinkDom: any = createExternalLinkDom()
+
+ externalLinkDom.value = externalLink
+ externalLinkDom.select()
+ document.execCommand('copy')
+ ElMessage.success(`${successInfo} 外链复制成功!`)
+}
+
+/**
+ * 批量复制图片外链
+ * @param imgCardList 图片列表
+ * @param type 当前选择的外链类型
+ */
+export const batchCopyExternalLink = (
+ imgCardList: Array,
+ type: ExternalLinkType
+) => {
+ let externalLink = ''
+ const externalLinkDom: any = createExternalLinkDom()
+ externalLinkDom.value = ''
+ if (imgCardList?.length > 0) {
+ imgCardList.forEach((item: UploadedImageModel, index) => {
+ const isMD = item.is_transform_md
+ switch (type) {
+ case ExternalLinkType.jsdelivr:
+ externalLink = isMD
+ ? formatMarkdown(item.name, item.jsdelivr_cdn_url)
+ : item.jsdelivr_cdn_url
+ break
+
+ case ExternalLinkType.staticaly:
+ externalLink = isMD
+ ? formatMarkdown(item.name, item.staticaly_cdn_url)
+ : item.staticaly_cdn_url
+ break
+
+ case ExternalLinkType.cloudflare:
+ externalLink = isMD
+ ? formatMarkdown(item.name, item.cloudflare_cdn_url)
+ : item.cloudflare_cdn_url
+ break
+
+ default:
+ externalLink = isMD
+ ? formatMarkdown(item.name, item.github_url)
+ : item.github_url
+ }
+
+ if (index < imgCardList.length - 1) {
+ // eslint-disable-next-line prefer-template
+ externalLinkDom.value += externalLink + '\n'
+ } else {
+ externalLinkDom.value += externalLink
+ }
+ })
+ externalLinkDom.select()
+ document.execCommand('copy')
+ ElMessage.success(`批量复制图片链接成功`)
+ } else {
+ console.warn('请先选择图片')
+ }
+}
diff --git a/picx/src/utils/file-handle-helper.ts b/picx/src/utils/file-handle-helper.ts
new file mode 100644
index 0000000..27baf52
--- /dev/null
+++ b/picx/src/utils/file-handle-helper.ts
@@ -0,0 +1,50 @@
+import { getUuid } from './common-utils'
+
+/**
+ * get filename
+ * @param filename
+ */
+export const getFilename = (filename: string) => {
+ const splitIndex = filename.indexOf('.')
+ return filename.substr(0, splitIndex).trim().replace(/\s+/g, '-')
+}
+
+/**
+ * get filename suffix
+ * @param filename
+ */
+export const getFileSuffix = (filename: string) => {
+ const splitIndex = filename.lastIndexOf('.')
+ return filename.substr(splitIndex + 1, filename.length)
+}
+
+export const isImage = (suffix: string): boolean => {
+ return /(png|jpg|gif|jpeg|webp|avif|svg\+xml|image\/x-icon)$/.test(suffix)
+}
+
+/**
+ * get file size (KB)
+ * @param size
+ */
+export const getFileSize = (size: number) => {
+ return Number((size / 1024).toFixed(2))
+}
+
+/**
+ * filename handle
+ * @param filename
+ */
+export const filenameHandle = (filename: string | undefined) => {
+ if (filename) {
+ return {
+ name: getFilename(filename),
+ hash: getUuid(),
+ suffix: getFileSuffix(filename)
+ }
+ }
+ return {
+ name: '',
+ hash: '',
+ suffix: ''
+ }
+}
diff --git a/picx/src/utils/image-helper.ts b/picx/src/utils/image-helper.ts
new file mode 100644
index 0000000..1ee88b2
--- /dev/null
+++ b/picx/src/utils/image-helper.ts
@@ -0,0 +1,42 @@
+import { computed } from 'vue'
+import { UploadedImageModel } from '@/common/model/upload.model'
+import { getUuid } from '@/utils/common-utils'
+import { generateExternalLink } from '@/utils/external-link-handler'
+import ExternalLinkType from '@/common/model/external-link.model'
+import { store } from '@/store'
+
+const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
+
+export default function structureImageObject(
+ item: any,
+ selectedDir: string
+): UploadedImageModel {
+ return {
+ type: 'image',
+ uuid: getUuid(),
+ dir: selectedDir,
+ name: item.name,
+ path: item.path,
+ sha: item.sha,
+ deleting: false,
+ is_transform_md: false,
+ size: item.size,
+ checked: false,
+ github_url: generateExternalLink(ExternalLinkType.github, item, userConfigInfo),
+ jsdelivr_cdn_url: generateExternalLink(
+ ExternalLinkType.jsdelivr,
+ item,
+ userConfigInfo
+ ),
+ staticaly_cdn_url: generateExternalLink(
+ ExternalLinkType.staticaly,
+ item,
+ userConfigInfo
+ ),
+ cloudflare_cdn_url: generateExternalLink(
+ ExternalLinkType.cloudflare,
+ item,
+ userConfigInfo
+ )
+ }
+}
diff --git a/picx/src/utils/object-helper.ts b/picx/src/utils/object-helper.ts
new file mode 100644
index 0000000..ea91287
--- /dev/null
+++ b/picx/src/utils/object-helper.ts
@@ -0,0 +1,59 @@
+import { getType } from './common-utils'
+
+/**
+ * 根据 object 每个 key 上值的数据类型,赋对应的初始值
+ * @param object
+ */
+export const cleanObject = (object: any) => {
+ // eslint-disable-next-line guard-for-in,no-restricted-syntax
+ for (const key in object) {
+ // eslint-disable-next-line default-case
+ switch (getType(object[key])) {
+ case 'object':
+ cleanObject(object[key])
+ break
+
+ case 'string':
+ // eslint-disable-next-line no-param-reassign
+ object[key] = ''
+ break
+
+ case 'array':
+ // eslint-disable-next-line no-param-reassign
+ object[key] = []
+ break
+
+ case 'number':
+ // eslint-disable-next-line no-param-reassign
+ object[key] = 0
+ break
+
+ case 'boolean':
+ // eslint-disable-next-line no-param-reassign
+ object[key] = false
+ break
+ }
+ }
+}
+
+/**
+ * 将 obj2 对象的值深度赋值给 obj1 对象
+ * @param obj1{Object}
+ * @param obj2{Object}
+ */
+export const deepAssignObject = (obj1: object, obj2: object): any => {
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key in obj2) {
+ // @ts-ignore
+ if (getType(obj2[key]) !== 'object') {
+ if (obj1) {
+ // @ts-ignore
+ // eslint-disable-next-line no-param-reassign
+ obj1[key] = obj2[key]
+ }
+ } else {
+ // @ts-ignore
+ deepAssignObject(obj1[key], obj2[key])
+ }
+ }
+}
diff --git a/picx/src/utils/paste.ts b/picx/src/utils/paste.ts
new file mode 100644
index 0000000..a9b1678
--- /dev/null
+++ b/picx/src/utils/paste.ts
@@ -0,0 +1,28 @@
+import selectedFileHandle from './selected-file-handle'
+
+const onPaste = (e: any, maxsize: number): Promise | null => {
+ if (!(e.clipboardData && e.clipboardData.items)) {
+ return null
+ }
+
+ // eslint-disable-next-line consistent-return
+ return new Promise((resolve) => {
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0, len = e.clipboardData.items.length; i < len; i++) {
+ const item = e.clipboardData.items[i]
+ if (item.kind === 'file') {
+ const pasteFile = item.getAsFile()
+
+ selectedFileHandle(pasteFile, maxsize)?.then((result) => {
+ if (!result) {
+ return
+ }
+ const { base64, originalFile, compressFile } = result
+ resolve({ base64, originalFile, compressFile })
+ })
+ }
+ }
+ })
+}
+
+export default onPaste
diff --git a/picx/src/utils/register-sw.ts b/picx/src/utils/register-sw.ts
new file mode 100644
index 0000000..c569ffb
--- /dev/null
+++ b/picx/src/utils/register-sw.ts
@@ -0,0 +1,3 @@
+import { registerSW } from 'virtual:pwa-register'
+
+registerSW()
diff --git a/picx/src/utils/rename-image.ts b/picx/src/utils/rename-image.ts
new file mode 100644
index 0000000..ce25394
--- /dev/null
+++ b/picx/src/utils/rename-image.ts
@@ -0,0 +1,56 @@
+import { computed } from 'vue'
+import { store } from '@/store'
+import createToUploadImageObject from '@/utils/create-to-upload-image'
+import { filenameHandle } from './file-handle-helper'
+
+/**
+ * 根据图片链接获取图片 base64 编码
+ * @param url 图片路径
+ * @param ext 图片格式
+ */
+export function getBase64ByImageUrl(url: string, ext: string): Promise {
+ const canvas = document.createElement('canvas')
+ const ctx = canvas.getContext('2d')
+ const img = new Image()
+ img.crossOrigin = 'Anonymous'
+ img.src = url
+ return new Promise((resolve) => {
+ img.onload = () => {
+ const { width } = img
+ const { height } = img
+ canvas.width = width // 指定画板的高度,自定义
+ canvas.height = height // 指定画板的宽度,自定义
+ ctx?.drawImage(img, 0, 0, width, height) // 参数可自定义
+ const dataURL: string = canvas.toDataURL(`image/${ext}`)
+ resolve(dataURL)
+ }
+ })
+}
+
+// 获取图片对象
+export function getImage(base64Data: string, file: any) {
+ const userSettings = computed(() => store.getters.getUserSettings).value
+ const curImg = createToUploadImageObject()
+
+ curImg.imgData.base64Url = base64Data
+ // eslint-disable-next-line prefer-destructuring
+ curImg.imgData.base64Content = base64Data.split(',')[1]
+
+ const { name, hash, suffix } = filenameHandle(file.name)
+ curImg.uuid = hash
+
+ curImg.fileInfo.size = file.size
+ curImg.fileInfo.originSize = file.size
+ curImg.fileInfo.lastModified = file.lastModified
+
+ curImg.filename.name = name
+ curImg.filename.hash = hash
+ curImg.filename.suffix = suffix
+ curImg.filename.now = userSettings.defaultHash
+ ? `${name}.${hash}.${suffix}`
+ : `${name}.${suffix}`
+ curImg.filename.initName = name
+ curImg.filename.isHashRename = userSettings.defaultHash
+
+ return curImg
+}
diff --git a/picx/src/utils/selected-file-handle.ts b/picx/src/utils/selected-file-handle.ts
new file mode 100644
index 0000000..7cb1f91
--- /dev/null
+++ b/picx/src/utils/selected-file-handle.ts
@@ -0,0 +1,67 @@
+import { store } from '@/store'
+import { compress } from './compress'
+import { getFileSize, isImage } from './file-handle-helper'
+
+export type handleResult = { base64: string; originalFile: File; compressFile?: File }
+
+const selectedFileHandle = async (
+ file: File,
+ maxsize: number
+): Promise => {
+ if (!file) {
+ return null
+ }
+
+ if (!isImage(file.type)) {
+ ElMessage.error('该文件格式不支持!')
+ return null
+ }
+ let compressFile: NonNullable
+ const { isCompress, compressEncoder } = store.getters.getUserSettings
+ const isGif = file.type === 'image/gif'
+ if (!isGif && isCompress) {
+ const loadingInstance = ElLoading.service({
+ target: '.upload-area',
+ text: '正在压缩图片'
+ })
+ compressFile = await compress(file, compressEncoder)
+ loadingInstance.close()
+ }
+
+ return new Promise((resolve) => {
+ const reader = new FileReader()
+ reader.readAsDataURL(!isGif && isCompress ? compressFile : file)
+ reader.onload = (e: ProgressEvent) => {
+ const base64: any = e.target?.result
+ const curImgSize = getFileSize(base64.length)
+
+ if (curImgSize >= maxsize) {
+ // 给出提示,引导用户自行去压缩图片
+ ElMessageBox.confirm(
+ `当前图片 ${(curImgSize / 1024).toFixed(
+ 2
+ )} M,CDN 只能加速小于 50 MB 的图片,建议使用第三方工具 TinyPNG 压缩`,
+ '图片过大,禁止上传',
+ {
+ confirmButtonText: '前往 TinyPNG',
+ cancelButtonText: '放弃上传'
+ }
+ )
+ .then(() => {
+ window.open('https://tinypng.com/')
+ })
+ .catch(() => {
+ console.log('放弃上传')
+ })
+ } else {
+ resolve({
+ base64,
+ originalFile: file,
+ compressFile: !isGif && isCompress ? compressFile : file
+ })
+ }
+ }
+ })
+}
+
+export default selectedFileHandle
diff --git a/picx/src/utils/set-theme-mode.ts b/picx/src/utils/set-theme-mode.ts
new file mode 100644
index 0000000..4fcb0e3
--- /dev/null
+++ b/picx/src/utils/set-theme-mode.ts
@@ -0,0 +1,51 @@
+import { watch, nextTick } from 'vue'
+import { useStore } from '@/store'
+import { UserSettingsModel } from '@/common/model/user-settings.model'
+
+const setThemeMode = () => {
+ const store = useStore()
+
+ const setBodyClassName = async (theme: 'dark' | 'light') => {
+ await nextTick(() => {
+ const body = document.getElementsByTagName('html')[0]
+ if (theme === 'dark') {
+ body.classList.remove('light')
+ body.classList.add('dark')
+ } else {
+ body.classList.remove('dark')
+ body.classList.add('light')
+ }
+ })
+ }
+
+ const autoThemeModeTimeHandle = (autoLightThemeTime: string[]) => {
+ const getTimestamp = (i: number) => {
+ const D = new Date()
+ const yyyy = D.getFullYear()
+ const mm = D.getMonth() + 1
+ const dd = D.getDate()
+ return new Date(`${yyyy}/${mm}/${dd} ${autoLightThemeTime[i]}:00`).getTime()
+ }
+ const now = Date.now()
+ return getTimestamp(0) <= now && now <= getTimestamp(1)
+ }
+
+ const setThemeByConfigFn = (settings: UserSettingsModel) => {
+ const { themeMode, autoLightThemeTime } = settings
+ if (themeMode === 'auto') {
+ setBodyClassName(autoThemeModeTimeHandle(autoLightThemeTime) ? 'light' : 'dark')
+ } else {
+ setBodyClassName(themeMode)
+ }
+ }
+
+ watch(
+ (): UserSettingsModel => store.getters.getUserSettings,
+ (newValue) => {
+ setThemeByConfigFn(newValue)
+ },
+ { deep: true, immediate: true }
+ )
+}
+
+export default setThemeMode
diff --git a/picx/src/utils/time-helper.ts b/picx/src/utils/time-helper.ts
new file mode 100644
index 0000000..ba84dca
--- /dev/null
+++ b/picx/src/utils/time-helper.ts
@@ -0,0 +1,26 @@
+export default class TimeHelper {
+ private static zerofill(n: number) {
+ return n < 10 ? `0${n}` : n
+ }
+
+ static getYyyyMmDd(now: number = Date.now()) {
+ const date: Date = new Date(now)
+ const yyyy = date.getFullYear()
+ const MM = date.getMonth() + 1
+ const DD = date.getDate()
+ return `${yyyy}${this.zerofill(MM)}${this.zerofill(DD)}`
+ }
+
+ static formatTimestamp(now: number = Date.now()) {
+ const date: Date = new Date(now)
+ const YYYY = date.getFullYear()
+ const MM = date.getMonth() + 1
+ const DD = date.getDate()
+ const hh = date.getHours()
+ const mm = date.getMinutes()
+ const ss = date.getSeconds()
+ return `${YYYY}-${this.zerofill(MM)}-${this.zerofill(DD)} ${this.zerofill(
+ hh
+ )}:${this.zerofill(mm)}:${this.zerofill(ss)}`
+ }
+}
diff --git a/picx/src/utils/upload-helper.ts b/picx/src/utils/upload-helper.ts
new file mode 100644
index 0000000..982b85b
--- /dev/null
+++ b/picx/src/utils/upload-helper.ts
@@ -0,0 +1,134 @@
+import { UserConfigInfoModel } from '@/common/model/user-config-info.model'
+import { ToUploadImageModel, UploadedImageModel } from '@/common/model/upload.model'
+import axios from '@/utils/axios'
+import { store } from '@/store'
+import { generateExternalLink } from '@/utils/external-link-handler'
+import ExternalLinkType from '@/common/model/external-link.model'
+
+export const uploadUrlHandle = (
+ config: UserConfigInfoModel,
+ filename: string
+): string => {
+ let path = ''
+ if (config.selectedDir !== '/') {
+ path = `${config.selectedDir}/`
+ }
+ return `/repos/${config.owner}/${config.selectedRepos}/contents/${path}${filename}`
+}
+
+export function uploadImage_single(
+ userConfigInfo: UserConfigInfoModel,
+ img: ToUploadImageModel
+): Promise {
+ const { selectedBranch, email, owner } = userConfigInfo
+ // eslint-disable-next-line no-param-reassign
+ img.uploadStatus.uploading = true
+
+ const data: any = {
+ message: 'Upload picture via PicX(https://github.com/XPoet/picx)',
+ branch: selectedBranch,
+ content: img.imgData.base64Content
+ }
+
+ if (email) {
+ data.committer = {
+ name: owner,
+ email
+ }
+ }
+
+ return new Promise((resolve, reject) => {
+ axios
+ .put(uploadUrlHandle(userConfigInfo, img.filename.now), data)
+ .then((res) => {
+ if (res && res.status === 201) {
+ // eslint-disable-next-line no-use-before-define
+ uploadedHandle(res, img, userConfigInfo)
+ store.dispatch('TO_UPLOAD_IMAGE_UPLOADED', img.uuid)
+ resolve(true)
+ } else {
+ // eslint-disable-next-line no-param-reassign
+ img.uploadStatus.uploading = false
+ resolve(false)
+ }
+ })
+ .catch((error) => {
+ reject(error)
+ })
+ })
+}
+
+function uploadedHandle(
+ res: any,
+ img: ToUploadImageModel,
+ userConfigInfo: UserConfigInfoModel
+) {
+ const userSettings = store.getters.getUserSettings
+
+ // 上传状态处理
+ // eslint-disable-next-line no-param-reassign
+ img.uploadStatus.progress = 100
+ // eslint-disable-next-line no-param-reassign
+ img.uploadStatus.uploading = false
+
+ // 生成 GitHub 外链
+ // eslint-disable-next-line no-param-reassign
+ img.externalLink.github = generateExternalLink(
+ ExternalLinkType.github,
+ res.data.content,
+ userConfigInfo
+ )
+
+ // 生成 jsDelivr CDN 外链
+ // eslint-disable-next-line no-param-reassign
+ img.externalLink.jsdelivr = generateExternalLink(
+ ExternalLinkType.jsdelivr,
+ res.data.content,
+ userConfigInfo
+ )
+
+ // 生成 Staticaly CDN 外链
+ // eslint-disable-next-line no-param-reassign
+ img.externalLink.staticaly = generateExternalLink(
+ ExternalLinkType.staticaly,
+ res.data.content,
+ userConfigInfo
+ )
+
+ // 生成 Cloudflare CDN 外链
+ // eslint-disable-next-line no-param-reassign
+ img.externalLink.cloudflare = generateExternalLink(
+ ExternalLinkType.cloudflare,
+ res.data.content,
+ userConfigInfo
+ )
+
+ const item: UploadedImageModel = {
+ checked: false,
+ type: 'image',
+ uuid: img.uuid,
+ dir: userConfigInfo.selectedDir,
+ name: res.data.content.name,
+ path: res.data.content.path,
+ sha: res.data.content.sha,
+ github_url: img.externalLink.github,
+ jsdelivr_cdn_url: img.externalLink.jsdelivr,
+ staticaly_cdn_url: img.externalLink.staticaly,
+ cloudflare_cdn_url: img.externalLink.cloudflare,
+ is_transform_md: userSettings.defaultMarkdown,
+ deleting: false,
+ size: img.fileInfo.size
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ img.uploadedImg = item
+
+ // uploadedList 增加图片
+ store.dispatch('UPLOADED_LIST_ADD', item)
+
+ // dirImageList 增加目录
+ store.dispatch('DIR_IMAGE_LIST_ADD_DIR', userConfigInfo.selectedDir)
+
+ // dirImageList 增加图片
+ store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', item)
+}
diff --git a/picx/src/views/about/about.styl b/picx/src/views/about/about.styl
new file mode 100644
index 0000000..99ae5ac
--- /dev/null
+++ b/picx/src/views/about/about.styl
@@ -0,0 +1,25 @@
+.feedback-page-container {
+
+ .help-info-item {
+ font-size: 16rem;
+ padding: 6rem;
+ display: flex;
+ align-items: center;
+ margin-bottom: 10rem;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+
+ }
+
+ .description {
+ font-weight: bold;
+ line-height: 28rem;
+ }
+
+ .red-text {
+ color: #de1a1a;
+ }
+}
diff --git a/picx/src/views/about/about.vue b/picx/src/views/about/about.vue
new file mode 100644
index 0000000..6b26e69
--- /dev/null
+++ b/picx/src/views/about/about.vue
@@ -0,0 +1,77 @@
+
+
+
+ PicX 是一款基于 GitHub API & jsDelivr 开发的具有 CDN 加速功能的图床工具。
+
+ 无需下载!无需安装!打开网站即用!免费!极速!稳定!
+
+
+
建议将本站添加至浏览器收藏夹,方便下次使用 😊
+
+
+ 作者:
+ @XPoet
+
+
+
+ 仓库:
+
+ https://github.com/XPoet/picx
+
+
+
+
+ 教程:
+
+ https://github.com/XPoet/picx/blob/master/README.md
+
+
+
+
+ 在使用过程中遇到问题,请仔细阅读文档,或者给作者提
+
+ Issue
+
+ 。
+
+
+
+ 图片上传缓慢或加载不出来等情况,可借助
+ VPN 工具
+
+ 。
+
+
+
+
+ 郑重声明:请勿通过本站上传违反你当地法律的图片,所造成的一切后果与本站无关。
+
+
+
+
+
+
+
+
diff --git a/picx/src/views/config/config.styl b/picx/src/views/config/config.styl
new file mode 100644
index 0000000..095223b
--- /dev/null
+++ b/picx/src/views/config/config.styl
@@ -0,0 +1,13 @@
+.config-page-container {
+ .operation {
+ text-align right
+
+ .el-button {
+ margin-left 20rem
+
+ &:first-child {
+ margin-left 0
+ }
+ }
+ }
+}
diff --git a/picx/src/views/config/config.vue b/picx/src/views/config/config.vue
new file mode 100644
index 0000000..8e9b540
--- /dev/null
+++ b/picx/src/views/config/config.vue
@@ -0,0 +1,498 @@
+
+
+
+
+
+
+
+
+
+
+ 确认 Token
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择 {{ userConfigInfo.selectedRepos }} 仓库下的分支
+
+
+
+ 新建分支
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新建目录
+
+
+
+ 根目录
+
+
+
+ 自动目录
+
+
+
+
+ 选择 {{ userConfigInfo.selectedRepos }} 仓库目录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 重置
+
+
+ 完成配置
+
+
+
+
+
+
+
+
+
diff --git a/picx/src/views/management/management.styl b/picx/src/views/management/management.styl
new file mode 100644
index 0000000..2d43c51
--- /dev/null
+++ b/picx/src/views/management/management.styl
@@ -0,0 +1,70 @@
+@import "../../style/base.styl"
+
+$infoBarHeight = 50rem
+
+.management-page-container {
+ padding-bottom 0 !important
+
+ .content-container {
+ position relative
+ width 100%
+ height 100%
+ padding-top $infoBarHeight
+ box-sizing border-box
+
+ .top {
+ position absolute
+ top 0
+ left 0
+ width 100%
+ height $infoBarHeight
+ box-sizing border-box
+ display flex
+ align-items center
+ justify-content space-between
+ font-size 14rem
+ padding-bottom 20rem
+
+ .right {
+
+ .btn-icon {
+ cursor pointer
+ font-size 22rem
+ margin-left 10rem
+ }
+
+ }
+ }
+
+
+ .bottom {
+ position relative
+ width 100%
+ height 100%
+ box-sizing border-box
+ border 1rem solid var(--border-color)
+
+ .image-list {
+ width 100%
+ //height 100%
+ //max-height calc(100% - 60rem)
+ margin 0
+ padding 2rem
+ list-style none
+ overflow-y auto
+ box-sizing border-box
+
+ li.image-item {
+ float left
+ box-sizing border-box
+ padding 10rem
+
+ &:last-child {
+ margin-right 0
+ }
+ }
+ }
+
+ }
+ }
+}
diff --git a/picx/src/views/management/management.util.ts b/picx/src/views/management/management.util.ts
new file mode 100644
index 0000000..4ba691a
--- /dev/null
+++ b/picx/src/views/management/management.util.ts
@@ -0,0 +1,57 @@
+import { Store } from 'vuex'
+import { DirObject } from '@/store/modules/dir-image-list/types'
+
+function getContent(targetContent: any, dirList: string[], n: number): any {
+ if (targetContent) {
+ if (dirList.length === n) {
+ return targetContent
+ }
+ return getContent(
+ targetContent.childrenDirs?.find((v: any) => v.dir === dirList[n]),
+ dirList,
+ // eslint-disable-next-line no-param-reassign,no-plusplus
+ ++n
+ )
+ }
+ return null
+}
+
+/**
+ * 获取当前目录下所有内容(子目录和图片)
+ * @param dirPath
+ * @param dirObj
+ */
+export const getDirContent = (dirPath: string, dirObj: DirObject) => {
+ if (dirPath === '/') {
+ return dirObj
+ }
+ const dirList: string[] = dirPath.split('/')
+ return getContent(dirObj, dirList, 0)
+}
+
+/**
+ * 过滤当前目录的内容(子目录或图片)
+ * @param dirPath
+ * @param content
+ * @param type
+ */
+export const filterDirContent = (dirPath: string, content: any, type: string): any => {
+ if (type === 'dir') {
+ return content.childrenDirs?.filter((x: any) => x.type === 'dir')
+ }
+
+ if (type === 'image') {
+ return content.imageList.filter((x: any) => x.type === 'image')
+ }
+
+ return []
+}
+
+export const dirModeHandle = (dir: string, store: Store) => {
+ if (dir === '/') {
+ store.dispatch('SET_USER_CONFIG_INFO', {
+ dirMode: 'rootDir',
+ needPersist: false
+ })
+ }
+}
diff --git a/picx/src/views/management/management.vue b/picx/src/views/management/management.vue
new file mode 100644
index 0000000..4edcdb5
--- /dev/null
+++ b/picx/src/views/management/management.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/picx/src/views/settings/settings.styl b/picx/src/views/settings/settings.styl
new file mode 100644
index 0000000..43fe857
--- /dev/null
+++ b/picx/src/views/settings/settings.styl
@@ -0,0 +1,44 @@
+.setting-title {
+ font-size 16rem
+ font-weight bold
+ margin 40rem 0 20rem 0
+
+ &:first-child {
+ margin-top 0
+ }
+}
+
+.setting-list {
+ padding 0
+ margin 0
+
+ .setting-item {
+ margin-bottom 10rem
+
+ &.last-child {
+ margin-bottom 0
+ }
+
+ .prefix-input {
+ width calc(100% - 50rem)
+ margin-left 50rem
+ margin-top 15rem
+ }
+
+ .img-encoder-title {
+ margin-bottom 12rem
+ }
+
+ :deep() .el-radio-group {
+
+ display inline-block
+
+ .el-radio {
+ display block
+ }
+
+ }
+
+ }
+
+}
diff --git a/picx/src/views/settings/settings.vue b/picx/src/views/settings/settings.vue
new file mode 100644
index 0000000..69834c3
--- /dev/null
+++ b/picx/src/views/settings/settings.vue
@@ -0,0 +1,149 @@
+
+
+
个性设置:
+
+
+
CDN 提供商:
+
+
+
压缩设置:
+
+ -
+
+
+ -
+
选择图像编码器(压缩算法):
+
+
+ {{ compressEncoder.webP }} (压缩后图片格式为 webp,大多数现代浏览器支持)
+
+
+ {{ compressEncoder.mozJPEG }} (压缩后图片格式为 jpg,兼容性最好)
+
+
+ {{ compressEncoder.avif }}
+ (压缩后图片格式为 avif,压缩比最高,目前仅谷歌浏览器支持)
+
+
+
+
+
+
主题设置:
+
+
+
+ 设置白昼模式时间区间:
+
+
+
+
+
+
+
+
diff --git a/picx/src/views/tutorials/tutorials.styl b/picx/src/views/tutorials/tutorials.styl
new file mode 100644
index 0000000..3ddce1a
--- /dev/null
+++ b/picx/src/views/tutorials/tutorials.styl
@@ -0,0 +1,15 @@
+.tutorials-page-container {
+
+ .step-content {
+ padding-top 30rem
+ display flex
+ justify-content center
+ text-align center
+ }
+
+ .btn-next-prev {
+ text-align center
+ padding-top 30rem
+ }
+
+}
diff --git a/picx/src/views/tutorials/tutorials.vue b/picx/src/views/tutorials/tutorials.vue
new file mode 100644
index 0000000..72ee2d3
--- /dev/null
+++ b/picx/src/views/tutorials/tutorials.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
👍 恭喜你完成图床使用教程 🌟 快去试试吧~
+ https://github.com/XPoet/picx
+
+
+
+ 上一步
+ 下一步
+
+
+
+
+
+
+
diff --git a/picx/src/views/upload/upload.styl b/picx/src/views/upload/upload.styl
new file mode 100644
index 0000000..cbc313f
--- /dev/null
+++ b/picx/src/views/upload/upload.styl
@@ -0,0 +1,135 @@
+@import "../../style/base.styl"
+
+.upload-page-container {
+ width 100%
+ height 100%
+ display flex
+ justify-content space-between
+
+ .upload-page-left {
+ height 100%
+ box-sizing border-box
+ margin-right $component-interval
+ flex-shrink 0
+
+ .uploaded-item {
+ margin-bottom 20rem
+
+ &:last-child {
+ margin-bottom 0
+ }
+ }
+ }
+
+
+ .upload-page-right {
+ width 100%
+ height 100%
+ box-sizing border-box
+ overflow-y auto
+
+ .row-item {
+ width 100%
+ display flex
+ justify-content center
+ margin-bottom 16rem
+ box-sizing border-box
+
+ &:last-child {
+ margin-bottom 0
+ }
+
+ .content-box {
+ width 100%
+ max-width $content-max-width
+ margin 0 auto
+ box-sizing border-box
+ }
+
+ }
+
+
+ .upload-status {
+ position relative
+ width 100%
+ padding 10rem
+ background var(--second-background-color)
+ color #666
+ font-size 12rem
+ box-sizing border-box
+
+ .info-item {
+ margin-top 4rem
+ }
+
+ .file-status {
+ display flex
+ justify-content space-between
+ align-items center
+ }
+
+ .upload-tips {
+
+ display flex
+ align-items center
+
+ i {
+ margin-left 2rem
+ font-size 20rem
+ }
+ }
+
+ .wait-upload {
+ color var(--await-upload-color)
+ }
+
+ .uploading {
+ color var(--uploading-color)
+ }
+
+ .uploaded {
+ color var(--uploaded-color)
+ }
+
+ }
+
+
+ .external-link {
+ width 100%
+
+ .external-link-input {
+ margin-bottom 10rem
+
+ &:last-child {
+ margin-bottom 0
+ }
+
+ .el-input-group__append {
+ width 100rem
+ text-align-last justify
+ }
+ }
+ }
+
+
+ .upload-tools {
+ width 100%
+
+ .repos-dir-info {
+ margin-bottom 20rem
+ font-size 12rem
+
+ .repos-dir-info-item {
+ margin-right 10rem
+
+ &:last-child {
+ margin-right 0
+ }
+ }
+
+
+ }
+ }
+ }
+
+}
diff --git a/picx/src/views/upload/upload.vue b/picx/src/views/upload/upload.vue
new file mode 100644
index 0000000..5ce57f5
--- /dev/null
+++ b/picx/src/views/upload/upload.vue
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+