From e76e856dbae0ca073a8c1da821de21ffd0dcfa29 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Thu, 12 Feb 2026 14:24:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20MVP=20V1=20=E9=8D=8F=E3=84=A6=E7=88=A4?= =?UTF-8?q?=E9=8E=BC=EE=85=9E=E7=BC=93=E7=80=B9=E5=B1=BE=E5=9E=9A=20-=20Fa?= =?UTF-8?q?stAPI=E9=8D=9A=E5=BA=A3=EE=81=AC=20+=20Vue3=E9=8D=93=E5=B6=87?= =?UTF-8?q?=EE=81=AC=20+=20=E7=BB=89=E5=B6=85=E7=93=99=E9=8F=81=E7=89=88?= =?UTF-8?q?=E5=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- backend/__pycache__/auth.cpython-310.pyc | Bin 0 -> 2811 bytes .../__pycache__/calculations.cpython-310.pyc | Bin 0 -> 6155 bytes backend/__pycache__/config.cpython-310.pyc | Bin 0 -> 419 bytes backend/__pycache__/database.cpython-310.pyc | Bin 0 -> 685 bytes backend/__pycache__/main.cpython-310.pyc | Bin 0 -> 2305 bytes backend/__pycache__/models.cpython-310.pyc | Bin 0 -> 7976 bytes backend/__pycache__/schemas.cpython-310.pyc | Bin 0 -> 8310 bytes backend/airlabs.db | Bin 0 -> 90112 bytes backend/auth.py | 62 + backend/calculations.py | 271 +++ backend/config.py | 13 + backend/database.py | 17 + backend/main.py | 72 + backend/models.py | 238 ++ backend/requirements.txt | 7 + backend/routers/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 139 bytes .../routers/__pycache__/auth.cpython-310.pyc | Bin 0 -> 1602 bytes .../routers/__pycache__/costs.cpython-310.pyc | Bin 0 -> 5416 bytes .../__pycache__/dashboard.cpython-310.pyc | Bin 0 -> 4190 bytes .../__pycache__/projects.cpython-310.pyc | Bin 0 -> 4859 bytes .../__pycache__/submissions.cpython-310.pyc | Bin 0 -> 5074 bytes .../routers/__pycache__/users.cpython-310.pyc | Bin 0 -> 2889 bytes backend/routers/auth.py | 35 + backend/routers/costs.py | 205 ++ backend/routers/dashboard.py | 152 ++ backend/routers/projects.py | 158 ++ backend/routers/submissions.py | 193 ++ backend/routers/users.py | 88 + backend/schemas.py | 249 ++ backend/seed.py | 195 ++ frontend/.gitignore | 24 + frontend/.vscode/extensions.json | 3 + frontend/README.md | 5 + frontend/index.html | 13 + frontend/package-lock.json | 2041 +++++++++++++++++ frontend/package.json | 24 + frontend/public/vite.svg | 1 + frontend/src/App.vue | 8 + frontend/src/api/index.js | 85 + frontend/src/assets/vue.svg | 1 + frontend/src/components/HelloWorld.vue | 43 + frontend/src/components/Layout.vue | 121 + frontend/src/main.js | 20 + frontend/src/router/index.js | 38 + frontend/src/stores/auth.js | 35 + frontend/src/style.css | 79 + frontend/src/views/Costs.vue | 156 ++ frontend/src/views/Dashboard.vue | 147 ++ frontend/src/views/Login.vue | 75 + frontend/src/views/ProjectDetail.vue | 154 ++ frontend/src/views/Projects.vue | 146 ++ frontend/src/views/Settlement.vue | 131 ++ frontend/src/views/Submissions.vue | 180 ++ frontend/src/views/Users.vue | 101 + frontend/vite.config.js | 15 + 56 files changed, 5601 insertions(+) create mode 100644 backend/__pycache__/auth.cpython-310.pyc create mode 100644 backend/__pycache__/calculations.cpython-310.pyc create mode 100644 backend/__pycache__/config.cpython-310.pyc create mode 100644 backend/__pycache__/database.cpython-310.pyc create mode 100644 backend/__pycache__/main.cpython-310.pyc create mode 100644 backend/__pycache__/models.cpython-310.pyc create mode 100644 backend/__pycache__/schemas.cpython-310.pyc create mode 100644 backend/airlabs.db create mode 100644 backend/auth.py create mode 100644 backend/calculations.py create mode 100644 backend/config.py create mode 100644 backend/database.py create mode 100644 backend/main.py create mode 100644 backend/models.py create mode 100644 backend/requirements.txt create mode 100644 backend/routers/__init__.py create mode 100644 backend/routers/__pycache__/__init__.cpython-310.pyc create mode 100644 backend/routers/__pycache__/auth.cpython-310.pyc create mode 100644 backend/routers/__pycache__/costs.cpython-310.pyc create mode 100644 backend/routers/__pycache__/dashboard.cpython-310.pyc create mode 100644 backend/routers/__pycache__/projects.cpython-310.pyc create mode 100644 backend/routers/__pycache__/submissions.cpython-310.pyc create mode 100644 backend/routers/__pycache__/users.cpython-310.pyc create mode 100644 backend/routers/auth.py create mode 100644 backend/routers/costs.py create mode 100644 backend/routers/dashboard.py create mode 100644 backend/routers/projects.py create mode 100644 backend/routers/submissions.py create mode 100644 backend/routers/users.py create mode 100644 backend/schemas.py create mode 100644 backend/seed.py create mode 100644 frontend/.gitignore create mode 100644 frontend/.vscode/extensions.json create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/index.js create mode 100644 frontend/src/assets/vue.svg create mode 100644 frontend/src/components/HelloWorld.vue create mode 100644 frontend/src/components/Layout.vue create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/stores/auth.js create mode 100644 frontend/src/style.css create mode 100644 frontend/src/views/Costs.vue create mode 100644 frontend/src/views/Dashboard.vue create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/ProjectDetail.vue create mode 100644 frontend/src/views/Projects.vue create mode 100644 frontend/src/views/Settlement.vue create mode 100644 frontend/src/views/Submissions.vue create mode 100644 frontend/src/views/Users.vue create mode 100644 frontend/vite.config.js diff --git a/backend/__pycache__/auth.cpython-310.pyc b/backend/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..472fea6fb64bc96b192cb40e73a912b38f9350c8 GIT binary patch literal 2811 zcmai0>u(#!5x>0`A0kCPEZLG}n|7Q6Od8oupJ-hpip11z63Ng5Y7Z_Br`wfu%pH&3 z+p|o80-9KXZ8QqfM+!JdTM23F_)DCoK%628`hV=#S_=Is-wU+q%u z+1YvghHN$cusbR_B}He$o~YO9Rdg=wi~5~@ zMd!mq(SS1$4LXBr-W48>jyOl6A!jHWc7}<@O0=*?A$POr{TMYKKin=&^OnImK)?a-G1~ zf1NDq^f>H2!FoYv0QQ&ZNsxJro??Tr_82`4+D_A<8Lje-bO^lm)A`wQ`@KK6@4Z}p zvb^($>kn_gvGe+!?VBGyghj2Q%PjQ-6G6me4oAvD;Z=ytPBlaj*S)Y}$jk(5usTg- z@xtuvrRp`GsWoXQ!V_r%(ubzTQnCE>B`--`EpHG(rBv3w?smjbZE$%^=w>gQ~69x5DKu8TZ7UnrGT#9)hmZM5untt49a`b|~;h|P~GmU&gu6#17)t~`;g79${(z~ zBnv~943cirL--Rgk(OGNI$e0gH#Q2wt9-K#gwO=3>44VlT_RsAw9J=u zpo0Zr}f){m&aaZ~y7@PhM-^yZ`XU#?J5G?<}qM%+Jq1g($)5BEaHv z>|K~JWLF1BpjarAHf6#KLYY}+9s~v8#mVo*6+)e)3zL-+N0)Kz4bA|flKY*r-*V?B z$L3})05~|+iQUjr_GoCO0Wia%&p<4$q>MLtH`4ZE5JEagJ)qKB40s}BE=VAvBDl&b zHh&UN`=&Y#f=%%bUYNLS4+}F5uNlT3m6;S^%LO$i5?@V~9t1f*LYoQB_=VyC?NNXVWS&og#QQxrNsdh6CcRE2eTY&3;mH`{bK$=w;+@V zeNn%m0R@?~UR)zBz^96tVgYvALHnIoAO7Y2?c48u{>fYIzrMWlw^t5Ecl*`*J8$3G zdF{sbdvCSx{HA^Dg9rck)q{_IDGLX0@ikz?V(&*|&ce{rYN~0kYd5tC?K-(i_#pCb zYDft%v1U*Mk{lCH1M{rOM-<}iuykioA(rnFsQiV6cd2Q=)~_I*AzlFjSXaF;WdOeV z=vhg==KlP-Fi3MjgO4`aAO0N>^eJHRQ5Y(wG*Q8UMD5#6)Ni}bO-(<0ablthAjW?T!gmSBfE)na z!okD84{He?b{tYfrOAIybM@5!x4L3*ObeAaD$&70bTYUAJ`T6cbZ-?#uodu+xb@jup)S;rJ4(I9IB=UJuUEs0`86nl^gauqoS1S pvghFJG-T)TPk}0DQ|loGatL1h;;#p0-Flw%8H47Cc{ysNtwV&yEXf%55%ImeWb{qk>E)n4Hx|fH`xG+h$I1t-6HbpTwCB~XrNAgJ8 z?HNH9MpX-b8xq^Z#<|G`?{ecxND&7LDi6nm5BwF(N3zn|A1FRR)xDue;CH$ut#%X4 z+Un^!bI$2AJ^lOjIo-`FCTA1+mcQ9c`^>ZbJEl!9<7JQQ&eS?-&dWo(Wu3I(jX}C~ zM0zJYjC(~$w~a{ef^>s70cmMOO1c}pNl4%4Z91ToH@D|81@_HrODoqdcW+$kJ@I(y z5AzrCztlT3-&;7-JAJ;l@bm8Jlf4UP-<068k{=aHA%iCHC$;2V1S+-c}eT6RZ8*LC~tY5r#{0``S_1YV)cu zl(N+B`!9tT~s2q&X&|g4Ocx@j#A%P zQWni6rNd~U9#>*^pK?s?Xr9*5W9>fW;h%R5VVM$ksk#*#!Y&OPGd9ERvs+@bZg*0B z|CzpjMtZ|>*l-*xw%~RCklINLi>%Pk)}?;7o{Q74%l6~b7HJ$iw)u3d$96scX{Dpj zeo>CKZlnxZE-~>onWzwr;XSgrj^moc`{X^nQ;0JdeOqTN&cTY380%2UTze*F@J%h#`W z&ps_7BNfG>|I>}9#NGB10JZYmk9wDW0h8Xj^UK$tE&bu-k4n8Cp6Q-HE}bo3f3AD# zy432ubaDBOA4u@^E?*sS3|E&I|CK8_LhqR+40thf=4oa~-w-(HaFp^A2v=dc` zpLiaMoVJ>z(CU*(#cj0O&4?Q$HKv1>3#YYal;omTd}R z!8gFfv88`F51rhwkBCDjBRz;YNwhZN)9G#azlotJpfFLr#2SR+Se`pqfsfEB44A4v zyptaYsuXSLj7#6bKt|{kRz*UIZxZ8h`gE(`nr|lQmfL(Sn&I#fG}*MJvcFr5y_45; zX0a(%SD9+6SpJ_JZrISV`x-hlEYpIWu3BtDEvh#DSy~Y` zMW(YnW52h{{1X^#4GCu{deUG`=sOJS$7bI2lJ@90LSdIGF zgyLyih>F5U%A>n@rood zJ_R)iMvr=1(UdpxJ8?-;p<5BVzZSL|JVPS0kDI&brT`T7ckW7rJ(!OPtNjn{d*J?i z4@%6MA=aGBIelCHK58f~nAR#0asf#iHIILQq&9q1{buAI@e}>v{ktC&Y{1K8_F**T z>?#BkLo9s9^lBW5hGRC|X4?%CLj;xR#72%C`EEmu5jqwg5UxWfO{fxG{&5;st*!^k zodPIpVjLm1Lof=d(OFT$!Uia4$unB^I)Q-wT?c$DZ7TpUp|W`!T2iJ95Xe4*PEnVj z8rmClKc?D@@5TUY;GsbNZMb3whATouqRO}3F!DcAqKyVC-fLGW#~w)Kw$WD+mkRa_s7Efx zrZ?CDJHoFZ2>wZo&bOc`n@N7HTCz#{8*GK%z&cA(qGG zx}-%Q7a~!LWRQt|Sp8_^9yzjtDZ7w-A~K5fw^33^?keC3km=3RkP}l5L<}>S>A_Y!oC9a#z20+>Acz;@U!s840cBDEMZWL|?K}b1a3`86gLYOE z}rOyEUx5v_vby%tcON;3U zMGerG@vuYVK8t(WBQqX~rX>xiG*PM$%An_I5;f>knMC_7_T+FEc_s{?05J#=z{~@& z78_d>AVuq#M+i%_cYpqn*0DNi?1O~nae?}ApdJ|k`h11Od34X$;h!VyG z_HKi@FOCMJ} z+sizLvx+zcd}iA}#XJhYPyzDHsqU5Q-BTyKFTIAOzxS(4tL4Y&hN~mbobG!Q{wJ z6YH=W`a!MfJ9+=nDh?2a;LY^!iD1^$Ff=N|?*M)wJ)jM!oW_u+}2^y&F)koKZ z#M&9Sjl-V1i%JSnj#$cf{#_tPZ2JE6bgf$Ro7K6|Iz*ZrL^Y%x_2-fl_d$-KU!MG@ zl*0GGzsy5u9!9vNjN8OOp6>_zzv+=IQjVZnn5i6|6WLCp1%6XhDuPRpKh4abyh?PM z$V3lYJSyi``Gr47e)ghqEaXBJnsdi)wi}fhzcmy1oFPz=9_X3CVTqH8)5kTGQTte>1@HTIUCmbpB#cXbLZvkeTy)GYB0 ze$?7;6>VMR--N3*{6gG9R4w@B9dvHbFSJ)jMhq?Np;~12dwP`W_pral`X(hdu(z5> zZlq=^LiQ|dknVsBSly10AJ?-U{P11WtLK`?2I^U|kBuk~d?PLLv<2ktS5;4st(tlj z2Q4FZ&Zr$7tav!4;cZ9@vISe1I~DkXgcyfBLYCU8#OVV#U6jwowJ$5D+f<9KT)cvO ztAA|q`V%z%#Yy>K=$zU2legttNm&=X54JE#K1q8Z*%%AMG&wRe&oO<0rs+DvNz zddiEF!a9hwX`Jr0pY~3_(tGiLyEo3sz>)&0;E21`s9!H98H6&8x;kiu;okDtKS#|c z;83KnB6$5MVwbj&83D?NM1uz&aA_OLl|puC)IyFCp~;Kegp=I zA+}1#UWGibketm#>mjg7KwvPRp&vzo-$2FjN9cz;&`dExfz7C5&5F8}@UR){oBpAu zeG4V=|LbY<|E6+zJfBc?!+INX#+xwTTYi6%s#LsIwNgp);cS58kw^T-+&wMc;KX!^ zy(;7y1d>p6dX%ZQf&jG)vb0d1OjvYWK&PwX_<~c)o0yFj&RRoBDg-P^P6RVqWr-|Z zoOw`ABa1QgwhWD iT}0y{6_UD=)yFVzW9moLtUhT>J}|j`(w;0&ef%FiXAsf= literal 0 HcmV?d00001 diff --git a/backend/__pycache__/config.cpython-310.pyc b/backend/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec6d96a6c013860923ff3bea5173bff836cc3188 GIT binary patch literal 419 zcmYjNy-vbV6u$id3IRtJ7Y7%HLSoc`7-NMSAV49#CCUxSP3gU20b6W~3G9w+JOFOG z_y8upNLOPBui)gZ#>A7HFW+}g&aaotWdOIaw{nmGz(*|po6N-p_u!ZV1sG6)ic3)`L^D!pLdU|?fdJFYoJHjs%C1J z8iJ%}w3hkce(Hp*Tq>1{CCl-r)-Wj2VU$6zVnCBLVSo7-S7*LAW_G9s%=THRPS`@V zZ!LGkRL6~zbZ&=^=i)>i9i5yyn*;#6JRe$6q7)(2vk8erFNl&O7Bct27kSQ?IN-F_ zs&$NpS#RTgtx|yq5wp{TI)T^Sh5<>tq4!LPaQ>=eG#h%2RJA@LU1*SYM>p#)z*pj4 zF?=0X%LC2vTl|uP?(fEfp=D2)O9!^+-Z-P;Y!NAEcYNO|JLm5Q!5#-0C_+}sO0pme Giu4U?*>dUt literal 0 HcmV?d00001 diff --git a/backend/__pycache__/database.cpython-310.pyc b/backend/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aec53ff42e49771b9b97ff6c0fefe2afd82e2005 GIT binary patch literal 685 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 literal 0 HcmV?d00001 diff --git a/backend/__pycache__/schemas.cpython-310.pyc b/backend/__pycache__/schemas.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f288d7cb551e886baa82b4c9935760398fd10dd GIT binary patch literal 8310 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 literal 0 HcmV?d00001 diff --git a/backend/airlabs.db b/backend/airlabs.db new file mode 100644 index 0000000000000000000000000000000000000000..91408df6080fb63ad6a36ccb15ede3e233e7d3fe GIT binary patch literal 90112 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≪YAtz;L!U)%r(8*Q~YEm$4d=G4Kgjx)CM;F2H z+T|lFk|V!qkgJw2s7RLoTGp#xZ?t!H5!asJ^vaj<|F%e!%IB)cR6W2|3`iB^l7%MJ zB^FDp(5x<(v|Z7BA|)BjVR0o(g#wnQy2SauRgj`Yq!P{XyC%8Oozet=VT#Ic;KOrwW_0k~Q@679rgURGS+8^p8PWon1wWu%OS%vzjHWDf00e-*2Z_MNc686$MO?A*!<6d0EWVIti@Cy@ zWOm4B9+VGY6pzE>qU{Cc`wsp4jCPlcxY{C{GV&g_A9p}9=-FPbGC&vYT;sNMav_nO zuYE(QTpSnOzLX4{a~bYtQ-XSY3RPKrM>We_#CHx9O8(#h3m!F#Z;neUDH-%uOO)?} zRx2+#tTL`d15u0P3P~F6fm6hl2Y983ZvlxHIFMdy744I&rq+&?Nn5RAPC<=ZZ-KQC zVG*S|+2vXt)f00e*l5C8%|K$!si{$H63{`)mE$ZOIF(QBg>5Wm*&gncGEXZ zKI2b~D@L>7tA?SLpR}yBwClgAKcoAV?wanX_Pg3Ju|-@a?3y?BjL=*!sUghW-A7FX zq3QH=yIUqB;nCPgG!PC|w|pV=lX{#*(?P#))F0V;?KSu97q4#IcxUta8ylZ`b?b%i z57AC~u-{4dI|unK7fm?_AEn)oI)~;StoSkCk)MN;HT|6AtvfyZme|h&jjE5Ax9&W_ zZ;AaJYgBy%T28l;pPok5N1)}TmHgb_sQL)BoGv9l-HobGg_f(|t>}kpRDCM6kRPW) zeLjAa^V6Yp{Y3FA2kzj?shq#7pBAnmki;d-W5a=jJuFf6#v5eL9BT!O~T=!J`@DX3V zKB9;(uTP!GW#5q>zS&XFBKK1#avjhUYHU*xCjRJU7Ky=tmk!u8YNkAE^83HgFZ2`+R6m9HJDpDGVFTtXC9 zj0Q{XqE_nqP(5$tX?1Txz*Gzki~D$ICPGu+*@xAAsfULpQjD=~#p+>Hj}c34e8tdc zt$xMoQB)5Ui#Fa_BPgl|i>20vQdEx^OIE&O4WXzWHc}SOS;tZHc8ZdLNF5cb2Mxwt z@q#-KnJ^??>gi8CW=xxq8{$Amr^#f}bh*3TTGUDTCxbrp)A=d%9xwhv+E{;O^Tul% zFTb+&&YO5jvi0h_{1{EKkDWH|$}TOoE3~<&Z=U_6z8os&sCX!R@p0bK(rUn|9jDdo%{wFX?wrzW%L$6nE%t>=FI<*pQgR*}rp*75pQgD#{XL!kBl~+g|3~)sbpDT&)Rg%@ z^3#<0Kl0O*`9Jd0l=(mM)0Fu?^3#<0Kl0O*`9Jd0l=(mHZRY%+_BL_;PkWm;|3?*T z-uxdqYu@}HQEb}$A62Y*^MB;5dGmkT+l=`??QP2ZpZ4y}{2$rfllecgt1|yrxnJQD z-&YV;QACsCSPP!DitmFc-Uhk#%4cq^zj^EC4>xXp5#0vyY^^`TPvsRB`)F@WzZ str: + return pwd_context.hash(password) + + +def verify_password(plain: str, hashed: str) -> bool: + return pwd_context.verify(plain, hashed) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User: + """从 JWT token 解析当前用户""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="登录已过期,请重新登录", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: int = payload.get("sub") + if user_id is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + user = db.query(User).filter(User.id == user_id).first() + if user is None or not user.is_active: + raise credentials_exception + return user + + +def require_role(*roles: UserRole): + """权限装饰器:要求当前用户具有指定角色之一""" + def role_checker(current_user: User = Depends(get_current_user)): + if current_user.role not in [r.value for r in roles] and current_user.role not in roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="权限不足" + ) + return current_user + return role_checker diff --git a/backend/calculations.py b/backend/calculations.py new file mode 100644 index 0000000..c678849 --- /dev/null +++ b/backend/calculations.py @@ -0,0 +1,271 @@ +""" +计算引擎 —— 所有成本分摊、损耗、效率计算逻辑集中在此模块 +修改计算规则只需改此文件。 +""" +from sqlalchemy.orm import Session +from sqlalchemy import func as sa_func, and_ +from collections import defaultdict +from datetime import date, timedelta +from models import ( + User, Project, Submission, AIToolCost, AIToolCostAllocation, + OutsourceCost, CostOverride, WorkType, CostAllocationType +) +from config import WORKING_DAYS_PER_MONTH + + +# ──────────────────────────── 人力成本分摊 ──────────────────────────── + +def calc_labor_cost_for_project(project_id: int, db: Session) -> float: + """ + 计算某项目的累计人力成本 + 规则: + - 有秒数的提交 → 按各项目产出秒数比例分摊日成本 + - 无秒数的提交 → 按各项目提交条数比例分摊日成本 + - 管理员手动调整优先 + """ + # 找出所有给此项目提交过的人 + submitters = db.query(Submission.user_id).filter( + Submission.project_id == project_id + ).distinct().all() + submitter_ids = [s[0] for s in submitters] + + total_labor = 0.0 + + for uid in submitter_ids: + user = db.query(User).filter(User.id == uid).first() + if not user: + continue + daily_cost = user.daily_cost + + # 找这个人在此项目的所有提交日期 + dates = db.query(Submission.submit_date).filter( + Submission.user_id == uid, + Submission.project_id == project_id, + ).distinct().all() + + for (d,) in dates: + # 检查是否有手动调整 + override = db.query(CostOverride).filter( + CostOverride.user_id == uid, + CostOverride.date == d, + CostOverride.project_id == project_id, + ).first() + if override: + total_labor += override.override_amount + continue + + # 这个人这天所有项目的提交 + day_subs = db.query(Submission).filter( + Submission.user_id == uid, + Submission.submit_date == d, + ).all() + + # 计算这天各项目的秒数和条数 + project_seconds = defaultdict(float) + project_counts = defaultdict(int) + total_day_seconds = 0.0 + total_day_count = 0 + + for s in day_subs: + project_seconds[s.project_id] += s.total_seconds + project_counts[s.project_id] += 1 + total_day_seconds += s.total_seconds + total_day_count += 1 + + # 分摊 + if total_day_seconds > 0: + # 有秒数 → 按秒数比例 + ratio = project_seconds.get(project_id, 0) / total_day_seconds + elif total_day_count > 0: + # 无秒数 → 按条数比例 + ratio = project_counts.get(project_id, 0) / total_day_count + else: + ratio = 0 + + total_labor += daily_cost * ratio + + return round(total_labor, 2) + + +# ──────────────────────────── AI 工具成本 ──────────────────────────── + +def calc_ai_tool_cost_for_project(project_id: int, db: Session) -> float: + """计算某项目的 AI 工具成本""" + total = 0.0 + + # 1. 直接指定项目的 + direct = db.query(sa_func.sum(AIToolCost.amount)).filter( + AIToolCost.allocation_type == CostAllocationType.PROJECT, + AIToolCost.project_id == project_id, + ).scalar() or 0 + total += direct + + # 2. 手动分摊的 + manual = db.query(AIToolCostAllocation).filter( + AIToolCostAllocation.project_id == project_id, + ).all() + for alloc in manual: + cost = db.query(AIToolCost).filter(AIToolCost.id == alloc.ai_tool_cost_id).first() + if cost: + total += cost.amount * alloc.percentage / 100 + + # 3. 内容组整体(按产出秒数比例分摊) + team_costs = db.query(AIToolCost).filter( + AIToolCost.allocation_type == CostAllocationType.TEAM, + ).all() + if team_costs: + # 所有项目的总秒数 + all_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.total_seconds > 0 + ).scalar() or 0 + # 此项目的秒数 + proj_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == project_id, + Submission.total_seconds > 0, + ).scalar() or 0 + + if all_secs > 0: + ratio = proj_secs / all_secs + for c in team_costs: + total += c.amount * ratio + + return round(total, 2) + + +# ──────────────────────────── 外包成本 ──────────────────────────── + +def calc_outsource_cost_for_project(project_id: int, db: Session) -> float: + """计算某项目的外包成本""" + total = db.query(sa_func.sum(OutsourceCost.amount)).filter( + OutsourceCost.project_id == project_id, + ).scalar() or 0 + return round(total, 2) + + +# ──────────────────────────── 损耗计算 ──────────────────────────── + +def calc_waste_for_project(project_id: int, db: Session) -> dict: + """ + 计算项目损耗 + 返回: {test_waste, overproduction_waste, total_waste, waste_rate, target_seconds} + """ + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + return {} + + target = project.target_total_seconds + + # 测试损耗:工作类型为"测试"的全部秒数 + test_waste = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == project_id, + Submission.work_type == WorkType.TEST, + ).scalar() or 0 + + # 全部有秒数的提交总量 + total_submitted = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == project_id, + Submission.total_seconds > 0, + ).scalar() or 0 + + # 超产损耗 + overproduction_waste = max(0, total_submitted - target) + + total_waste = test_waste + overproduction_waste + waste_rate = round(total_waste / target * 100, 1) if target > 0 else 0 + + return { + "target_seconds": target, + "total_submitted_seconds": round(total_submitted, 1), + "test_waste_seconds": round(test_waste, 1), + "overproduction_waste_seconds": round(overproduction_waste, 1), + "total_waste_seconds": round(total_waste, 1), + "waste_rate": waste_rate, + } + + +# ──────────────────────────── 团队效率 ──────────────────────────── + +def calc_team_efficiency(project_id: int, db: Session) -> list: + """ + 人均基准对比法: + - 人均基准 = 目标秒数 ÷ 参与制作人数 + - 每人超出比例 = (个人提交 - 人均基准) / 人均基准 + """ + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + return [] + + target = project.target_total_seconds + + # 获取每个人的提交总秒数(仅有秒数的提交) + per_user = db.query( + Submission.user_id, + sa_func.sum(Submission.total_seconds).label("total_secs"), + sa_func.count(Submission.id).label("count"), + ).filter( + Submission.project_id == project_id, + Submission.total_seconds > 0, + ).group_by(Submission.user_id).all() + + if not per_user: + return [] + + num_people = len(per_user) + baseline = target / num_people if num_people > 0 else 0 + + result = [] + for user_id, total_secs, count in per_user: + user = db.query(User).filter(User.id == user_id).first() + excess = total_secs - baseline + excess_rate = round(excess / baseline * 100, 1) if baseline > 0 else 0 + result.append({ + "user_id": user_id, + "user_name": user.name if user else "未知", + "total_seconds": round(total_secs, 1), + "submission_count": count, + "baseline": round(baseline, 1), + "excess_seconds": round(excess, 1), + "excess_rate": excess_rate, + }) + + result.sort(key=lambda x: x["total_seconds"], reverse=True) + return result + + +# ──────────────────────────── 项目完整结算 ──────────────────────────── + +def calc_project_settlement(project_id: int, db: Session) -> dict: + """生成项目结算报告""" + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + return {} + + labor = calc_labor_cost_for_project(project_id, db) + ai_tool = calc_ai_tool_cost_for_project(project_id, db) + outsource = calc_outsource_cost_for_project(project_id, db) + total_cost = labor + ai_tool + outsource + waste = calc_waste_for_project(project_id, db) + efficiency = calc_team_efficiency(project_id, db) + + result = { + "project_id": project.id, + "project_name": project.name, + "project_type": project.project_type.value if hasattr(project.project_type, 'value') else project.project_type, + "labor_cost": labor, + "ai_tool_cost": ai_tool, + "outsource_cost": outsource, + "total_cost": round(total_cost, 2), + **waste, + "team_efficiency": efficiency, + } + + # 客户正式项目计算盈亏 + pt = project.project_type.value if hasattr(project.project_type, 'value') else project.project_type + if pt == "客户正式项目" and project.contract_amount: + result["contract_amount"] = project.contract_amount + result["profit_loss"] = round(project.contract_amount - total_cost, 2) + else: + result["contract_amount"] = None + result["profit_loss"] = None + + return result diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..e25ee8d --- /dev/null +++ b/backend/config.py @@ -0,0 +1,13 @@ +"""应用配置""" +import os + +# 数据库 +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./airlabs.db") + +# JWT 认证 +SECRET_KEY = os.getenv("SECRET_KEY", "airlabs-project-secret-key-change-in-production") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 24 小时 + +# 成本计算 +WORKING_DAYS_PER_MONTH = 22 diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..3c26832 --- /dev/null +++ b/backend/database.py @@ -0,0 +1,17 @@ +"""数据库初始化""" +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +from config import DATABASE_URL + +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +def get_db(): + """获取数据库会话(FastAPI 依赖注入用)""" + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..ffccbdc --- /dev/null +++ b/backend/main.py @@ -0,0 +1,72 @@ +"""AirLabs Project —— 主入口""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from database import engine, Base +from models import User, UserRole, PhaseGroup +from auth import hash_password +import os + +# 创建所有表 +Base.metadata.create_all(bind=engine) + +app = FastAPI(title="AirLabs Project", version="1.0.0") + +# CORS(开发阶段允许所有来源) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 注册路由 +from routers.auth import router as auth_router +from routers.users import router as users_router +from routers.projects import router as projects_router +from routers.submissions import router as submissions_router +from routers.costs import router as costs_router +from routers.dashboard import router as dashboard_router + +app.include_router(auth_router) +app.include_router(users_router) +app.include_router(projects_router) +app.include_router(submissions_router) +app.include_router(costs_router) +app.include_router(dashboard_router) + +# 前端静态文件 +frontend_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist") +if os.path.exists(frontend_dir): + app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dir, "assets")), name="assets") + + @app.get("/{full_path:path}") + async def serve_frontend(full_path: str): + file_path = os.path.join(frontend_dir, full_path) + if os.path.isfile(file_path): + return FileResponse(file_path) + return FileResponse(os.path.join(frontend_dir, "index.html")) + + +@app.on_event("startup") +def init_default_owner(): + """首次启动时创建默认 Owner 账号""" + from database import SessionLocal + db = SessionLocal() + try: + if not db.query(User).filter(User.role == UserRole.OWNER).first(): + owner = User( + username="admin", + password_hash=hash_password("admin123"), + name="管理员", + phase_group=PhaseGroup.PRODUCTION, + role=UserRole.OWNER, + monthly_salary=0, + ) + db.add(owner) + db.commit() + print("[OK] default admin created: admin / admin123") + finally: + db.close() diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..ab32cb1 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,238 @@ +"""数据库模型 —— 所有表定义""" +from sqlalchemy import ( + Column, Integer, String, Float, Date, DateTime, Text, + ForeignKey, Enum as SAEnum, JSON +) +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base +import enum + + +# ──────────────────────────── 枚举定义 ──────────────────────────── + +class ProjectType(str, enum.Enum): + CLIENT_FORMAL = "客户正式项目" + CLIENT_TEST = "客户测试项目" + INTERNAL_ORIGINAL = "内部原创项目" + INTERNAL_TEST = "内部测试项目" + + +class ProjectStatus(str, enum.Enum): + IN_PROGRESS = "制作中" + COMPLETED = "已完成" + + +class PhaseGroup(str, enum.Enum): + PRE = "前期" + PRODUCTION = "制作" + POST = "后期" + + +class UserRole(str, enum.Enum): + MEMBER = "成员" + LEADER = "组长" + SUPERVISOR = "主管" + OWNER = "Owner" + + +class WorkType(str, enum.Enum): + PRODUCTION = "制作" + TEST = "测试" + PLAN = "方案" + + +class ContentType(str, enum.Enum): + ANIMATION = "内容制作" + DESIGN = "设定策划" + EDITING = "剪辑后期" + OTHER = "其他" + + +class SubmitTo(str, enum.Enum): + LEADER = "组长" + PRODUCER = "制片" + INTERNAL = "内部" + EXTERNAL = "外部" + + +class SubscriptionPeriod(str, enum.Enum): + MONTHLY = "月" + YEARLY = "年" + + +class CostAllocationType(str, enum.Enum): + PROJECT = "指定项目" + TEAM = "内容组整体" + MANUAL = "手动分摊" + + +class OutsourceType(str, enum.Enum): + ANIMATION = "动画" + EDITING = "剪辑" + FULL_EPISODE = "整集" + + +# ──────────────────────────── 用户 ──────────────────────────── + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(50), unique=True, nullable=False, index=True) + password_hash = Column(String(255), nullable=False) + name = Column(String(50), nullable=False) + phase_group = Column(SAEnum(PhaseGroup), nullable=False) + role = Column(SAEnum(UserRole), nullable=False, default=UserRole.MEMBER) + monthly_salary = Column(Float, nullable=False, default=0) + is_active = Column(Integer, nullable=False, default=1) + created_at = Column(DateTime, server_default=func.now()) + + # 关系 + submissions = relationship("Submission", back_populates="user") + led_projects = relationship("Project", back_populates="leader") + + @property + def daily_cost(self): + """日成本 = 月薪 ÷ 22""" + from config import WORKING_DAYS_PER_MONTH + return round(self.monthly_salary / WORKING_DAYS_PER_MONTH, 2) if self.monthly_salary else 0 + + +# ──────────────────────────── 项目 ──────────────────────────── + +class Project(Base): + __tablename__ = "projects" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + project_type = Column(SAEnum(ProjectType), nullable=False) + status = Column(SAEnum(ProjectStatus), nullable=False, default=ProjectStatus.IN_PROGRESS) + leader_id = Column(Integer, ForeignKey("users.id"), nullable=True) + current_phase = Column(SAEnum(PhaseGroup), nullable=False, default=PhaseGroup.PRE) + episode_duration_minutes = Column(Float, nullable=False) + episode_count = Column(Integer, nullable=False) + estimated_completion_date = Column(Date, nullable=True) + actual_completion_date = Column(Date, nullable=True) + contract_amount = Column(Float, nullable=True) # 仅客户正式项目 + created_at = Column(DateTime, server_default=func.now()) + + # 关系 + leader = relationship("User", back_populates="led_projects") + submissions = relationship("Submission", back_populates="project") + outsource_costs = relationship("OutsourceCost", back_populates="project") + ai_tool_allocations = relationship("AIToolCostAllocation", back_populates="project") + + @property + def target_total_seconds(self): + """目标总秒数 = 单集时长(分) × 60 × 集数""" + return int(self.episode_duration_minutes * 60 * self.episode_count) + + +# ──────────────────────────── 内容提交 ──────────────────────────── + +class Submission(Base): + __tablename__ = "submissions" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + project_phase = Column(SAEnum(PhaseGroup), nullable=False) + work_type = Column(SAEnum(WorkType), nullable=False) + content_type = Column(SAEnum(ContentType), nullable=False) + duration_minutes = Column(Float, nullable=True, default=0) + duration_seconds = Column(Float, nullable=True, default=0) + total_seconds = Column(Float, nullable=False, default=0) # 系统自动计算 + hours_spent = Column(Float, nullable=True) # 可选:投入时长(小时) + submit_to = Column(SAEnum(SubmitTo), nullable=False) + description = Column(Text, nullable=True) + submit_date = Column(Date, nullable=False) + created_at = Column(DateTime, server_default=func.now()) + + # 关系 + user = relationship("User", back_populates="submissions") + project = relationship("Project", back_populates="submissions") + history = relationship("SubmissionHistory", back_populates="submission") + + +# ──────────────────────────── AI 工具成本 ──────────────────────────── + +class AIToolCost(Base): + __tablename__ = "ai_tool_costs" + + id = Column(Integer, primary_key=True, index=True) + tool_name = Column(String(100), nullable=False) + subscription_period = Column(SAEnum(SubscriptionPeriod), nullable=False) + amount = Column(Float, nullable=False) + allocation_type = Column(SAEnum(CostAllocationType), nullable=False) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=True) # 指定项目时 + recorded_by = Column(Integer, ForeignKey("users.id"), nullable=False) + record_date = Column(Date, nullable=False) + created_at = Column(DateTime, server_default=func.now()) + + # 关系 + allocations = relationship("AIToolCostAllocation", back_populates="ai_tool_cost") + + +class AIToolCostAllocation(Base): + """AI 工具成本手动分摊明细""" + __tablename__ = "ai_tool_cost_allocations" + + id = Column(Integer, primary_key=True, index=True) + ai_tool_cost_id = Column(Integer, ForeignKey("ai_tool_costs.id"), nullable=False) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + percentage = Column(Float, nullable=False) # 0-100 + + ai_tool_cost = relationship("AIToolCost", back_populates="allocations") + project = relationship("Project", back_populates="ai_tool_allocations") + + +# ──────────────────────────── 外包成本 ──────────────────────────── + +class OutsourceCost(Base): + __tablename__ = "outsource_costs" + + id = Column(Integer, primary_key=True, index=True) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + outsource_type = Column(SAEnum(OutsourceType), nullable=False) + episode_start = Column(Integer, nullable=True) + episode_end = Column(Integer, nullable=True) + amount = Column(Float, nullable=False) + recorded_by = Column(Integer, ForeignKey("users.id"), nullable=False) + record_date = Column(Date, nullable=False) + created_at = Column(DateTime, server_default=func.now()) + + project = relationship("Project", back_populates="outsource_costs") + + +# ──────────────────────────── 人力成本手动调整 ──────────────────────────── + +class CostOverride(Base): + """管理员手动修改某人某天的成本分摊""" + __tablename__ = "cost_overrides" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + date = Column(Date, nullable=False) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + override_amount = Column(Float, nullable=False) + adjusted_by = Column(Integer, ForeignKey("users.id"), nullable=False) + reason = Column(Text, nullable=True) + created_at = Column(DateTime, server_default=func.now()) + + +# ──────────────────────────── 提交历史版本 ──────────────────────────── + +class SubmissionHistory(Base): + """内容提交的修改历史""" + __tablename__ = "submission_history" + + id = Column(Integer, primary_key=True, index=True) + submission_id = Column(Integer, ForeignKey("submissions.id"), nullable=False) + changed_by = Column(Integer, ForeignKey("users.id"), nullable=False) + change_reason = Column(Text, nullable=False) + old_data = Column(JSON, nullable=False) + new_data = Column(JSON, nullable=False) + created_at = Column(DateTime, server_default=func.now()) + + submission = relationship("Submission", back_populates="history") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..38c85ae --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn[standard] +sqlalchemy +pydantic +python-jose[cryptography] +passlib[bcrypt] +python-multipart diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/routers/__pycache__/__init__.cpython-310.pyc b/backend/routers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5cc7b474205f0b672836c138f97bf2212934a67 GIT binary patch literal 139 zcmd1j<>g`kg6;XenIQTxh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2DH#VW=zv&bhg zsaPSPC_gJTxg;hjF*!RmFD0fZzqBN^s5mA*J~J<~BtBlRpz;=nO>TZlX-=vg$edy( IAi=@_0K51erT_o{ literal 0 HcmV?d00001 diff --git a/backend/routers/__pycache__/auth.cpython-310.pyc b/backend/routers/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..851f5329550d7c9728552fa8a139354958903f52 GIT binary patch literal 1602 zcmY*Z&1)n@6z{73n(xVXB4kBGaA(zIm-QqfMibeX#!2pR87*e-P4n*>bMiK zhZvE)40;HHu!k&&=*6H2IePPtsHUcd4iH-2Nh+@@@tO&FgUHu=D6Oyw0w z=JMAcFHa9I&rUCne|rIQtKsP4!rJnBmMbo_zsP&M%cQQ}+t^rJdeq`Q6=z-TNfoJF zHi-5ga47)-)7$1MWKFP~P02;W)|LC&cHCX(yE&IiyBpa<-qk)XuI38X>w8?p?S9yc zr2H}yOpmt&j}#B1Rtv(0%4`_HxUF0W-c?}^oA-VTYBiaaAHRFRH- zV0G+=6lTsd>oGl`Vp_R?c?Y!PA5rtq?K|P-%EIQx zz18*QU};eoV+mbV@gCP>R~~o6FILw-TV7mTS`k&;N@Lb=#2AJe$GL-d(5|`bFP3;0 zuzzkXezdg^i~CVizO^Q@4sWTgX4C>=u`OXpE4PpsbG^QHP5<%YvIc6Fj*%MiKt@Q3 zhz3CW_B>=*xlXPY0sPfKy9{DDu>pp%pyvdHA|I3k+GXU3icz#WUKerK;J(3cz}&yW zeXt84MUWCm8F*2I?%ONc-HVc(H;UpNI7Hk4r5#iKau4du!)?f|ryY?cT-Va9t9Fup zD5E42eO+R}g+4F?y7V^gEDc`~3nSI22)zI6u;P96@F8fVln@`_;(Ms(P|c&djS2&a zJD?gw+(H|xKU6ot8b0=vPlGL|KvC)t{AS=AS`P7Eio3A1(s)DrVaT#p7;3*A0h4>N zz^5l{RIj;HleAwRU!!%AWMwGDqQ=8o6WGl`lU3m?N-dBQ(I8c2Mgjx|Fn=9?fBq(&?D JEUA=c$-f0ax`_Y) literal 0 HcmV?d00001 diff --git a/backend/routers/__pycache__/costs.cpython-310.pyc b/backend/routers/__pycache__/costs.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3ee76a0277f0ae51aab4a6ac04d36684c67b55d GIT binary patch literal 5416 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 literal 0 HcmV?d00001 diff --git a/backend/routers/__pycache__/dashboard.cpython-310.pyc b/backend/routers/__pycache__/dashboard.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d10805cf2c65e9ca8154ab40339acf52a63ec3d5 GIT binary patch literal 4190 zcmZ`+-ESMm5x>1V-W@+hQIsf3vL%1Uv>OWr`T-P%8`l<61FkJdN(>xa5GU?QI_i$3 z_LjEg9#NxA(IS9?1Wiz*Hc+uZ0k=Ti=BX+2(wF`d`x?uRfxZU_V!JbYq(1D1#O>|Q z&d$uv%>HJtna!pY_+4vldDq7j=j+6dmFlA2#hwMYaVf%0}ZBGX?_DpcZK0*{%r74CA!2uA^8j6!aw4k z++AbPA{~`$-jHj?UR?uvPQg>@YEsw1wIw<(*Pf>3MWr&)9tRIU{`{|B_8vaHe{bf@ z%+t?*_w>>4zx?OlpML!5Gni8(~RWc~ntrGv+pUwT((L z%-v{v%&js|R53)U=J>U0E7n&HUCwlu=MrmX*mtv3!80Lb6;mDN(SyaLDgMZ@oJvisBPZSXPuVUF6@ua}DL+4PL+vCxsVEtxR@6>9N=MdB!qy^dJ0GQcgv53ZOFM__ zg(xX4837A2Q3m+TC{r)~Q0bT(7o`=W+he>`AMeFGE7i-=4t&RUO!So2FR9+i?sBK= zlYQ=2tOn~RslKlFv~67V2UwLG>{Qy*=Z8M-ZVGG~;)f`S^e8c|)DKfqQo3p<&!?lD zqz`q~NhQip0(b#^cva~XA{BhF*cst7(Fp7g`HN9;`v^@?y{C3YfqJwv7LB1D>wC1>z(nauzg%g8>ye*C!zNSHM=T%j=lP+hKlc~6peu_ z)MTM1UWvxL1ZIwXNZ1HJjlC`b`AM33ki;htY_J zqe4`UCh7QtB*0_2JqqV!=hqK+KE1mOl21PQ$CIr`&!8wN0B9O@Hdmo5e+-GD#GBml zt0BS|=Pm_=sWkyJpHZAm+;sf5D^fAaFhIzwm84KTD)fdExFR#aC62(BhtVnnWUaPb zK#vBubKGHwaDAcn@N0V5G1Qat!fNxT%P1%-yCHxusGHH1n+?kf8Wq^Zc z&-qON*6=)ZV%{ZyO>N!X6ee@QFHjgx!a@O_id}%fE`}gdoH;dKb%JIaN5WjSFF$hE|-kA)F z89G(GQUHHDU^(P62H|1g4Mt_R5 zjuUoju+H)9%%Bg6655%5dLkQ)Zo$xeY5bP(|!=uWsVuM!7oUiCHI_~DYF#EfGHQKqj zehseO?|}qaDJD!E9G)Vq1(a`f+ukMlfXC2#_(+c-oN%Z-eLoQSnG^4I=l-9b-TUK{ zPk#IKgWtrZyh2zR_$%5g_WxhA_q2ZmYh>+7nQR04S1V_PS*_A$ty&f43VcMs4W401 zU}%?gl*ok}K74$vxxwbyCJR^sW<+X_$P8qiN2-NK8^4_-z%neQlIAQsaB!12Gy({c zAI9jJ#+3-+5_=^~O7LcRqzxm#?zH(D!;cC!0*#%)Pmi|m;B_FBL6n%t;B#DIC2a8X zP7cKz{dS;&^Wzm;M z-Z}@yw|)0KUKqkF(CA80p2|G5r$}C(Avwd+3^haa7jULts%qwi)i3}L^yl$deW7ZZ O=T^qhiU(-<=l=tKN!G{! literal 0 HcmV?d00001 diff --git a/backend/routers/__pycache__/projects.cpython-310.pyc b/backend/routers/__pycache__/projects.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..330608ef8023c57a74f2d0e9e958120e8ae5e5d4 GIT binary patch literal 4859 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 literal 0 HcmV?d00001 diff --git a/backend/routers/__pycache__/submissions.cpython-310.pyc b/backend/routers/__pycache__/submissions.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..530a0d07e08f998ac94b429bd3f1c7eebf9a6525 GIT binary patch literal 5074 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 literal 0 HcmV?d00001 diff --git a/backend/routers/__pycache__/users.cpython-310.pyc b/backend/routers/__pycache__/users.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7234d282c659e6fc8bdb704131adf9126d14cca0 GIT binary patch literal 2889 zcmah~&u`qu73K`NB$vxwt=219mTg5zf1ry1au6gzlQ?dTSZxCsvLM+lK)4{NosmR^ zT&`z^sg#^QJ6QQX5NZgM(1TSc{^(7v(aqci8@B-XQ%RR)XnFj zxqLpFCl;ry&Vn7n8izDG%|afsChu%Jti@*TIMD(>y?2JSxz9UiEH-;cxy^$y0Xff` zdy6p6!ZH1g+Jxb^Y@C2OCAHxX|7eKI8)?b@OM}udlD(?(?Bai$RZQ zZ<9+|LFZ~J6{y@Du41+Y9-JEzjD3_FMaFfozMDw?Q&E&dm<>?>zTk<18N@e+46%p! zTB%@38(sBFA^1SWC8%{1JnY89M9NM#Nr4Mq>^5PN$~*FH+JlK1MGrcC>|=|Pfn?HJh@Vx;0743&29 zaJrQj1GSs&$1=$hv9AM`r0~@*q|)_N#z|kLA3-GhCZa4(R4)`wIDnW(aT>(}2F0YGXkN4Hht)%}U zq{dCyDP)SgX`;U}+}906u_|Jiz-`DD2ueb-06!mo?!Uw<=#^Z6q-hB*hs6NSoEMB| znzAh&9%sH!VeRCx0XQ%WNk0HEvhG`o9FhA}kv;pqb7TRo9a#qyo6H?+M6Xz5Vgi4H zZFGu`Hetvvh#heCOZR{P>rk)1p||K}b$AE`?iSLBuDoWVVq#K>mtjT+7c(e;evuC^ zVE`ri?phx&q1Gg?T)CJP{Uno@08EzlTwS|Wa0{s~4^HEMnjqc=Fe1l;2-#|XNFj_6 zOwm=4^Nce4kbX)OR)eivklq-vuS|ZAiE6-CXmm~H9J&WK_-u{rBSO&o%#mf@?MkC& zN6<%sl-gQ)7wYanK!*%{X^;|fy0=bD_>%}Fze zu{D?!uQGd@)V+@A!SSj9yW437iCWp#wo@UMz(|N05Za$OtNh@VRXM98ntB9A7}G9r zou^8B&{iOQcdG<(#06aYj4TJqdY-FBRi-8{mSMfb!aL`Ngc1j80|!d?>%jL(09d>W z3!uY&V#+FI2ip4<^>O=?rv?DO4XM8eFrp_(olttzM%rdMq3BeFxvMJ!MX;#L!3Lp#BmGrm299H~`*Fe{6KjTom0g9@ty@|-shpnxm%8+KLGNhTdW;)sramm{# zOeNREMZ_oB67QgHc`8h0H%fCe8M5AjDT(uoo&(JcsR_(`f{{2w*dLcvYBc^zh- zFSsc;_Q*cT4HTOx74aUJ^}PQR3v)cDI^Hx7|Le(oykIepu7O{P-C)thV?n3mv!Wlz+S^W`42P+}#wM_nn8@pvyp<(cf0yU`D}~4frl5A!{xBWv2p2zmxYv^{ zxX^@W=1hy1rXGdi2r1K^L?4M1c>M~zzs>cfoupK|0&fpfqoR$A-RizEOlliWp=;NE zv9`LQgUuW3s~bQ6VDs9BcCW5pzOuSe^*?6_H>If}Z4XPO?R207a%4_=jPhmF8Q%Si zu+uW*mmo7O$v`+(7ovl|uKBfS$8HBM?GooHCG_8vhEGG^wL{`OslMq`YKKq4IoAQ* VH_vqR33bA6!UflMPpoY}`45kuwBi5& literal 0 HcmV?d00001 diff --git a/backend/routers/auth.py b/backend/routers/auth.py new file mode 100644 index 0000000..8ee2375 --- /dev/null +++ b/backend/routers/auth.py @@ -0,0 +1,35 @@ +"""认证路由""" +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from database import get_db +from models import User +from schemas import LoginRequest, Token, UserOut +from auth import verify_password, create_access_token, get_current_user + +router = APIRouter(prefix="/api/auth", tags=["认证"]) + + +@router.post("/login", response_model=Token) +def login(req: LoginRequest, db: Session = Depends(get_db)): + user = db.query(User).filter(User.username == req.username).first() + if not user or not verify_password(req.password, user.password_hash): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误") + if not user.is_active: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已停用") + token = create_access_token(data={"sub": user.id}) + return {"access_token": token, "token_type": "bearer"} + + +@router.get("/me", response_model=UserOut) +def get_me(current_user: User = Depends(get_current_user)): + return UserOut( + id=current_user.id, + username=current_user.username, + name=current_user.name, + phase_group=current_user.phase_group.value if hasattr(current_user.phase_group, 'value') else current_user.phase_group, + role=current_user.role.value if hasattr(current_user.role, 'value') else current_user.role, + monthly_salary=current_user.monthly_salary, + daily_cost=current_user.daily_cost, + is_active=current_user.is_active, + created_at=current_user.created_at, + ) diff --git a/backend/routers/costs.py b/backend/routers/costs.py new file mode 100644 index 0000000..86e2326 --- /dev/null +++ b/backend/routers/costs.py @@ -0,0 +1,205 @@ +"""成本管理路由""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional +from datetime import date +from database import get_db +from models import ( + User, UserRole, AIToolCost, AIToolCostAllocation, OutsourceCost, + CostOverride, SubscriptionPeriod, CostAllocationType, OutsourceType +) +from schemas import ( + AIToolCostCreate, AIToolCostOut, OutsourceCostCreate, OutsourceCostOut, + CostOverrideCreate +) +from auth import get_current_user, require_role + +router = APIRouter(prefix="/api/costs", tags=["成本管理"]) + + +# ──────────────────── AI 工具成本 ──────────────────── + +@router.get("/ai-tools", response_model=List[AIToolCostOut]) +def list_ai_tool_costs( + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + costs = db.query(AIToolCost).order_by(AIToolCost.record_date.desc()).all() + return [ + AIToolCostOut( + id=c.id, tool_name=c.tool_name, + subscription_period=c.subscription_period.value if hasattr(c.subscription_period, 'value') else c.subscription_period, + amount=c.amount, + allocation_type=c.allocation_type.value if hasattr(c.allocation_type, 'value') else c.allocation_type, + project_id=c.project_id, + recorded_by=c.recorded_by, + record_date=c.record_date, + created_at=c.created_at, + ) + for c in costs + ] + + +@router.post("/ai-tools", response_model=AIToolCostOut) +def create_ai_tool_cost( + req: AIToolCostCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + cost = AIToolCost( + tool_name=req.tool_name, + subscription_period=SubscriptionPeriod(req.subscription_period), + amount=req.amount, + allocation_type=CostAllocationType(req.allocation_type), + project_id=req.project_id, + recorded_by=current_user.id, + record_date=req.record_date, + ) + db.add(cost) + db.flush() + + # 处理手动分摊 + if req.allocation_type == "手动分摊" and req.allocations: + for alloc in req.allocations: + db.add(AIToolCostAllocation( + ai_tool_cost_id=cost.id, + project_id=alloc["project_id"], + percentage=alloc["percentage"], + )) + db.commit() + db.refresh(cost) + return AIToolCostOut( + id=cost.id, tool_name=cost.tool_name, + subscription_period=cost.subscription_period.value, + amount=cost.amount, + allocation_type=cost.allocation_type.value, + project_id=cost.project_id, + recorded_by=cost.recorded_by, + record_date=cost.record_date, + created_at=cost.created_at, + ) + + +@router.delete("/ai-tools/{cost_id}") +def delete_ai_tool_cost( + cost_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + cost = db.query(AIToolCost).filter(AIToolCost.id == cost_id).first() + if not cost: + raise HTTPException(status_code=404, detail="记录不存在") + db.query(AIToolCostAllocation).filter(AIToolCostAllocation.ai_tool_cost_id == cost_id).delete() + db.delete(cost) + db.commit() + return {"message": "已删除"} + + +# ──────────────────── 外包成本 ──────────────────── + +@router.get("/outsource", response_model=List[OutsourceCostOut]) +def list_outsource_costs( + project_id: Optional[int] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + q = db.query(OutsourceCost) + if project_id: + q = q.filter(OutsourceCost.project_id == project_id) + costs = q.order_by(OutsourceCost.record_date.desc()).all() + return [ + OutsourceCostOut( + id=c.id, project_id=c.project_id, + outsource_type=c.outsource_type.value if hasattr(c.outsource_type, 'value') else c.outsource_type, + episode_start=c.episode_start, episode_end=c.episode_end, + amount=c.amount, recorded_by=c.recorded_by, + record_date=c.record_date, created_at=c.created_at, + ) + for c in costs + ] + + +@router.post("/outsource", response_model=OutsourceCostOut) +def create_outsource_cost( + req: OutsourceCostCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + cost = OutsourceCost( + project_id=req.project_id, + outsource_type=OutsourceType(req.outsource_type), + episode_start=req.episode_start, + episode_end=req.episode_end, + amount=req.amount, + recorded_by=current_user.id, + record_date=req.record_date, + ) + db.add(cost) + db.commit() + db.refresh(cost) + return OutsourceCostOut( + id=cost.id, project_id=cost.project_id, + outsource_type=cost.outsource_type.value, + episode_start=cost.episode_start, episode_end=cost.episode_end, + amount=cost.amount, recorded_by=cost.recorded_by, + record_date=cost.record_date, created_at=cost.created_at, + ) + + +@router.delete("/outsource/{cost_id}") +def delete_outsource_cost( + cost_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + cost = db.query(OutsourceCost).filter(OutsourceCost.id == cost_id).first() + if not cost: + raise HTTPException(status_code=404, detail="记录不存在") + db.delete(cost) + db.commit() + return {"message": "已删除"} + + +# ──────────────────── 人力成本手动调整 ──────────────────── + +@router.post("/overrides") +def create_cost_override( + req: CostOverrideCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR)) +): + override = CostOverride( + user_id=req.user_id, + date=req.date, + project_id=req.project_id, + override_amount=req.override_amount, + adjusted_by=current_user.id, + reason=req.reason, + ) + db.add(override) + db.commit() + return {"message": "已保存成本调整"} + + +@router.get("/overrides") +def list_cost_overrides( + user_id: Optional[int] = Query(None), + project_id: Optional[int] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR)) +): + q = db.query(CostOverride) + if user_id: + q = q.filter(CostOverride.user_id == user_id) + if project_id: + q = q.filter(CostOverride.project_id == project_id) + records = q.order_by(CostOverride.date.desc()).all() + return [ + { + "id": r.id, "user_id": r.user_id, "date": r.date, + "project_id": r.project_id, "override_amount": r.override_amount, + "adjusted_by": r.adjusted_by, "reason": r.reason, + "created_at": r.created_at, + } + for r in records + ] diff --git a/backend/routers/dashboard.py b/backend/routers/dashboard.py new file mode 100644 index 0000000..7e2595d --- /dev/null +++ b/backend/routers/dashboard.py @@ -0,0 +1,152 @@ +"""仪表盘 + 结算路由""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import func as sa_func +from datetime import date, timedelta +from database import get_db +from models import ( + User, UserRole, Project, Submission, AIToolCost, + ProjectStatus, ProjectType, WorkType +) +from auth import get_current_user, require_role +from calculations import ( + calc_project_settlement, calc_waste_for_project, + calc_labor_cost_for_project, calc_ai_tool_cost_for_project, + calc_outsource_cost_for_project, calc_team_efficiency +) + +router = APIRouter(prefix="/api", tags=["仪表盘与结算"]) + + +@router.get("/dashboard") +def get_dashboard( + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + """全局仪表盘数据""" + # 项目概览 + active = db.query(Project).filter(Project.status == ProjectStatus.IN_PROGRESS).all() + completed = db.query(Project).filter(Project.status == ProjectStatus.COMPLETED).all() + + # 当月日期范围 + today = date.today() + month_start = today.replace(day=1) + + # 本月人力成本(简化:统计本月所有有提交的人的日成本) + monthly_submitters = db.query(Submission.user_id, Submission.submit_date).filter( + Submission.submit_date >= month_start, + Submission.submit_date <= today, + ).distinct().all() + + monthly_labor = 0.0 + processed_user_dates = set() + for uid, d in monthly_submitters: + key = (uid, d) + if key not in processed_user_dates: + processed_user_dates.add(key) + user = db.query(User).filter(User.id == uid).first() + if user: + monthly_labor += user.daily_cost + + # 本月 AI 工具成本 + monthly_ai = db.query(sa_func.sum(AIToolCost.amount)).filter( + AIToolCost.record_date >= month_start, + AIToolCost.record_date <= today, + ).scalar() or 0 + + # 本月总产出秒数 + monthly_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.submit_date >= month_start, + Submission.submit_date <= today, + Submission.total_seconds > 0, + ).scalar() or 0 + + # 活跃人数 + active_users = db.query(Submission.user_id).filter( + Submission.submit_date >= month_start, + ).distinct().count() + working_days = max(1, (today - month_start).days + 1) + avg_daily = round(monthly_secs / max(1, active_users) / working_days, 1) + + # 各项目摘要 + project_summaries = [] + for p in active: + waste = calc_waste_for_project(p.id, db) + total_secs = waste.get("total_submitted_seconds", 0) + target = p.target_total_seconds + progress = round(total_secs / target * 100, 1) if target > 0 else 0 + is_overdue = ( + p.estimated_completion_date and today > p.estimated_completion_date + ) + project_summaries.append({ + "id": p.id, + "name": p.name, + "project_type": p.project_type.value if hasattr(p.project_type, 'value') else p.project_type, + "progress_percent": progress, + "target_seconds": target, + "submitted_seconds": total_secs, + "waste_rate": waste.get("waste_rate", 0), + "is_overdue": bool(is_overdue), + "estimated_completion_date": str(p.estimated_completion_date) if p.estimated_completion_date else None, + }) + + # 损耗排行 + waste_ranking = [] + for p in active + completed: + w = calc_waste_for_project(p.id, db) + if w.get("total_waste_seconds", 0) > 0: + waste_ranking.append({ + "project_id": p.id, + "project_name": p.name, + "waste_seconds": w["total_waste_seconds"], + "waste_rate": w["waste_rate"], + }) + waste_ranking.sort(key=lambda x: x["waste_rate"], reverse=True) + + # 已结算项目 + settled = [] + for p in completed: + settlement = calc_project_settlement(p.id, db) + settled.append({ + "project_id": p.id, + "project_name": p.name, + "project_type": settlement.get("project_type", ""), + "total_cost": settlement.get("total_cost", 0), + "contract_amount": settlement.get("contract_amount"), + "profit_loss": settlement.get("profit_loss"), + }) + + return { + "active_projects": len(active), + "completed_projects": len(completed), + "monthly_labor_cost": round(monthly_labor, 2), + "monthly_ai_tool_cost": round(monthly_ai, 2), + "monthly_total_seconds": round(monthly_secs, 1), + "avg_daily_seconds_per_person": avg_daily, + "projects": project_summaries, + "waste_ranking": waste_ranking, + "settled_projects": settled, + } + + +@router.get("/projects/{project_id}/settlement") +def get_settlement( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + """项目结算报告""" + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="项目不存在") + return calc_project_settlement(project_id, db) + + +@router.get("/projects/{project_id}/efficiency") +def get_efficiency( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + """项目团队效率数据""" + return calc_team_efficiency(project_id, db) diff --git a/backend/routers/projects.py b/backend/routers/projects.py new file mode 100644 index 0000000..92c7a6a --- /dev/null +++ b/backend/routers/projects.py @@ -0,0 +1,158 @@ +"""项目管理路由""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from sqlalchemy import func as sa_func +from typing import List, Optional +from database import get_db +from models import ( + User, Project, Submission, UserRole, ProjectType, + ProjectStatus, PhaseGroup, WorkType +) +from schemas import ProjectCreate, ProjectUpdate, ProjectOut +from auth import get_current_user, require_role + +router = APIRouter(prefix="/api/projects", tags=["项目管理"]) + + +def enrich_project(p: Project, db: Session) -> ProjectOut: + """将项目对象转为带计算字段的输出""" + # 累计提交秒数(仅有秒数的提交) + total_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == p.id, + Submission.total_seconds > 0 + ).scalar() or 0 + + target = p.target_total_seconds + progress = round(total_secs / target * 100, 1) if target > 0 else 0 + + # 损耗 = 测试损耗 + 超产损耗 + test_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == p.id, + Submission.work_type == WorkType.TEST + ).scalar() or 0 + overproduction = max(0, total_secs - target) + waste = test_secs + overproduction + waste_rate = round(waste / target * 100, 1) if target > 0 else 0 + + leader_name = p.leader.name if p.leader else None + + return ProjectOut( + id=p.id, name=p.name, + project_type=p.project_type.value if hasattr(p.project_type, 'value') else p.project_type, + status=p.status.value if hasattr(p.status, 'value') else p.status, + leader_id=p.leader_id, leader_name=leader_name, + current_phase=p.current_phase.value if hasattr(p.current_phase, 'value') else p.current_phase, + episode_duration_minutes=p.episode_duration_minutes, + episode_count=p.episode_count, + target_total_seconds=target, + estimated_completion_date=p.estimated_completion_date, + actual_completion_date=p.actual_completion_date, + contract_amount=p.contract_amount, + created_at=p.created_at, + total_submitted_seconds=round(total_secs, 1), + progress_percent=progress, + waste_seconds=round(waste, 1), + waste_rate=waste_rate, + ) + + +@router.get("/", response_model=List[ProjectOut]) +def list_projects( + status: Optional[str] = Query(None), + project_type: Optional[str] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + q = db.query(Project) + if status: + q = q.filter(Project.status == ProjectStatus(status)) + if project_type: + q = q.filter(Project.project_type == ProjectType(project_type)) + projects = q.order_by(Project.created_at.desc()).all() + return [enrich_project(p, db) for p in projects] + + +@router.post("/", response_model=ProjectOut) +def create_project( + req: ProjectCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR)) +): + project = Project( + name=req.name, + project_type=ProjectType(req.project_type), + leader_id=req.leader_id, + current_phase=PhaseGroup(req.current_phase), + episode_duration_minutes=req.episode_duration_minutes, + episode_count=req.episode_count, + estimated_completion_date=req.estimated_completion_date, + contract_amount=req.contract_amount, + ) + db.add(project) + db.commit() + db.refresh(project) + return enrich_project(project, db) + + +@router.get("/{project_id}", response_model=ProjectOut) +def get_project( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + p = db.query(Project).filter(Project.id == project_id).first() + if not p: + raise HTTPException(status_code=404, detail="项目不存在") + return enrich_project(p, db) + + +@router.put("/{project_id}", response_model=ProjectOut) +def update_project( + project_id: int, + req: ProjectUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR)) +): + p = db.query(Project).filter(Project.id == project_id).first() + if not p: + raise HTTPException(status_code=404, detail="项目不存在") + if req.name is not None: + p.name = req.name + if req.project_type is not None: + p.project_type = ProjectType(req.project_type) + if req.status is not None: + p.status = ProjectStatus(req.status) + if req.leader_id is not None: + p.leader_id = req.leader_id + if req.current_phase is not None: + p.current_phase = PhaseGroup(req.current_phase) + if req.episode_duration_minutes is not None: + p.episode_duration_minutes = req.episode_duration_minutes + if req.episode_count is not None: + p.episode_count = req.episode_count + if req.estimated_completion_date is not None: + p.estimated_completion_date = req.estimated_completion_date + if req.actual_completion_date is not None: + p.actual_completion_date = req.actual_completion_date + if req.contract_amount is not None: + p.contract_amount = req.contract_amount + db.commit() + db.refresh(p) + return enrich_project(p, db) + + +@router.post("/{project_id}/complete") +def complete_project( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + """Owner 手动确认项目完成""" + p = db.query(Project).filter(Project.id == project_id).first() + if not p: + raise HTTPException(status_code=404, detail="项目不存在") + from datetime import date + p.status = ProjectStatus.COMPLETED + p.actual_completion_date = date.today() + db.commit() + return {"message": "项目已标记为完成", "project_id": project_id} diff --git a/backend/routers/submissions.py b/backend/routers/submissions.py new file mode 100644 index 0000000..7d937da --- /dev/null +++ b/backend/routers/submissions.py @@ -0,0 +1,193 @@ +"""内容提交路由""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional +from datetime import date +from database import get_db +from models import ( + User, Submission, SubmissionHistory, Project, UserRole, + PhaseGroup, WorkType, ContentType, SubmitTo +) +from schemas import SubmissionCreate, SubmissionUpdate, SubmissionOut +from auth import get_current_user, require_role + +router = APIRouter(prefix="/api/submissions", tags=["内容提交"]) + + +def submission_to_out(s: Submission) -> SubmissionOut: + return SubmissionOut( + id=s.id, user_id=s.user_id, + user_name=s.user.name if s.user else None, + project_id=s.project_id, + project_name=s.project.name if s.project else None, + project_phase=s.project_phase.value if hasattr(s.project_phase, 'value') else s.project_phase, + work_type=s.work_type.value if hasattr(s.work_type, 'value') else s.work_type, + content_type=s.content_type.value if hasattr(s.content_type, 'value') else s.content_type, + duration_minutes=s.duration_minutes, + duration_seconds=s.duration_seconds, + total_seconds=s.total_seconds, + hours_spent=s.hours_spent, + submit_to=s.submit_to.value if hasattr(s.submit_to, 'value') else s.submit_to, + description=s.description, + submit_date=s.submit_date, + created_at=s.created_at, + ) + + +@router.get("/", response_model=List[SubmissionOut]) +def list_submissions( + project_id: Optional[int] = Query(None), + user_id: Optional[int] = Query(None), + start_date: Optional[date] = Query(None), + end_date: Optional[date] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + q = db.query(Submission) + # 成员只能看自己的 + if current_user.role == UserRole.MEMBER: + q = q.filter(Submission.user_id == current_user.id) + elif user_id: + q = q.filter(Submission.user_id == user_id) + if project_id: + q = q.filter(Submission.project_id == project_id) + if start_date: + q = q.filter(Submission.submit_date >= start_date) + if end_date: + q = q.filter(Submission.submit_date <= end_date) + subs = q.order_by(Submission.submit_date.desc(), Submission.created_at.desc()).all() + return [submission_to_out(s) for s in subs] + + +@router.post("/", response_model=SubmissionOut) +def create_submission( + req: SubmissionCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + # 校验项目存在 + project = db.query(Project).filter(Project.id == req.project_id).first() + if not project: + raise HTTPException(status_code=404, detail="项目不存在") + + # 自动计算总秒数 + total_seconds = (req.duration_minutes or 0) * 60 + (req.duration_seconds or 0) + + sub = Submission( + user_id=current_user.id, + project_id=req.project_id, + project_phase=PhaseGroup(req.project_phase), + work_type=WorkType(req.work_type), + content_type=ContentType(req.content_type), + duration_minutes=req.duration_minutes or 0, + duration_seconds=req.duration_seconds or 0, + total_seconds=total_seconds, + hours_spent=req.hours_spent, + submit_to=SubmitTo(req.submit_to), + description=req.description, + submit_date=req.submit_date, + ) + db.add(sub) + db.commit() + db.refresh(sub) + return submission_to_out(sub) + + +@router.put("/{submission_id}", response_model=SubmissionOut) +def update_submission( + submission_id: int, + req: SubmissionUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + """高权限修改提交记录(需填写原因)""" + sub = db.query(Submission).filter(Submission.id == submission_id).first() + if not sub: + raise HTTPException(status_code=404, detail="提交记录不存在") + + # 保存旧数据用于历史记录 + old_data = { + "project_phase": sub.project_phase.value if hasattr(sub.project_phase, 'value') else sub.project_phase, + "work_type": sub.work_type.value if hasattr(sub.work_type, 'value') else sub.work_type, + "content_type": sub.content_type.value if hasattr(sub.content_type, 'value') else sub.content_type, + "duration_minutes": sub.duration_minutes, + "duration_seconds": sub.duration_seconds, + "total_seconds": sub.total_seconds, + "hours_spent": sub.hours_spent, + "submit_to": sub.submit_to.value if hasattr(sub.submit_to, 'value') else sub.submit_to, + "description": sub.description, + "submit_date": str(sub.submit_date), + } + + # 更新字段 + if req.project_phase is not None: + sub.project_phase = PhaseGroup(req.project_phase) + if req.work_type is not None: + sub.work_type = WorkType(req.work_type) + if req.content_type is not None: + sub.content_type = ContentType(req.content_type) + if req.duration_minutes is not None: + sub.duration_minutes = req.duration_minutes + if req.duration_seconds is not None: + sub.duration_seconds = req.duration_seconds + if req.hours_spent is not None: + sub.hours_spent = req.hours_spent + if req.submit_to is not None: + sub.submit_to = SubmitTo(req.submit_to) + if req.description is not None: + sub.description = req.description + if req.submit_date is not None: + sub.submit_date = req.submit_date + + # 重算总秒数 + sub.total_seconds = (sub.duration_minutes or 0) * 60 + (sub.duration_seconds or 0) + + # 保存新数据 + new_data = { + "project_phase": sub.project_phase.value if hasattr(sub.project_phase, 'value') else sub.project_phase, + "work_type": sub.work_type.value if hasattr(sub.work_type, 'value') else sub.work_type, + "content_type": sub.content_type.value if hasattr(sub.content_type, 'value') else sub.content_type, + "duration_minutes": sub.duration_minutes, + "duration_seconds": sub.duration_seconds, + "total_seconds": sub.total_seconds, + "hours_spent": sub.hours_spent, + "submit_to": sub.submit_to.value if hasattr(sub.submit_to, 'value') else sub.submit_to, + "description": sub.description, + "submit_date": str(sub.submit_date), + } + + # 写入修改历史 + history = SubmissionHistory( + submission_id=sub.id, + changed_by=current_user.id, + change_reason=req.change_reason, + old_data=old_data, + new_data=new_data, + ) + db.add(history) + db.commit() + db.refresh(sub) + return submission_to_out(sub) + + +@router.get("/{submission_id}/history") +def get_submission_history( + submission_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + """查看提交的修改历史""" + records = db.query(SubmissionHistory).filter( + SubmissionHistory.submission_id == submission_id + ).order_by(SubmissionHistory.created_at.desc()).all() + return [ + { + "id": r.id, + "changed_by": r.changed_by, + "change_reason": r.change_reason, + "old_data": r.old_data, + "new_data": r.new_data, + "created_at": r.created_at, + } + for r in records + ] diff --git a/backend/routers/users.py b/backend/routers/users.py new file mode 100644 index 0000000..5435975 --- /dev/null +++ b/backend/routers/users.py @@ -0,0 +1,88 @@ +"""用户管理路由""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from database import get_db +from models import User, UserRole, PhaseGroup +from schemas import UserCreate, UserUpdate, UserOut +from auth import get_current_user, hash_password, require_role + +router = APIRouter(prefix="/api/users", tags=["用户管理"]) + + +def user_to_out(u: User) -> UserOut: + return UserOut( + id=u.id, username=u.username, name=u.name, + phase_group=u.phase_group.value if hasattr(u.phase_group, 'value') else u.phase_group, + role=u.role.value if hasattr(u.role, 'value') else u.role, + monthly_salary=u.monthly_salary, daily_cost=u.daily_cost, + is_active=u.is_active, created_at=u.created_at, + ) + + +@router.get("/", response_model=List[UserOut]) +def list_users( + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + users = db.query(User).order_by(User.created_at.desc()).all() + return [user_to_out(u) for u in users] + + +@router.post("/", response_model=UserOut) +def create_user( + req: UserCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + if db.query(User).filter(User.username == req.username).first(): + raise HTTPException(status_code=400, detail="用户名已存在") + user = User( + username=req.username, + password_hash=hash_password(req.password), + name=req.name, + phase_group=PhaseGroup(req.phase_group), + role=UserRole(req.role), + monthly_salary=req.monthly_salary, + ) + db.add(user) + db.commit() + db.refresh(user) + return user_to_out(user) + + +@router.put("/{user_id}", response_model=UserOut) +def update_user( + user_id: int, + req: UserUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + if req.name is not None: + user.name = req.name + if req.phase_group is not None: + user.phase_group = PhaseGroup(req.phase_group) + if req.role is not None: + user.role = UserRole(req.role) + if req.monthly_salary is not None: + user.monthly_salary = req.monthly_salary + if req.is_active is not None: + user.is_active = req.is_active + db.commit() + db.refresh(user) + return user_to_out(user) + + +@router.get("/{user_id}", response_model=UserOut) +def get_user( + user_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + return user_to_out(user) diff --git a/backend/schemas.py b/backend/schemas.py new file mode 100644 index 0000000..9fef907 --- /dev/null +++ b/backend/schemas.py @@ -0,0 +1,249 @@ +"""Pydantic 数据模型 —— API 请求/响应格式定义""" +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import date, datetime + + +# ──────────────────────────── 认证 ──────────────────────────── + +class LoginRequest(BaseModel): + username: str + password: str + + +class Token(BaseModel): + access_token: str + token_type: str = "bearer" + + +# ──────────────────────────── 用户 ──────────────────────────── + +class UserCreate(BaseModel): + username: str + password: str + name: str + phase_group: str # 前期/制作/后期 + role: str = "成员" + monthly_salary: float = 0 + + +class UserUpdate(BaseModel): + name: Optional[str] = None + phase_group: Optional[str] = None + role: Optional[str] = None + monthly_salary: Optional[float] = None + is_active: Optional[int] = None + + +class UserOut(BaseModel): + id: int + username: str + name: str + phase_group: str + role: str + monthly_salary: float + daily_cost: float + is_active: int + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# ──────────────────────────── 项目 ──────────────────────────── + +class ProjectCreate(BaseModel): + name: str + project_type: str + leader_id: Optional[int] = None + current_phase: str = "前期" + episode_duration_minutes: float + episode_count: int + estimated_completion_date: Optional[date] = None + contract_amount: Optional[float] = None + + +class ProjectUpdate(BaseModel): + name: Optional[str] = None + project_type: Optional[str] = None + status: Optional[str] = None + leader_id: Optional[int] = None + current_phase: Optional[str] = None + episode_duration_minutes: Optional[float] = None + episode_count: Optional[int] = None + estimated_completion_date: Optional[date] = None + actual_completion_date: Optional[date] = None + contract_amount: Optional[float] = None + + +class ProjectOut(BaseModel): + id: int + name: str + project_type: str + status: str + leader_id: Optional[int] = None + leader_name: Optional[str] = None + current_phase: str + episode_duration_minutes: float + episode_count: int + target_total_seconds: int + estimated_completion_date: Optional[date] = None + actual_completion_date: Optional[date] = None + contract_amount: Optional[float] = None + created_at: Optional[datetime] = None + # 动态计算字段 + total_submitted_seconds: Optional[float] = 0 + progress_percent: Optional[float] = 0 + waste_seconds: Optional[float] = 0 + waste_rate: Optional[float] = 0 + + class Config: + from_attributes = True + + +# ──────────────────────────── 内容提交 ──────────────────────────── + +class SubmissionCreate(BaseModel): + project_id: int + project_phase: str + work_type: str + content_type: str + duration_minutes: Optional[float] = 0 + duration_seconds: Optional[float] = 0 + hours_spent: Optional[float] = None + submit_to: str + description: Optional[str] = None + submit_date: date + + +class SubmissionUpdate(BaseModel): + project_phase: Optional[str] = None + work_type: Optional[str] = None + content_type: Optional[str] = None + duration_minutes: Optional[float] = None + duration_seconds: Optional[float] = None + hours_spent: Optional[float] = None + submit_to: Optional[str] = None + description: Optional[str] = None + submit_date: Optional[date] = None + change_reason: str # 修改必须填原因 + + +class SubmissionOut(BaseModel): + id: int + user_id: int + user_name: Optional[str] = None + project_id: int + project_name: Optional[str] = None + project_phase: str + work_type: str + content_type: str + duration_minutes: Optional[float] = 0 + duration_seconds: Optional[float] = 0 + total_seconds: float + hours_spent: Optional[float] = None + submit_to: str + description: Optional[str] = None + submit_date: date + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# ──────────────────────────── AI 工具成本 ──────────────────────────── + +class AIToolCostCreate(BaseModel): + tool_name: str + subscription_period: str + amount: float + allocation_type: str + project_id: Optional[int] = None + record_date: date + allocations: Optional[List[dict]] = None # [{project_id, percentage}] + + +class AIToolCostOut(BaseModel): + id: int + tool_name: str + subscription_period: str + amount: float + allocation_type: str + project_id: Optional[int] = None + recorded_by: int + record_date: date + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# ──────────────────────────── 外包成本 ──────────────────────────── + +class OutsourceCostCreate(BaseModel): + project_id: int + outsource_type: str + episode_start: Optional[int] = None + episode_end: Optional[int] = None + amount: float + record_date: date + + +class OutsourceCostOut(BaseModel): + id: int + project_id: int + outsource_type: str + episode_start: Optional[int] = None + episode_end: Optional[int] = None + amount: float + recorded_by: int + record_date: date + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# ──────────────────────────── 成本调整 ──────────────────────────── + +class CostOverrideCreate(BaseModel): + user_id: int + date: date + project_id: int + override_amount: float + reason: Optional[str] = None + + +# ──────────────────────────── 仪表盘 ──────────────────────────── + +class DashboardSummary(BaseModel): + """全局仪表盘数据""" + active_projects: int = 0 + completed_projects: int = 0 + monthly_labor_cost: float = 0 + monthly_ai_tool_cost: float = 0 + monthly_total_seconds: float = 0 + avg_daily_seconds_per_person: float = 0 + projects: List[dict] = [] + waste_ranking: List[dict] = [] + settled_projects: List[dict] = [] + + +# ──────────────────────────── 项目结算 ──────────────────────────── + +class SettlementOut(BaseModel): + """项目结算报告""" + project_id: int + project_name: str + project_type: str + labor_cost: float = 0 + ai_tool_cost: float = 0 + outsource_cost: float = 0 + total_cost: float = 0 + waste_seconds: float = 0 + waste_rate: float = 0 + test_waste_seconds: float = 0 + overproduction_waste_seconds: float = 0 + contract_amount: Optional[float] = None + profit_loss: Optional[float] = None + team_efficiency: List[dict] = [] diff --git a/backend/seed.py b/backend/seed.py new file mode 100644 index 0000000..cd722ae --- /dev/null +++ b/backend/seed.py @@ -0,0 +1,195 @@ +"""种子数据 —— 用于测试和演示""" +from datetime import date, timedelta +from database import SessionLocal, engine, Base +from models import * +from auth import hash_password + +Base.metadata.create_all(bind=engine) +db = SessionLocal() + + +def seed(): + # 清空数据 + for table in reversed(Base.metadata.sorted_tables): + db.execute(table.delete()) + db.commit() + + # ── 用户 ── + users = [ + User(username="admin", password_hash=hash_password("admin123"), + name="老板", phase_group=PhaseGroup.PRODUCTION, role=UserRole.OWNER, monthly_salary=0), + User(username="zhangsan", password_hash=hash_password("123456"), + name="张三", phase_group=PhaseGroup.PRE, role=UserRole.LEADER, monthly_salary=15000), + User(username="lisi", password_hash=hash_password("123456"), + name="李四", phase_group=PhaseGroup.PRODUCTION, role=UserRole.LEADER, monthly_salary=18000), + User(username="wangwu", password_hash=hash_password("123456"), + name="王五", phase_group=PhaseGroup.PRODUCTION, role=UserRole.MEMBER, monthly_salary=12000), + User(username="zhaoliu", password_hash=hash_password("123456"), + name="赵六", phase_group=PhaseGroup.PRODUCTION, role=UserRole.MEMBER, monthly_salary=12000), + User(username="sunqi", password_hash=hash_password("123456"), + name="孙七", phase_group=PhaseGroup.POST, role=UserRole.MEMBER, monthly_salary=13000), + User(username="producer", password_hash=hash_password("123456"), + name="陈制片", phase_group=PhaseGroup.PRODUCTION, role=UserRole.SUPERVISOR, monthly_salary=20000), + ] + db.add_all(users) + db.flush() + + admin, zhangsan, lisi, wangwu, zhaoliu, sunqi, producer = users + + # ── 项目 ── + proj_a = Project( + name="星际漫游 第一季", project_type=ProjectType.CLIENT_FORMAL, + leader_id=lisi.id, current_phase=PhaseGroup.PRODUCTION, + episode_duration_minutes=5, episode_count=13, + estimated_completion_date=date.today() + timedelta(days=60), + contract_amount=100000, + ) + proj_b = Project( + name="品牌方 TVC", project_type=ProjectType.CLIENT_FORMAL, + leader_id=lisi.id, current_phase=PhaseGroup.PRODUCTION, + episode_duration_minutes=1, episode_count=3, + estimated_completion_date=date.today() + timedelta(days=20), + contract_amount=50000, + ) + proj_c = Project( + name="甲方风格测试", project_type=ProjectType.CLIENT_TEST, + leader_id=zhangsan.id, current_phase=PhaseGroup.PRE, + episode_duration_minutes=1, episode_count=1, + ) + proj_d = Project( + name="AI 短剧原创", project_type=ProjectType.INTERNAL_ORIGINAL, + leader_id=lisi.id, current_phase=PhaseGroup.PRE, + episode_duration_minutes=8, episode_count=6, + estimated_completion_date=date.today() + timedelta(days=90), + ) + db.add_all([proj_a, proj_b, proj_c, proj_d]) + db.flush() + + # ── 内容提交(模拟近两周的数据) ── + base_date = date.today() - timedelta(days=14) + submissions = [] + + # 张三(前期组)给项目 A 和 D 做前期 + for i in range(5): + d = base_date + timedelta(days=i) + submissions.append(Submission( + user_id=zhangsan.id, project_id=proj_a.id, + project_phase=PhaseGroup.PRE, work_type=WorkType.PLAN, + content_type=ContentType.DESIGN, total_seconds=0, + submit_to=SubmitTo.INTERNAL, description=f"角色设定第{i+1}版", + submit_date=d, + )) + for i in range(3): + d = base_date + timedelta(days=i + 5) + submissions.append(Submission( + user_id=zhangsan.id, project_id=proj_d.id, + project_phase=PhaseGroup.PRE, work_type=WorkType.PLAN, + content_type=ContentType.DESIGN, total_seconds=0, + submit_to=SubmitTo.INTERNAL, description=f"剧本大纲第{i+1}稿", + submit_date=d, + )) + + # 李四(制作组组长)主要做项目 A + for i in range(10): + d = base_date + timedelta(days=i) + secs = 45 + (i % 3) * 15 # 45-75秒 + wt = WorkType.TEST if i < 2 else WorkType.PRODUCTION + submissions.append(Submission( + user_id=lisi.id, project_id=proj_a.id, + project_phase=PhaseGroup.PRODUCTION, work_type=wt, + content_type=ContentType.ANIMATION, total_seconds=secs, + duration_minutes=secs // 60, duration_seconds=secs % 60, + submit_to=SubmitTo.INTERNAL, description=f"第1集片段 - 场景{i+1}", + submit_date=d, + )) + + # 王五(制作组)做项目 A 和 B + for i in range(8): + d = base_date + timedelta(days=i) + secs = 30 + (i % 4) * 20 # 30-90秒 + submissions.append(Submission( + user_id=wangwu.id, project_id=proj_a.id, + project_phase=PhaseGroup.PRODUCTION, work_type=WorkType.PRODUCTION, + content_type=ContentType.ANIMATION, total_seconds=secs, + duration_minutes=secs // 60, duration_seconds=secs % 60, + submit_to=SubmitTo.LEADER, description=f"第2集动画片段{i+1}", + submit_date=d, + )) + for i in range(4): + d = base_date + timedelta(days=i + 8) + secs = 20 + i * 10 + submissions.append(Submission( + user_id=wangwu.id, project_id=proj_b.id, + project_phase=PhaseGroup.PRODUCTION, work_type=WorkType.PRODUCTION, + content_type=ContentType.ANIMATION, total_seconds=secs, + duration_minutes=secs // 60, duration_seconds=secs % 60, + submit_to=SubmitTo.LEADER, description=f"TVC 片段{i+1}", + submit_date=d, + )) + + # 赵六(制作组)做项目 A + for i in range(10): + d = base_date + timedelta(days=i) + secs = 50 + (i % 2) * 30 # 50-80秒 + wt = WorkType.TEST if i < 1 else WorkType.PRODUCTION + submissions.append(Submission( + user_id=zhaoliu.id, project_id=proj_a.id, + project_phase=PhaseGroup.PRODUCTION, work_type=wt, + content_type=ContentType.ANIMATION, total_seconds=secs, + duration_minutes=secs // 60, duration_seconds=secs % 60, + submit_to=SubmitTo.LEADER, description=f"第3集场景动画{i+1}", + submit_date=d, + )) + + # 孙七(后期组)剪辑 + for i in range(3): + d = base_date + timedelta(days=i + 10) + submissions.append(Submission( + user_id=sunqi.id, project_id=proj_a.id, + project_phase=PhaseGroup.POST, work_type=WorkType.PRODUCTION, + content_type=ContentType.EDITING, total_seconds=0, + submit_to=SubmitTo.PRODUCER, description=f"第{i+1}集粗剪完成", + submit_date=d, + )) + # 后期补拍 + submissions.append(Submission( + user_id=sunqi.id, project_id=proj_a.id, + project_phase=PhaseGroup.POST, work_type=WorkType.PRODUCTION, + content_type=ContentType.ANIMATION, total_seconds=15, + duration_seconds=15, + submit_to=SubmitTo.PRODUCER, description="第1集补拍修改镜头", + submit_date=base_date + timedelta(days=12), + )) + + db.add_all(submissions) + + # ── AI 工具成本 ── + db.add(AIToolCost( + tool_name="Midjourney", subscription_period=SubscriptionPeriod.MONTHLY, + amount=200, allocation_type=CostAllocationType.TEAM, + recorded_by=producer.id, record_date=date.today().replace(day=1), + )) + db.add(AIToolCost( + tool_name="Runway", subscription_period=SubscriptionPeriod.MONTHLY, + amount=600, allocation_type=CostAllocationType.PROJECT, + project_id=proj_a.id, + recorded_by=producer.id, record_date=date.today().replace(day=1), + )) + + # ── 外包成本 ── + db.add(OutsourceCost( + project_id=proj_a.id, outsource_type=OutsourceType.ANIMATION, + episode_start=10, episode_end=13, amount=20000, + recorded_by=producer.id, record_date=date.today() - timedelta(days=5), + )) + + db.commit() + print("[OK] seed data generated") + print(f" - users: {len(users)}") + print(f" - projects: 4") + print(f" - submissions: {len(submissions)}") + print(f" - default account: admin / admin123") + + +if __name__ == "__main__": + seed() diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..1511959 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..97d02f3 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2041 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.13.5", + "echarts": "^6.0.0", + "element-plus": "^2.13.2", + "pinia": "^3.0.4", + "vue": "^3.5.25", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.2", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "vue": "3.5.28" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/element-plus": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.2.tgz", + "integrity": "sha512-Zjzm1NnFXGhV4LYZ6Ze9skPlYi2B4KAmN18FL63A3PZcjhDfroHwhtM6RE8BonlOPHXUnPQynH0BgaoEfvhrGw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "^10.11.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..bb931c6 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.13.5", + "echarts": "^6.0.0", + "element-plus": "^2.13.2", + "pinia": "^3.0.4", + "vue": "^3.5.25", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.2", + "vite": "^7.3.1" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..6d0ab4d --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,8 @@ + + + diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js new file mode 100644 index 0000000..3ab61a7 --- /dev/null +++ b/frontend/src/api/index.js @@ -0,0 +1,85 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' +import router from '../router' + +const api = axios.create({ + baseURL: '/api', + timeout: 30000, +}) + +// 请求拦截:自动带 token +api.interceptors.request.use(config => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +// 响应拦截:统一错误处理 +api.interceptors.response.use( + res => res.data, + err => { + const msg = err.response?.data?.detail || '请求失败' + if (err.response?.status === 401) { + localStorage.removeItem('token') + router.push('/login') + ElMessage.error('登录已过期,请重新登录') + } else { + ElMessage.error(msg) + } + return Promise.reject(err) + } +) + +export default api + +// ── 认证 ── +export const authApi = { + login: (data) => api.post('/auth/login', data), + me: () => api.get('/auth/me'), +} + +// ── 用户 ── +export const userApi = { + list: () => api.get('/users/'), + create: (data) => api.post('/users/', data), + update: (id, data) => api.put(`/users/${id}`, data), + get: (id) => api.get(`/users/${id}`), +} + +// ── 项目 ── +export const projectApi = { + list: (params) => api.get('/projects/', { params }), + create: (data) => api.post('/projects/', data), + update: (id, data) => api.put(`/projects/${id}`, data), + get: (id) => api.get(`/projects/${id}`), + complete: (id) => api.post(`/projects/${id}/complete`), + settlement: (id) => api.get(`/projects/${id}/settlement`), + efficiency: (id) => api.get(`/projects/${id}/efficiency`), +} + +// ── 内容提交 ── +export const submissionApi = { + list: (params) => api.get('/submissions/', { params }), + create: (data) => api.post('/submissions/', data), + update: (id, data) => api.put(`/submissions/${id}`, data), + history: (id) => api.get(`/submissions/${id}/history`), +} + +// ── 成本 ── +export const costApi = { + listAITools: () => api.get('/costs/ai-tools'), + createAITool: (data) => api.post('/costs/ai-tools', data), + deleteAITool: (id) => api.delete(`/costs/ai-tools/${id}`), + listOutsource: (params) => api.get('/costs/outsource', { params }), + createOutsource: (data) => api.post('/costs/outsource', data), + deleteOutsource: (id) => api.delete(`/costs/outsource/${id}`), + createOverride: (data) => api.post('/costs/overrides', data), + listOverrides: (params) => api.get('/costs/overrides', { params }), +} + +// ── 仪表盘 ── +export const dashboardApi = { + get: () => api.get('/dashboard'), +} diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..546ebbc --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue new file mode 100644 index 0000000..8f7100d --- /dev/null +++ b/frontend/src/components/Layout.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..2de1c69 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,20 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +// 注册所有图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(createPinia()) +app.use(ElementPlus, { locale: zhCn }) +app.use(router) +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..bdc8b60 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,38 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const routes = [ + { path: '/login', name: 'Login', component: () => import('../views/Login.vue'), meta: { public: true } }, + { + path: '/', + component: () => import('../components/Layout.vue'), + redirect: '/dashboard', + children: [ + { path: 'dashboard', name: 'Dashboard', component: () => import('../views/Dashboard.vue'), meta: { roles: ['Owner'] } }, + { path: 'projects', name: 'Projects', component: () => import('../views/Projects.vue') }, + { path: 'projects/:id', name: 'ProjectDetail', component: () => import('../views/ProjectDetail.vue') }, + { path: 'submissions', name: 'Submissions', component: () => import('../views/Submissions.vue') }, + { path: 'costs', name: 'Costs', component: () => import('../views/Costs.vue'), meta: { roles: ['Owner', '主管', '组长'] } }, + { path: 'users', name: 'Users', component: () => import('../views/Users.vue'), meta: { roles: ['Owner'] } }, + { path: 'settlement/:id', name: 'Settlement', component: () => import('../views/Settlement.vue'), meta: { roles: ['Owner'] } }, + ], + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +// 路由守卫 +router.beforeEach(async (to, from, next) => { + const token = localStorage.getItem('token') + if (to.meta.public) { + next() + } else if (!token) { + next('/login') + } else { + next() + } +}) + +export default router diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js new file mode 100644 index 0000000..e97d866 --- /dev/null +++ b/frontend/src/stores/auth.js @@ -0,0 +1,35 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { authApi } from '../api' + +export const useAuthStore = defineStore('auth', () => { + const user = ref(null) + const token = ref(localStorage.getItem('token') || '') + + async function login(username, password) { + const res = await authApi.login({ username, password }) + token.value = res.access_token + localStorage.setItem('token', res.access_token) + await fetchUser() + } + + async function fetchUser() { + try { + user.value = await authApi.me() + } catch { + logout() + } + } + + function logout() { + user.value = null + token.value = '' + localStorage.removeItem('token') + } + + const isOwner = () => user.value?.role === 'Owner' + const isSupervisor = () => ['Owner', '主管'].includes(user.value?.role) + const isLeaderOrAbove = () => ['Owner', '主管', '组长'].includes(user.value?.role) + + return { user, token, login, fetchUser, logout, isOwner, isSupervisor, isLeaderOrAbove } +}) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..f691315 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,79 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend/src/views/Costs.vue b/frontend/src/views/Costs.vue new file mode 100644 index 0000000..14c6bd8 --- /dev/null +++ b/frontend/src/views/Costs.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..8b1827d --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..fc94a0b --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/frontend/src/views/ProjectDetail.vue b/frontend/src/views/ProjectDetail.vue new file mode 100644 index 0000000..33ee2e3 --- /dev/null +++ b/frontend/src/views/ProjectDetail.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/frontend/src/views/Projects.vue b/frontend/src/views/Projects.vue new file mode 100644 index 0000000..185a6aa --- /dev/null +++ b/frontend/src/views/Projects.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/frontend/src/views/Settlement.vue b/frontend/src/views/Settlement.vue new file mode 100644 index 0000000..ff88d83 --- /dev/null +++ b/frontend/src/views/Settlement.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/frontend/src/views/Submissions.vue b/frontend/src/views/Submissions.vue new file mode 100644 index 0000000..e9fc64b --- /dev/null +++ b/frontend/src/views/Submissions.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/frontend/src/views/Users.vue b/frontend/src/views/Users.vue new file mode 100644 index 0000000..618701c --- /dev/null +++ b/frontend/src/views/Users.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..d62dc96 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + } + } + } +})