From 39bbc94d073860e53ba3c5f3b1425d263fad9e46 Mon Sep 17 00:00:00 2001
From: Yuerchu
Sr;bekC3kn~s*I<{RDOSwnsdTHk z4iD&&Q4OX?I5!@iZoi0t!Ic7b?TwWDJ~_F#h%KIQ6DVtjo*gW+!XNEE&Ggq!r#nL{ zV5ccIm&coBAo5oFh2z%NP*}(g9K#VbM7Y0E7Z)ZT-=9%oY4AB$I;;UHlm3wo%IXM} z?3|oQZG53bW|V;waSvykI2AxZ;5jom{S2iqgWVEK%bdfWZgBGJ$)9T*1;o5TgLL+x zkhT$^jY}ue%kbkrTndUJ`R5)rl4)vNdS^ZVx$_|@OK?UCv6U&ccd^iL3!NGC-7~~qLnxkp&_=0B9=I5ag=qcz6g^B@qwZN z1v$CB2UdEpU)ar{XR6~V-c1o}+2p}mSr=W-K`X&KGTAcCln&1d6FmV06=E2#PnAx= zjC(A{8?NK>-hz|7C6SfQ`KGV0FVZ4jmBFofzY#du5rb)t2WpJppt`UEVz?K{pFl)D zl 00|k{hw)vZ-8$k-OrSm#eEmtz7GqkEk&!70 zD&I+O;_D~TzGDWItC1viRH^l6!z?2X_)qk$neF201mI9?_8Zp@3To%8ZfyKE6&Nbm zIDc0_-poVdN88@K904dm%{TJOUEc2t7 8E#k z?GaDPqb--w##C0s+JIH?^<4{7509G75QpJbUD#|mDi;@6bgHIHJh=6 4e2&Tg>uEC-U#*e*zHa8|^K}xFKZJKXjt9K7`F~yb73>t$?l6 z9TFs}=qOg=mwI0FS3S#$)0On_UE5f_f1hBCn1sPW*kqEUiUR%cYVI=m5~m98}}<}ZJk zAMK=RlyKx^kq+YNeXr`!a*8(KwxpKF-@eK&ZUj#N_v^bF=OQ~Pw{TB%nKx~IaAsYv zd2iK}pO6Rs6d2=T#~p(S)|8p5;a1}o1oc$h_I%^q$j4ON6h70er>~fkfm!G5^O3m; z6x|sitkD0kO7`6?Jy>1p7c^;0I2_gm@$GiKx<93Fh*1y-2@}hSkS71>K|qMA1hh^x zT~I^GErOSM0cYC1oD`}NUr-*A*qV+F7dz+mY-R1EF;bOeT BAmWTg zhlle&-@2|RFP|m}RkycipAN)|#EBN#5-nEdponbS9>ZsoZP1ct%lV#( g`X=U^%7F8_AA|WAtyt1e~Nqf?Mum zvgc5bTWm$`w91cu!(455h0uiqtdHaP6s(274&~ZjclR5o^O^@A6Y-u!jD@puEHMxP zyDH^sC4Vd|NEi`h9qU7^RSEzAL;lkOoa4iLLJzyV$z*&WSulPV%DB;wr#OgL5TYvz zd LSf%NZn?W zx@~}b^gZiCZ6!#7U{AOTjn0C`-$ZoKNn%{-buUrRT-b?mOd~e$uXx&`H~$|J+ZieD zW4K+FfkB+ubjblHzeoye`#og&(L^mPd-s1pr1xcx7Zrjc33JVw8Tz;<6ef^`L3|Se z@I?e0#kVdoF^r037(xrvVl#D(zdHnbZ}B= +jvzj?bmpF=0x=A` ztQ=()hl(B29^$_x<>eE+=e?N;C`B3Un*2)YIyR|%aIV?2hiqM4wNkn7VN|?sAop(8 zqAXCXo@`is2ZGgj3-ffHXWO`!p>kd>`WHm}?zg^ts?rJXW&dCAk8p-@3vj@<6r!kK zYmW;z6NGohpJ#f=zOcO<$=^9v9uA-smzx!fYXhnd3WBJ3L98C&-9!pAmiX!f!gc)H z$GpFyv!_`-4{$P|@a#H#5(D(CZP0>MEdL4UC;A)X+!$m3W6;s(zd2Tq1uUp#f|Q3> zfye4)6QyH_M?rS+tL_TEo`L{$N!Pq8!Ve1fRbdObBm`nQ4mZ(B902&>5_Ahc%Rm#| z FWFML$ku1V6Hi)&UZzqh> z@!9FsPI4zhl&Ila4mr-B)Lco$ukU F)M*tuE zDF3w+E>=x@f;Ozs20|$ld~%#kN%U``D1VAKhR8f!+BaU7K#+rjLt~atf#M!Sm(2wc z#n2O1*NGK_aJ<7(ra34faP?XUU$KR)f!2z>0a^f`24nC4Rf-hpizo}}0d9;iT%7P# z!p_f<=P>xI2m}FpA>(wt<^&-E>O&fpG}^*br^_5&9ex^RZFy!K6|p86p0zP2768Pz0;8CD>lgDTB}`hQrG< zAxX}qt1>2EHU9m$2_b=5ka6fvi}h+K(sQWW@&gX3%{w4hG5z)52t^Pn)pX>5RZSqg zmhi24v9gq=q|R7(eQa&)%Yv>Q8JtD@wx~cHgjWLfEQgI9l0>j68*B1P{WVkkCk7l{ zhq(3ELh3Z^5CTN&SQHGgyMWY6S|>z^4(iPFN%q9F@+v_oUT%LavJ|HYr%7N01E&V- ktw>sM|HI@^4+ekf&GQ2HlEY}M)(JdP(NX^R&^rA804*)Y=Kufz literal 0 HcmV?d00001 diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..e5373b1 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,92 @@ + + + + + + + + Findreve + + + + + + + + + + +++ + + + + + + + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..ee385eb --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,86 @@ + + ++ + +++ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a5f23ae7bff64954cf3537377a9f99306baf083d GIT binary patch literal 11955 zcmd6Ni9eKI^#7gJGKPulg;G%xiD=PGd&*jhP(($RY!y;H&uF0{l@?o>HdMBj2_-WU zNeh(_F{45yOO~6)1Q=?HBxq?7z}sB>)eS zX_8$O02XYpTeH$Nn$X+k6gjP_RPeX^z|cl2Yy0C`%k3^z#7&pBl;8Ed>bUL7pLv7R z6W014JxNhW@JjXCeXZg}#GXB2moGgZ_(`#Ou?&nUCg$zmsv3Xoj7bwQKX%vaiyIO? z86DLj?XyWG9Ns%o7xRAn*ge+ ++ ++ ^bgdgipfcE@<^A+f`+mR>sKJ-efbO9(E zUaqO#{4M!gQ>Emm+h|>u3SJs1DO?hNToWIvqpIOS`jT%Uk7n0;wKs3);uG&)iMaiW z42@3$1li}%73SD=>Gs3DkumeZ(hj})J_6P`bPG8mIUj{f#s@0AbH4E`#H@}=-la`W z*u-6V5+J4|1lMME>4E(_7k=r-a@s6UON w$AHLHXEdyf4jQ(1c*F zHX0VCGG13c_-7ZhVu;e1xZ~-O*zV`wu73WG?8he3+^K-fpOZbBGjhjX!#qkpY$}^2 z4DKV~SN+$Aj>^@wcz4v@ZyvOj2#vbRjlahsRjv5BL?s}SnD`cA&Y<(5bo6RrO`Jvx znD!<=I9mzuY3JM~`gZl}NmlSWoZubDU0tboo)y#DPzA~P3Y5;^TRwUm8zx+<*< Lup&B 5dj3P;M2pm*yn?)%cf z*jta-^OmWv_5}lD61#!eu~1K9rlYlQ_q%3GH}RsGO}p3iEeCx6>e|ZecI;X;6&>VB zykbrS%t6 FrbVSFgrVUEtVt>TR8qf{Wd+$-?>Wp`X>I{2#LbG~P}J-H+N z;0nz?@he1Q{Kuw>g(h-=q2ku)A9~n*3%N=Fl1p2%Eb~4P_~C3;&>lBbxzzgTs1Qex zq`Z%A?^Y*;;_JE0)|41Jb$vXMP`>b?bobG_f0sLcC$Yvg?K;_DdRPzky Yjke4!FGRLEZC_p=-#}d@k>7aa6%OS8+zy zn>xzCrbsR?JYNZPdK;S}_0F_4-_Hpq+9 F+aEHx0D$onB;5~u wypWanbFKNY&1vgk7*t?{kcTW Nt+PUAcde2^Cz<5|&5#cei;2B5J8-OEgrc*Y@tsfPlItbJ5t(>);7UJuPnU*TcLc zQhNR*4kP1F_8ooa9{)1c+iuHODte#&A!)wd#qtBU9}zft6b~Xhw{J|eqI|S&eQykY zuo27dSOk{VAL ^X#KWItbVl4{@?n3A``TNL9G`s**Qn-Gsj6<$?0O znVV=8JT+>=ruvd#ynUn7@{V^v7|342<^TLUP?+4(BB(YT9q_*V wc^l##+^;FSJ8nomv2*=A+c%V>FpWlpU!Ky zoELOoWUZhJERCmp-=X*F8Nnyo(}2xSD0w7( 0>;~pBYp=(obXy#QCE5-?M`NonC^pUh@_-k%~bVtg9 z3j~&0g!o2sD1Pp$Y>)B>b@$GZZ`5ag-^TOM{uQKzU4EJ?b$G!*565g3>vs17;nbiG ztCi=y+WP%0+_-b=#5tm3n+TA5Ayl>&6;R0%78n@!J!2l^9airdia!&C*#0Fi0S(nP zPyV~a@!~^@{Oj OyC hGa0#>@zRF6YnN{< z9_xlao2Coq&l$2Ev)%}|Im`)lJTh9CMZ@TuU9=^S8YN!KkBH4K5co6mwjR8de-DN# zXN4=4-Vuj*2(YA@qkpv>z}u?HNIg6A9u@ez(`GSpU+R~36du~s>$Gp6?it*`BUJvh z$7FonU0ndvRiA=0e>SGloZI+b>A>|Q7bmncss8+!y^v4)j xz7+wCgVLf{^bGDqxSnc@K`}*9>L_Z-Snf z>$T_#mub?pR@Va9oc-7$uo)qtt4-@w8ZaH7+l33q+%|6(FJlux&KpsG{_rfYQVHy* znb#cDJEyAHj_LYzvU)=h-Q=&8$X3gn)PyZ1Em`E52?Tz}b#(t 9}&AH4cz-A=^iO!jXZV4tFUS(QjRWMrDio84Hv^_8gY5XJ4xIX^` zls`flCbfjPOL M=d?h!D5F*T#J9%dCG6?8XdobvFYeNDr?Mm-nZ z;qqvsQ44bOrG%U}E6VQ=J>flqA!XA*%y^ZCk3by57zwp?82&19qi6%AldfW2%|C!; zTC|-MJ1o^7371{LeDK7GuuusYrybh>qoYC@I9|LWOPX|>=#68Aj`!-sbq5z~ppQ;y zNp#hR|4SMiZT-ma-uY9TumZW5R{d_UrmkgqjZl!wkpuqGPQVk@IqD(_mJFJ#+>tn} z-h?%@iQE*jfbZ2ibLbXh*7L?-WcWyPvCo@eR+#Y_L#Hel)0}AKsY!sa!~>Gr1OvOD zEM9YG@!0xam0h)~l(D2Uz91~(jRr@#VAN@>P;bGQ^@g>b=^s4Qw!tmaMgux660%q6 zO|oy5(^uo{r*pywoQ_&HVTlRtt=K&S5@YTsd`MjQXXC7!+F)9xErAx^hC6M*o8nsZ zg<16wb2oKBE@eUWLEI(9YyX*5dj`y2w& d0vBn~sjPJdBL zlE-G)f9Z%wh538VQv&}pG15eZyePZ}p`wQa@})aGkg10TR6ay-dB?5P+PXV_!d>)t zd$*X&|KcpYfBLe+^9rW!p@Gn;rfBM#EpUT6fzX+j2^r|@B-oyB|6@E>Jnw?gf)hQ< zcmIalH>ZPB#S~TfyaYP`+tjR4-o^oyT^7=2nSs+m<_Qm<*?k>#P#Cm+@(@32JTi;r zlgE{N@ENOKYYQ%olAUSG)q3zMTWcs_*ZoKb?1fcSo33KS=(mjUwv9V1R *^?YL%XQ>Z;h?QjUu0TU#g%59b^#Z z7Dj}*b!t$x%I9{Ge~1%Wul3~|S7d#T;A3p}g=qg`6d$KJi%D@Q?v*RURx5$kowkGq zqSIJhKOPB #vPKadD!OG>_wG$I+lWP2)`bh0nP742O=sV?g=BfBE9nO|nX8ldb89^~ zNsAa8K-l6ySuTxdxrmqPlK9&DcUVcaNbF#+*Jn=(1g&}?qHYdc&$pJWnnecpBP)1y zSJ!r@|CM3J=ClSHxcFVD;`^7tnJv>sziy??QIU%~ *YiB$BA{k+lH7g<70OEB+)m-x{Yq5g|Cy@gBROfAc27MDdtKPx4EWt{XHs7oj_ zVFg4yvm202Ybyn@)5v|=0=zDJxC~dV;61uPs5frq6@OH7tp?_#Vv5#}P6hVL?+nf& z_a)Ar|5XA+@~I)r{}nL5Ro|~*0Nh`%uIiTZj9Ag4T zpL@oM31x@A0^@pOi$EfqYb25+N$V+LPrFaQ2S1BMQ4Y>K(iNxH^v0J->yimRgyMF~ ziqAy3ZRQGGUdiola{Db&u>lpOoZLqHp~kmc0Jq)DA{dmd?+p!C0yM>ZjScbxvu-^T zHL1NlM@I#T!CS (yMeeG>USmB881jn*(t9$sMm^>ba|Kh!4*y`O7=z$>25J;8fm^X>l%dd+rB)Aufz zcnt-Gl^@7WyHC-%5aJSWp7z6dEN7&bH}*WryGUyus%IgUJ~Nqz6ig%%I0|+#o}8yF z;S3VrMW$YoTf7~bL3F&(dyy8 QVXn&)BiRLu(D=kdcmQAZxeX#bHX44+u!OPwCnwpKpAVD~j4Dol?OhpKkGf1fM$m z=>A^MEvDwbBSBv9Z#uRe8vHPmB%}o*bUzS~+U4Oh-)o?MXwNk+Z`VNRR+JBkQ(mVE z)E9Mxu-oN`ek(})&)Ab=yBoNRYgZ%k*ym&=RL-WKM^^%-8$J%2zUz^*&+`XR44)p) z=NwZ4*KUcLl&Pr3B!3Qn Jh$CF=cyCeru=`zIdY3oKi5sH_V^TmvsNaP`LZ(k z(Go6`ybpTw8NBhz?WM`-bUSTH?D}6B>lWDh{fOgCx#9$_*}eQa_3;@3XAdM$#X$Cq zNLT p!`XUbU9Bqb65+@Hl}(+GBcNFmO#v>)Pld z*lLMG-hEeN_1Hg{(%H+?RRYsx>Q~h;Hcn&=(^v^FlHKCHtVE7;@XM>Hs|WQervoag zOs9U;Xx7k_K-LyFemc>LwXC9Yw!&9KXU_;^_x`WK2$0i8HPTjadHWZF1Hm%Q+idW; zTiU%;mlk+|5J0cs^}&g}JW7NU>(&_p%YPu$uX`z%r}1|nG>N;Uf~jH-q79a~wXdzy z;3&Yh*^$=Zv+J(3VjgQjk?Hop&2fmH4;Wx=r#&VUGdQ9$BoL9mM8p1y`sJv=>Fd+L z2{JKg@189oGKlurP)pRcE0_p;=&a%szP~q5R+Ot6bpnwc5PDt XL03n8%ojk224^3 z8wM7P4W?lWOLNP%Jyyb8#68M6V6jng1{s&l-k8)9?>FGbv$U>7&1Xn@G_pYY#pw$o z+j+@aVEK &f`j>V^EIFqxRBTw3kIf+lHaZj@2yLi zJ=S#R&YqQqxzd 5Q?aCCY@rxL<0LAzc#Mea4jKRAMBY9}9`6F+G{w28rP^SA@1? z;m|_2Mxy70?yo&}JnOsHh4vP@KlM^OkWAog%Wn1>6!kNyfU^JjzFdl<-=sLdDe9&x z6Ygz&f`r~N8qg!5sv$77Zx%hfvAo~A=YNE`P;M2 V&$%T7SlDhHH&M@ zW?tDwK5KGna7Q3=*c=#Ny!~!J!E!nkwgcK1FLl!VUWV`nMBW*$O2yK-F94_JoynX> zF_9*r4&6W|#mb&XCbeYw+mLhTG+>W+0q4EZPAo$%%gUOl^D8J{dPjlF6u2)22VsEx zsstSS6fVm&3qW@L8iL67Y!pA~BK~xmz?$Jk3x YOqOMY(^zKl>`m znL V1sd*09ty7LjAIIX3Z5&yacDQt@R?}9 zhJ8W({JEjDJS)b}9)@!xc8Ln7W6mk@zsh63ssX3DNW;A+gk8Rk@QyQ{!pInSzHC|l zX7P}gI=^hMHu{IRdK`f+{7w?9capc{j26?iclws(#024`}w_F?;UXDPVJ70Zt|) z)+dsw&Sx7c9|35<+$(gx7Zf?gh*S#@2OzsXEAZe%X# y8R zJi3$y&0gWH@vd767q(X$?A`lGl5HTb1lU^b8YN){^*MC(zBEKyV;S)$Jf*f6i9?o# zQTd7X`0Hy!v2h0GfIQ3YP4WAada32X0IKu6*<(3Wb$*(&m_{S9ShcZ4jXcW4LxPr8 z=?ukEVb+uwHzBhTfAqR{P6e+B-rm*d+4&$dZ9Zpc2J2T0K06LM45ifCNnFV*l2|Bp z+&8}Kz+tWPmq}bMFKK;W2XCG;ThZrW=BWN0DhlW%_A`r|&i&RvMLla2zs^!wL2%{5 zTy76nvGL=0G1zgGX#B6%B%Kx#53A2eVTPWOGECYE(2~R`<*2A#N?i`B^D@VaEqX$LaA6 zoU1gD?SNC=U?$$J1!it;B!+NV92#Nq7qYXi AFl&MgURP2`xqpBfL3!x@dSd4~g32$(Rn8nWi{OZfGML2O%CZrK}0lv?fDxR+GfK zeGk&hUGc-mUamV$JY>Oil1=5Gl_^#;*vXrs+em!04=Hb4Mwl$-(tbG-Lm3j9Tqu6) z;Vip}xPXOV&0DDc#(xzLJm0)UnHALA|8WB96hem>x6YM9???EzsX1>_udmH+7iP^K z(^S;~uVfP-r-`U(JEceodwy$p?}J-H!94@5U~mq$*VA6DgbFOIjFPG)?`0{UJ5q)Z z*6YV19lHR-O&lB1UG{#ta$preH(T7%C=p)Z$HLU@d(1?hMn_e-%xQjrb+^pOr&i@u z43o6n8Y aQCQJgja}^tp6|16%3>go-B6^0%zf^(BR}KN?@Lff)Y_8P=2FLA zGvx=@KtPD&fXZcaz`6L{LrC)khEnhHkSA*m--I9!io&OZOLykX9*f_o1)S}mJQ?({ zl>4Vo!cYHH0-BQVz4}`h0?tEEHwV1)K(rHj*@0WZ3`D|DcPpWZLjAoJ08J|!w0;=# zeuHTI;ZLtcda`65o{)=Yz438CwY3O)hrO{61?9MG%ZA(;^w_@_o0|mZ2huQ1yh#b9 z!j|z{`|p*!HN9plE)3&$rMvW$K*B0*7_pkyjOU>;O&%X(4h)pZ__wLYDCWz&y2_BZ z@dQu-n}i8NnBUZjR-UX!m_{PNnhWK~XgSuKaZSDv>iQxE6`V+F@h&6k1DU*V2P6=; zwGorY)|&jGqfks#-qZq&`SAEDE}EhP9yjVicAE}|W=) zkcUoJ0g5&06ZpOpf8V(daD~aR4M|GO#>=P zyGY>kxppkX2^i(}<1$`~@kCD*5zze&nEA}x{Wuw~x1@sHk$=g$GS|luyzQOCWm+Z? zK;Z0Z$wt&=*AZgP#aX{-_2w49`E-lEW0(r=iYHqJA^|iKXn?V07pye`ArU~&0-QQ{ zHn WP74U1cf{cR3g$`e zbiN*!c>snI2A}Y$Y p-1Ss5o{$U;9sI(;McnxfC8zH H;P zAP^aVzAGF2>~>L|x0{$*^<0|My+#2JE6c8|=Y< F3!=}Mg7=)=r1Njk24>ky0L-Mnc EPBLp5x7(R(tWFQbOg6n@nAv*Z#E1Iuci!)_qz~o@> zuPx#J+5W`DL7d9}6P5;1Z4Ivtfko1qlc6P{_;m!2npFJVCeaWy?ItAlx%+p;kRA5fPzPh |o3n|E4@}J7%DFLnJrS3=BXpVO4kQIOHD&5e8UyAbZ =}c nR1ZU_{P{h8JCbCOZsU@Z{4c97 &3+<>Wm?A&yy1ogY(^#av>Hu``bT`_M z>wtMM1%sVTn}xt&qf=I`h9|%e3`n6fZbD}m(?e;ydwVjjSc3&)uUeVDYv~M^XJrqb z8A5gQ)NQ$B=sxq325js#Kwm^mQ~q(bF&XCuqYM}-Yx3bS^CsyVQZE%aAb4Nx6EMXO z0M$N)pI&cX;i?i6{M1z-TQ6XwcQWb#6>xsQ^C`15j+$vB{;;yM)0Dsmzy@x)nqrUR zrXIviyv%*kkv;-cE@LfNVQ6rq6HD&OmDZ)0HLNm)usy=3 21O3NNAT`4#Ag}__MX2i3S{T+yMg)V+j1aRLDERnZdc=La3~>p_#_LGjKVd6 zc~$2~+p8+G`*GDj1iYDpEo=8Nx}~HoMFfz&@Q_1&UUn0H`j-NFS_`0aVSSWKS)9`` zk){6Sj!4e_6#$vAPT!J_VOeK7@MXfO(3rVjZ3$I5K=lVb;0XqEm<=SuFlv M3o)rOZtZPiKR@Fhji!i1rkcD&FeEh@I?V{poWE8nDELmbv3tr|E^mi$DX0E_pxz zUF%H4_F9F4;B>)M5V;FXwr=z99NKm{b}Xm+_KN?Kp1CF!_z;{_I#;myfrCxuCI@ed z iio!4?E(#-EqHu+e*zQ)iI#KCu_L2YXtmB_qn&U2gw{& zX*f;*xr9-2W eNE`s54Js ziP6cN47vLGfhddi-|nj_{HGz8xQIy}@Dx5^t(PZwRx^scxu(d{h5M;d;=xh>az(4; zGyd_++gGv^?U#RfYV`|Ybj_}AmRm?BYL-gkh5Ge+vXKIffQtj6w9Hs(R;xoC!itqW zqSV9Z@1{97B6{%g-^iDa!NDsafDR442@gXDI#@#m_>SIwpLbzN7WFfsadNBjDP3;u z>D?3F5Ug^|09=y~qn*zlS}r-OzpCpL2$&9UW)D7L1s#9dxxf5AvZP=Cry?A?0FyEE zwudl}gidC>`svUn!vzd2SJSX+z E5%_2Rtk~|YI1eZUzyOc-F?T*fx-J$mD<_!h zw~l=$oPcH706#_WwfWJ_MU2$C10q 5W^vD=!R&)ho&wJ|vA51nv*;W`(CqHhzBN`?AV zz+V&?$iCb(nFR+_NI*FISy!w ~ylxlh5yPzq_I9@fat{0tA__MXW^ zOgi8Lz@;!Q|Jzmjr3b1fnExVj9>n{`5)YsK%gT*}mquXnBRz#fXa3+5)rZ_h$<1zN znNKhQ K$deIX5-Hg=C8W2b|yzl z7qAxjHQnCz=>^h&fi}3fD#*(^1(X)Mcw7s%VB~&Q1CZM!)ZmX`M$@nq_fNg-H9!Ln z8h7_81l-dIX#0X2!BfyxF!PW(cRhqBMj$K<7!X>(%hdT}x|4in(Ld+2&7DbP;=hzE zXlS}k6@8oippdt4!VCr{5tg^|2@6NsG@^Z_f)R%k05%T*`SueJ;m3%-uWD;}J`>_7 zwqISe^rGj}bC9rFOwUpt^857EKUa-(^TT}M9*c<;A{4uWk6KpY;v8~pA$C9Dl; z0yY si$Nz%X_E{!ik#^umnao2C}_hPwKFnOI=a2FQ6@Dnt0P_(ng zpppEo`IKWvhoHbPfK6Z)88+d^W-sd%_$4djuO~|t)&RUr9s=W7%XVzPl=m1m@X4&H zaBzbR8luOs#KAZC#=M%ol#x5N?3LgtWILC%&9vosh+S2l0vZ5uFeRMJGVx>c2Y(7e zp(AI)LTXLdmq(gG5JOE+AF<&1AzGu + + + + + diff --git a/frontend/src/assets/styles/global.css b/frontend/src/assets/styles/global.css new file mode 100644 index 0000000..d0a0af7 --- /dev/null +++ b/frontend/src/assets/styles/global.css @@ -0,0 +1,23 @@ +/* 全局样式定义 */ +.hover-scale { + transition: transform 0.3s ease; +} + +.hover-scale:hover { + transform: scale(1.1); +} + +.max-width-7xl { + max-width: 1280px; +} + +/* 响应式调整 */ +@media (max-width: 600px) { + .text-h3 { + font-size: 1.75rem !important; + } + + .text-h4 { + font-size: 1.5rem !important; + } +} diff --git a/frontend/src/components/AppFooter.vue b/frontend/src/components/AppFooter.vue new file mode 100644 index 0000000..7444827 --- /dev/null +++ b/frontend/src/components/AppFooter.vue @@ -0,0 +1,82 @@ + + + + + + + + + diff --git a/frontend/src/components/CacheStatus.vue b/frontend/src/components/CacheStatus.vue new file mode 100644 index 0000000..3f85db2 --- /dev/null +++ b/frontend/src/components/CacheStatus.vue @@ -0,0 +1,136 @@ + ++ + + + © 2016-{{ (new Date()).getFullYear() }} Vuetify, LLC + — + + MIT License + +++ + + + diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..f74d894 --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,13 @@ + ++ + ++ 本地缓存状态 + + +++ ++++ 已缓存物品数量: {{ cachedItemsCount }} +++ 上次清理时间: {{ lastCleanTime ? formatDate(lastCleanTime) : '从未清理' }} +++ + 清除缓存 + ++ {{ cacheMessage }} + ++ + + diff --git a/frontend/src/components/README.md b/frontend/src/components/README.md new file mode 100644 index 0000000..ab0e8ff --- /dev/null +++ b/frontend/src/components/README.md @@ -0,0 +1,35 @@ +# 组件 + +此文件夹中的 Vue 模板文件会被自动导入。 + +## 🚀 使用方法 + +自动导入功能由 [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components) 实现。该插件会自动导入 `src/components` 目录下创建的 `.vue` 文件,并将它们注册为全局组件。这意味着你可以在应用程序中直接使用任何组件而无需手动导入。 + +以下示例假设存在一个位于 `src/components/MyComponent.vue` 的组件: + +```vue + ++ + + Findreve + + ++ + ++ + + +``` + +当模板渲染时,组件的导入语句会被自动内联,最终呈现为: + +```vue + ++ ++ + + +``` \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..1788c87 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,72 @@ +/** + * main.js + * + * Bootstraps Vuetify and other plugins then mounts the App` + */ + +// Components +import App from './App.vue' + +// Composables +import { createApp } from 'vue' + +// 先导入样式,确保在应用挂载前已加载CSS +import './assets/styles/global.css' // 导入全局样式 + +// 添加预加载完成标志以避免闪屏 +document.addEventListener('DOMContentLoaded', () => { + document.documentElement.setAttribute('data-app-loaded', 'true') +}) + +// 创建应用实例 +const app = createApp(App) + +// 异步导入其他依赖以优化初始加载 +Promise.all([ + import('./plugins/vuetify'), // Vuetify + import('./router'), // 路由 + import('./services/api_service'), // API服务 + import('./services/storage_service') // 本地存储服务 +]).then(([{ default: vuetify }, { default: router }, { default: apiService }, { default: storageService }]) => { + + // 添加全局事件总线功能 + app.config.globalProperties.$root = { + $on: (event, callback) => { + if (!app.config.globalProperties._eventBus) app.config.globalProperties._eventBus = {} + if (!app.config.globalProperties._eventBus[event]) app.config.globalProperties._eventBus[event] = [] + app.config.globalProperties._eventBus[event].push(callback) + }, + $off: (event, callback) => { + if (!app.config.globalProperties._eventBus || !app.config.globalProperties._eventBus[event]) return + if (!callback) { + app.config.globalProperties._eventBus[event] = [] + } else { + app.config.globalProperties._eventBus[event] = app.config.globalProperties._eventBus[event].filter(cb => cb !== callback) + } + }, + $emit: (event, ...args) => { + if (!app.config.globalProperties._eventBus || !app.config.globalProperties._eventBus[event]) return + app.config.globalProperties._eventBus[event].forEach(callback => callback(...args)) + } + } + + // 将API服务注册为全局属性 + app.config.globalProperties.$api = apiService + + // 将存储服务注册为全局属性 + app.config.globalProperties.$storage = storageService + + // 定期清理过期缓存 + setInterval(() => { + storageService.cleanExpiredCache(); + }, 30 * 60 * 1000); // 每30分钟执行一次 + + // 使用插件 + app.use(router) + app.use(vuetify) + + // 确保所有资源都加载完毕后再挂载应用 + setTimeout(() => { + app.mount('#app') + }, 0) +}) diff --git a/frontend/src/plugins/README.md b/frontend/src/plugins/README.md new file mode 100644 index 0000000..62201c7 --- /dev/null +++ b/frontend/src/plugins/README.md @@ -0,0 +1,3 @@ +# Plugins + +Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally. diff --git a/frontend/src/plugins/index.js b/frontend/src/plugins/index.js new file mode 100644 index 0000000..705f228 --- /dev/null +++ b/frontend/src/plugins/index.js @@ -0,0 +1,12 @@ +/** + * plugins/index.js + * + * Automatically included in `./src/main.js` + */ + +// Plugins +import vuetify from './vuetify' + +export function registerPlugins (app) { + app.use(vuetify) +} diff --git a/frontend/src/plugins/vuetify.js b/frontend/src/plugins/vuetify.js new file mode 100644 index 0000000..c73600f --- /dev/null +++ b/frontend/src/plugins/vuetify.js @@ -0,0 +1,61 @@ +/** + * plugins/vuetify.js + * + * Framework documentation: https://vuetifyjs.com` + */ + +// Styles +import '@mdi/font/css/materialdesignicons.css' +import 'vuetify/styles' + +// Composables +import { createVuetify } from 'vuetify' + +// 预设主题以防止闪烁 +const setInitialTheme = () => { + // 检查本地存储中的主题首选项 + const savedTheme = localStorage.getItem('vuetify-theme-preference') || 'dark' + + // 在DOM加载前应用主题类,避免闪烁 + document.documentElement.classList.add(`v-theme--${savedTheme}`) + + return savedTheme +} + +const defaultTheme = setInitialTheme() + +// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides +export default createVuetify({ + theme: { + defaultTheme, + themes: { + light: { + dark: false, + colors: { + primary: '#1867C0', + secondary: '#5CBBF6', + } + }, + dark: { + dark: true, + colors: { + primary: '#2196F3', + secondary: '#03A9F4', + } + } + }, + options: { + // 启用自定义属性以提高渲染性能 + customProperties: true, + // 缓存主题以避免重新计算 + cspNonce: 'findreve-theme', + // 减少主题变化时的闪烁 + variations: false + } + }, + defaults: { + VBtn: { + variant: 'flat' + }, + } +}) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..556b715 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,66 @@ +// src/router/index.js +import { createRouter, createWebHistory } from 'vue-router' +import Home from '@/views/Home.vue' +import Found from '@/views/Found.vue' +import Admin from '@/views/Admin.vue' +import Login from '@/views/Login.vue' +import NotFound from '@/views/NotFound.vue' + +const routes = [ + { + path: '/', + name: 'Home', + meta: { title: '主页'}, + component: Home + }, + { + path: '/found', + name: 'Found', + meta: { title: '关于此物品'}, + component: Found + }, + { + path: '/admin', + name: 'Admin', + component: Admin, + meta: { + requiresAuth: true, + title: 'Findreve 仪表盘' + } + }, + { + path: '/login', + name: 'Login', + meta: { title: '登录 Findreve'}, + component: Login + }, + // 添加404路由,必须放在最后以匹配所有未定义的路径 + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: NotFound, + meta: { title: '404 - 页面未找到' } + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 路由守卫,用于检查用户是否已登录并更新页面标题 +router.beforeEach((to, from, next) => { + // 更新页面标题 + document.title = to.meta.title || 'Findreve' + + const isAuthenticated = localStorage.getItem('user-token') // 简单的认证检查,实际应用中可能更复杂 + + if (to.meta.requiresAuth && !isAuthenticated) { + // 如果路由需要认证但用户未登录,重定向到登录页 + next({ name: 'Login', query: { redirect: to.fullPath } }) + } else { + next() + } +}) + +export default router \ No newline at end of file diff --git a/frontend/src/services/api_service.js b/frontend/src/services/api_service.js new file mode 100644 index 0000000..c8e1bfc --- /dev/null +++ b/frontend/src/services/api_service.js @@ -0,0 +1,315 @@ +/** + * API 服务 + * + * 提供统一的 HTTP 请求处理,包括认证令牌管理、错误处理等功能。 + * 自动处理令牌过期情况,在令牌失效时重定向到登录页面。 + * 集成了本地缓存功能,支持优先使用缓存数据。 + */ + +import router from '@/router'; +import storageService from './storage_service'; + +class ApiService { + /** + * 发送 HTTP 请求 + * + * @param {string} url - 请求地址 + * @param {Object} options - 请求选项 + * @returns {Promise+