Compare commits
765 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a48fcb3819 | ||
|
|
68a07bc952 | ||
|
|
828dba09b0 | ||
|
|
0d2189e9e8 | ||
|
|
f0f0ab81e4 | ||
|
|
64b5fa7038 | ||
|
|
1d04c9b9c9 | ||
|
|
dee719ac25 | ||
|
|
ea676876f1 | ||
|
|
c1a4d5d81e | ||
|
|
95d88804e4 | ||
|
|
1fa072790f | ||
|
|
fe19c1183c | ||
|
|
be40f55bd9 | ||
|
|
30a10eaf6d | ||
|
|
3bc0c86df4 | ||
|
|
03c8726e6e | ||
|
|
de47491ded | ||
|
|
c691cdaa0e | ||
|
|
53efdc2802 | ||
|
|
9644076463 | ||
|
|
cb4e88f8aa | ||
|
|
adc16fc58d | ||
|
|
d6860a3e24 | ||
|
|
7e6116de45 | ||
|
|
1688a2ca25 | ||
|
|
fe57acfce0 | ||
|
|
1ae49b28b1 | ||
|
|
ef4e9c8b40 | ||
|
|
5da0758e89 | ||
|
|
816cab252d | ||
|
|
843f638835 | ||
|
|
e4684b2e12 | ||
|
|
c17365b6c9 | ||
|
|
01835c0ac5 | ||
|
|
e5749bd6ef | ||
|
|
689e58737b | ||
|
|
38da061cf1 | ||
|
|
e79940e52e | ||
|
|
88dd6068b6 | ||
|
|
7dd10f9c96 | ||
|
|
94aaf83107 | ||
|
|
e84fc5f424 | ||
|
|
f342b08179 | ||
|
|
0fcad02f3b | ||
|
|
43d2406ee9 | ||
|
|
78e2d05730 | ||
|
|
425bf808ed | ||
|
|
6d2916dc9f | ||
|
|
2281e4224b | ||
|
|
95282f9883 | ||
|
|
b470f182c9 | ||
|
|
0bba1068af | ||
|
|
947a7d8296 | ||
|
|
bd36cbf888 | ||
|
|
d8fa47bff7 | ||
|
|
1132beea5e | ||
|
|
2e3314e6c3 | ||
|
|
daa8f857f8 | ||
|
|
6d14271fe8 | ||
|
|
9284d48f67 | ||
|
|
c5d1c5a468 | ||
|
|
b98512789f | ||
|
|
6b8ed8d527 | ||
|
|
ec4500dcef | ||
|
|
288e63ce68 | ||
|
|
b3885584bb | ||
|
|
968b24be1e | ||
|
|
5a23c1783a | ||
|
|
ddeeb5a7c3 | ||
|
|
0b9bbcc7b8 | ||
|
|
022c8b4515 | ||
|
|
be04991928 | ||
|
|
34770567a5 | ||
|
|
6154fc2157 | ||
|
|
e77dcdd3d4 | ||
|
|
58a3532c1b | ||
|
|
116a5eeb43 | ||
|
|
decd50cb40 | ||
|
|
355563244c | ||
|
|
51aad628b5 | ||
|
|
7dd7a2cf34 | ||
|
|
4c0ff7c7f2 | ||
|
|
8aba3cbe00 | ||
|
|
e21c3ec507 | ||
|
|
fdbb0b2ca8 | ||
|
|
180195ab7d | ||
|
|
8add4e6b46 | ||
|
|
3d622d2efe | ||
|
|
bb7ed7b963 | ||
|
|
d541ea41ad | ||
|
|
7c7ebc9eb7 | ||
|
|
22275c3b12 | ||
|
|
8744a34e8e | ||
|
|
e98836fd0e | ||
|
|
feb62196a2 | ||
|
|
9fd29a2958 | ||
|
|
546c82ca40 | ||
|
|
f132dc38f4 | ||
|
|
58c70b8ca6 | ||
|
|
147f55eefe | ||
|
|
229b7b0c12 | ||
|
|
4b7b5ff8a4 | ||
|
|
4906bde746 | ||
|
|
a87a1a8988 | ||
|
|
e05f45e681 | ||
|
|
b4acacea81 | ||
|
|
fa9645b05b | ||
|
|
1ed4052814 | ||
|
|
7dc814461f | ||
|
|
9154ec0e8c | ||
|
|
3a2ea60583 | ||
|
|
b36bff3a1e | ||
|
|
b3d8cbf280 | ||
|
|
38fb02d112 | ||
|
|
2597f893cd | ||
|
|
ebdd036654 | ||
|
|
5032f0e6a9 | ||
|
|
ad963d718d | ||
|
|
69d314bce3 | ||
|
|
4a7425a947 | ||
|
|
c172ac0d5c | ||
|
|
01a66493a8 | ||
|
|
188f8b3faa | ||
|
|
ebcf5fad71 | ||
|
|
d1a656db82 | ||
|
|
4f6a11fd7c | ||
|
|
1d09a946bb | ||
|
|
6c4eb7edbd | ||
|
|
4f9f669ac6 | ||
|
|
f9e0e78473 | ||
|
|
b004facfca | ||
|
|
fb6ee2910f | ||
|
|
3fedc9b730 | ||
|
|
b260427312 | ||
|
|
dd1447e93c | ||
|
|
dbcc213562 | ||
|
|
1c019cd5c8 | ||
|
|
e37bde77a1 | ||
|
|
57bf0d2021 | ||
|
|
88b00f7069 | ||
|
|
7b08cbb2f7 | ||
|
|
97c0ec184d | ||
|
|
d18c845088 | ||
|
|
a64d97774d | ||
|
|
2ddc51aa4f | ||
|
|
28afe2a922 | ||
|
|
c2e97bf191 | ||
|
|
c922752a1f | ||
|
|
08f36a74ca | ||
|
|
d7809dd00c | ||
|
|
27582004da | ||
|
|
3d6a176cde | ||
|
|
4a2073a038 | ||
|
|
c8a65ecbe4 | ||
|
|
3750d5cba0 | ||
|
|
55b383780e | ||
|
|
6aec0ddf88 | ||
|
|
7c8e94d1df | ||
|
|
5ecbf626c8 | ||
|
|
584f580e3b | ||
|
|
280de47dac | ||
|
|
c7c05f5897 | ||
|
|
bb86180582 | ||
|
|
aff228edd3 | ||
|
|
f65ae6d703 | ||
|
|
0fccc06883 | ||
|
|
8652966645 | ||
|
|
6d84eb9f09 | ||
|
|
1a3dccac29 | ||
|
|
fa8de34fc5 | ||
|
|
10cfd6be80 | ||
|
|
a390b36e7c | ||
|
|
d6b5994e22 | ||
|
|
08611a97e7 | ||
|
|
35bbb44ce3 | ||
|
|
8ff879661a | ||
|
|
a8f01f099d | ||
|
|
040ab1096b | ||
|
|
0cbdf24315 | ||
|
|
164ea79bd1 | ||
|
|
97f3435bb3 | ||
|
|
63b108ff6b | ||
|
|
b0880cb369 | ||
|
|
5f70ee8e18 | ||
|
|
4c64f7a2c3 | ||
|
|
262927e459 | ||
|
|
b16c566004 | ||
|
|
1af82dbee6 | ||
|
|
2e9a5a4e13 | ||
|
|
b455f603dc | ||
|
|
37c0c3e339 | ||
|
|
b6cb341082 | ||
|
|
1af1a06700 | ||
|
|
79e4ecfdbe | ||
|
|
1585271e37 | ||
|
|
c240b171e4 | ||
|
|
9c405e90ac | ||
|
|
3ec3212ca5 | ||
|
|
b1289f6177 | ||
|
|
64b7ba48c8 | ||
|
|
f093053ea4 | ||
|
|
9faa0ded59 | ||
|
|
0f7dafeb23 | ||
|
|
472d1960d9 | ||
|
|
6e50acf106 | ||
|
|
a3fb4b1534 | ||
|
|
382cae32a2 | ||
|
|
0aa4851f8e | ||
|
|
65271e6d13 | ||
|
|
671cf8d588 | ||
|
|
afc7c81028 | ||
|
|
c330aee560 | ||
|
|
eafe63c886 | ||
|
|
53206d05b8 | ||
|
|
af085d457e | ||
|
|
fb36033939 | ||
|
|
584e7672df | ||
|
|
d4f7a5a1c0 | ||
|
|
2a9ea81ad4 | ||
|
|
276948dd68 | ||
|
|
990c5583f2 | ||
|
|
644f1b5640 | ||
|
|
5261fbe870 | ||
|
|
e4f2d85e2b | ||
|
|
8e3ccdc24a | ||
|
|
cd6d93affd | ||
|
|
6096ab0c9b | ||
|
|
0a87bb1db1 | ||
|
|
a19042c655 | ||
|
|
a889687a6a | ||
|
|
e1cdc715aa | ||
|
|
a82b3a0a29 | ||
|
|
d93a71f0be | ||
|
|
899dc765bc | ||
|
|
449490e52d | ||
|
|
5541d7974e | ||
|
|
ae3eb36183 | ||
|
|
d57e9a397c | ||
|
|
9d4fd16d81 | ||
|
|
3b16e7a123 | ||
|
|
1c4a2176e9 | ||
|
|
62f9243714 | ||
|
|
03bd23d314 | ||
|
|
27497d1812 | ||
|
|
f36c1bd2b5 | ||
|
|
cf72b2cdb9 | ||
|
|
44f6950fea | ||
|
|
308ddfedea | ||
|
|
ac7c330e2f | ||
|
|
1bde3492da | ||
|
|
f884518df3 | ||
|
|
1f7f9ce9db | ||
|
|
58acde2292 | ||
|
|
4e0fe2f449 | ||
|
|
536793ab25 | ||
|
|
23a48e07a2 | ||
|
|
1e55557154 | ||
|
|
752231086d | ||
|
|
6f315a408a | ||
|
|
6fa4caa85e | ||
|
|
1b36c1752f | ||
|
|
cd58498971 | ||
|
|
1586137a5d | ||
|
|
6cb8bf74df | ||
|
|
787802d0db | ||
|
|
b4ad39db12 | ||
|
|
c13edbe017 | ||
|
|
7546da4f90 | ||
|
|
76b9a8d9e7 | ||
|
|
d6d52338e9 | ||
|
|
caa67a0f49 | ||
|
|
6ddc3ea996 | ||
|
|
7edbf7c724 | ||
|
|
4f233ca886 | ||
|
|
457831536a | ||
|
|
ccef0d87db | ||
|
|
584d290283 | ||
|
|
2ab14fa33b | ||
|
|
f0317e1d74 | ||
|
|
17a206e0f4 | ||
|
|
8ea352cc2f | ||
|
|
0f10920898 | ||
|
|
eb098ca775 | ||
|
|
e25caddfef | ||
|
|
c74cf6cf6e | ||
|
|
ce2d04fa64 | ||
|
|
40a4e29c7e | ||
|
|
60385715e6 | ||
|
|
3cce92e83d | ||
|
|
602b0067d2 | ||
|
|
51d07db99b | ||
|
|
33d121fd64 | ||
|
|
e409dbd5b8 | ||
|
|
79d203470a | ||
|
|
0f1341615b | ||
|
|
97f5410b1c | ||
|
|
195f6b7e50 | ||
|
|
6691f40c49 | ||
|
|
bc1849f0a0 | ||
|
|
0f64ea1403 | ||
|
|
320fc1604c | ||
|
|
a8eaf3b995 | ||
|
|
308a951f78 | ||
|
|
9f98b549e9 | ||
|
|
0e2a259999 | ||
|
|
b3d3561111 | ||
|
|
ad857b0810 | ||
|
|
0918fa1685 | ||
|
|
273d1f8ef2 | ||
|
|
af1e0a2a60 | ||
|
|
79ae772367 | ||
|
|
d57c8aa305 | ||
|
|
bbd8c1b6d4 | ||
|
|
ced9288ed7 | ||
|
|
cf87e2d5ac | ||
|
|
153d4c1d01 | ||
|
|
1c50fa228e | ||
|
|
0067dc6be3 | ||
|
|
36389a5b8c | ||
|
|
c7443d993e | ||
|
|
9f8dbf3c75 | ||
|
|
35332544e4 | ||
|
|
f2bc832aca | ||
|
|
a6847f7f53 | ||
|
|
396ab64874 | ||
|
|
59ee3d8ceb | ||
|
|
3e152bd389 | ||
|
|
56e8f61bbf | ||
|
|
83c00b0544 | ||
|
|
5f82cc715e | ||
|
|
3ce7fc34f0 | ||
|
|
9fc5291fec | ||
|
|
27c7a842db | ||
|
|
ffe1992df1 | ||
|
|
a80877bab7 | ||
|
|
c787a3c786 | ||
|
|
abda382b96 | ||
|
|
c5ab0a2cc6 | ||
|
|
15340dd550 | ||
|
|
deaf444864 | ||
|
|
a5413d1116 | ||
|
|
6cb6a5822b | ||
|
|
2ffd6f7430 | ||
|
|
cd9eaf4fd7 | ||
|
|
3cfe27b7b3 | ||
|
|
44d78fd2ea | ||
|
|
0cf3342449 | ||
|
|
7e4c6516c5 | ||
|
|
73d7eb65b8 | ||
|
|
fca4afb606 | ||
|
|
b15672d593 | ||
|
|
7a37a18f23 | ||
|
|
a14806e840 | ||
|
|
bbd2851f36 | ||
|
|
48418771d4 | ||
|
|
a81071a50a | ||
|
|
304b990994 | ||
|
|
8824869cd1 | ||
|
|
325cce5f82 | ||
|
|
85db26a704 | ||
|
|
65b0acdcb4 | ||
|
|
9a27af8c5a | ||
|
|
93ad0859e8 | ||
|
|
5e62bac245 | ||
|
|
bea6c1e326 | ||
|
|
df76b01826 | ||
|
|
5d22cb84bf | ||
|
|
f01c61e09f | ||
|
|
d50e67f3bc | ||
|
|
3726c472fc | ||
|
|
dc174e81cf | ||
|
|
c9867bc453 | ||
|
|
8e282fb216 | ||
|
|
e9c0792cb3 | ||
|
|
e7e1b4c43f | ||
|
|
dc56c177b7 | ||
|
|
c0ee998874 | ||
|
|
e1ff50e1e3 | ||
|
|
0e440955c8 | ||
|
|
a16dd497c4 | ||
|
|
5aa4e9339d | ||
|
|
723fa96519 | ||
|
|
75252fded6 | ||
|
|
51fbcdfa56 | ||
|
|
61c9b97d70 | ||
|
|
23b09d09ce | ||
|
|
a00f6ab8ff | ||
|
|
bb59095bad | ||
|
|
da57124d5e | ||
|
|
a00800a128 | ||
|
|
a98db1699d | ||
|
|
e3d9e736ad | ||
|
|
28f38d8b80 | ||
|
|
3b7c34258f | ||
|
|
9dde646695 | ||
|
|
4bdee63f28 | ||
|
|
20dced021d | ||
|
|
17cf640e23 | ||
|
|
24369daea0 | ||
|
|
873bf905ab | ||
|
|
da0756adf0 | ||
|
|
09942ec946 | ||
|
|
2650bc6068 | ||
|
|
6bd7274c9c | ||
|
|
129ccf9e39 | ||
|
|
e2b789cfbc | ||
|
|
bb70e91277 | ||
|
|
f6c07a29ce | ||
|
|
4347983fc7 | ||
|
|
12b463d9e8 | ||
|
|
edc0949bed | ||
|
|
85780917c2 | ||
|
|
e45919cac1 | ||
|
|
c61821ef4e | ||
|
|
011902598b | ||
|
|
3186c6ca0e | ||
|
|
3a680a132f | ||
|
|
455dda54e8 | ||
|
|
5ea5ab07d9 | ||
|
|
35c8025b00 | ||
|
|
615c162663 | ||
|
|
c4bd15e5a0 | ||
|
|
edc92905f7 | ||
|
|
bf5bbd3689 | ||
|
|
eb70ca233b | ||
|
|
8718816fce | ||
|
|
7d36330b4b | ||
|
|
1fa0474fef | ||
|
|
4070b27148 | ||
|
|
3892b0ed05 | ||
|
|
a06cf69d7a | ||
|
|
61dc2568e8 | ||
|
|
ac6362e698 | ||
|
|
94afdf5495 | ||
|
|
d96f8acdbc | ||
|
|
d39c795f92 | ||
|
|
8e12e0562b | ||
|
|
7a1babb418 | ||
|
|
8d65f0c2a8 | ||
|
|
b8dff560f0 | ||
|
|
b48c26ee73 | ||
|
|
8328e51ae0 | ||
|
|
7070eb8a7d | ||
|
|
d0aa26441c | ||
|
|
1bba7103c8 | ||
|
|
7f8dd744f2 | ||
|
|
2f4a707498 | ||
|
|
569bc3c8ec | ||
|
|
b01421aa94 | ||
|
|
30d933bd85 | ||
|
|
377998335b | ||
|
|
21d21aa438 | ||
|
|
18cf1ea3d7 | ||
|
|
60ea884fe2 | ||
|
|
999fa9d9a6 | ||
|
|
e80034e7f8 | ||
|
|
b16f99941a | ||
|
|
3503e7d5b1 | ||
|
|
d1d80acef8 | ||
|
|
16fe916b07 | ||
|
|
d754c3dae3 | ||
|
|
1b32a3e8cd | ||
|
|
15a6f215b4 | ||
|
|
38014ba342 | ||
|
|
7dcc293a09 | ||
|
|
35ce244490 | ||
|
|
3bade2060a | ||
|
|
f8307f25c9 | ||
|
|
5c9ebb9aae | ||
|
|
ebc2a764c2 | ||
|
|
bed21856ab | ||
|
|
61805d13ab | ||
|
|
e47d8d5d2b | ||
|
|
0bd81499f6 | ||
|
|
201ae2c237 | ||
|
|
df4c3c7676 | ||
|
|
667693902f | ||
|
|
9e261d30f8 | ||
|
|
5f6bade809 | ||
|
|
273168ae5c | ||
|
|
a55269e9e6 | ||
|
|
9c386f8533 | ||
|
|
17ee5f456a | ||
|
|
6cefdb5d37 | ||
|
|
74fc8bd131 | ||
|
|
aa9dab5d96 | ||
|
|
5b461f8e1f | ||
|
|
bde06be3df | ||
|
|
fe17986b2a | ||
|
|
e9160ecefd | ||
|
|
05ebd48f09 | ||
|
|
6dbc3f4bab | ||
|
|
bc7166789b | ||
|
|
750b91db66 | ||
|
|
b69a338e13 | ||
|
|
036fe65b12 | ||
|
|
732017ac77 | ||
|
|
5bd71b4688 | ||
|
|
44ba2dff78 | ||
|
|
0954e4bde2 | ||
|
|
5b183d31e2 | ||
|
|
b2017764eb | ||
|
|
f27cd796b6 | ||
|
|
3c051b8698 | ||
|
|
052d6edd13 | ||
|
|
e7dc61e3d9 | ||
|
|
f0aefdfdf8 | ||
|
|
0beec368b8 | ||
|
|
3f1d03a127 | ||
|
|
eb143c28e3 | ||
|
|
1631951a24 | ||
|
|
31bdd89373 | ||
|
|
ad5ae12d44 | ||
|
|
c838db262c | ||
|
|
623b807a11 | ||
|
|
ce9335a842 | ||
|
|
1c62465c3e | ||
|
|
a2c176bdee | ||
|
|
bff8c0f86b | ||
|
|
1065973e07 | ||
|
|
8e042d5691 | ||
|
|
d9a6b32e5f | ||
|
|
eed3f97fbf | ||
|
|
6b9a8ed108 | ||
|
|
adc718b751 | ||
|
|
df9981d0c9 | ||
|
|
f58b661b1b | ||
|
|
ec1926ba60 | ||
|
|
e853851933 | ||
|
|
3705ce3b90 | ||
|
|
7ad73ff251 | ||
|
|
6c23e8892a | ||
|
|
58efafac71 | ||
|
|
abf2364bf6 | ||
|
|
0650f35dbb | ||
|
|
cc593634d2 | ||
|
|
79a3b9de8a | ||
|
|
ceb46ec974 | ||
|
|
a7e2893a57 | ||
|
|
2efe8efde0 | ||
|
|
31047b0d44 | ||
|
|
7c2b724d10 | ||
|
|
ca5670f06b | ||
|
|
427e05871d | ||
|
|
bef56bdb56 | ||
|
|
d450d02e18 | ||
|
|
85a766cc7b | ||
|
|
a473f356c9 | ||
|
|
52b5fdf383 | ||
|
|
b886f02043 | ||
|
|
61963ea497 | ||
|
|
2f9b27ad9e | ||
|
|
9334109767 | ||
|
|
2bc52576d9 | ||
|
|
700d2c4a51 | ||
|
|
103bdb32c8 | ||
|
|
92b745e180 | ||
|
|
a2007083b8 | ||
|
|
36a5f7ff29 | ||
|
|
f727aea51d | ||
|
|
936ca24328 | ||
|
|
62f49b6087 | ||
|
|
e9ddbf9962 | ||
|
|
196cf522e6 | ||
|
|
3fce3bf4a7 | ||
|
|
1cfee25695 | ||
|
|
5711285a77 | ||
|
|
e6f537ca3a | ||
|
|
3b5220af57 | ||
|
|
fa6b4b1d2d | ||
|
|
7968e5374b | ||
|
|
64997ebe45 | ||
|
|
f8592b01e2 | ||
|
|
087474f514 | ||
|
|
1725088f05 | ||
|
|
ec1b756a3d | ||
|
|
76a06e0817 | ||
|
|
02fb608d7b | ||
|
|
e17fc2fc12 | ||
|
|
4f6c317652 | ||
|
|
46c198be26 | ||
|
|
8552203d43 | ||
|
|
139eaa7016 | ||
|
|
d81120ab8f | ||
|
|
6353d56beb | ||
|
|
aa05496b42 | ||
|
|
dc15e537d8 | ||
|
|
6fbd41f40a | ||
|
|
0181f614e1 | ||
|
|
fded7b0b28 | ||
|
|
7e637f835a | ||
|
|
deaaf1834d | ||
|
|
139c870f99 | ||
|
|
4cc2350bc6 | ||
|
|
8b31a118da | ||
|
|
cca26acb78 | ||
|
|
245edbd2f6 | ||
|
|
903d22c622 | ||
|
|
8b1805628e | ||
|
|
11c8c488da | ||
|
|
4dd4e0e148 | ||
|
|
21f352aa64 | ||
|
|
6c4beffdb7 | ||
|
|
43d3efa838 | ||
|
|
1c99839ab4 | ||
|
|
c9e05ce5b1 | ||
|
|
3fe7ed0e1d | ||
|
|
b3bff5c6f5 | ||
|
|
e357bac70f | ||
|
|
ad51d4e4f3 | ||
|
|
912d8ced93 | ||
|
|
8334999e98 | ||
|
|
5e23ea7809 | ||
|
|
b62d291aab | ||
|
|
a34dd8148f | ||
|
|
ba13e6ac35 | ||
|
|
8efa5f7a28 | ||
|
|
f0ef9565e2 | ||
|
|
78688ab63c | ||
|
|
e90b30bf63 | ||
|
|
5312b82ba7 | ||
|
|
bc705f2560 | ||
|
|
6477f43de1 | ||
|
|
bdc0fdd076 | ||
|
|
1f09e1ff93 | ||
|
|
4bcc89d9da | ||
|
|
8f93b49dde | ||
|
|
74eeae900e | ||
|
|
63424bb134 | ||
|
|
1c5e410881 | ||
|
|
f79cc41f3c | ||
|
|
49cccbe69e | ||
|
|
c4a02f7497 | ||
|
|
59e12c5e96 | ||
|
|
a347bdc412 | ||
|
|
3f3c1ecd02 | ||
|
|
d5d9c78c91 | ||
|
|
5b0d8d902b | ||
|
|
2978e46d02 | ||
|
|
54e0633d77 | ||
|
|
ab3db66195 | ||
|
|
17e19da3d8 | ||
|
|
f22aca0c5d | ||
|
|
c257e11ee3 | ||
|
|
8b23f0bb2e | ||
|
|
a82a89afd3 | ||
|
|
5c0d0d5a95 | ||
|
|
9dbd090482 | ||
|
|
e25583dff9 | ||
|
|
d997dc0394 | ||
|
|
6b6353ed41 | ||
|
|
e73d906564 | ||
|
|
7e3e850e21 | ||
|
|
56b2dc4ebf | ||
|
|
9444b0e518 | ||
|
|
bcb72118f5 | ||
|
|
c59be8d981 | ||
|
|
8466a40455 | ||
|
|
f435b4fc52 | ||
|
|
5686c6fe65 | ||
|
|
6810112eda | ||
|
|
11a2d07935 | ||
|
|
02cd2f1570 | ||
|
|
924c1d72ea | ||
|
|
5d9b2e1919 | ||
|
|
f7fa440f9a | ||
|
|
d4aaa46968 | ||
|
|
93ac5e1b3b | ||
|
|
c7a8c68e14 | ||
|
|
77afb4d736 | ||
|
|
141796ab24 | ||
|
|
30d733f55d | ||
|
|
6a39e65b6b | ||
|
|
c27013b7ad | ||
|
|
582ce496fa | ||
|
|
5b4dbb82d5 | ||
|
|
011a0d16ab | ||
|
|
ac5539194d | ||
|
|
6b7e1b3c4e | ||
|
|
30c3d00139 | ||
|
|
36d460cd74 | ||
|
|
af287f50bb | ||
|
|
3199392637 | ||
|
|
11cb2eb0f8 | ||
|
|
4dce1c94a3 | ||
|
|
4e3a61b8a8 | ||
|
|
3b1e65fc75 | ||
|
|
32b4b944cc | ||
|
|
22a51a524e | ||
|
|
ac0cbbdb95 | ||
|
|
2260f23d3c | ||
|
|
d43952c0bf | ||
|
|
bd368123d2 | ||
|
|
cbdd70427e | ||
|
|
d7526f5283 | ||
|
|
08e914a968 | ||
|
|
53a8835b6d | ||
|
|
e3bff71a91 | ||
|
|
6276009e88 | ||
|
|
ddc5320f71 | ||
|
|
15af66aaaf | ||
|
|
fe7a080553 | ||
|
|
66bfc3e868 | ||
|
|
93aa3fb95d | ||
|
|
4f5caf1712 | ||
|
|
9d27e967cd | ||
|
|
eb3e035a7c | ||
|
|
04200e94ff | ||
|
|
ae9a13e0fa | ||
|
|
df8857fb52 | ||
|
|
9642fed1f1 | ||
|
|
1a273ea2d6 | ||
|
|
c0ba921a7e | ||
|
|
8bbad227eb | ||
|
|
d3f9c04209 | ||
|
|
d3a6703a77 | ||
|
|
1100fa47be | ||
|
|
1e33087786 | ||
|
|
e59423e912 | ||
|
|
146a1fe23d | ||
|
|
4586f6982a | ||
|
|
703204c69a | ||
|
|
05cc160311 | ||
|
|
0568f8a85d | ||
|
|
36b113ef1c | ||
|
|
520180f6f5 | ||
|
|
d349d2b500 | ||
|
|
643ca35aed | ||
|
|
36ef7ba589 | ||
|
|
b5761bd18d | ||
|
|
047e827884 | ||
|
|
48828fd72d | ||
|
|
3f4165e4b1 | ||
|
|
6d789ed73b | ||
|
|
e77297f7b2 | ||
|
|
bb52a4704f | ||
|
|
127df15674 | ||
|
|
95bcc263e8 | ||
|
|
20b120c247 | ||
|
|
e644f6bacc | ||
|
|
5e9c7124ce | ||
|
|
84e121bc0e | ||
|
|
abff2071bd | ||
|
|
078afd5174 | ||
|
|
4a8cf16012 | ||
|
|
04e9b68e4a | ||
|
|
f12c3dac9f | ||
|
|
73b9663b27 | ||
|
|
a73068069c | ||
|
|
2f963ba7ab | ||
|
|
9df70e5485 | ||
|
|
dfa34f090b | ||
|
|
388e9987cd | ||
|
|
9fe434849c | ||
|
|
95edaa99b6 | ||
|
|
b3501d791e | ||
|
|
5f2e93dde3 | ||
|
|
bf22d7f5e9 | ||
|
|
08bbe8d841 | ||
|
|
572293bb4d | ||
|
|
f56d1c68c7 | ||
|
|
900dd6e958 | ||
|
|
5327c04e7e | ||
|
|
f1835dd46c |
3
.github/workflows/build.yml
vendored
@@ -44,6 +44,7 @@ jobs:
|
||||
|
||||
- name: Delete Release
|
||||
uses: dev-drprasad/delete-tag-and-release@v1.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
tag_name: ${{ env.frontend_version }}
|
||||
delete_release: true
|
||||
@@ -56,7 +57,7 @@ jobs:
|
||||
name: ${{ env.frontend_version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
make_latest: false
|
||||
make_latest: true
|
||||
files: |
|
||||
dist.zip
|
||||
env:
|
||||
|
||||
5
.vscode/settings.json
vendored
@@ -106,5 +106,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": false
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": false,
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/locales"
|
||||
]
|
||||
}
|
||||
|
||||
25
README.md
@@ -4,15 +4,34 @@
|
||||
|
||||
[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目,NodeJS版本:>= `v20.12.1`。
|
||||
|
||||
## 推荐的IDE设置
|
||||
## 特性
|
||||
|
||||
- 基于 Vue 3 和 Vuetify 3 构建的现代化界面
|
||||
- 使用 Vite 作为构建工具,提供快速的开发体验
|
||||
- 支持多语言(中文/英文)
|
||||
- 完整的插件系统支持,包括远程组件动态加载
|
||||
|
||||
## 模块联邦功能
|
||||
|
||||
MoviePilot 现已支持模块联邦(Module Federation)功能,允许插件开发者创建可动态加载的远程组件,实现更丰富的插件用户界面。
|
||||
|
||||
### 相关文档
|
||||
|
||||
- [模块联邦开发指南](docs/module-federation-guide.md) - 如何开发远程组件插件
|
||||
- [模块联邦问题排查指南](docs/federation-troubleshooting.md) - 常见问题和解决方案
|
||||
- [插件远程组件示例](examples/plugin-component/) - 开发插件组件的完整示例项目
|
||||
|
||||
## 开发部署
|
||||
|
||||
### 推荐的IDE设置
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (并禁用 Vetur).
|
||||
|
||||
## 配置Vite
|
||||
### 配置Vite
|
||||
|
||||
请参阅 [Vite 配置参考](https://vitejs.dev/config/).
|
||||
|
||||
## 依赖安装
|
||||
### 依赖安装
|
||||
|
||||
```sh
|
||||
yarn
|
||||
|
||||
38
README_EN.md
@@ -2,39 +2,57 @@
|
||||
|
||||
*[中文](README.md) | English*
|
||||
|
||||
Frontend project for [MoviePilot](https://github.com/jxxghp/MoviePilot), NodeJS version: >= `v20.12.1`.
|
||||
Frontend project for [MoviePilot](https://github.com/jxxghp/MoviePilot), NodeJS version required: >= `v20.12.1`.
|
||||
|
||||
## Recommended IDE Setup
|
||||
## Features
|
||||
|
||||
- Modern interface built with Vue 3 and Vuetify 3
|
||||
- Fast development experience with Vite build tool
|
||||
- Multi-language support (Chinese/English)
|
||||
- Complete plugin system with dynamic remote component loading
|
||||
|
||||
## Module Federation
|
||||
|
||||
MoviePilot now supports Module Federation, allowing plugin developers to create dynamically loadable remote components for richer plugin user interfaces.
|
||||
|
||||
### Documentation
|
||||
|
||||
- [Module Federation Troubleshooting Guide](docs/federation-troubleshooting.md) - Common issues and solutions
|
||||
- [Plugin Remote Component Example](examples/plugin-component/) - Complete example project for developing plugin components
|
||||
|
||||
## Development
|
||||
|
||||
### Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (disable Vetur).
|
||||
|
||||
## Vite Configuration
|
||||
### Configure Vite
|
||||
|
||||
Please refer to [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Installation
|
||||
### Install Dependencies
|
||||
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Development
|
||||
### Development Server
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Build
|
||||
### Build for Production
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Production Deployment
|
||||
### Static Deployment
|
||||
|
||||
1. Use `nginx` or other web servers to host the `dist` static files. See `public/nginx.conf` for nginx configuration reference.
|
||||
1. Host the `dist` static files using a web server like `nginx`. Refer to `public/nginx.conf` for nginx configuration.
|
||||
|
||||
2. Use `node` command to run `service.js` directly. It listens on port `3000` by default. Set the environment variable `NGINX_PORT` to adjust the port.
|
||||
2. Alternatively, run the `service.js` directly with the `node` command. It listens on port `3000` by default. Set the `NGINX_PORT` environment variable to adjust the port.
|
||||
|
||||
```shell
|
||||
node dist/service.js
|
||||
|
||||
3
components.d.ts
vendored
@@ -8,17 +8,16 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ConfirmDialog: typeof import('./src/@core/components/ConfirmDialog.vue')['default']
|
||||
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
|
||||
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
|
||||
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
|
||||
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
|
||||
LocaleSwitcher: typeof import('./src/@core/components/LocaleSwitcher.vue')['default']
|
||||
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
|
||||
PageContentTitle: typeof import('./src/@core/components/PageContentTitle.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScrollToTopBtn: typeof import('./src/@core/components/ScrollToTopBtn.vue')['default']
|
||||
StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default']
|
||||
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
110
docs/federation-troubleshooting.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# MoviePilot 模块联邦问题排查指南
|
||||
|
||||
本文档提供了针对 MoviePilot 项目中使用模块联邦时可能遇到的常见问题及解决方案。
|
||||
|
||||
## 远程组件注册机制
|
||||
|
||||
MoviePilot 使用自动注册机制来加载远程组件:
|
||||
|
||||
1. 对于使用 Vue 渲染模式的插件,自动注册其远程组件
|
||||
2. 每个远程组件根据插件 ID 唯一标识,确保不会冲突
|
||||
3. 在需要加载组件时,会优先检查已注册的组件信息
|
||||
|
||||
这种设计使得插件开发者只需专注于组件开发,而不需要担心加载机制的复杂性。
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 1. "Module name 'vue' does not resolve to a valid URL"
|
||||
|
||||
**原因**:远程组件无法正确解析共享依赖的 URL,通常是因为共享依赖配置不正确。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. 在 **插件组件项目** 的 `vite.config.js` 中正确配置共享依赖:
|
||||
|
||||
```js
|
||||
federation({
|
||||
// ...
|
||||
shared: {
|
||||
vue: {
|
||||
singleton: true,
|
||||
requiredVersion: false // 关闭版本检查
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
2. 在 **主应用** 的 `vite.config.ts` 中确保共享依赖配置正确:
|
||||
|
||||
```ts
|
||||
federation({
|
||||
name: 'host',
|
||||
remotes: {},
|
||||
shared: ['vue', 'vuetify']
|
||||
})
|
||||
```
|
||||
|
||||
### 2. "Top-level await is not available in the configured target environment"
|
||||
|
||||
**原因**:模块联邦使用了顶层 await,但目标构建环境不支持此功能。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
在 **主应用** 和 **插件组件项目** 的构建配置中添加 `target: 'esnext'`:
|
||||
|
||||
```js
|
||||
build: {
|
||||
target: 'esnext', // 支持顶层await
|
||||
// 其他配置...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. "TypeError: Failed to fetch dynamically imported module"
|
||||
|
||||
**原因**:远程组件 JS 文件无法被正确加载,可能是路径错误或网络问题。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. 检查网络请求是否成功(状态码200)
|
||||
2. 确认组件 URL 是否正确
|
||||
3. 确保服务器允许访问该 JS 文件(CORS 配置)
|
||||
4. 检查插件后端是否正确提供了静态文件服务
|
||||
|
||||
### 4. 组件加载后渲染为空白或出现错误
|
||||
|
||||
**原因**:组件内部代码错误或与主应用不兼容。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. 检查浏览器控制台错误信息
|
||||
2. 确保组件代码没有语法错误
|
||||
3. 避免在组件中使用主应用未提供的依赖
|
||||
4. 确保所有路径(如图片、API请求URL等)都是正确的
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 启用详细日志
|
||||
|
||||
在浏览器控制台中设置:
|
||||
|
||||
```js
|
||||
localStorage.setItem('debug', 'vite:*')
|
||||
```
|
||||
|
||||
### 2. 分析网络请求
|
||||
|
||||
1. 打开浏览器开发者工具
|
||||
2. 转到 Network 标签页
|
||||
3. 确认远程组件 JS 文件请求是否成功
|
||||
4. 分析响应内容是否为有效的 JavaScript
|
||||
|
||||
### 3. 隔离测试远程组件
|
||||
|
||||
创建一个独立的简单页面来测试插件组件,排除主应用的干扰因素。
|
||||
|
||||
## 其他资源
|
||||
|
||||
- [MoviePilot 插件组件示例](../examples/plugin-component/)
|
||||
- [Vite 模块联邦插件文档](https://github.com/originjs/vite-plugin-federation)
|
||||
- [Vite 官方文档](https://vitejs.dev/guide/build.html)
|
||||
- [Origin.js 模块联邦示例](https://github.com/originjs/vite-plugin-federation/tree/main/packages/examples)
|
||||
388
docs/module-federation-guide.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# MoviePilot前端远程模块开发指南
|
||||
|
||||
## 1. 概述
|
||||
|
||||
MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态加载和集成。本文档详细说明如何开发符合要求的远程模块,以便在MoviePilot中作为插件使用。
|
||||
|
||||
关联阅读后端插件开发文档:[第三方插件开发说明](https://github.com/jxxghp/MoviePilot-Plugins/blob/main/README.md)
|
||||
|
||||
|
||||
## 2. 技术要求
|
||||
|
||||
- Node.js 20+
|
||||
- Vue 3
|
||||
- Vite 4+
|
||||
- TypeScript 5+
|
||||
|
||||
## 3. 核心概念
|
||||
|
||||
每个插件需要提供三个标准组件:
|
||||
|
||||
| 组件名称 | 文件名 | 用途 |
|
||||
|---------|-------|------|
|
||||
| Page | Page.vue | 插件详情页面 |
|
||||
| Config | Config.vue | 插件配置页面 |
|
||||
| Dashboard | Dashboard.vue | 仪表板组件 |
|
||||
|
||||
## 4. 快速开始
|
||||
|
||||
### 创建项目
|
||||
|
||||
```bash
|
||||
# 创建项目
|
||||
npm create vite@latest my-plugin -- --template vue-ts
|
||||
|
||||
# 进入项目目录
|
||||
cd my-plugin
|
||||
|
||||
# 安装依赖
|
||||
yarn
|
||||
```
|
||||
|
||||
### 配置vite.config.ts
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import federation from '@originjs/vite-plugin-federation'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
federation({
|
||||
name: 'MyPlugin',
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: {
|
||||
'./Page': './src/components/Page.vue',
|
||||
'./Config': './src/components/Config.vue',
|
||||
'./Dashboard': './src/components/Dashboard.vue',
|
||||
},
|
||||
shared: {
|
||||
vue: {
|
||||
requiredVersion: false,
|
||||
generate: false,
|
||||
},
|
||||
vuetify: {
|
||||
requiredVersion: false,
|
||||
generate: false,
|
||||
singleton: true,
|
||||
},
|
||||
'vuetify/styles': {
|
||||
requiredVersion: false,
|
||||
generate: false,
|
||||
singleton: true,
|
||||
},
|
||||
},
|
||||
format: 'esm'
|
||||
})
|
||||
],
|
||||
build: {
|
||||
target: 'esnext', // 必须设置为esnext以支持顶层await
|
||||
minify: false, // 开发阶段建议关闭混淆
|
||||
cssCodeSplit: true, // 改为true以便能分离样式文件
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '/* 覆盖vuetify样式 */',
|
||||
}
|
||||
},
|
||||
postcss: {
|
||||
plugins: [
|
||||
{
|
||||
postcssPlugin: 'internal:charset-removal',
|
||||
AtRule: {
|
||||
charset: (atRule) => {
|
||||
if (atRule.name === 'charset') {
|
||||
atRule.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
postcssPlugin: 'vuetify-filter',
|
||||
Root(root) {
|
||||
// 过滤掉所有vuetify相关的CSS
|
||||
root.walkRules(rule => {
|
||||
if (rule.selector && (
|
||||
rule.selector.includes('.v-') ||
|
||||
rule.selector.includes('.mdi-'))) {
|
||||
rule.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5001, // 使用不同于主应用的端口
|
||||
cors: true, // 启用CORS
|
||||
origin: 'http://localhost:5001'
|
||||
},
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
## 5. 组件开发规范
|
||||
|
||||
### 5.1 Page组件(详情页面)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 自定义事件,用于通知主应用刷新数据
|
||||
const emit = defineEmits(['action', 'switch', 'close'])
|
||||
|
||||
// 接收API对象
|
||||
const props = defineProps({
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
// 页面逻辑代码...
|
||||
|
||||
// 通知主应用刷新数据
|
||||
function notifyRefresh() {
|
||||
emit('action')
|
||||
}
|
||||
|
||||
// 通知主应用切换到配置页面
|
||||
function notifySwitch() {
|
||||
emit('switch')
|
||||
}
|
||||
|
||||
// 通知主应用关闭当前页面
|
||||
function notifyClose() {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plugin-page">
|
||||
<!-- 插件详情页面操作按钮示例 -->
|
||||
<v-btn @click="notifyRefresh">刷新数据</v-btn>
|
||||
<v-btn @click="notifySwitch">配置插件</v-btn>
|
||||
<v-btn @click="notifyClose">关闭页面</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 5.2 Config组件(配置页面)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 接收初始配置和API对象
|
||||
const props = defineProps({
|
||||
initialConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
// 配置数据
|
||||
const config = ref({...props.initialConfig})
|
||||
|
||||
// 自定义事件,用于保存配置
|
||||
const emit = defineEmits(['save', 'close', 'switch'])
|
||||
|
||||
// 保存配置
|
||||
function saveConfig() {
|
||||
emit('save', config.value)
|
||||
}
|
||||
|
||||
// 通知主应用切换到详情页面
|
||||
function notifySwitch() {
|
||||
emit('switch')
|
||||
}
|
||||
|
||||
// 通知主应用关闭当前页面
|
||||
function notifyClose() {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plugin-config">
|
||||
<!-- 配置表单示例 -->
|
||||
<v-text-field v-model="config.someField" label="配置项"></v-text-field>
|
||||
|
||||
<!-- 保存按钮示例 -->
|
||||
<v-btn color="primary" @click="saveConfig">保存配置</v-btn>
|
||||
|
||||
<!-- 关闭按钮示例 -->
|
||||
<v-btn color="primary" @click="notifyClose">关闭页面</v-btn>
|
||||
|
||||
<!-- 切换按钮示例 -->
|
||||
<v-btn color="primary" @click="notifySwitch">切换到详情页面</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 5.3 Dashboard组件(仪表板)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 接收配置和刷新控制
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
allowRefresh: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
// 仪表板逻辑...
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard-widget">
|
||||
<v-hover>
|
||||
<!-- 仪表板内容 -->
|
||||
<template #default="{ isHovering, props: hoverProps }">
|
||||
<v-card v-bind="hoverProps">
|
||||
<v-card-title>{{ config.title || '仪表板组件' }}</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- 组件内容 -->
|
||||
</v-card-text>
|
||||
<!-- 只在悬停时显示拖拽图标 -->
|
||||
<div v-show="isHovering" class="absolute right-5 top-5">
|
||||
<v-icon class="cursor-move">mdi-drag</v-icon>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-hover>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 6. 构建和部署
|
||||
|
||||
### 构建项目
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
- 将生成的dist文件夹上传到插件后端目录下(默认为`dist/assets`)
|
||||
|
||||
**注意: `__federation_shared_vuetify` 目录以及 `index-`、`date-`、`runtime-` 开头的文件不需要上传**,只需要上传以下命名格式文件:`__federation_*`、`_plugin-vue_export-helper-*`、`remoteEntry.js`
|
||||
|
||||
|
||||
- 在插件的后端python代码中,实现以下方法来集成远程组件:
|
||||
|
||||
```python
|
||||
def get_render_mode() -> Tuple[str, str]:
|
||||
"""
|
||||
获取插件渲染模式
|
||||
:return: 1、渲染模式,支持:vue/vuetify,默认vuetify
|
||||
:return: 2、组件路径,默认 dist/assets
|
||||
"""
|
||||
return "vue", "dist/assets"
|
||||
```
|
||||
|
||||
- 需要在插件前端页面调用后端接口时,通过传入的api模块发起调用,后端api接口声明认证类型为:`bear`
|
||||
```typescript
|
||||
// 演示使用api模块调用插件接口
|
||||
recentItems.value = await props.api.get(`plugin/MyPlugin/history`)
|
||||
```
|
||||
|
||||
```python
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
注册插件API
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"path": "/history",
|
||||
"endpoint": self.get_history,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear", # 认证类型设为bear
|
||||
"summary": "查询历史记录"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## 7. 调试与排错
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **模块无法加载**
|
||||
- 检查网络请求是否成功(状态码200)
|
||||
- 确认文件路径是否正确
|
||||
- 检查CORS跨域设置
|
||||
|
||||
2. **模块加载但组件不显示**
|
||||
- 检查控制台错误信息
|
||||
- 确认组件是否正确导出
|
||||
- 验证共享依赖配置
|
||||
|
||||
3. **"Module name 'vue' does not resolve to a valid URL"**
|
||||
- 检查`shared`配置是否正确
|
||||
- 设置`requiredVersion: false`尝试解决
|
||||
|
||||
4. **"Top-level await is not available"**
|
||||
- 确保`build.target`设置为`esnext`
|
||||
|
||||
## 8. 高级配置
|
||||
|
||||
### 8.1 CSS隔离
|
||||
|
||||
为防止样式冲突,建议使用CSS Modules或scoped样式:
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
/* 组件样式 */
|
||||
</style>
|
||||
```
|
||||
|
||||
### 8.2 共享更多依赖
|
||||
|
||||
如果您的插件需要共享更多依赖,可以扩展shared配置:
|
||||
|
||||
```js
|
||||
shared: {
|
||||
vue: { requiredVersion: false },
|
||||
vuetify: { requiredVersion: false },
|
||||
'@vueuse/core': { requiredVersion: false },
|
||||
pinia: { requiredVersion: false }
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 开发环境测试
|
||||
|
||||
开发期间可以使用以下配置在本地测试:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 5001, // 使用不同于主应用的端口
|
||||
cors: true, // 启用CORS
|
||||
origin: 'http://localhost:5001'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 9. 示例代码
|
||||
|
||||
- [插件远程组件示例](../examples/plugin-component/) - 开发插件组件的完整示例项目
|
||||
- [模块联邦问题排查指南](./federation-troubleshooting.md) - 常见问题排查
|
||||
|
||||
## 10. 参考资料
|
||||
|
||||
- [Vite Plugin Federation](https://github.com/originjs/vite-plugin-federation)
|
||||
- [Vue 3官方文档](https://vuejs.org/)
|
||||
|
||||
---
|
||||
|
||||
如有问题,请提交Issue。
|
||||
7
env.d.ts
vendored
@@ -8,3 +8,10 @@ declare module 'vue-router' {
|
||||
navActiveLink?: RouteLocationRaw
|
||||
}
|
||||
}
|
||||
|
||||
// 支持动态导入远程模块
|
||||
declare module '*' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
42
examples/plugin-component/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# MoviePilot 插件远程组件示例
|
||||
|
||||
这是 MoviePilot 插件远程组件的示例项目,展示了如何正确配置和开发与主应用兼容的远程组件。本示例实现了三个标准组件:Page(详情页面)、Config(配置页面)和Dashboard(仪表板组件)。
|
||||
|
||||
## 1. 开发环境准备
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# 或
|
||||
yarn
|
||||
```
|
||||
|
||||
### 开发模式运行
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# 或
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## 2. 项目结构
|
||||
|
||||
```
|
||||
plugin-component/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── Page.vue # 插件详情页面组件
|
||||
│ │ ├── Config.vue # 插件配置页面组件
|
||||
│ │ └── Dashboard.vue # 插件仪表板组件
|
||||
│ ├── App.vue # 本地开发入口组件
|
||||
│ └── main.js # 本地开发入口文件
|
||||
├── vite.config.js # Vite和模块联邦配置
|
||||
├── index.html # 本地开发HTML入口
|
||||
└── package.json # 依赖配置
|
||||
```
|
||||
|
||||
## 3. 开发指引
|
||||
|
||||
- [模块联邦开发指南](../../docs/module-federation-guide.md)
|
||||
- [模块联邦问题排查指南](../../docs/federation-troubleshooting.md)。
|
||||
24
examples/plugin-component/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MoviePilot插件组件示例</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
23
examples/plugin-component/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "moviepilot-plugin-component",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "3.7.3",
|
||||
"echarts": "^5.4.3",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"@vueuse/core": "^12.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@originjs/vite-plugin-federation": "^1.4.1",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"vite": "^5.4.11"
|
||||
}
|
||||
}
|
||||
128
examples/plugin-component/src/App.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<v-app>
|
||||
<v-app-bar color="primary" app>
|
||||
<v-app-bar-title>MoviePilot插件组件示例</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main>
|
||||
<v-container>
|
||||
<v-tabs v-model="activeTab" bg-color="primary">
|
||||
<v-tab value="page">详情页面</v-tab>
|
||||
<v-tab value="config">配置页面</v-tab>
|
||||
<v-tab value="dashboard">仪表板</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeTab" class="mt-4">
|
||||
<v-window-item value="page">
|
||||
<h2 class="text-h5 mb-4">Page组件</h2>
|
||||
<div class="component-preview">
|
||||
<page-component @action="handleAction"></page-component>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="config">
|
||||
<h2 class="text-h5 mb-4">Config组件</h2>
|
||||
<div class="component-preview">
|
||||
<config-component :initial-config="initialConfig" @save="handleConfigSave"></config-component>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="dashboard">
|
||||
<h2 class="text-h5 mb-4">Dashboard组件</h2>
|
||||
<v-switch v-model="dashboardConfig.attrs.border" label="显示边框" color="primary" class="mb-4"></v-switch>
|
||||
<div class="component-preview">
|
||||
<dashboard-component :config="dashboardConfig" :allow-refresh="true"></dashboard-component>
|
||||
</div>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<v-footer app color="primary" class="text-center d-flex justify-center">
|
||||
<span class="text-white">MoviePilot 模块联邦示例 ©{{ new Date().getFullYear() }}</span>
|
||||
</v-footer>
|
||||
</v-app>
|
||||
|
||||
<!-- 通知弹窗 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="snackbar.timeout">
|
||||
{{ snackbar.text }}
|
||||
<template v-slot:actions>
|
||||
<v-btn variant="text" @click="snackbar.show = false"> 关闭 </v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import PageComponent from './components/Page.vue'
|
||||
import ConfigComponent from './components/Config.vue'
|
||||
import DashboardComponent from './components/Dashboard.vue'
|
||||
|
||||
// 活动标签页
|
||||
const activeTab = ref('page')
|
||||
|
||||
// 配置初始值
|
||||
const initialConfig = {
|
||||
name: '测试插件',
|
||||
description: '这是一个测试配置',
|
||||
enable_notifications: true,
|
||||
update_interval: 30,
|
||||
api_url: 'https://api.example.com',
|
||||
api_key: 'test_api_key_123',
|
||||
concurrent_tasks: 2,
|
||||
tags: ['电影', '测试'],
|
||||
}
|
||||
|
||||
// 仪表板配置
|
||||
const dashboardConfig = reactive({
|
||||
id: 'test_plugin',
|
||||
name: '测试插件',
|
||||
attrs: {
|
||||
title: '仪表板示例',
|
||||
subtitle: '插件数据展示',
|
||||
border: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 通知状态
|
||||
const snackbar = reactive({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
timeout: 3000,
|
||||
})
|
||||
|
||||
// 显示通知
|
||||
function showNotification(text, color = 'success') {
|
||||
snackbar.text = text
|
||||
snackbar.color = color
|
||||
snackbar.show = true
|
||||
}
|
||||
|
||||
// 处理详情页面操作
|
||||
function handleAction() {
|
||||
showNotification('Page组件触发了action事件')
|
||||
}
|
||||
|
||||
// 处理配置保存
|
||||
function handleConfigSave(config) {
|
||||
console.log('配置已保存:', config)
|
||||
showNotification('配置已保存')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 为了使测试应用更美观 */
|
||||
.app-container {
|
||||
block-size: 100vh;
|
||||
inline-size: 100vw;
|
||||
}
|
||||
|
||||
.component-preview {
|
||||
overflow: hidden;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
224
examples/plugin-component/src/components/Config.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="plugin-config">
|
||||
<v-card>
|
||||
<v-card-item>
|
||||
<v-card-title>插件配置</v-card-title>
|
||||
<template #append>
|
||||
<v-btn icon color="primary" variant="text" @click="notifyClose">
|
||||
<v-icon left>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card-item>
|
||||
<v-card-text class="overflow-y-auto">
|
||||
<v-alert v-if="error" type="error" class="mb-4">{{ error }}</v-alert>
|
||||
|
||||
<v-form ref="form" v-model="isFormValid" @submit.prevent="saveConfig">
|
||||
<!-- 基本设置区域 -->
|
||||
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">基本设置</div>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-switch
|
||||
v-model="config.enable"
|
||||
label="启用插件"
|
||||
color="primary"
|
||||
inset
|
||||
hint="启用插件后,插件将开始工作"
|
||||
persistent-hint
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="config.name"
|
||||
label="插件名称"
|
||||
variant="outlined"
|
||||
:rules="[v => !!v || '名称不能为空']"
|
||||
hint="显示在插件列表中的名称"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="config.description"
|
||||
label="插件描述"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
hint="简要说明插件的功能和用途"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- 功能配置区域 -->
|
||||
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">功能配置</div>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="config.update_interval"
|
||||
label="更新频率"
|
||||
:items="updateIntervalOptions"
|
||||
variant="outlined"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- API配置区域 -->
|
||||
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">API设置</div>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="config.api_url"
|
||||
label="API地址"
|
||||
variant="outlined"
|
||||
hint="外部服务API地址"
|
||||
:rules="[v => !v || v.startsWith('http') || '请输入有效的URL']"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="config.api_key"
|
||||
label="API密钥"
|
||||
variant="outlined"
|
||||
:append-inner-icon="showApiKey ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:type="showApiKey ? 'text' : 'password'"
|
||||
@click:append-inner="showApiKey = !showApiKey"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- 高级选项区域 -->
|
||||
<v-expansion-panels variant="accordion">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>高级选项</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-slider
|
||||
v-model="config.concurrent_tasks"
|
||||
label="并发任务数"
|
||||
min="1"
|
||||
max="10"
|
||||
step="1"
|
||||
thumb-label
|
||||
></v-slider>
|
||||
|
||||
<v-combobox
|
||||
v-model="config.tags"
|
||||
label="标签"
|
||||
variant="outlined"
|
||||
chips
|
||||
multiple
|
||||
closable-chips
|
||||
></v-combobox>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="secondary" @click="resetForm">重置</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" :disabled="!isFormValid" @click="saveConfig" :loading="saving">保存配置</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
// 接收初始配置
|
||||
const props = defineProps({
|
||||
initialConfig: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
// 表单状态
|
||||
const form = ref(null)
|
||||
const isFormValid = ref(true)
|
||||
const error = ref(null)
|
||||
const saving = ref(false)
|
||||
const showApiKey = ref(false)
|
||||
|
||||
// 更新频率选项
|
||||
const updateIntervalOptions = [
|
||||
{ text: '5分钟', value: 5 },
|
||||
{ text: '15分钟', value: 15 },
|
||||
{ text: '30分钟', value: 30 },
|
||||
{ text: '1小时', value: 60 },
|
||||
{ text: '2小时', value: 120 },
|
||||
{ text: '6小时', value: 360 },
|
||||
{ text: '12小时', value: 720 },
|
||||
{ text: '1天', value: 1440 },
|
||||
]
|
||||
|
||||
// 配置数据,使用默认值和初始配置合并
|
||||
const defaultConfig = {
|
||||
name: '我的插件',
|
||||
description: '',
|
||||
enable: true,
|
||||
update_interval: 60,
|
||||
api_url: '',
|
||||
api_key: '',
|
||||
concurrent_tasks: 3,
|
||||
tags: [],
|
||||
}
|
||||
|
||||
// 合并默认配置和初始配置
|
||||
const config = reactive({ ...defaultConfig })
|
||||
|
||||
// 初始化配置
|
||||
onMounted(() => {
|
||||
// 加载初始配置
|
||||
if (props.initialConfig) {
|
||||
Object.keys(props.initialConfig).forEach(key => {
|
||||
if (key in config) {
|
||||
config[key] = props.initialConfig[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 自定义事件,用于保存配置
|
||||
const emit = defineEmits(['save', 'close', 'switch'])
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
if (!isFormValid.value) {
|
||||
error.value = '请修正表单错误'
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 模拟API调用等待
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 发送保存事件
|
||||
emit('save', { ...config })
|
||||
} catch (err) {
|
||||
console.error('保存配置失败:', err)
|
||||
error.value = err.message || '保存配置失败'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
Object.keys(defaultConfig).forEach(key => {
|
||||
config[key] = defaultConfig[key]
|
||||
})
|
||||
|
||||
if (form.value) {
|
||||
form.value.resetValidation()
|
||||
}
|
||||
}
|
||||
|
||||
// 通知主应用关闭组件
|
||||
function notifyClose() {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
298
examples/plugin-component/src/components/Dashboard.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div class="dashboard-widget">
|
||||
<v-card v-if="!config?.attrs?.border" flat>
|
||||
<v-card-text class="pa-0">
|
||||
<div class="dashboard-content">
|
||||
<!-- 加载中状态 -->
|
||||
<div v-if="loading" class="d-flex justify-center align-center py-4">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
|
||||
<!-- 数据内容 -->
|
||||
<div v-else>
|
||||
<!-- 数据图表 -->
|
||||
<div v-if="chartData" class="chart-container">
|
||||
<v-chart class="chart" :option="chartOptions" autoresize />
|
||||
</div>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<v-list v-if="items.length" density="compact" class="py-0">
|
||||
<v-list-item v-for="(item, index) in items" :key="index" :title="item.title" :subtitle="item.subtitle">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar :color="getStatusColor(item.status)" size="small">
|
||||
<v-icon size="small" color="white">{{ getStatusIcon(item.status) }}</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<template v-slot:append v-if="item.value">
|
||||
<span class="text-caption">{{ item.value }}</span>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 带边框的卡片 -->
|
||||
<v-card v-else>
|
||||
<v-card-item>
|
||||
<v-card-title>{{ config?.attrs?.title || '仪表板组件' }}</v-card-title>
|
||||
<v-card-subtitle v-if="config?.attrs?.subtitle">{{ config.attrs.subtitle }}</v-card-subtitle>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<!-- 加载中状态 -->
|
||||
<div v-if="loading" class="d-flex justify-center align-center py-4">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
|
||||
<!-- 数据内容 -->
|
||||
<div v-else>
|
||||
<!-- 数据图表 -->
|
||||
<div v-if="chartData" class="chart-container">
|
||||
<v-chart class="chart" :option="chartOptions" autoresize />
|
||||
</div>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<v-list v-if="items.length" density="compact" class="rounded pa-0">
|
||||
<v-list-item v-for="(item, index) in items" :key="index" :title="item.title" :subtitle="item.subtitle">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar :color="getStatusColor(item.status)" size="small">
|
||||
<v-icon size="small" color="white">{{ getStatusIcon(item.status) }}</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<template v-slot:append v-if="item.value">
|
||||
<span class="text-caption">{{ item.value }}</span>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart, PieChart } from 'echarts/charts'
|
||||
import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components'
|
||||
|
||||
// 注册ECharts组件
|
||||
try {
|
||||
use([CanvasRenderer, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent])
|
||||
} catch (e) {
|
||||
console.warn('ECharts components registration failed', e)
|
||||
}
|
||||
|
||||
// 接收仪表板配置
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
allowRefresh: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 组件状态
|
||||
const loading = ref(true)
|
||||
const items = ref([])
|
||||
const chartData = ref(null)
|
||||
let refreshTimer = null
|
||||
|
||||
// 获取状态图标
|
||||
function getStatusIcon(status) {
|
||||
const icons = {
|
||||
'success': 'mdi-check-circle',
|
||||
'warning': 'mdi-alert',
|
||||
'error': 'mdi-alert-circle',
|
||||
'info': 'mdi-information',
|
||||
'running': 'mdi-play-circle',
|
||||
'pending': 'mdi-clock-outline',
|
||||
'completed': 'mdi-check-circle-outline',
|
||||
}
|
||||
return icons[status] || 'mdi-help-circle'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
'success': 'success',
|
||||
'warning': 'warning',
|
||||
'error': 'error',
|
||||
'info': 'info',
|
||||
'running': 'primary',
|
||||
'pending': 'secondary',
|
||||
'completed': 'success',
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
// 图表选项
|
||||
const chartOptions = computed(() => {
|
||||
if (!chartData.value) return {}
|
||||
|
||||
const { type, data } = chartData.value
|
||||
|
||||
if (type === 'line') {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.xAxis,
|
||||
axisLabel: {
|
||||
color: '#888',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#888',
|
||||
},
|
||||
},
|
||||
series: data.series.map(series => ({
|
||||
name: series.name,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: series.data,
|
||||
areaStyle: { opacity: 0.1 },
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'pie') {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: data.name,
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: data.items,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
})
|
||||
|
||||
// 获取仪表板数据
|
||||
async function fetchDashboardData() {
|
||||
if (!props.allowRefresh) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 随机决定显示饼图或折线图
|
||||
const showPie = Math.random() > 0.5
|
||||
|
||||
if (showPie) {
|
||||
// 饼图数据
|
||||
chartData.value = {
|
||||
type: 'pie',
|
||||
data: {
|
||||
name: '文件分布',
|
||||
items: [
|
||||
{ value: Math.floor(Math.random() * 50) + 30, name: '电影' },
|
||||
{ value: Math.floor(Math.random() * 40) + 20, name: '电视剧' },
|
||||
{ value: Math.floor(Math.random() * 30) + 10, name: '动漫' },
|
||||
{ value: Math.floor(Math.random() * 20) + 5, name: '纪录片' },
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// 折线图数据
|
||||
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
chartData.value = {
|
||||
type: 'line',
|
||||
data: {
|
||||
xAxis: days,
|
||||
series: [
|
||||
{
|
||||
name: '下载量',
|
||||
data: days.map(() => Math.floor(Math.random() * 10) + 1),
|
||||
},
|
||||
{
|
||||
name: '完成量',
|
||||
data: days.map(() => Math.floor(Math.random() * 8) + 1),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 生成列表数据
|
||||
const statuses = ['success', 'warning', 'error', 'info', 'running', 'pending', 'completed']
|
||||
items.value = Array.from({ length: 5 }, (_, i) => {
|
||||
const status = statuses[Math.floor(Math.random() * statuses.length)]
|
||||
return {
|
||||
title: `项目 ${i + 1}`,
|
||||
subtitle: `上次更新: ${new Date().toLocaleTimeString()}`,
|
||||
status,
|
||||
value: Math.floor(Math.random() * 100) + '%',
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取仪表板数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置定时刷新
|
||||
function setupRefreshTimer() {
|
||||
if (props.allowRefresh) {
|
||||
// 每30秒刷新一次
|
||||
refreshTimer = setInterval(() => {
|
||||
fetchDashboardData()
|
||||
}, 30000)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchDashboardData()
|
||||
setupRefreshTimer()
|
||||
})
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
169
examples/plugin-component/src/components/Page.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="plugin-page">
|
||||
<v-card>
|
||||
<v-card-item>
|
||||
<v-card-title>{{ title }}</v-card-title>
|
||||
<template #append>
|
||||
<v-btn icon color="primary" variant="text" @click="notifyClose">
|
||||
<v-icon left>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card-item>
|
||||
<v-card-text>
|
||||
<v-alert v-if="error" type="error" class="mb-4">{{ error }}</v-alert>
|
||||
<v-skeleton-loader v-if="loading" type="card"></v-skeleton-loader>
|
||||
<div v-else>
|
||||
<!-- 数据统计展示 -->
|
||||
<v-row v-if="stats">
|
||||
<v-col v-for="(value, key) in stats" :key="key" cols="12" sm="6" md="4">
|
||||
<v-card variant="outlined" class="text-center">
|
||||
<v-card-text>
|
||||
<div class="text-h4 font-weight-bold">{{ value }}</div>
|
||||
<div class="text-subtitle-1">{{ key }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 最近记录展示 -->
|
||||
<div v-if="recentItems && recentItems.length" class="mt-4">
|
||||
<div class="text-h6 mb-2">最近记录</div>
|
||||
<v-timeline density="compact">
|
||||
<v-timeline-item
|
||||
v-for="(item, index) in recentItems"
|
||||
:key="index"
|
||||
:dot-color="getItemColor(item.type)"
|
||||
size="small"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon :color="getItemColor(item.type)" size="small" class="mr-2">
|
||||
{{ getItemIcon(item.type) }}
|
||||
</v-icon>
|
||||
<span class="font-weight-medium">{{ item.title }}</span>
|
||||
</div>
|
||||
<div class="text-caption text-secondary">{{ item.time }}</div>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
</div>
|
||||
|
||||
<!-- 当前状态 -->
|
||||
<div class="mt-4 text-subtitle-2">
|
||||
<div>
|
||||
<strong>状态:</strong>
|
||||
<v-chip size="small" :color="status === 'running' ? 'success' : 'warning'">{{ status }}</v-chip>
|
||||
</div>
|
||||
<div><strong>最后更新:</strong> {{ lastUpdated }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" @click="refreshData" :loading="loading">
|
||||
<v-icon left>mdi-refresh</v-icon>
|
||||
刷新数据
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="notifySwitch">
|
||||
<v-icon left>mdi-cog</v-icon>
|
||||
配置
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 接收初始配置
|
||||
const props = defineProps({
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
// 组件状态
|
||||
const title = ref('插件详情页面')
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const stats = ref(null)
|
||||
const recentItems = ref([])
|
||||
const status = ref('running')
|
||||
const lastUpdated = ref('')
|
||||
|
||||
// 自定义事件,用于通知主应用刷新数据
|
||||
const emit = defineEmits(['action', 'switch', 'close'])
|
||||
|
||||
// 获取状态图标
|
||||
function getItemIcon(type) {
|
||||
const icons = {
|
||||
'movie': 'mdi-movie',
|
||||
'tv': 'mdi-television-classic',
|
||||
'download': 'mdi-download',
|
||||
'error': 'mdi-alert-circle',
|
||||
'success': 'mdi-check-circle',
|
||||
}
|
||||
return icons[type] || 'mdi-information'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
function getItemColor(type) {
|
||||
const colors = {
|
||||
'movie': 'blue',
|
||||
'tv': 'green',
|
||||
'download': 'purple',
|
||||
'error': 'red',
|
||||
'success': 'success',
|
||||
}
|
||||
return colors[type] || 'grey'
|
||||
}
|
||||
|
||||
// 获取和刷新数据
|
||||
async function refreshData() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 模拟数据
|
||||
stats.value = {
|
||||
'电影': Math.floor(Math.random() * 100) + 50,
|
||||
'电视剧': Math.floor(Math.random() * 100) + 30,
|
||||
'动漫': Math.floor(Math.random() * 100) + 20,
|
||||
'纪录片': Math.floor(Math.random() * 100) + 10,
|
||||
'综艺': Math.floor(Math.random() * 100) + 5,
|
||||
}
|
||||
|
||||
// 演示使用api模块调用插件接口
|
||||
recentItems.value = await props.api.get(`plugin/MyPlugin/history`)
|
||||
|
||||
status.value = Math.random() > 0.2 ? 'running' : 'paused'
|
||||
lastUpdated.value = new Date().toLocaleString()
|
||||
} catch (err) {
|
||||
console.error('获取数据失败:', err)
|
||||
error.value = err.message || '获取数据失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 通知主应用组件已更新
|
||||
emit('action')
|
||||
}
|
||||
}
|
||||
|
||||
// 通知主应用切换到配置页面
|
||||
function notifySwitch() {
|
||||
emit('switch')
|
||||
}
|
||||
|
||||
// 通知主应用关闭组件
|
||||
function notifyClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
refreshData()
|
||||
})
|
||||
</script>
|
||||
25
examples/plugin-component/src/main.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
import defaults from './vuetify/defaults'
|
||||
import theme from './vuetify/theme'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// 创建Vuetify实例
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme,
|
||||
defaults
|
||||
})
|
||||
|
||||
// 创建应用
|
||||
const app = createApp(App)
|
||||
|
||||
// 使用插件
|
||||
app.use(vuetify)
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
148
examples/plugin-component/src/vuetify/defaults.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
export default {
|
||||
IconBtn: {
|
||||
icon: true,
|
||||
color: 'default',
|
||||
variant: 'text',
|
||||
VIcon: {
|
||||
size: 24,
|
||||
},
|
||||
},
|
||||
VAlert: {
|
||||
VBtn: {
|
||||
color: undefined,
|
||||
},
|
||||
},
|
||||
VAvatar: {
|
||||
// ℹ️ Remove after next release
|
||||
variant: 'flat',
|
||||
VIcon: {
|
||||
size: 24,
|
||||
},
|
||||
},
|
||||
VBadge: {
|
||||
// set v-badge default color to primary
|
||||
color: 'primary',
|
||||
},
|
||||
VBtn: {
|
||||
// set v-btn default color to primary
|
||||
color: 'primary',
|
||||
elevation: 0,
|
||||
},
|
||||
VCard: {
|
||||
elevation: 0,
|
||||
rounded: 'lg',
|
||||
},
|
||||
VMenu: {
|
||||
elevation: 0,
|
||||
},
|
||||
VChip: {
|
||||
elevation: 0,
|
||||
},
|
||||
VBottomSheet: {
|
||||
elevation: 0,
|
||||
},
|
||||
VDialog: {
|
||||
elevation: 0,
|
||||
rounded: 'lg',
|
||||
},
|
||||
VExpansionPanels: {
|
||||
elevation: 0,
|
||||
},
|
||||
VList: {
|
||||
color: 'primary',
|
||||
elevation: 0,
|
||||
},
|
||||
VListItem: {
|
||||
rounded: 'md',
|
||||
},
|
||||
VPagination: {
|
||||
activeColor: 'primary',
|
||||
},
|
||||
VTabs: {
|
||||
// set v-tabs default color to primary
|
||||
color: 'primary',
|
||||
VSlideGroup: {
|
||||
showArrows: true,
|
||||
},
|
||||
},
|
||||
VTooltip: {
|
||||
// set v-tooltip default location to top
|
||||
location: 'top',
|
||||
},
|
||||
VCheckboxBtn: {
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VCheckbox: {
|
||||
// set v-checkbox default color to primary
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VRadioGroup: {
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VRadio: {
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VSelect: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
menuProps: { elevation: 0 },
|
||||
},
|
||||
VRangeSlider: {
|
||||
// set v-range-slider default color to primary
|
||||
color: 'primary',
|
||||
density: 'comfortable',
|
||||
thumbLabel: true,
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VRating: {
|
||||
// set v-rating default color to primary
|
||||
color: 'rgba(var(--v-theme-on-background),0.23)',
|
||||
activeColor: 'warning',
|
||||
halfIncrements: true,
|
||||
},
|
||||
VProgressCircular: {
|
||||
// set v-progress-circular default color to primary
|
||||
color: 'primary',
|
||||
},
|
||||
VSlider: {
|
||||
// set v-slider default color to primary
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VTextField: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VAutocomplete: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VCombobox: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
menuProps: { elevation: 0 },
|
||||
},
|
||||
VFileInput: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VTextarea: {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
VSwitch: {
|
||||
// set v-switch default color to primary
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
},
|
||||
}
|
||||
216
examples/plugin-component/src/vuetify/theme.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import type { VuetifyOptions } from 'vuetify'
|
||||
|
||||
const theme: VuetifyOptions['theme'] = {
|
||||
defaultTheme: 'light',
|
||||
themes: {
|
||||
light: {
|
||||
dark: false,
|
||||
colors: {
|
||||
'primary': '#9155FD',
|
||||
'secondary': '#8A8D93',
|
||||
'on-secondary': '#FFFFFF',
|
||||
'success': '#56CA00',
|
||||
'info': '#16B1FF',
|
||||
'warning': '#FFB400',
|
||||
'error': '#FF4C51',
|
||||
'on-primary': '#FFFFFF',
|
||||
'on-success': '#FFFFFF',
|
||||
'on-warning': '#FFFFFF',
|
||||
'background': '#F4F5FA',
|
||||
'on-background': '#3A3541',
|
||||
'on-surface': '#3A3541',
|
||||
'grey-50': '#FAFAFA',
|
||||
'grey-100': '#F0F2F8',
|
||||
'grey-200': '#EEEEEE',
|
||||
'grey-300': '#E0E0E0',
|
||||
'grey-400': '#BDBDBD',
|
||||
'grey-500': '#9E9E9E',
|
||||
'grey-600': '#757575',
|
||||
'grey-700': '#616161',
|
||||
'grey-800': '#424242',
|
||||
'grey-900': '#212121',
|
||||
'perfect-scrollbar-thumb': '#DBDADE',
|
||||
'skin-bordered-background': '#FFFFFF',
|
||||
'skin-bordered-surface': '#FFFFFF',
|
||||
},
|
||||
|
||||
variables: {
|
||||
'code-color': '#D400FF',
|
||||
'overlay-scrim-background': '#3A3541',
|
||||
'overlay-scrim-opacity': 0.5,
|
||||
'hover-opacity': 0.04,
|
||||
'focus-opacity': 0.1,
|
||||
'selected-opacity': 0.12,
|
||||
'activated-opacity': 0.1,
|
||||
'pressed-opacity': 0.14,
|
||||
'dragged-opacity': 0.1,
|
||||
'border-color': '#3A3541',
|
||||
'table-header-background': '#F9FAFC',
|
||||
'custom-background': '#F9F8F9',
|
||||
|
||||
// Shadows
|
||||
'shadow-key-umbra-opacity': 'rgba(var(--v-theme-on-surface), 0.08)',
|
||||
'shadow-key-penumbra-opacity': 'rgba(var(--v-theme-on-surface), 0.12)',
|
||||
'shadow-key-ambient-opacity': 'rgba(var(--v-theme-on-surface), 0.04)',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
dark: true,
|
||||
colors: {
|
||||
'primary': '#6E66ED',
|
||||
'secondary': '#8A8D93',
|
||||
'on-secondary': '#FFFFFF',
|
||||
'success': '#56CA00',
|
||||
'info': '#16B1FF',
|
||||
'warning': '#FFB400',
|
||||
'error': '#FF4C51',
|
||||
'on-primary': '#FFFFFF',
|
||||
'on-success': '#FFFFFF',
|
||||
'on-warning': '#FFFFFF',
|
||||
'background': '#0E1116',
|
||||
'on-background': '#E7E3FC',
|
||||
'surface': '#14161F',
|
||||
'on-surface': '#E7E3FC',
|
||||
'grey-50': '#2A2E42',
|
||||
'grey-100': '#474360',
|
||||
'grey-200': '#4A5072',
|
||||
'grey-300': '#5E6692',
|
||||
'grey-400': '#7983BB',
|
||||
'grey-500': '#8692D0',
|
||||
'grey-600': '#AAB3DE',
|
||||
'grey-700': '#B6BEE3',
|
||||
'grey-800': '#CFD3EC',
|
||||
'grey-900': '#E7E9F6',
|
||||
'perfect-scrollbar-thumb': '#4A5072',
|
||||
'skin-bordered-background': '#312d4b',
|
||||
'skin-bordered-surface': '#312d4b',
|
||||
},
|
||||
variables: {
|
||||
'code-color': '#d400ff',
|
||||
'overlay-scrim-background': '#191D21',
|
||||
'overlay-scrim-opacity': 0.6,
|
||||
'hover-opacity': 0.04,
|
||||
'focus-opacity': 0.1,
|
||||
'selected-opacity': 0.12,
|
||||
'activated-opacity': 0.1,
|
||||
'pressed-opacity': 0.14,
|
||||
'dragged-opacity': 0.1,
|
||||
'border-color': '#E7E3FC',
|
||||
'table-header-background': '#14161F',
|
||||
'custom-background': '#373452',
|
||||
// Shadows
|
||||
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
|
||||
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',
|
||||
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
|
||||
},
|
||||
},
|
||||
purple: {
|
||||
dark: true,
|
||||
colors: {
|
||||
'primary': '#9155FD',
|
||||
'secondary': '#8A8D93',
|
||||
'on-secondary': '#FFFFFF',
|
||||
'success': '#56CA00',
|
||||
'info': '#16B1FF',
|
||||
'warning': '#FFB400',
|
||||
'error': '#FF4C51',
|
||||
'on-primary': '#FFFFFF',
|
||||
'on-success': '#FFFFFF',
|
||||
'on-warning': '#FFFFFF',
|
||||
'background': '#28243D',
|
||||
'on-background': '#E7E3FC',
|
||||
'surface': '#312D4B',
|
||||
'on-surface': '#E7E3FC',
|
||||
'grey-50': '#2A2E42',
|
||||
'grey-100': '#474360',
|
||||
'grey-200': '#4A5072',
|
||||
'grey-300': '#5E6692',
|
||||
'grey-400': '#7983BB',
|
||||
'grey-500': '#8692D0',
|
||||
'grey-600': '#AAB3DE',
|
||||
'grey-700': '#B6BEE3',
|
||||
'grey-800': '#CFD3EC',
|
||||
'grey-900': '#E7E9F6',
|
||||
'perfect-scrollbar-thumb': '#4A5072',
|
||||
'skin-bordered-background': '#312d4b',
|
||||
'skin-bordered-surface': '#312d4b',
|
||||
},
|
||||
variables: {
|
||||
'code-color': '#d400ff',
|
||||
'overlay-scrim-background': '#2C2942',
|
||||
'overlay-scrim-opacity': 0.6,
|
||||
'hover-opacity': 0.04,
|
||||
'focus-opacity': 0.1,
|
||||
'selected-opacity': 0.12,
|
||||
'activated-opacity': 0.1,
|
||||
'pressed-opacity': 0.14,
|
||||
'dragged-opacity': 0.1,
|
||||
'border-color': '#E7E3FC',
|
||||
'table-header-background': '#3D3759',
|
||||
'custom-background': '#373452',
|
||||
|
||||
// Shadows
|
||||
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
|
||||
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',
|
||||
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
|
||||
},
|
||||
},
|
||||
transparent: {
|
||||
dark: true,
|
||||
colors: {
|
||||
'primary': '#A370F7',
|
||||
'secondary': '#8A8D93',
|
||||
'on-secondary': '#FFFFFF',
|
||||
'success': '#66BB6A',
|
||||
'info': '#42A5F5',
|
||||
'warning': '#FFA726',
|
||||
'error': '#EF5350',
|
||||
'on-primary': '#FFFFFF',
|
||||
'on-success': '#FFFFFF',
|
||||
'on-warning': '#FFFFFF',
|
||||
'background': '#000000',
|
||||
'on-background': '#E7E3FC',
|
||||
'surface': 'rgba(30, 30, 30, 0.3)',
|
||||
'on-surface': '#E7E3FC',
|
||||
'surface-variant': 'rgba(30, 30, 30, 0.2)',
|
||||
'on-surface-variant': 'rgba(255, 255, 255, 0.65)',
|
||||
'grey-50': 'rgba(42, 46, 66, 0.15)',
|
||||
'grey-100': 'rgba(71, 67, 96, 0.15)',
|
||||
'grey-200': 'rgba(74, 80, 114, 0.15)',
|
||||
'grey-300': 'rgba(94, 102, 146, 0.15)',
|
||||
'grey-400': 'rgba(121, 131, 187, 0.15)',
|
||||
'grey-500': 'rgba(134, 146, 208, 0.15)',
|
||||
'grey-600': 'rgba(170, 179, 222, 0.15)',
|
||||
'grey-700': 'rgba(182, 190, 227, 0.15)',
|
||||
'grey-800': 'rgba(207, 211, 236, 0.15)',
|
||||
'grey-900': 'rgba(231, 233, 246, 0.15)',
|
||||
'perfect-scrollbar-thumb': 'rgba(158, 158, 190, 0.4)',
|
||||
'skin-bordered-background': 'rgba(30, 30, 30, 0.3)',
|
||||
'skin-bordered-surface': 'rgba(30, 30, 30, 0.3)',
|
||||
'card-background': 'rgba(30, 30, 30, 0.3)',
|
||||
},
|
||||
variables: {
|
||||
'code-color': '#6D9EEB',
|
||||
'overlay-scrim-background': '0, 0, 0',
|
||||
'overlay-scrim-opacity': 0.7,
|
||||
'hover-opacity': 0.1,
|
||||
'focus-opacity': 0.15,
|
||||
'selected-opacity': 0.2,
|
||||
'activated-opacity': 0.15,
|
||||
'pressed-opacity': 0.2,
|
||||
'dragged-opacity': 0.15,
|
||||
'border-color': '#E7E3FC',
|
||||
'table-header-background': 'rgba(30, 30, 30, 0.3)',
|
||||
'custom-background': 'rgba(30, 30, 30, 0.3)',
|
||||
'card-background': 'rgba(30, 30, 30, 0.3)',
|
||||
|
||||
// Shadows
|
||||
'shadow-key-umbra-opacity': 'rgba(0, 0, 0, 0.07)',
|
||||
'shadow-key-penumbra-opacity': 'rgba(0, 0, 0, 0.1)',
|
||||
'shadow-key-ambient-opacity': 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default theme
|
||||
79
examples/plugin-component/vite.config.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import federation from '@originjs/vite-plugin-federation'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
federation({
|
||||
name: 'MyPlugin',
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: {
|
||||
'./Page': './src/components/Page.vue',
|
||||
'./Config': './src/components/Config.vue',
|
||||
'./Dashboard': './src/components/Dashboard.vue',
|
||||
},
|
||||
shared: {
|
||||
vue: {
|
||||
requiredVersion: false,
|
||||
generate: false,
|
||||
},
|
||||
vuetify: {
|
||||
requiredVersion: false,
|
||||
generate: false,
|
||||
singleton: true,
|
||||
},
|
||||
'vuetify/styles': {
|
||||
requiredVersion: false,
|
||||
generate: false,
|
||||
singleton: true,
|
||||
},
|
||||
},
|
||||
format: 'esm'
|
||||
})
|
||||
],
|
||||
build: {
|
||||
target: 'esnext', // 必须设置为esnext以支持顶层await
|
||||
minify: false, // 开发阶段建议关闭混淆
|
||||
cssCodeSplit: true, // 改为true以便能分离样式文件
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '/* 覆盖vuetify样式 */',
|
||||
}
|
||||
},
|
||||
postcss: {
|
||||
plugins: [
|
||||
{
|
||||
postcssPlugin: 'internal:charset-removal',
|
||||
AtRule: {
|
||||
charset: (atRule) => {
|
||||
if (atRule.name === 'charset') {
|
||||
atRule.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
postcssPlugin: 'vuetify-filter',
|
||||
Root(root) {
|
||||
// 过滤掉所有vuetify相关的CSS
|
||||
root.walkRules(rule => {
|
||||
if (rule.selector && (
|
||||
rule.selector.includes('.v-') ||
|
||||
rule.selector.includes('.mdi-'))) {
|
||||
rule.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5001, // 使用不同于主应用的端口
|
||||
cors: true, // 启用CORS
|
||||
origin: 'http://localhost:5001'
|
||||
},
|
||||
})
|
||||
561
examples/plugin-component/yarn.lock
Normal file
@@ -0,0 +1,561 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/helper-string-parser@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
||||
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
|
||||
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
|
||||
|
||||
"@babel/parser@^7.25.3":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.1.tgz#c55d5bed74449d1223701f1869b9ee345cc94cc9"
|
||||
integrity sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==
|
||||
dependencies:
|
||||
"@babel/types" "^7.27.1"
|
||||
|
||||
"@babel/types@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560"
|
||||
integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
|
||||
"@esbuild/aix-ppc64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
|
||||
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
|
||||
|
||||
"@esbuild/android-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
|
||||
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
|
||||
|
||||
"@esbuild/android-arm@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
|
||||
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
|
||||
|
||||
"@esbuild/android-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
|
||||
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
|
||||
|
||||
"@esbuild/darwin-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
|
||||
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
|
||||
|
||||
"@esbuild/darwin-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
|
||||
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
|
||||
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
|
||||
|
||||
"@esbuild/freebsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
|
||||
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
|
||||
|
||||
"@esbuild/linux-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
|
||||
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
|
||||
|
||||
"@esbuild/linux-arm@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
|
||||
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
|
||||
|
||||
"@esbuild/linux-ia32@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
|
||||
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
|
||||
|
||||
"@esbuild/linux-loong64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
|
||||
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
|
||||
|
||||
"@esbuild/linux-mips64el@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
|
||||
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
|
||||
|
||||
"@esbuild/linux-ppc64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
|
||||
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
|
||||
|
||||
"@esbuild/linux-riscv64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
|
||||
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
|
||||
|
||||
"@esbuild/linux-s390x@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
|
||||
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
|
||||
|
||||
"@esbuild/linux-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
|
||||
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
|
||||
|
||||
"@esbuild/netbsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
|
||||
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
|
||||
|
||||
"@esbuild/openbsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
|
||||
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
|
||||
|
||||
"@esbuild/sunos-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
|
||||
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
|
||||
|
||||
"@esbuild/win32-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
|
||||
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
|
||||
|
||||
"@esbuild/win32-ia32@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
|
||||
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
|
||||
|
||||
"@esbuild/win32-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
|
||||
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
|
||||
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
|
||||
|
||||
"@originjs/vite-plugin-federation@^1.4.1":
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@originjs/vite-plugin-federation/-/vite-plugin-federation-1.4.1.tgz#e6abc8f18f2cf82783eb87853f4d03e6358b43c2"
|
||||
integrity sha512-Uo08jW5pj1t58OUKuZNkmzcfTN2pqeVuAWCCiKf/75/oll4Efq4cHOqSE1FXMlvwZNGDziNdDyBbQ5IANem3CQ==
|
||||
dependencies:
|
||||
estree-walker "^3.0.2"
|
||||
magic-string "^0.27.0"
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz#c228d00a41f0dbd6fb8b7ea819bbfbf1c1157a10"
|
||||
integrity sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz#e2b38d0c912169fd55d7e38d723aada208d37256"
|
||||
integrity sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz#1fddb3690f2ae33df16d334c613377f05abe4878"
|
||||
integrity sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz#818298d11c8109e1112590165142f14be24b396d"
|
||||
integrity sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz#91a28dc527d5bed7f9ecf0e054297b3012e19618"
|
||||
integrity sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz#28acadefa76b5c7bede1576e065b51d335c62c62"
|
||||
integrity sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz#819691464179cbcd9a9f9d3dc7617954840c6186"
|
||||
integrity sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz#d149207039e4189e267e8724050388effc80d704"
|
||||
integrity sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz#fa72ebddb729c3c6d88973242f1a2153c83e86ec"
|
||||
integrity sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz#2054216e34469ab8765588ebf343d531fc3c9228"
|
||||
integrity sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz#818de242291841afbfc483a84f11e9c7a11959bc"
|
||||
integrity sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz#0bb4cb8fc4a2c635f68c1208c924b2145eb647cb"
|
||||
integrity sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz#4b3b8e541b7b13e447ae07774217d98c06f6926d"
|
||||
integrity sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz#e065405e67d8bd64a7d0126c931bd9f03910817f"
|
||||
integrity sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz#dda3265bbbfe16a5d0089168fd07f5ebb2a866fe"
|
||||
integrity sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz#90993269b8b995b4067b7b9d72ff1c360ef90a17"
|
||||
integrity sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz#fdf5b09fd121eb8d977ebb0fda142c7c0167b8de"
|
||||
integrity sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz#6397e1e012db64dfecfed0774cb9fcf89503d716"
|
||||
integrity sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz#df0991464a52a35506103fe18d29913bf8798a0c"
|
||||
integrity sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.40.2":
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz#8dae04d01a2cbd84d6297d99356674c6b993f0fc"
|
||||
integrity sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==
|
||||
|
||||
"@types/estree@1.0.7", "@types/estree@^1.0.0":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
|
||||
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
|
||||
|
||||
"@types/web-bluetooth@^0.0.21":
|
||||
version "0.0.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63"
|
||||
integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
|
||||
|
||||
"@vitejs/plugin-vue@^4.4.0":
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz#057d2ded94c4e71b94e9814f92dcd9306317aa46"
|
||||
integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==
|
||||
|
||||
"@vue/compiler-core@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05"
|
||||
integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.25.3"
|
||||
"@vue/shared" "3.5.13"
|
||||
entities "^4.5.0"
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
"@vue/compiler-dom@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58"
|
||||
integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/compiler-sfc@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46"
|
||||
integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.25.3"
|
||||
"@vue/compiler-core" "3.5.13"
|
||||
"@vue/compiler-dom" "3.5.13"
|
||||
"@vue/compiler-ssr" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.11"
|
||||
postcss "^8.4.48"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
"@vue/compiler-ssr@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba"
|
||||
integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/reactivity@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f"
|
||||
integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==
|
||||
dependencies:
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/runtime-core@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz#1fafa4bf0b97af0ebdd9dbfe98cd630da363a455"
|
||||
integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/runtime-dom@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz#610fc795de9246300e8ae8865930d534e1246215"
|
||||
integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.13"
|
||||
"@vue/runtime-core" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
csstype "^3.1.3"
|
||||
|
||||
"@vue/server-renderer@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz#429ead62ee51de789646c22efe908e489aad46f7"
|
||||
integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/shared@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
|
||||
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
|
||||
|
||||
"@vueuse/core@^12.4.0":
|
||||
version "12.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-12.8.2.tgz#007c6dd29a7d1f6933e916e7a2f8ef3c3f968eaa"
|
||||
integrity sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==
|
||||
dependencies:
|
||||
"@types/web-bluetooth" "^0.0.21"
|
||||
"@vueuse/metadata" "12.8.2"
|
||||
"@vueuse/shared" "12.8.2"
|
||||
vue "^3.5.13"
|
||||
|
||||
"@vueuse/metadata@12.8.2":
|
||||
version "12.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-12.8.2.tgz#6cb3a4e97cdcf528329eebc1bda73cd7f64318d3"
|
||||
integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==
|
||||
|
||||
"@vueuse/shared@12.8.2":
|
||||
version "12.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.8.2.tgz#b9e4611d0603629c8e151f982459da394e22f930"
|
||||
integrity sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==
|
||||
dependencies:
|
||||
vue "^3.5.13"
|
||||
|
||||
csstype@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
|
||||
echarts@^5.4.3:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.6.0.tgz#2377874dca9fb50f104051c3553544752da3c9d6"
|
||||
integrity sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==
|
||||
dependencies:
|
||||
tslib "2.3.0"
|
||||
zrender "5.6.1"
|
||||
|
||||
entities@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
esbuild@^0.21.3:
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
|
||||
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
|
||||
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"
|
||||
|
||||
estree-walker@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||
|
||||
estree-walker@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
|
||||
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.0"
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
magic-string@^0.27.0:
|
||||
version "0.27.0"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
|
||||
integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.4.13"
|
||||
|
||||
magic-string@^0.30.11:
|
||||
version "0.30.17"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
||||
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||
|
||||
nanoid@^3.3.8:
|
||||
version "3.3.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||
|
||||
picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
postcss@^8.4.43, postcss@^8.4.48:
|
||||
version "8.5.3"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb"
|
||||
integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
|
||||
dependencies:
|
||||
nanoid "^3.3.8"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
resize-detector@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/resize-detector/-/resize-detector-0.3.0.tgz#fe495112e184695500a8f51e0389f15774cb1cfc"
|
||||
integrity sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==
|
||||
|
||||
rollup@^4.20.0:
|
||||
version "4.40.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.2.tgz#778e88b7a197542682b3e318581f7697f55f0619"
|
||||
integrity sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.7"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.40.2"
|
||||
"@rollup/rollup-android-arm64" "4.40.2"
|
||||
"@rollup/rollup-darwin-arm64" "4.40.2"
|
||||
"@rollup/rollup-darwin-x64" "4.40.2"
|
||||
"@rollup/rollup-freebsd-arm64" "4.40.2"
|
||||
"@rollup/rollup-freebsd-x64" "4.40.2"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.40.2"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.40.2"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.40.2"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.40.2"
|
||||
"@rollup/rollup-linux-loongarch64-gnu" "4.40.2"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.40.2"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.40.2"
|
||||
"@rollup/rollup-linux-riscv64-musl" "4.40.2"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.40.2"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.40.2"
|
||||
"@rollup/rollup-linux-x64-musl" "4.40.2"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.40.2"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.40.2"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.40.2"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
source-map-js@^1.2.0, source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
tslib@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
vite@^5.4.11:
|
||||
version "5.4.19"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959"
|
||||
integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==
|
||||
dependencies:
|
||||
esbuild "^0.21.3"
|
||||
postcss "^8.4.43"
|
||||
rollup "^4.20.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vue-demi@^0.13.11:
|
||||
version "0.13.11"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99"
|
||||
integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==
|
||||
|
||||
vue-echarts@^6.6.1:
|
||||
version "6.7.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-echarts/-/vue-echarts-6.7.3.tgz#30efafc51a4a9de1b8117d3b63e74b0c761ff3ba"
|
||||
integrity sha512-vXLKpALFjbPphW9IfQPOVfb1KjGZ/f8qa/FZHi9lZIWzAnQC1DgnmEK3pJgEkyo6EP7UnX6Bv/V3Ke7p+qCNXA==
|
||||
dependencies:
|
||||
resize-detector "^0.3.0"
|
||||
vue-demi "^0.13.11"
|
||||
|
||||
vue@^3.5.13:
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"
|
||||
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.5.13"
|
||||
"@vue/compiler-sfc" "3.5.13"
|
||||
"@vue/runtime-dom" "3.5.13"
|
||||
"@vue/server-renderer" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
vuetify@3.7.3:
|
||||
version "3.7.3"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.7.3.tgz#0e89f7f0298d452510bcbc01b0e9b53a5ce6e883"
|
||||
integrity sha512-bpuvBpZl1/+nLlXDgdVXekvMNR6W/ciaoa8CYlpeAzAARbY8zUFSoBq05JlLhkIHI58AnzKVy4c09d0OtfYAPg==
|
||||
|
||||
zrender@5.6.1:
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.6.1.tgz#e08d57ecf4acac708c4fcb7481eb201df7f10a6b"
|
||||
integrity sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==
|
||||
dependencies:
|
||||
tslib "2.3.0"
|
||||
506
index.html
@@ -1,211 +1,359 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="
|
||||
<html lang="zh-CN" style="
|
||||
overflow: hidden auto;
|
||||
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
--safe-area-inset-bottom: env(safe-area-inset-bottom);
|
||||
--safe-area-inset-top: env(safe-area-inset-top);
|
||||
background: var(--initial-loader-bg, #fff);
|
||||
">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
|
||||
<title>MoviePilot</title>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 核心viewport设置 - 针对PWA优化 -->
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=no, interactive-widget=resizes-content" />
|
||||
|
||||
<!-- 防止缩放和选择,提供原生应用体验 -->
|
||||
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
|
||||
|
||||
<!-- 基础信息 -->
|
||||
<meta name="description" content="MoviePilot - 智能影视媒体库管理工具" />
|
||||
<meta name="author" content="MoviePilot" />
|
||||
<meta name="keywords" content="MoviePilot,影视,媒体库,管理" />
|
||||
|
||||
<!-- 安全和隐私 -->
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
|
||||
<!-- PWA - 基础图标 -->
|
||||
<link rel="icon" type="image/png" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" sizes="any" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
|
||||
<!-- iOS Safari PWA 优化 -->
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.png" />
|
||||
|
||||
<!-- iOS Safari 全屏模式 -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
|
||||
<meta name="description" content="MoviePilot" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="referrer" content="never" />
|
||||
<meta name="msapplication-TileColor" content="#7D34FD" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
|
||||
<!-- iOS Safari 防止自动识别 -->
|
||||
<meta name="apple-mobile-web-app-orientations" content="portrait" />
|
||||
|
||||
<!-- Android Chrome PWA 优化 -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="mobile-web-app-title" content="MoviePilot" />
|
||||
|
||||
<!-- Microsoft Windows PWA -->
|
||||
<meta name="msapplication-TileColor" content="#0E1116" />
|
||||
<meta name="msapplication-TileImage" content="/android-chrome-192x192.png" />
|
||||
<meta name="msapplication-config" content="none" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<meta name="msapplication-navbutton-color" content="#0E1116" />
|
||||
|
||||
<!-- 主题色彩 - 适配深色和浅色模式 -->
|
||||
<meta name="theme-color" content="#0E1116" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
|
||||
<!-- 屏幕方向锁定 -->
|
||||
<meta name="screen-orientation" content="portrait" />
|
||||
<meta name="x5-orientation" content="portrait" />
|
||||
<meta name="x5-fullscreen" content="true" />
|
||||
<meta name="x5-page-mode" content="app" />
|
||||
|
||||
<!-- UC浏览器优化 -->
|
||||
<meta name="browsermode" content="application" />
|
||||
<meta name="wap-font-scale" content="no" />
|
||||
|
||||
<!-- 360浏览器优化 -->
|
||||
<meta name="renderer" content="webkit" />
|
||||
|
||||
<!-- 触摸优化 -->
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
|
||||
<!-- 缓存控制 -->
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
|
||||
<!-- DNS预解析和预连接 -->
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="//cdn.jsdelivr.net" />
|
||||
<link rel="dns-prefetch" href="//image.tmdb.org" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||
|
||||
<style>
|
||||
#app {
|
||||
min-block-size: 100%;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
#loading-bg {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
display: block;
|
||||
background: var(--initial-loader-bg, #fff);
|
||||
block-size: 100vh;
|
||||
inline-size: 100vw;
|
||||
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
position: absolute;
|
||||
inset-block-start: 35%;
|
||||
inset-inline-start: calc(50% - 5rem);
|
||||
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
|
||||
}
|
||||
|
||||
.loading-complete .loading-logo {
|
||||
filter: blur(10px);
|
||||
opacity: 0;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.loading-complete {
|
||||
filter: blur(15px);
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
block-size: 55px;
|
||||
inline-size: 55px;
|
||||
inset-block-start: 80%;
|
||||
inset-inline-start: calc(50% - 27.5px);
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
|
||||
.loading-complete .loading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.loading .effect-1,
|
||||
.loading .effect-2,
|
||||
.loading .effect-3 {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
block-size: 100%;
|
||||
border-inline-start: 3px solid var(--initial-loader-color, #eee);
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.loading .effect-1 {
|
||||
animation: rotate 1s ease infinite;
|
||||
}
|
||||
|
||||
.loading .effect-2 {
|
||||
animation: rotate-opacity 1s ease infinite 0.1s;
|
||||
}
|
||||
|
||||
.loading .effect-3 {
|
||||
animation: rotate-opacity 1s ease infinite 0.2s;
|
||||
}
|
||||
|
||||
.loading .effects {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-opacity {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
/* 超时通知样式 */
|
||||
#loading-timeout {
|
||||
position: absolute;
|
||||
z-index: 2500;
|
||||
display: none;
|
||||
inset-block-end: 20px;
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
#timeout-btn {
|
||||
color: var(--initial-loader-color, #9155FD);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
margin-inline-start: 8px;
|
||||
border-bottom: 1px solid var(--initial-loader-color, #9155FD);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
// 检测系统主题是否为深色模式
|
||||
function checkPrefersColorSchemeIsDark() {
|
||||
try {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 主题色彩初始化
|
||||
let loaderColor = localStorage.getItem('materio-initial-loader-bg')
|
||||
let primaryColor = localStorage.getItem('materio-initial-loader-color')
|
||||
|
||||
// 检查主题设置
|
||||
const savedTheme = localStorage.getItem('theme') || 'auto'
|
||||
const isAutoTheme = savedTheme === 'auto'
|
||||
|
||||
// 如果是自动主题或者没有保存的背景色,根据系统主题设置背景色
|
||||
if (isAutoTheme || !loaderColor) {
|
||||
loaderColor = checkPrefersColorSchemeIsDark() ? '#0E1116' : '#FFFFFF'
|
||||
}
|
||||
if (!primaryColor) {
|
||||
primaryColor = '#9155FD'
|
||||
}
|
||||
|
||||
// 应用主题色彩
|
||||
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
|
||||
// 状态栏适配
|
||||
if (window.navigator.standalone) {
|
||||
document.documentElement.style.setProperty('--status-bar-height', '20px')
|
||||
}
|
||||
|
||||
// 安全区域适配
|
||||
function updateSafeArea() {
|
||||
const safeAreaTop = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)')
|
||||
const safeAreaBottom = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-bottom)',
|
||||
)
|
||||
|
||||
if (safeAreaTop) document.documentElement.style.setProperty('--safe-area-top', safeAreaTop)
|
||||
if (safeAreaBottom) document.documentElement.style.setProperty('--safe-area-bottom', safeAreaBottom)
|
||||
}
|
||||
|
||||
updateSafeArea()
|
||||
window.addEventListener('resize', updateSafeArea)
|
||||
window.addEventListener('orientationchange', updateSafeArea)
|
||||
|
||||
// 清除缓存处理逻辑
|
||||
window.clearAndReload = async function() {
|
||||
try {
|
||||
// 1. 清除所有缓存
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys()
|
||||
await Promise.all(cacheNames.map(name => caches.delete(name)))
|
||||
console.log('[VersionChecker] 已清除所有缓存')
|
||||
}
|
||||
// 2. 注销 Service Worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations()
|
||||
await Promise.all(registrations.map(registration => registration.unregister()))
|
||||
console.log('[VersionChecker] 已注销所有 Service Worker')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[VersionChecker] 清除缓存时出错:', e)
|
||||
} finally {
|
||||
// 3. 重载页面
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('_t', Date.now().toString())
|
||||
window.location.replace(url.pathname + url.search + url.hash)
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(function() {
|
||||
const timeoutEl = document.getElementById('loading-timeout');
|
||||
if (timeoutEl) {
|
||||
// 适配多语言
|
||||
const lang = navigator.language || 'zh-CN';
|
||||
const messages = {
|
||||
'zh-CN': {
|
||||
text: '页面加载似乎遇到了阻碍,请尝试',
|
||||
btn: '清除缓存'
|
||||
},
|
||||
'zh-TW': {
|
||||
text: '頁面載入似乎遇到了阻礙,請嘗試',
|
||||
btn: '清除快取'
|
||||
},
|
||||
'en-US': {
|
||||
text: 'Page loading seems to be blocked, please try',
|
||||
btn: 'Clear Cache'
|
||||
}
|
||||
};
|
||||
|
||||
// 默认匹配前缀,如 en-GB 匹配 en-US 的逻辑
|
||||
let msg = messages['zh-CN'];
|
||||
if (lang.startsWith('zh-TW') || lang.startsWith('zh-HK')) {
|
||||
msg = messages['zh-TW'];
|
||||
} else if (lang.startsWith('en')) {
|
||||
msg = messages['en-US'];
|
||||
}
|
||||
|
||||
const textNode = document.createTextNode(msg.text + ' ');
|
||||
const btnLink = document.createElement('a');
|
||||
btnLink.href = 'javascript:void(0)';
|
||||
btnLink.id = 'timeout-btn';
|
||||
btnLink.onclick = window.clearAndReload;
|
||||
btnLink.textContent = msg.btn;
|
||||
|
||||
timeoutEl.innerHTML = '';
|
||||
timeoutEl.appendChild(textNode);
|
||||
timeoutEl.appendChild(btnLink);
|
||||
timeoutEl.style.display = 'block';
|
||||
}
|
||||
}, 15000); // 15秒后显示超时提示
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body style="margin: 0">
|
||||
<body style="margin: 0; overflow: hidden; overscroll-behavior: none; -webkit-overflow-scrolling: touch">
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg width="160px" height="160px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2">
|
||||
<style>
|
||||
/* 添加SVG内部的动画样式 */
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
filter: drop-shadow(0 0 3px rgba(141, 81, 249, 0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: drop-shadow(0 0 6px rgba(141, 81, 249, 0.6));
|
||||
}
|
||||
}
|
||||
|
||||
/* 为各个元素添加动画 */
|
||||
#a2-c {
|
||||
filter: drop-shadow(0 0 5px rgba(141, 81, 249, 0.3));
|
||||
animation: glow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
path {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 错开不同元素的动画开始时间 */
|
||||
g:nth-child(2) path {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
g:nth-child(3) path {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
g:nth-child(4) path {
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
|
||||
g:nth-child(5) path {
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
</style>
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill: none" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<path
|
||||
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
|
||||
style="fill: url(#_Linear1)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
|
||||
style="fill: url(#_Linear2)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
|
||||
style="fill: url(#_Linear3)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
|
||||
style="fill: rgb(165, 118, 255)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
|
||||
style="fill: url(#_Linear4)" />
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path
|
||||
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
style="fill: rgb(104, 0, 197)" />
|
||||
<clipPath id="_clip5">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path
|
||||
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
|
||||
style="fill: url(#_Linear6)" />
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
|
||||
style="fill: url(#_Radial7)" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)">
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)">
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<img src="/logo.svg" alt="MoviePilot" width="160px" height="160px" />
|
||||
</div>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
<!-- 超时提示 - 默认隐藏 -->
|
||||
<div id="loading-timeout"></div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
23
package.json
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.4.3",
|
||||
"version": "2.9.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"prebuild": "npm run build:icons",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 5050",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
@@ -27,6 +28,7 @@
|
||||
"@fullcalendar/timegrid": "^6.1.15",
|
||||
"@fullcalendar/vue3": "^6.1.15",
|
||||
"@iconify/utils": "^2.2.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.2",
|
||||
@@ -40,41 +42,53 @@
|
||||
"ace-builds": "^1.37.4",
|
||||
"apexcharts": "^4.0.0",
|
||||
"axios": "^1.7.9",
|
||||
"body-scroll-lock": "^3.1.5",
|
||||
"colorthief": "^2.6.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.2",
|
||||
"express-http-proxy": "^2.1.1",
|
||||
"http-proxy-middleware": "^3.0.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.1",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"sass": "^1.83.4",
|
||||
"tailwindcss": "^ 3.4.17",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-toast-notification": "^3.1.3",
|
||||
"vue-toastification": "^2.0.0-rc.5",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-apexcharts": "^1.8.0",
|
||||
"vue3-perfect-scrollbar": "^2.0.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "3.7.3",
|
||||
"vuetify-use-dialog": "^0.6.11",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/line-md": "^1.2.13",
|
||||
"@iconify-json/lucide": "^1.2.85",
|
||||
"@iconify-json/material-symbols": "^1.2.51",
|
||||
"@iconify-json/mdi": "^1.1.52",
|
||||
"@iconify/tools": "^4.0.4",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||
"@originjs/vite-plugin-federation": "^1.4.1",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@types/body-scroll-lock": "^3.1.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/markdown-it-link-attributes": "^3.0.5",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^20.1.4",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
||||
"@typescript-eslint/parser": "^8.20.0",
|
||||
@@ -104,6 +118,7 @@
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-pages": "^0.32.1",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-plugin-vuetify": "2.0.4",
|
||||
"vue-shepherd": "^4.1.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 102 KiB |
@@ -1,97 +0,0 @@
|
||||
#loading-bg {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
display: block;
|
||||
background: var(--initial-loader-bg, #fff);
|
||||
block-size: 100vh;
|
||||
inline-size: 100vw;
|
||||
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
position: absolute;
|
||||
inset-block-start: 35%;
|
||||
inset-inline-start: calc(50% - 5rem);
|
||||
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
|
||||
}
|
||||
|
||||
/* 添加logo完成动画 - 放大虚化效果 */
|
||||
.loading-complete .loading-logo {
|
||||
filter: blur(10px);
|
||||
opacity: 0;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
/* 添加加载背景消失动画 - 放大虚化效果 */
|
||||
.loading-complete {
|
||||
filter: blur(15px);
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
block-size: 55px;
|
||||
inline-size: 55px;
|
||||
inset-block-start: 80%;
|
||||
inset-inline-start: calc(50% - 27.5px);
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
|
||||
/* 完成时隐藏加载动画 */
|
||||
.loading-complete .loading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.loading .effect-1,
|
||||
.loading .effect-2,
|
||||
.loading .effect-3 {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
block-size: 100%;
|
||||
border-inline-start: 3px solid var(--initial-loader-color, #eee);
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.loading .effect-1 {
|
||||
animation: rotate 1s ease infinite;
|
||||
}
|
||||
|
||||
.loading .effect-2 {
|
||||
animation: rotate-opacity 1s ease infinite 0.1s;
|
||||
}
|
||||
|
||||
.loading .effect-3 {
|
||||
animation: rotate-opacity 1s ease infinite 0.2s;
|
||||
}
|
||||
|
||||
.loading .effects {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-opacity {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
53
public/logo.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<svg width="3em" height="3em" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill:none;"/>
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<path d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z" style="fill:url(#_Linear1);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z" style="fill:url(#_Linear2);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z" style="fill:rgb(141,81,249);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z" style="fill:url(#_Linear3);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z" style="fill:rgb(165,118,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z" style="fill:url(#_Linear4);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z" style="fill:rgb(141,81,249);"/>
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" style="fill:rgb(104,0,197);"/>
|
||||
<clipPath id="_clip5">
|
||||
<path d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z" style="fill:url(#_Linear6);"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z" style="fill:rgb(141,81,249);"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z" style="fill:url(#_Radial7);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)"><stop offset="0" style="stop-color:rgb(141,81,249);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(116,50,223);stop-opacity:1"/></linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)"><stop offset="0" style="stop-color:rgb(141,81,249);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(116,50,223);stop-opacity:1"/></linearGradient>
|
||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)"><stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0"/></linearGradient>
|
||||
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"><stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0"/></linearGradient>
|
||||
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"><stop offset="0" style="stop-color:rgb(116,50,223);stop-opacity:1"/><stop offset="0.51" style="stop-color:rgb(110,38,217);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(91,0,197);stop-opacity:1"/></linearGradient>
|
||||
<radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"><stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0"/></radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
160
public/offline.html
Normal file
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>MoviePilot - 离线</title>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #9155FD;
|
||||
--surface-color: #FFFFFF;
|
||||
--text-color: #333333;
|
||||
--border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--surface-color: #0E1116;
|
||||
--text-color: #FFFFFF;
|
||||
--border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: var(--surface-color);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.offline-container {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
background: var(--surface-color);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1), 0 0 0 1px var(--border-color);
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 32px;
|
||||
background: rgba(145, 85, 253, 0.1);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
fill: var(--primary-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 32px;
|
||||
font-size: 1rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.retry-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(145, 85, 253, 0.1);
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #EF5350;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="offline-container">
|
||||
<div class="icon-wrapper">
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path d="M12,2.03C17.73,2.5 22,7.08 22,12.75C22,13.84 21.79,14.89 21.4,15.86L19.53,14C19.5,13.83 19.5,13.67 19.5,13.5A2.5,2.5 0 0,0 17,11A2.5,2.5 0 0,0 14.5,13.5A2.5,2.5 0 0,0 17,16A2.5,2.5 0 0,0 19.5,13.5C19.5,13.67 19.5,13.83 19.53,14L21.4,15.86C20.04,19.09 16.9,21.47 13.19,21.97L11.75,20.53C11.83,20.5 11.92,20.5 12,20.5A2.5,2.5 0 0,0 14.5,18A2.5,2.5 0 0,0 12,15.5A2.5,2.5 0 0,0 9.5,18C9.5,18.08 9.5,18.17 9.53,18.25L7.66,16.38C7.25,15.96 6.86,15.5 6.5,15H8.17C8.06,14.7 8,14.35 8,14A3,3 0 0,1 11,11A3,3 0 0,1 14,14C14,14.35 13.94,14.7 13.83,15H15.5C15.14,15.5 14.75,15.96 14.34,16.38L12.47,14.5C12.5,14.42 12.5,14.33 12.47,14.25L10.6,12.38C10.18,11.97 9.72,11.59 9.23,11.25L7.36,9.38C6.94,8.96 6.5,8.61 6,8.31V6.64L4.14,4.78C3.6,5.55 3.17,6.4 2.86,7.31L1,5.45V4.46L2.05,3.41C2.5,2.86 3.05,2.41 3.66,2.06L20,18.4L18.73,19.67L12.47,13.41L11.75,20.53C11.83,20.5 11.92,20.5 12,20.5A2.5,2.5 0 0,0 14.5,18A2.5,2.5 0 0,0 12,15.5A2.5,2.5 0 0,0 9.5,18C9.5,18.08 9.5,18.17 9.53,18.25L7.66,16.38C7.25,15.96 6.86,15.5 6.5,15H8.17C8.06,14.7 8,14.35 8,14A3,3 0 0,1 11,11A3,3 0 0,1 14,14C14,14.35 13.94,14.7 13.83,15H15.5C15.14,15.5 14.75,15.96 14.34,16.38L2.46,4.5C3.5,3.17 4.9,2.15 6.5,1.58V3.25C5.43,3.7 4.47,4.33 3.66,5.11L2.61,6.16V8.03C3.16,7.33 3.82,6.73 4.57,6.25V8.31C3.57,9.14 2.75,10.19 2.21,11.39L1,10.18V8.65C1.5,6.16 3.03,4.03 5.11,2.71L6.39,4C8.97,2.73 12.03,2.24 14.97,3.03L16.84,4.9C18.17,5.86 19.25,7.16 19.94,8.68L18.07,6.81C17.07,5.5 15.66,4.5 14,4.04V5.71C15.93,6.17 17.5,7.53 18.33,9.3L16.46,7.43C15.46,6.61 14.2,6.08 12.82,6V7.67C13.69,7.79 14.47,8.11 15.14,8.58L13.27,6.71C12.94,6.66 12.6,6.63 12.25,6.63L10.38,4.76C10.87,4.66 11.37,4.59 11.88,4.56L10,2.68C10.66,2.56 11.33,2.5 12,2.5V2.03Z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1>您当前处于离线状态</h1>
|
||||
<p>无法连接到 MoviePilot 服务器。请检查您的网络连接后重试。</p>
|
||||
|
||||
<button class="retry-button" onclick="window.location.reload()">
|
||||
重新加载
|
||||
</button>
|
||||
|
||||
<div class="status-badge">
|
||||
<span class="status-dot"></span>
|
||||
<span>离线状态</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 监听网络状态变化
|
||||
window.addEventListener('online', function() {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
// Service Worker 消息处理
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'OFFLINE_STATUS' && !event.data.offline) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
shims.d.ts
vendored
@@ -12,3 +12,4 @@ declare module 'vue-prism-component' {
|
||||
export default component
|
||||
}
|
||||
declare module 'vue-shepherd';
|
||||
declare module 'colorthief';
|
||||
|
||||
86
src/@core/components/ConfirmDialog.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
type?: 'info' | 'warn' | 'error'
|
||||
title?: string
|
||||
content?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
width?: string | number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'info',
|
||||
title: '',
|
||||
content: '',
|
||||
confirmText: '',
|
||||
cancelText: '',
|
||||
width: '28rem',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
// 对话框类型对应的图标和颜色
|
||||
const typeConfig = {
|
||||
info: {
|
||||
icon: 'mdi-information',
|
||||
color: 'info',
|
||||
},
|
||||
warn: {
|
||||
icon: 'mdi-alert',
|
||||
color: 'warning',
|
||||
},
|
||||
error: {
|
||||
icon: 'mdi-alert-circle',
|
||||
color: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
// 获取当前类型的配置
|
||||
const currentType = computed(() => typeConfig[props.type])
|
||||
|
||||
// 确认按钮点击
|
||||
function handleConfirm() {
|
||||
emit('confirm')
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
// 取消按钮点击
|
||||
function handleCancel() {
|
||||
emit('cancel')
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" :max-width="width">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<div class="d-flex align-center justify-start mt-3">
|
||||
<VAvatar :color="currentType.color" variant="text" size="x-large">
|
||||
<VIcon size="x-large" :icon="currentType.icon" />
|
||||
</VAvatar>
|
||||
<div class="mx-3">
|
||||
<p class="font-weight-bold text-xl text-high-emphasis">{{ title }}</p>
|
||||
<p>{{ content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</VCardItem>
|
||||
<VCardActions class="mx-auto">
|
||||
<VBtn variant="tonal" color="secondary" class="px-5" @click="handleCancel">
|
||||
{{ cancelText }}
|
||||
</VBtn>
|
||||
<VBtn variant="elevated" :color="currentType.color" @click="handleConfirm" class="px-5">
|
||||
{{ confirmText }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
<VDialogCloseBtn @click="handleCancel" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -1,15 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
progress: Number,
|
||||
text: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center">
|
||||
<VProgressCircular v-if="!props.text || !props.progress" class="mb-3" size="64" indeterminate color="primary" />
|
||||
<VProgressCircular v-if="props.progress" class="mb-3" color="primary" :model-value="props.progress" size="64" />
|
||||
<span>{{ props.text }}</span>
|
||||
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center my-5">
|
||||
<div class="initial-loading-container">
|
||||
<div class="initial-loading-content">
|
||||
<div class="wave-loader">
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
</div>
|
||||
<div class="initial-loading-text" v-if="props.text">{{ props.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 初始的加载状态 */
|
||||
.initial-loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-block-size: 20vh;
|
||||
}
|
||||
|
||||
.initial-loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.wave-loader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
block-size: 40px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wave-dot {
|
||||
border-radius: 50%;
|
||||
animation: wave 1.5s ease-in-out infinite;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
block-size: 8px;
|
||||
inline-size: 8px;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(4) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.initial-loading-text {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@ defineProps({
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="title" class="my-3 md:flex md:items-center md:justify-between">
|
||||
<div v-if="title" class="my-3 mx-3 md:flex md:items-center md:justify-between">
|
||||
<div class="min-w-0 flex-1 mx-0">
|
||||
<h2
|
||||
class="ms-1 truncate text-2xl font-bold leading-7 text-gray-100 sm:overflow-visible sm:text-3xl sm:leading-9 md:mb-0"
|
||||
|
||||
@@ -51,8 +51,8 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
inset-block-end: 30px;
|
||||
inset-inline-end: 30px;
|
||||
inset-block-end: 2rem;
|
||||
inset-inline-end: 2rem;
|
||||
}
|
||||
|
||||
.global-action-button {
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
@use "@layouts/styles/_placeholders";
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// 👉 Alert
|
||||
.v-alert {
|
||||
.v-alert__close {
|
||||
.v-icon {
|
||||
block-size: 20px !important;
|
||||
font-size: 20px !important;
|
||||
inline-size: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.v-alert--prominent) .v-alert__prepend {
|
||||
.v-icon {
|
||||
block-size: 1.375rem !important;
|
||||
font-size: 1.375rem !important;
|
||||
inline-size: 1.375rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-alert-title {
|
||||
line-height: 1.5rem;
|
||||
margin-block-end: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Avatar font-size
|
||||
.v-avatar {
|
||||
@include mixins.avatar-font-sizes($map: variables.$avatar-font-sizes);
|
||||
@@ -33,6 +57,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Button
|
||||
.v-btn {
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
&:not(.v-btn--icon) .v-icon {
|
||||
--v-icon-size-multiplier: 0.9525 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Chip
|
||||
.v-chip.v-chip--size-default .v-avatar {
|
||||
--v-avatar-height: 24px;
|
||||
}
|
||||
|
||||
.v-chip.v-chip--density-comfortable {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// Dialog responsive width
|
||||
.v-dialog {
|
||||
.v-card {
|
||||
@@ -40,7 +81,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
@media (width >= 576px) {
|
||||
.v-dialog {
|
||||
&.v-dialog-sm,
|
||||
&.v-dialog-lg,
|
||||
@@ -50,7 +91,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
@media (width >= 992px) {
|
||||
.v-dialog {
|
||||
&.v-dialog-lg,
|
||||
&.v-dialog-xl {
|
||||
@@ -59,18 +100,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
@media (width >= 1200px) {
|
||||
.v-dialog.v-dialog-xl,
|
||||
.v-dialog.v-dialog-xl .v-overlay__content > .v-card {
|
||||
inline-size: 1165px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// v-tab with pill support
|
||||
// 👉 Expansion Panel
|
||||
.v-expansion-panel {
|
||||
.v-expansion-panel-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Tooltip
|
||||
.v-tooltip > .v-overlay__content {
|
||||
font-weight: 500;
|
||||
line-height: 0.875rem;
|
||||
}
|
||||
|
||||
// 👉 List
|
||||
|
||||
// 👉 Tab with pill support
|
||||
.v-tabs.v-tabs-pill {
|
||||
.v-tab.v-btn {
|
||||
border-radius: 0.375rem !important;
|
||||
border-radius: 6px !important;
|
||||
min-inline-size: 8.125rem;
|
||||
transition: none;
|
||||
|
||||
@@ -94,7 +149,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 added box shadow
|
||||
// 👉 Timeline added box shadow
|
||||
.v-timeline-item {
|
||||
.v-timeline-divider__dot {
|
||||
.v-timeline-divider__inner-dot {
|
||||
@@ -160,7 +215,6 @@
|
||||
}
|
||||
|
||||
// 👉 Slider
|
||||
|
||||
.v-slider.v-input--horizontal .v-slider-track__fill {
|
||||
block-size: var(--v-slider-track-size);
|
||||
}
|
||||
@@ -171,7 +225,19 @@
|
||||
|
||||
.v-slider-thumb {
|
||||
.v-slider-thumb__label {
|
||||
background: rgb(117, 117, 117);
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
|
||||
&::before {
|
||||
color: rgb(117, 117, 117);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Switch
|
||||
.v-switch {
|
||||
.v-selection-control:not(.v-selection-control--dirty) .v-switch__thumb {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,5 +245,45 @@
|
||||
.v-table--density-default > .v-table__wrapper > table > tbody > tr > td,
|
||||
.v-table--density-default > .v-table__wrapper > table > thead > tr > td,
|
||||
.v-table--density-default > .v-table__wrapper > table > tfoot > tr > td {
|
||||
block-size: 50px;
|
||||
block-size: 50px !important;
|
||||
}
|
||||
|
||||
.v-table {
|
||||
--v-table-header-height: 54px !important;
|
||||
|
||||
th {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
font-size: 0.75rem;
|
||||
|
||||
.v-data-table-header__content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.v-selection-control {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.v-data-table {
|
||||
th {
|
||||
background: rgb(var(--v-table-header-background)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Pagination
|
||||
.v-pagination {
|
||||
.v-btn {
|
||||
border-radius: 4px;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 SnackBar
|
||||
.v-snackbar--variant-elevated {
|
||||
@include mixins.elevation(6);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "placeholders" as *;
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
@use "../misc";
|
||||
@use "misc";
|
||||
@use "mixins";
|
||||
|
||||
$header: ".layout-navbar";
|
||||
@@ -16,25 +16,43 @@ $header: ".layout-navbar";
|
||||
@if variables.$vertical-nav-navbar-style == "elevated" {
|
||||
// Add transition
|
||||
#{$header} {
|
||||
transition: padding 0.2s ease, background-color 0.18s ease;
|
||||
transition: padding 0.2s ease;
|
||||
}
|
||||
|
||||
// If navbar is contained => Add border radius to header
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
#{$header} {
|
||||
// #{$header} {
|
||||
// border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
// Scrolled styles for sticky navbar
|
||||
@at-root {
|
||||
/* ℹ️ This html selector with not selector is required when:
|
||||
dialog is opened and window don't have any scroll. This removes window-scrolled class from layout and out style broke
|
||||
/* ℹ️ Only apply scrolled styles when window is actually scrolled,
|
||||
not when dialog is opened without scroll
|
||||
*/
|
||||
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-fixed,
|
||||
&.window-scrolled.layout-navbar-fixed {
|
||||
|
||||
#{$header} {
|
||||
padding-inline: 1rem;
|
||||
|
||||
@extend %default-layout-vertical-nav-scrolled-sticky-elevated-nav;
|
||||
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
|
||||
}
|
||||
|
||||
.navbar-blur#{$header} {
|
||||
@extend %blurry-bg;
|
||||
}
|
||||
}
|
||||
|
||||
/* ℹ️ Ensure header styles are preserved when dialog is opened,
|
||||
but only if window was scrolled before dialog opened
|
||||
*/
|
||||
html.v-overlay-scroll-blocked &.window-scrolled.layout-navbar-fixed {
|
||||
|
||||
#{$header} {
|
||||
padding-inline: 1rem;
|
||||
|
||||
@extend %default-layout-vertical-nav-scrolled-sticky-elevated-nav;
|
||||
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
|
||||
}
|
||||
@@ -63,7 +81,7 @@ $header: ".layout-navbar";
|
||||
|
||||
#{$header} {
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
// border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
}
|
||||
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
@@ -1,5 +1,5 @@
|
||||
@use "@core/scss/placeholders";
|
||||
@use "@core/scss/variables" as core-vars;
|
||||
@use "placeholders";
|
||||
@use "variables" as core-vars;
|
||||
|
||||
.layout-navbar {
|
||||
@if core-vars.$navbar-high-emphasis-text {
|
||||
|
||||
@@ -11,11 +11,58 @@
|
||||
|
||||
// ℹ️ adding styling for code tag
|
||||
code {
|
||||
background: rgba(var(--v-code-background-color), var(--v-focus-opacity));
|
||||
border-radius: 3px;
|
||||
background: rgba(var(--v-code-background-color), var(--v-focus-opacity));
|
||||
color: currentcolor;
|
||||
font-size: 85%;
|
||||
font-weight: 400;
|
||||
padding-block: 0.2em;
|
||||
padding-inline: 0.4em;
|
||||
}
|
||||
|
||||
%blurry-bg {
|
||||
position: relative;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 4%), 0 1px 2px rgba(0, 0, 0, 2%);
|
||||
|
||||
@media (width >= 1280px) and (hover: hover) {
|
||||
background: rgba(var(--v-theme-background), 1);
|
||||
|
||||
.v-theme--transparent & {
|
||||
backdrop-filter: blur(var(--transparent-blur-light, 5px));
|
||||
background: rgba(var(--v-theme-background), var(--transparent-opacity-light, 0.1)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 1280px), (hover: none) {
|
||||
background: transparent;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
backdrop-filter: blur(24px);
|
||||
block-size: calc(env(safe-area-inset-top, 0px) + var(--navbar-tab-height) + 4rem);
|
||||
content: "";
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
pointer-events: none;
|
||||
transition: padding 0.3s ease-in-out;
|
||||
|
||||
.v-theme--light & {
|
||||
background: rgba(var(--v-theme-surface), 0.6);
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background: rgba(var(--v-theme-background), 0.5);
|
||||
}
|
||||
|
||||
.v-theme--purple & {
|
||||
background: rgba(var(--v-theme-background), 0.5);
|
||||
}
|
||||
|
||||
.v-theme--transparent & {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy, 16px));
|
||||
background: rgba(var(--v-theme-background), var(--transparent-opacity-heavy, 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@use "vuetify/lib/styles/settings" as vuetify_settings;
|
||||
@use "sass:map";
|
||||
@use "vuetify/lib/styles/settings/_index.sass" as vuetify_settings;
|
||||
@use "@styles/variables/_vuetify.scss" as vuetify;
|
||||
|
||||
@mixin themed($property, $light-value, $dark-value) {
|
||||
@at-root {
|
||||
@@ -17,11 +19,12 @@
|
||||
// ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element
|
||||
@mixin before-pseudo() {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
background: currentcolor;
|
||||
block-size: 100%;
|
||||
border-radius: inherit;
|
||||
content: "";
|
||||
inline-size: 100%;
|
||||
inset: 0;
|
||||
@@ -43,8 +46,8 @@
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
background-color: currentcolor;
|
||||
border-radius: inherit;
|
||||
background-color: currentcolor;
|
||||
content: "";
|
||||
inset: 0;
|
||||
opacity: $opacity;
|
||||
@@ -56,10 +59,81 @@
|
||||
@mixin avatar-font-sizes($map: $avatar-sizes) {
|
||||
@each $sizeName, $multiplier in vuetify_settings.$size-scales {
|
||||
/* stylelint-disable-next-line scss/no-global-function-names */
|
||||
$size: map-get($map, $sizeName);
|
||||
$size: map.get($map, $sizeName);
|
||||
|
||||
&.v-avatar--size-#{$sizeName} {
|
||||
font-size: #{$size}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin elevation($z, $important: false) {
|
||||
box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null);
|
||||
}
|
||||
|
||||
@mixin bordered-skin($component, $border-property: "border", $important: false) {
|
||||
#{$component} {
|
||||
box-shadow: none !important;
|
||||
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin selected-states($selector) {
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:hover
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:focus-visible
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
&:focus {
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin push-anchors() {
|
||||
:target {
|
||||
scroll-margin-block-start: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin xs {
|
||||
@media (width >= 0) and (width <= 599.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin sm {
|
||||
@media (width >= 600px) and (width <= 959.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin md {
|
||||
@media (width >= 960px) and (width <= 1279.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin lg {
|
||||
@media (width >= 1280px) and (width <= 1919.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin xl {
|
||||
@media (width >= 1920px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,25 @@
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// 👉 Demo spacers
|
||||
// TODO: Use vuetify SCSS variable here
|
||||
$card-spacer-content: 16px;
|
||||
|
||||
.demo-space-x {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-block-start: -$card-spacer-content;
|
||||
|
||||
& > * {
|
||||
margin-block-start: $card-spacer-content;
|
||||
margin-inline-end: $card-spacer-content;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-space-y {
|
||||
& > * {
|
||||
margin-block-end: $card-spacer-content;
|
||||
|
||||
&:last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Card match height
|
||||
.match-height.v-row {
|
||||
.v-card {
|
||||
block-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Whitespace
|
||||
.whitespace-no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 👉 Colors
|
||||
|
||||
/*
|
||||
ℹ️ Vuetify is applying `.text-white` class to badge icon but don't provide its styles
|
||||
Moreover, we also use this class in some places
|
||||
|
||||
ℹ️ In vuetify 2 with `$color-pack: false` SCSS var config this class was getting generated but this is not the case in v3
|
||||
|
||||
ℹ️ We also need !important to get correct color in badge icon
|
||||
*/
|
||||
.text-white {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.bg-var-theme-background {
|
||||
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity)) !important;
|
||||
}
|
||||
|
||||
// [/^bg-light-(\w+)$/, ([, w]) => ({ backgroundColor: `rgba(var(--v-theme-${w}), var(--v-activated-opacity))` })],
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
.bg-light-#{$color-name} {
|
||||
background-color: rgba(var(--v-theme-#{$color-name}), var(--v-activated-opacity)) !important;
|
||||
// 👉 Pagination small-select dropdown for table
|
||||
// TODO: remove this class after vuetify datatable implememtation
|
||||
|
||||
.per-page-select {
|
||||
margin-block: auto;
|
||||
|
||||
.v-field__input {
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.v-field__append-inner {
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
|
||||
.v-icon {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Typography
|
||||
.font-weight-semibold {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.leading-normal {
|
||||
line-height: normal !important;
|
||||
}
|
||||
|
||||
@@ -9,20 +9,53 @@
|
||||
- You can also use variable for consistency (e.g. mx 1 rem should be applied to both vertical nav items and vertical nav header)
|
||||
*/
|
||||
@use "sass:map";
|
||||
|
||||
// 使用模板中的变量,不再进行配置
|
||||
@use "@layouts/styles/variables" as layouts-vars;
|
||||
@use "utils";
|
||||
@use "vuetify/lib/styles/tools/functions" as *;
|
||||
|
||||
// 👉 Default layout
|
||||
// 合并两个文件中的@forward配置
|
||||
@forward "@layouts/styles/variables" with (
|
||||
// 来自_variables.scss的配置
|
||||
$layout-vertical-nav-collapsed-width: 68px !default,
|
||||
|
||||
// 来自template/_variables.scss的配置
|
||||
$layout-vertical-nav-z-index: 1004,
|
||||
$layout-overlay-z-index: 1003
|
||||
);
|
||||
|
||||
$navbar-high-emphasis-text: true !default;
|
||||
// 使用命名空间来避免变量冲突
|
||||
@use "@layouts/styles/variables" as layouts-vars;
|
||||
|
||||
// 移除@forward配置,已合并到template/_variables.scss
|
||||
// @forward "@layouts/styles/variables" with (
|
||||
// $layout-vertical-nav-collapsed-width: 68px !default,
|
||||
// );
|
||||
// @use "@layouts/styles/variables" as *;
|
||||
$vertical-nav-horizontal-padding-custom: 1.375rem 1rem;
|
||||
|
||||
// ℹ️ We created this SCSS var to extract the start padding
|
||||
// Docs: https://sass-lang.com/documentation/modules/string
|
||||
// $vertical-nav-horizontal-padding => 0 8px;
|
||||
// string.index(#{$vertical-nav-horizontal-padding}, " ") + 1 => 2
|
||||
// string.index(#{$vertical-nav-horizontal-padding}, " ") => 1
|
||||
// string.slice(0 8px, 2, -1) => 8px => $card-actions-padding-x
|
||||
|
||||
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding-custom) !default;
|
||||
$vertical-nav-items-icon-margin-inline-end: 0.625rem !default;
|
||||
|
||||
// Vertical Nav Configuration
|
||||
$vertical-nav-collapsed-width: 68px !default;
|
||||
|
||||
// ℹ️ This is used to keep consistency between nav items and nav header left & right margin
|
||||
// This is used by nav items & nav header
|
||||
$vertical-nav-horizontal-spacing: 0 1.125rem !default;
|
||||
$vertical-nav-horizontal-padding: $vertical-nav-horizontal-padding-custom !default;
|
||||
|
||||
// Vertical nav header padding
|
||||
$vertical-nav-header-padding: 1rem 0.25rem 1rem $vertical-nav-horizontal-padding-start !default;
|
||||
|
||||
// 👉 Custom Variables
|
||||
$avatar-font-sizes: (
|
||||
"x-small":12,
|
||||
"small":14,
|
||||
"default":18,
|
||||
"large":20,
|
||||
"x-large":24
|
||||
) !default;
|
||||
|
||||
$theme-colors-name: (
|
||||
"primary",
|
||||
@@ -41,31 +74,16 @@ $default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
|
||||
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
|
||||
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
|
||||
|
||||
// ℹ️ This is used to keep consistency between nav items and nav header left & right margin
|
||||
// This is used by nav items & nav header
|
||||
$vertical-nav-horizontal-spacing: 0 1.125rem !default;
|
||||
$vertical-nav-horizontal-padding: 1.375rem 1rem !default;
|
||||
|
||||
/*
|
||||
ℹ️ We created this SCSS var to extract the start padding
|
||||
Docs: https://sass-lang.com/documentation/modules/string
|
||||
$vertical-nav-horizontal-padding => 0 8px;
|
||||
string.index(#{$vertical-nav-horizontal-padding}, " ") + 1 => 2
|
||||
string.index(#{$vertical-nav-horizontal-padding}, " ") => 1
|
||||
string.slice(0 8px, 2, -1) => 8px => $card-actions-padding-x
|
||||
*/
|
||||
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding);
|
||||
|
||||
// Vertical nav header height. Mostly we will align it with navbar height;
|
||||
$vertical-nav-header-height: layouts-vars.$layout-vertical-nav-navbar-height !default;
|
||||
$vertical-nav-navbar-shadow: 0 4px 8px -4px rgb(94 86 105 / 42%);
|
||||
$vertical-nav-navbar-elevation: 3 !default;
|
||||
$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating
|
||||
$vertical-nav-floating-navbar-top: 1rem !default;
|
||||
|
||||
// Vertical nav header padding
|
||||
$vertical-nav-header-padding: 1rem 0.25rem 1rem $vertical-nav-horizontal-padding-start !default;
|
||||
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default;
|
||||
|
||||
// Move logo when vertical nav is mini (collapsed but not hovered)
|
||||
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px;
|
||||
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default;
|
||||
|
||||
// Space between logo and title
|
||||
$vertical-nav-header-logo-title-spacing: 0.9rem !default;
|
||||
@@ -77,22 +95,130 @@ $vertical-nav-section-title-mt: 1.5rem !default;
|
||||
$vertical-nav-section-title-mb: 0.5rem !default;
|
||||
|
||||
// Vertical nav icons
|
||||
$vertical-nav-items-icon-size: 1.5rem;
|
||||
$vertical-nav-items-icon-margin-inline-end: 0.625rem;
|
||||
$vertical-nav-items-icon-size: 1.5rem !default;
|
||||
$vertical-nav-items-nested-icon-size: 0.9rem !default;
|
||||
|
||||
// Transition duration for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default;
|
||||
|
||||
// Timing function for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default;
|
||||
|
||||
// 👉 Horizontal nav
|
||||
|
||||
/*
|
||||
❗ Heads up
|
||||
==================
|
||||
Here we assume we will always use shorthand property which will apply same padding on four side
|
||||
This is because this have been used as value of top property by `.popper-content`
|
||||
*/
|
||||
$horizontal-nav-padding: 0.6875rem !default;
|
||||
|
||||
// Gap between top level horizontal nav items
|
||||
$horizontal-nav-top-level-items-gap: 4px !default;
|
||||
|
||||
// Horizontal nav icons
|
||||
$horizontal-nav-items-icon-size: 1.5rem !default;
|
||||
$horizontal-nav-third-level-icon-size: 0.9rem !default;
|
||||
$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default;
|
||||
|
||||
// ℹ️ We used SCSS variable because we want to allow users to update max height of popper content
|
||||
// 120px is combined height of navbar & horizontal nav
|
||||
$horizontal-nav-popper-content-max-height: calc((var(--vh, 1vh) * 100) - 120px - 4rem) !default;
|
||||
|
||||
// ℹ️ This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values.
|
||||
$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default;
|
||||
|
||||
// 👉 Plugins
|
||||
|
||||
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35);
|
||||
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default;
|
||||
|
||||
// 👉 Custom Variables
|
||||
$avatar-font-sizes: () !default;
|
||||
$avatar-font-sizes: map.deep-merge(
|
||||
// 👉 Vuetify
|
||||
|
||||
// Used in src/@core/scss/base/libs/vuetify/_overrides.scss
|
||||
$vuetify-reduce-default-compact-button-icon-size: true !default;
|
||||
|
||||
// 👉 Custom variables
|
||||
// for utility classes
|
||||
$font-sizes: () !default;
|
||||
$font-sizes: map-deep-merge(
|
||||
(
|
||||
"x-small":12,
|
||||
"small":14,
|
||||
"default":18,
|
||||
"large":20,
|
||||
"x-large":24
|
||||
"xs": 0.75rem,
|
||||
"sm": 0.875rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.25rem,
|
||||
"2xl": 1.5rem,
|
||||
"3xl": 1.875rem,
|
||||
"4xl": 2.25rem,
|
||||
"5xl": 3rem,
|
||||
"6xl": 3.75rem,
|
||||
"7xl": 4.5rem,
|
||||
"8xl": 6rem,
|
||||
"9xl": 8rem
|
||||
),
|
||||
$avatar-font-sizes
|
||||
$font-sizes
|
||||
);
|
||||
|
||||
// line height
|
||||
$font-line-height: () !default;
|
||||
$font-line-height: map-deep-merge(
|
||||
(
|
||||
"xs": 1rem,
|
||||
"sm": 1.25rem,
|
||||
"base": 1.5rem,
|
||||
"lg": 1.75rem,
|
||||
"xl": 1.75rem,
|
||||
"2xl": 2rem,
|
||||
"3xl": 2.25rem,
|
||||
"4xl": 2.5rem,
|
||||
"5xl": 1,
|
||||
"6xl": 1,
|
||||
"7xl": 1,
|
||||
"8xl": 1,
|
||||
"9xl": 1
|
||||
),
|
||||
$font-line-height
|
||||
);
|
||||
|
||||
// gap utility class
|
||||
$gap: () !default;
|
||||
$gap: map-deep-merge(
|
||||
(
|
||||
"0": 0,
|
||||
"1": 0.25rem,
|
||||
"2": 0.5rem,
|
||||
"3": 0.75rem,
|
||||
"4": 1rem,
|
||||
"5": 1.25rem,
|
||||
"6":1.5rem,
|
||||
"7": 1.75rem,
|
||||
"8": 2rem,
|
||||
"9": 2.25rem,
|
||||
"10": 2.5rem,
|
||||
"11": 2.75rem,
|
||||
"12": 3rem,
|
||||
"14": 3.5rem,
|
||||
"16": 4rem,
|
||||
"20": 5rem,
|
||||
"24": 6rem,
|
||||
"28": 7rem,
|
||||
"32": 8rem,
|
||||
"36": 9rem,
|
||||
"40": 10rem,
|
||||
"44": 11rem,
|
||||
"48": 12rem,
|
||||
"52": 13rem,
|
||||
"56": 14rem,
|
||||
"60": 15rem,
|
||||
"64": 16rem,
|
||||
"72": 18rem,
|
||||
"80": 20rem,
|
||||
"96": 24rem
|
||||
),
|
||||
$gap
|
||||
);
|
||||
|
||||
// 👉 Default layout
|
||||
|
||||
$navbar-high-emphasis-text: true !default;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use "./placeholders";
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@core/scss/mixins" as mixins;
|
||||
@use "./mixins" as mixins;
|
||||
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
|
||||
@use "vuetify/lib/styles/tools/elevation" as elevation;
|
||||
|
||||
@@ -118,11 +118,6 @@
|
||||
opacity: var(--v-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Vertical nav link
|
||||
.nav-link {
|
||||
@extend %nav-link;
|
||||
|
||||
> .router-link-exact-active {
|
||||
@extend %nav-link-active;
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
@use "sass:map";
|
||||
@use "template/index";
|
||||
|
||||
// 保留这个引用以向后兼容,但实际功能已经移至template/index.scss
|
||||
// 基础变量和配置
|
||||
@use "variables";
|
||||
@use "mixins";
|
||||
@use "utils";
|
||||
|
||||
// 布局相关
|
||||
@use "default-layout";
|
||||
@use "vertical-nav";
|
||||
@use "default-layout-w-vertical-nav";
|
||||
|
||||
// 组件样式
|
||||
@use "components";
|
||||
|
||||
// 工具类
|
||||
@use "utilities";
|
||||
|
||||
// 其他样式
|
||||
@use "misc";
|
||||
@use "dark";
|
||||
|
||||
// 第三方库样式
|
||||
@use "libs/perfect-scrollbar";
|
||||
@use "libs/apex-chart";
|
||||
@use "libs/full-calendar";
|
||||
@use "libs/vuetify";
|
||||
|
||||
// 全局样式
|
||||
a {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Vuetify 3 don't provide margin bottom style like vuetify 2
|
||||
p {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
// Iconify icon size
|
||||
svg.iconify {
|
||||
block-size: 1em;
|
||||
inline-size: 1em;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,88 @@
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
@use "@configured-variables" as variables;
|
||||
@use "../mixins";
|
||||
|
||||
// 👉 Apex chart
|
||||
.apexcharts-canvas {
|
||||
&line[stroke="transparent"] {
|
||||
display: "none";
|
||||
// For RTL alignment
|
||||
.apexcharts-yaxis-texts-g {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
.apexcharts-tooltip {
|
||||
@include mixins_elevation.elevation(3);
|
||||
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
line-height: 1.5;
|
||||
|
||||
.apexcharts-tooltip-title {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
font-weight: 500;
|
||||
margin-block-end: 0.25rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
font-size: inherit;
|
||||
gap: 0.5rem;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text-label,
|
||||
.apexcharts-tooltip-text-value {
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group {
|
||||
padding-block: 0 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
&:last-child {
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.apexcharts-theme-light {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
&.apexcharts-theme-dark {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group:first-of-type {
|
||||
padding-block-end: 0;
|
||||
border-color: rgb(var(--v-border-color));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: none;
|
||||
|
||||
.apexcharts-tooltip-text-label,
|
||||
.apexcharts-tooltip-text-value {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-grey-50));
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
|
||||
&::after {
|
||||
border-block-end-color: rgb(var(--v-theme-grey-50));
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-block-end-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
.apexcharts-marker {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 👉 stroke-dasharray
|
||||
.apexcharts-radialbar,
|
||||
.apexcharts-radialbar-slice-current {
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip,
|
||||
.apexcharts-yaxistooltip {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-grey-50));
|
||||
|
||||
&::after {
|
||||
border-inline-start-color: rgb(var(--v-theme-grey-50));
|
||||
}
|
||||
border-color: rgb(var(--v-border-color));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
border-inline-start-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-block-end-color: rgb(var(--v-border-color));
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-text,
|
||||
.apexcharts-yaxistooltip-text {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
// 👉 Text color
|
||||
.apexcharts-text,
|
||||
.apexcharts-tooltip-text,
|
||||
.apexcharts-datalabel-label,
|
||||
@@ -69,19 +90,16 @@
|
||||
.apexcharts-xaxistooltip-text,
|
||||
.apexcharts-yaxistooltip-text,
|
||||
.apexcharts-legend-text {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)) !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.apexcharts-pie-label {
|
||||
fill: white;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.apexcharts-marker {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.apexcharts-legend-marker {
|
||||
margin-inline-end: 0.3875rem;
|
||||
// 👉 Annotation Label
|
||||
.apexcharts-annotation-rect {
|
||||
&.apexcharts-xaxis-annotation-rect,
|
||||
&.apexcharts-yaxis-annotation-rect {
|
||||
fill-opacity: 0.05;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@use "@core/scss/utils";
|
||||
@use "@configured-variables" as variables;
|
||||
@use "../../utils";
|
||||
|
||||
// 👉 Application
|
||||
// ℹ️ We need accurate vh in mobile devices as well
|
||||
@@ -45,6 +45,17 @@ h6,
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Button
|
||||
@if variables.$vuetify-reduce-default-compact-button-icon-size {
|
||||
.v-btn--density-compact.v-btn--size-default {
|
||||
.v-btn__content > svg {
|
||||
block-size: 22px;
|
||||
font-size: 22px;
|
||||
inline-size: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Card
|
||||
// Removes padding-top for immediately placed v-card-text after itself
|
||||
.v-card-text {
|
||||
@@ -71,7 +82,9 @@ h6,
|
||||
&.v-checkbox-btn,
|
||||
&.v-radio,
|
||||
&.v-radio-btn {
|
||||
margin-inline-start: -0.5625rem;
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: -0.5625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +92,9 @@ h6,
|
||||
&.v-radio,
|
||||
&.v-radio-btn,
|
||||
&.v-checkbox-btn {
|
||||
margin-inline-start: -0.3125rem;
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: -0.3125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +102,9 @@ h6,
|
||||
&.v-checkbox-btn,
|
||||
&.v-radio,
|
||||
&.v-radio-btn {
|
||||
margin-inline-start: -0.6875rem;
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: -0.6875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,13 +171,141 @@ h6,
|
||||
padding-block: 0 !important;
|
||||
padding-inline: 0 !important;
|
||||
|
||||
> .v-ripple__container {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-block-end: var(--v-card-list-gap) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item:hover,
|
||||
.v-list-item:focus,
|
||||
.v-list-item:active,
|
||||
.v-list-item.active {
|
||||
> .v-list-item__overlay {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Table
|
||||
.v-table {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
// 👉 Divider
|
||||
.v-divider {
|
||||
color: rgb(var(--v-border-color));
|
||||
}
|
||||
|
||||
// 👉 DataTable
|
||||
.v-data-table {
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
.v-checkbox-btn .v-selection-control__wrapper {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.v-selection-control {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.v-pagination {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 v-field
|
||||
.v-field:hover .v-field__outline {
|
||||
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
|
||||
}
|
||||
|
||||
// 👉 VLabel
|
||||
.v-label {
|
||||
opacity: 1 !important;
|
||||
|
||||
&:not(.v-field-label--floating) {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Overlay
|
||||
.v-overlay__scrim,
|
||||
.v-navigation-drawer__scrim {
|
||||
background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 透明主题下全屏弹窗的overlay背景透明度调整
|
||||
html[data-theme="transparent"] .v-dialog--fullscreen .v-overlay__scrim {
|
||||
background: rgba(var(--v-overlay-scrim-background), 0.3);
|
||||
}
|
||||
|
||||
// 👉 VMessages
|
||||
.v-messages {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 👉 Alert close btn
|
||||
.v-alert__close {
|
||||
.v-btn--icon .v-icon {
|
||||
--v-icon-size-multiplier: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Badge icon alignment
|
||||
.v-badge__badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 👉 Dialog
|
||||
.v-dialog--fullscreen {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
// 透明主题下全屏弹窗背景透明
|
||||
html[data-theme="transparent"] .v-dialog--fullscreen {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
// For dialog card title
|
||||
.v-card-item + .v-card-text {
|
||||
padding-block-start: 0 !important;
|
||||
}
|
||||
|
||||
// 👉 v-slide-group (List of chips)
|
||||
.v-slide-group {
|
||||
.v-slide-group__container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
// Spacing between buttons in v-slide-group
|
||||
.v-slide-group-item:not(:last-child) {
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Expansion Panel
|
||||
.v-expansion-panels {
|
||||
.v-expansion-panel-title {
|
||||
min-block-size: unset !important;
|
||||
padding-block: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 v-textarea
|
||||
.v-textarea {
|
||||
textarea {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Cursor
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
|
||||
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
|
||||
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
|
||||
/* stylelint-disable-next-line max-line-length */
|
||||
$font-family-custom: 'Inter', 'Noto Sans SC', sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
||||
// 👉 Card transition properties
|
||||
$card-transition-property-custom: box-shadow, opacity;
|
||||
|
||||
@forward "vuetify/settings" with (
|
||||
// 👉 General settings
|
||||
// 👉 General settings
|
||||
$color-pack: false !default,
|
||||
$body-font-family: $font-family-custom !default,
|
||||
$border-radius-root: 6px !default,
|
||||
|
||||
// 👉 Shadow opacity
|
||||
// 👉 Shadow opacity
|
||||
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
|
||||
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
|
||||
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
|
||||
|
||||
$body-font-family: $font-family-custom !default,
|
||||
$border-radius-root: 6px !default,
|
||||
|
||||
$shadow-key-umbra: (
|
||||
0: (0 0 0 0 var(--v-shadow-key-umbra-opacity)),
|
||||
1: (0 2px 1px -1px var(--v-shadow-key-umbra-opacity)),
|
||||
@@ -119,6 +121,18 @@ $card-transition-property-custom: box-shadow, opacity;
|
||||
24: (0 9px 46px 8px $shadow-key-ambient-opacity-custom)
|
||||
) !default,
|
||||
|
||||
// 👉 Card
|
||||
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
|
||||
$card-elevation: 6 !default,
|
||||
$card-title-line-height: 2rem !default,
|
||||
$card-actions-min-height: unset !default,
|
||||
$card-text-padding: 1.25rem !default,
|
||||
$card-item-padding: 1.25rem !default,
|
||||
$card-actions-padding: 0 12px 12px !default,
|
||||
$card-transition-property: $card-transition-property-custom !default,
|
||||
$card-subtitle-opacity: 1 !default,
|
||||
$card-title-letter-spacing: 0.0094rem !default,
|
||||
|
||||
// 👉 Typography
|
||||
$typography: (
|
||||
"h1": (
|
||||
@@ -170,29 +184,14 @@ $card-transition-property-custom: box-shadow, opacity;
|
||||
)
|
||||
) !default,
|
||||
|
||||
// 👉 States
|
||||
$states: ("activated": 0.08) !default,
|
||||
|
||||
// 👉 Card
|
||||
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
|
||||
$card-elevation: 6 !default,
|
||||
$card-title-line-height: 1.6 !default,
|
||||
$card-actions-min-height: unset !default,
|
||||
$card-text-padding: 20px !default,
|
||||
$card-item-padding: 15px 20px !default,
|
||||
$card-actions-padding: 0 12px 12px !default,
|
||||
$card-title-letter-spacing: 0.0094rem !default,
|
||||
$card-subtitle-opacity: 1 !default,
|
||||
$card-transition-property: $card-transition-property-custom !default,
|
||||
|
||||
// 👉 Navigation Drawer
|
||||
$navigation-drawer-color: rgba(var(--v-theme-on-surface), var(--v-high-medium-opacity)) !default,
|
||||
|
||||
// 👉 Table
|
||||
$table-color: rgba(var(--v-theme-on-surface), var(--v-high-medium-opacity)) !default,
|
||||
// 👉 List
|
||||
$list-item-icon-margin-end: 16px !default,
|
||||
$list-item-icon-margin-start: 16px !default,
|
||||
$list-item-subtitle-opacity: 1 !default,
|
||||
$list-subheader-text-opacity: 1 !default,
|
||||
|
||||
// 👉 Tooltip
|
||||
$tooltip-background-color:#212121 !default,
|
||||
$tooltip-background-color: #212121 !default,
|
||||
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
|
||||
$tooltip-font-size: 0.75rem !default,
|
||||
$tooltip-border-radius: 4px !default,
|
||||
@@ -205,6 +204,8 @@ $card-transition-property-custom: box-shadow, opacity;
|
||||
|
||||
// 👉 Badge
|
||||
$badge-border-color:rgb(var(--v-theme-surface)) !default,
|
||||
$badge-dot-height: 0.5rem !default,
|
||||
$badge-dot-width: 0.5rem !default,
|
||||
|
||||
// 👉 Button
|
||||
$button-height: 38px !default,
|
||||
@@ -212,6 +213,7 @@ $card-transition-property-custom: box-shadow, opacity;
|
||||
$button-border-radius: 5px !default,
|
||||
$button-padding-ratio: 1.7 !default,
|
||||
$button-text-letter-spacing: 0.025rem !default,
|
||||
$button-icon-density: ("default": 0.5, "comfortable": -2, "compact": -3) !default,
|
||||
|
||||
// 👉 Dialog
|
||||
$dialog-card-header-padding: 20px !default,
|
||||
@@ -220,6 +222,7 @@ $card-transition-property-custom: box-shadow, opacity;
|
||||
|
||||
// 👉 Chip
|
||||
$chip-label-border-radius: 4px !default,
|
||||
$chip-close-size: 20px !default,
|
||||
|
||||
// 👉 Expansion panel
|
||||
$expansion-panel-title-padding: 16px 20px !default,
|
||||
@@ -232,9 +235,6 @@ $card-transition-property-custom: box-shadow, opacity;
|
||||
// 👉 Menu
|
||||
$menu-content-border-radius: 5px !default,
|
||||
|
||||
// 👉 List
|
||||
$list-subheader-text-opacity: 1 !default,
|
||||
|
||||
// 👉 Snackbar
|
||||
$snackbar-background:#212121 !default,
|
||||
$snackbar-border-radius: 4px !default,
|
||||
@@ -243,7 +243,12 @@ $card-transition-property-custom: box-shadow, opacity;
|
||||
// 👉 Tabs
|
||||
$tabs-height: 40px !default,
|
||||
|
||||
// 👉 Timeline
|
||||
// 👉 Slider
|
||||
$slider-track-active-size: 4px !default,
|
||||
$slider-thumb-label-padding: 4px 12px !default,
|
||||
$slider-thumb-label-font-size: 0.875rem !default,
|
||||
|
||||
// 👉 Timeline
|
||||
$timeline-dot-size: 34px !default,
|
||||
$timeline-dot-divider-background: transparent !default,
|
||||
|
||||
@@ -252,4 +257,7 @@ $card-transition-property-custom: box-shadow, opacity;
|
||||
|
||||
// 👉 Navigation Drawer
|
||||
$navigation-drawer-scrim-opacity:0.5 !default,
|
||||
|
||||
// 👉 Table
|
||||
$table-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)),
|
||||
);
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
@use "variables";
|
||||
@use "overrides";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.auth-wrapper {
|
||||
min-block-size: calc(var(--vh, 1vh) * 100 + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
min-block-size: 100%;
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
}
|
||||
|
||||
.auth-footer-mask {
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
%layout-navbar {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
// Vertical nav scrolled sticky elevated nav
|
||||
%default-layout-vertical-nav-scrolled-sticky-elevated-nav {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 4px 8px -4px rgb(94 86 105 / 42%);
|
||||
}
|
||||
|
||||
// Floating navbar and sticky elevated navbar scrolled
|
||||
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 4px 8px -4px rgb(94 86 105 / 42%);
|
||||
}
|
||||
|
||||
// Floating navbar overlay
|
||||
%default-layout-vertical-nav-floating-navbar-overlay {
|
||||
backdrop-filter: blur(8px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.9);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use "@core/scss/mixins";
|
||||
@use "../mixins";
|
||||
@use "@configured-variables" as variables;
|
||||
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
|
||||
@use "@core/scss/utils";
|
||||
@use "../utils";
|
||||
|
||||
// Nav items styles (including section title)
|
||||
%vertical-nav-item {
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
@use "@configured-variables" as variables;
|
||||
@use "mixins";
|
||||
|
||||
// 👉 Alert
|
||||
.v-alert {
|
||||
.v-alert__close {
|
||||
.v-icon {
|
||||
block-size: 20px !important;
|
||||
font-size: 20px !important;
|
||||
inline-size: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.v-alert--prominent) .v-alert__prepend {
|
||||
.v-icon {
|
||||
block-size: 1.375rem !important;
|
||||
font-size: 1.375rem !important;
|
||||
inline-size: 1.375rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-alert-title {
|
||||
line-height: 1.5rem;
|
||||
margin-block-end: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Avatar font-size
|
||||
.v-avatar {
|
||||
@include mixins.avatar-font-sizes($map: variables.$avatar-font-sizes);
|
||||
}
|
||||
|
||||
// 👉 Button
|
||||
.v-btn {
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
&:not(.v-btn--icon) .v-icon {
|
||||
--v-icon-size-multiplier: 0.9525 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Chip
|
||||
.v-chip.v-chip--size-default .v-avatar {
|
||||
--v-avatar-height: 24px;
|
||||
}
|
||||
|
||||
.v-chip.v-chip--density-comfortable {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// 👉 Expansion Panel
|
||||
.v-expansion-panel {
|
||||
.v-expansion-panel-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Tooltip
|
||||
.v-tooltip > .v-overlay__content {
|
||||
font-weight: 500;
|
||||
line-height: 0.875rem;
|
||||
}
|
||||
|
||||
// 👉 List
|
||||
|
||||
// 👉 Tab with pill support
|
||||
.v-tabs.v-tabs-pill {
|
||||
.v-tab.v-btn {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Timeline added box shadow
|
||||
.v-timeline-item {
|
||||
.v-timeline-divider__dot {
|
||||
.v-timeline-divider__inner-dot {
|
||||
box-shadow: 0 0 0 0.1875rem rgb(var(--v-theme-on-surface-variant));
|
||||
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
|
||||
&.bg-#{$color-name} {
|
||||
box-shadow: 0 0 0 0.1875rem rgba(var(--v-theme-#{$color-name}), 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Timeline Outlined style
|
||||
.v-timeline-variant-outlined.v-timeline {
|
||||
.v-timeline-divider__dot {
|
||||
.v-timeline-divider__inner-dot {
|
||||
box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-on-surface-variant));
|
||||
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
background-color: rgb(var(--v-theme-surface)) !important;
|
||||
|
||||
&.bg-#{$color-name} {
|
||||
box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-#{$color-name}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Expansion panels
|
||||
.v-expansion-panel-title,
|
||||
.v-expansion-panel-title--active,
|
||||
.v-expansion-panel-title:hover,
|
||||
.v-expansion-panel-title:focus,
|
||||
.v-expansion-panel-title:focus-visible,
|
||||
.v-expansion-panel-title--active:focus,
|
||||
.v-expansion-panel-title--active:hover {
|
||||
.v-expansion-panel-title__overlay {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Set Elevation when panel open
|
||||
|
||||
.v-expansion-panels:not(.v-expansion-panels--variant-accordion) {
|
||||
.v-expansion-panel.v-expansion-panel--active {
|
||||
.v-expansion-panel__shadow {
|
||||
@include mixins_elevation.elevation(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Slider
|
||||
.v-slider-thumb {
|
||||
.v-slider-thumb__label {
|
||||
background: rgb(117, 117, 117);
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
|
||||
&::before {
|
||||
color: rgb(117, 117, 117);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Switch
|
||||
.v-switch {
|
||||
.v-selection-control:not(.v-selection-control--dirty) .v-switch__thumb {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Table
|
||||
.v-table--density-default > .v-table__wrapper > table > tbody > tr > td,
|
||||
.v-table--density-default > .v-table__wrapper > table > thead > tr > td,
|
||||
.v-table--density-default > .v-table__wrapper > table > tfoot > tr > td {
|
||||
block-size: 50px !important;
|
||||
}
|
||||
|
||||
.v-table {
|
||||
--v-table-header-height: 54px !important;
|
||||
|
||||
th {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
font-size: 0.75rem;
|
||||
|
||||
.v-data-table-header__content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.v-selection-control {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.v-data-table {
|
||||
th {
|
||||
background: rgb(var(--v-table-header-background)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Pagination
|
||||
.v-pagination {
|
||||
.v-btn {
|
||||
border-radius: 4px;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 SnackBar
|
||||
.v-snackbar--variant-elevated {
|
||||
@include mixins.elevation(6);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
@use "sass:map";
|
||||
@use "vuetify/lib/styles/settings" as vuetify_settings;
|
||||
@use "@styles/variables/_vuetify.scss" as vuetify;
|
||||
|
||||
@mixin avatar-font-sizes($map: $avatar-sizes) {
|
||||
@each $sizeName, $multiplier in vuetify_settings.$size-scales {
|
||||
/* stylelint-disable-next-line scss/no-global-function-names */
|
||||
$size: map.get($map, $sizeName);
|
||||
|
||||
&.v-avatar--size-#{$sizeName} {
|
||||
font-size: #{$size}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin elevation($z, $important: false) {
|
||||
box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null);
|
||||
}
|
||||
|
||||
@mixin before-pseudo() {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
background: currentcolor;
|
||||
block-size: 100%;
|
||||
content: "";
|
||||
inline-size: 100%;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin bordered-skin($component, $border-property: "border", $important: false) {
|
||||
#{$component} {
|
||||
box-shadow: none !important;
|
||||
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin selected-states($selector) {
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:hover
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:focus-visible
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
&:focus {
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin push-anchors() {
|
||||
:target {
|
||||
scroll-margin-block-start: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin xs {
|
||||
@media (width >= 0) and (width <= 599.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin sm {
|
||||
@media (width >= 600px) and (width <= 959.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin md {
|
||||
@media (width >= 960px) and (width <= 1279.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin lg {
|
||||
@media (width >= 1280px) and (width <= 1919.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin xl {
|
||||
@media (width >= 1920px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
.bg-var-theme-background {
|
||||
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity)) !important;
|
||||
}
|
||||
|
||||
// 👉 Pagination small-select dropdown for table
|
||||
// TODO: remove this class after vuetify datatable implememtation
|
||||
|
||||
.per-page-select {
|
||||
margin-block: auto;
|
||||
|
||||
.v-field__input {
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.v-field__append-inner {
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
|
||||
.v-icon {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
@use "sass:string";
|
||||
|
||||
/*
|
||||
ℹ️ This function is helpful when we have multi dimensional value
|
||||
|
||||
Assume we have padding variable `$nav-padding-horizontal: 10px;`
|
||||
With above variable let's say we use it in some style:
|
||||
```scss
|
||||
.selector {
|
||||
margin-left: $nav-padding-horizontal;
|
||||
}
|
||||
```
|
||||
|
||||
Now, problem is we can also have value as `$nav-padding-horizontal: 10px 15px;`
|
||||
In this case above style will be invalid.
|
||||
|
||||
This function will extract the left most value from the variable value.
|
||||
|
||||
$nav-padding-horizontal: 10px; => 10px;
|
||||
$nav-padding-horizontal: 10px 15px; => 10px;
|
||||
|
||||
This is safe:
|
||||
```scss
|
||||
.selector {
|
||||
margin-left: get-first-value($nav-padding-horizontal);
|
||||
}
|
||||
```
|
||||
*/
|
||||
@function get-first-value($var) {
|
||||
$start-at: string.index(#{$var}, " ");
|
||||
|
||||
@if $start-at {
|
||||
@return string.slice(
|
||||
#{$var},
|
||||
0,
|
||||
$start-at
|
||||
);
|
||||
} @else {
|
||||
@return $var;
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
@use "sass:map";
|
||||
@use "utils";
|
||||
@use "vuetify/lib/styles/tools/functions" as *;
|
||||
|
||||
$vertical-nav-horizontal-padding-custom: 1.375rem 1rem;
|
||||
|
||||
// ℹ️ We created this SCSS var to extract the start padding
|
||||
// Docs: https://sass-lang.com/documentation/modules/string
|
||||
// $vertical-nav-horizontal-padding => 0 8px;
|
||||
// string.index(#{$vertical-nav-horizontal-padding}, " ") + 1 => 2
|
||||
// string.index(#{$vertical-nav-horizontal-padding}, " ") => 1
|
||||
// string.slice(0 8px, 2, -1) => 8px => $card-actions-padding-x
|
||||
|
||||
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding-custom) !default;
|
||||
$vertical-nav-items-icon-margin-inline-end: 0.625rem !default;
|
||||
|
||||
// Vertical Nav Configuration
|
||||
$vertical-nav-collapsed-width: 68px !default;
|
||||
|
||||
// ℹ️ This is used to keep consistency between nav items and nav header left & right margin
|
||||
// This is used by nav items & nav header
|
||||
$vertical-nav-horizontal-spacing: 0 1.125rem !default;
|
||||
$vertical-nav-horizontal-padding: $vertical-nav-horizontal-padding-custom !default;
|
||||
|
||||
// Vertical nav header padding
|
||||
$vertical-nav-header-padding: 1rem 0.25rem 1rem $vertical-nav-horizontal-padding-start !default;
|
||||
|
||||
// 👉 Custom Variables
|
||||
$avatar-font-sizes: (
|
||||
"x-small":12,
|
||||
"small":14,
|
||||
"default":18,
|
||||
"large":20,
|
||||
"x-large":24
|
||||
) !default;
|
||||
|
||||
// 合并两个文件中的@forward配置
|
||||
@forward "@layouts/styles/variables" with (
|
||||
// 来自_variables.scss的配置
|
||||
$layout-vertical-nav-collapsed-width: 68px !default,
|
||||
|
||||
// 来自template/_variables.scss的配置
|
||||
$layout-vertical-nav-z-index: 1004,
|
||||
$layout-overlay-z-index: 1003
|
||||
);
|
||||
|
||||
// 使用命名空间来避免变量冲突
|
||||
@use "@layouts/styles/variables" as layouts-vars;
|
||||
|
||||
$theme-colors-name: (
|
||||
"primary",
|
||||
"secondary",
|
||||
"error",
|
||||
"info",
|
||||
"success",
|
||||
"warning"
|
||||
) !default;
|
||||
|
||||
// 👉 Default layout with vertical nav
|
||||
|
||||
$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
|
||||
|
||||
// 👉 Vertical nav
|
||||
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
|
||||
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
|
||||
|
||||
// ℹ️ This is used to keep consistency between nav items and nav header left & right margin
|
||||
// This is used by nav items & nav header
|
||||
$vertical-nav-horizontal-spacing: 1rem !default;
|
||||
$vertical-nav-horizontal-padding: 0.75rem !default;
|
||||
|
||||
// Vertical nav header height. Mostly we will align it with navbar height;
|
||||
$vertical-nav-header-height: layouts-vars.$layout-vertical-nav-navbar-height !default;
|
||||
$vertical-nav-navbar-elevation: 3 !default;
|
||||
$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating
|
||||
$vertical-nav-floating-navbar-top: 1rem !default;
|
||||
|
||||
// Vertical nav header padding
|
||||
$vertical-nav-header-padding: 1rem $vertical-nav-horizontal-padding !default;
|
||||
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default;
|
||||
|
||||
// Move logo when vertical nav is mini (collapsed but not hovered)
|
||||
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default;
|
||||
|
||||
// Space between logo and title
|
||||
$vertical-nav-header-logo-title-spacing: 0.9rem !default;
|
||||
|
||||
// Section title margin top (when its not first child)
|
||||
$vertical-nav-section-title-mt: 1.5rem !default;
|
||||
|
||||
// Section title margin bottom
|
||||
$vertical-nav-section-title-mb: 0.5rem !default;
|
||||
|
||||
// Vertical nav icons
|
||||
$vertical-nav-items-icon-size: 1.5rem !default;
|
||||
$vertical-nav-items-nested-icon-size: 0.9rem !default;
|
||||
$vertical-nav-items-icon-margin-inline-end: 0.5rem !default;
|
||||
|
||||
// Transition duration for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default;
|
||||
|
||||
// Timing function for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default;
|
||||
|
||||
// 👉 Horizontal nav
|
||||
|
||||
/*
|
||||
❗ Heads up
|
||||
==================
|
||||
Here we assume we will always use shorthand property which will apply same padding on four side
|
||||
This is because this have been used as value of top property by `.popper-content`
|
||||
*/
|
||||
$horizontal-nav-padding: 0.6875rem !default;
|
||||
|
||||
// Gap between top level horizontal nav items
|
||||
$horizontal-nav-top-level-items-gap: 4px !default;
|
||||
|
||||
// Horizontal nav icons
|
||||
$horizontal-nav-items-icon-size: 1.5rem !default;
|
||||
$horizontal-nav-third-level-icon-size: 0.9rem !default;
|
||||
$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default;
|
||||
|
||||
// ℹ️ We used SCSS variable because we want to allow users to update max height of popper content
|
||||
// 120px is combined height of navbar & horizontal nav
|
||||
$horizontal-nav-popper-content-max-height: calc((var(--vh, 1vh) * 100) - 120px - 4rem) !default;
|
||||
|
||||
// ℹ️ This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values.
|
||||
$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default;
|
||||
|
||||
// 👉 Plugins
|
||||
|
||||
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default;
|
||||
|
||||
// 👉 Vuetify
|
||||
|
||||
// Used in src/@core/scss/base/libs/vuetify/_overrides.scss
|
||||
$vuetify-reduce-default-compact-button-icon-size: true !default;
|
||||
|
||||
// 👉 Custom variables
|
||||
// for utility classes
|
||||
$font-sizes: () !default;
|
||||
$font-sizes: map-deep-merge(
|
||||
(
|
||||
"xs": 0.75rem,
|
||||
"sm": 0.875rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.25rem,
|
||||
"2xl": 1.5rem,
|
||||
"3xl": 1.875rem,
|
||||
"4xl": 2.25rem,
|
||||
"5xl": 3rem,
|
||||
"6xl": 3.75rem,
|
||||
"7xl": 4.5rem,
|
||||
"8xl": 6rem,
|
||||
"9xl": 8rem
|
||||
),
|
||||
$font-sizes
|
||||
);
|
||||
|
||||
// line height
|
||||
$font-line-height: () !default;
|
||||
$font-line-height: map-deep-merge(
|
||||
(
|
||||
"xs": 1rem,
|
||||
"sm": 1.25rem,
|
||||
"base": 1.5rem,
|
||||
"lg": 1.75rem,
|
||||
"xl": 1.75rem,
|
||||
"2xl": 2rem,
|
||||
"3xl": 2.25rem,
|
||||
"4xl": 2.5rem,
|
||||
"5xl": 1,
|
||||
"6xl": 1,
|
||||
"7xl": 1,
|
||||
"8xl": 1,
|
||||
"9xl": 1
|
||||
),
|
||||
$font-line-height
|
||||
);
|
||||
|
||||
// gap utility class
|
||||
$gap: () !default;
|
||||
$gap: map-deep-merge(
|
||||
(
|
||||
"0": 0,
|
||||
"1": 0.25rem,
|
||||
"2": 0.5rem,
|
||||
"3": 0.75rem,
|
||||
"4": 1rem,
|
||||
"5": 1.25rem,
|
||||
"6":1.5rem,
|
||||
"7": 1.75rem,
|
||||
"8": 2rem,
|
||||
"9": 2.25rem,
|
||||
"10": 2.5rem,
|
||||
"11": 2.75rem,
|
||||
"12": 3rem,
|
||||
"14": 3.5rem,
|
||||
"16": 4rem,
|
||||
"20": 5rem,
|
||||
"24": 6rem,
|
||||
"28": 7rem,
|
||||
"32": 8rem,
|
||||
"36": 9rem,
|
||||
"40": 10rem,
|
||||
"44": 11rem,
|
||||
"48": 12rem,
|
||||
"52": 13rem,
|
||||
"56": 14rem,
|
||||
"60": 15rem,
|
||||
"64": 16rem,
|
||||
"72": 18rem,
|
||||
"80": 20rem,
|
||||
"96": 24rem
|
||||
),
|
||||
$gap
|
||||
);
|
||||
|
||||
// Avatar sizes map
|
||||
$avatar-font-sizes: (
|
||||
"x-small": 0.625rem,
|
||||
"small": 0.75rem,
|
||||
"default": 0.875rem,
|
||||
"large": 1rem,
|
||||
"x-large": 1.125rem,
|
||||
) !default;
|
||||
@@ -1,42 +0,0 @@
|
||||
@use "sass:map";
|
||||
|
||||
// Layout
|
||||
@use "../vertical-nav";
|
||||
@use "../default-layout";
|
||||
@use "default-layout-w-vertical-nav";
|
||||
|
||||
// Components
|
||||
@use "components";
|
||||
|
||||
// Utilities
|
||||
@use "utilities";
|
||||
@use "../utils";
|
||||
|
||||
// Misc
|
||||
@use "../misc";
|
||||
|
||||
// Dark
|
||||
@use "../dark";
|
||||
|
||||
// Variables
|
||||
@use "variables";
|
||||
|
||||
// libs
|
||||
@use "libs/perfect-scrollbar";
|
||||
@use "libs/vuetify";
|
||||
|
||||
a {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Vuetify 3 don't provide margin bottom style like vuetify 2
|
||||
p {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
// Iconify icon size
|
||||
svg.iconify {
|
||||
block-size: 1em;
|
||||
inline-size: 1em;
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
@use "@configureTheme" as theme;
|
||||
@use "@configured-variables" as variables;
|
||||
@use "../mixins";
|
||||
|
||||
// 👉 Apex chart
|
||||
.apexcharts-canvas {
|
||||
// For RTL alignment
|
||||
.apexcharts-yaxis-texts-g {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
.apexcharts-tooltip {
|
||||
line-height: 1.5;
|
||||
|
||||
.apexcharts-tooltip-title {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
font-weight: 500;
|
||||
margin-block-end: 0.25rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
font-size: inherit;
|
||||
gap: 0.5rem;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text-label,
|
||||
.apexcharts-tooltip-text-value {
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group {
|
||||
padding-block: 0 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
&:last-child {
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.apexcharts-theme-light {
|
||||
border-color: rgb(var(--v-border-color));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: none;
|
||||
|
||||
.apexcharts-tooltip-text-label,
|
||||
.apexcharts-tooltip-text-value {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-marker {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 👉 stroke-dasharray
|
||||
.apexcharts-radialbar,
|
||||
.apexcharts-radialbar-slice-current {
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip,
|
||||
.apexcharts-yaxistooltip {
|
||||
border-color: rgb(var(--v-border-color));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
border-block-end-color: rgb(var(--v-border-color));
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Text color
|
||||
.apexcharts-text,
|
||||
.apexcharts-tooltip-text,
|
||||
.apexcharts-datalabel-label,
|
||||
.apexcharts-datalabel,
|
||||
.apexcharts-xaxistooltip-text,
|
||||
.apexcharts-yaxistooltip-text,
|
||||
.apexcharts-legend-text {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)) !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
// 👉 Annotation Label
|
||||
.apexcharts-annotation-rect {
|
||||
&.apexcharts-xaxis-annotation-rect,
|
||||
&.apexcharts-yaxis-annotation-rect {
|
||||
fill-opacity: 0.05;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "../../../utils";
|
||||
|
||||
// 👉 Application
|
||||
// ℹ️ We need accurate vh in mobile devices as well
|
||||
.v-application__wrap {
|
||||
/* stylelint-disable-next-line liberty/use-logical-spec */
|
||||
min-height: calc(var(--vh, 1vh) * 100);
|
||||
}
|
||||
|
||||
// 👉 Typography
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.text-h1,
|
||||
.text-h2,
|
||||
.text-h3,
|
||||
.text-h4,
|
||||
.text-h5,
|
||||
.text-h6,
|
||||
.text-button,
|
||||
.text-overline,
|
||||
.v-card-title {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
.text-body-1,
|
||||
.text-body-2,
|
||||
.text-subtitle-1,
|
||||
.text-subtitle-2 {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
|
||||
// 👉 Grid
|
||||
// Remove margin-bottom of v-input_details inside grid (validation error message)
|
||||
.v-row {
|
||||
.v-col,
|
||||
[class^="v-col-*"] {
|
||||
.v-input__details {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Button
|
||||
@if variables.$vuetify-reduce-default-compact-button-icon-size {
|
||||
.v-btn--density-compact.v-btn--size-default {
|
||||
.v-btn__content > svg {
|
||||
block-size: 22px;
|
||||
font-size: 22px;
|
||||
inline-size: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Card
|
||||
// Removes padding-top for immediately placed v-card-text after itself
|
||||
.v-card-text {
|
||||
& + & {
|
||||
padding-block-start: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
👉 Checkbox & Radio Ripple
|
||||
|
||||
TODO Checkbox and switch component. Remove it when vuetify resolve the extra spacing: https://github.com/vuetifyjs/vuetify/issues/15519
|
||||
We need this because form elements likes checkbox and switches are by default set to height of textfield height which is way big than we want
|
||||
Tested with checkbox & switches
|
||||
*/
|
||||
.v-checkbox.v-input,
|
||||
.v-switch.v-input {
|
||||
--v-input-control-height: auto;
|
||||
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.v-selection-control--density-comfortable {
|
||||
&.v-checkbox-btn,
|
||||
&.v-radio,
|
||||
&.v-radio-btn {
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: -0.5625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-selection-control--density-compact {
|
||||
&.v-radio,
|
||||
&.v-radio-btn,
|
||||
&.v-checkbox-btn {
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: -0.3125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-selection-control--density-default {
|
||||
&.v-checkbox-btn,
|
||||
&.v-radio,
|
||||
&.v-radio-btn {
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: -0.6875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-radio-group {
|
||||
.v-selection-control-group {
|
||||
.v-radio:not(:last-child) {
|
||||
margin-inline-end: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
👉 Tabs
|
||||
Disable tab transition
|
||||
|
||||
This is for tabs where we don't have card wrapper to tabs and have multiple cards as tab content.
|
||||
|
||||
This class will disable transition and adds `overflow: unset` on `VWindow` to allow spreading shadow
|
||||
*/
|
||||
.disable-tab-transition {
|
||||
overflow: unset !important;
|
||||
|
||||
.v-window__container {
|
||||
block-size: auto !important;
|
||||
}
|
||||
|
||||
.v-window-item:not(.v-window-item--active) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.v-window__container .v-window-item {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 List
|
||||
.v-list {
|
||||
// Set icons opacity to .87
|
||||
.v-list-item__prepend > .v-icon,
|
||||
.v-list-item__append > .v-icon {
|
||||
opacity: var(--v-high-emphasis-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Card list
|
||||
|
||||
/*
|
||||
ℹ️ Custom class
|
||||
|
||||
Remove list spacing inside card
|
||||
|
||||
This is because card title gets padding of 20px and list item have padding of 16px. Moreover, list container have padding-bottom as well.
|
||||
*/
|
||||
.card-list {
|
||||
--v-card-list-gap: 20px;
|
||||
|
||||
&.v-list {
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
min-block-size: unset;
|
||||
min-block-size: auto !important;
|
||||
padding-block: 0 !important;
|
||||
padding-inline: 0 !important;
|
||||
|
||||
> .v-ripple__container {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-block-end: var(--v-card-list-gap) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item:hover,
|
||||
.v-list-item:focus,
|
||||
.v-list-item:active,
|
||||
.v-list-item.active {
|
||||
> .v-list-item__overlay {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Divider
|
||||
.v-divider {
|
||||
color: rgb(var(--v-border-color));
|
||||
}
|
||||
|
||||
// 👉 DataTable
|
||||
.v-data-table {
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
.v-checkbox-btn .v-selection-control__wrapper {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.v-selection-control {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.v-pagination {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 v-field
|
||||
.v-field:hover .v-field__outline {
|
||||
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
|
||||
}
|
||||
|
||||
// 👉 VLabel
|
||||
.v-label {
|
||||
opacity: 1 !important;
|
||||
|
||||
&:not(.v-field-label--floating) {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Overlay
|
||||
.v-overlay__scrim,
|
||||
.v-navigation-drawer__scrim {
|
||||
background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity)) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
// 👉 VMessages
|
||||
.v-messages {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 👉 Alert close btn
|
||||
.v-alert__close {
|
||||
.v-btn--icon .v-icon {
|
||||
--v-icon-size-multiplier: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Badge icon alignment
|
||||
.v-badge__badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 👉 Dialog
|
||||
.v-dialog--fullscreen {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
// For dialog card title
|
||||
.v-card-item + .v-card-text {
|
||||
padding-block-start: 0 !important;
|
||||
}
|
||||
|
||||
// 👉 v-slide-group (List of chips)
|
||||
.v-slide-group {
|
||||
.v-slide-group__container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
// Spacing between buttons in v-slide-group
|
||||
.v-slide-group-item:not(:last-child) {
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Expansion Panel
|
||||
.v-expansion-panels {
|
||||
.v-expansion-panel-title {
|
||||
min-block-size: unset !important;
|
||||
padding-block: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 v-textarea
|
||||
.v-textarea {
|
||||
textarea {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Cursor
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
|
||||
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
|
||||
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
|
||||
/* stylelint-disable-next-line max-line-length */
|
||||
$font-family-custom: 'Inter', 'Noto Sans SC', sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
||||
// 👉 Card transition properties
|
||||
$card-transition-property-custom: box-shadow, opacity;
|
||||
|
||||
@forward "vuetify/settings" with (
|
||||
// 👉 General settings
|
||||
$color-pack: false !default,
|
||||
|
||||
// 👉 Shadow opacity
|
||||
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
|
||||
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
|
||||
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
|
||||
|
||||
$body-font-family: $font-family-custom !default,
|
||||
$border-radius-root: 6px !default,
|
||||
|
||||
$shadow-key-umbra: (
|
||||
0: (0 0 0 0 var(--v-shadow-key-umbra-opacity)),
|
||||
1: (0 2px 1px -1px var(--v-shadow-key-umbra-opacity)),
|
||||
2: (0 3px 1px -2px var(--v-shadow-key-umbra-opacity)),
|
||||
|
||||
// ℹ️ Modified
|
||||
3: (0 4px 14px -4px var(--v-shadow-key-umbra-opacity)),
|
||||
|
||||
4: (0 2px 4px -1px var(--v-shadow-key-umbra-opacity)),
|
||||
5: (0 3px 5px -1px var(--v-shadow-key-umbra-opacity)),
|
||||
|
||||
// ℹ️ Modified
|
||||
6: (0 4px 5px -2px var(--v-shadow-key-umbra-opacity)),
|
||||
|
||||
7: (0 4px 5px -2px var(--v-shadow-key-umbra-opacity)),
|
||||
8: (0 5px 5px -3px var(--v-shadow-key-umbra-opacity)),
|
||||
9: (0 5px 6px -3px var(--v-shadow-key-umbra-opacity)),
|
||||
10: (0 6px 6px -3px var(--v-shadow-key-umbra-opacity)),
|
||||
11: (0 6px 7px -4px var(--v-shadow-key-umbra-opacity)),
|
||||
12: (0 7px 8px -4px var(--v-shadow-key-umbra-opacity)),
|
||||
13: (0 7px 8px -4px var(--v-shadow-key-umbra-opacity)),
|
||||
14: (0 7px 9px -4px var(--v-shadow-key-umbra-opacity)),
|
||||
15: (0 8px 9px -5px var(--v-shadow-key-umbra-opacity)),
|
||||
16: (0 8px 10px -5px var(--v-shadow-key-umbra-opacity)),
|
||||
17: (0 8px 11px -5px var(--v-shadow-key-umbra-opacity)),
|
||||
18: (0 9px 11px -5px var(--v-shadow-key-umbra-opacity)),
|
||||
19: (0 9px 12px -6px var(--v-shadow-key-umbra-opacity)),
|
||||
20: (0 10px 13px -6px var(--v-shadow-key-umbra-opacity)),
|
||||
21: (0 10px 13px -6px var(--v-shadow-key-umbra-opacity)),
|
||||
22: (0 10px 14px -6px var(--v-shadow-key-umbra-opacity)),
|
||||
23: (0 11px 14px -7px var(--v-shadow-key-umbra-opacity)),
|
||||
24: (0 11px 15px -7px var(--v-shadow-key-umbra-opacity))
|
||||
) !default,
|
||||
|
||||
$shadow-key-penumbra: (
|
||||
0: (0 0 0 0 $shadow-key-penumbra-opacity-custom),
|
||||
1: (0 1px 1px 0 $shadow-key-penumbra-opacity-custom),
|
||||
2: (0 2px 2px 0 $shadow-key-penumbra-opacity-custom),
|
||||
|
||||
// ℹ️ Modified
|
||||
3: (0 4px 8px -4px $shadow-key-penumbra-opacity-custom),
|
||||
|
||||
4: (0 4px 5px 0 $shadow-key-penumbra-opacity-custom),
|
||||
5: (0 5px 8px 0 $shadow-key-penumbra-opacity-custom),
|
||||
|
||||
// ℹ️ Modified
|
||||
6: (0 2px 10px 1px $shadow-key-penumbra-opacity-custom),
|
||||
|
||||
7: (0 7px 10px 1px $shadow-key-penumbra-opacity-custom),
|
||||
8: (0 8px 10px 1px $shadow-key-penumbra-opacity-custom),
|
||||
9: (0 9px 12px 1px $shadow-key-penumbra-opacity-custom),
|
||||
10: (0 10px 14px 1px $shadow-key-penumbra-opacity-custom),
|
||||
11: (0 11px 15px 1px $shadow-key-penumbra-opacity-custom),
|
||||
12: (0 12px 17px 2px $shadow-key-penumbra-opacity-custom),
|
||||
13: (0 13px 19px 2px $shadow-key-penumbra-opacity-custom),
|
||||
14: (0 14px 21px 2px $shadow-key-penumbra-opacity-custom),
|
||||
15: (0 15px 22px 2px $shadow-key-penumbra-opacity-custom),
|
||||
16: (0 16px 24px 2px $shadow-key-penumbra-opacity-custom),
|
||||
17: (0 17px 26px 2px $shadow-key-penumbra-opacity-custom),
|
||||
18: (0 18px 28px 2px $shadow-key-penumbra-opacity-custom),
|
||||
19: (0 19px 29px 2px $shadow-key-penumbra-opacity-custom),
|
||||
20: (0 20px 31px 3px $shadow-key-penumbra-opacity-custom),
|
||||
21: (0 21px 33px 3px $shadow-key-penumbra-opacity-custom),
|
||||
22: (0 22px 35px 3px $shadow-key-penumbra-opacity-custom),
|
||||
23: (0 23px 36px 3px $shadow-key-penumbra-opacity-custom),
|
||||
24: (0 24px 38px 3px $shadow-key-penumbra-opacity-custom)
|
||||
) !default,
|
||||
|
||||
$shadow-key-ambient: (
|
||||
0: (0 0 0 0 $shadow-key-ambient-opacity-custom),
|
||||
1: (0 1px 3px 0 $shadow-key-ambient-opacity-custom),
|
||||
2: (0 1px 5px 0 $shadow-key-ambient-opacity-custom),
|
||||
|
||||
// ℹ️ Modified
|
||||
3: (0 4px 8px -4px $shadow-key-ambient-opacity-custom),
|
||||
|
||||
4: (0 1px 10px 0 $shadow-key-ambient-opacity-custom),
|
||||
5: (0 1px 14px 0 $shadow-key-ambient-opacity-custom),
|
||||
|
||||
// ℹ️ Modified
|
||||
6: (0 2px 16px 1px $shadow-key-ambient-opacity-custom),
|
||||
|
||||
7: (0 2px 16px 1px $shadow-key-ambient-opacity-custom),
|
||||
8: (0 3px 14px 2px $shadow-key-ambient-opacity-custom),
|
||||
9: (0 3px 16px 2px $shadow-key-ambient-opacity-custom),
|
||||
10: (0 4px 18px 3px $shadow-key-ambient-opacity-custom),
|
||||
11: (0 4px 20px 3px $shadow-key-ambient-opacity-custom),
|
||||
12: (0 5px 22px 4px $shadow-key-ambient-opacity-custom),
|
||||
13: (0 5px 24px 4px $shadow-key-ambient-opacity-custom),
|
||||
14: (0 5px 26px 4px $shadow-key-ambient-opacity-custom),
|
||||
15: (0 6px 28px 5px $shadow-key-ambient-opacity-custom),
|
||||
16: (0 6px 30px 5px $shadow-key-ambient-opacity-custom),
|
||||
17: (0 6px 32px 5px $shadow-key-ambient-opacity-custom),
|
||||
18: (0 7px 34px 6px $shadow-key-ambient-opacity-custom),
|
||||
19: (0 7px 36px 6px $shadow-key-ambient-opacity-custom),
|
||||
20: (0 8px 38px 7px $shadow-key-ambient-opacity-custom),
|
||||
21: (0 8px 40px 7px $shadow-key-ambient-opacity-custom),
|
||||
22: (0 8px 42px 7px $shadow-key-ambient-opacity-custom),
|
||||
23: (0 9px 44px 8px $shadow-key-ambient-opacity-custom),
|
||||
24: (0 9px 46px 8px $shadow-key-ambient-opacity-custom)
|
||||
) !default,
|
||||
|
||||
// 👉 Card
|
||||
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
|
||||
$card-elevation: 6 !default,
|
||||
$card-title-line-height: 2rem !default,
|
||||
$card-actions-min-height: unset !default,
|
||||
$card-text-padding: 1.25rem !default,
|
||||
$card-item-padding: 1.25rem !default,
|
||||
$card-actions-padding: 0 12px 12px !default,
|
||||
$card-transition-property: $card-transition-property-custom !default,
|
||||
$card-subtitle-opacity: 1 !default,
|
||||
$card-title-letter-spacing: 0.0094rem !default,
|
||||
|
||||
// 👉 Typography
|
||||
$typography: (
|
||||
"h1": (
|
||||
"weight": 500,
|
||||
"line-height": 7rem,
|
||||
"letter-spacing": -0.0938rem
|
||||
),
|
||||
"h2": (
|
||||
"weight": 500,
|
||||
"line-height": 4.5rem,
|
||||
"letter-spacing": -0.0313rem
|
||||
),
|
||||
"h3": (
|
||||
"weight": 500,
|
||||
"line-height": 3.5rem
|
||||
),
|
||||
"h4": (
|
||||
"weight": 500,
|
||||
"line-height": 2.625rem,
|
||||
"letter-spacing": 0.0156rem
|
||||
),
|
||||
"h5": (
|
||||
"weight": 500,
|
||||
"line-height": 2rem
|
||||
),
|
||||
"h6": (
|
||||
"letter-spacing": 0.0094rem
|
||||
),
|
||||
"subtitle-1": (
|
||||
"letter-spacing": 0.0094rem
|
||||
),
|
||||
"subtitle-2": (
|
||||
"line-height": 1.375rem,
|
||||
"letter-spacing": 0.0063rem,
|
||||
),
|
||||
"body-1": (
|
||||
"letter-spacing": 0.0094rem,
|
||||
),
|
||||
"body-2": (
|
||||
"letter-spacing": 0.0094rem,
|
||||
),
|
||||
"caption": (
|
||||
"letter-spacing": 0.025rem,
|
||||
),
|
||||
"overline": (
|
||||
"weight": 400,
|
||||
"line-height": 1.125rem,
|
||||
"letter-spacing": 0.0625rem,
|
||||
)
|
||||
) !default,
|
||||
|
||||
// 👉 List
|
||||
$list-item-icon-margin-end: 16px !default,
|
||||
$list-item-icon-margin-start: 16px !default,
|
||||
$list-item-subtitle-opacity: 1 !default,
|
||||
$list-subheader-text-opacity: 1 !default,
|
||||
|
||||
// 👉 Tooltip
|
||||
$tooltip-background-color: #212121 !default,
|
||||
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
|
||||
$tooltip-font-size: 0.75rem !default,
|
||||
$tooltip-border-radius: 4px !default,
|
||||
$tooltip-padding: 4px 8px !default,
|
||||
|
||||
// 👉 Alert
|
||||
$alert-title-font-size: 1rem !default,
|
||||
$alert-border-radius: 5px !default,
|
||||
$alert-title-letter-spacing: 0.15px !default,
|
||||
|
||||
// 👉 Badge
|
||||
$badge-border-color:rgb(var(--v-theme-surface)) !default,
|
||||
$badge-dot-height: 0.5rem !default,
|
||||
$badge-dot-width: 0.5rem !default,
|
||||
|
||||
// 👉 Button
|
||||
$button-height: 38px !default,
|
||||
$button-elevation: ("default": 3, "hover": 4, "active": 8) !default,
|
||||
$button-border-radius: 5px !default,
|
||||
$button-padding-ratio: 1.7 !default,
|
||||
$button-text-letter-spacing: 0.025rem !default,
|
||||
$button-icon-density: ("default": 0.5, "comfortable": -2, "compact": -3) !default,
|
||||
|
||||
// 👉 Dialog
|
||||
$dialog-card-header-padding: 20px !default,
|
||||
$dialog-card-header-text-padding-top: 0 !default,
|
||||
$dialog-card-text-padding: 20px !default,
|
||||
|
||||
// 👉 Chip
|
||||
$chip-label-border-radius: 4px !default,
|
||||
$chip-close-size: 20px !default,
|
||||
|
||||
// 👉 Expansion panel
|
||||
$expansion-panel-title-padding: 16px 20px !default,
|
||||
$expansion-panel-title-font-size: 1rem !default,
|
||||
$expansion-panel-disabled-overlay: 0 !default,
|
||||
$expansion-panel-active-title-min-height: 51px !default,
|
||||
$expansion-panel-title-min-height: 51px !default,
|
||||
$expansion-panel-text-padding: 0 20px 20px !default,
|
||||
|
||||
// 👉 Menu
|
||||
$menu-content-border-radius: 5px !default,
|
||||
|
||||
// 👉 Snackbar
|
||||
$snackbar-background:#212121 !default,
|
||||
$snackbar-border-radius: 4px !default,
|
||||
$snackbar-color: rgb(var(--v-theme-on-primary)) !default,
|
||||
|
||||
// 👉 Tabs
|
||||
$tabs-height: 40px !default,
|
||||
|
||||
// 👉 Slider
|
||||
$slider-track-active-size: 4px !default,
|
||||
$slider-thumb-label-padding: 4px 12px !default,
|
||||
$slider-thumb-label-font-size: 0.875rem !default,
|
||||
|
||||
// 👉 Timeline
|
||||
$timeline-dot-size: 34px !default,
|
||||
$timeline-dot-divider-background: transparent !default,
|
||||
|
||||
// 👉 Overlay
|
||||
$overlay-opacity: 0.5 !default,
|
||||
|
||||
// 👉 Navigation Drawer
|
||||
$navigation-drawer-scrim-opacity:0.5 !default,
|
||||
|
||||
// 👉 Table
|
||||
$table-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)),
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
@use "variables";
|
||||
@use "overrides";
|
||||
@@ -1,25 +0,0 @@
|
||||
.layout-blank {
|
||||
.misc-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.25rem;
|
||||
overflow: hidden;
|
||||
|
||||
.misc-footer-img {
|
||||
position: absolute;
|
||||
inline-size: 100%;
|
||||
inset-block-end: 0;
|
||||
}
|
||||
|
||||
.misc-footer-tree {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.misc-avatar {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
.layout-blank {
|
||||
.auth-wrapper {
|
||||
min-block-size: calc(var(--vh, 1vh) * 100);
|
||||
|
||||
.auth-footer-mask {
|
||||
position: absolute;
|
||||
inset-block-end: 0;
|
||||
min-inline-size: 100%;
|
||||
}
|
||||
|
||||
.auth-footer-start-tree,
|
||||
.auth-footer-end-tree {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.auth-footer-start-tree {
|
||||
inset-block-end: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
.auth-footer-end-tree {
|
||||
inset-block-end: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.auth-illustration {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
z-index: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.skin--bordered {
|
||||
.auth-card-v2 {
|
||||
border-inline-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset-block-start: 2rem;
|
||||
inset-inline-start: 2.3rem;
|
||||
}
|
||||
|
||||
.auth-card-v2 {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "misc";
|
||||
@use "../mixins";
|
||||
|
||||
%default-layout-vertical-nav-scrolled-sticky-elevated-nav {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
|
||||
// @include mixins.elevation(variables.$vertical-nav-navbar-elevation);
|
||||
|
||||
// If navbar is contained => Squeeze navbar content on scroll
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
%default-layout-vertical-nav-floating-navbar-overlay {
|
||||
isolation: isolate;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
/* stylelint-enable */
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(var(--v-theme-background), 70%) 44%,
|
||||
rgba(var(--v-theme-background), 43%) 73%,
|
||||
rgba(var(--v-theme-background), 0%)
|
||||
);
|
||||
background-repeat: repeat;
|
||||
block-size: calc(variables.$layout-vertical-nav-navbar-height + variables.$vertical-nav-floating-navbar-top + 0.5rem);
|
||||
content: "";
|
||||
inset-block-start: -(variables.$vertical-nav-floating-navbar-top);
|
||||
inset-inline: 0;
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
-webkit-mask: linear-gradient(black, black 18%, transparent 100%);
|
||||
mask: linear-gradient(black, black 18%, transparent 100%);
|
||||
/* stylelint-enable */
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
%layout-navbar {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
@forward "nav";
|
||||
@forward "vertical-nav";
|
||||
@forward "default-layout";
|
||||
@forward "default-layout-vertical-nav";
|
||||
@forward "misc";
|
||||
@@ -1,120 +0,0 @@
|
||||
%blurry-bg {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
block-size: calc(env(safe-area-inset-top, 0px) + 5rem);
|
||||
content: "";
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease-in-out, background 0.2s ease-in-out;
|
||||
|
||||
// PC端样式 (默认)
|
||||
.v-theme--light & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-surface), 0.9) 0%,
|
||||
rgba(var(--v-theme-surface), 0.7) 20%,
|
||||
rgba(var(--v-theme-surface), 0.5) 40%,
|
||||
rgba(var(--v-theme-surface), 0.3) 60%,
|
||||
rgba(var(--v-theme-surface), 0.1) 80%,
|
||||
rgba(var(--v-theme-surface), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 0.8) 0%,
|
||||
rgba(var(--v-theme-background), 0.6) 20%,
|
||||
rgba(var(--v-theme-background), 0.4) 40%,
|
||||
rgba(var(--v-theme-background), 0.25) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--purple & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 0.8) 0%,
|
||||
rgba(var(--v-theme-background), 0.6) 20%,
|
||||
rgba(var(--v-theme-background), 0.4) 40%,
|
||||
rgba(var(--v-theme-background), 0.25) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--transparent & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(11, 11, 11, 60%) 0%,
|
||||
rgba(11, 11, 11, 50%) 20%,
|
||||
rgba(11, 11, 11, 40%) 40%,
|
||||
rgba(11, 11, 11, 25%) 60%,
|
||||
rgba(11, 11, 11, 10%) 80%,
|
||||
rgba(11, 11, 11, 0%) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端样式
|
||||
@media (pointer: coarse) {
|
||||
%blurry-bg {
|
||||
&::before {
|
||||
.v-theme--light & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-surface), 1) 0%,
|
||||
rgba(var(--v-theme-surface), 0.9) 20%,
|
||||
rgba(var(--v-theme-surface), 0.7) 40%,
|
||||
rgba(var(--v-theme-surface), 0.5) 60%,
|
||||
rgba(var(--v-theme-surface), 0.2) 80%,
|
||||
rgba(var(--v-theme-surface), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 1) 0%,
|
||||
rgba(var(--v-theme-background), 0.85) 20%,
|
||||
rgba(var(--v-theme-background), 0.7) 40%,
|
||||
rgba(var(--v-theme-background), 0.5) 60%,
|
||||
rgba(var(--v-theme-background), 0.3) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--purple & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 1) 0%,
|
||||
rgba(var(--v-theme-background), 0.85) 20%,
|
||||
rgba(var(--v-theme-background), 0.7) 40%,
|
||||
rgba(var(--v-theme-background), 0.5) 60%,
|
||||
rgba(var(--v-theme-background), 0.3) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--transparent & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(11, 11, 11, 90%) 0%,
|
||||
rgba(11, 11, 11, 80%) 20%,
|
||||
rgba(11, 11, 11, 60%) 40%,
|
||||
rgba(11, 11, 11, 40%) 60%,
|
||||
rgba(11, 11, 11, 15%) 80%,
|
||||
rgba(11, 11, 11, 0%) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
%nav-link-active {
|
||||
background:
|
||||
linear-gradient(
|
||||
-72.47deg,
|
||||
rgb(var(--v-theme-primary)) 22.16%,
|
||||
rgba(var(--v-theme-primary), 0.7) 76.47%
|
||||
) !important;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// ℹ️ Add divider around section title
|
||||
%vertical-nav-section-title {
|
||||
/*
|
||||
ℹ️ We will use this to add gap between divider and text.
|
||||
Moreover, we will use this to adjust the `flex-basis` property of left divider
|
||||
*/
|
||||
$divider-gap: 0.625rem;
|
||||
|
||||
// Thanks: https://stackoverflow.com/a/62359101/10796681
|
||||
.title-text {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
column-gap: $divider-gap;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
content: "";
|
||||
}
|
||||
|
||||
&::after {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&::before {
|
||||
flex: 0 1 calc(variables.$vertical-nav-horizontal-padding-start - $divider-gap);
|
||||
margin-inline-start: -#{variables.$vertical-nav-horizontal-padding-start};
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ Update the margin-inline-end when vertical nav is in mini state. We done same for link & group.
|
||||
@at-root {
|
||||
.layout-nav-type-vertical.layout-vertical-nav-collapsed .layout-vertical-nav:not(.hovered) .nav-section-title {
|
||||
margin-inline: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
%vertical-nav-item-interactive {
|
||||
// Add pill shape styles
|
||||
block-size: 2.625rem !important;
|
||||
border-end-end-radius: 3.125rem !important;
|
||||
border-end-start-radius: 0 !important;
|
||||
border-start-end-radius: 3.125rem !important;
|
||||
border-start-start-radius: 0 !important;
|
||||
}
|
||||
|
||||
%vertical-nav-item-interactive {
|
||||
// ℹ️ Wobble effect
|
||||
// transition: margin-inline 0.4s ease-in-out;
|
||||
// will-change: margin-inline;
|
||||
|
||||
transition: margin-inline 0.15s ease-in-out;
|
||||
will-change: margin-inline;
|
||||
|
||||
// Reduce margin inline end when vertical nav is in collapsed mode and not hovered
|
||||
.layout-nav-type-vertical.layout-vertical-nav-collapsed .layout-vertical-nav:not(.hovered) & {
|
||||
margin-inline: 0 5px;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,13 @@ export function kFormatter(num: number) {
|
||||
: Math.abs(num).toFixed(0).replace(regex, ',')
|
||||
}
|
||||
|
||||
// 格式化下载量显示,超过1000显示为x.xk格式
|
||||
export function formatDownloadCount(num: number): string {
|
||||
if (!num || num < 1000) return num?.toLocaleString() || '0'
|
||||
|
||||
return `${(num / 1000).toFixed(1)}k`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format and return date in Humanize format
|
||||
* Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format
|
||||
|
||||
@@ -2,8 +2,7 @@ import ColorThief from 'colorthief'
|
||||
|
||||
// 将 RGB 转换为十六进制
|
||||
function rgbStringToHex(rgbArray: number[]): string {
|
||||
if (rgbArray.length !== 3 || rgbArray.some(isNaN))
|
||||
throw new Error('Invalid RGB string format')
|
||||
if (rgbArray.length !== 3 || rgbArray.some(isNaN)) throw new Error('Invalid RGB string format')
|
||||
|
||||
const [r, g, b] = rgbArray
|
||||
|
||||
@@ -21,3 +20,27 @@ export async function getDominantColor(image: HTMLImageElement): Promise<string>
|
||||
const dominantColor = colorThief.getColor(image)
|
||||
return rgbStringToHex(dominantColor)
|
||||
}
|
||||
|
||||
// 预加载图片
|
||||
export async function preloadImage(url: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = () => resolve(true)
|
||||
img.onerror = () => resolve(false)
|
||||
|
||||
// 设置超时,防止图片长时间加载
|
||||
const timeout = setTimeout(() => {
|
||||
img.src = ''
|
||||
resolve(false)
|
||||
}, 5000) // 5秒超时
|
||||
|
||||
img.src = url
|
||||
|
||||
// 如果图片已经缓存,onload可能不会触发
|
||||
if (img.complete) {
|
||||
clearTimeout(timeout)
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,3 +65,6 @@ export function getQueryValue(key: string, url = window.location.href): string {
|
||||
const res = reg.exec(url)
|
||||
return res ? res[1] : ''
|
||||
}
|
||||
|
||||
// 导出 navigator 相关函数
|
||||
export { isMobileDevice, isIOSDevice, isAndroidDevice } from './navigator'
|
||||
|
||||
@@ -35,6 +35,19 @@ export function urlBase64ToUint8Array(base64String: string) {
|
||||
return outputArray
|
||||
}
|
||||
|
||||
// Uint8Array 转 Base64URL
|
||||
export function bufferToBase64Url(buffer: ArrayBuffer): string {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '')
|
||||
}
|
||||
|
||||
// Base64URL 转 Uint8Array
|
||||
export function base64UrlToUint8Array(base64Url: string): Uint8Array {
|
||||
return Uint8Array.from(atob(base64Url.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
|
||||
}
|
||||
|
||||
// 判断是否为PWA
|
||||
export const isPWA = async (): Promise<boolean> => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
@@ -43,3 +56,56 @@ export const isPWA = async (): Promise<boolean> => {
|
||||
}
|
||||
return (window.navigator as any).standalone === true
|
||||
}
|
||||
|
||||
// 同步检测PWA显示模式
|
||||
export const isPWADisplayMode = (): boolean => {
|
||||
return (
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(window.navigator as any).standalone ||
|
||||
document.referrer.includes('android-app://')
|
||||
)
|
||||
}
|
||||
|
||||
// 全面的PWA检测(推荐使用)
|
||||
export const checkPWAStatus = async () => {
|
||||
const hasServiceWorker = await isPWA()
|
||||
const isStandaloneMode = isPWADisplayMode()
|
||||
|
||||
return {
|
||||
// 是否有PWA功能(Service Worker)
|
||||
hasPWAFeatures: hasServiceWorker,
|
||||
// 是否在独立显示模式下运行
|
||||
isStandaloneMode,
|
||||
// 综合判断:更宽松的检测,在移动设备上默认启用PWA功能
|
||||
isPWAEnvironment: hasServiceWorker || isStandaloneMode || isMobileDevice(),
|
||||
// 完整的PWA体验:既有功能又在独立模式下运行
|
||||
isFullPWA: hasServiceWorker && isStandaloneMode,
|
||||
}
|
||||
}
|
||||
|
||||
// 检测是否为移动设备
|
||||
export const isMobileDevice = (): boolean => {
|
||||
// 检查用户代理字符串
|
||||
const userAgent = navigator.userAgent || ''
|
||||
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i
|
||||
|
||||
// 检查触摸屏支持
|
||||
const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0
|
||||
|
||||
// 检查屏幕尺寸(小于768px认为是移动设备)
|
||||
const isMobileSize = window.innerWidth < 768
|
||||
|
||||
return mobileRegex.test(userAgent) || hasTouchScreen || isMobileSize
|
||||
}
|
||||
|
||||
// 检测是否为iOS设备
|
||||
export const isIOSDevice = (): boolean => {
|
||||
const userAgent = navigator.userAgent.toLowerCase()
|
||||
return /iphone|ipad|ipod/.test(userAgent) && !(window as any).MSStream
|
||||
}
|
||||
|
||||
// 检测是否为Android设备
|
||||
export const isAndroidDevice = (): boolean => {
|
||||
const userAgent = navigator.userAgent.toLowerCase()
|
||||
return /android/.test(userAgent)
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ const sources: BundleScriptConfig = {
|
||||
// 'mdi:logout',
|
||||
// 'octicon:book-24',
|
||||
// 'octicon:code-square-24',
|
||||
'lucide:sparkles',
|
||||
'material-symbols:passkey',
|
||||
'line-md:loading-twotone-loop',
|
||||
],
|
||||
|
||||
json: [
|
||||
@@ -154,7 +157,13 @@ const target = join(__dirname, 'icons-bundle.js');
|
||||
// Sort icons by prefix
|
||||
const organizedList = organizeIconsList(sources.icons)
|
||||
for (const prefix in organizedList) {
|
||||
const filename = require.resolve(`@iconify/json/json/${prefix}.json`)
|
||||
let filename
|
||||
try {
|
||||
filename = require.resolve(`@iconify-json/${prefix}/icons.json`)
|
||||
}
|
||||
catch (err) {
|
||||
filename = require.resolve(`@iconify/json/json/${prefix}.json`)
|
||||
}
|
||||
|
||||
sourcesJSON.push({
|
||||
filename,
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Transition } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import VerticalNav from '@layouts/components/VerticalNav.vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup(props, { slots }) {
|
||||
const isOverlayNavActive = ref(false)
|
||||
const isLayoutOverlayVisible = ref(false)
|
||||
const toggleIsOverlayNavActive = useToggle(isOverlayNavActive)
|
||||
|
||||
const route = useRoute()
|
||||
const { mdAndDown } = useDisplay()
|
||||
|
||||
// ℹ️ This is alternative to below two commented watcher
|
||||
// We want to show overlay if overlay nav is visible and want to hide overlay if overlay is hidden and vice versa.
|
||||
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
|
||||
|
||||
const scrollDistance = ref(window.scrollY)
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', () => {
|
||||
scrollDistance.value = window.scrollY
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
// 👉 Vertical nav
|
||||
const verticalNav = h(
|
||||
VerticalNav,
|
||||
{ isOverlayNavActive: isOverlayNavActive.value, toggleIsOverlayNavActive },
|
||||
{
|
||||
'nav-header': () => slots['vertical-nav-header']?.(),
|
||||
'before-nav-items': () => slots['before-vertical-nav-items']?.(),
|
||||
'default': () => slots['vertical-nav-content']?.(),
|
||||
'after-nav-items': () => slots['after-vertical-nav-items']?.(),
|
||||
},
|
||||
)
|
||||
|
||||
// 👉 Navbar
|
||||
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
|
||||
h(
|
||||
'div',
|
||||
{ class: 'navbar-content-container' },
|
||||
slots.navbar?.({
|
||||
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
|
||||
}),
|
||||
),
|
||||
])
|
||||
|
||||
const main = h(
|
||||
'main',
|
||||
{ class: 'layout-page-content' },
|
||||
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true }, () =>
|
||||
h('section', { class: 'page-content-container' }, slots.default?.()),
|
||||
),
|
||||
)
|
||||
|
||||
// 👉 根据路由 meta 决定 footer 高度
|
||||
const shouldShowFooter = !route.meta.hideFooter
|
||||
|
||||
// 👉 Footer
|
||||
const footer = h('footer', { class: 'layout-footer' }, [
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: ['footer-content-container', !shouldShowFooter && 'footer-content-container-noheight'],
|
||||
},
|
||||
slots.footer?.(),
|
||||
),
|
||||
])
|
||||
|
||||
// 👉 Overlay
|
||||
const layoutOverlay = h('div', {
|
||||
class: ['layout-overlay', 'touch-none', { visible: isLayoutOverlayVisible.value }],
|
||||
onClick: () => {
|
||||
isLayoutOverlayVisible.value = !isLayoutOverlayVisible.value
|
||||
},
|
||||
})
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: [
|
||||
'layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid',
|
||||
'layout-navbar-fixed',
|
||||
mdAndDown.value && 'layout-overlay-nav',
|
||||
route.meta.layoutWrapperClasses,
|
||||
scrollDistance.value && 'window-scrolled',
|
||||
],
|
||||
},
|
||||
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@configured-variables' as variables;
|
||||
@use '@layouts/styles/placeholders';
|
||||
@use '@layouts/styles/mixins';
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
|
||||
block-size: 100%;
|
||||
|
||||
.layout-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-block-size: calc(var(--vh, 1vh) * 100);
|
||||
transition: padding-inline-start 0.2s ease-in-out;
|
||||
will-change: padding-inline-start;
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
inset-block-start: 0;
|
||||
|
||||
.navbar-content-container {
|
||||
block-size: calc(env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height);
|
||||
}
|
||||
|
||||
@at-root {
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
.layout-navbar {
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
@include mixins.boxed-content;
|
||||
} @else {
|
||||
.navbar-content-container {
|
||||
// @include mixins.boxed-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-navbar-fixed .layout-navbar {
|
||||
@extend %layout-navbar-fixed;
|
||||
}
|
||||
|
||||
&.layout-navbar-hidden .layout-navbar {
|
||||
@extend %layout-navbar-hidden;
|
||||
}
|
||||
|
||||
// 👉 Footer
|
||||
.layout-footer {
|
||||
@include mixins.boxed-content;
|
||||
}
|
||||
|
||||
// 👉 Layout overlay
|
||||
.layout-overlay {
|
||||
position: fixed;
|
||||
z-index: variables.$layout-overlay-z-index;
|
||||
background-color: rgb(0 0 0 / 60%);
|
||||
cursor: pointer;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
will-change: transform;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.layout-overlay-nav) .layout-content-wrapper {
|
||||
padding-inline-start: variables.$layout-vertical-nav-width;
|
||||
}
|
||||
|
||||
// Adjust right column pl when vertical nav is collapsed
|
||||
&.layout-vertical-nav-collapsed .layout-content-wrapper {
|
||||
padding-inline-start: variables.$layout-vertical-nav-collapsed-width;
|
||||
}
|
||||
|
||||
// 👉 Content height fixed
|
||||
&.layout-content-height-fixed {
|
||||
.layout-content-wrapper {
|
||||
max-block-size: calc(var(--vh) * 100);
|
||||
}
|
||||
|
||||
.layout-page-content {
|
||||
// display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
.page-content-container {
|
||||
inline-size: 100%;
|
||||
|
||||
> :first-child {
|
||||
max-block-size: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
|
||||
.layout-navbar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -17,11 +17,34 @@ export default defineComponent({
|
||||
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
|
||||
|
||||
const scrollDistance = ref(window.scrollY)
|
||||
const isDialogOpen = ref(false)
|
||||
const wasScrolledBeforeDialog = ref(false)
|
||||
|
||||
// 监听弹窗状态变化
|
||||
const checkDialogState = () => {
|
||||
const wasDialogOpen = isDialogOpen.value
|
||||
isDialogOpen.value = document.documentElement.classList.contains('v-overlay-scroll-blocked')
|
||||
|
||||
// 当弹窗刚打开时,记录当前的滚动状态
|
||||
if (!wasDialogOpen && isDialogOpen.value) {
|
||||
wasScrolledBeforeDialog.value = scrollDistance.value > 0
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', () => {
|
||||
scrollDistance.value = window.scrollY
|
||||
})
|
||||
|
||||
// 初始检查弹窗状态
|
||||
checkDialogState()
|
||||
|
||||
// 监听 DOM 变化以检测弹窗状态
|
||||
const observer = new MutationObserver(checkDialogState)
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
@@ -38,15 +61,25 @@ export default defineComponent({
|
||||
)
|
||||
|
||||
// 👉 Navbar
|
||||
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
|
||||
h(
|
||||
'div',
|
||||
{ class: 'navbar-content-container' },
|
||||
slots.navbar?.({
|
||||
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
|
||||
}),
|
||||
),
|
||||
])
|
||||
const navbar = h(
|
||||
'header',
|
||||
{ class: ['layout-navbar navbar-blur'] },
|
||||
[
|
||||
h(
|
||||
'div',
|
||||
{ class: 'navbar-content-container' },
|
||||
[
|
||||
slots.navbar?.({
|
||||
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
|
||||
}),
|
||||
// 👉 Dynamic Header Tab in NavBar
|
||||
slots['dynamic-header-tab']?.()
|
||||
? h('div', { class: 'layout-dynamic-header-tab' }, slots['dynamic-header-tab']?.())
|
||||
: null,
|
||||
].filter(Boolean),
|
||||
),
|
||||
].filter(Boolean),
|
||||
)
|
||||
|
||||
const main = h(
|
||||
'main',
|
||||
@@ -86,7 +119,7 @@ export default defineComponent({
|
||||
'layout-navbar-fixed',
|
||||
mdAndDown.value && 'layout-overlay-nav',
|
||||
route.meta.layoutWrapperClasses,
|
||||
scrollDistance.value && 'window-scrolled',
|
||||
(scrollDistance.value > 5 || (isDialogOpen.value && wasScrolledBeforeDialog.value)) && 'window-scrolled',
|
||||
],
|
||||
},
|
||||
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
|
||||
@@ -109,7 +142,7 @@ export default defineComponent({
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
|
||||
block-size: 100%;
|
||||
min-block-size: 100%;
|
||||
|
||||
.layout-content-wrapper {
|
||||
display: flex;
|
||||
@@ -127,7 +160,9 @@ export default defineComponent({
|
||||
inset-block-start: 0;
|
||||
|
||||
.navbar-content-container {
|
||||
block-size: calc(env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height);
|
||||
block-size: calc(
|
||||
env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height + var(--navbar-tab-height)
|
||||
);
|
||||
}
|
||||
|
||||
@at-root {
|
||||
@@ -135,10 +170,6 @@ export default defineComponent({
|
||||
.layout-navbar {
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
@include mixins.boxed-content;
|
||||
} @else {
|
||||
.navbar-content-container {
|
||||
// @include mixins.boxed-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +224,9 @@ export default defineComponent({
|
||||
|
||||
.layout-page-content {
|
||||
// display: flex;
|
||||
overflow: hidden;
|
||||
// 使用 clip 替代 hidden,避免 Chrome 144+ 滚动锁定问题
|
||||
overflow-x: clip;
|
||||
overflow-y: auto;
|
||||
|
||||
.page-content-container {
|
||||
inline-size: 100%;
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
|
||||
html {
|
||||
background: rgb(var(--v-theme-background));
|
||||
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
overflow-y: overlay;
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
}
|
||||
|
||||
body {
|
||||
background: rgb(var(--v-theme-background));
|
||||
overscroll-behavior-y: contain;
|
||||
// Chrome 144+ 兼容性:覆盖 Vuetify 的内联 overflow: hidden 样式
|
||||
overflow: visible !important;
|
||||
|
||||
--webkit-overflow-scrolling: touch;
|
||||
}
|
||||
@@ -35,13 +37,15 @@ body,
|
||||
.layout-page-content {
|
||||
@include mixins.boxed-content(true);
|
||||
|
||||
overflow: hidden;
|
||||
// Chrome 144+ 兼容性:使用 clip 替代 hidden,避免滚动锁定问题
|
||||
// overflow: hidden 在新版 Chrome 中可能意外阻止垂直滚动
|
||||
overflow: clip;
|
||||
flex-grow: 1;
|
||||
|
||||
// TODO: Use grid gutter variable here;
|
||||
padding-block: 1.5rem;
|
||||
padding-inline: 0.5rem;
|
||||
padding-block-start: calc(env(safe-area-inset-top) + 4.5rem);
|
||||
padding-inline: 0.5rem;
|
||||
|
||||
// display: flex;display
|
||||
|
||||
|
||||
@@ -7,5 +7,7 @@
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
min-height: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom))
|
||||
min-block-size: 100%;
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { ValidationRule } from 'vuetify/types/services/validation'
|
||||
type ValidationRule = (value: any) => string | boolean
|
||||
|
||||
// 必输校验
|
||||
export const requiredValidator: ValidationRule = (value: any) => !!value || '此项为必填项'
|
||||
export const requiredValidator: ValidationRule = (value: any) => {
|
||||
return !!value
|
||||
}
|
||||
|
||||
// 数字校验
|
||||
export const numberValidator: ValidationRule = (value: any) => !isNaN(value) || '请输入数字'
|
||||
export const numberValidator: ValidationRule = (value: any) => {
|
||||
return !isNaN(value)
|
||||
}
|
||||
|
||||
365
src/App.vue
@@ -3,17 +3,19 @@ import { useTheme } from 'vuetify'
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
|
||||
import api from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore, useGlobalSettingsStore } from '@/stores'
|
||||
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
|
||||
import { SupportedLocale } from '@/types/i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
import { checkAndEmitUnreadMessages } from '@/utils/badge'
|
||||
import { preloadImage } from './@core/utils/image'
|
||||
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
|
||||
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
|
||||
import PWAInstallPrompt from '@/components/PWAInstallPrompt.vue'
|
||||
import { themeManager } from '@/utils/themeManager'
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
let themeValue = localStorage.getItem('theme') || 'light'
|
||||
let themeValue = localStorage.getItem('theme') || 'auto'
|
||||
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
|
||||
@@ -21,21 +23,23 @@ globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
const localeValue = getBrowserLocale()
|
||||
setI18nLanguage(localeValue as SupportedLocale)
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettings: any = inject('globalSettings')
|
||||
|
||||
// 显示状态
|
||||
const show = ref(false)
|
||||
|
||||
// 检查是否登录
|
||||
const authStore = useAuthStore()
|
||||
const isLogin = computed(() => authStore.token)
|
||||
|
||||
// 全局设置store
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
|
||||
// 生成背景图片key
|
||||
const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'))
|
||||
|
||||
// 背景图片
|
||||
const backgroundImages = ref<string[]>([])
|
||||
const activeImageIndex = ref(0)
|
||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||
let backgroundRotationTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 心跳检测
|
||||
let heartbeatInterval: number | null = null
|
||||
|
||||
// ApexCharts 全局配置
|
||||
declare global {
|
||||
@@ -44,108 +48,121 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
if (window.Apex) {
|
||||
// 数据标签
|
||||
window.Apex.dataLabels = {
|
||||
formatter: function (_: number, { seriesIndex, w }: { seriesIndex: number; w: any }) {
|
||||
// 如果有小数点,保留两位小数,否则保留整数
|
||||
const data = w.config.series[seriesIndex]
|
||||
return data.toFixed(data % 1 === 0 ? 0 : 1)
|
||||
},
|
||||
// 启动心跳
|
||||
const startHeartbeat = () => {
|
||||
// 如果已经有心跳,则先停止
|
||||
if (heartbeatInterval) {
|
||||
stopHeartbeat()
|
||||
}
|
||||
// 图例
|
||||
window.Apex.legend = {
|
||||
labels: {
|
||||
useSeriesColors: true,
|
||||
},
|
||||
|
||||
// 开始心跳任务
|
||||
heartbeatInterval = window.setInterval(async () => {
|
||||
try {
|
||||
if (isLogin.value) {
|
||||
await api.get('dashboard/cpu')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Heartbeat request failed:', error)
|
||||
}
|
||||
}, 5 * 60 * 1000)
|
||||
}
|
||||
|
||||
// 停止心跳
|
||||
const stopHeartbeat = () => {
|
||||
if (heartbeatInterval) {
|
||||
window.clearInterval(heartbeatInterval)
|
||||
heartbeatInterval = null
|
||||
}
|
||||
// 标题
|
||||
window.Apex.title = {
|
||||
style: {
|
||||
color: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
|
||||
},
|
||||
}
|
||||
|
||||
// 配置 ApexCharts 全局选项
|
||||
function configureApexCharts() {
|
||||
if (typeof window !== 'undefined' && window.Apex) {
|
||||
try {
|
||||
// 获取当前主题
|
||||
const currentTheme = globalTheme.name.value
|
||||
const isDark = currentTheme === 'dark' || currentTheme === 'transparent'
|
||||
|
||||
// 数据标签
|
||||
window.Apex.dataLabels = {
|
||||
formatter: function (_: number, { seriesIndex, w }: { seriesIndex: number; w: any }) {
|
||||
// 如果有小数点,保留两位小数,否则保留整数
|
||||
const data = w.config.series[seriesIndex]
|
||||
return data.toFixed(data % 1 === 0 ? 0 : 1)
|
||||
},
|
||||
}
|
||||
// 图例
|
||||
window.Apex.legend = {
|
||||
labels: {
|
||||
useSeriesColors: true,
|
||||
},
|
||||
}
|
||||
// 标题
|
||||
window.Apex.title = {
|
||||
style: {
|
||||
color: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
|
||||
},
|
||||
}
|
||||
// 鼠标悬浮提示
|
||||
window.Apex.tooltip = {
|
||||
theme: isDark ? 'dark' : 'light',
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('ApexCharts 全局配置失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新data-theme属性以便CSS选择器能正确匹配
|
||||
function updateHtmlThemeAttribute(themeName: string) {
|
||||
document.documentElement.setAttribute('data-theme', themeName)
|
||||
// 确保body元素也有相同的主题属性,以便更好地选择弹出窗口
|
||||
document.body.setAttribute('data-theme', themeName)
|
||||
}
|
||||
|
||||
// 获取背景图片
|
||||
async function fetchBackgroundImages() {
|
||||
try {
|
||||
backgroundImages.value = await api.get(`/login/wallpapers`)
|
||||
const controller = new AbortController()
|
||||
backgroundImages.value = await api.get(`/login/wallpapers`, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
activeImageIndex.value = 0
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// 背景图片轮换函数
|
||||
function rotateBackgroundImage() {
|
||||
if (backgroundImages.value.length > 1) {
|
||||
// 计算下一个图片索引
|
||||
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
||||
// 预加载下一张图片
|
||||
preloadImage(backgroundImages.value[nextIndex]).then(success => {
|
||||
// 只有图片成功加载才切换
|
||||
if (success) {
|
||||
activeImageIndex.value = nextIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 开始背景图片轮换
|
||||
function startBackgroundRotation() {
|
||||
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
|
||||
// 清除现有定时器
|
||||
removeBackgroundTimer('background-rotation')
|
||||
|
||||
if (backgroundImages.value.length > 1) {
|
||||
backgroundRotationTimer = setInterval(() => {
|
||||
// 计算下一个图片索引
|
||||
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
||||
// 预加载下一张图片
|
||||
preloadImage(backgroundImages.value[nextIndex]).then(success => {
|
||||
// 只有图片成功加载才切换
|
||||
if (success) {
|
||||
activeImageIndex.value = nextIndex
|
||||
}
|
||||
})
|
||||
}, 10000) // 每10秒切换一次
|
||||
}
|
||||
}
|
||||
|
||||
// 预加载图片
|
||||
function preloadImage(url: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const img = new Image()
|
||||
const imageUrl = getImgUrl(url)
|
||||
|
||||
img.onload = () => resolve(true)
|
||||
img.onerror = () => resolve(false)
|
||||
|
||||
// 设置超时,防止图片长时间加载
|
||||
const timeout = setTimeout(() => {
|
||||
img.src = ''
|
||||
resolve(false)
|
||||
}, 5000) // 5秒超时
|
||||
|
||||
img.src = imageUrl
|
||||
|
||||
// 如果图片已经缓存,onload可能不会触发
|
||||
if (img.complete) {
|
||||
clearTimeout(timeout)
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
function getImgUrl(url: string) {
|
||||
// 使用图片缓存
|
||||
if (globalSettings.GLOBAL_IMAGE_CACHE && isLogin.value)
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
||||
return url
|
||||
}
|
||||
|
||||
// 处理页面可见性变化
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// 如果已有背景图片数据,直接重启轮换
|
||||
if (backgroundImages.value.length > 0) {
|
||||
startBackgroundRotation()
|
||||
}
|
||||
// 如果没有背景图片数据,重新获取
|
||||
else {
|
||||
fetchBackgroundImages().then(() => startBackgroundRotation())
|
||||
}
|
||||
// 使用优化的定时器管理器,后台时自动暂停
|
||||
addBackgroundTimer(
|
||||
'background-rotation',
|
||||
rotateBackgroundImage,
|
||||
10000, // 每10秒切换一次
|
||||
{
|
||||
runInBackground: false, // 后台时不运行
|
||||
skipInitialRun: true, // 不需要立即执行
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,71 +170,143 @@ function handleVisibilityChange() {
|
||||
function animateAndRemoveLoader() {
|
||||
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
||||
if (loadingBg) {
|
||||
// 先添加完成动画类
|
||||
loadingBg.classList.add('loading-complete')
|
||||
|
||||
// 等待动画完成后再移除元素
|
||||
setTimeout(() => {
|
||||
removeEl('#loading-bg')
|
||||
// 将background属性从html的style中移除
|
||||
document.documentElement.style.removeProperty('background')
|
||||
// 显示页面
|
||||
show.value = true
|
||||
}, 500) // 与CSS动画持续时间匹配
|
||||
removeEl('#loading-bg')
|
||||
document.documentElement.style.removeProperty('background')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查PWA状态并移除加载界面
|
||||
async function removeLoadingWithStateCheck() {
|
||||
try {
|
||||
// 设置各个组件的加载状态
|
||||
globalLoadingStateManager.setLoadingState('pwa-state', true)
|
||||
globalLoadingStateManager.setLoadingState('global-settings', true)
|
||||
globalLoadingStateManager.setLoadingState('background-images', true)
|
||||
|
||||
// 静默检查PWA状态恢复
|
||||
const pwaController = (window as any).pwaStateController
|
||||
if (pwaController) {
|
||||
await pwaController.waitForStateRestore()
|
||||
}
|
||||
globalLoadingStateManager.setLoadingState('pwa-state', false)
|
||||
|
||||
// 并行加载关键资源
|
||||
await Promise.all([
|
||||
globalSettingsStore.initialize().then(async () => {
|
||||
// 如果已登录,加载用户相关设置
|
||||
if (isLogin.value) {
|
||||
await globalSettingsStore.loadUserSettings()
|
||||
}
|
||||
globalLoadingStateManager.setLoadingState('global-settings', false)
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
globalLoadingStateManager.setLoadingState('background-images', false)
|
||||
resolve(void 0)
|
||||
}, 50)
|
||||
}),
|
||||
])
|
||||
|
||||
// 等待所有加载完成
|
||||
await globalLoadingStateManager.waitForAllComplete()
|
||||
|
||||
// 移除加载界面
|
||||
animateAndRemoveLoader()
|
||||
|
||||
// 检查未读消息
|
||||
checkAndEmitUnreadMessages()
|
||||
} catch (error) {
|
||||
// 即使出错也要移除加载界面
|
||||
globalLoadingStateManager.reset()
|
||||
animateAndRemoveLoader()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载背景图片
|
||||
async function loadBackgroundImages(retryCount = 0) {
|
||||
const maxRetries = 3
|
||||
try {
|
||||
await fetchBackgroundImages()
|
||||
startBackgroundRotation()
|
||||
} catch (error: any) {
|
||||
const isAbortError = error.name === 'AbortError' || error.code === 'ERR_CANCELED'
|
||||
if (retryCount < maxRetries) {
|
||||
const baseDelay = isAbortError ? 1000 : 3000
|
||||
const retryDelay = Math.min(baseDelay * Math.pow(2, retryCount), 10000)
|
||||
setTimeout(() => {
|
||||
loadBackgroundImages(retryCount + 1)
|
||||
}, retryDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 移除URL中的时间戳参数
|
||||
const url = new URL(window.location.href)
|
||||
if (url.searchParams.has('_t')) {
|
||||
url.searchParams.delete('_t')
|
||||
const newUrl = url.pathname + url.search + url.hash
|
||||
window.history.replaceState(null, '', newUrl)
|
||||
}
|
||||
|
||||
// 配置 ApexCharts
|
||||
configureApexCharts()
|
||||
|
||||
// 初始化data-theme属性
|
||||
updateHtmlThemeAttribute(globalTheme.name.value)
|
||||
|
||||
// 加载背景图片并开始轮换
|
||||
fetchBackgroundImages().then(() => startBackgroundRotation())
|
||||
// 初始化主题管理器 - 统一处理主题初始化
|
||||
await themeManager.setTheme(themeValue)
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
// 监听主题变化
|
||||
watch(
|
||||
() => globalTheme.name.value,
|
||||
newTheme => {
|
||||
// 更新HTML主题属性
|
||||
updateHtmlThemeAttribute(newTheme)
|
||||
// 重新配置ApexCharts以适应新主题
|
||||
configureApexCharts()
|
||||
},
|
||||
)
|
||||
|
||||
// 加载背景图片
|
||||
loadBackgroundImages()
|
||||
|
||||
// 使用优化后的加载界面移除逻辑
|
||||
ensureRenderComplete(() => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// 移除加载动画
|
||||
animateAndRemoveLoader()
|
||||
}, 1500)
|
||||
})
|
||||
nextTick(removeLoadingWithStateCheck)
|
||||
})
|
||||
// 启动心跳
|
||||
startHeartbeat()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除页面可见性监听
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
// 清除轮换定时器
|
||||
if (backgroundRotationTimer) {
|
||||
clearInterval(backgroundRotationTimer)
|
||||
backgroundRotationTimer = null
|
||||
}
|
||||
// 清除背景轮换定时器
|
||||
removeBackgroundTimer('background-rotation')
|
||||
// 停止心跳
|
||||
stopHeartbeat()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-wrapper">
|
||||
<!-- 透明主题背景 -->
|
||||
<template v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)">
|
||||
<div class="background-container">
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
:key="index"
|
||||
class="background-image"
|
||||
:class="{ 'active': index === activeImageIndex }"
|
||||
:style="{ backgroundImage: `url(${getImgUrl(imageUrl)})` }"
|
||||
></div>
|
||||
<!-- 全局磨砂层 -->
|
||||
<div v-if="isLogin" class="global-blur-layer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }">
|
||||
<div v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)" class="background-container">
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
:key="`bg-${index}-${loginStateKey}`"
|
||||
class="background-image"
|
||||
:class="{ 'active': index === activeImageIndex }"
|
||||
:style="{ 'backgroundImage': `url(${imageUrl})` }"
|
||||
/>
|
||||
<!-- 全局磨砂层 -->
|
||||
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
|
||||
</div>
|
||||
<!-- 页面内容 -->
|
||||
<VApp>
|
||||
<RouterView />
|
||||
<!-- PWA安装提示 -->
|
||||
<PWAInstallPrompt />
|
||||
</VApp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,39 +1,84 @@
|
||||
import i18n from '@/plugins/i18n'
|
||||
|
||||
export const storageOptions = [
|
||||
export const storageAttributes = [
|
||||
{
|
||||
title: i18n.global.t('storage.local'),
|
||||
value: 'local',
|
||||
type: 'local',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('storage.alipan'),
|
||||
value: 'alipan',
|
||||
type: 'alipan',
|
||||
icon: 'mdi-cloud-outline',
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('storage.u115'),
|
||||
value: 'u115',
|
||||
type: 'u115',
|
||||
icon: 'mdi-cloud-outline',
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('storage.rclone'),
|
||||
value: 'rclone',
|
||||
type: 'rclone',
|
||||
icon: 'mdi-server-network-outline',
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('storage.alist'),
|
||||
value: 'alist',
|
||||
type: 'alist',
|
||||
icon: 'mdi-server-network-outline',
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
type: 'smb',
|
||||
icon: 'mdi-folder-network-outline',
|
||||
remote: true,
|
||||
},
|
||||
]
|
||||
|
||||
export const storageDict = storageOptions.reduce((dict, item) => {
|
||||
export const storageIconDict = storageAttributes.reduce((dict, item) => {
|
||||
dict[item.type] = item.icon
|
||||
return dict
|
||||
}, {} as Record<string, string>)
|
||||
|
||||
export const storageRemoteDict = storageAttributes.reduce((dict, item) => {
|
||||
dict[item.type] = item.remote
|
||||
return dict
|
||||
}, {} as Record<string, boolean>)
|
||||
|
||||
export const downloaderOptions = [
|
||||
{
|
||||
value: 'qbittorrent',
|
||||
title: i18n.global.t('setting.system.qbittorrent'),
|
||||
},
|
||||
{
|
||||
value: 'transmission',
|
||||
title: i18n.global.t('setting.system.transmission'),
|
||||
},
|
||||
]
|
||||
|
||||
export const downloaderDict = downloaderOptions.reduce((dict, item) => {
|
||||
dict[item.value] = item.title
|
||||
return dict
|
||||
}, {} as Record<string, string>)
|
||||
|
||||
export const mediaServerOptions = [
|
||||
{
|
||||
value: 'emby',
|
||||
title: i18n.global.t('setting.system.emby'),
|
||||
},
|
||||
{
|
||||
value: 'jellyfin',
|
||||
title: i18n.global.t('setting.system.jellyfin'),
|
||||
},
|
||||
{
|
||||
value: 'plex',
|
||||
title: i18n.global.t('setting.system.plex'),
|
||||
},
|
||||
{
|
||||
value: 'trimemedia',
|
||||
title: i18n.global.t('setting.system.trimeMedia'),
|
||||
},
|
||||
]
|
||||
|
||||
export const mediaServerDict = mediaServerOptions.reduce((dict, item) => {
|
||||
dict[item.value] = item.title
|
||||
return dict
|
||||
}, {} as Record<string, string>)
|
||||
@@ -295,6 +340,14 @@ export const actionStepOptions = [
|
||||
title: i18n.global.t('actionStep.transferFile'),
|
||||
value: '整理文件',
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('actionStep.invokePlugin'),
|
||||
value: '调用插件',
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('actionStep.note'),
|
||||
value: '备注',
|
||||
},
|
||||
]
|
||||
|
||||
// 操作步骤字典
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import axios from 'axios'
|
||||
import router from '@/router'
|
||||
import { useAuthStore } from '@/stores'
|
||||
import { initializeRequestOptimizer } from '@/utils/requestOptimizer'
|
||||
import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
})
|
||||
|
||||
// 声明全局变量类型
|
||||
declare global {
|
||||
interface Window {
|
||||
MoviePilotAPI: typeof api
|
||||
}
|
||||
}
|
||||
|
||||
// 将 API 实例暴露到全局,供插件使用
|
||||
window.MoviePilotAPI = api
|
||||
|
||||
// 初始化请求优化器(必须在其他拦截器之前)
|
||||
initializeRequestOptimizer(api)
|
||||
|
||||
// 添加请求拦截器
|
||||
api.interceptors.request.use(config => {
|
||||
// 认证 Store
|
||||
@@ -18,15 +33,47 @@ api.interceptors.request.use(config => {
|
||||
return config
|
||||
})
|
||||
|
||||
// 离线状态管理
|
||||
const globalOfflineStatus = useGlobalOfflineStatus()
|
||||
|
||||
// 添加响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
// 成功响应时,清除应用离线状态并重置连续错误计数
|
||||
globalOfflineStatus.setAppOffline(false)
|
||||
globalOfflineStatus.resetConsecutiveErrors()
|
||||
return response.data
|
||||
},
|
||||
error => {
|
||||
if (!error.response) {
|
||||
// 请求超时
|
||||
return Promise.reject(new Error(error))
|
||||
// 网络错误或请求超时 - 通知离线状态管理系统
|
||||
const isNetworkError =
|
||||
error.code === 'NETWORK_ERROR' ||
|
||||
error.code === 'ERR_NETWORK' ||
|
||||
error.code === 'ECONNABORTED' ||
|
||||
error.name === 'NetworkError'
|
||||
|
||||
if (isNetworkError) {
|
||||
let reason = 'Network connection failed'
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
reason = 'Request timeout'
|
||||
}
|
||||
// 记录网络错误,只有连续三次才会设置为离线模式
|
||||
globalOfflineStatus.recordNetworkError(reason)
|
||||
}
|
||||
|
||||
if (error.code === 'NETWORK_ERROR' || error.code === 'ERR_NETWORK') {
|
||||
// 网络连接问题
|
||||
return Promise.reject(new Error('Network connection failed, please check your network status'))
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
// 请求超时
|
||||
return Promise.reject(new Error('Request timeout, please try again later'))
|
||||
} else if (error.name === 'AbortError') {
|
||||
// 请求被中止(路由切换等)
|
||||
return Promise.reject(new Error('Request cancelled'))
|
||||
}
|
||||
// 其他网络错误
|
||||
return Promise.reject(new Error(error.message || 'Network error'))
|
||||
} else if (error.response.status === 403) {
|
||||
// 认证 Store
|
||||
const authStore = useAuthStore()
|
||||
@@ -41,17 +88,3 @@ api.interceptors.response.use(
|
||||
)
|
||||
|
||||
export default api
|
||||
|
||||
export async function fetchGlobalSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/global', {
|
||||
params: {
|
||||
token: 'moviepilot',
|
||||
},
|
||||
})
|
||||
return result.data || {}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch global settings', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
149
src/api/types.ts
@@ -144,6 +144,42 @@ export interface SubscribeShare {
|
||||
episode_group?: string
|
||||
}
|
||||
|
||||
// 工作流分享
|
||||
export interface WorkflowShare {
|
||||
// 分享ID
|
||||
id?: string
|
||||
// 工作流ID
|
||||
workflow_id?: string
|
||||
// 分享标题
|
||||
share_title?: string
|
||||
// 分享说明
|
||||
share_comment?: string
|
||||
// 分享人
|
||||
share_user?: string
|
||||
// 分享人唯一ID
|
||||
share_uid?: string
|
||||
// 工作流名称
|
||||
name?: string
|
||||
// 工作流描述
|
||||
description?: string
|
||||
// 定时器
|
||||
timer?: string
|
||||
// 触发类型:timer-定时触发 event-事件触发 manual-手动触发
|
||||
trigger_type?: string
|
||||
// 事件类型(当trigger_type为event时使用)
|
||||
event_type?: string
|
||||
// 动作列表
|
||||
actions?: any[]
|
||||
// 动作流
|
||||
flows?: any[]
|
||||
// 上下文
|
||||
context?: string
|
||||
// 时间
|
||||
date?: string
|
||||
// 复用次数
|
||||
count?: number
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
export interface TransferHistory {
|
||||
// ID
|
||||
@@ -278,6 +314,8 @@ export interface MediaInfo {
|
||||
production_countries?: any[]
|
||||
// 语种
|
||||
spoken_languages?: string[]
|
||||
// 数字/实体发行日期
|
||||
release_dates?: MediaRelease[]
|
||||
// 状态
|
||||
status?: string
|
||||
// 标签
|
||||
@@ -332,6 +370,18 @@ export interface TmdbSeason {
|
||||
vote_average?: number
|
||||
}
|
||||
|
||||
// 发行信息
|
||||
export interface MediaRelease {
|
||||
// 发行日期
|
||||
date: string
|
||||
// 发行地区
|
||||
iso_code: string
|
||||
// 备注
|
||||
note?: string
|
||||
// 发行类型
|
||||
type: number
|
||||
}
|
||||
|
||||
// TMDB集信息
|
||||
export interface TmdbEpisode {
|
||||
// 上映日期
|
||||
@@ -484,7 +534,7 @@ export interface SiteUserData {
|
||||
// 用户名
|
||||
username?: string
|
||||
// 用户ID
|
||||
userid?: number
|
||||
userid?: string
|
||||
// 用户等级
|
||||
user_level?: string
|
||||
// 加入时间
|
||||
@@ -565,9 +615,9 @@ export interface NotExistMediaInfo {
|
||||
|
||||
// 插件
|
||||
export interface Plugin {
|
||||
id?: string
|
||||
id: string
|
||||
// 插件名称
|
||||
plugin_name?: string
|
||||
plugin_name: string
|
||||
// 插件描述
|
||||
plugin_desc?: string
|
||||
// 插件图标
|
||||
@@ -631,6 +681,8 @@ export interface DashboardItem {
|
||||
cols: { [key: string]: number }
|
||||
// 页面元素
|
||||
elements: RenderProps[]
|
||||
// 渲染方式
|
||||
render_mode?: string
|
||||
}
|
||||
|
||||
// 种子信息
|
||||
@@ -767,6 +819,8 @@ export interface MetaInfo {
|
||||
audio_term: string
|
||||
// 资源类型+特效
|
||||
edition: string
|
||||
// 流媒体平台
|
||||
web_source: string
|
||||
// 应用的自定义识别词
|
||||
apply_words: string[]
|
||||
}
|
||||
@@ -807,6 +861,16 @@ export interface User {
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
// 通行密钥
|
||||
export interface PassKey {
|
||||
id: number
|
||||
name: string
|
||||
created_at: string
|
||||
last_used_at?: string
|
||||
aaguid?: string
|
||||
transports?: string
|
||||
}
|
||||
|
||||
// 存储空间
|
||||
export interface Storage {
|
||||
// 总空间
|
||||
@@ -950,6 +1014,10 @@ export interface MediaServerPlayItem {
|
||||
link?: string
|
||||
// 播放百分比
|
||||
percent?: number
|
||||
// 媒体服务器类型
|
||||
server_type?: string
|
||||
// 图片是否需要Cookies
|
||||
use_cookies?: boolean
|
||||
}
|
||||
|
||||
// 媒体服务器媒体库
|
||||
@@ -970,6 +1038,10 @@ export interface MediaServerLibrary {
|
||||
image_list?: string[]
|
||||
// 链接
|
||||
link?: string
|
||||
// 媒体服务器类型
|
||||
server_type?: string
|
||||
// 图片是否需要Cookies
|
||||
use_cookies?: boolean
|
||||
}
|
||||
|
||||
// 消息通知
|
||||
@@ -1006,6 +1078,8 @@ export interface SystemNotification {
|
||||
text: string
|
||||
// 通知时间
|
||||
date: string
|
||||
// 是否已读
|
||||
read?: boolean
|
||||
}
|
||||
|
||||
// 下载器配置
|
||||
@@ -1020,6 +1094,8 @@ export interface DownloaderConf {
|
||||
config: { [key: string]: any }
|
||||
// 是否启用
|
||||
enabled: boolean
|
||||
// 路径映射
|
||||
path_mapping?: Array<[storagePath: string, downloadPath: string]>
|
||||
}
|
||||
|
||||
// 通知配置
|
||||
@@ -1286,6 +1362,10 @@ export interface Workflow {
|
||||
description?: string
|
||||
// 定时器
|
||||
timer?: string
|
||||
// 触发类型:timer-定时触发 event-事件触发 manual-手动触发
|
||||
trigger_type?: string
|
||||
// 事件类型(当trigger_type为event时使用)
|
||||
event_type?: string
|
||||
// 状态
|
||||
state?: string
|
||||
// 当前执行动作
|
||||
@@ -1303,3 +1383,66 @@ export interface Workflow {
|
||||
// 最后执行时间
|
||||
last_time?: string
|
||||
}
|
||||
|
||||
// 种子缓存项
|
||||
export interface TorrentCacheItem {
|
||||
// 种子hash(用于操作标识)
|
||||
hash: string
|
||||
// 站点域名
|
||||
domain: string
|
||||
// 种子标题
|
||||
title: string
|
||||
// 种子描述
|
||||
description?: string
|
||||
// 种子大小
|
||||
size: number
|
||||
// 发布时间
|
||||
pubdate?: string
|
||||
// 站点名称
|
||||
site_name?: string
|
||||
// 识别的媒体名称
|
||||
media_name?: string
|
||||
// 识别的媒体年份
|
||||
media_year?: string
|
||||
// 识别的媒体类型
|
||||
media_type?: string
|
||||
// 季集信息
|
||||
season_episode?: string
|
||||
// 资源信息
|
||||
resource_term?: string
|
||||
// 种子链接
|
||||
enclosure?: string
|
||||
// 详情页面
|
||||
page_url?: string
|
||||
// 海报图片
|
||||
poster_path?: string
|
||||
// 背景图片
|
||||
backdrop_path?: string
|
||||
}
|
||||
|
||||
// 种子缓存数据
|
||||
export interface TorrentCacheData {
|
||||
// 缓存数量
|
||||
count: number
|
||||
// 站点数量
|
||||
sites: number
|
||||
// 缓存数据
|
||||
data: TorrentCacheItem[]
|
||||
}
|
||||
|
||||
// 订阅分享统计
|
||||
export interface SubscribeShareStatistics {
|
||||
// 分享人
|
||||
share_user?: string
|
||||
// 分享数量
|
||||
share_count?: number
|
||||
// 总复用人次
|
||||
total_reuse_count?: number
|
||||
}
|
||||
|
||||
// 通用API响应
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
message?: string
|
||||
data: T
|
||||
}
|
||||
BIN
src/assets/images/logos/discord.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
src/assets/images/logos/downloader.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
src/assets/images/logos/mediaserver.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/images/logos/notification.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/images/logos/python.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/images/misc/database.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
10
src/assets/images/misc/openlist.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/images/misc/smb.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
1
src/assets/images/pages/404.svg
Normal file
|
After Width: | Height: | Size: 41 KiB |
@@ -3,12 +3,13 @@ import FileList from './filebrowser/FileList.vue'
|
||||
import FileToolbar from './filebrowser/FileToolbar.vue'
|
||||
import FileNavigator from './filebrowser/FileNavigator.vue'
|
||||
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
||||
import { storageOptions } from '@/api/constants'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storageIconDict } from '@/api/constants'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
// LocalStorage keys
|
||||
const SORT_KEY = 'fileBrowser.sort'
|
||||
const SHOW_TREE_KEY = 'fileBrowser.showDirTree'
|
||||
const NAV_WIDTH_KEY = 'fileBrowser.navigatorWidth'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -16,7 +17,7 @@ const props = defineProps({
|
||||
tree: Boolean,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: {
|
||||
type: Function,
|
||||
type: Object as PropType<AxiosInstance>,
|
||||
required: true,
|
||||
},
|
||||
axiosconfig: Object,
|
||||
@@ -33,12 +34,6 @@ const props = defineProps({
|
||||
// 对外事件
|
||||
const emit = defineEmits(['pathchanged'])
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// APP
|
||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||
|
||||
const fileIcons = {
|
||||
// 压缩包
|
||||
zip: 'mdi-folder-zip-outline',
|
||||
@@ -130,31 +125,51 @@ const fileIcons = {
|
||||
|
||||
// 加载次数
|
||||
const loading = ref(0)
|
||||
// 当前存储
|
||||
const activeStorage = ref('local')
|
||||
|
||||
// 刷新
|
||||
const refreshPending = ref(false)
|
||||
// 排序
|
||||
const sort = ref('name')
|
||||
// 排序 - 从localStorage恢复
|
||||
const sort = ref(localStorage.getItem(SORT_KEY) || 'name')
|
||||
|
||||
// 是否显示目录树
|
||||
const showDirTree = ref(false)
|
||||
// 是否显示目录树 - 从localStorage恢复
|
||||
const showDirTree = ref(localStorage.getItem(SHOW_TREE_KEY) === 'true')
|
||||
|
||||
// 拖动分隔条相关 - 从localStorage恢复宽度
|
||||
const navigatorWidth = ref(parseInt(localStorage.getItem(NAV_WIDTH_KEY) || '280'))
|
||||
const isDragging = ref(false)
|
||||
const dragStartX = ref(0)
|
||||
const dragStartWidth = ref(0)
|
||||
|
||||
watch(sort, (val) => {
|
||||
localStorage.setItem(SORT_KEY, val)
|
||||
})
|
||||
|
||||
watch(showDirTree, (val) => {
|
||||
localStorage.setItem(SHOW_TREE_KEY, String(val))
|
||||
})
|
||||
|
||||
watch(navigatorWidth, (val) => {
|
||||
localStorage.setItem(NAV_WIDTH_KEY, String(val))
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const storagesArray = computed(() => {
|
||||
const storageCodes = props.storages?.map(item => item.type)
|
||||
return storageOptions.filter(item => storageCodes?.includes(item.value))
|
||||
return props.storages?.map(item => ({
|
||||
title: item.name,
|
||||
value: item.type,
|
||||
icon: storageIconDict[item.type] ?? 'mdi-server-network-outline',
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
// 方法
|
||||
function loadingChanged(loading: number) {
|
||||
if (loading) loading++
|
||||
else if (loading > 0) loading--
|
||||
function loadingChanged(isLoading: number) {
|
||||
if (isLoading) loading.value++
|
||||
else if (loading.value > 0) loading.value--
|
||||
}
|
||||
|
||||
// 存储切换
|
||||
async function storageChanged(storage: string) {
|
||||
activeStorage.value = storage
|
||||
emit('pathchanged', { storage: storage, path: '/', fileid: 'root' })
|
||||
}
|
||||
|
||||
@@ -182,29 +197,67 @@ function fileListUpdated(items: FileItem[]) {
|
||||
fileListItems.value = items
|
||||
}
|
||||
|
||||
// 外层DIV大小控制
|
||||
const scrollStyle = computed(() => {
|
||||
return appMode
|
||||
? 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom) - 6.5rem)'
|
||||
: 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
// 阻止选择事件
|
||||
function preventSelect(event: Event) {
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
// 文件列表大小限制
|
||||
const fileListStyle = computed(() => {
|
||||
return appMode
|
||||
? 'height: calc(100vh - 14rem - env(safe-area-inset-bottom) - 7rem)'
|
||||
: 'height: calc(100vh - 14rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
// 拖动分隔条相关方法
|
||||
function startDrag(event: MouseEvent) {
|
||||
event.preventDefault() // 阻止默认行为
|
||||
event.stopPropagation() // 阻止事件冒泡
|
||||
|
||||
isDragging.value = true
|
||||
dragStartX.value = event.clientX
|
||||
dragStartWidth.value = navigatorWidth.value
|
||||
|
||||
document.addEventListener('mousemove', handleDrag, { passive: false })
|
||||
document.addEventListener('mouseup', stopDrag, { passive: false })
|
||||
document.addEventListener('selectstart', preventSelect) // 阻止选择开始
|
||||
|
||||
document.body.style.cursor = 'col-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
;(document.body.style as any).webkitUserSelect = 'none' // Safari兼容
|
||||
;(document.body.style as any).mozUserSelect = 'none' // Firefox兼容
|
||||
}
|
||||
|
||||
function handleDrag(event: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
|
||||
event.preventDefault() // 阻止默认行为
|
||||
|
||||
const deltaX = event.clientX - dragStartX.value
|
||||
const newWidth = dragStartWidth.value + deltaX
|
||||
|
||||
// 设置最小和最大宽度限制
|
||||
const minWidth = 200
|
||||
const maxWidth = window.innerWidth * 0.6
|
||||
|
||||
navigatorWidth.value = Math.max(minWidth, Math.min(maxWidth, newWidth))
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('selectstart', preventSelect)
|
||||
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
;(document.body.style as any).webkitUserSelect = ''
|
||||
;(document.body.style as any).mozUserSelect = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto" :loading="loading > 0">
|
||||
<div v-if="activeStorage && item">
|
||||
<div v-if="item">
|
||||
<FileToolbar
|
||||
:sort="sort"
|
||||
:item="item"
|
||||
:itemstack="itemstack"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="endpoints"
|
||||
:axios="axios"
|
||||
@storagechanged="storageChanged"
|
||||
@@ -212,26 +265,31 @@ const fileListStyle = computed(() => {
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<div class="flex" :style="scrollStyle">
|
||||
<div class="flex">
|
||||
<FileNavigator
|
||||
v-if="showDirTree"
|
||||
:storage="activeStorage"
|
||||
:storage="item.storage"
|
||||
:currentPath="item.path"
|
||||
:items="fileListItems"
|
||||
:endpoints="endpoints"
|
||||
:axios="axios"
|
||||
:style="{ width: `${navigatorWidth}px`, minWidth: `${navigatorWidth}px` }"
|
||||
@navigate="pathChanged"
|
||||
/>
|
||||
<!-- 拖动分隔条 -->
|
||||
<div v-if="showDirTree" class="divider" :class="{ 'divider-dragging': isDragging }" @mousedown="startDrag">
|
||||
<div class="divider-line"></div>
|
||||
<VIcon class="divider-icon" size="small">mdi-drag-vertical</VIcon>
|
||||
</div>
|
||||
<FileList
|
||||
:item="item"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axios"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
:listStyle="fileListStyle"
|
||||
:showTree="showDirTree"
|
||||
:style="{ flex: 1 }"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@@ -244,3 +302,64 @@ const fileListStyle = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.divider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
cursor: col-resize;
|
||||
inline-size: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.divider:hover {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.divider-dragging {
|
||||
background-color: rgba(var(--v-theme-primary), 0.12) !important;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
background-color: rgba(var(--v-theme-outline), 0.3);
|
||||
block-size: 100%;
|
||||
inline-size: 1px;
|
||||
transition: background-color 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.divider-dragging .divider-line {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.divider:hover .divider-line {
|
||||
background-color: rgba(var(--v-theme-primary), 0.8);
|
||||
}
|
||||
|
||||
.divider-icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(var(--v-theme-surface), 0.9);
|
||||
color: rgba(var(--v-theme-on-surface-variant), 0.6);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.divider-dragging .divider-icon {
|
||||
background-color: rgba(var(--v-theme-surface), 0.95);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.divider:hover .divider-icon {
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import page404 from '@images/pages/404.svg'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -19,16 +20,7 @@ interface Props {
|
||||
<div class="no-data-container">
|
||||
<!-- 图标容器 -->
|
||||
<div class="icon-wrapper">
|
||||
<div class="icon-glow"></div>
|
||||
<div class="icon-container">
|
||||
<VIcon
|
||||
:icon="props.icon || 'mdi-file-search-outline'"
|
||||
:color="props.iconColor || 'white'"
|
||||
size="48"
|
||||
class="main-icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="pulse-ring"></div>
|
||||
<img :src="page404" alt="404" />
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
@@ -57,8 +49,7 @@ interface Props {
|
||||
justify-content: center;
|
||||
inline-size: 100%;
|
||||
min-block-size: 300px;
|
||||
padding-block: 3rem;
|
||||
padding-inline: 1rem;
|
||||
padding-block-start: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -68,109 +59,17 @@ interface Props {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
block-size: 100px;
|
||||
inline-size: 100px;
|
||||
margin-block: 0 2rem;
|
||||
inline-size: 15rem;
|
||||
margin-block: 0 1rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.icon-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
animation: pulse 3s infinite ease-in-out;
|
||||
background: radial-gradient(circle, rgba(var(--v-theme-primary), 0.8) 0%, rgba(var(--v-theme-primary), 0) 70%);
|
||||
block-size: 80px;
|
||||
filter: blur(15px);
|
||||
inline-size: 80px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.9), rgba(var(--v-theme-secondary), 0.8));
|
||||
block-size: 80px;
|
||||
inline-size: 80px;
|
||||
}
|
||||
|
||||
.main-icon {
|
||||
animation: slight-bounce 3s infinite ease-in-out;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 30%));
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
border: 2px solid rgba(var(--v-theme-primary), 0.5);
|
||||
border-radius: 50%;
|
||||
animation: ripple 2s infinite ease-out;
|
||||
block-size: 100px;
|
||||
inline-size: 100px;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.pulse-ring::before {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(var(--v-theme-primary), 0.3);
|
||||
border-radius: 50%;
|
||||
animation: ripple 2s infinite 0.5s ease-out;
|
||||
block-size: 85px;
|
||||
content: '';
|
||||
inline-size: 85px;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slight-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 文字样式 */
|
||||
.error-title {
|
||||
position: relative;
|
||||
color: rgba(var(--v-theme-on-surface), 0.95);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
margin-block-end: 0.75rem;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 5%);
|
||||
}
|
||||
@@ -181,69 +80,15 @@ interface Props {
|
||||
background: linear-gradient(90deg, rgba(var(--v-theme-primary), 0.8), rgba(var(--v-theme-primary), 0.2));
|
||||
block-size: 3px;
|
||||
content: '';
|
||||
inline-size: 40px;
|
||||
margin-block: 0.5rem 0;
|
||||
inline-size: 60px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
color: rgba(var(--v-theme-on-surface), 0.75);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin-block-end: 1.5rem;
|
||||
font-size: 1rem;
|
||||
margin-block-end: 1rem;
|
||||
margin-inline: auto;
|
||||
max-inline-size: 80%;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
margin-block-start: 1.5rem;
|
||||
}
|
||||
|
||||
.actions-container :deep(.v-btn) {
|
||||
transform: translateY(0);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.actions-container :deep(.v-btn:hover) {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (width <= 600px) {
|
||||
.no-data-container {
|
||||
padding-block: 2rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
block-size: 80px;
|
||||
inline-size: 80px;
|
||||
margin-block-end: 1.5rem;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
block-size: 70px;
|
||||
inline-size: 70px;
|
||||
}
|
||||
|
||||
.icon-glow {
|
||||
block-size: 70px;
|
||||
inline-size: 70px;
|
||||
}
|
||||
|
||||
.pulse-ring,
|
||||
.pulse-ring::before {
|
||||
block-size: 80px;
|
||||
inline-size: 80px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 0.95rem;
|
||||
max-inline-size: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
201
src/components/PWAInstallPrompt.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import { usePWAInstall } from '@/composables/usePWAInstall'
|
||||
import { useAuthStore } from '@/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const { t, locale, messages } = useI18n()
|
||||
const { isInstalled, showInstallPrompt, getInstallInstructions } = usePWAInstall()
|
||||
|
||||
const showBanner = ref(false)
|
||||
const showInstructions = ref(false)
|
||||
const dismissed = ref(false)
|
||||
|
||||
// 检查是否登录
|
||||
const authStore = useAuthStore()
|
||||
const isLogin = computed(() => authStore.token)
|
||||
|
||||
// 检查当前是不是https环境
|
||||
const isHttps = computed(() => {
|
||||
return window.location.protocol === 'https:'
|
||||
})
|
||||
|
||||
// 检查是否应该显示横幅
|
||||
const shouldShowBanner = computed(() => {
|
||||
return !isInstalled.value && !dismissed.value && !showInstructions.value && isLogin.value && isHttps.value
|
||||
})
|
||||
|
||||
// 显示延迟(避免立即显示)
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// 检查本地存储,看用户是否已经关闭过提示
|
||||
const dismissedTime = localStorage.getItem('pwa-install-dismissed')
|
||||
if (dismissedTime) {
|
||||
const dismissedDate = new Date(dismissedTime)
|
||||
const now = new Date()
|
||||
const daysDiff = (now.getTime() - dismissedDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
|
||||
// 如果距离上次关闭不到30天,不显示
|
||||
if (daysDiff < 30) {
|
||||
dismissed.value = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
showBanner.value = true
|
||||
}, 5000) // 5秒后显示
|
||||
})
|
||||
|
||||
// 处理安装
|
||||
const handleInstall = async () => {
|
||||
const installed = await showInstallPrompt()
|
||||
if (installed) {
|
||||
showBanner.value = false
|
||||
// 显示成功消息
|
||||
useToast().success(t('pwa.installSuccess'))
|
||||
} else {
|
||||
// 如果用户拒绝,显示手动安装说明
|
||||
showInstructions.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭横幅
|
||||
const dismissBanner = () => {
|
||||
showBanner.value = false
|
||||
dismissed.value = true
|
||||
// 记录关闭时间
|
||||
localStorage.setItem('pwa-install-dismissed', new Date().toISOString())
|
||||
}
|
||||
|
||||
// 获取平台特定的安装说明
|
||||
const instructions = computed(() => {
|
||||
const rawInstructions = getInstallInstructions()
|
||||
const platformKey = rawInstructions.platformKey
|
||||
|
||||
// 获取平台显示名称
|
||||
const platformName = t(`pwa.platforms.${platformKey}`)
|
||||
|
||||
// 直接使用t函数获取安装步骤,避免编译对象的问题
|
||||
const steps = []
|
||||
const maxSteps = 10 // 最大步骤数,防止无限循环
|
||||
|
||||
for (let i = 0; i < maxSteps; i++) {
|
||||
try {
|
||||
const stepKey = `pwa.installSteps.${platformKey}.${i}`
|
||||
const stepText = t(stepKey)
|
||||
|
||||
// 如果返回的是键名本身,说明没有找到对应的翻译
|
||||
if (stepText === stepKey) {
|
||||
break
|
||||
}
|
||||
|
||||
steps.push(stepText)
|
||||
} catch (error) {
|
||||
// 如果出现错误,说明没有更多步骤
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
platform: platformName,
|
||||
steps,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 安装横幅 -->
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300"
|
||||
enter-from-class="translate-y-full opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition-all duration-300"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-full opacity-0"
|
||||
>
|
||||
<VCard v-if="shouldShowBanner && showBanner" class="pwa-install-banner">
|
||||
<div class="banner-content">
|
||||
<VIcon icon="mdi-cellphone-link" size="24" class="me-3" />
|
||||
<div class="flex-grow-1">
|
||||
<div class="font-weight-medium">{{ t('pwa.installApp') }}</div>
|
||||
<div class="text-sm opacity-70">{{ t('pwa.installDescription') }}</div>
|
||||
</div>
|
||||
<VBtn color="primary" size="small" variant="flat" @click="handleInstall">
|
||||
{{ t('pwa.install') }}
|
||||
</VBtn>
|
||||
<VBtn icon size="small" variant="text" @click="dismissBanner">
|
||||
<VIcon icon="mdi-close" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCard>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- 手动安装说明对话框 -->
|
||||
<VDialog v-model="showInstructions" max-width="500">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle class="d-flex align-center">
|
||||
<VIcon icon="mdi-information-outline" class="me-2" />
|
||||
{{ t('pwa.installGuide') }}
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<div class="mb-4">
|
||||
<div class="text-subtitle-1 mb-2">
|
||||
{{ t('pwa.installInstructions', { platform: instructions.platform }) }}
|
||||
</div>
|
||||
<VList density="compact">
|
||||
<VListItem
|
||||
v-for="(step, index) in instructions.steps"
|
||||
:key="index"
|
||||
:prepend-icon="`mdi-numeric-${index + 1}-circle`"
|
||||
>
|
||||
<VListItemTitle>{{ step }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</div>
|
||||
|
||||
<VAlert type="info" variant="tonal" density="compact">
|
||||
{{ t('pwa.installNote') }}
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn color="primary" variant="text" @click="showInstructions = false">
|
||||
{{ t('pwa.gotIt') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pwa-install-banner {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 12px;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 10%);
|
||||
inset-block-end: 5rem;
|
||||
inset-inline: 20px;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
.pwa-install-banner {
|
||||
inset-inline: auto 20px;
|
||||
max-inline-size: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import { openMediaServerWithAutoDetect } from '@/utils/appDeepLink'
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerPlayItem>,
|
||||
@@ -16,19 +17,26 @@ function imageLoadHandler() {
|
||||
}
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link) window.open(props.media?.link, '_blank')
|
||||
async function goPlay() {
|
||||
if (props.media?.link) {
|
||||
await openMediaServerWithAutoDetect(props.media.link, undefined, props.media.server_type)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
const use_cookies = props.media?.use_cookies
|
||||
if (use_cookies) {
|
||||
url += `&use_cookies=${encodeURIComponent(use_cookies)}`
|
||||
}
|
||||
return url
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover v-bind="props">
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
@@ -48,16 +56,18 @@ const getImgUrl = computed(() => {
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<h1
|
||||
class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ..."
|
||||
<template #default>
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<span class="text-shadow">{{ props.media?.subtitle }}</span>
|
||||
</VCardText>
|
||||
<h1
|
||||
class="mb-1 text-white text-shadow font-bold text-lg line-clamp-2 overflow-hidden text-ellipsis ..."
|
||||
>
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<span class="text-shadow text-sm">{{ props.media?.subtitle }}</span>
|
||||
</VCardText>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="w-full absolute bottom-0">
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { CustomRule } from '@/api/types'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import filter_svg from '@images/svg/filter.svg'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { innerFilterRules } from '@/api/constants'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -83,6 +87,12 @@ function saveRuleInfo() {
|
||||
emit('done')
|
||||
}
|
||||
|
||||
// 验证规则ID输入
|
||||
function validateRuleId() {
|
||||
// 只允许英文和数字,不允许空格
|
||||
ruleInfo.value.id = ruleInfo.value.id.replace(/[^a-zA-Z0-9]/g, '')
|
||||
}
|
||||
|
||||
// 按钮点击
|
||||
function onClose() {
|
||||
emit('close')
|
||||
@@ -106,8 +116,20 @@ function onClose() {
|
||||
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VDialog v-if="ruleInfoDialog" v-model="ruleInfoDialog" scrollable max-width="40rem">
|
||||
<VCard :title="t('customRule.title', { id: props.rule.id })" class="rounded-t">
|
||||
<VDialog
|
||||
v-if="ruleInfoDialog"
|
||||
v-model="ruleInfoDialog"
|
||||
scrollable
|
||||
max-width="40rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-filter-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('customRule.title', { id: props.rule.id }) }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn v-model="ruleInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
@@ -121,6 +143,8 @@ function onClose() {
|
||||
:hint="t('customRule.hint.ruleId')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
@input="validateRuleId"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -131,6 +155,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.ruleName')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -141,6 +166,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.include')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-plus-circle"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -151,6 +177,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.exclude')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-minus-circle"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
@@ -161,6 +188,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.sizeRange')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-harddisk"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
@@ -171,6 +199,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.seeders')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
@@ -181,13 +210,14 @@ function onClose() {
|
||||
:hint="t('customRule.hint.publishTime')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-calendar-clock"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn @click="saveRuleInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">{{
|
||||
<VBtn @click="saveRuleInfo" prepend-icon="mdi-content-save" class="px-5">{{
|
||||
t('customRule.action.confirm')
|
||||
}}</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TransferDirectoryConf } from '@/api/types'
|
||||
import type { StorageConf, TransferDirectoryConf } from '@/api/types'
|
||||
import api from '@/api'
|
||||
import { nextTick } from 'vue'
|
||||
import { storageOptions } from '@/api/constants'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storageRemoteDict } from '@/api/constants'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -19,6 +19,10 @@ const props = defineProps({
|
||||
type: Object as PropType<{ [key: string]: any }>,
|
||||
required: true,
|
||||
},
|
||||
storages: {
|
||||
type: Array as PropType<StorageConf[]>,
|
||||
required: true,
|
||||
},
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
@@ -29,13 +33,26 @@ const isCollapsed = ref(true)
|
||||
// 类型下拉字典
|
||||
const typeItems = computed(() => [
|
||||
{ title: t('common.all'), value: '' },
|
||||
{ title: t('media.movie'), value: '电影' },
|
||||
{ title: t('media.tv'), value: '电视剧' },
|
||||
{ title: t('mediaType.movie'), value: '电影' },
|
||||
{ title: t('mediaType.tv'), value: '电视剧' },
|
||||
])
|
||||
|
||||
// 计算资源存储字典(整理方式为下载器时不能为远程存储)
|
||||
const resourceStorageOptions = computed(() => {
|
||||
return storageOptions.filter(item => !item.remote || props.directory.monitor_type !== 'downloader')
|
||||
return props.storages
|
||||
.filter(item => !storageRemoteDict[item.type] || props.directory.monitor_type !== 'downloader')
|
||||
.map(item => ({
|
||||
title: item.name,
|
||||
value: item.type,
|
||||
}))
|
||||
})
|
||||
|
||||
// 存储字典
|
||||
const libraryStorageOptions = computed(() => {
|
||||
return props.storages.map(item => ({
|
||||
title: item.name,
|
||||
value: item.type,
|
||||
}))
|
||||
})
|
||||
|
||||
// 自动整理方式下拉字典
|
||||
@@ -197,7 +214,7 @@ watch(
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.media_type"
|
||||
variant="underlined"
|
||||
:items="typeItems"
|
||||
@@ -206,7 +223,7 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.media_category"
|
||||
variant="underlined"
|
||||
:items="getCategories"
|
||||
@@ -214,7 +231,7 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.storage"
|
||||
variant="underlined"
|
||||
:items="resourceStorageOptions"
|
||||
@@ -260,10 +277,10 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.library_storage"
|
||||
variant="underlined"
|
||||
:items="storageOptions"
|
||||
:items="libraryStorageOptions"
|
||||
:label="t('directory.libraryStorage')"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
import api from '@/api'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { DownloaderConf } from '@/api/types'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import type { DownloaderInfo } from '@/api/types'
|
||||
import qbittorrent_image from '@images/logos/qbittorrent.png'
|
||||
import transmission_image from '@images/logos/transmission.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { downloaderDict, storageAttributes } from '@/api/constants'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 获取i18n实例
|
||||
const { t } = useI18n()
|
||||
const { useConditionalDataRefresh } = useBackgroundOptimization()
|
||||
|
||||
// 定义输入
|
||||
const props = defineProps({
|
||||
@@ -37,9 +43,6 @@ const emit = defineEmits(['close', 'done', 'change'])
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// timeout定时器
|
||||
let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
// 上传速率
|
||||
const upload_rate = ref(0)
|
||||
|
||||
@@ -49,6 +52,54 @@ const download_rate = ref(0)
|
||||
// 下载器详情弹窗
|
||||
const downloaderInfoDialog = ref(false)
|
||||
|
||||
// 表单
|
||||
const downloaderForm = ref()
|
||||
|
||||
// 路径前缀选项
|
||||
const prefixOptions = computed(() => {
|
||||
return storageAttributes.map(item => ({
|
||||
title: t(`storage.${item.type}`),
|
||||
value: item.type,
|
||||
}))
|
||||
})
|
||||
|
||||
function getStorageType(path: string) {
|
||||
if (!path) return 'local'
|
||||
// 查找匹配的存储类型
|
||||
const storage = storageAttributes.find(s => s.type !== 'local' && path.startsWith(`${s.type}:`))
|
||||
return storage?.type || 'local'
|
||||
}
|
||||
|
||||
function storage2Prefix(storage: string) {
|
||||
return storage === 'local' ? '' : storage + ':'
|
||||
}
|
||||
|
||||
// 获取存储路径前后缀
|
||||
function parseStoragePath(path: string): [prefix: string, suffix: string] {
|
||||
if (!path) return ['', '']
|
||||
const storage = getStorageType(path)
|
||||
const prefix = storage2Prefix(storage)
|
||||
return [prefix, path.slice(prefix.length)]
|
||||
}
|
||||
|
||||
// 更新存储路径前缀
|
||||
function updateStoragePrefix(row: PathMappingRow, storage: string) {
|
||||
const [, currentSuffix] = parseStoragePath(row.storage)
|
||||
const prefix = storage2Prefix(storage)
|
||||
row.storage = prefix + currentSuffix
|
||||
}
|
||||
|
||||
// 更新存储路径后缀
|
||||
function updateStorageSuffix(row: PathMappingRow, suffix: string) {
|
||||
const [currentPrefix] = parseStoragePath(row.storage)
|
||||
row.storage = currentPrefix + suffix
|
||||
}
|
||||
|
||||
const pathValidationRules = [
|
||||
(v: string) => !!v || t('downloader.pathMappingRequired'),
|
||||
(v: string) => v.startsWith('/') || t('downloader.pathMappingError'),
|
||||
]
|
||||
|
||||
// 下载器详情
|
||||
const downloaderInfo = ref<DownloaderConf>({
|
||||
name: '',
|
||||
@@ -56,11 +107,33 @@ const downloaderInfo = ref<DownloaderConf>({
|
||||
default: false,
|
||||
enabled: false,
|
||||
config: {},
|
||||
path_mapping: [],
|
||||
})
|
||||
|
||||
// 路径映射行定义
|
||||
interface PathMappingRow {
|
||||
id: string
|
||||
storage: string
|
||||
download: string
|
||||
}
|
||||
|
||||
// 路径映射行数据
|
||||
const pathMappingRows = ref<PathMappingRow[]>([])
|
||||
|
||||
// 生成随机ID
|
||||
function generateId() {
|
||||
return Math.random().toString(36).substring(2, 9)
|
||||
}
|
||||
|
||||
// 下载器是否应该刷新数据的计算属性
|
||||
const shouldRefresh = computed(() => props.allowRefresh && props.downloader.enabled)
|
||||
|
||||
// 调用API查询下载器数据
|
||||
async function loadDownloaderInfo() {
|
||||
if (!props.allowRefresh) {
|
||||
if (!shouldRefresh.value) {
|
||||
// 当下载器被禁用时,重置速率数据
|
||||
upload_rate.value = 0
|
||||
download_rate.value = 0
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -73,11 +146,6 @@ async function loadDownloaderInfo() {
|
||||
if (res) {
|
||||
upload_rate.value = res.upload_speed
|
||||
download_rate.value = res.download_speed
|
||||
// 定时查询
|
||||
clearTimeout(timeoutTimer)
|
||||
if (props.downloader.enabled) {
|
||||
timeoutTimer = setTimeout(loadDownloaderInfo, 3000)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
@@ -88,11 +156,24 @@ async function loadDownloaderInfo() {
|
||||
function openDownloaderInfoDialog() {
|
||||
// 深复制
|
||||
downloaderInfo.value = cloneDeep(props.downloader)
|
||||
// 初始化路径映射行数据
|
||||
pathMappingRows.value = (downloaderInfo.value.path_mapping || []).map(item => ({
|
||||
id: generateId(),
|
||||
storage: item[0],
|
||||
download: item[1],
|
||||
}))
|
||||
downloaderInfoDialog.value = true
|
||||
}
|
||||
|
||||
// 保存详情数据
|
||||
function saveDownloaderInfo() {
|
||||
async function saveDownloaderInfo() {
|
||||
// 表单校验
|
||||
const { valid } = await downloaderForm.value?.validate()
|
||||
if (!valid) return
|
||||
|
||||
// 同步路径映射数据
|
||||
downloaderInfo.value.path_mapping = pathMappingRows.value.map(row => [row.storage, row.download])
|
||||
|
||||
// 为空不保存,跳出警告框
|
||||
if (!downloaderInfo.value.name) {
|
||||
$toast.error(t('downloader.nameRequired'))
|
||||
@@ -122,29 +203,47 @@ function saveDownloaderInfo() {
|
||||
const getIcon = computed(() => {
|
||||
switch (props.downloader.type) {
|
||||
case 'qbittorrent':
|
||||
return qbittorrent_image
|
||||
return getLogoUrl('qbittorrent')
|
||||
case 'transmission':
|
||||
return transmission_image
|
||||
return getLogoUrl('transmission')
|
||||
default:
|
||||
return qbittorrent_image
|
||||
return getLogoUrl('downloader')
|
||||
}
|
||||
})
|
||||
|
||||
// 添加路径映射
|
||||
function addPathMapping() {
|
||||
pathMappingRows.value.push({
|
||||
id: generateId(),
|
||||
storage: '',
|
||||
download: '',
|
||||
})
|
||||
}
|
||||
|
||||
// 移除路径映射
|
||||
function removePathMapping(index: number) {
|
||||
pathMappingRows.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 按钮点击
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.downloader.enabled) {
|
||||
await loadDownloaderInfo()
|
||||
}
|
||||
})
|
||||
// 使用条件性数据刷新定时器(只在下载器启用时运行)
|
||||
const { stop: stopRefresh } = useConditionalDataRefresh(
|
||||
`downloader-${props.downloader.name}`,
|
||||
loadDownloaderInfo,
|
||||
shouldRefresh, // 响应式条件:只有当allowRefresh为true且downloader启用时才运行
|
||||
3000, // 3秒间隔
|
||||
true, // 立即执行一次
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
stopRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VHover v-slot="hover">
|
||||
@@ -172,23 +271,40 @@ onUnmounted(() => {
|
||||
/>
|
||||
<span class="text-h6">{{ downloader.name }}</span>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap text-sm" v-if="props.downloader.enabled">
|
||||
<div v-if="downloaderDict[downloader.type] && props.downloader.enabled" class="mt-1 flex flex-wrap text-sm">
|
||||
<span class="me-2">{{ `↑ ${formatFileSize(upload_rate, 1)}/s ` }}</span>
|
||||
<span>{{ `↓ ${formatFileSize(download_rate, 1)}/s` }}</span>
|
||||
</div>
|
||||
<div v-else-if="!downloaderDict[downloader.type]" class="mt-1 flex flex-wrap text-sm">
|
||||
<span class="me-2">自定义下载器</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-20">
|
||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
|
||||
<VImg :src="getIcon" cover class="mt-8 me-3" max-width="3rem" min-width="3rem" />
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VHover>
|
||||
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem" persistent>
|
||||
<VCard :title="`${props.downloader.name} - ${t('downloader.title')}`" class="rounded-t">
|
||||
|
||||
<VDialog
|
||||
v-if="downloaderInfoDialog"
|
||||
v-model="downloaderInfoDialog"
|
||||
scrollable
|
||||
max-width="40rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('common.config') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ props.downloader.name }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn v-model="downloaderInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VForm ref="downloaderForm">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch v-model="downloaderInfo.enabled" :label="t('downloader.enabled')" />
|
||||
@@ -210,6 +326,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -220,6 +337,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.host')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -229,6 +347,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.username')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -239,6 +358,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.password')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -278,7 +398,7 @@ onUnmounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="downloaderInfo.type == 'transmission'">
|
||||
<VRow v-else-if="downloaderInfo.type == 'transmission'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
@@ -287,6 +407,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -297,6 +418,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.host')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -306,6 +428,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.username')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -316,13 +439,119 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.password')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.type"
|
||||
:label="t('downloader.type')"
|
||||
:hint="t('downloader.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:hint="t('downloader.nameRequired')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VDivider class="my-2">
|
||||
<span class="text-body-1 font-weight-medium">{{ t('downloader.pathMapping') }}</span>
|
||||
</VDivider>
|
||||
|
||||
<div v-if="pathMappingRows.length === 0" class="text-center py-2">
|
||||
<VIcon icon="mdi-folder-network" size="48" class="text-disabled mb-1" />
|
||||
<div class="text-body-2 text-disabled">{{ t('common.noData') }}</div>
|
||||
</div>
|
||||
|
||||
<VCard v-for="(row, index) in pathMappingRows" :key="row.id" variant="outlined" class="my-2">
|
||||
<VCardText class="pa-3">
|
||||
<VRow align="center" no-gutters>
|
||||
<VCol cols="12" class="mb-2">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<VIcon icon="mdi-folder-outline" size="18" class="me-1 text-primary" />
|
||||
<span class="text-caption text-medium-emphasis">{{ t('downloader.storagePath') }}</span>
|
||||
</div>
|
||||
<VRow no-gutters>
|
||||
<VCol cols="12" sm="4" class="pe-2">
|
||||
<VSelect
|
||||
:model-value="getStorageType(row.storage)"
|
||||
:items="prefixOptions"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
@update:model-value="v => updateStoragePrefix(row, v)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" sm="8">
|
||||
<VTextField
|
||||
:model-value="parseStoragePath(row.storage)[1]"
|
||||
:placeholder="'/path/to/storage'"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
:rules="pathValidationRules"
|
||||
@update:model-value="v => updateStorageSuffix(row, v)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" class="mb-1">
|
||||
<div class="d-flex align-center justify-center my-1">
|
||||
<VIcon icon="mdi-arrow-down" size="18" class="text-medium-emphasis" />
|
||||
</div>
|
||||
<div class="d-flex align-center mb-1">
|
||||
<VIcon icon="mdi-download-outline" size="18" class="me-1 text-success" />
|
||||
<span class="text-caption text-medium-emphasis">{{ t('downloader.downloadPath') }}</span>
|
||||
</div>
|
||||
<VTextField
|
||||
v-model="row.download"
|
||||
:placeholder="'/path/to/download'"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
:rules="pathValidationRules"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" class="d-flex justify-end pt-1">
|
||||
<IconBtn variant="text" color="error" size="small" @click="removePathMapping(index)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus-circle-outline"
|
||||
@click="addPathMapping"
|
||||
class="mt-1"
|
||||
size="small"
|
||||
>
|
||||
{{ t('common.add') }} {{ t('downloader.pathMapping') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn @click="saveDownloaderInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
||||
<VBtn @click="saveDownloaderInfo" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { formatFileSize } from '@/@core/utils/formatters'
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
info: Object as PropType<DownloadingInfo>,
|
||||
downloaderName: String,
|
||||
})
|
||||
|
||||
// 是否显示卡片
|
||||
@@ -42,16 +43,15 @@ function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 计算文本类
|
||||
function getTextClass() {
|
||||
return imageLoaded.value ? 'text-white' : ''
|
||||
}
|
||||
|
||||
// 下载状态控制
|
||||
async function toggleDownload() {
|
||||
const operation = isDownloading.value ? 'stop' : 'start'
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`download/${operation}/${props.info?.hash}`)
|
||||
const result: { [key: string]: any } = await api.get(`download/${operation}/${props.info?.hash}`, {
|
||||
params: {
|
||||
name: props.downloaderName,
|
||||
},
|
||||
})
|
||||
|
||||
if (result.success) isDownloading.value = !isDownloading.value
|
||||
} catch (error) {
|
||||
@@ -62,7 +62,7 @@ async function toggleDownload() {
|
||||
// 删除下截
|
||||
async function deleteDownload() {
|
||||
try {
|
||||
await api.delete(`download/${props.info?.hash}`)
|
||||
await api.delete(`download/${props.info?.hash}`, { params: { name: props.downloaderName } })
|
||||
cardState.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -71,35 +71,52 @@ async function deleteDownload() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard v-if="cardState" :key="props.info?.hash">
|
||||
<VCard v-if="cardState" :key="props.info?.hash" class="flex flex-col h-full" min-height="150">
|
||||
<template #image>
|
||||
<VImg :src="props.info?.media.image" aspect-ratio="2/3" cover class="brightness-50" @load="imageLoadHandler" />
|
||||
<VImg :src="props.info?.media.image" aspect-ratio="2/3" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="absolute inset-0 outline-none downloading-card-background"></div>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
|
||||
<VCardTitle class="break-words whitespace-normal" :class="getTextClass()">
|
||||
{{ props.info?.media.title || props.info?.name }}
|
||||
{{
|
||||
props.info?.media.episode
|
||||
? `${props.info?.media.season} ${props.info?.media.episode}`
|
||||
: props.info?.season_episode
|
||||
}}
|
||||
</VCardTitle>
|
||||
<div>
|
||||
<VCardTitle class="break-words whitespace-normal text-white">
|
||||
{{ props.info?.media.title || props.info?.name }}
|
||||
{{
|
||||
props.info?.media.episode
|
||||
? `${props.info?.media.season} ${props.info?.media.episode}`
|
||||
: props.info?.season_episode
|
||||
}}
|
||||
</VCardTitle>
|
||||
|
||||
<VCardSubtitle class="break-words whitespace-normal" :class="getTextClass()">
|
||||
{{ props.info?.title }}
|
||||
</VCardSubtitle>
|
||||
<VCardSubtitle class="break-words whitespace-normal text-white">
|
||||
{{ props.info?.title }}
|
||||
</VCardSubtitle>
|
||||
|
||||
<VCardText class="text-subtitle-1 pt-3 pb-1" :class="getTextClass()">
|
||||
{{ getSpeedText() }}
|
||||
</VCardText>
|
||||
<VCardText class="text-subtitle-1 pt-3 pb-1 text-white">
|
||||
{{ getSpeedText() }}
|
||||
</VCardText>
|
||||
|
||||
<VCardText v-if="getPercentage() > 0" :class="getTextClass()">
|
||||
<VProgressLinear :model-value="getPercentage()" />
|
||||
</VCardText>
|
||||
<VCardText v-if="getPercentage() > 0" class="text-white">
|
||||
<VProgressLinear :model-value="getPercentage()" bg-color="success" color="success" />
|
||||
</VCardText>
|
||||
|
||||
<VCardActions class="justify-space-between">
|
||||
<VBtn :icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`" @click="toggleDownload" />
|
||||
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
|
||||
</VCardActions>
|
||||
<VCardActions class="justify-space-between">
|
||||
<VBtn :icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`" @click="toggleDownload" />
|
||||
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
|
||||
</VCardActions>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.downloading-card-background {
|
||||
background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,7 +56,7 @@ onMounted(() => {
|
||||
<VCardTitle>{{ t('filterRule.priority') }} {{ props.pri }}</VCardTitle>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.rules"
|
||||
variant="underlined"
|
||||
:items="selectFilterOptions"
|
||||
|
||||
@@ -3,11 +3,15 @@ import draggable from 'vuedraggable'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import { CustomRule, FilterRuleGroup } from '@/api/types'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
||||
import filter_group_svg from '@images/svg/filter-group.svg'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 获取i18n实例
|
||||
const { t } = useI18n()
|
||||
@@ -219,8 +223,14 @@ function onClose() {
|
||||
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem">
|
||||
<VCard :title="`${props.group.name} - ${t('filterRule.title')}`" class="rounded-t">
|
||||
<VDialog
|
||||
v-if="groupInfoDialog"
|
||||
v-model="groupInfoDialog"
|
||||
scrollable
|
||||
max-width="80rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard :title="`${props.group.name} - ${t('filterRule.title')}`">
|
||||
<VDialogCloseBtn v-model="groupInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardItem class="pt-1">
|
||||
@@ -233,26 +243,29 @@ function onClose() {
|
||||
:hint="t('filterRule.groupName')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="groupInfo.media_type"
|
||||
:label="t('filterRule.mediaType')"
|
||||
:items="mediaTypeItems"
|
||||
:hint="t('filterRule.mediaType')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-movie-open"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="groupInfo.category"
|
||||
:items="getCategories"
|
||||
:label="t('filterRule.category')"
|
||||
:hint="t('filterRule.category')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-folder-open"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -280,17 +293,17 @@ function onClose() {
|
||||
<div class="text-center" v-if="filterRuleCards.length == 0">{{ t('filterRule.add') }}</div>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn color="primary" variant="tonal" @click="addFilterCard">
|
||||
<VBtn color="primary" @click="addFilterCard">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="importRules('priority')">
|
||||
<VBtn color="success" @click="importRules('priority')">
|
||||
<VIcon icon="mdi-import" />
|
||||
</VBtn>
|
||||
<VBtn color="info" variant="tonal" @click="shareRules">
|
||||
<VBtn color="info" @click="shareRules">
|
||||
<VIcon icon="mdi-share" />
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="saveGroupInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
||||
<VBtn @click="saveGroupInfo" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||