From 3a20f6e7edfd1f842f680d0788c4be46209474b0 Mon Sep 17 00:00:00 2001 From: gaoxq <376340421@qq.com> Date: Wed, 1 Apr 2026 22:39:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=91=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 + .idea/compiler.xml | 19 + .idea/encodings.xml | 6 + .idea/jarRepositories.xml | 20 + .idea/misc.xml | 12 + .idea/system.iml | 9 + .idea/vcs.xml | 6 + data/filedb.mv.db | Bin 0 -> 28672 bytes data/filedb.trace.db | 100 + pom.xml | 115 ++ .../com/filesystem/FileSystemApplication.java | 14 + .../filesystem/config/MyBatisPlusConfig.java | 35 + .../com/filesystem/config/RedisConfig.java | 24 + .../com/filesystem/config/SecurityConfig.java | 117 ++ .../java/com/filesystem/config/WebConfig.java | 30 + .../config/WebSocketAuthInterceptor.java | 44 + .../filesystem/config/WebSocketConfig.java | 26 + .../filesystem/controller/AuthController.java | 112 ++ .../filesystem/controller/FileController.java | 254 +++ .../controller/MessageController.java | 279 +++ .../filesystem/controller/UserController.java | 125 ++ .../com/filesystem/entity/BaseEntity.java | 21 + .../com/filesystem/entity/FileEntity.java | 34 + .../java/com/filesystem/entity/FileShare.java | 16 + .../java/com/filesystem/entity/Message.java | 30 + src/main/java/com/filesystem/entity/User.java | 33 + .../com/filesystem/mapper/FileMapper.java | 9 + .../filesystem/mapper/FileShareMapper.java | 9 + .../com/filesystem/mapper/MessageMapper.java | 9 + .../com/filesystem/mapper/UserMapper.java | 9 + .../security/JwtAuthenticationFilter.java | 82 + .../java/com/filesystem/security/JwtUtil.java | 130 ++ .../filesystem/security/UserPrincipal.java | 11 + .../com/filesystem/service/FileService.java | 502 +++++ .../filesystem/service/MessageService.java | 85 + .../com/filesystem/service/UserService.java | 150 ++ .../com/filesystem/websocket/ChatHandler.java | 140 ++ src/main/resources/application.yml | 46 + src/main/resources/db/init.sql | 78 + web-vue/index.html | 12 + web-vue/package-lock.json | 1760 +++++++++++++++++ web-vue/package.json | 23 + web-vue/src/App.vue | 16 + web-vue/src/api/auth.js | 7 + web-vue/src/api/file.js | 28 + web-vue/src/api/message.js | 20 + web-vue/src/api/request.js | 44 + web-vue/src/api/user.js | 3 + web-vue/src/components/ChatDialog.vue | 727 +++++++ web-vue/src/components/FileSidebar.vue | 90 + web-vue/src/components/FileTable.vue | 199 ++ web-vue/src/components/FileToolbar.vue | 138 ++ web-vue/src/components/FolderDialog.vue | 53 + web-vue/src/components/PreviewDialog.vue | 105 + web-vue/src/components/ProfileDialog.vue | 274 +++ web-vue/src/components/RenameDialog.vue | 104 + web-vue/src/components/ShareDialog.vue | 90 + web-vue/src/components/TopNavbar.vue | 195 ++ web-vue/src/components/UploadDialog.vue | 168 ++ web-vue/src/main.js | 21 + web-vue/src/router/index.js | 59 + web-vue/src/services/chat.js | 105 + web-vue/src/store/index.js | 1 + web-vue/src/store/user.js | 86 + web-vue/src/styles/global.css | 116 ++ web-vue/src/views/desktop/index.vue | 30 + web-vue/src/views/files/index.vue | 728 +++++++ web-vue/src/views/login/LoginBg.svg | 46 + web-vue/src/views/login/LoginForm.vue | 78 + web-vue/src/views/login/RegisterForm.vue | 115 ++ web-vue/src/views/login/index.vue | 254 +++ web-vue/src/views/not-found/index.vue | 73 + web-vue/src/views/preview/index.vue | 143 ++ web-vue/vite.config.js | 33 + 74 files changed, 8693 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/system.iml create mode 100644 .idea/vcs.xml create mode 100644 data/filedb.mv.db create mode 100644 data/filedb.trace.db create mode 100644 pom.xml create mode 100644 src/main/java/com/filesystem/FileSystemApplication.java create mode 100644 src/main/java/com/filesystem/config/MyBatisPlusConfig.java create mode 100644 src/main/java/com/filesystem/config/RedisConfig.java create mode 100644 src/main/java/com/filesystem/config/SecurityConfig.java create mode 100644 src/main/java/com/filesystem/config/WebConfig.java create mode 100644 src/main/java/com/filesystem/config/WebSocketAuthInterceptor.java create mode 100644 src/main/java/com/filesystem/config/WebSocketConfig.java create mode 100644 src/main/java/com/filesystem/controller/AuthController.java create mode 100644 src/main/java/com/filesystem/controller/FileController.java create mode 100644 src/main/java/com/filesystem/controller/MessageController.java create mode 100644 src/main/java/com/filesystem/controller/UserController.java create mode 100644 src/main/java/com/filesystem/entity/BaseEntity.java create mode 100644 src/main/java/com/filesystem/entity/FileEntity.java create mode 100644 src/main/java/com/filesystem/entity/FileShare.java create mode 100644 src/main/java/com/filesystem/entity/Message.java create mode 100644 src/main/java/com/filesystem/entity/User.java create mode 100644 src/main/java/com/filesystem/mapper/FileMapper.java create mode 100644 src/main/java/com/filesystem/mapper/FileShareMapper.java create mode 100644 src/main/java/com/filesystem/mapper/MessageMapper.java create mode 100644 src/main/java/com/filesystem/mapper/UserMapper.java create mode 100644 src/main/java/com/filesystem/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/filesystem/security/JwtUtil.java create mode 100644 src/main/java/com/filesystem/security/UserPrincipal.java create mode 100644 src/main/java/com/filesystem/service/FileService.java create mode 100644 src/main/java/com/filesystem/service/MessageService.java create mode 100644 src/main/java/com/filesystem/service/UserService.java create mode 100644 src/main/java/com/filesystem/websocket/ChatHandler.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/db/init.sql create mode 100644 web-vue/index.html create mode 100644 web-vue/package-lock.json create mode 100644 web-vue/package.json create mode 100644 web-vue/src/App.vue create mode 100644 web-vue/src/api/auth.js create mode 100644 web-vue/src/api/file.js create mode 100644 web-vue/src/api/message.js create mode 100644 web-vue/src/api/request.js create mode 100644 web-vue/src/api/user.js create mode 100644 web-vue/src/components/ChatDialog.vue create mode 100644 web-vue/src/components/FileSidebar.vue create mode 100644 web-vue/src/components/FileTable.vue create mode 100644 web-vue/src/components/FileToolbar.vue create mode 100644 web-vue/src/components/FolderDialog.vue create mode 100644 web-vue/src/components/PreviewDialog.vue create mode 100644 web-vue/src/components/ProfileDialog.vue create mode 100644 web-vue/src/components/RenameDialog.vue create mode 100644 web-vue/src/components/ShareDialog.vue create mode 100644 web-vue/src/components/TopNavbar.vue create mode 100644 web-vue/src/components/UploadDialog.vue create mode 100644 web-vue/src/main.js create mode 100644 web-vue/src/router/index.js create mode 100644 web-vue/src/services/chat.js create mode 100644 web-vue/src/store/index.js create mode 100644 web-vue/src/store/user.js create mode 100644 web-vue/src/styles/global.css create mode 100644 web-vue/src/views/desktop/index.vue create mode 100644 web-vue/src/views/files/index.vue create mode 100644 web-vue/src/views/login/LoginBg.svg create mode 100644 web-vue/src/views/login/LoginForm.vue create mode 100644 web-vue/src/views/login/RegisterForm.vue create mode 100644 web-vue/src/views/login/index.vue create mode 100644 web-vue/src/views/not-found/index.vue create mode 100644 web-vue/src/views/preview/index.vue create mode 100644 web-vue/vite.config.js diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..a891a69 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1e86ad3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/system.iml b/.idea/system.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/system.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/data/filedb.mv.db b/data/filedb.mv.db new file mode 100644 index 0000000000000000000000000000000000000000..bde8e8fcafc7d9a1ce228d6ef879585969c48c38 GIT binary patch literal 28672 zcmeHQ3y>Ved7eFKPkZmv6QLKQlXMTbSGn~$GJE54i=4i4`sL|_8&77rk39a`oJ~MlI3W~uGoC!Y1?&LEG zM~$8jhJr8nx#2POgE9DDUJ$XD5f$|Vo=U;6w81wmQD~cV?@y% zq%#~NswGodMlpgN6_z`|EfYD4!t9sENI5C3}YB7_=42P<^ z3O_fA?Eo$kGcD#23z;#~(YP#|7``}~V{th~HD>C>A~A|EVg@y0lo*z3V;ZB3Ad^uo zMmd6x7;4NS2oYvu_+oI!R7i|js;L^xh}pz6bp@jsr%qiEBjy;$ zwx|JLEJsyr-P$)bb1HtP8b5p2?A+80kK0y!WPW1eIGvd1DLO}U=coD4vrO+YJagyb z$Afh!-1b3vfc6hk258S8SXLOok57G|0=rVGenZ9fex4m!tG2EJA?u?7AuEBnoZ>m4 zKi+?Sjwfek==loOE2-%hvCLH*5x%zOJrrU1ZH07nhl%uPZ}S zlcy?bvgR#@6~-^?AXlO?A%~aeaAhXd)La>4{9{P!(23JJ;$^_fkR4Z6%C76c!9KCWlE1~3y`nQq_1!0}s`bH$O zL#T(mAg<8hz@y%o>VInfB~1wq!8P5#gZ)q9e;WR$>3=f+)2^SSXL&r|;vY^=+WcHx zQJVcT5S%wAG*wPOn$_tTuRl~cRsMK0s9$k(<6CC%R;pY>T*uM8JIsu7P zkdu&_A8IT3T_v9PIZEXH4kwm(SoABcVqI3P!t{3*P%kXdSp!SH5Pc;#I^Y)rp#VV_ zmZ~HVJwatvDlaTq$*ijNfh_$}m5jvatdiLU;L56S>B?|TA7JIWzhb>267?cKE9ZNG zL@H``@!ys6HXl_bGm5AYJ7Pv~#E57Sy-uNZ3XiY|sne7?O%;U5jcrfX2{EC;PqgD8 zg$-xH-s7k3dM~ApD;t6X4^GwKpa%yQ9PB3|l{8&1FV<&%x{k`}I_;e}eHS0|Q+ML0 z?i#aX|Kc#@@hXKAu+W4i2gy09r1Qv2=OJlS{s41sK!2u}R^byH?q<&Lac3a#8xxoK8UbfP0%xuY8} zu>hX*9znQa2ORr^bxjZXo^{>VAMlUiN60|PK*&JIK*&JIK*&JIK*&JIK*&JIK*+!^ z5CfIOzViJ2uIes&ytIqn#)NWNSJ&!E`cgfOmG+50p`eu~6bye7LG>mP{CR`w^MB?| z7#NVYmnRIo=l|TUogE;RDF*NPKRi&Dml|8;H+T}(c(5rYMB9Z(-MaPB`VFy$#-`?$ z*0y$Guh@A+sLvktxG(b%pMiHFg9TwfyxY-a@(?*?I={y4Bav~Cd9_b zacpP_Gh$$5W5!GsBcwaFLl~BI+9f7a!uPP{oik{*YCrNsUdVk$kDJO+T1SuvAJkK{7B z6Jj=f+p+Y>Ky6*w6WLsPIA6Jwmkm>q(`Yb1s3`q;)Stw8(;#YoFqKZC!GVDRBbn~- zsTnyoG}Iel2;ipj$y|q+%M7Qpx#aLzH|{Cl9vmG?rN{G2NXyqV*?e#ddN4XVlunM6 zZ;T{|p^<^3$?@bs4uo+$IesEDa&xz;>pjcc9!utquC#16^Gi@~z*f1b+=;PEHdQX- z=|c_pL4apYAj>PZ1(oiaj@!@ z>A%;%OWBJqFPTeYPd6ytudQ6^R@S*1EnB8^>IFL213H&pvCe^0euv7H1?els^`LTd z7gxEU*ossRTDUTmLmf9j7k;DMg<~TbaHpc@2eV=d#)-_xK<)ySJ$7q;9BIixGdYIR zsbQoI59e-^mC;n{7M%C>JAbL3E{Qg1V=yzG&Gqy`-~Tl7?mB-tFdbshq3(`qyFKr& zHxiiT>p#ORJqMD{f``9qBUv(;)HRYa^I(<(c{HfuyfL6@c~c%FgM-PGrsDo9HBKM( z(m1w!C>XsR2H?vK6=Uh~;Y>E086CNlb(e=6pBT?qd!neRu(2n8Wh2MRNVv2SEAj=kUC7+&p)VC+826 zGiOK=XYRgrNWWzOWryc8)INK966)U6`0ctDsJnVW=xY4Lx;=FfL3sa*Lft=a^N;I~ zZTFASZ=Gyx*xUjaqYG`>^?Mp2hOI=f0lNSC(f2#Q^YUIHdGqY-Y)ZbxnVPWqj3NlC zhyCB)_(A6nKiMkujh#K8ot`;8c`E)sI@4kgBu*Zig`DB!DLQrT{U_t^XA|@TCqFPX zb7pp$GJeuIJ;9~f^Rw_s`=mwL89r&BoS&YUqW0`b4l6osC?p}3s1kzpuafD=szhM% zCoB@RRap4ywGw^w`_JEc)T;^5=pLHKANz6VbN{kQNZv9F6;ZnuF7^sM^t~Tdv3g=5F^DK4!ZG%(!OZvAS!HcjoVY~Tnk?fm$HK>Cq$EU&9Ws# z6h$&ElS>ALwX}w25fz($Gts9D_eLt+X1`aR5C1GlaPW<%}^*Lf~wa_^s7&OzOE|8CXa;= zefZg~r=EFFSXel;uP8MQ^X997{V%Sn7SXeGgNX8aenrut5%{A4AoEOo6 ziHHCF+v}=KGjLzJ^=+lDh*XgFxrf_|4KXo)+>9%p174kfuuR>C`7Q+R)4~#BmL?gtjlf-)8VsVA z!YK1viGKN+JyoLHy)G@j^0lrvKHebo{qo|5jjnW!yLXG*dR?)-{_Vp%789MNx`T^b z_9EFeQSnfl8#UbP%u-w`Y`wOyyS-cOS-m)zs=|gIjwDK{Ck{p=1IAKN6Rr^W$5kg# z<$LaVqp6C0qlf+8m)`8U=cD_DzS~DE!K_-+twcd@q@gHo=6P+<#@CM!gjsA7ZJD+nmVpqa#i zxI>k7O?E7wvd{k2SMDxT))(F4;rol%@9nwo6~K3Og~~AOZF0a1K(y&ujIKhE$v^=W z9UHK*Nx?ioSR>TF=vEKmgI~Y5=h2q{VapAHR13}93a#50jMm`PZQfS8slC*Gz>V)I z#I}oKLD*d)2me)w6ti29G zwq3LR+8sN0?cSpy9TUS$96BsSw|Qi|_S^@1p8lJJ(Dy4k%)TQmR>P%$6>uX#loa&c z>vDY0!i`OCtiG_Pef1sFD03LrRPg40WCXo4;HiWJ+tVEID28cT*zeeVpZvRBm5xQP z@u2_S^B?TF=gR;+A9#wuiHM4PO3dCiEie*BWtEwoz(f#ry0^1t=a_qJ6rZuc-g0~lZaG+;b7#%Cr<2c=TC5I8u`S{Fcj*2G>l&S2nU zj-;D9sE318$#NVbQ3mliBsLs20QxLGa{qs{R1se5A$$`M`U=o8vS0{>UA>^frMS8% zw3UrRJp`i(*gt!FA%1dq zc9b^mTSzo5ZoLWWNhODqUO*1RaC2h8IOG~qp|!iz(hk2V9qcUKB-b{tEg07qb|u^x zhMyV`q%RmZ7Fw@|6H@GIfsh_D0`H{I*yzT3+=h*BA8uH^+~Ii*RIr-QltNql(<$t6>?|(SljoiUgVq{!#H928NN(rbJ~J zc4$Ig>0_@MmiwYRJ?!`Wby*|Uc+f+ZqeCtWrcOBwDmnxSmH`2!X4}|A7OW;F*78BG zFS^Tv^Z-Ee&FDm+6(xQBLZYh}-wUb<{s{E1cA2LbcPJ`bi}#vJ(5V*DByLZsq# z2TBL)i*;r}fE0AIw0Lc-h{R$$T7x+Q2kV%^R%8e%EJ(2cxnyw-NwUKtH|2B|rMr>ziW8xn@M&x2MfNV4GRGfV=R zu)rNwqjNnNfd2f2-YO2{;rQ?)RUCfGy1J0if%WXB<;dV-90&;=MUyD(k%nLzn*^p0 zxUB^S`>p%UDh}PlasMOoqpt>h_%XohaOj2xKx|8L7|i5&!yCxO(Il>0FiT;nuz@|0 z%XeRztKz^Ojt7?b@FV`!;b2^4Dpo8>)nrJK9n2-lHOTtlX}KwL4a646<;y{ju6$C>*(@Y83peYao!-uL89b_VdvH5y>{};QeI1CTRLrXY(q^rZh zi2_LrHo=T#1spn%V(6Gj8iP#@m_vS}2Wjt5o*t;;Fg+Z9w1k7K-fndUHUh6|kaR}C zZaEB!@NNJM@tY1a05|Lptt2BD#5A5nEj5q=jtivIEnb`*JQfX*e}v7_kSY$x*H72>TGx^Q<9 z{#m<=U>-?(yb2kYH$kb(-dVD=2}<=L`|mIDE_$1w{QuIVyqKx{hLWH0PLs>y#MKv^ z)b4^p7w&=bmrTLK6m-EJDF04nLsU@A!pz$P6`U}5vk6GPFW3XMP7vxo^99&}*(cP! z@}eMYxJRgaZ6G*$51ZiJf7k>^c-RCFs01Ol_!0jYeuNB!41^4X41^4X41^4X4E(=g YVEOv{rS< + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + com.filesystem + file-system-backend + 1.0.0 + file-system-backend + File Management System Backend + + + 17 + 0.12.3 + 3.5.5 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + + + com.mysql + mysql-connector-j + runtime + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/src/main/java/com/filesystem/FileSystemApplication.java b/src/main/java/com/filesystem/FileSystemApplication.java new file mode 100644 index 0000000..a1820b6 --- /dev/null +++ b/src/main/java/com/filesystem/FileSystemApplication.java @@ -0,0 +1,14 @@ +package com.filesystem; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("com.filesystem.mapper") +public class FileSystemApplication { + + public static void main(String[] args) { + SpringApplication.run(FileSystemApplication.class, args); + } +} diff --git a/src/main/java/com/filesystem/config/MyBatisPlusConfig.java b/src/main/java/com/filesystem/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..1d863dd --- /dev/null +++ b/src/main/java/com/filesystem/config/MyBatisPlusConfig.java @@ -0,0 +1,35 @@ +package com.filesystem.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; + +@Configuration +public class MyBatisPlusConfig implements MetaObjectHandler { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; + } + + @Override + public void insertFill(MetaObject metaObject) { + // MyBatis-Plus 使用属性名,不是数据库列名 + this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); + this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); + this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0); + } + + @Override + public void updateFill(MetaObject metaObject) { + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); + } +} diff --git a/src/main/java/com/filesystem/config/RedisConfig.java b/src/main/java/com/filesystem/config/RedisConfig.java new file mode 100644 index 0000000..287707f --- /dev/null +++ b/src/main/java/com/filesystem/config/RedisConfig.java @@ -0,0 +1,24 @@ +package com.filesystem.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.afterPropertiesSet(); + return template; + } +} diff --git a/src/main/java/com/filesystem/config/SecurityConfig.java b/src/main/java/com/filesystem/config/SecurityConfig.java new file mode 100644 index 0000000..8b2da7e --- /dev/null +++ b/src/main/java/com/filesystem/config/SecurityConfig.java @@ -0,0 +1,117 @@ +package com.filesystem.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.filesystem.security.JwtAuthenticationFilter; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Resource + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/login", "/api/auth/register", "/api/auth/logout").permitAll() + .requestMatchers("/api/files/test").permitAll() + .requestMatchers("/ws/**").permitAll() + .requestMatchers("/files/avatar/**").permitAll() + .requestMatchers("/api/files/avatar/**").permitAll() + .requestMatchers("/api/messages/file/**").permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(authenticationEntryPoint()) + .accessDeniedHandler(accessDeniedHandler()) + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + /** + * 未认证处理:返回 401 JSON,不跳转 + */ + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new AuthenticationEntryPoint() { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(response.getWriter(), + Map.of("code", 401, "message", "未登录或登录已过期")); + } + }; + } + + /** + * 无权限处理:返回 403 JSON + */ + @Bean + public AccessDeniedHandler accessDeniedHandler() { + return new AccessDeniedHandler() { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + org.springframework.security.access.AccessDeniedException accessDeniedException) throws IOException { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(response.getWriter(), + Map.of("code", 403, "message", "无权限访问")); + } + }; + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/src/main/java/com/filesystem/config/WebConfig.java b/src/main/java/com/filesystem/config/WebConfig.java new file mode 100644 index 0000000..58c1d0c --- /dev/null +++ b/src/main/java/com/filesystem/config/WebConfig.java @@ -0,0 +1,30 @@ +package com.filesystem.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.nio.file.Paths; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Value("${file.upload-dir:./uploads}") + private String uploadDir; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("*") + .allowedHeaders("*"); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/uploads/**") + .addResourceLocations("file:" + Paths.get(uploadDir).toAbsolutePath() + "/"); + } +} diff --git a/src/main/java/com/filesystem/config/WebSocketAuthInterceptor.java b/src/main/java/com/filesystem/config/WebSocketAuthInterceptor.java new file mode 100644 index 0000000..e9faba4 --- /dev/null +++ b/src/main/java/com/filesystem/config/WebSocketAuthInterceptor.java @@ -0,0 +1,44 @@ +package com.filesystem.config; + +import com.filesystem.security.JwtUtil; +import jakarta.annotation.Resource; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +@Component +public class WebSocketAuthInterceptor implements HandshakeInterceptor { + + @Resource + private JwtUtil jwtUtil; + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Map attributes) throws Exception { + if (request instanceof ServletServerHttpRequest servletRequest) { + String token = servletRequest.getServletRequest().getParameter("token"); + if (token != null) { + try { + Long userId = jwtUtil.getUserIdFromToken(token); + if (userId != null) { + attributes.put("userId", userId.toString()); + return true; + } + } catch (Exception e) { + // JWT 解析失败 + } + } + } + return false; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Exception exception) { + } +} \ No newline at end of file diff --git a/src/main/java/com/filesystem/config/WebSocketConfig.java b/src/main/java/com/filesystem/config/WebSocketConfig.java new file mode 100644 index 0000000..f86d9cf --- /dev/null +++ b/src/main/java/com/filesystem/config/WebSocketConfig.java @@ -0,0 +1,26 @@ +package com.filesystem.config; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import com.filesystem.websocket.ChatHandler; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Resource + private ChatHandler chatHandler; + + @Resource + private WebSocketAuthInterceptor authInterceptor; + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(chatHandler, "/ws/chat") + .addInterceptors(authInterceptor) + .setAllowedOrigins("*"); + } +} diff --git a/src/main/java/com/filesystem/controller/AuthController.java b/src/main/java/com/filesystem/controller/AuthController.java new file mode 100644 index 0000000..02ac8bc --- /dev/null +++ b/src/main/java/com/filesystem/controller/AuthController.java @@ -0,0 +1,112 @@ +package com.filesystem.controller; + +import com.filesystem.entity.User; +import com.filesystem.security.UserPrincipal; +import com.filesystem.service.UserService; +import jakarta.annotation.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @Resource + private UserService userService; + + @PostMapping("/login") + public ResponseEntity login(@RequestBody Map request) { + String username = request.get("username"); + String password = request.get("password"); + + try { + String token = userService.login(username, password); + User user = userService.findByUsername(username); + + // 精确重算存储空间 + long storageUsed = userService.recalculateStorage(user.getId()); + long storageLimit = user.getStorageLimit() != null ? user.getStorageLimit() : 20L * 1024 * 1024 * 1024; + + Map userData = new HashMap<>(); + userData.put("id", user.getId()); + userData.put("username", user.getUsername()); + userData.put("nickname", user.getNickname() != null ? user.getNickname() : ""); + userData.put("signature", user.getSignature() != null ? user.getSignature() : ""); + userData.put("avatar", user.getAvatar() != null ? user.getAvatar() : ""); + userData.put("phone", user.getPhone() != null ? user.getPhone() : ""); + userData.put("email", user.getEmail() != null ? user.getEmail() : ""); + userData.put("storageUsed", storageUsed); + userData.put("storageLimit", storageLimit); + + Map result = new HashMap<>(); + result.put("token", token); + result.put("user", userData); + + Map body = new HashMap<>(); + body.put("data", result); + body.put("message", "登录成功"); + + return ResponseEntity.ok(body); + } catch (Exception e) { + return ResponseEntity.status(401).body(Map.of("message", e.getMessage())); + } + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody Map request) { + String username = request.get("username"); + String password = request.get("password"); + String nickname = request.get("nickname"); + + if (userService.findByUsername(username) != null) { + return ResponseEntity.badRequest().body(Map.of("message", "用户名已存在")); + } + + User user = userService.createUser(username, password, nickname != null ? nickname : username); + return ResponseEntity.ok(Map.of("message", "注册成功", "data", Map.of("id", user.getId()))); + } + + @PostMapping("/logout") + public ResponseEntity logout(@AuthenticationPrincipal UserPrincipal principal) { + if (principal != null) { + userService.logout(principal.getUserId()); + } + return ResponseEntity.ok(Map.of("message", "退出成功")); + } + + @GetMapping("/info") + public ResponseEntity getUserInfo(@AuthenticationPrincipal UserPrincipal principal) { + if (principal == null) { + return ResponseEntity.status(401).body(Map.of("message", "未登录")); + } + + User user = userService.findById(principal.getUserId()); + if (user == null) { + return ResponseEntity.status(401).body(Map.of("message", "用户不存在")); + } + + // 精确重算存储空间 + long storageUsed = userService.recalculateStorage(user.getId()); + long storageLimit = user.getStorageLimit() != null ? user.getStorageLimit() : 20L * 1024 * 1024 * 1024; + + Map userData = new HashMap<>(); + userData.put("id", user.getId()); + userData.put("username", user.getUsername()); + userData.put("nickname", user.getNickname() != null ? user.getNickname() : ""); + userData.put("signature", user.getSignature() != null ? user.getSignature() : ""); + userData.put("avatar", user.getAvatar() != null ? user.getAvatar() : ""); + userData.put("email", user.getEmail() != null ? user.getEmail() : ""); + userData.put("phone", user.getPhone() != null ? user.getPhone() : ""); + userData.put("storageUsed", storageUsed); + userData.put("storageLimit", storageLimit); + + Map body = new HashMap<>(); + body.put("data", userData); + + return ResponseEntity.ok(body); + } +} diff --git a/src/main/java/com/filesystem/controller/FileController.java b/src/main/java/com/filesystem/controller/FileController.java new file mode 100644 index 0000000..1197a4e --- /dev/null +++ b/src/main/java/com/filesystem/controller/FileController.java @@ -0,0 +1,254 @@ +package com.filesystem.controller; + +import com.filesystem.entity.FileEntity; +import com.filesystem.entity.FileShare; +import com.filesystem.security.UserPrincipal; +import com.filesystem.service.FileService; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/files") +public class FileController { + + @Resource + private FileService fileService; + + @Value("${file.storage.path:./uploads}") + private String storagePath; + + public FileController() { + } + + @GetMapping("/test") + public ResponseEntity test() { + return ResponseEntity.ok(Map.of("message", "Backend is running", "timestamp", System.currentTimeMillis())); + } + + @GetMapping + public ResponseEntity getFiles( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam(required = false) Long folderId, + @RequestParam(required = false) String keyword) { + List files = fileService.getFiles(principal.getUserId(), folderId, keyword); + return ResponseEntity.ok(Map.of("data", files)); + } + + @GetMapping("/trashFiles") + public ResponseEntity getTrashFiles( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam(required = false) Long folderId) { + return ResponseEntity.ok(Map.of("data", fileService.getTrashFiles(principal.getUserId(), folderId))); + } + + @GetMapping("/sharedByMe") + public ResponseEntity getSharedByMe(@AuthenticationPrincipal UserPrincipal principal) { + return ResponseEntity.ok(Map.of("data", fileService.getSharedByMe(principal.getUserId()))); + } + + @GetMapping("/sharedByMe/folder") + public ResponseEntity getSharedByMeFolderFiles( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam Long folderId) { + List files = fileService.getSharedByMeFolderFiles(principal.getUserId(), folderId); + return ResponseEntity.ok(Map.of("data", files)); + } + + @GetMapping("/sharedToMe") + public ResponseEntity getSharedToMe(@AuthenticationPrincipal UserPrincipal principal) { + return ResponseEntity.ok(Map.of("data", fileService.getSharedToMe(principal.getUserId()))); + } + + @GetMapping("/sharedToMe/folder") + public ResponseEntity getSharedFolderFiles( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam Long folderId) { + List files = fileService.getSharedFolderFiles(principal.getUserId(), folderId); + return ResponseEntity.ok(Map.of("data", files)); + } + + @PostMapping("/uploadBatch") + public ResponseEntity uploadFiles( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam("files") List files, + @RequestParam(required = false) Long folderId) throws IOException { + List uploaded = fileService.uploadFiles(files, principal.getUserId(), folderId); + return ResponseEntity.ok(Map.of("data", uploaded, "message", "上传成功")); + } + + @PostMapping("/createFolder") + public ResponseEntity createFolder( + @AuthenticationPrincipal UserPrincipal principal, + @RequestBody Map request) { + String name = (String) request.get("name"); + Long parentId = request.get("parentId") != null ? Long.valueOf(request.get("parentId").toString()) : null; + FileEntity folder = fileService.createFolder(name, principal.getUserId(), parentId); + return ResponseEntity.ok(Map.of("data", folder, "message", "创建成功")); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteFile( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id) { + fileService.moveToTrash(id, principal.getUserId()); + return ResponseEntity.ok(Map.of("message", "已移至回收站")); + } + + @PostMapping("/{id}/restore") + public ResponseEntity restoreFile( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id) { + fileService.restoreFile(id, principal.getUserId()); + return ResponseEntity.ok(Map.of("message", "已还原")); + } + + @DeleteMapping("/{id}/deletePermanent") + public ResponseEntity deletePermanently( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id) { + fileService.deletePermanently(id, principal.getUserId()); + return ResponseEntity.ok(Map.of("message", "已彻底删除")); + } + + @DeleteMapping("/emptyTrash") + public ResponseEntity emptyTrash(@AuthenticationPrincipal UserPrincipal principal) { + fileService.emptyTrash(principal.getUserId()); + return ResponseEntity.ok(Map.of("message", "已清空回收站")); + } + + @GetMapping("/{id}/download") + public ResponseEntity downloadFile( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id) throws IOException { + FileEntity file = fileService.getById(id); + if (file == null) return ResponseEntity.notFound().build(); + byte[] content = fileService.getFileContent(file); + if (content == null) return ResponseEntity.notFound().build(); + String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20"); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedName + "\"") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(content); + } + + @GetMapping("/{id}/preview") + public ResponseEntity previewFile( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id) throws IOException { + FileEntity file = fileService.getById(id); + if (file == null) return ResponseEntity.notFound().build(); + byte[] content = fileService.getFileContent(file); + if (content == null) return ResponseEntity.notFound().build(); + String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20"); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodedName + "\"") + .contentType(getMediaType(file.getName())) + .body(content); + } + + @PostMapping("/{id}/shareFile") + public ResponseEntity shareFile( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id, + @RequestBody Map request) { + Long shareToUserId = Long.valueOf(request.get("userId").toString()); + String permission = (String) request.getOrDefault("permission", "view"); + FileShare share = fileService.shareFile(id, principal.getUserId(), shareToUserId, permission); + return ResponseEntity.ok(Map.of("data", share, "message", "共享成功")); + } + + @DeleteMapping("/{id}/cancelShare") + public ResponseEntity cancelShare( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id) { + fileService.cancelShare(id, principal.getUserId()); + return ResponseEntity.ok(Map.of("message", "已取消共享")); + } + + /** + * 获取头像图片 + */ + @GetMapping("/avatar/**") + public ResponseEntity getAvatar(jakarta.servlet.http.HttpServletRequest request) throws IOException { + String uri = request.getRequestURI(); + String prefix = "/api/files/avatar/"; + String relativePath = uri.substring(uri.indexOf(prefix) + prefix.length()); + // relativePath = "2026/04/xxx.jpg" + + Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("avatars").resolve(relativePath); + if (!Files.exists(filePath)) { + return ResponseEntity.notFound().build(); + } + byte[] content = Files.readAllBytes(filePath); + String fileName = relativePath.contains("/") ? relativePath.substring(relativePath.lastIndexOf("/") + 1) : relativePath; + String ext = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase() : "jpg"; + String contentType = switch (ext) { + case "png" -> "image/png"; + case "gif" -> "image/gif"; + case "webp" -> "image/webp"; + default -> "image/jpeg"; + }; + return ResponseEntity.ok() + .contentType(org.springframework.http.MediaType.parseMediaType(contentType)) + .header(HttpHeaders.CACHE_CONTROL, "max-age=86400") + .body(content); + } + + private MediaType getMediaType(String filename) { + String ext = filename.contains(".") ? filename.substring(filename.lastIndexOf(".")).toLowerCase() : ""; + return switch (ext) { + case ".jpg", ".jpeg" -> MediaType.IMAGE_JPEG; + case ".png" -> MediaType.IMAGE_PNG; + case ".gif" -> MediaType.IMAGE_GIF; + case ".pdf" -> MediaType.APPLICATION_PDF; + default -> MediaType.APPLICATION_OCTET_STREAM; + }; + } + + @PutMapping("/{id}/rename") + public ResponseEntity renameFile( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id, + @RequestBody Map request) { + String newName = request.get("name"); + if (newName == null || newName.trim().isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("message", "名称不能为空")); + } + try { + fileService.renameFile(id, principal.getUserId(), newName.trim()); + return ResponseEntity.ok(Map.of("message", "重命名成功")); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(Map.of("message", e.getMessage())); + } + } + + @PutMapping("/{id}/move") + public ResponseEntity moveFile( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable Long id, + @RequestBody Map request) { + Long folderId = request.get("folderId"); + try { + fileService.moveFile(id, principal.getUserId(), folderId); + return ResponseEntity.ok(Map.of("message", "移动成功")); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(Map.of("message", e.getMessage())); + } + } +} +} diff --git a/src/main/java/com/filesystem/controller/MessageController.java b/src/main/java/com/filesystem/controller/MessageController.java new file mode 100644 index 0000000..aa1e1da --- /dev/null +++ b/src/main/java/com/filesystem/controller/MessageController.java @@ -0,0 +1,279 @@ +package com.filesystem.controller; + +import com.filesystem.entity.Message; +import com.filesystem.entity.User; +import com.filesystem.security.UserPrincipal; +import com.filesystem.service.MessageService; +import com.filesystem.service.UserService; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; +import java.util.LinkedHashMap; + +@RestController +@RequestMapping("/api/messages") +public class MessageController { + + @Resource + private MessageService messageService; + + @Resource + private UserService userService; + + @Value("${file.storage.path:./uploads}") + private String storagePath; + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy/MM"); + + // ==================== 聊天文件上传 ==================== + + @PostMapping("/upload") + public ResponseEntity uploadChatFile( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam("file") MultipartFile file) throws IOException { + + if (file.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("message", "请选择文件")); + } + + String originalName = file.getOriginalFilename(); + String ext = originalName != null && originalName.contains(".") + ? originalName.substring(originalName.lastIndexOf(".")) : ""; + String storedName = UUID.randomUUID().toString() + ext; + String datePath = LocalDateTime.now().format(DATE_FMT); + + Path targetDir = Paths.get(storagePath).toAbsolutePath().resolve("chat").resolve(datePath); + if (!Files.exists(targetDir)) { + Files.createDirectories(targetDir); + } + + Path filePath = targetDir.resolve(storedName); + file.transferTo(filePath.toFile()); + + String fileUrl = "/api/messages/file/" + datePath + "/" + storedName; + return ResponseEntity.ok(Map.of("url", fileUrl, "message", "上传成功")); + } + + // ==================== 聊天文件访问 ==================== + + @GetMapping("/file/**") + public ResponseEntity getChatFile(HttpServletRequest request) throws IOException { + String uri = request.getRequestURI(); + String prefix = "/api/messages/file/"; + String relativePath = uri.substring(uri.indexOf(prefix) + prefix.length()); + + // relativePath = "2026/04/xxx.jpg" + int lastSlash = relativePath.lastIndexOf('/'); + String datePath = relativePath.substring(0, lastSlash); + String fileName = relativePath.substring(lastSlash + 1); + + Path filePath = Paths.get(storagePath).toAbsolutePath() + .resolve("chat").resolve(datePath).resolve(fileName); + + if (!Files.exists(filePath)) { + return ResponseEntity.notFound().build(); + } + + byte[] content = Files.readAllBytes(filePath); + String ext = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase() : ""; + + boolean isImage = Set.of("png", "jpg", "jpeg", "gif", "webp", "bmp", "svg").contains(ext); + + String contentType = switch (ext) { + case "pdf" -> "application/pdf"; + case "png" -> "image/png"; + case "gif" -> "image/gif"; + case "webp" -> "image/webp"; + case "jpg", "jpeg" -> "image/jpeg"; + case "bmp" -> "image/bmp"; + case "svg" -> "image/svg+xml"; + default -> "application/octet-stream"; + }; + + String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20"); + String disposition = isImage + ? "inline; filename=\"" + encodedName + "\"" + : "attachment; filename=\"" + encodedName + "\""; + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(contentType)) + .header(HttpHeaders.CONTENT_DISPOSITION, disposition) + .header(HttpHeaders.CACHE_CONTROL, "max-age=86400") + .body(content); + } + + // ==================== 消息收发 ==================== + + @GetMapping + public ResponseEntity getMessages( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam Long userId) { + List messages = messageService.getMessages(principal.getUserId(), userId); + + for (Message msg : messages) { + if (msg.getToUserId().equals(principal.getUserId()) && msg.getIsRead() == 0) { + msg.setIsRead(1); + messageService.updateMessage(msg); + } + } + + Set userIds = new HashSet<>(); + userIds.add(principal.getUserId()); + userIds.add(userId); + for (Message msg : messages) { + userIds.add(msg.getFromUserId()); + userIds.add(msg.getToUserId()); + } + Map userMap = new HashMap<>(); + for (Long uid : userIds) { + User u = userService.findById(uid); + if (u != null) userMap.put(uid, u); + } + + List> result = messages.stream().map(msg -> { + Map m = new HashMap<>(); + m.put("id", msg.getId()); + m.put("fromUserId", msg.getFromUserId()); + m.put("toUserId", msg.getToUserId()); + m.put("content", msg.getContent()); + m.put("type", msg.getType()); + m.put("fileName", msg.getFileName()); + m.put("fileSize", msg.getFileSize()); + m.put("isRead", msg.getIsRead()); + m.put("createTime", msg.getCreateTime() != null ? msg.getCreateTime().toString() : ""); + + User fromUser = userMap.get(msg.getFromUserId()); + if (fromUser != null) { + m.put("fromUsername", fromUser.getUsername()); + m.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername()); + m.put("fromAvatar", fromUser.getAvatar()); + m.put("fromSignature", fromUser.getSignature()); + } + return m; + }).collect(Collectors.toList()); + + return ResponseEntity.ok(Map.of("data", result)); + } + + @GetMapping("/users") + public ResponseEntity getUsers(@AuthenticationPrincipal UserPrincipal principal) { + List users = userService.getAllUsersExcept(principal.getUserId()); + List> result = users.stream() + .map(u -> { + Map m = new HashMap<>(); + m.put("id", u.getId()); + m.put("username", u.getUsername()); + m.put("nickname", u.getNickname() != null ? u.getNickname() : u.getUsername()); + m.put("avatar", u.getAvatar()); + m.put("signature", u.getSignature()); + m.put("email", u.getEmail()); + m.put("phone", u.getPhone()); + return m; + }) + .collect(Collectors.toList()); + return ResponseEntity.ok(Map.of("data", result)); + } + + @PostMapping + public ResponseEntity sendMessage( + @AuthenticationPrincipal UserPrincipal principal, + @RequestBody Map request) { + Long toUserId = Long.valueOf(request.get("toUserId").toString()); + String content = (String) request.get("content"); + String type = (String) request.getOrDefault("type", "text"); + String fileName = (String) request.get("fileName"); + Object fileSizeObj = request.get("fileSize"); + Long fileSize = fileSizeObj != null ? Long.valueOf(fileSizeObj.toString()) : null; + + Message message = messageService.sendMessage(principal.getUserId(), toUserId, content, type); + + if (("file".equals(type) || "image".equals(type)) && fileName != null) { + message.setFileName(fileName); + message.setFileSize(fileSize); + messageService.updateMessage(message); + } + + Map result = new HashMap<>(); + result.put("id", message.getId()); + result.put("fromUserId", message.getFromUserId()); + result.put("toUserId", message.getToUserId()); + result.put("content", message.getContent()); + result.put("type", message.getType()); + result.put("fileName", message.getFileName()); + result.put("fileSize", message.getFileSize()); + result.put("createTime", message.getCreateTime() != null ? message.getCreateTime().toString() : ""); + + User fromUser = userService.findById(principal.getUserId()); + if (fromUser != null) { + result.put("fromUsername", fromUser.getUsername()); + result.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername()); + result.put("fromSignature", fromUser.getSignature()); + } + + return ResponseEntity.ok(Map.of("data", result, "message", "发送成功")); + } + + @GetMapping("/unreadCount") + public ResponseEntity getUnreadCount(@AuthenticationPrincipal UserPrincipal principal) { + int count = messageService.getUnreadCount(principal.getUserId()); + return ResponseEntity.ok(Map.of("data", Map.of("count", count))); + } + + @GetMapping("/unreadList") + public ResponseEntity getUnreadList(@AuthenticationPrincipal UserPrincipal principal) { + List unreadMessages = messageService.getUnreadMessages(principal.getUserId()); + + // 按发送人分组 + Map> grouped = unreadMessages.stream() + .collect(Collectors.groupingBy(Message::getFromUserId, LinkedHashMap::new, Collectors.toList())); + + List> result = new ArrayList<>(); + for (Map.Entry> entry : grouped.entrySet()) { + Long fromUserId = entry.getKey(); + List msgs = entry.getValue(); + Message lastMsg = msgs.get(0); // 已按时间倒序,第一条就是最新的 + User fromUser = userService.findById(fromUserId); + + Map item = new HashMap<>(); + item.put("userId", fromUserId); + item.put("unread", msgs.size()); + item.put("lastMsg", lastMsg.getType() != null && lastMsg.getType().startsWith("image") + ? "[图片]" : (lastMsg.getType() != null && lastMsg.getType().equals("file") + ? "[文件]" : (lastMsg.getContent() != null && lastMsg.getContent().length() > 30 + ? lastMsg.getContent().substring(0, 30) + "..." : lastMsg.getContent()))); + item.put("lastTime", lastMsg.getCreateTime() != null ? lastMsg.getCreateTime().toString() : ""); + if (fromUser != null) { + item.put("username", fromUser.getUsername()); + item.put("nickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername()); + item.put("avatar", fromUser.getAvatar()); + item.put("signature", fromUser.getSignature()); + } + result.add(item); + } + + return ResponseEntity.ok(Map.of("data", result)); + } + + @PostMapping("/{id}/read") + public ResponseEntity markAsRead(@PathVariable Long id) { + messageService.markAsRead(id); + return ResponseEntity.ok(Map.of("message", "已标记已读")); + } +} diff --git a/src/main/java/com/filesystem/controller/UserController.java b/src/main/java/com/filesystem/controller/UserController.java new file mode 100644 index 0000000..75a8ff6 --- /dev/null +++ b/src/main/java/com/filesystem/controller/UserController.java @@ -0,0 +1,125 @@ +package com.filesystem.controller; + +import com.filesystem.entity.User; +import com.filesystem.security.UserPrincipal; +import com.filesystem.service.UserService; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Resource + private UserService userService; + + @Value("${file.storage.path:./uploads}") + private String storagePath; + + /** + * 获取所有可用用户(用于文件共享等场景) + */ + @GetMapping + public ResponseEntity getAllUsers(@AuthenticationPrincipal UserPrincipal principal) { + List users = userService.getAllUsersExcept(principal.getUserId()); + List> result = users.stream() + .filter(u -> u.getStatus() == 1) // 只返回启用状态的用户 + .map(u -> { + Map m = new java.util.HashMap<>(); + m.put("id", u.getId()); + m.put("username", u.getUsername()); + m.put("nickname", u.getNickname() != null ? u.getNickname() : u.getUsername()); + m.put("avatar", u.getAvatar()); + m.put("signature", u.getSignature()); + return m; + }) + .collect(Collectors.toList()); + return ResponseEntity.ok(Map.of("data", result)); + } + + /** + * 上传头像 + */ + @PostMapping("/avatar") + public ResponseEntity uploadAvatar( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam("avatar") MultipartFile file) throws IOException { + + if (file.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("message", "请选择图片")); + } + + // 限制文件大小 2MB + if (file.getSize() > 2 * 1024 * 1024) { + return ResponseEntity.badRequest().body(Map.of("message", "图片大小不能超过2MB")); + } + + // 保存文件 + String originalFilename = file.getOriginalFilename(); + String ext = originalFilename != null && originalFilename.contains(".") + ? originalFilename.substring(originalFilename.lastIndexOf(".")) + : ".jpg"; + String fileName = UUID.randomUUID().toString() + ext; + String datePath = java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy/MM")); + + // 使用配置文件中的路径 + 日期目录 + Path uploadPath = Paths.get(storagePath).toAbsolutePath().resolve("avatars").resolve(datePath); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + Path filePath = uploadPath.resolve(fileName); + Files.copy(file.getInputStream(), filePath); + + // 更新用户头像 + String avatarUrl = "/api/files/avatar/" + datePath + "/" + fileName; + userService.updateAvatar(principal.getUserId(), avatarUrl); + + return ResponseEntity.ok(Map.of("data", Map.of("url", avatarUrl), "message", "上传成功")); + } + + /** + * 获取当前用户信息 + */ + @GetMapping("/me") + public ResponseEntity getCurrentUser(@AuthenticationPrincipal UserPrincipal principal) { + User user = userService.findById(principal.getUserId()); + Map result = new java.util.HashMap<>(); + result.put("id", user.getId()); + result.put("username", user.getUsername()); + result.put("nickname", user.getNickname()); + result.put("avatar", user.getAvatar()); + result.put("signature", user.getSignature()); + result.put("email", user.getEmail()); + result.put("phone", user.getPhone()); + return ResponseEntity.ok(Map.of("data", result)); + } + + /** + * 更新个人信息 + */ + @PutMapping("/profile") + public ResponseEntity updateProfile( + @AuthenticationPrincipal UserPrincipal principal, + @RequestBody Map request) { + String nickname = request.get("nickname"); + String signature = request.get("signature"); + String phone = request.get("phone"); + String email = request.get("email"); + userService.updateProfile(principal.getUserId(), nickname, signature, phone, email); + return ResponseEntity.ok(Map.of("message", "更新成功")); + } +} diff --git a/src/main/java/com/filesystem/entity/BaseEntity.java b/src/main/java/com/filesystem/entity/BaseEntity.java new file mode 100644 index 0000000..c1db283 --- /dev/null +++ b/src/main/java/com/filesystem/entity/BaseEntity.java @@ -0,0 +1,21 @@ +package com.filesystem.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class BaseEntity { + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableField("is_deleted") + private Integer isDeleted; +} diff --git a/src/main/java/com/filesystem/entity/FileEntity.java b/src/main/java/com/filesystem/entity/FileEntity.java new file mode 100644 index 0000000..05f22a1 --- /dev/null +++ b/src/main/java/com/filesystem/entity/FileEntity.java @@ -0,0 +1,34 @@ +package com.filesystem.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "sys_file", autoResultMap = true) +public class FileEntity extends BaseEntity { + + private String name; + + private String type; + + private Long size; + + private String path; + + @TableField("folder_id") + private Long folderId; + + @TableField("user_id") + private Long userId; + + @TableField("is_folder") + private Integer isFolder; + + @TableField("is_shared") + private Integer isShared; + + @TableField("deleted_at") + private String deletedAt; +} diff --git a/src/main/java/com/filesystem/entity/FileShare.java b/src/main/java/com/filesystem/entity/FileShare.java new file mode 100644 index 0000000..245c6ef --- /dev/null +++ b/src/main/java/com/filesystem/entity/FileShare.java @@ -0,0 +1,16 @@ +package com.filesystem.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_file_share") +public class FileShare extends BaseEntity { + + private Long fileId; + private Long ownerId; + private Long shareToUserId; + private String permission; +} diff --git a/src/main/java/com/filesystem/entity/Message.java b/src/main/java/com/filesystem/entity/Message.java new file mode 100644 index 0000000..a64286e --- /dev/null +++ b/src/main/java/com/filesystem/entity/Message.java @@ -0,0 +1,30 @@ +package com.filesystem.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "sys_message", autoResultMap = true) +public class Message extends BaseEntity { + + @TableField("from_user_id") + private Long fromUserId; + + @TableField("to_user_id") + private Long toUserId; + + private String content; + + private String type; // text, image, file, emoji + + @TableField("file_name") + private String fileName; + + @TableField("file_size") + private Long fileSize; + + @TableField("is_read") + private Integer isRead; +} diff --git a/src/main/java/com/filesystem/entity/User.java b/src/main/java/com/filesystem/entity/User.java new file mode 100644 index 0000000..af138c4 --- /dev/null +++ b/src/main/java/com/filesystem/entity/User.java @@ -0,0 +1,33 @@ +package com.filesystem.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "sys_user", autoResultMap = true) +public class User extends BaseEntity { + + private String username; + + private String password; + + private String nickname; + + private String avatar; + + private String signature; + + private String email; + + private String phone; + + private Integer status; + + @TableField("storage_used") + private Long storageUsed; + + @TableField("storage_limit") + private Long storageLimit; +} diff --git a/src/main/java/com/filesystem/mapper/FileMapper.java b/src/main/java/com/filesystem/mapper/FileMapper.java new file mode 100644 index 0000000..e78c054 --- /dev/null +++ b/src/main/java/com/filesystem/mapper/FileMapper.java @@ -0,0 +1,9 @@ +package com.filesystem.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.filesystem.entity.FileEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FileMapper extends BaseMapper { +} diff --git a/src/main/java/com/filesystem/mapper/FileShareMapper.java b/src/main/java/com/filesystem/mapper/FileShareMapper.java new file mode 100644 index 0000000..caff5c0 --- /dev/null +++ b/src/main/java/com/filesystem/mapper/FileShareMapper.java @@ -0,0 +1,9 @@ +package com.filesystem.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.filesystem.entity.FileShare; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FileShareMapper extends BaseMapper { +} diff --git a/src/main/java/com/filesystem/mapper/MessageMapper.java b/src/main/java/com/filesystem/mapper/MessageMapper.java new file mode 100644 index 0000000..379e60f --- /dev/null +++ b/src/main/java/com/filesystem/mapper/MessageMapper.java @@ -0,0 +1,9 @@ +package com.filesystem.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.filesystem.entity.Message; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MessageMapper extends BaseMapper { +} diff --git a/src/main/java/com/filesystem/mapper/UserMapper.java b/src/main/java/com/filesystem/mapper/UserMapper.java new file mode 100644 index 0000000..c8b89dd --- /dev/null +++ b/src/main/java/com/filesystem/mapper/UserMapper.java @@ -0,0 +1,9 @@ +package com.filesystem.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.filesystem.entity.User; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper extends BaseMapper { +} diff --git a/src/main/java/com/filesystem/security/JwtAuthenticationFilter.java b/src/main/java/com/filesystem/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..052a034 --- /dev/null +++ b/src/main/java/com/filesystem/security/JwtAuthenticationFilter.java @@ -0,0 +1,82 @@ +package com.filesystem.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.annotation.Resource; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Resource + private JwtUtil jwtUtil; + + private static final List EXCLUDE_MATCHERS = List.of( + new AntPathRequestMatcher("/api/auth/login"), + new AntPathRequestMatcher("/api/auth/register"), + new AntPathRequestMatcher("/api/auth/logout"), + new AntPathRequestMatcher("/api/files/test"), + new AntPathRequestMatcher("/ws/**"), + new AntPathRequestMatcher("/api/files/avatar/**"), + new AntPathRequestMatcher("/api/messages/file/**") + ); + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + for (AntPathRequestMatcher matcher : EXCLUDE_MATCHERS) { + if (matcher.matches(request)) return true; + } + return false; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + String authHeader = request.getHeader("Authorization"); + String token = null; + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + } else { + token = request.getParameter("token"); + } + + if (token != null && !token.isEmpty()) { + try { + if (jwtUtil.validateToken(token)) { + String username = jwtUtil.getUsernameFromToken(token); + Long userId = jwtUtil.getUserIdFromToken(token); + + UserPrincipal principal = new UserPrincipal(userId, username); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(principal, null, new ArrayList<>()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + // token 无效,清除认证上下文,继续放行(由 Security 决定是否拦截) + SecurityContextHolder.clearContext(); + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/filesystem/security/JwtUtil.java b/src/main/java/com/filesystem/security/JwtUtil.java new file mode 100644 index 0000000..12f93f1 --- /dev/null +++ b/src/main/java/com/filesystem/security/JwtUtil.java @@ -0,0 +1,130 @@ +package com.filesystem.security; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; +import jakarta.annotation.PostConstruct; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration:86400000}") + private Long expiration; + + @Resource + private RedisTemplate redisTemplate; + + private SecretKey signingKey; + + @PostConstruct + public void init() { + // 确保 secret 至少 256 位 (32 字节) + String paddedSecret = secret; + while (paddedSecret.getBytes(StandardCharsets.UTF_8).length < 32) { + paddedSecret = paddedSecret + secret; + } + this.signingKey = Keys.hmacShaKeyFor(paddedSecret.getBytes(StandardCharsets.UTF_8)); + + // 启动时清空所有 token,强制重新登录 + try { + var keys = redisTemplate.keys("token:*"); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + } + } catch (Exception e) { + System.out.println("Redis unavailable on startup, skip token cleanup"); + } + } + + private SecretKey getSigningKey() { + return signingKey; + } + + public String generateToken(String username, Long userId) { + String token = Jwts.builder() + .subject(username) + .claim("userId", userId) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + + // 存储到 Redis + try { + String key = "token:" + userId; + redisTemplate.opsForValue().set(key, token, expiration, TimeUnit.MILLISECONDS); + } catch (Exception e) { + // Redis 不可用时忽略,继续返回 token + System.out.println("Redis unavailable, token stored in memory only"); + } + + return token; + } + + public String getUsernameFromToken(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .getSubject(); + } + + public Long getUserIdFromToken(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .get("userId", Long.class); + } + + public boolean validateToken(String token) { + try { + // 先验证 JWT 签名 + Claims claims = Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + + // 如果 JWT 已过期,直接返回 false + if (claims.getExpiration().before(new Date())) { + return false; + } + + // 检查 Redis 中的 token 是否匹配 + try { + Long userId = getUserIdFromToken(token); + String key = "token:" + userId; + Object storedToken = redisTemplate.opsForValue().get(key); + return storedToken != null && storedToken.equals(token); + } catch (Exception e) { + // Redis 不可用时,只要 JWT 本身未过期就返回 true + return true; + } + } catch (Exception e) { + return false; + } + } + + public void invalidateToken(Long userId) { + try { + String key = "token:" + userId; + redisTemplate.delete(key); + } catch (Exception e) { + System.out.println("Redis unavailable, cannot invalidate token"); + } + } +} diff --git a/src/main/java/com/filesystem/security/UserPrincipal.java b/src/main/java/com/filesystem/security/UserPrincipal.java new file mode 100644 index 0000000..69ea98c --- /dev/null +++ b/src/main/java/com/filesystem/security/UserPrincipal.java @@ -0,0 +1,11 @@ +package com.filesystem.security; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class UserPrincipal { + private Long userId; + private String username; +} diff --git a/src/main/java/com/filesystem/service/FileService.java b/src/main/java/com/filesystem/service/FileService.java new file mode 100644 index 0000000..1dd830e --- /dev/null +++ b/src/main/java/com/filesystem/service/FileService.java @@ -0,0 +1,502 @@ +package com.filesystem.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.filesystem.entity.FileEntity; +import com.filesystem.entity.FileShare; +import com.filesystem.mapper.FileMapper; +import com.filesystem.mapper.FileShareMapper; +import com.filesystem.mapper.UserMapper; +import com.filesystem.entity.User; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +public class FileService { + + @Resource + private FileMapper fileMapper; + + @Resource + private FileShareMapper fileShareMapper; + + @Resource + private UserMapper userMapper; + + @Resource + private UserService userService; + + @Value("${file.storage.path:./uploads}") + private String storagePath; + + public List getFiles(Long userId, Long folderId, String keyword) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FileEntity::getUserId, userId) + .eq(FileEntity::getIsDeleted, 0); + + if (folderId != null) { + wrapper.eq(FileEntity::getFolderId, folderId); + } else { + wrapper.isNull(FileEntity::getFolderId); + } + + if (keyword != null && !keyword.isEmpty()) { + wrapper.like(FileEntity::getName, keyword); + } + + wrapper.orderByDesc(FileEntity::getIsFolder) + .orderByDesc(FileEntity::getCreateTime); + + return fileMapper.selectList(wrapper); + } + + public List getTrashFiles(Long userId, Long folderId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FileEntity::getUserId, userId) + .eq(FileEntity::getIsDeleted, 1); + + if (folderId != null) { + // 查询指定文件夹下的已删除文件 + wrapper.eq(FileEntity::getFolderId, folderId); + } else { + // 查询根目录下已删除的文件和文件夹 + wrapper.isNull(FileEntity::getFolderId) + .or(w -> w.eq(FileEntity::getFolderId, 0)); + } + + wrapper.orderByDesc(FileEntity::getDeletedAt); + return fileMapper.selectList(wrapper); + } + + public FileEntity getById(Long id) { + return fileMapper.selectById(id); + } + + @Transactional + public void moveToTrash(Long id, Long userId) { + FileEntity file = fileMapper.selectById(id); + if (file != null && file.getUserId().equals(userId)) { + // 使用 LambdaUpdateWrapper 明确指定要更新的字段 + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(FileEntity::getId, id) + .set(FileEntity::getIsDeleted, 1) + .set(FileEntity::getDeletedAt, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + fileMapper.update(null, wrapper); + } + } + + @Transactional + public void restoreFile(Long id, Long userId) { + FileEntity file = fileMapper.selectById(id); + if (file == null || !file.getUserId().equals(userId)) return; + + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(FileEntity::getId, id) + .set(FileEntity::getIsDeleted, 0) + .set(FileEntity::getDeletedAt, null); + fileMapper.update(null, wrapper); + + // 如果是文件夹,递归还原所有子文件 + if (file.getIsFolder() != null && file.getIsFolder() == 1) { + restoreChildren(id, userId); + } + } + + private void restoreChildren(Long parentFolderId, Long userId) { + List children = fileMapper.selectList( + new LambdaQueryWrapper() + .eq(FileEntity::getFolderId, parentFolderId) + .eq(FileEntity::getIsDeleted, 1) + .eq(FileEntity::getUserId, userId) + ); + for (FileEntity child : children) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(FileEntity::getId, child.getId()) + .set(FileEntity::getIsDeleted, 0) + .set(FileEntity::getDeletedAt, null); + fileMapper.update(null, wrapper); + + if (child.getIsFolder() != null && child.getIsFolder() == 1) { + restoreChildren(child.getId(), userId); + } + } + } + + @Transactional + public void deletePermanently(Long id, Long userId) { + FileEntity file = fileMapper.selectById(id); + if (file == null || !file.getUserId().equals(userId)) return; + + // 如果是文件夹,先递归删除所有子文件 + if (file.getIsFolder() != null && file.getIsFolder() == 1) { + deleteChildrenPermanently(id, userId); + } + + // 删除当前文件的物理文件并扣减存储 + if (file.getPath() != null && !file.getPath().isEmpty()) { + try { + Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(file.getPath()); + Files.deleteIfExists(filePath); + } catch (IOException e) { + // ignore + } + if (file.getSize() != null && file.getSize() > 0) { + userService.decreaseStorage(userId, file.getSize()); + } + } + fileMapper.deleteById(id); + } + + private void deleteChildrenPermanently(Long parentFolderId, Long userId) { + List children = fileMapper.selectList( + new LambdaQueryWrapper() + .eq(FileEntity::getFolderId, parentFolderId) + .eq(FileEntity::getIsDeleted, 1) + .eq(FileEntity::getUserId, userId) + ); + for (FileEntity child : children) { + if (child.getIsFolder() != null && child.getIsFolder() == 1) { + deleteChildrenPermanently(child.getId(), userId); + } + if (child.getPath() != null && !child.getPath().isEmpty()) { + try { + Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(child.getPath()); + Files.deleteIfExists(filePath); + } catch (IOException e) { + // ignore + } + if (child.getSize() != null && child.getSize() > 0) { + userService.decreaseStorage(userId, child.getSize()); + } + } + fileMapper.deleteById(child.getId()); + } + } + + @Transactional + public void emptyTrash(Long userId) { + List trashFiles = getTrashFiles(userId, null); + for (FileEntity file : trashFiles) { + deletePermanently(file.getId(), userId); + } + } + + @Transactional + public FileEntity createFolder(String name, Long userId, Long parentId) { + FileEntity folder = new FileEntity(); + folder.setName(name); + folder.setType("folder"); + folder.setIsFolder(1); + folder.setUserId(userId); + folder.setFolderId(parentId); + folder.setSize(0L); + folder.setIsShared(0); + folder.setIsDeleted(0); + fileMapper.insert(folder); + return folder; + } + + @Transactional + public List uploadFiles(List files, Long userId, Long folderId) throws IOException { + List uploadedFiles = new ArrayList<>(); + + // 确保存储目录存在(使用配置文件中的路径 + files 子目录) + Path uploadDir = Paths.get(storagePath).toAbsolutePath().resolve("files"); + if (!Files.exists(uploadDir)) { + Files.createDirectories(uploadDir); + } + + for (MultipartFile file : files) { + if (file.isEmpty()) continue; + + String originalName = file.getOriginalFilename(); + String extension = originalName.contains(".") ? originalName.substring(originalName.lastIndexOf(".")) : ""; + String storedName = UUID.randomUUID().toString() + extension; + String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM")); + + Path targetDir = uploadDir.resolve(datePath); + if (!Files.exists(targetDir)) { + Files.createDirectories(targetDir); + } + + Path targetPath = targetDir.resolve(storedName); + file.transferTo(targetPath.toFile()); + + FileEntity fileEntity = new FileEntity(); + fileEntity.setName(originalName); + fileEntity.setType(getFileType(extension)); + fileEntity.setSize(file.getSize()); + fileEntity.setPath(datePath + "/" + storedName); + fileEntity.setUserId(userId); + fileEntity.setFolderId(folderId); + fileEntity.setIsFolder(0); + fileEntity.setIsShared(0); + fileEntity.setIsDeleted(0); + + fileMapper.insert(fileEntity); + uploadedFiles.add(fileEntity); + + // 更新用户存储空间 + userService.updateStorage(userId, file.getSize()); + } + + return uploadedFiles; + } + + private String getFileType(String extension) { + if (extension == null || extension.isEmpty()) return "file"; + extension = extension.toLowerCase(); + if (".jpg".equals(extension) || ".jpeg".equals(extension) || ".png".equals(extension) + || ".gif".equals(extension) || ".webp".equals(extension) || ".svg".equals(extension)) { + return "image"; + } + if (".mp4".equals(extension) || ".avi".equals(extension) || ".mov".equals(extension)) { + return "video"; + } + if (".mp3".equals(extension) || ".wav".equals(extension) || ".flac".equals(extension)) { + return "audio"; + } + if (".pdf".equals(extension)) { + return "pdf"; + } + return "file"; + } + + public byte[] getFileContent(FileEntity file) throws IOException { + if (file == null || file.getPath() == null) return null; + Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(file.getPath()); + return Files.readAllBytes(filePath); + } + + // 共享相关 + public List getSharedByMe(Long userId) { + List shares = fileShareMapper.selectList( + new LambdaQueryWrapper().eq(FileShare::getOwnerId, userId) + ); + + return shares.stream() + .map(share -> fileMapper.selectById(share.getFileId())) + .filter(f -> f != null && f.getIsDeleted() == 0) + .collect(Collectors.toList()); + } + + public List getSharedByMeFolderFiles(Long userId, Long folderId) { + try { + // 检查该文件夹是否由当前用户共享 + FileEntity folder = fileMapper.selectById(folderId); + if (folder == null || folder.getIsDeleted() == 1 || !folder.getUserId().equals(userId)) { + return new ArrayList<>(); + } + + // 检查是否有共享记录 + List shares = fileShareMapper.selectList( + new LambdaQueryWrapper() + .eq(FileShare::getFileId, folderId) + .eq(FileShare::getOwnerId, userId) + ); + + if (shares.isEmpty()) { + return new ArrayList<>(); + } + + // 返回该文件夹内的文件 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FileEntity::getFolderId, folderId) + .eq(FileEntity::getIsDeleted, 0) + .isNull(FileEntity::getDeletedAt); + + return fileMapper.selectList(wrapper); + } catch (Exception e) { + return new ArrayList<>(); + } + } + + public List getSharedToMe(Long userId) { + List shares = fileShareMapper.selectList( + new LambdaQueryWrapper().eq(FileShare::getShareToUserId, userId) + ); + + return shares.stream() + .map(share -> fileMapper.selectById(share.getFileId())) + .filter(f -> f != null && f.getIsDeleted() == 0) + .collect(Collectors.toList()); + } + + public List getSharedFolderFiles(Long userId, Long folderId) { + try { + // 检查该文件夹是否被共享给当前用户 + FileEntity folder = fileMapper.selectById(folderId); + if (folder == null || folder.getIsDeleted() == 1) { + return new ArrayList<>(); + } + + // 检查是否有共享权限 + List shares = fileShareMapper.selectList( + new LambdaQueryWrapper() + .eq(FileShare::getFileId, folderId) + .eq(FileShare::getShareToUserId, userId) + ); + + if (shares.isEmpty()) { + return new ArrayList<>(); + } + + // 返回该文件夹内的文件 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FileEntity::getFolderId, folderId) + .eq(FileEntity::getIsDeleted, 0) + .isNull(FileEntity::getDeletedAt); + + return fileMapper.selectList(wrapper); + } catch (Exception e) { + return new ArrayList<>(); + } + } + + @Transactional + public FileShare shareFile(Long fileId, Long ownerId, Long shareToUserId, String permission) { + FileShare share = new FileShare(); + share.setFileId(fileId); + share.setOwnerId(ownerId); + share.setShareToUserId(shareToUserId); + share.setPermission(permission != null ? permission : "view"); + fileShareMapper.insert(share); + + // 更新文件共享状态 + FileEntity file = fileMapper.selectById(fileId); + if (file != null) { + file.setIsShared(1); + fileMapper.updateById(file); + } + + return share; + } + + @Transactional + public void cancelShare(Long fileId, Long ownerId) { + fileShareMapper.delete( + new LambdaQueryWrapper() + .eq(FileShare::getFileId, fileId) + .eq(FileShare::getOwnerId, ownerId) + ); + + // 检查是否还有其他共享 + Long count = fileShareMapper.selectCount( + new LambdaQueryWrapper().eq(FileShare::getFileId, fileId) + ); + if (count == 0) { + FileEntity file = fileMapper.selectById(fileId); + if (file != null) { + file.setIsShared(0); + fileMapper.updateById(file); + } + } + } + + public String getOwnerName(Long userId) { + User user = userMapper.selectById(userId); + return user != null ? user.getNickname() : "未知用户"; + } + + public void renameFile(Long fileId, Long userId, String newName) { + FileEntity file = fileMapper.selectById(fileId); + if (file == null) { + throw new RuntimeException("文件不存在"); + } + if (!file.getUserId().equals(userId)) { + throw new RuntimeException("无权操作此文件"); + } + if (file.getIsDeleted() == 1) { + throw new RuntimeException("无法重命名回收站中的文件"); + } + + // 检查新名称是否已存在(同一目录下) + FileEntity existing = fileMapper.selectOne( + new LambdaQueryWrapper() + .eq(FileEntity::getUserId, userId) + .eq(FileEntity::getFolderId, file.getFolderId()) + .eq(FileEntity::getName, newName) + .eq(FileEntity::getIsDeleted, 0) + .ne(FileEntity::getId, fileId) + ); + if (existing != null) { + throw new RuntimeException("该名称已存在"); + } + + file.setName(newName); + fileMapper.updateById(file); + } + + public void moveFile(Long fileId, Long userId, Long targetFolderId) { + FileEntity file = fileMapper.selectById(fileId); + if (file == null) { + throw new RuntimeException("文件不存在"); + } + if (!file.getUserId().equals(userId)) { + throw new RuntimeException("无权操作此文件"); + } + if (file.getIsDeleted() == 1) { + throw new RuntimeException("无法移动回收站中的文件"); + } + + // 检查目标文件夹是否存在(如果不是根目录) + if (targetFolderId != null) { + FileEntity targetFolder = fileMapper.selectById(targetFolderId); + if (targetFolder == null) { + throw new RuntimeException("目标文件夹不存在"); + } + if (!targetFolder.getUserId().equals(userId)) { + throw new RuntimeException("无权访问目标文件夹"); + } + if (targetFolder.getIsDeleted() == 1) { + throw new RuntimeException("目标文件夹在回收站中"); + } + // 检查是否移动到自己里面(如果是文件夹) + if (file.getIsFolder() == 1) { + if (file.getId().equals(targetFolderId)) { + throw new RuntimeException("不能移动到自己里面"); + } + // 检查目标是否是自己子文件夹 + Long parentId = targetFolder.getFolderId(); + while (parentId != null) { + if (parentId.equals(file.getId())) { + throw new RuntimeException("不能移动到自己的子文件夹中"); + } + FileEntity parent = fileMapper.selectById(parentId); + if (parent == null) break; + parentId = parent.getFolderId(); + } + } + } + + // 检查目标位置是否已有同名文件 + FileEntity existing = fileMapper.selectOne( + new LambdaQueryWrapper() + .eq(FileEntity::getUserId, userId) + .eq(FileEntity::getFolderId, targetFolderId) + .eq(FileEntity::getName, file.getName()) + .eq(FileEntity::getIsDeleted, 0) + .ne(FileEntity::getId, fileId) + ); + if (existing != null) { + throw new RuntimeException("目标位置已存在同名文件"); + } + + file.setFolderId(targetFolderId); + fileMapper.updateById(file); + } +} diff --git a/src/main/java/com/filesystem/service/MessageService.java b/src/main/java/com/filesystem/service/MessageService.java new file mode 100644 index 0000000..1b46683 --- /dev/null +++ b/src/main/java/com/filesystem/service/MessageService.java @@ -0,0 +1,85 @@ +package com.filesystem.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.filesystem.entity.Message; +import com.filesystem.mapper.MessageMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class MessageService { + + @Resource + private MessageMapper messageMapper; + + public List getMessages(Long userId1, Long userId2) { + return messageMapper.selectList( + new LambdaQueryWrapper() + .and(wrapper -> wrapper + .eq(Message::getFromUserId, userId1).eq(Message::getToUserId, userId2) + .or() + .eq(Message::getFromUserId, userId2).eq(Message::getToUserId, userId1) + ) + .orderByAsc(Message::getCreateTime) + ); + } + + public Message sendMessage(Long fromUserId, Long toUserId, String content, String type) { + Message message = new Message(); + message.setFromUserId(fromUserId); + message.setToUserId(toUserId); + message.setContent(content); + message.setType(type); + message.setIsRead(0); + messageMapper.insert(message); + return message; + } + + public void updateMessage(Message message) { + messageMapper.updateById(message); + } + + public Message sendFileMessage(Long fromUserId, Long toUserId, String fileName, Long fileSize) { + Message message = new Message(); + message.setFromUserId(fromUserId); + message.setToUserId(toUserId); + message.setContent(fileName); + message.setType("file"); + message.setFileName(fileName); + message.setFileSize(fileSize); + message.setIsRead(0); + messageMapper.insert(message); + return message; + } + + public int getUnreadCount(Long userId) { + return Math.toIntExact(messageMapper.selectCount( + new LambdaQueryWrapper() + .eq(Message::getToUserId, userId) + .eq(Message::getIsRead, 0) + )); + } + + public void markAsRead(Long messageId) { + Message message = messageMapper.selectById(messageId); + if (message != null) { + message.setIsRead(1); + messageMapper.updateById(message); + } + } + + /** + * 获取未读消息列表:按发送人分组,返回每个联系人的未读数和最后一条消息 + */ + public List getUnreadMessages(Long userId) { + return messageMapper.selectList( + new LambdaQueryWrapper() + .eq(Message::getToUserId, userId) + .eq(Message::getIsRead, 0) + .orderByDesc(Message::getCreateTime) + ); + } +} diff --git a/src/main/java/com/filesystem/service/UserService.java b/src/main/java/com/filesystem/service/UserService.java new file mode 100644 index 0000000..043ffb2 --- /dev/null +++ b/src/main/java/com/filesystem/service/UserService.java @@ -0,0 +1,150 @@ +package com.filesystem.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.filesystem.entity.FileEntity; +import com.filesystem.entity.User; +import com.filesystem.mapper.FileMapper; +import com.filesystem.mapper.UserMapper; +import com.filesystem.security.JwtUtil; +import jakarta.annotation.Resource; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserService { + + @Resource + private UserMapper userMapper; + + @Resource + private FileMapper fileMapper; + + @Resource + private JwtUtil jwtUtil; + + @Resource + private BCryptPasswordEncoder passwordEncoder; + + public User findByUsername(String username) { + return userMapper.selectOne( + new LambdaQueryWrapper().eq(User::getUsername, username) + ); + } + + public User findById(Long id) { + return userMapper.selectById(id); + } + + public String login(String username, String password) { + User user = findByUsername(username); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + if (!passwordEncoder.matches(password, user.getPassword())) { + throw new RuntimeException("密码错误"); + } + return jwtUtil.generateToken(username, user.getId()); + } + + public void logout(Long userId) { + jwtUtil.invalidateToken(userId); + } + + public User createUser(String username, String password, String nickname) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.setNickname(nickname); + user.setStatus(1); + user.setStorageUsed(0L); + user.setStorageLimit(20L * 1024 * 1024 * 1024); // 20GB + userMapper.insert(user); + return user; + } + + /** + * 精确重算用户存储空间:统计该用户所有未删除(非回收站)文件的总大小 + */ + public long recalculateStorage(Long userId) { + List userFiles = fileMapper.selectList( + new LambdaQueryWrapper() + .eq(FileEntity::getUserId, userId) + .eq(FileEntity::getIsDeleted, 0) + .ne(FileEntity::getIsFolder, 1) + ); + long total = 0L; + for (FileEntity f : userFiles) { + if (f.getSize() != null) { + total += f.getSize(); + } + } + // 更新到用户表 + User user = userMapper.selectById(userId); + if (user != null) { + user.setStorageUsed(total); + // 同步存储上限为 20GB(兼容老用户) + if (user.getStorageLimit() == null || user.getStorageLimit() < 20L * 1024 * 1024 * 1024) { + user.setStorageLimit(20L * 1024 * 1024 * 1024); + } + userMapper.updateById(user); + } + return total; + } + + public void updateStorage(Long userId, Long size) { + User user = userMapper.selectById(userId); + if (user != null) { + user.setStorageUsed(user.getStorageUsed() + size); + userMapper.updateById(user); + } + } + + /** + * 扣减存储空间(用于彻底删除文件时) + */ + public void decreaseStorage(Long userId, Long size) { + User user = userMapper.selectById(userId); + if (user != null) { + long newValue = user.getStorageUsed() - size; + user.setStorageUsed(Math.max(0L, newValue)); + userMapper.updateById(user); + } + } + + public List getAllUsersExcept(Long excludeId) { + return userMapper.selectList( + new LambdaQueryWrapper() + .ne(User::getId, excludeId) + .eq(User::getStatus, 1) + ); + } + + public void updateAvatar(Long userId, String avatarUrl) { + User user = userMapper.selectById(userId); + if (user != null) { + user.setAvatar(avatarUrl); + userMapper.updateById(user); + } + } + + public void updateProfile(Long userId, String nickname, String signature, String phone, String email) { + User user = userMapper.selectById(userId); + if (user != null) { + if (nickname != null) { + user.setNickname(nickname); + } + if (signature != null) { + user.setSignature(signature); + } + if (phone != null) { + user.setPhone(phone); + } + if (email != null) { + user.setEmail(email); + } + userMapper.updateById(user); + } + } +} diff --git a/src/main/java/com/filesystem/websocket/ChatHandler.java b/src/main/java/com/filesystem/websocket/ChatHandler.java new file mode 100644 index 0000000..c25378d --- /dev/null +++ b/src/main/java/com/filesystem/websocket/ChatHandler.java @@ -0,0 +1,140 @@ +package com.filesystem.websocket; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.filesystem.entity.User; +import com.filesystem.security.JwtUtil; +import com.filesystem.service.MessageService; +import com.filesystem.service.UserService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class ChatHandler extends TextWebSocketHandler { + + @Resource + private JwtUtil jwtUtil; + + @Resource + private MessageService messageService; + + @Resource + private UserService userService; + + private final Map onlineUsers = new ConcurrentHashMap<>(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + Long userId = getUserIdFromSession(session); + if (userId != null) { + onlineUsers.put(userId, session); + broadcastOnlineUsers(); + } + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + Long fromUserId = getUserIdFromSession(session); + if (fromUserId == null) return; + + Map data = objectMapper.readValue(message.getPayload(), Map.class); + String type = (String) data.get("type"); + + switch (type) { + case "chat" -> handleChat(fromUserId, data); + case "ping" -> session.sendMessage(new TextMessage("{\"type\":\"pong\"}")); + } + } + + private void handleChat(Long fromUserId, Map data) throws IOException { + Long toUserId = Long.valueOf(data.get("toUserId").toString()); + String content = (String) data.get("content"); + String msgType = (String) data.getOrDefault("msgType", "text"); + String fileName = (String) data.get("fileName"); + Object fileSizeObj = data.get("fileSize"); + Long fileSize = fileSizeObj != null ? Long.valueOf(fileSizeObj.toString()) : null; + + // 保存到数据库 + com.filesystem.entity.Message msg = messageService.sendMessage(fromUserId, toUserId, content, msgType); + + if ("file".equals(msgType) && fileName != null) { + msg.setFileName(fileName); + msg.setFileSize(fileSize); + messageService.updateMessage(msg); + } + + // 查询发送者用户信息 + User fromUser = userService.findById(fromUserId); + + Map messageData = new HashMap<>(); + messageData.put("id", msg.getId()); + messageData.put("fromUserId", msg.getFromUserId()); + messageData.put("toUserId", msg.getToUserId()); + messageData.put("content", msg.getContent()); + messageData.put("type", msg.getType()); + messageData.put("fileName", msg.getFileName()); + messageData.put("fileSize", msg.getFileSize()); + messageData.put("createTime", msg.getCreateTime() != null ? msg.getCreateTime().toString() : ""); + if (fromUser != null) { + messageData.put("fromUsername", fromUser.getUsername()); + messageData.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername()); + messageData.put("fromAvatar", fromUser.getAvatar()); + messageData.put("fromSignature", fromUser.getSignature()); + } + + Map resp = Map.of("type", "chat", "message", messageData); + String respJson = objectMapper.writeValueAsString(resp); + + // 发送给接收者 + WebSocketSession toSession = onlineUsers.get(toUserId); + if (toSession != null && toSession.isOpen()) { + toSession.sendMessage(new TextMessage(respJson)); + } + + // 发送回发送者确认 + WebSocketSession fromSession = onlineUsers.get(fromUserId); + if (fromSession != null && fromSession.isOpen()) { + fromSession.sendMessage(new TextMessage(respJson)); + } + } + + private void broadcastOnlineUsers() throws IOException { + Map resp = Map.of("type", "online", "users", onlineUsers.keySet()); + String json = objectMapper.writeValueAsString(resp); + for (WebSocketSession session : onlineUsers.values()) { + if (session.isOpen()) { + session.sendMessage(new TextMessage(json)); + } + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + Long userId = getUserIdFromSession(session); + if (userId != null) { + onlineUsers.remove(userId); + broadcastOnlineUsers(); + } + } + + private Long getUserIdFromSession(WebSocketSession session) { + Object userId = session.getAttributes().get("userId"); + if (userId != null) { + try { + return Long.valueOf(userId.toString()); + } catch (Exception e) { + return null; + } + } + return null; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..6bfdeaa --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,46 @@ +server: + port: 8080 + +spring: + datasource: + url: jdbc:mysql://127.0.0.1:33069/prd?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=true + username: root + password: root_dream + driver-class-name: com.mysql.cj.jdbc.Driver + + data: + redis: + host: 192.168.31.194 + port: 16379 + database: 0 + timeout: 10000ms + password: admin + + servlet: + multipart: + enabled: true + max-file-size: 1024MB + max-request-size: 2048MB + file-size-threshold: 0 + +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + type-aliases-package: com.filesystem.entity + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto + +file: + storage: + path: /ogsapp/uploads + +jwt: + secret: mySecretKeyForJWTTokenGenerationThatIsLongEnough256BitsForHS256Algorithm + expiration: 86400000 + +logging: + level: + com.filesystem: DEBUG diff --git a/src/main/resources/db/init.sql b/src/main/resources/db/init.sql new file mode 100644 index 0000000..96e2380 --- /dev/null +++ b/src/main/resources/db/init.sql @@ -0,0 +1,78 @@ +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS file_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE file_system; + +-- 用户表 +CREATE TABLE IF NOT EXISTS sys_user ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', + password VARCHAR(255) NOT NULL COMMENT '密码', + nickname VARCHAR(50) COMMENT '昵称', + avatar VARCHAR(255) COMMENT '头像', + signature VARCHAR(255) COMMENT '个性签名', + email VARCHAR(100) COMMENT '邮箱', + phone VARCHAR(20) COMMENT '手机号', + status INT DEFAULT 1 COMMENT '状态 0-禁用 1-启用', + storage_used BIGINT DEFAULT 0 COMMENT '已用存储空间(字节)', + storage_limit BIGINT DEFAULT 10737418240 COMMENT '存储限制(字节) 默认10GB', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + is_deleted INT DEFAULT 0 COMMENT '是否删除 0-否 1-是' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- 文件表 +CREATE TABLE IF NOT EXISTS sys_file ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + name VARCHAR(255) NOT NULL COMMENT '文件名', + type VARCHAR(50) COMMENT '文件类型', + size BIGINT DEFAULT 0 COMMENT '文件大小(字节)', + path VARCHAR(500) COMMENT '存储路径', + folder_id BIGINT COMMENT '所属文件夹ID', + user_id BIGINT NOT NULL COMMENT '所属用户ID', + is_folder INT DEFAULT 0 COMMENT '是否文件夹 0-否 1-是', + is_shared INT DEFAULT 0 COMMENT '是否共享 0-否 1-是', + deleted_at DATETIME COMMENT '删除时间', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + is_deleted INT DEFAULT 0 COMMENT '是否删除 0-否 1-是', + INDEX idx_user_id (user_id), + INDEX idx_folder_id (folder_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件表'; + +-- 文件共享表 +CREATE TABLE IF NOT EXISTS sys_file_share ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + file_id BIGINT NOT NULL COMMENT '文件ID', + owner_id BIGINT NOT NULL COMMENT '所有者ID', + share_to_user_id BIGINT NOT NULL COMMENT '共享给用户ID', + permission VARCHAR(20) DEFAULT 'view' COMMENT '权限 view-查看 edit-编辑', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + is_deleted INT DEFAULT 0 COMMENT '是否删除 0-否 1-是', + INDEX idx_file_id (file_id), + INDEX idx_owner_id (owner_id), + INDEX idx_share_to_user_id (share_to_user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件共享表'; + +-- 消息表 +CREATE TABLE IF NOT EXISTS sys_message ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + from_user_id BIGINT NOT NULL COMMENT '发送者ID', + to_user_id BIGINT NOT NULL COMMENT '接收者ID', + content TEXT COMMENT '消息内容', + type VARCHAR(20) DEFAULT 'text' COMMENT '消息类型 text-文本 image-图片 file-文件 emoji-表情', + file_name VARCHAR(255) COMMENT '文件名', + file_size BIGINT COMMENT '文件大小', + is_read INT DEFAULT 0 COMMENT '是否已读 0-未读 1-已读', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + is_deleted INT DEFAULT 0 COMMENT '是否删除 0-否 1-是', + INDEX idx_from_user_id (from_user_id), + INDEX idx_to_user_id (to_user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表'; + +-- 插入默认管理员账户 (密码: admin123) +INSERT INTO sys_user (username, password, nickname, status, storage_limit) +VALUES ('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', '管理员', 1, 10737418240) +ON DUPLICATE KEY UPDATE username = username; diff --git a/web-vue/index.html b/web-vue/index.html new file mode 100644 index 0000000..287d09f --- /dev/null +++ b/web-vue/index.html @@ -0,0 +1,12 @@ + + + + + + 文件管理系统 + + +
+ + + diff --git a/web-vue/package-lock.json b/web-vue/package-lock.json new file mode 100644 index 0000000..a21e891 --- /dev/null +++ b/web-vue/package-lock.json @@ -0,0 +1,1760 @@ +{ + "name": "file-system-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "file-system-frontend", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.2", + "element-plus": "^2.4.4", + "jit-viewer": "^1.1.4", + "pinia": "^2.1.7", + "vue": "^3.4.0", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.2", + "vite": "^5.0.10" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "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": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "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.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "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", + "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": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", + "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.31", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", + "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz", + "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.31", + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz", + "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "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/@vue/reactivity": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz", + "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz", + "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz", + "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/runtime-core": "3.5.31", + "@vue/shared": "3.5.31", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz", + "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "vue": "3.5.31" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", + "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "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/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.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "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/element-plus": { + "version": "2.13.6", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.6.tgz", + "integrity": "sha512-XHgwXr8Fjz6i+6BaqFhAbae/dJbG7bBAAlHrY3pWL7dpj+JcqcOyKYt4Oy5KP86FQwS1k4uIZDjCx2FyUR5lDg==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@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": "12.0.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", + "vue-component-type-helpers": "^3.2.4" + }, + "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.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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/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/jit-viewer": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jit-viewer/-/jit-viewer-1.1.4.tgz", + "integrity": "sha512-QGiIrMkSmubNe/ARzkCh972z4LbSmgSs2zewgxyub371akFP6B9ZezAQz7ZTWJXNP0DMBMbCGZNvx1zNfmCDow==", + "license": "Apache-2.0" + }, + "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" + }, + "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" + }, + "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/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/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/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "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.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz", + "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-sfc": "3.5.31", + "@vue/runtime-dom": "3.5.31", + "@vue/server-renderer": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.6.tgz", + "integrity": "sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==", + "license": "MIT" + }, + "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/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" + } + } + } +} diff --git a/web-vue/package.json b/web-vue/package.json new file mode 100644 index 0000000..784a06f --- /dev/null +++ b/web-vue/package.json @@ -0,0 +1,23 @@ +{ + "name": "file-system-frontend", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.2", + "element-plus": "^2.4.4", + "jit-viewer": "^1.1.4", + "pinia": "^2.1.7", + "vue": "^3.4.0", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.2", + "vite": "^5.0.10" + } +} diff --git a/web-vue/src/App.vue b/web-vue/src/App.vue new file mode 100644 index 0000000..a3cf66c --- /dev/null +++ b/web-vue/src/App.vue @@ -0,0 +1,16 @@ + + + diff --git a/web-vue/src/api/auth.js b/web-vue/src/api/auth.js new file mode 100644 index 0000000..dc94d5b --- /dev/null +++ b/web-vue/src/api/auth.js @@ -0,0 +1,7 @@ +import request from './request' + +export const login = (data) => request.post('/auth/login', data) + +export const register = (data) => request.post('/auth/register', data) + +export const getCurrentUser = () => request.get('/auth/info') diff --git a/web-vue/src/api/file.js b/web-vue/src/api/file.js new file mode 100644 index 0000000..c12b757 --- /dev/null +++ b/web-vue/src/api/file.js @@ -0,0 +1,28 @@ +import request from './request' + +export const getFiles = (params) => request.get('/files', { params }) +export const getTrashFiles = (params) => request.get('/files/trashFiles', { params }) +export const getSharedByMe = () => request.get('/files/sharedByMe') +export const getSharedByMeFolderFiles = (folderId) => request.get('/files/sharedByMe/folder', { params: { folderId } }) +export const getSharedToMe = () => request.get('/files/sharedToMe') +export const getSharedFolderFiles = (folderId) => request.get('/files/sharedToMe/folder', { params: { folderId } }) +export const uploadFiles = (files, folderId) => { + const formData = new FormData() + files.forEach(file => formData.append('files', file)) + if (folderId) formData.append('folderId', folderId) + return request.post('/files/uploadBatch', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 600000 + }) +} +export const downloadFile = (id) => request.get(`/files/${id}/download`, { responseType: 'blob' }) +export const deleteFile = (id) => request.delete(`/files/${id}`) +export const restoreFile = (id) => request.post(`/files/${id}/restore`) +export const deletePermanently = (id) => request.delete(`/files/${id}/deletePermanent`) +export const emptyTrash = () => request.delete('/files/emptyTrash') +export const createFolder = (data) => request.post('/files/createFolder', data) +export const shareFileApi = (id, data) => request.post(`/files/${id}/shareFile`, data) +export const cancelShare = (id) => request.delete(`/files/${id}/cancelShare`) +export const renameFile = (id, name) => request.put(`/files/${id}/rename`, { name }) +export const moveFile = (id, folderId) => request.put(`/files/${id}/move`, { folderId }) +export const getFilePreview = (id) => request.get(`/files/${id}/preview`, { responseType: 'blob' }) diff --git a/web-vue/src/api/message.js b/web-vue/src/api/message.js new file mode 100644 index 0000000..d9ac430 --- /dev/null +++ b/web-vue/src/api/message.js @@ -0,0 +1,20 @@ +import request from './request' + +export const getMessages = (params) => request.get('/messages', { params }) + +export const sendMessage = (data) => request.post('/messages', data) + +export const getUnreadCount = () => request.get('/messages/unreadCount') + +export const getUnreadList = () => request.get('/messages/unreadList') + +export const markAsRead = (id) => request.post(`/messages/${id}/read`) + +export const getUsers = () => request.get('/messages/users') + +// 聊天文件上传(图片和文件统一接口) +export const uploadChatFile = (formData) => { + return request.post('/messages/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) +} diff --git a/web-vue/src/api/request.js b/web-vue/src/api/request.js new file mode 100644 index 0000000..7eb8a15 --- /dev/null +++ b/web-vue/src/api/request.js @@ -0,0 +1,44 @@ +import axios from 'axios' + +const request = axios.create({ + baseURL: '/api', + timeout: 300000 +}) + +request.interceptors.request.use( + config => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + error => Promise.reject(error) +) + +request.interceptors.response.use( + response => response.data, + error => { + const status = error.response?.status + + // 只有 401/403 才清理 token 并跳转登录页 + if (status === 401 || status === 403) { + localStorage.removeItem('token') + localStorage.removeItem('username') + localStorage.removeItem('userId') + localStorage.removeItem('nickname') + localStorage.removeItem('avatar') + localStorage.removeItem('storageUsed') + localStorage.removeItem('storageLimit') + + if (window.location.pathname !== '/login') { + window.location.href = '/login' + } + } + + // 其他错误(404、500、网络错误等)正常 reject,不跳转 + return Promise.reject(error) + } +) + +export default request diff --git a/web-vue/src/api/user.js b/web-vue/src/api/user.js new file mode 100644 index 0000000..0e8ca22 --- /dev/null +++ b/web-vue/src/api/user.js @@ -0,0 +1,3 @@ +import request from './request' + +export const getUsers = () => request.get('/users') diff --git a/web-vue/src/components/ChatDialog.vue b/web-vue/src/components/ChatDialog.vue new file mode 100644 index 0000000..936b15a --- /dev/null +++ b/web-vue/src/components/ChatDialog.vue @@ -0,0 +1,727 @@ + + + + + diff --git a/web-vue/src/components/FileSidebar.vue b/web-vue/src/components/FileSidebar.vue new file mode 100644 index 0000000..30abe23 --- /dev/null +++ b/web-vue/src/components/FileSidebar.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/web-vue/src/components/FileTable.vue b/web-vue/src/components/FileTable.vue new file mode 100644 index 0000000..38be80d --- /dev/null +++ b/web-vue/src/components/FileTable.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/web-vue/src/components/FileToolbar.vue b/web-vue/src/components/FileToolbar.vue new file mode 100644 index 0000000..94b94aa --- /dev/null +++ b/web-vue/src/components/FileToolbar.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/web-vue/src/components/FolderDialog.vue b/web-vue/src/components/FolderDialog.vue new file mode 100644 index 0000000..bb1bd38 --- /dev/null +++ b/web-vue/src/components/FolderDialog.vue @@ -0,0 +1,53 @@ + + + diff --git a/web-vue/src/components/PreviewDialog.vue b/web-vue/src/components/PreviewDialog.vue new file mode 100644 index 0000000..34c5674 --- /dev/null +++ b/web-vue/src/components/PreviewDialog.vue @@ -0,0 +1,105 @@ +