From 287f5058c7384464a00463fe91093c0f33c5068d Mon Sep 17 00:00:00 2001 From: Mujinniao <78023097+Mujinniao@users.noreply.github.com> Date: Mon, 21 Nov 2022 17:02:50 +0800 Subject: [PATCH] Add files via upload --- picx/src/App.vue | 74 +++ picx/src/assets/logo.png | Bin 0 -> 34955 bytes picx/src/common/api/index.ts | 82 +++ picx/src/common/model/delete.model.ts | 6 + picx/src/common/model/dir.model.ts | 11 + picx/src/common/model/external-link.model.ts | 8 + picx/src/common/model/storage.model.ts | 6 + picx/src/common/model/upload.model.ts | 68 +++ .../common/model/user-config-info.model.ts | 35 ++ picx/src/common/model/user-settings.model.ts | 15 + picx/src/common/model/vite-config.model.ts | 11 + .../copy-external-link.styl | 62 +++ .../copy-external-link/copy-external-link.vue | 104 ++++ .../components/folder-card/folder-card.styl | 47 ++ .../components/folder-card/folder-card.vue | 116 ++++ .../header-content/header-content.styl | 92 ++++ .../header-content/header-content.vue | 122 +++++ .../src/components/image-card/image-card.styl | 142 +++++ picx/src/components/image-card/image-card.vue | 287 ++++++++++ .../image-selector/image-selector.styl | 52 ++ .../image-selector/image-selector.vue | 103 ++++ .../components/image-viewer/image-viewer.styl | 75 +++ .../components/image-viewer/image-viewer.vue | 44 ++ .../main-container/main-container.styl | 73 +++ .../main-container/main-container.vue | 64 +++ .../components/nav-content/nav-content.styl | 40 ++ .../components/nav-content/nav-content.vue | 152 ++++++ .../selected-info-bar/selected-info-bar.styl | 16 + .../selected-info-bar/selected-info-bar.vue | 35 ++ picx/src/components/site-count/site-count.vue | 58 ++ .../to-upload-image-card.styl | 211 ++++++++ .../to-upload-image-card.vue | 300 +++++++++++ picx/src/components/tutorials-step/step1.vue | 40 ++ picx/src/components/tutorials-step/step2.vue | 54 ++ picx/src/components/tutorials-step/step3.vue | 62 +++ .../components/upload-area/upload-area.styl | 57 ++ .../components/upload-area/upload-area.vue | 148 ++++++ picx/src/main.ts | 21 + picx/src/plugins/index.ts | 32 ++ picx/src/plugins/pwa.ts | 24 + picx/src/router/index.ts | 82 +++ picx/src/shims-vue.d.ts | 6 + picx/src/store/index.ts | 57 ++ .../src/store/modules/dir-image-list/index.ts | 260 +++++++++ .../src/store/modules/dir-image-list/types.ts | 14 + .../src/store/modules/dir-image-list/utils.ts | 73 +++ picx/src/store/modules/image-card/index.ts | 41 ++ picx/src/store/modules/image-card/types.ts | 5 + picx/src/store/modules/image-viewer/index.ts | 29 + picx/src/store/modules/image-viewer/types.ts | 12 + .../store/modules/to-upload-image/index.ts | 92 ++++ .../store/modules/to-upload-image/types.ts | 6 + .../store/modules/upload-area-active/index.ts | 25 + .../store/modules/upload-area-active/types.ts | 3 + .../store/modules/upload-settings/index.ts | 24 + .../store/modules/upload-settings/types.ts | 6 + .../modules/uploaded-image-list/index.ts | 53 ++ .../modules/uploaded-image-list/types.ts | 5 + .../store/modules/user-config-info/index.ts | 155 ++++++ .../store/modules/user-config-info/types.ts | 5 + picx/src/store/modules/user-settings/index.ts | 67 +++ picx/src/store/modules/user-settings/types.ts | 5 + picx/src/store/types.ts | 21 + picx/src/style/base.styl | 98 ++++ picx/src/style/theme.styl | 25 + picx/src/style/variables.styl | 110 ++++ picx/src/utils/axios.ts | 51 ++ picx/src/utils/common-utils.ts | 27 + picx/src/utils/compress.ts | 26 + picx/src/utils/create-to-upload-image.ts | 42 ++ picx/src/utils/delete-image-card.ts | 64 +++ picx/src/utils/env.ts | 23 + picx/src/utils/external-link-handler.ts | 178 +++++++ picx/src/utils/file-handle-helper.ts | 50 ++ picx/src/utils/image-helper.ts | 42 ++ picx/src/utils/object-helper.ts | 59 +++ picx/src/utils/paste.ts | 28 + picx/src/utils/register-sw.ts | 3 + picx/src/utils/rename-image.ts | 56 ++ picx/src/utils/selected-file-handle.ts | 67 +++ picx/src/utils/set-theme-mode.ts | 51 ++ picx/src/utils/time-helper.ts | 26 + picx/src/utils/upload-helper.ts | 134 +++++ picx/src/views/about/about.styl | 25 + picx/src/views/about/about.vue | 77 +++ picx/src/views/config/config.styl | 13 + picx/src/views/config/config.vue | 498 ++++++++++++++++++ picx/src/views/management/management.styl | 70 +++ picx/src/views/management/management.util.ts | 57 ++ picx/src/views/management/management.vue | 201 +++++++ picx/src/views/settings/settings.styl | 44 ++ picx/src/views/settings/settings.vue | 149 ++++++ picx/src/views/tutorials/tutorials.styl | 15 + picx/src/views/tutorials/tutorials.vue | 64 +++ picx/src/views/upload/upload.styl | 135 +++++ picx/src/views/upload/upload.vue | 151 ++++++ 96 files changed, 6654 insertions(+) create mode 100644 picx/src/App.vue create mode 100644 picx/src/assets/logo.png create mode 100644 picx/src/common/api/index.ts create mode 100644 picx/src/common/model/delete.model.ts create mode 100644 picx/src/common/model/dir.model.ts create mode 100644 picx/src/common/model/external-link.model.ts create mode 100644 picx/src/common/model/storage.model.ts create mode 100644 picx/src/common/model/upload.model.ts create mode 100644 picx/src/common/model/user-config-info.model.ts create mode 100644 picx/src/common/model/user-settings.model.ts create mode 100644 picx/src/common/model/vite-config.model.ts create mode 100644 picx/src/components/copy-external-link/copy-external-link.styl create mode 100644 picx/src/components/copy-external-link/copy-external-link.vue create mode 100644 picx/src/components/folder-card/folder-card.styl create mode 100644 picx/src/components/folder-card/folder-card.vue create mode 100644 picx/src/components/header-content/header-content.styl create mode 100644 picx/src/components/header-content/header-content.vue create mode 100644 picx/src/components/image-card/image-card.styl create mode 100644 picx/src/components/image-card/image-card.vue create mode 100644 picx/src/components/image-selector/image-selector.styl create mode 100644 picx/src/components/image-selector/image-selector.vue create mode 100644 picx/src/components/image-viewer/image-viewer.styl create mode 100644 picx/src/components/image-viewer/image-viewer.vue create mode 100644 picx/src/components/main-container/main-container.styl create mode 100644 picx/src/components/main-container/main-container.vue create mode 100644 picx/src/components/nav-content/nav-content.styl create mode 100644 picx/src/components/nav-content/nav-content.vue create mode 100644 picx/src/components/selected-info-bar/selected-info-bar.styl create mode 100644 picx/src/components/selected-info-bar/selected-info-bar.vue create mode 100644 picx/src/components/site-count/site-count.vue create mode 100644 picx/src/components/to-upload-image-card/to-upload-image-card.styl create mode 100644 picx/src/components/to-upload-image-card/to-upload-image-card.vue create mode 100644 picx/src/components/tutorials-step/step1.vue create mode 100644 picx/src/components/tutorials-step/step2.vue create mode 100644 picx/src/components/tutorials-step/step3.vue create mode 100644 picx/src/components/upload-area/upload-area.styl create mode 100644 picx/src/components/upload-area/upload-area.vue create mode 100644 picx/src/main.ts create mode 100644 picx/src/plugins/index.ts create mode 100644 picx/src/plugins/pwa.ts create mode 100644 picx/src/router/index.ts create mode 100644 picx/src/shims-vue.d.ts create mode 100644 picx/src/store/index.ts create mode 100644 picx/src/store/modules/dir-image-list/index.ts create mode 100644 picx/src/store/modules/dir-image-list/types.ts create mode 100644 picx/src/store/modules/dir-image-list/utils.ts create mode 100644 picx/src/store/modules/image-card/index.ts create mode 100644 picx/src/store/modules/image-card/types.ts create mode 100644 picx/src/store/modules/image-viewer/index.ts create mode 100644 picx/src/store/modules/image-viewer/types.ts create mode 100644 picx/src/store/modules/to-upload-image/index.ts create mode 100644 picx/src/store/modules/to-upload-image/types.ts create mode 100644 picx/src/store/modules/upload-area-active/index.ts create mode 100644 picx/src/store/modules/upload-area-active/types.ts create mode 100644 picx/src/store/modules/upload-settings/index.ts create mode 100644 picx/src/store/modules/upload-settings/types.ts create mode 100644 picx/src/store/modules/uploaded-image-list/index.ts create mode 100644 picx/src/store/modules/uploaded-image-list/types.ts create mode 100644 picx/src/store/modules/user-config-info/index.ts create mode 100644 picx/src/store/modules/user-config-info/types.ts create mode 100644 picx/src/store/modules/user-settings/index.ts create mode 100644 picx/src/store/modules/user-settings/types.ts create mode 100644 picx/src/store/types.ts create mode 100644 picx/src/style/base.styl create mode 100644 picx/src/style/theme.styl create mode 100644 picx/src/style/variables.styl create mode 100644 picx/src/utils/axios.ts create mode 100644 picx/src/utils/common-utils.ts create mode 100644 picx/src/utils/compress.ts create mode 100644 picx/src/utils/create-to-upload-image.ts create mode 100644 picx/src/utils/delete-image-card.ts create mode 100644 picx/src/utils/env.ts create mode 100644 picx/src/utils/external-link-handler.ts create mode 100644 picx/src/utils/file-handle-helper.ts create mode 100644 picx/src/utils/image-helper.ts create mode 100644 picx/src/utils/object-helper.ts create mode 100644 picx/src/utils/paste.ts create mode 100644 picx/src/utils/register-sw.ts create mode 100644 picx/src/utils/rename-image.ts create mode 100644 picx/src/utils/selected-file-handle.ts create mode 100644 picx/src/utils/set-theme-mode.ts create mode 100644 picx/src/utils/time-helper.ts create mode 100644 picx/src/utils/upload-helper.ts create mode 100644 picx/src/views/about/about.styl create mode 100644 picx/src/views/about/about.vue create mode 100644 picx/src/views/config/config.styl create mode 100644 picx/src/views/config/config.vue create mode 100644 picx/src/views/management/management.styl create mode 100644 picx/src/views/management/management.util.ts create mode 100644 picx/src/views/management/management.vue create mode 100644 picx/src/views/settings/settings.styl create mode 100644 picx/src/views/settings/settings.vue create mode 100644 picx/src/views/tutorials/tutorials.styl create mode 100644 picx/src/views/tutorials/tutorials.vue create mode 100644 picx/src/views/upload/upload.styl create mode 100644 picx/src/views/upload/upload.vue diff --git a/picx/src/App.vue b/picx/src/App.vue new file mode 100644 index 0000000..d239c0c --- /dev/null +++ b/picx/src/App.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/picx/src/assets/logo.png b/picx/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a33970843f08ce369553bc7c592b0c759add947a GIT binary patch literal 34955 zcmX`S2RNJW7dM`miCGj?vs4vD?W$3m+M5y-wO491#0sLVP0iY~c7hhMiCLRgL#0UV znzgmcoA2-c{@+|zuH<=ga_)J~{W<4+o>(ITEn2GEQ~&^g_NlhI2>?I>C%!{Kq{KUF z_s4Pp0HChBnN@(5zTRUeUvE)|7ru_pqCwtJ;_m=}!jmAVgOjIo0FR@ytGkaP?|yqX zFOT~RMP5s3{fGKcHD@<>ZMdJaDcr!!3GV46_k#C{5|u*GW1<3Y=Ku$uAa5@p|Hna! zy#LetnE3s_+hV*t{}T!DROD6puR$IweIp(3KO z6NO2N`ue$wiOb2!i9M7MlaLT0N{ILe`vf=yiTL>Q{iovp)1mI{@8stW4RH7M;rUOe zgQIU?fFdui*nf?P{ohuI!xQ^|<0D@DzZr7&Ax?`Qar`1`>Mg{%BXd>JR{;PTlBq8p z$cfiLe-kZL0BV9`3jiPm80nd55N}$R);VI|J7RNQ3}%^Re{$^3bN-n3VmQODt4QwZ#wMSik$|st*UBWM$?Smwc27zo_7{L4eBR;(Fw;sh4ovM)I0R#IS*$T zXEs~cS6VkzxQ?f~O((fdq?+Y7>c8!J`l|PNSAju%he2XHq^V3hu*ab{&wV=4X)sFz z5ouKemA5T4O2<5_u5p`8v2QQdi|Tdk$=83~ZH=lX{vFa-3HkKFWi;I=t<^BG!z8=e zsuD%)1F_Fe*nAUWqYagij}=Y>?~PL1tm}wMQmxTdw#{XRDa6sC^boyIUk%!~me^uS z4dXhWRo59NbeLpd9J>pM{dXSBwkktEtEzieQ}eu|*rK%lY4Ct`ebuwNYT|@AV)Gn( z^DRmmbVB>|V!Cxg`VA90-DeWrrV<^y3QgZP>Bn|C_7u8|r`fiaxJ{(GjAU3(&W^i=Qf#S zT~lLGhSrPfaT?5Y>dW!`8s|KeZJ5~ZJd|OS)}|ARC2lI0@l=b#Myt|#_b&-nRWm98s1BVxByFd_Fh?{UR1v)wR{MYA3Gu*JKg*G;(Z+t(3kg3tgCe>bxi>goZ0 zj*e+_bJtpH{ev=3_4JaG-o8zslBNu|kL(M2pkkZ~@M%b77Z?~C2y{EyMu&`s3jsGO zfc&Hsm-`5JQVK{{NBF;N@olkWwxiXw`I2Y(qF8#KcIxp3H85?I(0fx}<01Bd6GTRDEy1=2Blwo1Z@rmj65Uh8tyPYh*N4 z;R&%`Iqu&~SuX0?mxAs*7iCmMshrVyeye>(qNGbjokb&dX^vQgl63-a9$y(o*~!p$ zO4dAiP+3(8D2(gKxsB*8(*yKY_V_-6)*2Y82V@QY=Cq!*fB%>{^n-zI;|p2e{mWU}wy@2fc@l zk){u=gItK7Bhbcj3kCz_ktDZVz-G$V^2Uhg?XdGNJ5R>%ZEUzY%uA+2Ri;VJ>GQoS z{F>K}W1~;R?kn&h|5eO2z4e|V7#=8(DN|23_i4Ei|u~c zfvJUN5dnb>d{G4lg4+>>5{ib(|dW9RCj-w!^qbvxt-z+W1<< z5%ddi`Ahji?ST3KJ!v5iXzz|bqGvl+o(*~RDoiobF6VfX-gk;r*dAn}wJq%LujbnCjcarWdu=PDAVhfBz7J(Jr}B?( zFI99n{9gcxB0>=iFIuG4nvw?>Sy~*L#?ruV1oIwSkZwAo2mi>R*ICXbAE7mA;eUCK zC-U3L(f%PJ-__xY*`GU<+eJz`i+kG_s?(@uzctJOJgIKPO7Jm^sbrB0_hv-OM#5uy}=ylu~GYm*NIgswCvAY5nPG7JpxNl2tc ztM#dkN7fXc6%4Bz?JdF$0bGd1Cs)9fGjs%aI4#LI(=TSmf63G63F3bFX0snjug=Yl6c8Wd2L?2RU#f>Qx~W7XyC~L@1Z{It`bDB;2nZH5PeanlnCfd@ zl%=nRR^`Df=wpAtDw33akwoAYY_4J6&B|OJ2$rFo+Tc8Ezek+I!_V7=Koog|5-Ehb z=j=9Osr&0Va}E@^gX*DP%8_pI6-#9hm9hj73dpEhXyPHU(9}AC8+&ZZn0Z$!9>O9c zs6|tQkQ9Y%--p;vnL?HLZug;DNt_3d;keb4o9jABZLK8LT$t^D zcI2QI8DZ+s#>SCR`45!24~iWkJqs%sb}I^%_@D0!0z)4&qa2E>6xyj;4Acm!`Nqg} zO_s;GJf|Ze_xN-5*Xsel;YPh7t6vPk$97uj@o@zou>a)EPms8TtzT%iz8G8r+p2Ua zS*Ht)QN;N02YX2#!uk(YG+SKt8w4eom8CN*9Xo(#JE*oqLeI1qYT+S`&{^T`R(IQA z3em7g3qC^(VE`7s`bA5rJg&eWDgR>y)S{z;*{qevOIA4q6&<(!QwD?Cs~>I z?jbsJW$)~Ls~a7+os*R8e?dh{O{+~O%b}}!*`O5>ay+dC{NsiVi{pj32|8h8G8?(D zDlR#dUW8LTL%goxv6PLJ1~ zjh-U<;~p{Coyt&Z`N3>`f&*|daF-d4)8g(tz-oyp{6Zt+i_QvT*s2^-V5D%Ivy0Bg zRhq+ipH>=lX{HA1+HOejSUoWc1#qcjo=L=n+)}2`Kcb%OK6jkd?2Xhd-2OE1A}A^B zuF5{cXI!82KGVpYzolDsl$_Lwd_(#fDAVu4;eY{=MV)%~GhX>6;DDUGXG3UN|7x=| znQH(UUrY(IROf<^2kF+Q-}wVNm}2nYqTs%VcZ~>VxgmN_2sx{w)iKew?J+k(Kj;Cu@4`sq`rMXJ3KiJGI*?G|Y|t6(FY@ot zn+NY_(C&axP0NleK)|gq5WIW-E~E?^^X5~;rJAIWP_}Riw0EOf9TU}B!o~c*byN@% zx(~s&Sgh;NJ=Kpz>W(q1V&))%e@dwo%YZS$?}ZW2hE@`trC^Yv(aACxF{!=GX|u?! zO8;|W&^B2_$!KyHW$EC%PKDWCa~q_5cph%qPbpLN)nL9GD)jX@v)AV5G3IuWWy`PDJ+5=^!0y`bB;mT*N=Bvbs9~F)Udxx_ zn%r$$kvWv6BlUYv*Qq*o8T`p1CxAWCp6{~GBU$$0a+UTjfKYr`$9n!!M1&kF=$}1+ z8_=FC^ zbyHfH4KIeheI2Ywiv}k80}V-qvxi%c2O$8PdrX1}A&!~Tm~cfg&MX}Bx2+QTB<<#& zjbBVn29D`<-j7T{ysF3WyrB`H;OlL_W^1|C2lV>fmByd9!D9f0u+RhbgOK>=tol8JqpTWp?yAJ8eQC#x0+TmS6s3#2sbOToJ77FctaGqSgSJS4yLE3-lVOV)yQ2V z3)&acl5o24GbT{{B-M7&i(q>(EIzxMS|fw26S|)3 z68;#wjaU=nrgEigoU7R)#Q=BT7^?11AJA~p7Hcu>iN`@BK5AeJ!w%Z`MT&W-z94#qY*B6r!S9|RM!ecEAAehf}JZhgkvnOLxT&?`b4b(Xe2+4*DIAM3i ztVfb(kTpT$LH_~=#xli+Oc3m8gRc&p|6Xx6 z7gNUQA*m<%@5=(VDX0oXf?qCuZ}J2lECLw&1aA6}r(x@y%`kjgBv{4p%Xl&C53kj} z(cV-Vcm23XNk38~YYkL=`Pkc^#OM1xYGCFOT{>vEtKrdzJ2mg++SKo)T!s7tFl{Y)IJ>ROuuw;g|eDI z+}Vj|Wfr#iwrMl($F_9xP1vHjmWA|C`nIr*W@?g@gY&3R04~~@o>9~UW1<|Me1jVk zED7W6+7`LF2iGQuLV|#V$v-IK0gFEowUrNdA4*DBCYZ`+-aDiH>oR98jQwYj2ozG*dKhh|qKZ$%R@HzP zwGZN;{({(n%tKXMWIJh|K~<(Q;Ui;nr$CrmQAIRVp#2fg{(S<=BM$CL$pYxn5o~AJ zmoueJM{K|AY2Q>Na?}kKWMPMn5=5W1e+*U}f%GCnz&Awu?Q}PPlT#HQhl0W$i#cb{lFE!O1M`{u(VSSUi-*`hNbBU+da<4Dh zX5!!!9~JCAoqlYq;DX39+NKH7{h@_CZ=VIqF0%ahxCzlWbrZAquegRB!Vb@J+uZ#g z^Y!(7w|6GX84y2CeFG&7W-UQGHx|~=`X2=!737jN$kJ@l26OC+K(AJQ`qKS0W{84uP}JTec*o>9e6r D_u|G9zP>Je=cB$K%p{;sGL@?? zjE`0-huTW5j=jbzqKq&HJKvi^U~$}OKJkAGLVUO$^Act<57{TW30gPS8w>x=0-seoOLlYjX=<5U23hpcsqRTwglOw#V zwrmT5ZTVCN{G0q4zoW>&QFsqeMTfNMkAg7;K#6nPaldK&lPod3h9x z%#xroaB0>(Pwjk>>9`s!lA3a7I=IuX_?Mtfk918z3NFpO(}YYBx~2<0V$I?&#vR!d zMuczsGr7me(~~6MhY;6%@#KZ1Wzu|hEAQ@jkt?dGoeQw!`AUA~s|#vOnF=Omf_bu) z-^y!=kp{rNNm`x~B~8lZ{MQa1&%PIqUX`@ebc~^%g3$`$S1t}3Dfi$}*dOL`>6%-X z;L|$L8BtfzkeMX&@|hxvtd=itHcQ35H-W}X)ACop47KS)hxo6m#bSiLJ>W-N%j&u zJKBHMTlsdS!{ld{Bl+p~v}_v5Mrj97(_V=+Owy$12ljUY%2cIck9$4J>X;GpdE%@%K$%Eb~=_okJ0Y*}S z&vs6nanqx(1;0`@;7EA(gBjpg4uY0Jk>v?RXR{~Uu2~Mn?6=oFhqr$p^wEpG16kS< zeg<`H|0Inl`AK@hg-f+~p{ch4zJD>BdP;EzLZ+yP8P&kX ztm{PZ6Li)`O#QH7x_nAJf_S(iN*fsxLR%Grgc>?xW2XMbdI4MR;U%RA>rXk|rL&=w z)W~>EK_+ac+8A?U(kI1i=rv-UDzDWq`#Xs!!ydUDp=5+|oOy;NfR@3hYmS9%{C|*n zKCJVmq=9J~10)s}6R)L()T_vtb6YK|5@iKhHuO zvfW%0^NZ8H&+PFGiA_)b4-x}d87JM{L{l{0pYW zB(WTPbp7m45l8=kbXY!stkAV%*OsruQX09aMkfrhx0!#p5h-jFAPfoJqdmPx+-}uM z4+WGlUCJ1(J*v5*xCbg`Xj1O6Dilh_fMD>-iqN%g5@WMmkFE1>!j9ugp+JI?|0-Mh z(h(KW68;taiKefv*#OCQW_hok)V>oQnMzO7eVS3qY5%2(9FHh3BW|?(k4@=jVZ-mE zb)S0E%L$vWp78wOdRGJ^Q{I1n6Zv>@GKr!|9=c4bjG;GwvWh9MHNaS89#X?!=Y?T? zhv%V`#0}rjoId*|FE;Z~$Xxe=UKqRfgor)LX`qy#v%Mefb>oYOPUzc?niS~9BNa@Z zs;OZxvOI67i*I~9Q-T=3C*i%Ry1P zPo|amIo<5&V#=@#MEMST=Ak4R{9F#VQ~b@ZKkP;@H)3HiuO`JO7Rx{7UrQe9wl6bo zb5~uR{Fz>6QRQ1CON00AfNx7ASH!(@shHJ*U=4DoepCeqy)v6+pZOeJ=+75o+c^>g zUE2K7p;7kzI+~@SQ=SrpQ$)(Y0EK@$=)VY2@kT41Q;~6{vkgOfGbNl+58!q#f|Jq3 zRU0>Ldgz-y869dGWn%X^=13;m$RUmC-Pp{==ct~kuPtxf{MSS)!)^qr`Ow_?);Q!a zIkT5R$&HjwCZ-|xXBB#t&_gmHJ?6kBY$x!F#@*w$wrA^wFj^t#gjpRk%gv!IZ3xo; zXTCp{v8c~`(#a_6euLNwh8#kp50*yGRwvHAjuJ(&?VW?B%g zC(m=I#@r@wH9p12%D@W-80W7VEO`ia{K#z#);n$a5q^Qpy2myJ$3!N27hjsIsJ~9p zL_X|v2XCjJ*V>ko8$f>p{4q?hFY_44U^+jphWrBPm2q#v`@`czAtW+cBnKK)(UK}@^dIhOIkhxR#@ zQKZ~jxc)mgOWA8{sDf#;?%DUmdTa-r68)t%rVg;qSQ;J85y*#CQ+wC`;)8Q7)R+2E z$1hF>@Y!HuqE@mGq~n6?uJl+j7ms-J6AHR=DDd>TR!*g)mmtJtRb|JWV)e&->NjcB zOGl|vQWrV{1H+L`!Ep!y4b=_oCh*2kPy8I<;+6hGGS?}hOyBL&bBUN>fzl24%^g*W zEZ3~uhJj;$p0LwVp6iZ2^rOZkvOtNx@-3({@%|?-zoS4jSSUDIa6E(b&sc_U&cVgK z$01WGl14i;pm%Rx2Rt2xy4o2&v;-UiP7i7{>*ejGd3g?-qZ_qyg=e|VxGOUs&mgct z@3V#Dr-asHp{$a;`1QQ5M5_xSmlwO88e`06=XQN)HldpA&$je+j#n4+Rrk@9xQFOx zQ7wp-4lamUQ%kKL52P_J_@9&mvt--0vnxBPJUD#IU2)VQhCsh1u5i5_geQfJ_$_u zPnh^%PjcOV5;4Hn_WQ8QL+~H`bHQlz004g8#=4ie&4tF^0SWgchN!`M6g_X+55eskZSoNk@jBei&-#fh!=COFDSuobdSD_y*;i|70d*HoPuFwDbruN){#-@IHzQ;> zu4o71kTrR&B`|8t$Rf?S$TI)a&njFDiy(u^WuiN}3&Ar4XYu0OC9q+C%DL@4-U{Mo zah=1bzWX;pIB?8D?h|7T8K|w@Bq$3fYKT$sKJU;$X*Y^YZBf5)< zlyEBZv(VOz^ns)j1L%w?61voAh?%SwhVT=jN>~ebV*9!P`yPy z$|H$jz%F=)SzM~z3uu`y^u0f2zdH7}P0#4N62-uu`c@tkg(Az1RV8<&$n!JUWo1gI zzG^CC#5zCSO7D{0hh0Ql2QB1FY|2$bL)e_2st3wi6J(Me1|NVEw>EWRh{b?hLDVa& zoPB&%B|Hvs$*};uSf>cHZ}@}_^dh2<>7;JB%z@Fr*sV(I6O?$;t1{BVev`qekTwPs z233MbUOR4jXW2jPEXfoR`pbG4oF}>?_FQ@A*pdei<{0 zx781@ak74YoLPm?EV*M7Z@GSAR$5|BMJ(3cX_W}{C?12b(d`A@6O${hVoS%);z_z} zilJH0gdvYx{mv>Mv_>(DDiBq=U7Z`ybPl!nT2kxJdjowP=UTq?7qJ{XLIb%I?Mc@H!lEiv9c?Pl6)CN8yn16wujLgQ!U> zgS_xFa7#?&k{$I#u;PB(s^GC^X*k9Di8?myNFP^;KWP5jjlPU0fhRQ~mRw)-i!^@3 zo49xa&kRuI&v0*94if8E8T{9_Dwa6@9upToXmr>3g4KLe?jUD+Vam52IP~(#Z-#qf zPYB{7iDBb_spw5z=>k8#1-wreX( zPf#DAXrUuGy&&_Rg$hAN_Slk>m~;Qzj_O!EhN}?nY2<{i+->O(R@89Qse+Jc zRII}^<}uWUo@u@53nh#sz7?FhY$E)#&FMC`{=^%sO8O7T>Ttcx&h^{sn{7-NKd13A ziubqQRb~YU32={LN2}xRSBv(+HS=ANzP<~2;%?cpca!-hHujB)z*$zf! z^kInbCNT-q+=5^yB9Rm^{@Dy%DYIy8NL`ziURPk04SO1{kBU{*x2SmEongsx?+wy( z(nM2jLR(+dI`GXNH#ya4 zWH$78Qu<&`mhyI$R1RO;tUGFMWSb3g(tqA#W3YM>n(n1BGyZT40vx(u&hYzR_*(@L z-T6os=*m8Pq8ZVmBXw~O_ViYEqgtU0-IHC{FXbeXMUtP0s!X;Lg_u!kHa+s{xuI7` z*`M~QgRqT>ANvSw(-#sSsPF3)suq>9&({WHq?-33(1Yv+BPD0C zpG*F|^WEHXAv>9k#g9v4R!lWkO7*US-pRjgNuTZovrrc{;(Yct{5P&bx)X5;Emeq1 zN(^}(+3K|iXNqhWy`>lM4X67pcs-v!C;Yd{y=w(pc{$FMYZW?0H^KO+l`Ua`o|2X& zaou?f<;C+Y(j>A~_BXkgC&*7yQBX=c8p4{e&3A@XhnP{W6j!M*?wA&#o$T@!iewgsPnew8mU!myMak4~j*7^)!WxQ~xiC#dd7 z_ft@B?|+*)^*4U8LmBa0fva$L`D56I6B1fgd7oz3@=Hkew8>0Xd3l-U1TIa4pE+qN zw-}=HYhzd~(9lY!Z4540^*|^N%qqsO;ndNa%7hxmJVK9M=4B$*b|D;)U=CG!yVHlq zi<(o!lp%Mh-d#vKcO^>;qWkhtO-$Dp6VJk?U~Xn*caT|6ep%OI_XSa~#7FNyuS$PI zG<@7D&Q9`fm@tDV*DlC`vP-55Nr%}<41@3FI=N?rsA@CRXDaG0w622Ib=FpD)h1Z< zrJ$LOq-ql+!+diq9w?}A2-trkoNgcf_gX{{5~eo6HN)X|dTBu_Fucv}$D&+NDSe9= zg9a-S@|KNG%SvQ%7G_^PGX0vypN%jTO8WRx5Djzv!CH_%;8p+97fIb`4df6OW^-a$ z^Y~GECsLr`VHA`oXOm)wJ+TpZMjk?ca1YP4<4ixiFj*L+>VtYXL^)+*qV}rnX-)R- zt1{`gRLTYl;`9gc^fVmjhS0fDJ1o-;czSt9p_&_&%4EuT%AXblESUz7^tcKl;tWz; zZzQtnVxhuKOVIpz5}zI=$UmZ4V)*!(Z*#oM%Px4I&ccdonykvp%k%&H%kOk{x2C53 zONFN<#M;CK?&zmoFW>Budt0u7oMW~ltAI;aiXth_(SNN!mg$@yx7A4L1J`6g*|Z?q z71(aGyvw8GA!=*iM?vI-To)?yOmBq9$pr$!18pq4=OujbndSpEn#Wt;#YF@a<@yJN zUGy_Q?(cmrr01V&80ZUxIp1Dnq%opYmJK>s zhEbwJ&wdz}YI=|M4$H$tHy}T{wneRj` z`0-5FFGNBZ{0q5W4CgmlqYZz@{bUxd{_~=&T%aBGCnj*E8fy3eQ^?;la|YIKPO=%H zRI2Rq@bn!#sy|kgO%+~aY|HQkTDGLKw^!n00-i?It5GWZ@+)&8xOniUrOo*xY~(3f zY5p4=s*9N~Z8W{uj1zh(i!8~m&6u4}K5PGC{8?gV3Ds@Br{VKP5Rt)xQ&2PdP5LA#XW6yt_#|IMrvqZ#E$VQtWBw zJ??L#ETS|tH)puKw9C!pa(qR8CKHMvMi9#M$-Ph^6rKYxsg2Ell)4aTh}qoY!P5;c z2&ph^J*}apq7o7kIuruzdF1gzknPpqzkjc-RRn~ILVHYG8TX=?FmmIuqKWs-j%iFW z^*b;9tsL(h+(!>f!NxoYY>}k|fa6kbt@kG_r&w zY&KWkuc@qTel=N8TT-j9nAS$dKG8A$NC*Pm0|z$loCK*NxCG(XgZj&lHRZqmAfZcn zQzRRk{MBvd8J!}#O%E+gOYwB#iqa)ix;i+R`QXN8Z^@gYHy&IFJG$3)`5?t2sK#bp z&y;ZWz|<&_h1BG3s_F8nx4_;gZpEgjYC1`0Q$#xuwG-14gJFuK99@KnVE>`E<*fTN@=Nf!Xe0cg2aqH^CuQwui z4p$}Ah>)i{VOueIcqb?SACU$-KhIsjum?ACMLdbF+lR}^EtKIB!e(p@gU0XM86)Yt zws(7BowfS1z9<`5(2uqRy_FtU1Hw&iW#|3EiH;8^Vazo&pbmjD7HDEefs@E`3Y68M zEB}%EOj)pZo|tB;H&@urZyj2jB^BRD=n!XOeWLK*E!)CSMj$=pc*>UkKAz;hpYi8D zSZ78yw4uC%)`586Pu-vV*+@S`P!(x&62dHF>7jxBVb-;GKnQGA{6gD)6uHgiM2&&s zY@m);Bm!P*m8WP422sqm`8FRuIBi}3*X~ZcX24}B2u=*b-GH*2C9bi0`)yZGX%yp# z{5>AR_vZ919L<0&y$61&<{@E^0R;Qv(9_zw%rT)Y`zyAE(31CHjr?dstfw!6D7!`el32FBbNZN_DLqan!6qVj>=sBt8rxZ}gQ*B$^vO(segqLv zM5-6&VT)6OiiNIZII_LxWdwhRNEBCiZ^pBczumaTbi(vJ30(+p16r0oqtgKqz$q3A zMtDVpm_;p`%BY$uuiT!Yb4)U@enP=3W`nSkL2&Wn<7}2mMb>g|)9MDZD=)UnPvJ-t z;zgXz4vDPGxY%+?hIxAhi}eTyzLqPKv4OIo)Ru+aLD*ZXsr|yV<~Eln*!0L^Sq`$o zgZ`8cm{4O@xAPJ68IfcmLD)LdD;;bN864|Lf!UZwv)z?y@^4Qh5=s2TkGU*5Tplpm zNfzY3nW%R-dVS)tHpxH`q zqS-cfI$XZK!zC!*Jf@?blJAB(OzNqjLhs}Ubh|vT2pfN-GC?A6nehtkabqvs2?U!d zb4a^);#1OX&s+U8I@vCWP|6y>gktE@G0MV>Zr?e^t74Y7pHL?{{LqAeU_5v67j%QC zy!QDA)os{+i=Y_l?$`hE7V7`YVv_hCiW?n0pZ!t=r43T1e(E7j%K=cjFP>*YWFFne z^D@V@gN5-iZ~TFm%0J|<9Vb6f+!7%OcHG>%ym-{@I5Ub-RPPGZA$(`|b18=k(6Un~ zE!XbO;=69Wee3;@EzgNtF)mq^P)#SdV%V}S`tr+^%J4@EOutDpYtD=X#$1T11!9D` z5!4C@UVc$V!KTt0`k(3}PR85XFWdwnbZQe)AlRI&Cyq-YsNR;zW;5Q6aVgmEniVkr zSQT@*B~$jY=05`aAX0wN8`%yWNoW1bW^_)BAonQVAT_Z_umraJ5WeI;zWo##AHP^1 zO(gLSbx*ekQ%l#XH^1He(R#i5c{3V(X6%VIAKm`_!A92+j{uTt&oYg2X<*1#8M1gG zLq~98y>k5BBGC%?G=3vgVVwj0C32bGPre*$MiQ~n9i;4j`UT;>5_xX(R^(fNnt712O7u4N6(QSL9+ z+s3;B9lz*e;=wQxO4bV*5)GauiF{Nen)&69Me` zW)jbPuIW-hxYyXa=e&4pMI^P7n8DK({kFt&tC1sFFX*yHDW&ov*>@aMNy*;}FJMJU z$s{;a;`QP~w&Ue25_tX3*r~qs8j4OsrBrX=#zG_u-LM5>QHp>%TTT0fep3t^)ieuO zxUY`+gsxy@uB)pvIft*U-GaB09J8oy5)Ii|M>Bz{RD3l@Z9hXxW8X*VmyuZjFmmyd z_Q)xKI!~>lQnJl|g=C6>x)($+-O*HDi&)J1H)DQZ)6jNGW-2%CbcLKowNrNpHNhX@u`7ke^eKsW3(fJBKqtV9% zGZMZnR}%~Fg}0qQlIPvSE0PdgRIuYQ$lQ3)nSwrs^+V{zT9&@KXlSp2yjbQpFOg+l z!o<>zk}uyql^mj)v9q(OuV&q2;&++vOD+Nq*!7`r>SY7mB0@RLF%Zi}jiIdc48St& zts+yx_dRX-iYg)z^+7;cEEO!R4Vxb|K|V`u|Mm;XT?~_x)@@_Vk?f|IzGTZ2MHLh@ z21jFO)5CDw{0^aDfmxL%y^n9A9HO3LSIl`%j2qOsv7F}|eVe8BlC>mjg;zBk9@#=u|=>PHB>Y3{o`_UfH3fEZz-+fs-q zxDN@S2$QRHQe*xZa62TCW06FZez+7zbI`PG_ffPs^{R-Mm%_niLULE^@_& zLttdh5X6{lHP|De-@@4$vzf6eZJAASXf6Z{HuontQvh@dMpHB;7*~Z98wk_h^Iszq zw+yfe2@}wUbxuARdN24(I=YVqdRMYQZGEIHHtogwNN@BK2-Z1$jymH6F23+WEw@qH zOLCQuXBbvZrH4`n!NSpcnf(?55kf%ZU(?tzNTj9WG~^lW9(=w`_Eg%U#@S=5dXX3G_yu@A7`i|O+LfmpfEkk^bq&CcS+1B?4z}}F# zcsl^gGe{6wb}}B|D;E5XYAJrtJ0Ib3MRrGqq}x;-szOxj>v&Ez5ocq zPnkt=EYQDHte2EUcojv{I<(ou2>#&a2NtLbai0{`uyqW$Tb&g24Cm@Hp{_)hDZPAw ztY8?|UHry9l^qAHZ9K>_7*`60wI&eR%O831IA@z4cIft;>`=4Ng3T$@U3}U%Wc+?e z4UoWcA54580jro14Qinyx7;&Awo_3jG7B4Vqm(frWZ2GDLCdEYRvsc_4OB0?Nx}nx z{wb&Y9aT_O+-K7hqXdy7`1B;OdmI&NwiCT#pt15|J&wj_P{+t?K7kDYH*&s|>N&EL zB*St_xmk2;UP^@4$#u8@R=uZwXS0wW*Rg|P3a;41jW-=^A#WxRD2Q_L0;j5(es>_L zFTKAmu`n5Y6vlhpi;*uV#DXmsI_$0`>65JngXW zN9L64=+%?BPo#%Z$6Xu&k;=Xn-DY-ws@a^Bxk9&QFe3L3c<`h?{18F(P|YTP^KB*k zERn3GiunlG7=*rSRh@Ub3yFL=F7xjN7+%;X=;}|tx~o+zXmxU6GBdPQUiPUxA?uw5 zb%>;0_=WSDiHVzF=^a-lVkNXE`VWgR1ai_v`o?NR?G2-Du!Ka0K*aI1Z?a=dd`*FR z3);VHOJ#MJ)2_Lt)=R88-H<5r{nKbxg+6;UU3G9xF_t`ntc@Rdk<+`1h|^lvBiwPF zGI^~wF*uvZPWkIUin^e#T+Le=xdkKw=FgPU5>*~rU^Qa;A+M0+Iyzm!>S%fEGj+&# ze|cFh@RS&*zYvW6c#Bq!>C7deoLHiMBo6IT$GjpfRoOKxd`)#`ZXP2K>$Z!r86PkS z?~{uWUOzd%si+%JLy&aY`7!#8kTg>Y^^C9n^cUL1EPD0HYxs9F<*~@vwp<-_qdS_- zQnKsaL!hc}6eC>y#bvu+_d|0th8z=3jVCIgG^u$PWcXUf4@F9g0rbx2;i3 zWRIlyH8mXEJQ`ihjs@LzUJAjdUrOFMHrUIL6l7IFt1Ak@Unj+tRF&L<#}PRnTVPWS z^37Sn(vpEP^3Y^}qS2q_G3Hx2QO?Q851?aFtINAR0Zf^{!4g&)tPXG&DTKWcvX?LG$z zM^y3YBMQc05dRPo#f)k#7r}NO9$#-kd5)B88pq^OHBF-xES6Y2=OJVoA#3<}h|hea zSXwBaAVO2G5FNF@95%;2Mux0?;)-Ek3C~6vt6e4KG60bRizjzubnCq4(f-b(KozGs zt9eynB0|@;Gt?)<1$VAF{#Taib{B$(+PY<086CdvBfYt2R|btJxrs8j+o#FLr3vD1 zBjmR;ME-i!dKvF(w}YAY2!T_O1`CXRN&6xXL$Ksu>7O%ktbYAO&!$b(s)*=>$$)gf zn6b6U72CwLI`*?bUuH_g#xcgqCp4!IR93p7j#4% z8_V#jc{alQ>tlKg_+$FEl#5_mC!2;vrk~vDv7dQK3DHg_#=^xR=@xOOAJa8-+1?AY zdpT!_c$05-j^Ihc5#{80yV9h&!astos;4=rz=tl6{^mHK$rZFDdC-r|%{DwK;QEI3<4G>A_&qIr>r7XEzMz#~1Wi0F`8{j&`$EW8R1`_HlCOT*z7&3InPlc zIZXP5V6ALMEXoU}XQv{`B_y;-7_erXK!+4B@DTyc+QyWJwv)QWX1sw(@1q z5~E<;qy61S~paUFrjcYz{(jN45GD&`CAHy56ql8dB$wlmWR%EhGF#Q zW_NFsD6;6vO=@!cBt?c{By@wnjjap;Pjkd;@CQCHwm;BR?7!cf3NtpTcCr1^hQu_9 z0h3R;453S8iNxU9i2r@8`2va3OU4`}We}`RE%VTK<{0?C9RL1;pRcXYEDlOdjJiV( zDi=M1)d6EXMvN5>d2L5FOuG4svt92@>i3yb!-7(>wTQl48IK#fdwO zcx_$?ocjEWiE=@K(sK=rwrgcqdDA0(hP@g)eazSgJGaXmX)YBE$0Agm5dN}G`r{1f zRY3J-s3-QH^{z-AnoNyLny|MW>Q-AW}lk1<%QsjK4t;;X4u|B(Y8(z zhPuYFaUNu=hL!3XvH&bUU&V{lxbj_hKio%of9L;0)LDl$;YI)70i#<&R9Zk{AT81$ z5<^jH!Um|c)C3h6HAw{}q@+VaL5B2@bSNMViqwed2pQcV;O~CEzxu<+$H$G0?cQ_G zJ?H&?ozfG~{?zwx2^_KWd9&6uBj4X-?`mx!SN1nkSz`@js)#5%nAw#L8J2&^jd_2x zj*+lj$IN~2fAD0Y1D88Tv6xDO@i$f~+#fLETgP6W z{b9Gx&n0w_bTw!PY0IRx(>!7BGC$SLdL|H__R+S7`$)#wP8xX^D4o-BcAtK{p15ax zzbXZ?p>lli*2Lj_XuB0HS=*JV8_^4!Z5V=glrm3L~=P z1UYQUgdEC0&}qdoc8}3-@BX0PTKrPz%AF{&y?c6_mOKWY%=-m4XAn?D(tT@YA z!GyG}Xak3W`v&7wT5gF>IA*SwUZFsh1ao?wVu%q(|r2FLtW9l+nL;^9Z{KSPkjf8;^B{ewU!9`4jV$ zw>cFMt#fzw&R0Rnv(F3zCn%3HMNk6z1FU$DZoap9o-ZosoTM%AinR>>Eq@!Z9?WXs zg!90HmDxKOhOL9CemN9&U6Q9}UrTr&>q>ko=x2?mBNn5xuJlexTQpuqrOS$qzC{Cz zge!OBLeK;#6S*9}a~q=fDNjegnx(>l-SO@v+RGD?a4)q1VM*_g=O^(0=8tT8E9hb0 zD5*~RMf2>ZD$)h_ zO?d=iz6we-COFqxbIRS?btX)2p7{;;KJwE&`M+8Kw1f*m>{^EiUyoKHdLmaL{1=5% zd4MBKSN-Pnz=}nQRmRZoQ*K!I&xThnj_MDb8`HVr|EA1$YfM3^e*dv3y_K$OmRGEC zqu0E(*j-Y38&j6%%TtvS_vL-za+*S})Wb3Jf!U;gOpd6=0&ni zu5#k0sJ1*2E!}J113A5Ovv{oDN0rc%7S`{rjL=iQnBw=p&*~DoK5^q_z@3Ju=ohtG zmQKI)^qO205;Q6T;wxZtZU1n!DPBI1u_dF9G=)xkt+zu)kLpEN`3uou$4Lvv0!lj{ zfDm-g1(&{|HPeQdkXJyA^_j3a^9YP)vZvlimn3rsg zEIDAeWJ#|prS9uWtzqbcdth^Zi-oYcS-5}8D%b|>}j&b_*KM>Vl zdym6}h{tr}=NcpxN#2RAkgb;`4Wj*i4cHpz-3Js(soCObCdqxl

M;`C{18+Hamx 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�dx!+475npA6`ao1X&I0MZ$F(r@9n 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 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + + + 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 `![${getFilename(name)}](${url})` +} + +/** + * 复制图片外链 + * @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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + +