Compare commits
1181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff871e7049 | ||
|
|
20f80d0f1f | ||
|
|
36bb99cf06 | ||
|
|
24c2e3b49c | ||
|
|
486a471c0c | ||
|
|
ce32da9176 | ||
|
|
39dd1b9e00 | ||
|
|
807711cae8 | ||
|
|
00feeb86ca | ||
|
|
4378c4843b | ||
|
|
5676e0b9b1 | ||
|
|
38a841dc9c | ||
|
|
6e92673438 | ||
|
|
5e2585b210 | ||
|
|
c3c323b018 | ||
|
|
5390af50ac | ||
|
|
c0f58c7df4 | ||
|
|
9bdf102f7a | ||
|
|
52df6bef38 | ||
|
|
c5da08e388 | ||
|
|
8a069db55e | ||
|
|
9900042bc3 | ||
|
|
e4f02a326f | ||
|
|
199693abf8 | ||
|
|
32b0c1c00c | ||
|
|
e4d0afc380 | ||
|
|
8e89078055 | ||
|
|
2e0a582d22 | ||
|
|
9c2c4d9fc4 | ||
|
|
4e0191ba5c | ||
|
|
9247850411 | ||
|
|
29b4854dae | ||
|
|
338af516be | ||
|
|
ea892a31c0 | ||
|
|
c0f3814560 | ||
|
|
15f984ba44 | ||
|
|
362c601c7e | ||
|
|
4f0204d465 | ||
|
|
d92e79169c | ||
|
|
563849aab3 | ||
|
|
b0a27c6405 | ||
|
|
b664e0b447 | ||
|
|
d625d1ad46 | ||
|
|
cec9563963 | ||
|
|
2df3ce1b86 | ||
|
|
2a2ed9fc6a | ||
|
|
0921932601 | ||
|
|
50b8ea1bc9 | ||
|
|
f88ae5643c | ||
|
|
cf5f4e5b6a | ||
|
|
a8db14a069 | ||
|
|
a20977e522 | ||
|
|
7efc32637b | ||
|
|
5d1445fe64 | ||
|
|
2070240b50 | ||
|
|
1d130c99a3 | ||
|
|
7861afb2d7 | ||
|
|
5c4511f935 | ||
|
|
212ba94e11 | ||
|
|
f863a09212 | ||
|
|
728ced475b | ||
|
|
a192e00db5 | ||
|
|
85e6b17db7 | ||
|
|
156d835d6d | ||
|
|
b27eeea6c0 | ||
|
|
39a9fe0541 | ||
|
|
92d153760b | ||
|
|
ff9dc26040 | ||
|
|
269da6eefe | ||
|
|
817bc1019d | ||
|
|
de8c22c180 | ||
|
|
8f074fb381 | ||
|
|
e16870859d | ||
|
|
e1ba13abf2 | ||
|
|
8d1f226035 | ||
|
|
5367a07a90 | ||
|
|
8e681dbbe7 | ||
|
|
c26c3c4284 | ||
|
|
461ed560f0 | ||
|
|
dd27bcdaca | ||
|
|
c076be868a | ||
|
|
d2634095e4 | ||
|
|
a5a3e1e1e4 | ||
|
|
09a20967c0 | ||
|
|
9a2af0a412 | ||
|
|
6bbac7db7d | ||
|
|
b212e066b6 | ||
|
|
f7d354fca4 | ||
|
|
693b081ecf | ||
|
|
d5f0fed954 | ||
|
|
67ef98bfee | ||
|
|
1094d80b9c | ||
|
|
98e6481812 | ||
|
|
7db088eb79 | ||
|
|
dec154c042 | ||
|
|
d06ce5d984 | ||
|
|
af604d0c5c | ||
|
|
fa39c3750c | ||
|
|
ce64fb03ce | ||
|
|
764f14a7f4 | ||
|
|
621200b3b2 | ||
|
|
60c46ebbaf | ||
|
|
4fe65b770e | ||
|
|
c010162e54 | ||
|
|
5cc4ea7740 | ||
|
|
8a19fbc584 | ||
|
|
24412989bb | ||
|
|
55d0693e41 | ||
|
|
0459a2d108 | ||
|
|
624b55576d | ||
|
|
c848b90599 | ||
|
|
79bc99e624 | ||
|
|
850f7ee9be | ||
|
|
4cc105287f | ||
|
|
527c0845bc | ||
|
|
2a3ef1a1f6 | ||
|
|
7539fa26aa | ||
|
|
3547c146a8 | ||
|
|
da630b0966 | ||
|
|
cff4c1fecc | ||
|
|
3f525c6457 | ||
|
|
8c1eb3e981 | ||
|
|
effc83b9a1 | ||
|
|
39ce8d93dd | ||
|
|
fa32ac9f48 | ||
|
|
ab243bbbb7 | ||
|
|
abcd3f5e81 | ||
|
|
92bf1ca790 | ||
|
|
faadf39b80 | ||
|
|
878c3c98b2 | ||
|
|
2d8ae9f462 | ||
|
|
00051a0eac | ||
|
|
88726c0f12 | ||
|
|
79bc9f0ff0 | ||
|
|
4492e211bb | ||
|
|
bb5299c812 | ||
|
|
f54a5285d6 | ||
|
|
03b80d6434 | ||
|
|
a916a1569d | ||
|
|
5c27c32914 | ||
|
|
c57a8b7b48 | ||
|
|
0aa7d37e45 | ||
|
|
d3b3b08318 | ||
|
|
5127ab6c08 | ||
|
|
a3810fb9e2 | ||
|
|
f05eac0b93 | ||
|
|
ef170b54c6 | ||
|
|
536844d291 | ||
|
|
21d4f9fd73 | ||
|
|
2aae0f08ad | ||
|
|
248984261e | ||
|
|
82ab594e0c | ||
|
|
5ca374349d | ||
|
|
7452e18fc3 | ||
|
|
75dffdb7c2 | ||
|
|
a3d74929b3 | ||
|
|
fd9c314d55 | ||
|
|
d02e98736f | ||
|
|
2e46ebe353 | ||
|
|
fffb526a87 | ||
|
|
98769a313f | ||
|
|
1c28d98d70 | ||
|
|
7a430b095a | ||
|
|
864af36b1c | ||
|
|
e3fa0b9dae | ||
|
|
ed28832484 | ||
|
|
e692a91113 | ||
|
|
7574719e04 | ||
|
|
ca800f7ae7 | ||
|
|
7d0550825b | ||
|
|
59abda63e3 | ||
|
|
0776bc9fa0 | ||
|
|
8c78f9ca90 | ||
|
|
1889d5d1e3 | ||
|
|
165247b263 | ||
|
|
9a2384bf02 | ||
|
|
71c4f587d3 | ||
|
|
8edbfa0b0e | ||
|
|
ee187a1c75 | ||
|
|
afb617724c | ||
|
|
05bbecba53 | ||
|
|
deb9b103da | ||
|
|
2d92315e17 | ||
|
|
077f0c1366 | ||
|
|
eab4caa279 | ||
|
|
3bba73abf1 | ||
|
|
8edf1f686a | ||
|
|
3440b377ee | ||
|
|
9161e5e300 | ||
|
|
25fdb09c61 | ||
|
|
25119b224d | ||
|
|
76f79e1656 | ||
|
|
b5230cef79 | ||
|
|
f871746efb | ||
|
|
d17ca69a28 | ||
|
|
e445437129 | ||
|
|
090c5476c6 | ||
|
|
646c7b33dd | ||
|
|
0902f16f84 | ||
|
|
ec4eaed1df | ||
|
|
175f610524 | ||
|
|
530fe9d35b | ||
|
|
48ed396a19 | ||
|
|
b0356c217d | ||
|
|
55eed1ecb5 | ||
|
|
50ae739a4d | ||
|
|
d9cbcc2991 | ||
|
|
ad12701fe2 | ||
|
|
2b426a47c6 | ||
|
|
7f0f12ac41 | ||
|
|
6789d63ca1 | ||
|
|
3202251f55 | ||
|
|
8e99ad9cf9 | ||
|
|
83dde400e7 | ||
|
|
1b57f925ee | ||
|
|
16428066b9 | ||
|
|
e211a80cf4 | ||
|
|
ea0b5b62d9 | ||
|
|
62dc2c4f66 | ||
|
|
b2a2c7080e | ||
|
|
05c2e7855a | ||
|
|
8d9c622dc5 | ||
|
|
bf0b17c314 | ||
|
|
37f31f6554 | ||
|
|
3de409fb07 | ||
|
|
7e9c0fd206 | ||
|
|
fb4f5658a8 | ||
|
|
a9f4ec963b | ||
|
|
542e33d7b4 | ||
|
|
39c250ba09 | ||
|
|
924fcef403 | ||
|
|
e586342b19 | ||
|
|
c795de9b2d | ||
|
|
6fa1cf28f4 | ||
|
|
3f70aafdad | ||
|
|
f8ceee39b3 | ||
|
|
0a22f33e34 | ||
|
|
cf88ed9a58 | ||
|
|
49dfd794c1 | ||
|
|
68f2f010d1 | ||
|
|
9eed2fea87 | ||
|
|
1f170030ee | ||
|
|
e78ed20936 | ||
|
|
b1787b207d | ||
|
|
fdb34732cc | ||
|
|
fc1f163a94 | ||
|
|
a771dc5354 | ||
|
|
d28360a161 | ||
|
|
a730abc437 | ||
|
|
5b72eda4fc | ||
|
|
6c49d7a59e | ||
|
|
8900366faf | ||
|
|
e8e0ac9084 | ||
|
|
c66ee881b1 | ||
|
|
c055740926 | ||
|
|
a5bc4e6baf | ||
|
|
15b4ee5893 | ||
|
|
8868403ff3 | ||
|
|
3abff72e25 | ||
|
|
0c56cf0be7 | ||
|
|
ce12d04648 | ||
|
|
efc0ae4df6 | ||
|
|
2530c3bcd9 | ||
|
|
60e2402aff | ||
|
|
1a478f97fb | ||
|
|
33666703af | ||
|
|
cd69172a99 | ||
|
|
61749e3595 | ||
|
|
b658533262 | ||
|
|
d8015b7def | ||
|
|
33599cc21d | ||
|
|
bf22a4809d | ||
|
|
4a6f7390e6 | ||
|
|
405e460ad6 | ||
|
|
18566c0e9d | ||
|
|
2c471a936f | ||
|
|
2efb07402f | ||
|
|
9434ef71e4 | ||
|
|
e06b9537ff | ||
|
|
2829e3b082 | ||
|
|
1a0fc10559 | ||
|
|
5a1aec3323 | ||
|
|
48913b8811 | ||
|
|
0a7d53b5c7 | ||
|
|
da0cd14af8 | ||
|
|
342c62c085 | ||
|
|
891274cc0e | ||
|
|
889a4b744a | ||
|
|
7fc5b74851 | ||
|
|
785cbcf81d | ||
|
|
364b660390 | ||
|
|
599ca912f4 | ||
|
|
2f66f0f1fc | ||
|
|
cd2f561194 | ||
|
|
c59a555a2d | ||
|
|
4413fedec5 | ||
|
|
d7562ea506 | ||
|
|
951d76481b | ||
|
|
68b0071009 | ||
|
|
0594d1d5b2 | ||
|
|
4f328add1b | ||
|
|
62c9a10377 | ||
|
|
d3d0d847f6 | ||
|
|
b7dd397664 | ||
|
|
c0276fca9f | ||
|
|
4691d12faa | ||
|
|
d0cac34d08 | ||
|
|
2f46c19826 | ||
|
|
b1cb07ae8c | ||
|
|
19710a5f0f | ||
|
|
4362bbed42 | ||
|
|
89cf7070bb | ||
|
|
369afd6674 | ||
|
|
c4fd8f5631 | ||
|
|
c9ebf23977 | ||
|
|
e239c0c5ea | ||
|
|
87d780d985 | ||
|
|
f51b253c83 | ||
|
|
bf05cd0697 | ||
|
|
8e5b8f7207 | ||
|
|
5e3e106d91 | ||
|
|
c5fa6aad68 | ||
|
|
989e8b4c5e | ||
|
|
a865aa433c | ||
|
|
df8e6016cd | ||
|
|
75da7d35b4 | ||
|
|
e2722801e4 | ||
|
|
63e28b76c8 | ||
|
|
9dc63e2c21 | ||
|
|
08a2741c06 | ||
|
|
0dd95508b5 | ||
|
|
f3c524b6b5 | ||
|
|
a73c28c1f7 | ||
|
|
b3fb7e1de1 | ||
|
|
3620b2a979 | ||
|
|
1046cb276f | ||
|
|
6dfda4807c | ||
|
|
8d13f3e5ca | ||
|
|
9ebe740c69 | ||
|
|
73673820f1 | ||
|
|
c6d0116e0f | ||
|
|
6a06001dae | ||
|
|
ad75b50a0c | ||
|
|
2a68aa05f6 | ||
|
|
fa90411c7a | ||
|
|
643ddcef07 | ||
|
|
addc0838c0 | ||
|
|
8b43e0a754 | ||
|
|
f81a9f0929 | ||
|
|
3cf5cc24cd | ||
|
|
841e9479af | ||
|
|
8c3380e8f5 | ||
|
|
0ac42f0a76 | ||
|
|
caef6eca67 | ||
|
|
0867236b68 | ||
|
|
09dfdbaf67 | ||
|
|
57224e15fb | ||
|
|
200500a060 | ||
|
|
a4731aade1 | ||
|
|
7d21eabf1a | ||
|
|
b639737bd6 | ||
|
|
d02ece234c | ||
|
|
889a5c9e51 | ||
|
|
880a34f508 | ||
|
|
50b0148ed6 | ||
|
|
285ddab45a | ||
|
|
aa12f4b6b6 | ||
|
|
9bbb060073 | ||
|
|
3b0623628c | ||
|
|
b45c147452 | ||
|
|
25bc7c4b3c | ||
|
|
d6b7b6d813 | ||
|
|
a3ac46c891 | ||
|
|
b6e824246b | ||
|
|
5191f6780d | ||
|
|
261aaf17ad | ||
|
|
258e64bca7 | ||
|
|
e905df014e | ||
|
|
b93f8f2bff | ||
|
|
9aa0a5e1b7 | ||
|
|
ee9f41d015 | ||
|
|
ad6a664cbe | ||
|
|
3387067636 | ||
|
|
07dc3c3e9a | ||
|
|
262b4bebd4 | ||
|
|
6e50cf31de | ||
|
|
14aa75dfae | ||
|
|
348aa4757b | ||
|
|
6e6819acc1 | ||
|
|
51a58aaae0 | ||
|
|
fbde99389e | ||
|
|
5a4e345529 | ||
|
|
b446afb6d8 | ||
|
|
8580af36d1 | ||
|
|
95ca092117 | ||
|
|
ba200cae5c | ||
|
|
87c73e0253 | ||
|
|
d4d7f635f5 | ||
|
|
729db1510e | ||
|
|
8a12ecf918 | ||
|
|
cacc2602df | ||
|
|
8c6cfa7fc5 | ||
|
|
0113f28d8c | ||
|
|
d870b788bc | ||
|
|
19a3213be0 | ||
|
|
f5c8a463fa | ||
|
|
ff3b5b4232 | ||
|
|
6da0aae362 | ||
|
|
abbce2644a | ||
|
|
1c5773444e | ||
|
|
1674f15d7c | ||
|
|
c6981e9955 | ||
|
|
96d3426d0c | ||
|
|
c88b2abcce | ||
|
|
42fe928155 | ||
|
|
4cc455b948 | ||
|
|
bce073ebe0 | ||
|
|
c27167097e | ||
|
|
44d23480a3 | ||
|
|
01796b3dc5 | ||
|
|
dcf0924c73 | ||
|
|
cc8d5cf931 | ||
|
|
6083887675 | ||
|
|
beb0506b0c | ||
|
|
0f906f791a | ||
|
|
7614696e61 | ||
|
|
4235d3687c | ||
|
|
2960e7cfde | ||
|
|
e0ebc35178 | ||
|
|
07c9442ac8 | ||
|
|
ccc820e8d2 | ||
|
|
68bb568400 | ||
|
|
13cd214e6d | ||
|
|
311880bcd3 | ||
|
|
088ebbe0bb | ||
|
|
de3523056a | ||
|
|
cf139a938e | ||
|
|
be2f4d0170 | ||
|
|
79493665c1 | ||
|
|
106062da82 | ||
|
|
50e54e943d | ||
|
|
6b811f2250 | ||
|
|
fa7f2a6c7c | ||
|
|
e362f3cbdd | ||
|
|
f4c4d7495f | ||
|
|
5b850d9464 | ||
|
|
d7f74a3a8a | ||
|
|
91dbf065db | ||
|
|
1759e666ba | ||
|
|
65230f1ae8 | ||
|
|
508cf5d08f | ||
|
|
0e9ddc9da2 | ||
|
|
48e6fc4466 | ||
|
|
30a4c55050 | ||
|
|
dee5d9d213 | ||
|
|
c5e2b1349f | ||
|
|
0e005c3c7e | ||
|
|
348ae6b313 | ||
|
|
122ecc82fd | ||
|
|
88fad5b764 | ||
|
|
f01971ee3a | ||
|
|
5e8489c620 | ||
|
|
6900042cf7 | ||
|
|
75862c026a | ||
|
|
bbe3368c69 | ||
|
|
587f06eb9f | ||
|
|
7114c63e8f | ||
|
|
2a6f9e3cc0 | ||
|
|
00d37d7bda | ||
|
|
546af84dab | ||
|
|
5953496d84 | ||
|
|
0fda7c70de | ||
|
|
48546e1999 | ||
|
|
06355ff91d | ||
|
|
523f8c4cc8 | ||
|
|
73f6e7482f | ||
|
|
81ab3f9da8 | ||
|
|
d520645a8b | ||
|
|
af67fddce0 | ||
|
|
6d89dad8de | ||
|
|
f3ab2a8eff | ||
|
|
74c980c7a5 | ||
|
|
52fc2557ec | ||
|
|
34124418f8 | ||
|
|
e2d36da299 | ||
|
|
9965428bae | ||
|
|
e62a0b5a8d | ||
|
|
3c926f7485 | ||
|
|
de3f4e6374 | ||
|
|
2e22f6ae86 | ||
|
|
99665c7d79 | ||
|
|
a4a00586c7 | ||
|
|
cf59a07d4b | ||
|
|
8a362d0740 | ||
|
|
b49385af29 | ||
|
|
b227412c96 | ||
|
|
b3c8faab70 | ||
|
|
9a480dd803 | ||
|
|
847fd13982 | ||
|
|
7fa4f4a2f0 | ||
|
|
4207a70716 | ||
|
|
c97247b92b | ||
|
|
e9bed7ff8a | ||
|
|
f25a619f13 | ||
|
|
2065b05143 | ||
|
|
eec1f2d7b3 | ||
|
|
17a343392c | ||
|
|
a2b2e8cd94 | ||
|
|
9703b2dbee | ||
|
|
310a501380 | ||
|
|
30bf895ae1 | ||
|
|
4f9dce70d3 | ||
|
|
f495e13667 | ||
|
|
f293681588 | ||
|
|
2f1a356e65 | ||
|
|
5909d2423c | ||
|
|
42f7df8f4a | ||
|
|
abaa40d819 | ||
|
|
0d05a104c4 | ||
|
|
e8708f8de7 | ||
|
|
7918b21b5b | ||
|
|
088db67089 | ||
|
|
62e0d8e9dc | ||
|
|
96d655155a | ||
|
|
a475085d7b | ||
|
|
58fdb77b37 | ||
|
|
8a25c6578d | ||
|
|
ef62bd6e98 | ||
|
|
876a46607b | ||
|
|
107f70abde | ||
|
|
090b9d735d | ||
|
|
dbeea6afcc | ||
|
|
2931f5df46 | ||
|
|
e14c81d178 | ||
|
|
a9403c9c34 | ||
|
|
dc4914e3ca | ||
|
|
f3dbc4afad | ||
|
|
e3e22aebd9 | ||
|
|
0ca2f20b24 | ||
|
|
14279c773d | ||
|
|
8372f63eb6 | ||
|
|
b7b62d7922 | ||
|
|
162cce1f50 | ||
|
|
aa49c6ccbc | ||
|
|
a40e52079f | ||
|
|
c29e329548 | ||
|
|
e2d26f6a25 | ||
|
|
1752256868 | ||
|
|
23d7f0dcc1 | ||
|
|
288aeed178 | ||
|
|
9a9a618136 | ||
|
|
723eb319e1 | ||
|
|
96684a8d13 | ||
|
|
fc9fe5e21e | ||
|
|
24b763d808 | ||
|
|
f761cdff00 | ||
|
|
b785769138 | ||
|
|
6d1febd70a | ||
|
|
bdbaf503ca | ||
|
|
f9e74cf436 | ||
|
|
e043669a10 | ||
|
|
78d8fdba9d | ||
|
|
5c0f0386a6 | ||
|
|
30b39283b6 | ||
|
|
de84c39d2f | ||
|
|
65152e7e37 | ||
|
|
ba343ce5fa | ||
|
|
60495668a6 | ||
|
|
f2ac624dbb | ||
|
|
6238849d3f | ||
|
|
82cb903c1f | ||
|
|
5e5eb95b55 | ||
|
|
74e6f8b03e | ||
|
|
a2bf0d2b16 | ||
|
|
7532d39978 | ||
|
|
5cc9bf7418 | ||
|
|
20bdb940cd | ||
|
|
e9b214cff8 | ||
|
|
54f5fb2877 | ||
|
|
e86cb9e1cc | ||
|
|
3f258b9016 | ||
|
|
b54e144d0e | ||
|
|
7b20a7b775 | ||
|
|
df66b3e917 | ||
|
|
a919622d08 | ||
|
|
2a9ce950b7 | ||
|
|
48c12b765d | ||
|
|
1120055eed | ||
|
|
c66b6649e2 | ||
|
|
8479099926 | ||
|
|
cab65be1c9 | ||
|
|
6689e976c2 | ||
|
|
712dfa3fe1 | ||
|
|
346121f3c2 | ||
|
|
61c073ad6c | ||
|
|
4b3733bc19 | ||
|
|
b29c6bd83f | ||
|
|
b40fc4bd30 | ||
|
|
a225ba6075 | ||
|
|
303fe39c01 | ||
|
|
d343cbcf71 | ||
|
|
0eef8c5174 | ||
|
|
46fe257585 | ||
|
|
f69a57863e | ||
|
|
8876aadcfa | ||
|
|
485e9691a0 | ||
|
|
a0e7283ae6 | ||
|
|
b44c0647f1 | ||
|
|
7e60ab9064 | ||
|
|
f05c1f42b5 | ||
|
|
672bbb4265 | ||
|
|
10c1041b06 | ||
|
|
59c73facfe | ||
|
|
ba7d4cd392 | ||
|
|
d76a50c216 | ||
|
|
617223777b | ||
|
|
6ef047050d | ||
|
|
942ecc4c04 | ||
|
|
e72f9a8374 | ||
|
|
9cf782eb5b | ||
|
|
660338688a | ||
|
|
2d50bd7536 | ||
|
|
b02a4f1347 | ||
|
|
1748fdea34 | ||
|
|
6bbaf43671 | ||
|
|
4a66aaadad | ||
|
|
e2e239f6d9 | ||
|
|
fe22403e66 | ||
|
|
3313c71805 | ||
|
|
1e60e83514 | ||
|
|
9c893abcdf | ||
|
|
ead891ca2f | ||
|
|
8713e3cc86 | ||
|
|
3cc83d10d3 | ||
|
|
192ded374a | ||
|
|
13997c7e74 | ||
|
|
71b0dd4cc2 | ||
|
|
a58a0cdffe | ||
|
|
6aeb040db4 | ||
|
|
fef20e361e | ||
|
|
a63a07701d | ||
|
|
5dd56f2db3 | ||
|
|
275b095574 | ||
|
|
05eae71fba | ||
|
|
777b3c9445 | ||
|
|
a214168b1e | ||
|
|
9d55d02557 | ||
|
|
16c084ba80 | ||
|
|
b0f4ccc186 | ||
|
|
96d0606b4d | ||
|
|
450b9ec28a | ||
|
|
2ccf03fc1b | ||
|
|
38dfb3af07 | ||
|
|
ae4c59bfdb | ||
|
|
c9f4fdbee8 | ||
|
|
d21f461dda | ||
|
|
28a5a83315 | ||
|
|
11d11b88bf | ||
|
|
ff7658b5ba | ||
|
|
351faf2891 | ||
|
|
7d66229bad | ||
|
|
2b08be1e7d | ||
|
|
8255cfd479 | ||
|
|
f356bb4407 | ||
|
|
07e60291a2 | ||
|
|
2dbe8e6685 | ||
|
|
40f36b2afd | ||
|
|
d4260d5103 | ||
|
|
45f68bc936 | ||
|
|
9469074837 | ||
|
|
193807bb6f | ||
|
|
d4548db5b9 | ||
|
|
29aaea6fe6 | ||
|
|
369cc6438f | ||
|
|
d80b39c77b | ||
|
|
626725a8ca | ||
|
|
8be96358ae | ||
|
|
f2bfbfa3c5 | ||
|
|
7c9ffd6abc | ||
|
|
b370354287 | ||
|
|
145d71e283 | ||
|
|
eeea82d815 | ||
|
|
babd267bc4 | ||
|
|
e136c931ac | ||
|
|
ae00602345 | ||
|
|
5382108ee7 | ||
|
|
514063d3fb | ||
|
|
b08f396fec | ||
|
|
d37a7f06f1 | ||
|
|
ad7bca3aae | ||
|
|
4fb70ba80e | ||
|
|
1225b2eb9e | ||
|
|
24b2f103b9 | ||
|
|
0d304b58ca | ||
|
|
f419dbd794 | ||
|
|
7854cc81a8 | ||
|
|
9ad1bd29bd | ||
|
|
b88d4f0ecb | ||
|
|
44168b62d2 | ||
|
|
1dab013436 | ||
|
|
64a4a7aff5 | ||
|
|
e43b545c89 | ||
|
|
69fcde250e | ||
|
|
63d6290166 | ||
|
|
c1d759f3f3 | ||
|
|
3a782bc69c | ||
|
|
bea752879c | ||
|
|
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 |
2
.github/workflows/build.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
name: ${{ env.frontend_version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
make_latest: false
|
||||
make_latest: true
|
||||
files: |
|
||||
dist.zip
|
||||
env:
|
||||
|
||||
52
.github/workflows/pr-agent.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: PR-Agent
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
# Fork 审查需要目标仓库凭据;该 job 仅通过 GitHub API 读取 PR 内容,不 checkout 或执行 PR 分支代码。
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- ready_for_review
|
||||
- review_requested
|
||||
- synchronize
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
- edited
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
pr-agent:
|
||||
if: >-
|
||||
github.event.sender.type != 'Bot' &&
|
||||
(
|
||||
github.event_name == 'pull_request_target' ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request != null &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR"]'), github.event.comment.author_association)
|
||||
)
|
||||
)
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Run PR Review
|
||||
uses: docker://ghcr.io/infinitypacer/pr-review-runner:latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENAI.API_BASE: ${{ secrets.OPENAI_API_BASE }}
|
||||
PRR_AUTO_REVIEW_SCOPE: all
|
||||
PRR_ALLOWED_ASSOCIATIONS: '["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR"]'
|
||||
PRR_DISABLED_COMMANDS: '["/improve"]'
|
||||
PRR_SKIP_LABEL: skip pr-agent
|
||||
PRR_SKIP_TITLE_PATTERN: '^(?:\[Auto\]|Auto)'
|
||||
config.response_language: zh-CN
|
||||
37
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Frontend Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: frontend-tests-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: yarn
|
||||
cache-dependency-path: yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
|
||||
- name: Unit tests with coverage
|
||||
run: yarn test:coverage
|
||||
6
.gitignore
vendored
@@ -12,7 +12,9 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
dev-dist
|
||||
coverage
|
||||
*.local
|
||||
package-lock.json
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
@@ -34,3 +36,7 @@ dev-dist
|
||||
# iconify dist files
|
||||
src/@iconify/*.js
|
||||
public/plugin_icon/**
|
||||
docs-lock/
|
||||
.trae/
|
||||
output/
|
||||
.codex-qa/
|
||||
|
||||
27
README.md
@@ -11,15 +11,6 @@
|
||||
- 支持多语言(中文/英文)
|
||||
- 完整的插件系统支持,包括远程组件动态加载
|
||||
|
||||
## 模块联邦功能
|
||||
|
||||
MoviePilot 现已支持模块联邦(Module Federation)功能,允许插件开发者创建可动态加载的远程组件,实现更丰富的插件用户界面。
|
||||
|
||||
### 相关文档
|
||||
|
||||
- [模块联邦开发指南](docs/module-federation-guide.md) - 如何开发远程组件插件
|
||||
- [模块联邦问题排查指南](docs/federation-troubleshooting.md) - 常见问题和解决方案
|
||||
- [插件远程组件示例](examples/plugin-component/) - 开发插件组件的完整示例项目
|
||||
|
||||
## 开发部署
|
||||
|
||||
@@ -49,6 +40,15 @@ yarn dev
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 单元测试
|
||||
|
||||
```sh
|
||||
yarn test:run
|
||||
yarn test:coverage
|
||||
```
|
||||
|
||||
测试文件组织、共享测试设施、HTTP mock、覆盖率门禁和新增用例规范见[单元测试架构](docs/testing.md)。
|
||||
|
||||
### 静态运行
|
||||
|
||||
1. 使用 `nginx` 等Web服务器托管 `dist` 静态文件,nginx配置参考 `public/nginx.conf`。
|
||||
@@ -58,3 +58,12 @@ yarn build
|
||||
```shell
|
||||
node dist/service.js
|
||||
```
|
||||
|
||||
|
||||
### 模块联邦功能
|
||||
|
||||
MoviePilot 现已支持模块联邦(Module Federation)功能,允许插件开发者创建可动态加载的远程组件,实现更丰富的插件用户界面。
|
||||
|
||||
- [模块联邦开发指南](docs/module-federation-guide.md) - 如何开发远程组件插件
|
||||
- [模块联邦问题排查指南](docs/federation-troubleshooting.md) - 常见问题和解决方案
|
||||
- [插件远程组件示例](examples/plugin-component/) - 开发插件组件的完整示例项目
|
||||
|
||||
16
README_EN.md
@@ -11,15 +11,6 @@ Frontend project for [MoviePilot](https://github.com/jxxghp/MoviePilot), NodeJS
|
||||
- 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
|
||||
@@ -57,3 +48,10 @@ yarn build
|
||||
```shell
|
||||
node dist/service.js
|
||||
```
|
||||
|
||||
### Module Federation
|
||||
|
||||
MoviePilot now supports Module Federation, allowing plugin developers to create dynamically loadable remote components for richer plugin user interfaces.
|
||||
|
||||
- [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
|
||||
|
||||
@@ -16,13 +16,17 @@ MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态
|
||||
|
||||
## 3. 核心概念
|
||||
|
||||
每个插件需要提供三个标准组件:
|
||||
每个 Vue 联邦插件需要提供下列标准组件(`AppPage` 为可选,用于主界面侧栏全页入口):
|
||||
|
||||
| 组件名称 | 文件名 | 用途 |
|
||||
|---------|-------|------|
|
||||
| Page | Page.vue | 插件详情页面 |
|
||||
| Config | Config.vue | 插件配置页面 |
|
||||
| Dashboard | Dashboard.vue | 仪表板组件 |
|
||||
| 组件名称 | 暴露名 | 文件名 | 用途 |
|
||||
|---------|--------|--------|------|
|
||||
| Page | `./Page` | Page.vue | 插件管理中的详情弹窗 |
|
||||
| Config | `./Config` | Config.vue | 插件配置页面 |
|
||||
| Dashboard | `./Dashboard` | Dashboard.vue | 仪表盘小组件 |
|
||||
| AppPage | `./AppPage` | AppPage.vue | 主界面侧栏独立全页(主内容区由插件完全绘制) |
|
||||
| (可选) | `./AppPage{Xxx}` | 如 AppPageSettings.vue | 多 `nav_key` 时按名优先加载,见下文「多界面」 |
|
||||
|
||||
主应用在侧栏全页路由中按 `nav_key` 解析暴露名(如 `AppPageSettings`),再回退 `AppPage` → `Page`;`nav_key` 为 `main` 时仅尝试 `AppPage` → `Page`。
|
||||
|
||||
## 4. 快速开始
|
||||
|
||||
@@ -56,6 +60,8 @@ export default defineConfig({
|
||||
'./Page': './src/components/Page.vue',
|
||||
'./Config': './src/components/Config.vue',
|
||||
'./Dashboard': './src/components/Dashboard.vue',
|
||||
'./AppPage': './src/components/AppPage.vue',
|
||||
'./AppPageSettings': './src/components/AppPageSettings.vue',
|
||||
},
|
||||
shared: {
|
||||
vue: {
|
||||
@@ -245,17 +251,110 @@ const props = defineProps({
|
||||
|
||||
<template>
|
||||
<div class="dashboard-widget">
|
||||
<!-- 仪表板内容 -->
|
||||
<v-card>
|
||||
<v-card-title>{{ config.title || '仪表板组件' }}</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- 组件内容 -->
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<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>
|
||||
```
|
||||
|
||||
### 5.4 AppPage 组件(侧栏全页)
|
||||
|
||||
用于主应用左侧导航中的独立页面(路由 `#/plugin-app/:pluginId/:navKey?`),占据默认布局下的主内容区;与 `Page` 不同,不嵌在插件管理弹窗中。
|
||||
|
||||
主应用传入的 props:
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| `api` | 与 `Page` 相同,用于 `bear` 认证的插件 HTTP 调用 |
|
||||
| `navKey` | 与侧栏声明的 `nav_key` 一致,同一插件多入口时用于区分 |
|
||||
| `pluginId` | 当前插件 ID |
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
api: { type: Object, default: () => ({}) },
|
||||
navKey: { type: String, default: 'main' },
|
||||
pluginId: { type: String, default: '' },
|
||||
})
|
||||
const emit = defineEmits(['action'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pa-4">
|
||||
<div class="text-h6 mb-2">侧栏全页示例({{ pluginId }} / {{ navKey }})</div>
|
||||
<v-btn size="small" @click="emit('action')">通知主应用</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### 后端:注册侧栏入口
|
||||
|
||||
插件需为 **Vue** 渲染模式(`get_render_mode` 返回 `vue`),并实现 `get_sidebar_nav`,返回列表项字段与主应用 `GET /api/v1/plugin/sidebar_nav` 一致:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `nav_key` | URL 路径段,唯一标识本入口(同一插件可多入口) |
|
||||
| `title` | 侧栏显示标题 |
|
||||
| `icon` | MDI 图标名,如 `mdi-rss` |
|
||||
| `section` | 分组:`start` / `discovery` / `subscribe` / `organize` / `system` |
|
||||
| `permission` | 可选:`subscribe` / `discovery` / `search` / `manage` / `admin`,与主应用菜单权限一致 |
|
||||
| `order` | 可选:同组内排序,数值越小越靠前 |
|
||||
|
||||
```python
|
||||
def get_sidebar_nav(self) -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"nav_key": "main",
|
||||
"title": "示例订阅页",
|
||||
"icon": "mdi-rss",
|
||||
"section": "subscribe",
|
||||
"permission": "subscribe",
|
||||
"order": 10,
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 同一插件多个全页界面(多 `nav_key`)
|
||||
|
||||
在 `get_sidebar_nav` 中**返回多条**记录,每条使用不同的 `nav_key` / `title` / `section` 等,侧栏与「更多」中会出现多个入口,路由形如 `#/plugin-app/<插件ID>/<nav_key>`。
|
||||
|
||||
前端加载远程组件的顺序为:
|
||||
|
||||
| `nav_key` | 依次尝试的联邦暴露名 |
|
||||
|-----------|----------------------|
|
||||
| `main` 或省略 | `./AppPage` → `./Page` |
|
||||
| 其它(如 `settings`、`my_tool`) | `./AppPage{PascalCase}` → `./AppPage` → `./Page` |
|
||||
|
||||
`PascalCase` 规则:按 `-`、`_`、空格分段后首字母大写并拼接。例如 `nav_key=settings` → 先试 `./AppPageSettings`;`my_tool` → `./AppPageMyTool`。
|
||||
|
||||
**两种实现方式(二选一或混用):**
|
||||
|
||||
1. **单文件分支**:只暴露 `./AppPage`,在组件内根据 `navKey` prop 用 `v-if` / `<component>` 切换子界面。
|
||||
2. **多文件**:为某个入口单独暴露 `./AppPageSettings.vue` 等,主应用会优先加载对应模块,失败再回退到 `AppPage`。
|
||||
|
||||
`vite.config` 多暴露示例:
|
||||
|
||||
```typescript
|
||||
exposes: {
|
||||
'./AppPage': './src/components/AppPage.vue',
|
||||
'./AppPageSettings': './src/components/AppPageSettings.vue',
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 构建和部署
|
||||
|
||||
### 构建项目
|
||||
@@ -350,21 +449,16 @@ shared: {
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 开发环境测试
|
||||
### 8.3 本地监听构建
|
||||
|
||||
开发期间可以使用以下配置在本地测试:
|
||||
插件前端可使用 Vite 的监听构建模式:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 5001, // 使用不同于主应用的端口
|
||||
cors: true, // 启用CORS
|
||||
origin: 'http://localhost:5001'
|
||||
}
|
||||
})
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
将 `dev` 脚本配置为 `vite build --watch` 后,源码变化会自动重新构建。使用本地插件仓并启用 `DEV` 或 `PLUGIN_AUTO_RELOAD` 时,MoviePilot 会同步新的构建产物;刷新页面即可看到修改。
|
||||
|
||||
## 9. 示例代码
|
||||
|
||||
- [插件远程组件示例](../examples/plugin-component/) - 开发插件组件的完整示例项目
|
||||
|
||||
94
docs/testing.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 单元测试架构
|
||||
|
||||
MoviePilot-Frontend 使用 Vitest 运行单元测试和组件测试,使用 jsdom 提供 DOM 环境。测试代码参与 TypeScript 类型检查,但不作为生产构建入口。
|
||||
|
||||
## 测试类型
|
||||
|
||||
- 单元测试覆盖纯函数、store、composable、路由规则和独立模块的输入、输出及副作用。
|
||||
- 组件测试挂载 Vue 组件或页面,覆盖 props、emits、用户交互、可见 DOM、Router、Pinia、HTTP 请求和生命周期清理。
|
||||
- PWA、Service Worker、模块联邦远程入口、真实布局、拖拽和浏览器原生能力由真实浏览器验证,不由 jsdom 测试单独证明。
|
||||
|
||||
## 目录结构
|
||||
|
||||
业务 spec 与源码共置在对应责任域的 `__tests__/` 目录中,文件名与被测源码保持一致并使用 `*.spec.ts`:
|
||||
|
||||
```text
|
||||
src/
|
||||
├── pages/
|
||||
│ ├── recommend.vue
|
||||
│ └── __tests__/recommend.spec.ts
|
||||
├── stores/
|
||||
│ ├── auth.ts
|
||||
│ └── __tests__/auth.spec.ts
|
||||
├── utils/
|
||||
│ ├── permission.ts
|
||||
│ └── __tests__/permission.spec.ts
|
||||
└── views/dashboard/
|
||||
├── MediaRecommend.vue
|
||||
└── __tests__/MediaRecommend.spec.ts
|
||||
```
|
||||
|
||||
跨业务 spec 复用的测试设施位于 `tests/`:
|
||||
|
||||
```text
|
||||
tests/
|
||||
├── setup.ts
|
||||
└── support/
|
||||
├── render.ts
|
||||
├── factories/
|
||||
└── msw/
|
||||
├── server.ts
|
||||
└── handlers/
|
||||
```
|
||||
|
||||
- `tests/setup.ts` 注册 DOM matcher、MSW 生命周期、浏览器 API stub 和每例清理逻辑。
|
||||
- `tests/support/render.ts` 提供带 Vuetify、i18n、Router 和 Pinia 的标准渲染入口。
|
||||
- `tests/support/factories/` 按业务对象提供最小有效测试数据工厂。
|
||||
- `tests/support/msw/handlers/` 按业务域定义 HTTP handler;`server.ts` 只负责 MSW server 实例。
|
||||
- spec 通过 `@tests/*` 访问共享测试设施,通过 `@/*` 访问生产源码。
|
||||
|
||||
## 工具职责
|
||||
|
||||
- Vitest 提供 runner、断言、mock、fake timers 和覆盖率执行入口。
|
||||
- Vue Test Utils 用于 Vue 特有的 props、emits、slots 和局部组件控制。
|
||||
- Testing Library、jest-dom 和 user-event 用于按角色、可访问名称和用户操作验证可见行为。
|
||||
- MSW 在 HTTP 边界拦截真实 API 客户端请求。未声明请求会使测试失败,测试不得访问真实后端或外网。
|
||||
- `@pinia/testing` 用于依赖 store 的组件测试;store 自身使用真实 `createPinia()` 测试。
|
||||
|
||||
## 编写规范
|
||||
|
||||
- 一个 spec 对应一个主要源码文件;测试名称描述可观察行为或业务规则。
|
||||
- 组件测试断言可见 DOM、emits、路由、请求和持久化结果,不读取组件私有状态或私有方法。
|
||||
- 纯逻辑优先直接调用;依赖生命周期、provide 或 inject 的 composable 通过宿主组件挂载。
|
||||
- HTTP handler 和 factory 按业务域拆分,不建立包含所有接口或所有数据字段的全局万能 mock。
|
||||
- 只在与当前断言无关或无法由 jsdom 正确执行时 stub 子组件、浏览器能力或第三方重型组件。
|
||||
- 每个用例保持独立,不依赖文件执行顺序;timer、mock、storage、DOM 和未完成请求由全局 setup 恢复。
|
||||
- 不使用大面积快照或覆盖率占位用例。
|
||||
|
||||
## 新增测试
|
||||
|
||||
1. 在被测源码所在目录的 `__tests__/` 中创建同名 `*.spec.ts`。
|
||||
2. 纯函数、store 和无渲染模块直接使用 Vitest;Vue 组件使用标准渲染入口。
|
||||
3. 需要 HTTP 请求时,在 `tests/support/msw/handlers/<domain>.ts` 增加对应 handler。
|
||||
4. 需要结构化业务数据时,在 `tests/support/factories/` 增加最小工厂。
|
||||
5. 核心覆盖范围发生变化时,同步更新 `vite.config.ts` 的 `coverage.include`。
|
||||
6. 提交前运行测试、覆盖率、类型检查、lint 和生产构建。
|
||||
|
||||
## 配置边界
|
||||
|
||||
Vitest 只收集 `src/**/__tests__/**/*.spec.ts`。测试模式保留 Vue、Vue JSX、Vuetify、自动导入、自动组件和 i18n 插件,并禁用 PWA、模块联邦和 top-level-await 构建插件。
|
||||
|
||||
当前核心覆盖范围在 `vite.config.ts` 的 `coverage.include` 中显式维护。聚合门槛为 Lines、Statements、Functions 不低于 85%,Branches 不低于 80%;每个显式核心文件的 Lines、Statements、Functions 不低于 80%,Branches 不低于 75%。覆盖率报告写入 `coverage/`。
|
||||
|
||||
## 命令与 CI
|
||||
|
||||
```sh
|
||||
yarn test # watch 模式
|
||||
yarn test:run # 单次运行
|
||||
yarn test:coverage # 单次运行并检查覆盖率
|
||||
yarn typecheck
|
||||
yarn lint
|
||||
yarn build
|
||||
```
|
||||
|
||||
Pull Request 测试工作流使用 Node 24 LTS 和 frozen lockfile,依次执行类型检查和覆盖率门禁。现有 lint 基线问题按仓库当前维护约定单独处理,新增测试代码不得引入新的 lint 错误。
|
||||
5
env.d.ts
vendored
@@ -4,8 +4,13 @@ declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
action?: string
|
||||
subject?: string
|
||||
keepAlive?: boolean
|
||||
keepAliveKey?: string
|
||||
layoutWrapperClasses?: string
|
||||
navActiveLink?: RouteLocationRaw
|
||||
requiresAuth?: boolean
|
||||
subType?: string
|
||||
hideFooter?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MoviePilot 插件远程组件示例
|
||||
|
||||
这是 MoviePilot 插件远程组件的示例项目,展示了如何正确配置和开发与主应用兼容的远程组件。本示例实现了三个标准组件:Page(详情页面)、Config(配置页面)和Dashboard(仪表板组件)。
|
||||
这是 MoviePilot 插件远程组件的示例项目,展示了如何正确配置和开发与主应用兼容的远程组件。本示例包含 Page、Config、Dashboard、AppPage,以及可选的 `AppPageSettings`(`nav_key=settings` 时由主应用优先加载,用于演示「一插件多全页界面」)。
|
||||
|
||||
## 1. 开发环境准备
|
||||
|
||||
@@ -28,7 +28,9 @@ plugin-component/
|
||||
│ ├── components/
|
||||
│ │ ├── Page.vue # 插件详情页面组件
|
||||
│ │ ├── Config.vue # 插件配置页面组件
|
||||
│ │ └── Dashboard.vue # 插件仪表板组件
|
||||
│ │ ├── Dashboard.vue # 插件仪表板组件
|
||||
│ │ ├── AppPage.vue # 侧栏全页(主内容区,nav_key=main)
|
||||
│ │ └── AppPageSettings.vue # 可选第二全页(nav_key=settings)
|
||||
│ ├── App.vue # 本地开发入口组件
|
||||
│ └── main.js # 本地开发入口文件
|
||||
├── vite.config.js # Vite和模块联邦配置
|
||||
|
||||
32
examples/plugin-component/src/components/AppPage.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 侧栏全页:在主应用 #/plugin-app/:pluginId/:navKey 中渲染,占据主内容区。
|
||||
* 需在插件后端实现 get_sidebar_nav 才会出现在侧栏。
|
||||
*/
|
||||
const props = defineProps({
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
navKey: {
|
||||
type: String,
|
||||
default: 'main',
|
||||
},
|
||||
pluginId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['action'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plugin-app-page pa-4">
|
||||
<div class="text-h6 mb-2">AppPage(侧栏全页)</div>
|
||||
<div class="text-body-2 text-medium-emphasis mb-4">
|
||||
pluginId: {{ pluginId }} · navKey: {{ navKey }}
|
||||
</div>
|
||||
<v-btn size="small" variant="tonal" @click="emit('action')">action</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
17
examples/plugin-component/src/components/AppPageSettings.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 示例:nav_key=settings 时主应用会优先加载 AppPageSettings,再回退 AppPage。
|
||||
*/
|
||||
const props = defineProps({
|
||||
api: { type: Object, default: () => ({}) },
|
||||
navKey: { type: String, default: 'settings' },
|
||||
pluginId: { type: String, default: '' },
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pa-4">
|
||||
<div class="text-subtitle-1">Settings 子界面(AppPageSettings)</div>
|
||||
<div class="text-caption text-medium-emphasis">navKey={{ navKey }} · pluginId={{ pluginId }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,6 +12,8 @@ export default defineConfig({
|
||||
'./Page': './src/components/Page.vue',
|
||||
'./Config': './src/components/Config.vue',
|
||||
'./Dashboard': './src/components/Dashboard.vue',
|
||||
'./AppPage': './src/components/AppPage.vue',
|
||||
'./AppPageSettings': './src/components/AppPageSettings.vue',
|
||||
},
|
||||
shared: {
|
||||
vue: {
|
||||
|
||||
1008
index.html
48
package.json
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.5.7-1",
|
||||
"version": "2.14.5",
|
||||
"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",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"lint": "eslint . -c .eslintrc.js --fix --ext .ts,.js,.vue,.tsx,.jsx",
|
||||
"build:icons": "tsc -b src/@iconify && node src/@iconify/build-icons.js",
|
||||
@@ -19,6 +23,10 @@
|
||||
"dist/**/*"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"vitest/**/vite": "5.4.18",
|
||||
"vitest/vite": "5.4.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.15",
|
||||
"@fullcalendar/daygrid": "^6.1.15",
|
||||
@@ -27,6 +35,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,23 +49,31 @@
|
||||
"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",
|
||||
"gridstack": "^12.6.0",
|
||||
"gsap": "^3.15.0",
|
||||
"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": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"sass": "^1.83.4",
|
||||
"tailwindcss": "^ 3.4.17",
|
||||
"three": "^0.185.1",
|
||||
"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",
|
||||
@@ -65,21 +82,38 @@
|
||||
"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-json/tabler": "^1.2.23",
|
||||
"@iconify/tools": "^4.0.4",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||
"@originjs/vite-plugin-federation": "^1.4.1",
|
||||
"@pinia/testing": "1.0.3",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@testing-library/dom": "9.3.4",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@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/three": "^0.185.1",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
||||
"@typescript-eslint/parser": "^8.20.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vitest/coverage-v8": "3.2.7",
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/test-utils": "2.4.11",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
@@ -89,6 +123,8 @@
|
||||
"eslint-plugin-sonarjs": "^3.0.1",
|
||||
"eslint-plugin-unicorn": "^56.0.1",
|
||||
"eslint-plugin-vue": "^9.12.0",
|
||||
"jsdom": "26.1.0",
|
||||
"msw": "2.15.0",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-html": "^1.5.0",
|
||||
"stylelint": "^16.13.2",
|
||||
@@ -104,12 +140,14 @@
|
||||
"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",
|
||||
"vitest": "3.2.7",
|
||||
"vue-shepherd": "^4.1.0",
|
||||
"vue-tsc": "^2.0.10",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.18"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 102 KiB |
|
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 |
@@ -35,6 +35,23 @@ http {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = /service-worker.js {
|
||||
# Service Worker 必须保持稳定 URL 并每次重新验证,避免前端更新后继续注册旧版本。
|
||||
expires off;
|
||||
add_header Cache-Control "no-cache, must-revalidate";
|
||||
root html;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /manifest.webmanifest {
|
||||
# Web App Manifest 参与 PWA 安装与资源发现,不能跟普通静态资源一起长缓存。
|
||||
expires off;
|
||||
default_type application/manifest+json;
|
||||
add_header Cache-Control "no-cache, must-revalidate";
|
||||
root html;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
# 静态资源
|
||||
expires 1y;
|
||||
@@ -44,12 +61,11 @@ http {
|
||||
|
||||
location /assets {
|
||||
# 静态资源
|
||||
expires 1y;
|
||||
add_header Cache-Control "public";
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
root html;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/system/(message|progress/) {
|
||||
location ~ ^/api/v1/(system/(message|progress/|logging)|search/.*/stream$) {
|
||||
# SSE MIME类型设置
|
||||
default_type text/event-stream;
|
||||
|
||||
|
||||
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: #8D51F9;
|
||||
--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(141, 81, 249, 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(141, 81, 249, 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>
|
||||
12
scripts/check-season-label.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { formatSeasonLabel } from '../src/@core/utils/season.ts'
|
||||
|
||||
assert.equal(formatSeasonLabel(0, '特别篇'), '特别篇')
|
||||
assert.equal(formatSeasonLabel('0', 'Specials'), 'Specials')
|
||||
assert.equal(formatSeasonLabel(1, '特别篇'), 'S01')
|
||||
assert.equal(formatSeasonLabel('12', '特别篇'), 'S12')
|
||||
assert.equal(formatSeasonLabel(null, '特别篇'), '')
|
||||
assert.equal(formatSeasonLabel(undefined, '特别篇'), '')
|
||||
|
||||
console.log('season label checks passed')
|
||||
1
shims.d.ts
vendored
@@ -12,3 +12,4 @@ declare module 'vue-prism-component' {
|
||||
export default component
|
||||
}
|
||||
declare module 'vue-shepherd';
|
||||
declare module 'colorthief';
|
||||
|
||||
@@ -15,7 +15,7 @@ function onClick() {
|
||||
|
||||
<template>
|
||||
<IconBtn
|
||||
:class="props.innerClass ? props.innerClass : 'absolute right-3 top-3'"
|
||||
:class="props.innerClass ? props.innerClass : 'absolute right-3 top-3 z-10'"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<VIcon icon="mdi-close" />
|
||||
|
||||
@@ -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} {
|
||||
border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
}
|
||||
// #{$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 .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;
|
||||
}
|
||||
@@ -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,59 @@
|
||||
|
||||
// ℹ️ 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%);
|
||||
isolation: isolate;
|
||||
|
||||
@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;
|
||||
|
||||
|
||||
@@ -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,91 @@
|
||||
@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";
|
||||
// 图表仅保留纵向页面滚动,禁止双指缩放等触摸手势。
|
||||
touch-action: pan-y;
|
||||
|
||||
// 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 +93,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: var(--app-surface-shadow);
|
||||
}
|
||||
|
||||
// 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: var(--app-surface-shadow);
|
||||
}
|
||||
|
||||
// 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,72 +0,0 @@
|
||||
%blurry-bg {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
|
||||
.v-theme--light & {
|
||||
backdrop-filter: blur(16px);
|
||||
background: rgba(var(--v-theme-surface), 0.9);
|
||||
box-shadow: 0 0 8px 0 rgba(var(--v-theme-on-surface), 0.1);
|
||||
}
|
||||
|
||||
&::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: all 0.2s ease-in-out;
|
||||
|
||||
|
||||
.v-theme--dark & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 1) 0%,
|
||||
rgba(var(--v-theme-background), 0.8) 20%,
|
||||
rgba(var(--v-theme-background), 0.6) 40%,
|
||||
rgba(var(--v-theme-background), 0.4) 60%,
|
||||
rgba(var(--v-theme-background), 0.2) 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.8) 20%,
|
||||
rgba(var(--v-theme-background), 0.6) 40%,
|
||||
rgba(var(--v-theme-background), 0.4) 60%,
|
||||
rgba(var(--v-theme-background), 0.2) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--transparent & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 0.5) 0%,
|
||||
rgba(var(--v-theme-background), 0.4) 20%,
|
||||
rgba(var(--v-theme-background), 0.3) 40%,
|
||||
rgba(var(--v-theme-background), 0.2) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
|
||||
@media (width <= 640px) {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 0.9) 0%,
|
||||
rgba(var(--v-theme-background), 0.7) 20%,
|
||||
rgba(var(--v-theme-background), 0.5) 40%,
|
||||
rgba(var(--v-theme-background), 0.3) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 0.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
|
||||
@@ -59,6 +66,43 @@ export const prefixWithPlus = (value: number) => (value > 0 ? `+${value}` : valu
|
||||
// 格式化为Sxx
|
||||
export const formatSeason = (value: string) => (value ? `S${value.padStart(2, '0')}` : '')
|
||||
|
||||
/**
|
||||
* 格式化为 SxxExx 季集标识,多个连续集会合并为范围。
|
||||
*/
|
||||
export function formatSeasonEpisode(
|
||||
season: number | string | null | undefined,
|
||||
episodeNumbers: number[],
|
||||
): string {
|
||||
const seasonText = season === null || season === undefined || season === '' ? '' : formatSeason(String(season))
|
||||
const normalizedNumbers = [...new Set(episodeNumbers)]
|
||||
.map(number => Number(number))
|
||||
.filter(number => Number.isFinite(number) && number > 0)
|
||||
.sort((first, second) => first - second)
|
||||
|
||||
if (!normalizedNumbers.length) return seasonText
|
||||
|
||||
const formatEpisode = (number: number) => `E${String(number).padStart(2, '0')}`
|
||||
const ranges: string[] = []
|
||||
let start = normalizedNumbers[0]
|
||||
let end = normalizedNumbers[0]
|
||||
|
||||
for (let index = 1; index < normalizedNumbers.length; index++) {
|
||||
const currentNumber = normalizedNumbers[index]
|
||||
|
||||
if (currentNumber === end + 1) {
|
||||
end = currentNumber
|
||||
} else {
|
||||
ranges.push(start === end ? formatEpisode(start) : `${formatEpisode(start)}-${formatEpisode(end)}`)
|
||||
start = currentNumber
|
||||
end = currentNumber
|
||||
}
|
||||
}
|
||||
|
||||
ranges.push(start === end ? formatEpisode(start) : `${formatEpisode(start)}-${formatEpisode(end)}`)
|
||||
|
||||
return `${seasonText}${ranges.join('、')}`
|
||||
}
|
||||
|
||||
// 格式化为xx[TGMK]B
|
||||
export function formatFileSize(bytes: number, decimals = 2, prefix = false) {
|
||||
// 负数标记
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import ColorThief from 'colorthief'
|
||||
|
||||
const DEFAULT_DOMINANT_COLOR = '#28A9E1'
|
||||
const DOMINANT_COLOR_CACHE_LIMIT = 100
|
||||
const colorThief = new ColorThief()
|
||||
const dominantColorCache = new Map<string, Promise<string>>()
|
||||
|
||||
interface DominantColorOptions {
|
||||
fallback?: string
|
||||
quality?: number
|
||||
}
|
||||
|
||||
// 将 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
|
||||
|
||||
@@ -15,9 +24,68 @@ function rgbStringToHex(rgbArray: number[]): string {
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
// 提取主要颜色
|
||||
export async function getDominantColor(image: HTMLImageElement): Promise<string> {
|
||||
const colorThief = new ColorThief()
|
||||
const dominantColor = colorThief.getColor(image)
|
||||
return rgbStringToHex(dominantColor)
|
||||
function getImageCacheKey(image: HTMLImageElement) {
|
||||
return image.currentSrc || image.src || ''
|
||||
}
|
||||
|
||||
function rememberDominantColor(key: string, colorPromise: Promise<string>) {
|
||||
if (!key) return colorPromise
|
||||
|
||||
if (dominantColorCache.size >= DOMINANT_COLOR_CACHE_LIMIT) {
|
||||
const firstKey = dominantColorCache.keys().next().value
|
||||
if (firstKey) dominantColorCache.delete(firstKey)
|
||||
}
|
||||
|
||||
dominantColorCache.set(key, colorPromise)
|
||||
return colorPromise
|
||||
}
|
||||
|
||||
// 提取主要颜色
|
||||
export async function getDominantColor(
|
||||
image: HTMLImageElement | undefined | null,
|
||||
options: DominantColorOptions = {},
|
||||
): Promise<string> {
|
||||
const fallback = options.fallback ?? DEFAULT_DOMINANT_COLOR
|
||||
|
||||
if (!image) return fallback
|
||||
|
||||
const cacheKey = getImageCacheKey(image)
|
||||
const cachedColor = cacheKey ? dominantColorCache.get(cacheKey) : undefined
|
||||
if (cachedColor) return cachedColor
|
||||
|
||||
const colorPromise = Promise.resolve()
|
||||
.then(() => {
|
||||
const dominantColor = colorThief.getColor(image, options.quality ?? 20)
|
||||
return rgbStringToHex(dominantColor)
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('Failed to extract dominant color:', error)
|
||||
return fallback
|
||||
})
|
||||
|
||||
return rememberDominantColor(cacheKey, colorPromise)
|
||||
}
|
||||
|
||||
// 预加载图片
|
||||
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)
|
||||
}
|
||||
|
||||
15
src/@core/utils/season.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 格式化用户可见的季标签。
|
||||
*
|
||||
* TMDB 使用季号 0 表示特别季;调用方传入当前语言的特别季名称,
|
||||
* 其余季号保持 MoviePilot 现有的 Sxx 展示口径。
|
||||
*/
|
||||
export function formatSeasonLabel(
|
||||
season: number | string | null | undefined,
|
||||
specialsLabel: string,
|
||||
): string {
|
||||
if (season === null || season === undefined || season === '') return ''
|
||||
if (Number(season) === 0) return specialsLabel
|
||||
|
||||
return `S${String(season).padStart(2, '0')}`
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { createRequire } from 'node:module'
|
||||
|
||||
// Get current directory
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const projectSrcDir = join(__dirname, '..')
|
||||
|
||||
// Create require function for importing JSON files in ESM
|
||||
const require = createRequire(import.meta.url)
|
||||
@@ -86,33 +87,12 @@ const sources: BundleScriptConfig = {
|
||||
],
|
||||
|
||||
icons: [
|
||||
// 'mdi:home',
|
||||
// 'mdi:account',
|
||||
// 'mdi:login',
|
||||
// 'mdi:logout',
|
||||
// 'octicon:book-24',
|
||||
// 'octicon:code-square-24',
|
||||
'lucide:sparkles',
|
||||
'material-symbols:passkey',
|
||||
'line-md:loading-twotone-loop',
|
||||
],
|
||||
|
||||
json: [
|
||||
// Custom JSON file
|
||||
// 'json/gg.json',
|
||||
|
||||
// Iconify JSON file (@iconify/json is a package name, /json/ is directory where files are, then filename)
|
||||
require.resolve('@iconify-json/mdi/icons.json'),
|
||||
|
||||
// Custom file with only few icons
|
||||
// {
|
||||
// filename: require.resolve('@iconify-json/line-md/icons.json'),
|
||||
// icons: [
|
||||
// 'home-twotone-alt',
|
||||
// 'github',
|
||||
// 'document-list',
|
||||
// 'document-code',
|
||||
// 'image-twotone',
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
json: [],
|
||||
}
|
||||
|
||||
// Iconify component (this changes import statement in generated file)
|
||||
@@ -130,6 +110,15 @@ const target = join(__dirname, 'icons-bundle.js');
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
(async function () {
|
||||
const scannedIcons = await collectUsedIcons(projectSrcDir)
|
||||
|
||||
if (sources.icons) {
|
||||
sources.icons.push(...scannedIcons)
|
||||
sources.icons = Array.from(new Set(sources.icons)).sort()
|
||||
} else {
|
||||
sources.icons = scannedIcons
|
||||
}
|
||||
|
||||
let bundle = commonJS
|
||||
? `const { addCollection } = require('${component}');\n\n`
|
||||
: `import { addCollection } from '${component}';\n\n`
|
||||
@@ -154,7 +143,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,
|
||||
@@ -269,8 +264,60 @@ const target = join(__dirname, 'icons-bundle.js');
|
||||
console.log(`Saved ${target} (${bundle.length} bytes)`)
|
||||
})().catch((err) => {
|
||||
console.error(err)
|
||||
// 构建图标失败时必须终止构建,避免继续发布上一次遗留的超大 icons-bundle。
|
||||
process.exitCode = 1
|
||||
})
|
||||
|
||||
async function collectUsedIcons(rootDir: string): Promise<string[]> {
|
||||
const icons = new Set<string>()
|
||||
const files = await walkDirectory(rootDir)
|
||||
const sourceFiles = files.filter(file => /\.(vue|ts|js|tsx|jsx)$/.test(file))
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
if (file.includes('/@iconify/')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const content = await fs.readFile(file, 'utf8')
|
||||
|
||||
for (const match of content.matchAll(/\b(lucide|material-symbols|line-md|tabler):([a-z0-9-]+)\b/g)) {
|
||||
icons.add(`${match[1]}:${match[2]}`)
|
||||
}
|
||||
|
||||
for (const match of content.matchAll(/\bmdi:([a-z0-9-]+)\b/g)) {
|
||||
icons.add(`mdi:${match[1]}`)
|
||||
}
|
||||
|
||||
for (const match of content.matchAll(/\btabler-([a-z0-9-]+)\b/g)) {
|
||||
icons.add(`tabler:${match[1]}`)
|
||||
}
|
||||
|
||||
for (const match of content.matchAll(/\bmdi-([a-z0-9-]+)\b/g)) {
|
||||
icons.add(`mdi:${match[1]}`)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(icons).sort()
|
||||
}
|
||||
|
||||
async function walkDirectory(dir: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const files: string[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await walkDirectory(fullPath)))
|
||||
continue
|
||||
}
|
||||
|
||||
files.push(fullPath)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove metadata from icon set
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Component } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import logo from '@images/logo.svg?raw'
|
||||
import ThemeLogoMark from '@/components/misc/ThemeLogoMark.vue'
|
||||
|
||||
interface Props {
|
||||
tag?: string | Component
|
||||
@@ -51,7 +51,7 @@ function handleNavScroll(evt: Event) {
|
||||
<div class="nav-header">
|
||||
<slot name="nav-header">
|
||||
<RouterLink to="/" class="app-logo d-flex align-center app-title-wrapper">
|
||||
<div class="d-flex" v-html="logo" />
|
||||
<ThemeLogoMark />
|
||||
|
||||
<h1 class="font-weight-bold leading-normal text-xl">
|
||||
MOVIEPILOT <span class="text-sm text-gray-500">v2</span>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Transition } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import VerticalNav from '@layouts/components/VerticalNav.vue'
|
||||
import {
|
||||
readThemeCustomizerSettings,
|
||||
THEME_CUSTOMIZER_CHANGE_EVENT,
|
||||
type ThemeCustomizerSettings,
|
||||
} from '@/composables/useThemeCustomizer'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
|
||||
export default defineComponent({
|
||||
setup(props, { slots }) {
|
||||
@@ -11,19 +16,62 @@ export default defineComponent({
|
||||
|
||||
const route = useRoute()
|
||||
const { mdAndDown } = useDisplay()
|
||||
const { appMode } = usePWA()
|
||||
const themeLayout = ref(readThemeCustomizerSettings().layout)
|
||||
const canUseDesktopLayout = computed(() => !mdAndDown.value && !appMode.value)
|
||||
const isCollapsedLayout = computed(() => canUseDesktopLayout.value && themeLayout.value === 'collapsed')
|
||||
const isHorizontalLayout = computed(() => canUseDesktopLayout.value && themeLayout.value === 'horizontal')
|
||||
|
||||
// ℹ️ 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)
|
||||
const isDialogOpen = ref(false)
|
||||
const wasScrolledBeforeDialog = ref(false)
|
||||
let dialogObserver: MutationObserver | null = null
|
||||
|
||||
const handleScroll = () => {
|
||||
scrollDistance.value = window.scrollY
|
||||
}
|
||||
|
||||
const handleThemeCustomizerChange = (event: Event) => {
|
||||
themeLayout.value = (event as CustomEvent<ThemeCustomizerSettings>).detail.layout
|
||||
}
|
||||
|
||||
// 监听弹窗状态变化
|
||||
const checkDialogState = () => {
|
||||
const wasDialogOpen = isDialogOpen.value
|
||||
isDialogOpen.value = document.documentElement.classList.contains('v-overlay-scroll-blocked')
|
||||
|
||||
// 当弹窗刚打开时,记录当前的滚动状态
|
||||
if (!wasDialogOpen && isDialogOpen.value) {
|
||||
wasScrolledBeforeDialog.value = scrollDistance.value > 10
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', () => {
|
||||
scrollDistance.value = window.scrollY
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
window.addEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
|
||||
|
||||
// 初始检查弹窗状态
|
||||
checkDialogState()
|
||||
|
||||
// 监听 DOM 变化以检测弹窗状态
|
||||
dialogObserver = new MutationObserver(checkDialogState)
|
||||
dialogObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
window.removeEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
|
||||
dialogObserver?.disconnect()
|
||||
dialogObserver = null
|
||||
})
|
||||
|
||||
return () => {
|
||||
// 👉 Vertical nav
|
||||
const verticalNav = h(
|
||||
@@ -38,26 +86,35 @@ 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',
|
||||
{ class: 'layout-page-content' },
|
||||
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true }, () =>
|
||||
h('section', { class: 'page-content-container' }, slots.default?.()),
|
||||
),
|
||||
h('section', { class: 'page-content-container' }, slots.default?.()),
|
||||
)
|
||||
|
||||
// 👉 根据路由 meta 决定 footer 高度
|
||||
const shouldShowFooter = !route.meta.hideFooter
|
||||
const isNavbarScrolled = scrollDistance.value > 5 || (isDialogOpen.value && wasScrolledBeforeDialog.value)
|
||||
|
||||
// 👉 Footer
|
||||
const footer = h('footer', { class: 'layout-footer' }, [
|
||||
@@ -85,8 +142,11 @@ export default defineComponent({
|
||||
'layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid',
|
||||
'layout-navbar-fixed',
|
||||
mdAndDown.value && 'layout-overlay-nav',
|
||||
isCollapsedLayout.value && 'layout-vertical-nav-collapsed',
|
||||
isHorizontalLayout.value && 'layout-horizontal-nav-active',
|
||||
isHorizontalLayout.value && isNavbarScrolled && 'layout-horizontal-nav-scrolled',
|
||||
route.meta.layoutWrapperClasses,
|
||||
scrollDistance.value && 'window-scrolled',
|
||||
!isHorizontalLayout.value && isNavbarScrolled && 'window-scrolled',
|
||||
],
|
||||
},
|
||||
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
|
||||
@@ -97,6 +157,8 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
|
||||
@use '@configured-variables' as variables;
|
||||
@use '@layouts/styles/placeholders';
|
||||
@use '@layouts/styles/mixins';
|
||||
@@ -108,8 +170,12 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
--layout-navbar-block-size: calc(
|
||||
env(safe-area-inset-top, 0px) + #{variables.$layout-vertical-nav-navbar-height} + var(--navbar-tab-height)
|
||||
);
|
||||
|
||||
// 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;
|
||||
@@ -123,11 +189,16 @@ export default defineComponent({
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
// iOS Safari 在地址栏收起和惯性滚动时可能把 fixed 顶栏和页面滚动层合成到一起,
|
||||
// 单独提升顶栏图层可避免导航栏短暂上移到安全区下方。
|
||||
backface-visibility: hidden;
|
||||
block-size: var(--layout-navbar-block-size);
|
||||
inline-size: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
|
||||
inset-block-start: 0;
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
||||
.navbar-content-container {
|
||||
block-size: calc(env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height);
|
||||
block-size: var(--layout-navbar-block-size);
|
||||
}
|
||||
|
||||
@at-root {
|
||||
@@ -135,10 +206,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +250,202 @@ export default defineComponent({
|
||||
// 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;
|
||||
|
||||
.page-content-container > div:first-child {
|
||||
inline-size: calc(100vw - variables.$layout-vertical-nav-collapsed-width - 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-vertical-nav-collapsed .layout-navbar {
|
||||
inline-size: calc(100vw - variables.$layout-vertical-nav-collapsed-width - 0.5rem);
|
||||
}
|
||||
|
||||
&.layout-vertical-nav-collapsed .layout-vertical-nav:not(.overlay-nav) {
|
||||
.nav-header {
|
||||
justify-content: center;
|
||||
margin-inline: 0;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
justify-content: center;
|
||||
inline-size: 100%;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.app-logo .theme-logo-mark {
|
||||
block-size: 2.5rem;
|
||||
inline-size: 2.5rem;
|
||||
}
|
||||
|
||||
.app-logo h1,
|
||||
.nav-item-title,
|
||||
.nav-section-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-link > a {
|
||||
justify-content: center;
|
||||
border-radius: 0.75rem !important;
|
||||
block-size: 2.75rem;
|
||||
margin-inline: 0.75rem;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.nav-item-icon {
|
||||
margin-inline-end: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-horizontal-nav-active {
|
||||
.layout-vertical-nav:not(.overlay-nav) {
|
||||
pointer-events: none;
|
||||
transform: translateX(-100%);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.layout-content-wrapper {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
background: rgb(var(--v-theme-background));
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
inline-size: 100%;
|
||||
max-inline-size: none;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.navbar-content-container {
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
inline-size: 100%;
|
||||
margin-inline: auto;
|
||||
max-inline-size: variables.$layout-boxed-content-width;
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
|
||||
.layout-page-content {
|
||||
inline-size: 100%;
|
||||
margin-inline: auto;
|
||||
max-inline-size: variables.$layout-boxed-content-width;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.page-content-container > div:first-child {
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@at-root {
|
||||
.layout-wrapper.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed .layout-navbar {
|
||||
backdrop-filter: blur(12px) saturate(1.2);
|
||||
background: rgb(var(--v-theme-surface)) !important;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 4%),
|
||||
0 1px 2px rgba(0, 0, 0, 2%);
|
||||
}
|
||||
|
||||
.layout-wrapper.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed
|
||||
.navbar-content-container {
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
padding-inline: 1.5rem !important;
|
||||
|
||||
&::before {
|
||||
display: none !important;
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
content: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='transparent'] .layout-wrapper.layout-horizontal-nav-active .layout-navbar,
|
||||
.v-theme--transparent .layout-wrapper.layout-horizontal-nav-active .layout-navbar {
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
border-block-end-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
html[data-theme='transparent'] .layout-wrapper.layout-horizontal-nav-active .navbar-content-container,
|
||||
.v-theme--transparent .layout-wrapper.layout-horizontal-nav-active .navbar-content-container {
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
// 透明主题的水平导航不叠加滚动磨砂层,避免中间区域出现一块更深的背景。
|
||||
html[data-theme='transparent']
|
||||
.layout-wrapper.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed
|
||||
.layout-navbar,
|
||||
.v-theme--transparent
|
||||
.layout-wrapper.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed
|
||||
.layout-navbar {
|
||||
backdrop-filter: blur(var(--transparent-blur-light, 6px)) !important;
|
||||
background: rgba(var(--v-theme-surface), var(--transparent-opacity-light, 0.2)) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
// 透明主题滚动时只让外层导航栏承载整屏背景,避免内部最大宽度容器单独变深。
|
||||
html[data-theme='transparent']
|
||||
.layout-wrapper.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed
|
||||
.navbar-content-container,
|
||||
.v-theme--transparent
|
||||
.layout-wrapper.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed
|
||||
.navbar-content-container {
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
padding-inline: 1.5rem !important;
|
||||
|
||||
&::before {
|
||||
display: none !important;
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
content: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='light'][data-theme-semi-dark-menu='true'][data-theme-layout='vertical']
|
||||
.layout-wrapper.layout-nav-type-vertical:not(.layout-horizontal-nav-active)
|
||||
.layout-vertical-nav:not(.overlay-nav),
|
||||
html[data-theme='light'][data-theme-semi-dark-menu='true'][data-theme-layout='collapsed']
|
||||
.layout-wrapper.layout-nav-type-vertical:not(.layout-horizontal-nav-active)
|
||||
.layout-vertical-nav:not(.overlay-nav) {
|
||||
background: #2f3349;
|
||||
color: #e7e3fc;
|
||||
|
||||
.app-logo h1,
|
||||
.nav-section-title,
|
||||
.nav-link > a,
|
||||
.nav-item-icon {
|
||||
color: rgba(231, 227, 252, 78%) !important;
|
||||
}
|
||||
|
||||
.nav-link > a:hover {
|
||||
background-color: rgba(231, 227, 252, 6%);
|
||||
}
|
||||
|
||||
.nav-link > .router-link-exact-active {
|
||||
color: #fff !important;
|
||||
|
||||
.nav-item-icon,
|
||||
.nav-item-title {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Content height fixed
|
||||
@@ -193,7 +456,7 @@ export default defineComponent({
|
||||
|
||||
.layout-page-content {
|
||||
// display: flex;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
|
||||
.page-content-container {
|
||||
inline-size: 100%;
|
||||
|
||||
@@ -6,15 +6,16 @@
|
||||
|
||||
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 {
|
||||
overflow: visible !important;
|
||||
background: rgb(var(--v-theme-background));
|
||||
overscroll-behavior-y: contain;
|
||||
|
||||
--webkit-overflow-scrolling: touch;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body,
|
||||
@@ -35,13 +36,13 @@ body,
|
||||
.layout-page-content {
|
||||
@include mixins.boxed-content(true);
|
||||
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
overflow: clip visible;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
17
src/@layouts/types.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
import type { Component, Ref, VNode } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import type { UserPermissionFeatureKey, UserPermissionKey } from '@/utils/permission'
|
||||
import { ContentWidth, FooterType, NavbarType } from './enums'
|
||||
|
||||
export interface UserConfig {
|
||||
@@ -112,13 +113,27 @@ export interface NavLinkProps {
|
||||
rel?: ATagRelAttrValues
|
||||
}
|
||||
|
||||
export type NavMenuIconColor = 'primary' | 'info' | 'success' | 'warning' | 'secondary'
|
||||
|
||||
export interface NavLink extends NavLinkProps, Partial<AclProperties> {
|
||||
title: string
|
||||
full_title?: string
|
||||
icon?: unknown
|
||||
iconColor?: NavMenuIconColor
|
||||
badgeContent?: string
|
||||
badgeClass?: string
|
||||
disable?: boolean
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
}
|
||||
|
||||
export interface NavMenuTabItem {
|
||||
title: string
|
||||
icon?: string
|
||||
tab: string
|
||||
description?: string
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
}
|
||||
|
||||
export interface NavMenu extends NavLink {
|
||||
@@ -126,6 +141,8 @@ export interface NavMenu extends NavLink {
|
||||
description?: string
|
||||
admin?: boolean
|
||||
footer?: boolean
|
||||
// 水平三级菜单和页面动态标签页共用的静态标签定义。
|
||||
tabs?: NavMenuTabItem[]
|
||||
}
|
||||
|
||||
// 👉 Vertical nav group
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ValidationRule } from 'vuetify/types/services/validation'
|
||||
type ValidationRule = (value: any) => string | boolean
|
||||
|
||||
// 必输校验
|
||||
export const requiredValidator: ValidationRule = (value: any) => {
|
||||
|
||||
771
src/App.vue
@@ -1,29 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
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 api, { type ConnectionAwareRequestConfig } from '@/api'
|
||||
import { useAuthStore, useGlobalSettingsStore } from '@/stores'
|
||||
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
|
||||
import { SupportedLocale } from '@/types/i18n'
|
||||
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/pwa/PWAInstallPrompt.vue'
|
||||
import SharedDialogHost from '@/components/dialog/SharedDialogHost.vue'
|
||||
import { applyStoredThemeCustomizerAppearance } from '@/composables/useThemeCustomizer'
|
||||
import {
|
||||
applyStoredTransparencySettings,
|
||||
TRANSPARENCY_SETTINGS_CHANGED_EVENT,
|
||||
type TransparencyGlassQuality,
|
||||
type TransparencySettings,
|
||||
} from '@/composables/useTransparencySettings'
|
||||
import { completeLaunchLoading } from '@/composables/useLaunchLoading'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { themeManager } from '@/utils/themeManager'
|
||||
import { applyDocumentThemeChrome, resolveThemeName } from '@/utils/themePalette'
|
||||
import { configureApexChartsTheme } from '@/utils/apexCharts'
|
||||
import {
|
||||
useGlobalOfflineStatus,
|
||||
type ConnectionFailureReason,
|
||||
} from '@/composables/useOfflineStatus'
|
||||
|
||||
const LOGIN_WALLPAPER_ROUTE = '/login'
|
||||
const BACKGROUND_CROSSFADE_DURATION_MS = 1500
|
||||
const WINDOW_BLUR_RENDER_THROTTLE_DELAY_MS = 180_000
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
let themeValue = localStorage.getItem('theme') || 'light'
|
||||
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
const vuetifyTheme = useTheme()
|
||||
const { global: globalTheme } = vuetifyTheme
|
||||
let themeValue = localStorage.getItem('theme') || 'auto'
|
||||
globalTheme.name.value = resolveThemeName(themeValue)
|
||||
applyStoredThemeCustomizerAppearance(vuetifyTheme)
|
||||
|
||||
// 启动屏和 iOS safe area 在同一层显示,根节点底色需要尽早和当前主题保持一致。
|
||||
function syncRootLaunchPalette() {
|
||||
const { background, primary } = globalTheme.current.value.colors
|
||||
|
||||
applyDocumentThemeChrome(themeValue, {
|
||||
background,
|
||||
persistLoaderColors: true,
|
||||
primary,
|
||||
resolvedTheme: globalTheme.name.value,
|
||||
})
|
||||
}
|
||||
|
||||
// 生效语言
|
||||
const localeValue = getBrowserLocale()
|
||||
setI18nLanguage(localeValue as SupportedLocale)
|
||||
|
||||
// 显示状态
|
||||
const show = ref(false)
|
||||
|
||||
// 检查是否登录
|
||||
const authStore = useAuthStore()
|
||||
const isLogin = computed(() => authStore.token)
|
||||
const route = useRoute()
|
||||
const { initializePWA } = usePWA()
|
||||
const offlineStatus = useGlobalOfflineStatus()
|
||||
|
||||
// 全局设置store
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
|
||||
// 生成背景图片key
|
||||
const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'))
|
||||
@@ -31,212 +71,614 @@ const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'
|
||||
// 背景图片
|
||||
const backgroundImages = ref<string[]>([])
|
||||
const activeImageIndex = ref(0)
|
||||
const previousImageIndex = ref<number | null>(null)
|
||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||
let backgroundRotationTimer: NodeJS.Timeout | null = null
|
||||
const isLoginWallpaperRoute = computed(() => !isLogin.value && route.path === LOGIN_WALLPAPER_ROUTE)
|
||||
const shouldUseTransparentBackgroundTreatment = computed(() => Boolean(isLogin.value) && isTransparentTheme.value)
|
||||
const shouldLoadBackgroundImages = computed(
|
||||
() => isLoginWallpaperRoute.value || (Boolean(isLogin.value) && isTransparentTheme.value),
|
||||
)
|
||||
const transparentBackgroundBlur = ref(16)
|
||||
const transparencyGlassQuality = ref<TransparencyGlassQuality>(
|
||||
localStorage.getItem('transparency-glass-quality') === 'realtime' ? 'realtime' : 'lightweight',
|
||||
)
|
||||
const shouldRenderGlobalBlurLayer = computed(
|
||||
() =>
|
||||
shouldUseTransparentBackgroundTreatment.value &&
|
||||
transparentBackgroundBlur.value > 0 &&
|
||||
transparencyGlassQuality.value === 'realtime',
|
||||
)
|
||||
const isRenderThrottled = ref(document.visibilityState === 'hidden')
|
||||
let backgroundRetryTimer: number | null = null
|
||||
let backgroundRequestController: AbortController | null = null
|
||||
let backgroundCrossfadeTimer: number | null = null
|
||||
let authenticatedStateTimer: number | null = null
|
||||
let windowBlurRenderThrottleTimer: number | null = null
|
||||
|
||||
// ApexCharts 全局配置
|
||||
declare global {
|
||||
interface Window {
|
||||
Apex: any
|
||||
// 读取并同步透明主题背景设置到根组件响应式状态。
|
||||
function applyTransparentBackgroundSettings() {
|
||||
const settings = applyStoredTransparencySettings()
|
||||
|
||||
transparentBackgroundBlur.value = settings.backgroundBlur
|
||||
transparencyGlassQuality.value = settings.glassQuality
|
||||
}
|
||||
|
||||
// 响应透明主题设置变更事件,刷新背景模糊和玻璃质量。
|
||||
function handleTransparencySettingsChanged(event: Event) {
|
||||
const { backgroundBlur, glassQuality } = (event as CustomEvent<TransparencySettings>).detail
|
||||
|
||||
transparentBackgroundBlur.value = backgroundBlur
|
||||
transparencyGlassQuality.value = glassQuality
|
||||
}
|
||||
|
||||
applyTransparentBackgroundSettings()
|
||||
|
||||
function clearWindowBlurRenderThrottleTimer() {
|
||||
if (windowBlurRenderThrottleTimer) {
|
||||
window.clearTimeout(windowBlurRenderThrottleTimer)
|
||||
windowBlurRenderThrottleTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
}
|
||||
// 图例
|
||||
window.Apex.legend = {
|
||||
labels: {
|
||||
useSeriesColors: true,
|
||||
},
|
||||
}
|
||||
// 标题
|
||||
window.Apex.title = {
|
||||
style: {
|
||||
color: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
|
||||
},
|
||||
function restoreForegroundRendering() {
|
||||
const wasRenderThrottled = isRenderThrottled.value
|
||||
|
||||
clearWindowBlurRenderThrottleTimer()
|
||||
isRenderThrottled.value = false
|
||||
|
||||
if (wasRenderThrottled && backgroundImages.value.length > 1) {
|
||||
startBackgroundRotation()
|
||||
rotateBackgroundImage()
|
||||
}
|
||||
}
|
||||
|
||||
function throttleBackgroundRendering() {
|
||||
clearWindowBlurRenderThrottleTimer()
|
||||
resetBackgroundCrossfade()
|
||||
isRenderThrottled.value = true
|
||||
}
|
||||
|
||||
function handleWindowBlurRenderThrottle() {
|
||||
clearWindowBlurRenderThrottleTimer()
|
||||
if (document.visibilityState === 'hidden') {
|
||||
throttleBackgroundRendering()
|
||||
return
|
||||
}
|
||||
|
||||
windowBlurRenderThrottleTimer = window.setTimeout(() => {
|
||||
if (document.visibilityState === 'visible' && !document.hasFocus()) {
|
||||
isRenderThrottled.value = true
|
||||
}
|
||||
windowBlurRenderThrottleTimer = null
|
||||
}, WINDOW_BLUR_RENDER_THROTTLE_DELAY_MS)
|
||||
}
|
||||
|
||||
function handleWindowFocusRenderThrottle() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
restoreForegroundRendering()
|
||||
}
|
||||
}
|
||||
|
||||
let heartbeatInterval: number | null = null
|
||||
let connectionRetryTimer: number | null = null
|
||||
let connectionProbePromise: Promise<boolean> | null = null
|
||||
let connectionProbeFailures = 0
|
||||
let prefersColorSchemeMediaQuery: MediaQueryList | null = null
|
||||
|
||||
const SERVER_PROBE_TIMEOUT_MS = 8_000
|
||||
const SERVER_PROBE_FAILURE_THRESHOLD = 2
|
||||
const SERVER_RETRY_DELAYS_MS = [2_000, 5_000, 10_000, 30_000] as const
|
||||
|
||||
/** 清除等待中的服务重连任务。 */
|
||||
function clearConnectionRetryTimer() {
|
||||
if (!connectionRetryTimer) return
|
||||
|
||||
window.clearTimeout(connectionRetryTimer)
|
||||
connectionRetryTimer = null
|
||||
}
|
||||
|
||||
/** 根据浏览器状态和请求错误判断本次探测失败原因。 */
|
||||
function resolveProbeFailureReason(error: unknown): ConnectionFailureReason {
|
||||
if (!offlineStatus.browserOnline.value) return 'browser-offline'
|
||||
|
||||
const errorCode = (error as { code?: string } | null)?.code
|
||||
if (errorCode === 'ECONNABORTED' || errorCode === 'ETIMEDOUT') return 'timeout'
|
||||
|
||||
return 'server-unreachable'
|
||||
}
|
||||
|
||||
/** 按退避间隔安排下一次 MoviePilot 服务探测。 */
|
||||
function scheduleConnectionRetry() {
|
||||
clearConnectionRetryTimer()
|
||||
|
||||
const retryIndex = Math.min(Math.max(connectionProbeFailures - 1, 0), SERVER_RETRY_DELAYS_MS.length - 1)
|
||||
connectionRetryTimer = window.setTimeout(() => {
|
||||
connectionRetryTimer = null
|
||||
void probeServerConnection()
|
||||
}, SERVER_RETRY_DELAYS_MS[retryIndex])
|
||||
}
|
||||
|
||||
/** 使用后端 ping 接口执行去重后的权威服务连通性探测。 */
|
||||
async function probeServerConnection(showChecking = false): Promise<boolean> {
|
||||
if (!isLogin.value) return false
|
||||
if (connectionProbePromise) return connectionProbePromise
|
||||
|
||||
clearConnectionRetryTimer()
|
||||
if (showChecking) offlineStatus.markConnectionChecking(offlineStatus.connectionReason.value ?? undefined)
|
||||
|
||||
const successSequenceAtProbeStart = offlineStatus.serverSuccessSequence.value
|
||||
const probePromise = (async () => {
|
||||
try {
|
||||
await api.get(
|
||||
'system/ping',
|
||||
{
|
||||
skipConnectionTracking: true,
|
||||
timeout: SERVER_PROBE_TIMEOUT_MS,
|
||||
} as ConnectionAwareRequestConfig,
|
||||
)
|
||||
connectionProbeFailures = 0
|
||||
return true
|
||||
} catch (error) {
|
||||
if (!isLogin.value) {
|
||||
offlineStatus.markServerOnline()
|
||||
return false
|
||||
}
|
||||
|
||||
// 探测期间若已有其他接口成功,则以更新的成功响应为准,避免旧失败覆盖新状态。
|
||||
if (offlineStatus.serverSuccessSequence.value > successSequenceAtProbeStart) {
|
||||
connectionProbeFailures = 0
|
||||
return true
|
||||
}
|
||||
|
||||
connectionProbeFailures += 1
|
||||
const failureReason = resolveProbeFailureReason(error)
|
||||
|
||||
if (connectionProbeFailures >= SERVER_PROBE_FAILURE_THRESHOLD) {
|
||||
offlineStatus.markServerOffline(failureReason)
|
||||
} else {
|
||||
offlineStatus.markConnectionChecking(failureReason)
|
||||
}
|
||||
|
||||
scheduleConnectionRetry()
|
||||
return false
|
||||
}
|
||||
})()
|
||||
|
||||
connectionProbePromise = probePromise
|
||||
try {
|
||||
return await probePromise
|
||||
} finally {
|
||||
if (connectionProbePromise === probePromise) connectionProbePromise = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动即时服务探测和五分钟在线心跳。 */
|
||||
function startHeartbeat() {
|
||||
if (heartbeatInterval) window.clearInterval(heartbeatInterval)
|
||||
|
||||
void probeServerConnection()
|
||||
|
||||
heartbeatInterval = window.setInterval(async () => {
|
||||
if (isLogin.value) await probeServerConnection()
|
||||
}, 5 * 60 * 1000)
|
||||
}
|
||||
|
||||
/** 停止心跳和等待中的自动重连任务。 */
|
||||
function stopHeartbeat() {
|
||||
if (heartbeatInterval) {
|
||||
window.clearInterval(heartbeatInterval)
|
||||
heartbeatInterval = null
|
||||
}
|
||||
|
||||
clearConnectionRetryTimer()
|
||||
connectionProbeFailures = 0
|
||||
}
|
||||
|
||||
watch(
|
||||
() => offlineStatus.connectionCheckRequestId.value,
|
||||
() => {
|
||||
if (isLogin.value) void probeServerConnection(true)
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => offlineStatus.connectionStatus.value,
|
||||
status => {
|
||||
if (status !== 'online') return
|
||||
connectionProbeFailures = 0
|
||||
clearConnectionRetryTimer()
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => offlineStatus.browserOnline.value,
|
||||
browserIsOnline => {
|
||||
if (!isLogin.value) return
|
||||
offlineStatus.requestConnectionCheck(browserIsOnline ? undefined : 'browser-offline')
|
||||
},
|
||||
)
|
||||
|
||||
// 更新data-theme属性以便CSS选择器能正确匹配
|
||||
function updateHtmlThemeAttribute(themeName: string) {
|
||||
document.documentElement.setAttribute('data-theme', themeName)
|
||||
// 确保body元素也有相同的主题属性,以便更好地选择弹出窗口
|
||||
document.body.setAttribute('data-theme', themeName)
|
||||
syncRootLaunchPalette()
|
||||
}
|
||||
|
||||
// 从本地存储重新同步主题偏好、DOM 主题属性和相关外观配置。
|
||||
function syncThemePreferenceFromStorage() {
|
||||
themeValue = localStorage.getItem('theme') || 'auto'
|
||||
|
||||
const resolvedTheme = resolveThemeName(themeValue)
|
||||
if (globalTheme.name.value !== resolvedTheme) {
|
||||
globalTheme.name.value = resolvedTheme
|
||||
}
|
||||
|
||||
applyStoredThemeCustomizerAppearance(vuetifyTheme)
|
||||
updateHtmlThemeAttribute(resolvedTheme)
|
||||
configureApexChartsTheme(resolvedTheme)
|
||||
|
||||
// 前台恢复时重新跑一次主题管理器,补齐 transparent CSS 和 auto 的实际 DOM 主题。
|
||||
void themeManager
|
||||
.setTheme(themeValue)
|
||||
.then(() => {
|
||||
updateHtmlThemeAttribute(globalTheme.name.value)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('同步主题管理器失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
// 系统配色变化时,在自动主题模式下刷新当前实际主题。
|
||||
function handleSystemThemeChange() {
|
||||
if ((localStorage.getItem('theme') || 'auto') === 'auto') {
|
||||
syncThemePreferenceFromStorage()
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面重新可见时同步主题,并在连接异常时立即重新探测服务。 */
|
||||
function handleVisibilityThemeSync() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
restoreForegroundRendering()
|
||||
syncThemePreferenceFromStorage()
|
||||
if (isLogin.value && !offlineStatus.isOnline.value) offlineStatus.requestConnectionCheck()
|
||||
} else {
|
||||
throttleBackgroundRendering()
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面从缓存或重新聚焦恢复时刷新主题偏好和异常连接状态。 */
|
||||
function handlePageShowThemeSync() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
restoreForegroundRendering()
|
||||
if (isLogin.value && !offlineStatus.isOnline.value) offlineStatus.requestConnectionCheck()
|
||||
}
|
||||
syncThemePreferenceFromStorage()
|
||||
}
|
||||
|
||||
// 清理背景图交叉淡入淡出定时器。
|
||||
function clearBackgroundCrossfadeTimer() {
|
||||
if (backgroundCrossfadeTimer) {
|
||||
window.clearTimeout(backgroundCrossfadeTimer)
|
||||
backgroundCrossfadeTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 重置背景图交叉淡入淡出状态。
|
||||
function resetBackgroundCrossfade() {
|
||||
clearBackgroundCrossfadeTimer()
|
||||
previousImageIndex.value = null
|
||||
}
|
||||
|
||||
// 切换期保留上一张背景的渲染状态,避免图片合成层重建时露出透明底。
|
||||
function activateBackgroundImage(nextIndex: number) {
|
||||
if (nextIndex === activeImageIndex.value) return
|
||||
|
||||
clearBackgroundCrossfadeTimer()
|
||||
previousImageIndex.value = activeImageIndex.value
|
||||
activeImageIndex.value = nextIndex
|
||||
backgroundCrossfadeTimer = window.setTimeout(() => {
|
||||
previousImageIndex.value = null
|
||||
backgroundCrossfadeTimer = null
|
||||
}, BACKGROUND_CROSSFADE_DURATION_MS)
|
||||
}
|
||||
|
||||
// 获取背景图片
|
||||
async function fetchBackgroundImages() {
|
||||
try {
|
||||
backgroundImages.value = await api.get(`/login/wallpapers`)
|
||||
backgroundRequestController?.abort()
|
||||
backgroundRequestController = new AbortController()
|
||||
backgroundImages.value = await api.get(`/login/wallpapers`, {
|
||||
signal: backgroundRequestController.signal,
|
||||
})
|
||||
resetBackgroundCrossfade()
|
||||
activeImageIndex.value = 0
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// 背景图片轮换函数
|
||||
function rotateBackgroundImage() {
|
||||
if (isRenderThrottled.value) return
|
||||
|
||||
if (backgroundImages.value.length > 1) {
|
||||
// 计算下一个图片索引
|
||||
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
||||
// 预加载下一张图片
|
||||
preloadImage(backgroundImages.value[nextIndex]).then(success => {
|
||||
// 只有图片成功加载才切换
|
||||
if (success) {
|
||||
activateBackgroundImage(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秒切换一次
|
||||
// 使用优化的定时器管理器,后台时自动暂停
|
||||
addBackgroundTimer(
|
||||
'background-rotation',
|
||||
rotateBackgroundImage,
|
||||
10000, // 每10秒切换一次
|
||||
{
|
||||
runInBackground: false, // 后台时不运行
|
||||
skipInitialRun: true, // 不需要立即执行
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 预加载图片
|
||||
function preloadImage(url: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const img = new Image()
|
||||
// 停止登录页或透明主题背景图加载、重试和轮播。
|
||||
function stopBackgroundLoading() {
|
||||
backgroundRequestController?.abort()
|
||||
backgroundRequestController = null
|
||||
|
||||
img.onload = () => resolve(true)
|
||||
img.onerror = () => resolve(false)
|
||||
if (backgroundRetryTimer) {
|
||||
window.clearTimeout(backgroundRetryTimer)
|
||||
backgroundRetryTimer = null
|
||||
}
|
||||
|
||||
// 设置超时,防止图片长时间加载
|
||||
const timeout = setTimeout(() => {
|
||||
img.src = ''
|
||||
resolve(false)
|
||||
}, 5000) // 5秒超时
|
||||
resetBackgroundCrossfade()
|
||||
removeBackgroundTimer('background-rotation')
|
||||
}
|
||||
|
||||
img.src = url
|
||||
// 初始化登录后的全局设置和用户设置状态。
|
||||
async function initializeAuthenticatedState() {
|
||||
if (!isLogin.value) return
|
||||
|
||||
// 如果图片已经缓存,onload可能不会触发
|
||||
if (img.complete) {
|
||||
clearTimeout(timeout)
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
try {
|
||||
globalLoadingStateManager.setLoadingState('global-settings', true)
|
||||
await globalSettingsStore.initialize()
|
||||
await globalSettingsStore.loadUserSettings()
|
||||
} finally {
|
||||
globalLoadingStateManager.setLoadingState('global-settings', false)
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟初始化登录态数据,避开登录成功后的即时路由跳转窗口。
|
||||
function scheduleAuthenticatedStateInitialization() {
|
||||
if (authenticatedStateTimer) {
|
||||
window.clearTimeout(authenticatedStateTimer)
|
||||
}
|
||||
|
||||
// 登录后会立刻发生路由切换,稍后再拉取设置可避开导航中止请求。
|
||||
authenticatedStateTimer = window.setTimeout(() => {
|
||||
authenticatedStateTimer = null
|
||||
initializeAuthenticatedState()
|
||||
}, 150)
|
||||
}
|
||||
|
||||
// 添加logo动画效果并延迟移除加载界面
|
||||
function animateAndRemoveLoader() {
|
||||
async function animateAndRemoveLoader() {
|
||||
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
||||
if (loadingBg) {
|
||||
// 先添加完成动画类
|
||||
// 只收掉启动内容,背景层保持实色直到节点被移除,避免底部 safe area 先透出页面内容。
|
||||
loadingBg.classList.add('loading-complete')
|
||||
await new Promise<void>(resolve => {
|
||||
window.setTimeout(() => {
|
||||
removeEl('#loading-bg')
|
||||
|
||||
// 等待动画完成后再移除元素
|
||||
setTimeout(() => {
|
||||
removeEl('#loading-bg')
|
||||
// 将background属性从html的style中移除
|
||||
document.documentElement.style.removeProperty('background')
|
||||
// 显示页面
|
||||
show.value = true
|
||||
}, 500) // 与CSS动画持续时间匹配
|
||||
// 启动阶段的根节点锁定只在 loader 存在时生效,移除后恢复正常页面与弹窗布局。
|
||||
document.documentElement.removeAttribute('data-launch-loading')
|
||||
document.documentElement.style.removeProperty('overflow')
|
||||
document.body.style.removeProperty('overflow')
|
||||
completeLaunchLoading()
|
||||
resolve()
|
||||
}, 120)
|
||||
})
|
||||
} else {
|
||||
completeLaunchLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 检查PWA状态并移除加载界面
|
||||
async function removeLoadingWithStateCheck() {
|
||||
try {
|
||||
// 设置各个组件的加载状态
|
||||
globalLoadingStateManager.setLoadingState('pwa-state', true)
|
||||
|
||||
// 静默检查PWA状态恢复
|
||||
const pwaController = (window as any).pwaStateController
|
||||
if (pwaController) {
|
||||
await pwaController.waitForStateRestore()
|
||||
}
|
||||
globalLoadingStateManager.setLoadingState('pwa-state', false)
|
||||
|
||||
// PWA/App 模式会影响布局和底部导航,必须在启动屏退场前稳定下来。
|
||||
await initializePWA()
|
||||
await initializeAuthenticatedState()
|
||||
|
||||
// 等待所有加载完成
|
||||
await globalLoadingStateManager.waitForAllComplete()
|
||||
|
||||
// 移除加载界面
|
||||
await animateAndRemoveLoader()
|
||||
|
||||
// 检查未读消息
|
||||
if (isLogin.value) {
|
||||
checkAndEmitUnreadMessages()
|
||||
}
|
||||
} catch (error) {
|
||||
// 即使出错也要移除加载界面
|
||||
globalLoadingStateManager.reset()
|
||||
await animateAndRemoveLoader()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载背景图片
|
||||
async function loadBackgroundImages() {
|
||||
await fetchBackgroundImages()
|
||||
.then(() => {
|
||||
startBackgroundRotation()
|
||||
})
|
||||
.catch(() => {
|
||||
// 3秒后重试
|
||||
setTimeout(() => {
|
||||
loadBackgroundImages()
|
||||
}, 3000)
|
||||
})
|
||||
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)
|
||||
backgroundRetryTimer = window.setTimeout(() => {
|
||||
backgroundRetryTimer = null
|
||||
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
|
||||
configureApexChartsTheme(globalTheme.name.value)
|
||||
|
||||
// 初始化data-theme属性
|
||||
updateHtmlThemeAttribute(globalTheme.name.value)
|
||||
|
||||
// 默认隐藏页面
|
||||
show.value = false
|
||||
// 初始化主题管理器 - 统一处理主题初始化
|
||||
await themeManager.setTheme(themeValue)
|
||||
applyStoredThemeCustomizerAppearance(vuetifyTheme)
|
||||
updateHtmlThemeAttribute(globalTheme.name.value)
|
||||
|
||||
// 加载背景图片
|
||||
await loadBackgroundImages()
|
||||
// 监听主题变化
|
||||
watch(
|
||||
() => globalTheme.name.value,
|
||||
newTheme => {
|
||||
// 更新HTML主题属性
|
||||
updateHtmlThemeAttribute(newTheme)
|
||||
// 重新配置ApexCharts以适应新主题
|
||||
configureApexChartsTheme(newTheme)
|
||||
},
|
||||
)
|
||||
|
||||
// 移除加载动画
|
||||
prefersColorSchemeMediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)') ?? null
|
||||
prefersColorSchemeMediaQuery?.addEventListener('change', handleSystemThemeChange)
|
||||
document.addEventListener('visibilitychange', handleVisibilityThemeSync)
|
||||
window.addEventListener('pageshow', handlePageShowThemeSync)
|
||||
window.addEventListener('focus', handlePageShowThemeSync)
|
||||
window.addEventListener('focus', handleWindowFocusRenderThrottle)
|
||||
window.addEventListener('blur', handleWindowBlurRenderThrottle)
|
||||
window.addEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
||||
|
||||
// 登录页壁纸仅在未登录登录页需要,避免其他首屏额外发起图片列表请求。
|
||||
watch(
|
||||
shouldLoadBackgroundImages,
|
||||
shouldLoad => {
|
||||
stopBackgroundLoading()
|
||||
if (shouldLoad) {
|
||||
loadBackgroundImages()
|
||||
} else if (!isTransparentTheme.value) {
|
||||
backgroundImages.value = []
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 使用优化后的加载界面移除逻辑
|
||||
ensureRenderComplete(() => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// 移除加载动画,显示页面
|
||||
animateAndRemoveLoader()
|
||||
|
||||
// 页面完全显示后,检查未读消息
|
||||
setTimeout(() => {
|
||||
checkAndEmitUnreadMessages()
|
||||
}, 1000)
|
||||
}, 1500)
|
||||
})
|
||||
nextTick(removeLoadingWithStateCheck)
|
||||
})
|
||||
// 启动心跳
|
||||
if (isLogin.value) {
|
||||
startHeartbeat()
|
||||
}
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
loadBackgroundImages()
|
||||
// 页面恢复可见时检查未读消息
|
||||
setTimeout(() => {
|
||||
checkAndEmitUnreadMessages()
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加PWA的页面恢复事件监听
|
||||
window.addEventListener('pageshow', event => {
|
||||
// persisted属性为true表示页面是从bfcache中恢复的
|
||||
if (event.persisted) {
|
||||
loadBackgroundImages()
|
||||
// PWA恢复时检查未读消息
|
||||
setTimeout(() => {
|
||||
checkAndEmitUnreadMessages()
|
||||
}, 500)
|
||||
// 登录状态可能在当前单页会话中变化,这里按需补齐登录后初始化和心跳。
|
||||
watch(isLogin, loggedIn => {
|
||||
if (loggedIn) {
|
||||
startHeartbeat()
|
||||
scheduleAuthenticatedStateInitialization()
|
||||
} else {
|
||||
if (authenticatedStateTimer) {
|
||||
window.clearTimeout(authenticatedStateTimer)
|
||||
authenticatedStateTimer = null
|
||||
}
|
||||
stopHeartbeat()
|
||||
offlineStatus.markServerOnline()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除页面可见性监听
|
||||
document.removeEventListener('visibilitychange', () => {})
|
||||
// 移除PWA的页面恢复事件监听
|
||||
window.removeEventListener('pageshow', () => {})
|
||||
|
||||
// 清除轮换定时器
|
||||
if (backgroundRotationTimer) {
|
||||
clearInterval(backgroundRotationTimer)
|
||||
backgroundRotationTimer = null
|
||||
// 清除背景轮换定时器
|
||||
stopBackgroundLoading()
|
||||
if (authenticatedStateTimer) {
|
||||
window.clearTimeout(authenticatedStateTimer)
|
||||
authenticatedStateTimer = null
|
||||
}
|
||||
clearWindowBlurRenderThrottleTimer()
|
||||
// 停止心跳
|
||||
stopHeartbeat()
|
||||
prefersColorSchemeMediaQuery?.removeEventListener('change', handleSystemThemeChange)
|
||||
prefersColorSchemeMediaQuery = null
|
||||
document.removeEventListener('visibilitychange', handleVisibilityThemeSync)
|
||||
window.removeEventListener('pageshow', handlePageShowThemeSync)
|
||||
window.removeEventListener('focus', handlePageShowThemeSync)
|
||||
window.removeEventListener('focus', handleWindowFocusRenderThrottle)
|
||||
window.removeEventListener('blur', handleWindowBlurRenderThrottle)
|
||||
window.removeEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-wrapper">
|
||||
<div class="app-wrapper" :class="{ 'app-wrapper--render-throttled': isRenderThrottled }">
|
||||
<!-- 透明主题背景 -->
|
||||
<div v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)" class="background-container">
|
||||
<div
|
||||
v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)"
|
||||
class="background-container"
|
||||
:class="{
|
||||
'is-transparent-theme': shouldUseTransparentBackgroundTreatment,
|
||||
'is-transparent-glass-lightweight':
|
||||
shouldUseTransparentBackgroundTreatment && transparencyGlassQuality === 'lightweight',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
:key="`bg-${index}-${loginStateKey}`"
|
||||
class="background-image"
|
||||
:class="{ 'active': index === activeImageIndex }"
|
||||
:class="{ 'active': index === activeImageIndex, 'previous': index === previousImageIndex }"
|
||||
:style="{ 'backgroundImage': `url(${imageUrl})` }"
|
||||
></div>
|
||||
/>
|
||||
<!-- 全局磨砂层 -->
|
||||
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
|
||||
<div v-if="shouldRenderGlobalBlurLayer" class="global-blur-layer"></div>
|
||||
</div>
|
||||
<!-- 页面内容 -->
|
||||
<VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }">
|
||||
<VApp :class="{ 'app-shell--login-wallpaper': isLoginWallpaperRoute }">
|
||||
<RouterView />
|
||||
<!-- 全局共享弹窗入口,列表与卡片按需在这里挂载业务弹窗。 -->
|
||||
<SharedDialogHost />
|
||||
<!-- PWA安装提示 -->
|
||||
<PWAInstallPrompt />
|
||||
</VApp>
|
||||
</div>
|
||||
</template>
|
||||
@@ -282,19 +724,62 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
&.active {
|
||||
z-index: 2;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.previous {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.background-container.is-transparent-theme .background-image.active {
|
||||
opacity: var(--transparent-background-poster-opacity, 1);
|
||||
}
|
||||
|
||||
.background-container.is-transparent-glass-lightweight .background-image.active,
|
||||
.background-container.is-transparent-glass-lightweight .background-image.previous {
|
||||
filter: blur(var(--transparent-background-blur, 16px));
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.background-container.is-transparent-glass-lightweight .background-image.active::after,
|
||||
.background-container.is-transparent-glass-lightweight .background-image.previous::after {
|
||||
background:
|
||||
linear-gradient(rgba(0, 0, 0, 30%) 0%, rgba(0, 0, 0, 60%) 100%),
|
||||
rgba(128, 128, 128, 30%);
|
||||
}
|
||||
|
||||
/* 全局磨砂层 */
|
||||
.global-blur-layer {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
backdrop-filter: blur(16px);
|
||||
z-index: 3;
|
||||
backdrop-filter: blur(var(--transparent-background-blur, 16px));
|
||||
background-color: rgba(128, 128, 128, 30%);
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
.app-wrapper--render-throttled {
|
||||
.global-blur-layer {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.login-bg-decor *,
|
||||
.login-logo,
|
||||
.login-logo-wrapper,
|
||||
.login-logo-wrapper::before,
|
||||
.login-title,
|
||||
.login-subtitle,
|
||||
.agent-assistant-fab * {
|
||||
animation-play-state: paused !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录页壁纸在 VApp 外层渲染,登录页 VApp 需要透明才能露出壁纸。 */
|
||||
.app-shell--login-wallpaper.v-application {
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,10 @@ import modeIniUrl from 'ace-builds/src-noconflict/mode-ini?url'
|
||||
|
||||
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||
|
||||
import themeGithubDarkUrl from 'ace-builds/src-noconflict/theme-github_dark?url'
|
||||
|
||||
import themeGithubLightDefaultUrl from 'ace-builds/src-noconflict/theme-github_light_default?url'
|
||||
|
||||
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
||||
|
||||
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'
|
||||
@@ -44,6 +48,538 @@ import snippertsIniUrl from 'ace-builds/src-noconflict/snippets/ini?url'
|
||||
|
||||
import 'ace-builds/src-noconflict/ext-language_tools'
|
||||
|
||||
const aceModule = ace as typeof ace & {
|
||||
define?: (moduleName: string, deps: string[], payload: (...args: any[]) => void) => void
|
||||
}
|
||||
|
||||
function registerJinja2Mode() {
|
||||
aceModule.define?.(
|
||||
'ace/mode/jinja2_highlight_rules',
|
||||
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules'],
|
||||
(require: any, exports: any) => {
|
||||
const oop = require('../lib/oop')
|
||||
const TextHighlightRules = require('./text_highlight_rules').TextHighlightRules
|
||||
|
||||
const Jinja2HighlightRules = function (this: any) {
|
||||
const tags =
|
||||
'autoescape|block|call|do|elif|else|endautoescape|endblock|endcall|endfilter|endfor|endif|endmacro|endraw|endset|endtrans|endwith|extends|filter|for|from|if|import|include|macro|raw|set|trans|with'
|
||||
const filters =
|
||||
'abs|attr|batch|capitalize|center|count|d|default|dictsort|e|escape|filesizeformat|first|float|forceescape|format|groupby|indent|int|items|join|last|length|list|lower|map|max|min|pprint|random|reject|rejectattr|replace|reverse|round|safe|select|selectattr|slice|sort|string|striptags|sum|title|tojson|trim|truncate|unique|upper|urlencode|urlize|wordcount|wordwrap|xmlattr'
|
||||
const functions = 'cycler|dict|joiner|lipsum|namespace|range'
|
||||
const tests =
|
||||
'boolean|defined|divisibleby|eq|escaped|even|false|filter|float|ge|gt|in|integer|iterable|le|lower|lt|mapping|ne|none|number|odd|sameas|sequence|string|test|true|undefined|upper'
|
||||
const operators = 'and|in|is|not|or'
|
||||
const contextVariables =
|
||||
'title|en_title|original_title|season|season_fmt|year|title_year|type|category|vote_average|poster|backdrop|season_year|actors|overview|tmdbid|imdbid|doubanid|episode_title|episode_date|original_name|name|en_name|episode|season_episode|part|customization|fps|resourceType|effect|edition|videoFormat|resource_term|releaseGroup|videoCodec|audioCodec|webSource|torrent_title|pubdate|freedate|seeders|volume_factor|hit_and_run|labels|description|site_name|size|transfer_type|file_count|total_size|err_msg|fileExt|__meta__|__mediainfo__|__torrentinfo__|__transferinfo__|__episodes_info__'
|
||||
|
||||
const keywordMapper = this.createKeywordMapper(
|
||||
{
|
||||
'keyword.control.jinja2': tags,
|
||||
'keyword.operator.jinja2': operators,
|
||||
'support.function.jinja2': [filters, functions, tests].join('|'),
|
||||
'constant.language.jinja2': 'false|False|none|None|null|true|True',
|
||||
},
|
||||
'identifier',
|
||||
)
|
||||
|
||||
const jinjaExpressionRules = [
|
||||
{
|
||||
token: 'string',
|
||||
regex: "'",
|
||||
push: 'jinja2-qstring',
|
||||
},
|
||||
{
|
||||
token: 'string',
|
||||
regex: '"',
|
||||
push: 'jinja2-qqstring',
|
||||
},
|
||||
{
|
||||
token: 'constant.numeric',
|
||||
regex: /[+-]?(?:0[xX][0-9a-fA-F]+|\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\b/,
|
||||
},
|
||||
{
|
||||
token: ['keyword.operator.other.jinja2', 'text', 'support.function.jinja2'],
|
||||
regex: `(\\|)(\\s*)(${filters})\\b`,
|
||||
},
|
||||
{
|
||||
token: ['keyword.operator.jinja2', 'text', 'support.function.jinja2'],
|
||||
regex: `(\\bis\\b)(\\s*)(${tests})\\b`,
|
||||
},
|
||||
{
|
||||
token: ['support.function.jinja2', 'text', 'paren.lparen'],
|
||||
regex: `\\b(${functions})(\\s*)(\\()`,
|
||||
},
|
||||
{
|
||||
token: 'variable.language.jinja2',
|
||||
regex: `\\b(?:${contextVariables})\\b`,
|
||||
},
|
||||
{
|
||||
token: keywordMapper,
|
||||
regex: /[a-zA-Z_$][a-zA-Z0-9_$]*\b/,
|
||||
},
|
||||
{
|
||||
token: 'keyword.operator.assignment.jinja2',
|
||||
regex: /=|~/,
|
||||
},
|
||||
{
|
||||
token: 'keyword.operator.comparison.jinja2',
|
||||
regex: /==|!=|<=|>=|<|>/,
|
||||
},
|
||||
{
|
||||
token: 'keyword.operator.arithmetic.jinja2',
|
||||
regex: /\+|-|\/\/|\/|%|\*\*|\*/,
|
||||
},
|
||||
{
|
||||
token: 'keyword.operator.other.jinja2',
|
||||
regex: /\.{2}|\||:/,
|
||||
},
|
||||
{
|
||||
token: 'punctuation.operator.jinja2',
|
||||
regex: /[.,;?]/,
|
||||
},
|
||||
{
|
||||
token: 'paren.lparen',
|
||||
regex: /[\[({]/,
|
||||
},
|
||||
{
|
||||
token: 'paren.rparen',
|
||||
regex: /[\])}]/,
|
||||
},
|
||||
{
|
||||
token: 'text',
|
||||
regex: /\s+/,
|
||||
},
|
||||
]
|
||||
|
||||
this.$rules = {
|
||||
start: [
|
||||
{
|
||||
token: 'comment.block.jinja2',
|
||||
regex: /\{#-?/,
|
||||
push: 'jinja2-comment',
|
||||
},
|
||||
{
|
||||
token: 'constant.language.jinja2',
|
||||
regex: /\{\{-?/,
|
||||
push: 'jinja2-expression',
|
||||
},
|
||||
{
|
||||
token: 'keyword.control.jinja2',
|
||||
regex: /\{%-?/,
|
||||
push: 'jinja2-statement',
|
||||
},
|
||||
],
|
||||
'jinja2-comment': [
|
||||
{
|
||||
token: 'comment.block.jinja2',
|
||||
regex: /-?#\}/,
|
||||
next: 'pop',
|
||||
},
|
||||
{
|
||||
defaultToken: 'comment.block.jinja2',
|
||||
},
|
||||
],
|
||||
'jinja2-expression': [
|
||||
{
|
||||
token: 'constant.language.jinja2',
|
||||
regex: /-?\}\}/,
|
||||
next: 'pop',
|
||||
},
|
||||
...jinjaExpressionRules,
|
||||
],
|
||||
'jinja2-statement': [
|
||||
{
|
||||
token: 'keyword.control.jinja2',
|
||||
regex: /-?%\}/,
|
||||
next: 'pop',
|
||||
},
|
||||
...jinjaExpressionRules,
|
||||
],
|
||||
'jinja2-qqstring': [
|
||||
{
|
||||
token: 'constant.language.escape',
|
||||
regex: /\\[\\"ntr]/,
|
||||
},
|
||||
{
|
||||
token: 'string',
|
||||
regex: '"',
|
||||
next: 'pop',
|
||||
},
|
||||
{
|
||||
defaultToken: 'string',
|
||||
},
|
||||
],
|
||||
'jinja2-qstring': [
|
||||
{
|
||||
token: 'constant.language.escape',
|
||||
regex: /\\[\\'ntr]/,
|
||||
},
|
||||
{
|
||||
token: 'string',
|
||||
regex: "'",
|
||||
next: 'pop',
|
||||
},
|
||||
{
|
||||
defaultToken: 'string',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
this.normalizeRules()
|
||||
}
|
||||
|
||||
oop.inherits(Jinja2HighlightRules, TextHighlightRules)
|
||||
exports.Jinja2HighlightRules = Jinja2HighlightRules
|
||||
},
|
||||
)
|
||||
|
||||
aceModule.define?.(
|
||||
'ace/mode/jinja2',
|
||||
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text', 'ace/mode/jinja2_highlight_rules'],
|
||||
(require: any, exports: any) => {
|
||||
const oop = require('../lib/oop')
|
||||
const TextMode = require('./text').Mode
|
||||
const Jinja2HighlightRules = require('./jinja2_highlight_rules').Jinja2HighlightRules
|
||||
|
||||
const Mode = function (this: any) {
|
||||
TextMode.call(this)
|
||||
this.HighlightRules = Jinja2HighlightRules
|
||||
}
|
||||
|
||||
oop.inherits(Mode, TextMode)
|
||||
|
||||
;(function (this: any) {
|
||||
this.$id = 'ace/mode/jinja2'
|
||||
this.blockComment = { start: '{#', end: '#}' }
|
||||
}).call(Mode.prototype)
|
||||
|
||||
exports.Mode = Mode
|
||||
},
|
||||
)
|
||||
|
||||
aceModule.define?.('ace/snippets/jinja2', ['require', 'exports', 'module'], (_require: any, exports: any) => {
|
||||
exports.snippetText =
|
||||
'snippet if\n\t{% if ${1:condition} %}\n\t\t${0}\n\t{% endif %}\n' +
|
||||
'snippet for\n\t{% for ${1:item} in ${2:items} %}\n\t\t${0}\n\t{% endfor %}\n' +
|
||||
'snippet var\n\t{{ ${1:name} }}\n'
|
||||
exports.scope = 'jinja2'
|
||||
})
|
||||
|
||||
aceModule.define?.(
|
||||
'ace/mode/jinja2_json_highlight_rules',
|
||||
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules'],
|
||||
(require: any, exports: any) => {
|
||||
const oop = require('../lib/oop')
|
||||
const TextHighlightRules = require('./text_highlight_rules').TextHighlightRules
|
||||
|
||||
const Jinja2JsonHighlightRules = function (this: any) {
|
||||
const tags =
|
||||
'autoescape|block|call|do|elif|else|endautoescape|endblock|endcall|endfilter|endfor|endif|endmacro|endraw|endset|endtrans|endwith|extends|filter|for|from|if|import|include|macro|raw|set|trans|with'
|
||||
const filters =
|
||||
'abs|attr|batch|capitalize|center|count|d|default|dictsort|e|escape|filesizeformat|first|float|forceescape|format|groupby|indent|int|items|join|last|length|list|lower|map|max|min|pprint|random|reject|rejectattr|replace|reverse|round|safe|select|selectattr|slice|sort|string|striptags|sum|title|tojson|trim|truncate|unique|upper|urlencode|urlize|wordcount|wordwrap|xmlattr'
|
||||
const functions = 'cycler|dict|joiner|lipsum|namespace|range'
|
||||
const tests =
|
||||
'boolean|defined|divisibleby|eq|escaped|even|false|filter|float|ge|gt|in|integer|iterable|le|lower|lt|mapping|ne|none|number|odd|sameas|sequence|string|test|true|undefined|upper'
|
||||
const operators = 'and|in|is|not|or'
|
||||
const contextVariables =
|
||||
'title|en_title|original_title|season|season_fmt|year|title_year|type|category|vote_average|poster|backdrop|season_year|actors|overview|tmdbid|imdbid|doubanid|episode_title|episode_date|original_name|name|en_name|episode|season_episode|part|customization|fps|resourceType|effect|edition|videoFormat|resource_term|releaseGroup|videoCodec|audioCodec|webSource|torrent_title|pubdate|freedate|seeders|volume_factor|hit_and_run|labels|description|site_name|size|transfer_type|file_count|total_size|err_msg|fileExt|__meta__|__mediainfo__|__torrentinfo__|__transferinfo__|__episodes_info__'
|
||||
|
||||
const keywordMapper = this.createKeywordMapper(
|
||||
{
|
||||
'keyword.control.jinja2': tags,
|
||||
'keyword.operator.jinja2': operators,
|
||||
'support.function.jinja2': [filters, functions, tests].join('|'),
|
||||
'constant.language.jinja2': 'false|False|none|None|null|true|True',
|
||||
},
|
||||
'identifier',
|
||||
)
|
||||
|
||||
const jinjaRules = [
|
||||
{
|
||||
token: 'string',
|
||||
regex: "'",
|
||||
push: 'jinja2-json-qstring',
|
||||
},
|
||||
{
|
||||
token: 'constant.language.escape',
|
||||
regex: /\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|["\\\/bfnrt])/,
|
||||
},
|
||||
{
|
||||
token: 'constant.numeric',
|
||||
regex: /[+-]?(?:0[xX][0-9a-fA-F]+|\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\b/,
|
||||
},
|
||||
{
|
||||
token: ['keyword.operator.other.jinja2', 'text', 'support.function.jinja2'],
|
||||
regex: `(\\|)(\\s*)(${filters})\\b`,
|
||||
},
|
||||
{
|
||||
token: ['keyword.operator.jinja2', 'text', 'support.function.jinja2'],
|
||||
regex: `(\\bis\\b)(\\s*)(${tests})\\b`,
|
||||
},
|
||||
{
|
||||
token: ['support.function.jinja2', 'text', 'paren.lparen'],
|
||||
regex: `\\b(${functions})(\\s*)(\\()`,
|
||||
},
|
||||
{
|
||||
token: 'variable.language.jinja2',
|
||||
regex: `\\b(?:${contextVariables})\\b`,
|
||||
},
|
||||
{
|
||||
token: keywordMapper,
|
||||
regex: /[a-zA-Z_$][a-zA-Z0-9_$]*\b/,
|
||||
},
|
||||
{
|
||||
token: 'keyword.operator.assignment.jinja2',
|
||||
regex: /=|~/,
|
||||
},
|
||||
{
|
||||
token: 'keyword.operator.comparison.jinja2',
|
||||
regex: /==|!=|<=|>=|<|>/,
|
||||
},
|
||||
{
|
||||
token: 'keyword.operator.arithmetic.jinja2',
|
||||
regex: /\+|-|\/\/|\/|%|\*\*|\*/,
|
||||
},
|
||||
{
|
||||
token: 'keyword.operator.other.jinja2',
|
||||
regex: /\.{2}|\||:/,
|
||||
},
|
||||
{
|
||||
token: 'punctuation.operator.jinja2',
|
||||
regex: /[.,;?]/,
|
||||
},
|
||||
{
|
||||
token: 'paren.lparen',
|
||||
regex: /[\[({]/,
|
||||
},
|
||||
{
|
||||
token: 'paren.rparen',
|
||||
regex: /[\])}]/,
|
||||
},
|
||||
{
|
||||
token: 'text',
|
||||
regex: /\s+/,
|
||||
},
|
||||
]
|
||||
|
||||
this.$rules = {
|
||||
start: [
|
||||
{
|
||||
token: 'variable',
|
||||
regex: /"(?:(?:\\.)|(?:[^"\\]))*?"\s*(?=:)/,
|
||||
},
|
||||
{
|
||||
token: 'string',
|
||||
regex: '"',
|
||||
push: 'json-string',
|
||||
},
|
||||
{
|
||||
token: 'constant.numeric',
|
||||
regex: /0[xX][0-9a-fA-F]+\b/,
|
||||
},
|
||||
{
|
||||
token: 'constant.numeric',
|
||||
regex: /[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/,
|
||||
},
|
||||
{
|
||||
token: 'constant.language.boolean',
|
||||
regex: /(?:true|false|null)\b/,
|
||||
},
|
||||
{
|
||||
token: 'text',
|
||||
regex: /['](?:(?:\\.)|(?:[^'\\]))*?[']/,
|
||||
},
|
||||
{
|
||||
token: 'comment',
|
||||
regex: /\/\/.*$/,
|
||||
},
|
||||
{
|
||||
token: 'comment.start',
|
||||
regex: /\/\*/,
|
||||
push: 'comment',
|
||||
},
|
||||
{
|
||||
token: 'paren.lparen',
|
||||
regex: /[[({]/,
|
||||
},
|
||||
{
|
||||
token: 'paren.rparen',
|
||||
regex: /[\])}]/,
|
||||
},
|
||||
{
|
||||
token: 'punctuation.operator',
|
||||
regex: /[:,]/,
|
||||
},
|
||||
{
|
||||
token: 'text',
|
||||
regex: /\s+/,
|
||||
},
|
||||
],
|
||||
'json-string': [
|
||||
{
|
||||
token: 'constant.language.escape',
|
||||
regex: /\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|["\\\/bfnrt])/,
|
||||
},
|
||||
{
|
||||
token: 'comment.block.jinja2',
|
||||
regex: /\{#-?/,
|
||||
push: 'jinja2-json-comment',
|
||||
},
|
||||
{
|
||||
token: 'constant.language.jinja2',
|
||||
regex: /\{\{-?/,
|
||||
push: 'jinja2-json-expression',
|
||||
},
|
||||
{
|
||||
token: 'keyword.control.jinja2',
|
||||
regex: /\{%-?/,
|
||||
push: 'jinja2-json-statement',
|
||||
},
|
||||
{
|
||||
token: 'string',
|
||||
regex: /"|$/,
|
||||
next: 'pop',
|
||||
},
|
||||
{
|
||||
defaultToken: 'string',
|
||||
},
|
||||
],
|
||||
comment: [
|
||||
{
|
||||
token: 'comment.end',
|
||||
regex: /\*\//,
|
||||
next: 'pop',
|
||||
},
|
||||
{
|
||||
defaultToken: 'comment',
|
||||
},
|
||||
],
|
||||
'jinja2-json-comment': [
|
||||
{
|
||||
token: 'comment.block.jinja2',
|
||||
regex: /-?#\}/,
|
||||
next: 'pop',
|
||||
},
|
||||
{
|
||||
defaultToken: 'comment.block.jinja2',
|
||||
},
|
||||
],
|
||||
'jinja2-json-expression': [
|
||||
{
|
||||
token: 'constant.language.jinja2',
|
||||
regex: /-?\}\}/,
|
||||
next: 'pop',
|
||||
},
|
||||
...jinjaRules,
|
||||
],
|
||||
'jinja2-json-statement': [
|
||||
{
|
||||
token: 'keyword.control.jinja2',
|
||||
regex: /-?%\}/,
|
||||
next: 'pop',
|
||||
},
|
||||
...jinjaRules,
|
||||
],
|
||||
'jinja2-json-qstring': [
|
||||
{
|
||||
token: 'constant.language.escape',
|
||||
regex: /\\[\\'ntr]/,
|
||||
},
|
||||
{
|
||||
token: 'string',
|
||||
regex: "'",
|
||||
next: 'pop',
|
||||
},
|
||||
{
|
||||
defaultToken: 'string',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
this.normalizeRules()
|
||||
}
|
||||
|
||||
oop.inherits(Jinja2JsonHighlightRules, TextHighlightRules)
|
||||
exports.Jinja2JsonHighlightRules = Jinja2JsonHighlightRules
|
||||
},
|
||||
)
|
||||
|
||||
aceModule.define?.(
|
||||
'ace/mode/jinja2_json',
|
||||
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text', 'ace/mode/jinja2_json_highlight_rules'],
|
||||
(require: any, exports: any) => {
|
||||
const oop = require('../lib/oop')
|
||||
const TextMode = require('./text').Mode
|
||||
const Jinja2JsonHighlightRules = require('./jinja2_json_highlight_rules').Jinja2JsonHighlightRules
|
||||
|
||||
const Mode = function (this: any) {
|
||||
TextMode.call(this)
|
||||
this.HighlightRules = Jinja2JsonHighlightRules
|
||||
}
|
||||
|
||||
oop.inherits(Mode, TextMode)
|
||||
|
||||
;(function (this: any) {
|
||||
this.lineCommentStart = '//'
|
||||
this.blockComment = { start: '/*', end: '*/' }
|
||||
this.$id = 'ace/mode/jinja2_json'
|
||||
}).call(Mode.prototype)
|
||||
|
||||
exports.Mode = Mode
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function registerWordListMode() {
|
||||
aceModule.define?.(
|
||||
'ace/mode/word_list_highlight_rules',
|
||||
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules'],
|
||||
(require: any, exports: any) => {
|
||||
const oop = require('../lib/oop')
|
||||
const TextHighlightRules = require('./text_highlight_rules').TextHighlightRules
|
||||
|
||||
const WordListHighlightRules = function (this: any) {
|
||||
this.$rules = {
|
||||
start: [
|
||||
{
|
||||
token: 'comment.word-list',
|
||||
regex: /^#.*/,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
this.normalizeRules()
|
||||
}
|
||||
|
||||
oop.inherits(WordListHighlightRules, TextHighlightRules)
|
||||
exports.WordListHighlightRules = WordListHighlightRules
|
||||
},
|
||||
)
|
||||
|
||||
aceModule.define?.(
|
||||
'ace/mode/word_list',
|
||||
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text', 'ace/mode/word_list_highlight_rules'],
|
||||
(require: any, exports: any) => {
|
||||
const oop = require('../lib/oop')
|
||||
const TextMode = require('./text').Mode
|
||||
const WordListHighlightRules = require('./word_list_highlight_rules').WordListHighlightRules
|
||||
|
||||
const Mode = function (this: any) {
|
||||
TextMode.call(this)
|
||||
this.HighlightRules = WordListHighlightRules
|
||||
}
|
||||
|
||||
oop.inherits(Mode, TextMode)
|
||||
|
||||
;(function (this: any) {
|
||||
this.$id = 'ace/mode/word_list'
|
||||
}).call(Mode.prototype)
|
||||
|
||||
exports.Mode = Mode
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
|
||||
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
|
||||
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
||||
@@ -51,6 +587,8 @@ ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/css', modeCssUrl)
|
||||
ace.config.setModuleUrl('ace/mode/ini', modeIniUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github_dark', themeGithubDarkUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github_light_default', themeGithubLightDefaultUrl)
|
||||
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
|
||||
@@ -61,9 +599,11 @@ ace.config.setModuleUrl('ace/mode/yaml_worker', workerYamlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/css_worker', workerCssUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/html', snippetsHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/yaml', snippetsYamlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/ini', snippertsIniUrl)
|
||||
|
||||
registerJinja2Mode()
|
||||
registerWordListMode()
|
||||
ace.require('ace/ext/language_tools')
|
||||
|
||||
@@ -26,6 +26,11 @@ export const storageAttributes = [
|
||||
icon: 'mdi-server-network-outline',
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
type: 'smb',
|
||||
icon: 'mdi-folder-network-outline',
|
||||
remote: true,
|
||||
},
|
||||
]
|
||||
|
||||
export const storageIconDict = storageAttributes.reduce((dict, item) => {
|
||||
@@ -47,6 +52,10 @@ export const downloaderOptions = [
|
||||
value: 'transmission',
|
||||
title: i18n.global.t('setting.system.transmission'),
|
||||
},
|
||||
{
|
||||
value: 'rtorrent',
|
||||
title: i18n.global.t('setting.system.rtorrent'),
|
||||
},
|
||||
]
|
||||
|
||||
export const downloaderDict = downloaderOptions.reduce((dict, item) => {
|
||||
@@ -59,6 +68,10 @@ export const mediaServerOptions = [
|
||||
value: 'emby',
|
||||
title: i18n.global.t('setting.system.emby'),
|
||||
},
|
||||
{
|
||||
value: 'zspace',
|
||||
title: i18n.global.t('setting.system.zspace'),
|
||||
},
|
||||
{
|
||||
value: 'jellyfin',
|
||||
title: i18n.global.t('setting.system.jellyfin'),
|
||||
@@ -71,6 +84,10 @@ export const mediaServerOptions = [
|
||||
value: 'trimemedia',
|
||||
title: i18n.global.t('setting.system.trimeMedia'),
|
||||
},
|
||||
{
|
||||
value: 'ugreen',
|
||||
title: i18n.global.t('setting.system.ugreen'),
|
||||
},
|
||||
]
|
||||
|
||||
export const mediaServerDict = mediaServerOptions.reduce((dict, item) => {
|
||||
@@ -269,6 +286,10 @@ export const notificationSwitchOptions = [
|
||||
title: i18n.global.t('notificationSwitch.plugin'),
|
||||
value: '插件',
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('notificationSwitch.agent'),
|
||||
value: '智能体',
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('notificationSwitch.other'),
|
||||
value: '其它',
|
||||
@@ -339,6 +360,10 @@ export const actionStepOptions = [
|
||||
title: i18n.global.t('actionStep.invokePlugin'),
|
||||
value: '调用插件',
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('actionStep.note'),
|
||||
value: '备注',
|
||||
},
|
||||
]
|
||||
|
||||
// 操作步骤字典
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import axios from 'axios'
|
||||
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios'
|
||||
import router from '@/router'
|
||||
import { useAuthStore } from '@/stores'
|
||||
import { initializeRequestOptimizer } from '@/utils/requestOptimizer'
|
||||
import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
|
||||
import { getCurrentLocale } from '@/plugins/i18n'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
})
|
||||
|
||||
export interface ConnectionAwareRequestConfig extends AxiosRequestConfig {
|
||||
skipConnectionTracking?: boolean
|
||||
}
|
||||
|
||||
// 声明全局变量类型
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -17,6 +24,9 @@ declare global {
|
||||
// 将 API 实例暴露到全局,供插件使用
|
||||
window.MoviePilotAPI = api
|
||||
|
||||
// 初始化请求优化器(必须在其他拦截器之前)
|
||||
initializeRequestOptimizer(api)
|
||||
|
||||
// 添加请求拦截器
|
||||
api.interceptors.request.use(config => {
|
||||
// 认证 Store
|
||||
@@ -25,25 +35,86 @@ api.interceptors.request.use(config => {
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.token}`
|
||||
}
|
||||
const locale = getCurrentLocale()
|
||||
config.headers['X-MoviePilot-Locale'] = locale
|
||||
config.headers['Accept-Language'] = locale
|
||||
return config
|
||||
})
|
||||
|
||||
// 离线状态管理
|
||||
const globalOfflineStatus = useGlobalOfflineStatus()
|
||||
|
||||
/** 将 Axios 连接错误归类为全局服务探测可识别的原因。 */
|
||||
function resolveConnectionFailureReason(error: AxiosError): 'network-error' | 'timeout' | null {
|
||||
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') return 'timeout'
|
||||
|
||||
if (error.code === 'NETWORK_ERROR' || error.code === 'ERR_NETWORK' || error.name === 'NetworkError') {
|
||||
return 'network-error'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
interface LocalizedApiPayload {
|
||||
detail?: unknown
|
||||
detail_i18n?: unknown
|
||||
message?: unknown
|
||||
message_i18n?: unknown
|
||||
}
|
||||
|
||||
/** 前端展示默认使用后端提供的多语言消息,同时不改变后端接口兼容字段。 */
|
||||
function normalizeLocalizedMessage(payload: any): any {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload
|
||||
|
||||
const localizedPayload = payload as LocalizedApiPayload
|
||||
if (typeof localizedPayload.message_i18n === 'string' && localizedPayload.message_i18n) {
|
||||
localizedPayload.message = localizedPayload.message_i18n
|
||||
}
|
||||
if (typeof localizedPayload.detail_i18n === 'string' && localizedPayload.detail_i18n) {
|
||||
localizedPayload.detail = localizedPayload.detail_i18n
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
// 添加响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response.data
|
||||
// 任意 API 成功响应都可以证明 MoviePilot 服务当前可达。
|
||||
globalOfflineStatus.markServerOnline()
|
||||
return normalizeLocalizedMessage(response.data)
|
||||
},
|
||||
error => {
|
||||
(error: AxiosError) => {
|
||||
if (!error.response) {
|
||||
// 请求超时
|
||||
return Promise.reject(new Error(error))
|
||||
const requestConfig = error.config as ConnectionAwareRequestConfig | undefined
|
||||
const failureReason = resolveConnectionFailureReason(error)
|
||||
|
||||
// 普通请求失败只触发权威探测;探测请求自身失败由心跳管理器处理,避免递归。
|
||||
if (!requestConfig?.skipConnectionTracking && failureReason) {
|
||||
globalOfflineStatus.reportNetworkError(failureReason)
|
||||
}
|
||||
|
||||
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' || error.code === 'ETIMEDOUT') {
|
||||
// 请求超时
|
||||
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) {
|
||||
normalizeLocalizedMessage(error.response.data)
|
||||
// 认证 Store
|
||||
const authStore = useAuthStore()
|
||||
// 清除登录状态信息
|
||||
authStore.logout()
|
||||
// token验证失败,跳转到登录页面
|
||||
router.push('/login')
|
||||
} else {
|
||||
normalizeLocalizedMessage(error.response.data)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
|
||||
506
src/api/types.ts
@@ -46,8 +46,10 @@ export interface Subscribe {
|
||||
start_episode?: number
|
||||
// 缺失集数
|
||||
lack_episode?: number
|
||||
// 已完成集数(普通订阅 = 已入库集数,洗版订阅 = 起始集前 + [start, total] 范围内 priority==100 命中数)
|
||||
completed_episode?: number
|
||||
// 附加信息
|
||||
note?: string
|
||||
note?: string | number[]
|
||||
// 状态:N-新建 R-订阅中 P-待定 S-暂停
|
||||
state: string
|
||||
// 最后更新时间
|
||||
@@ -58,10 +60,14 @@ export interface Subscribe {
|
||||
sites: number[]
|
||||
// 是否洗版,数字或者boolean
|
||||
best_version: any
|
||||
// 是否只洗全集整包,数字或者boolean
|
||||
best_version_full?: any
|
||||
// 使用 imdbid 搜索
|
||||
search_imdbid?: any
|
||||
// 当前优先级
|
||||
current_priority: number
|
||||
// 洗版时已下载剧集的优先级状态
|
||||
episode_priority?: Record<string, number>
|
||||
// 保存目录
|
||||
save_path?: string
|
||||
// 时间
|
||||
@@ -144,6 +150,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 +320,8 @@ export interface MediaInfo {
|
||||
production_countries?: any[]
|
||||
// 语种
|
||||
spoken_languages?: string[]
|
||||
// 数字/实体发行日期
|
||||
release_dates?: MediaRelease[]
|
||||
// 状态
|
||||
status?: string
|
||||
// 标签
|
||||
@@ -332,6 +376,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 +540,7 @@ export interface SiteUserData {
|
||||
// 用户名
|
||||
username?: string
|
||||
// 用户ID
|
||||
userid?: number
|
||||
userid?: string
|
||||
// 用户等级
|
||||
user_level?: string
|
||||
// 加入时间
|
||||
@@ -594,6 +650,14 @@ export interface Plugin {
|
||||
has_page?: boolean
|
||||
// 是否有新版本
|
||||
has_update?: boolean
|
||||
// 主系统版本是否兼容
|
||||
system_version_compatible?: boolean
|
||||
// 主系统版本兼容提示
|
||||
system_version_message?: string
|
||||
// 主系统版本限定范围
|
||||
system_version?: string
|
||||
// 是否声明支持通过 GitHub Release 资产安装
|
||||
release?: boolean
|
||||
// 是否本地插件
|
||||
is_local?: boolean
|
||||
// 插件仓库地址
|
||||
@@ -606,6 +670,49 @@ export interface Plugin {
|
||||
page_open?: boolean
|
||||
}
|
||||
|
||||
// 插件 Release 可安装版本
|
||||
export interface PluginReleaseVersion {
|
||||
// 插件版本
|
||||
version: string
|
||||
// GitHub Release tag
|
||||
tag_name: string
|
||||
// Release 标题
|
||||
name?: string
|
||||
// 发布时间
|
||||
published_at?: string
|
||||
// Release 说明
|
||||
body?: string
|
||||
// 匹配到的资产文件名
|
||||
asset_name?: string
|
||||
// 是否为当前市场最新版本
|
||||
is_latest?: boolean
|
||||
// 是否为本地已安装版本
|
||||
is_current?: boolean
|
||||
}
|
||||
|
||||
// 插件 Release 可安装版本响应
|
||||
export interface PluginReleaseVersionsResponse {
|
||||
// 当前插件是否存在可直接安装的 Release 资产
|
||||
release_supported: boolean
|
||||
// 当前市场 package 声明的最新版本
|
||||
latest_version?: string | null
|
||||
// 本地已安装版本
|
||||
current_version?: string | null
|
||||
// 可安装版本列表
|
||||
items: PluginReleaseVersion[]
|
||||
}
|
||||
|
||||
// 插件侧栏全页导航项(与后端 PluginSidebarNavItem 对齐)
|
||||
export interface PluginSidebarNavItem {
|
||||
plugin_id: string
|
||||
nav_key: string
|
||||
title: string
|
||||
icon: string
|
||||
section: 'start' | 'discovery' | 'subscribe' | 'organize' | 'system'
|
||||
permission?: 'subscribe' | 'discovery' | 'search' | 'manage' | 'admin' | null
|
||||
order: number
|
||||
}
|
||||
|
||||
// 渲染结构
|
||||
export interface RenderProps {
|
||||
component: string
|
||||
@@ -629,6 +736,8 @@ export interface DashboardItem {
|
||||
attrs: { [key: string]: any }
|
||||
// col列数
|
||||
cols: { [key: string]: number }
|
||||
// Grid行数
|
||||
rows?: number
|
||||
// 页面元素
|
||||
elements: RenderProps[]
|
||||
// 渲染方式
|
||||
@@ -693,6 +802,58 @@ export interface TorrentInfo {
|
||||
category: string
|
||||
}
|
||||
|
||||
// 字幕信息
|
||||
export interface SubtitleInfo {
|
||||
// 站点ID
|
||||
site?: number
|
||||
// 站点名称
|
||||
site_name?: string
|
||||
// 站点Cookie
|
||||
site_cookie?: string
|
||||
// 站点UA
|
||||
site_ua?: string
|
||||
// 站点是否使用代理
|
||||
site_proxy?: boolean
|
||||
// 站点优先级
|
||||
site_order?: number
|
||||
// 字幕标题
|
||||
title?: string
|
||||
// 字幕描述
|
||||
description?: string
|
||||
// 字幕下载链接
|
||||
enclosure?: string
|
||||
// 详情页面
|
||||
page_url?: string
|
||||
// 语言
|
||||
language?: string
|
||||
// 语言图标
|
||||
language_icon?: string
|
||||
// 字幕大小
|
||||
size?: number
|
||||
// 发布时间
|
||||
pubdate?: string
|
||||
// 已过时间
|
||||
date_elapsed?: string
|
||||
// 点击/下载次数
|
||||
grabs?: number
|
||||
// 上传者
|
||||
uploader?: string
|
||||
// 举报页面
|
||||
report_url?: string
|
||||
// 种子ID
|
||||
torrent_id?: string
|
||||
// 字幕ID
|
||||
subtitle_id?: string
|
||||
// 下载文件名
|
||||
file_name?: string
|
||||
// 识别元数据
|
||||
meta_info?: MetaInfo
|
||||
// SxxExx
|
||||
season_episode?: string
|
||||
// 集列表
|
||||
episode_list?: number[]
|
||||
}
|
||||
|
||||
// 识别元数据
|
||||
export interface MetaInfo {
|
||||
// 是否处理的文件
|
||||
@@ -769,6 +930,8 @@ export interface MetaInfo {
|
||||
audio_term: string
|
||||
// 资源类型+特效
|
||||
edition: string
|
||||
// 流媒体平台
|
||||
web_source: string
|
||||
// 应用的自定义识别词
|
||||
apply_words: string[]
|
||||
}
|
||||
@@ -809,6 +972,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 {
|
||||
// 总空间
|
||||
@@ -823,10 +996,16 @@ export interface MediaStatistic {
|
||||
movie_count: number
|
||||
// 电视剧总数
|
||||
tv_count: number
|
||||
// 电视剧总集数
|
||||
episode_count: number
|
||||
// 电视剧总集数,未获取时为 null
|
||||
episode_count: number | null
|
||||
// 用户数量
|
||||
user_count: number
|
||||
// 本月新增电影数量
|
||||
movie_count_month: number
|
||||
// 本月新增电视剧数量
|
||||
tv_count_month: number
|
||||
// 本月新增剧集数量
|
||||
episode_count_month: number
|
||||
}
|
||||
|
||||
// 后台进程
|
||||
@@ -847,6 +1026,18 @@ export interface Process {
|
||||
memory: number
|
||||
}
|
||||
|
||||
// 仪表板系统摘要
|
||||
export interface DashboardSystemInfo {
|
||||
// 主机名称
|
||||
hostname: string
|
||||
// 操作系统名称
|
||||
operating_system: string
|
||||
// MoviePilot 运行时间,单位秒
|
||||
runtime: number
|
||||
// MoviePilot 后端版本
|
||||
version: string
|
||||
}
|
||||
|
||||
// 下载器信息
|
||||
export interface DownloaderInfo {
|
||||
// 下载速度
|
||||
@@ -861,18 +1052,86 @@ export interface DownloaderInfo {
|
||||
free_space: number
|
||||
}
|
||||
|
||||
// 仪表板系统内存信息
|
||||
export interface DashboardMemoryInfo {
|
||||
// 总内存字节数
|
||||
total: number
|
||||
// 已使用内存字节数,不包含缓存
|
||||
used: number
|
||||
// 缓存与缓冲区占用字节数
|
||||
cached: number
|
||||
// 可用内存字节数
|
||||
available: number
|
||||
// 已使用内存占总内存百分比,不包含缓存
|
||||
usage: number
|
||||
}
|
||||
|
||||
// 定时服务进度信息
|
||||
export interface ScheduleProgress {
|
||||
// ID
|
||||
id?: string
|
||||
// 名称
|
||||
name?: string
|
||||
// 多语言名称
|
||||
name_i18n?: string
|
||||
// 提供者
|
||||
provider?: string
|
||||
// 多语言提供者
|
||||
provider_i18n?: string
|
||||
// 是否正在执行
|
||||
enable?: boolean
|
||||
// 当前完成百分比
|
||||
value?: number
|
||||
// 当前进度文本
|
||||
text?: string
|
||||
// 多语言进度文本
|
||||
text_i18n?: string
|
||||
// 执行状态
|
||||
status?: string
|
||||
// 最近一次执行是否成功
|
||||
success?: boolean
|
||||
// 最近一次开始时间
|
||||
started_at?: string
|
||||
// 最近一次结束时间
|
||||
finished_at?: string
|
||||
// 最近一次错误信息
|
||||
error?: string
|
||||
// 多语言错误信息
|
||||
error_i18n?: string
|
||||
// 扩展数据
|
||||
data?: Record<string, unknown>
|
||||
}
|
||||
|
||||
// 定时服务信息
|
||||
export interface ScheduleInfo {
|
||||
// ID
|
||||
id: string
|
||||
// 名称
|
||||
name: string
|
||||
// 多语言名称
|
||||
name_i18n?: string
|
||||
// 提供者
|
||||
provider: string
|
||||
// 多语言提供者
|
||||
provider_i18n?: string
|
||||
// 状态
|
||||
status: string
|
||||
// 多语言状态
|
||||
status_i18n?: string
|
||||
// 下次运行时间
|
||||
next_run: string
|
||||
// 多语言下次运行时间
|
||||
next_run_i18n?: string
|
||||
// 当前完成百分比
|
||||
progress?: number
|
||||
// 进度文本
|
||||
progress_text?: string
|
||||
// 多语言进度文本
|
||||
progress_text_i18n?: string
|
||||
// 是否正在更新进度
|
||||
progress_enable?: boolean
|
||||
// 进度详情
|
||||
progress_detail?: ScheduleProgress
|
||||
}
|
||||
|
||||
// 消息通知
|
||||
@@ -940,6 +1199,10 @@ export interface FileItem {
|
||||
export interface MediaServerPlayItem {
|
||||
// ID
|
||||
id?: string | number
|
||||
// 媒体服务器项目ID
|
||||
item_id?: string | number
|
||||
// 媒体服务器ID
|
||||
server_id?: string
|
||||
// 标题
|
||||
title: string
|
||||
// 副标题
|
||||
@@ -952,6 +1215,10 @@ export interface MediaServerPlayItem {
|
||||
link?: string
|
||||
// 播放百分比
|
||||
percent?: number
|
||||
// 媒体服务器类型
|
||||
server_type?: string
|
||||
// 图片是否需要Cookies
|
||||
use_cookies?: boolean
|
||||
}
|
||||
|
||||
// 媒体服务器媒体库
|
||||
@@ -960,22 +1227,38 @@ export interface MediaServerLibrary {
|
||||
server: string
|
||||
// ID
|
||||
id?: string | number
|
||||
// 媒体服务器项目ID
|
||||
item_id?: string | number
|
||||
// 媒体服务器ID
|
||||
server_id?: string
|
||||
// 名称
|
||||
name: string
|
||||
// 路径
|
||||
path?: string
|
||||
// 类型
|
||||
type?: string
|
||||
// 库内媒体数量
|
||||
item_count?: number
|
||||
// 图片
|
||||
image?: string
|
||||
// 图片列表
|
||||
image_list?: string[]
|
||||
// 链接
|
||||
link?: string
|
||||
// 媒体服务器类型
|
||||
server_type?: string
|
||||
// 图片是否需要Cookies
|
||||
use_cookies?: boolean
|
||||
}
|
||||
|
||||
// 消息通知
|
||||
export interface Message {
|
||||
// 消息ID
|
||||
id?: number
|
||||
// 消息渠道
|
||||
channel?: string
|
||||
// 消息来源
|
||||
source?: string
|
||||
// 消息类型
|
||||
mtype?: string
|
||||
// 消息标题
|
||||
@@ -995,19 +1278,15 @@ export interface Message {
|
||||
// 消息方向:0-接收,1-发送
|
||||
action?: number
|
||||
// JSON
|
||||
note?: string
|
||||
note?: string | any[] | Record<string, any>
|
||||
}
|
||||
|
||||
// 系统通知
|
||||
export interface SystemNotification {
|
||||
// 通知类型 user/system/plugin
|
||||
type: string
|
||||
// 通知标题
|
||||
title: string
|
||||
// 通知内容
|
||||
text: string
|
||||
export interface SystemNotification extends Message {
|
||||
// 通知类型 user/system/plugin/notification
|
||||
type?: string
|
||||
// 通知时间
|
||||
date: string
|
||||
date?: string
|
||||
// 是否已读
|
||||
read?: boolean
|
||||
}
|
||||
@@ -1024,6 +1303,8 @@ export interface DownloaderConf {
|
||||
config: { [key: string]: any }
|
||||
// 是否启用
|
||||
enabled: boolean
|
||||
// 路径映射
|
||||
path_mapping?: Array<[storagePath: string, downloadPath: string]>
|
||||
}
|
||||
|
||||
// 通知配置
|
||||
@@ -1062,7 +1343,7 @@ export interface StorageConf {
|
||||
export interface MediaServerConf {
|
||||
// 名称
|
||||
name: string
|
||||
// 类型 emby/jellyfin/plex
|
||||
// 类型 emby/zspace/jellyfin/plex/trimemedia/ugreen
|
||||
type: string
|
||||
// 配置
|
||||
config: { [key: string]: any }
|
||||
@@ -1144,6 +1425,28 @@ export interface FilterRuleGroup {
|
||||
category?: string
|
||||
}
|
||||
|
||||
// 规则测试结果
|
||||
export interface RuleTestData {
|
||||
// 原始标题
|
||||
title?: string
|
||||
// 原始副标题
|
||||
subtitle?: string
|
||||
// 规则组名称
|
||||
rulegroup_name?: string
|
||||
// 规则组详情
|
||||
rulegroup?: FilterRuleGroup | null
|
||||
// 识别元数据
|
||||
meta_info?: MetaInfo | null
|
||||
// 媒体信息
|
||||
media_info?: MediaInfo | null
|
||||
// 种子信息
|
||||
torrent_info?: TorrentInfo | null
|
||||
// 优先级
|
||||
priority?: number | null
|
||||
// 是否命中过滤规则
|
||||
matched?: boolean
|
||||
}
|
||||
|
||||
// 订阅下载文件详情
|
||||
export interface SubscribeDownloadFileInfo {
|
||||
// 种子名称
|
||||
@@ -1164,6 +1467,12 @@ export interface SubscribeLibraryFileInfo {
|
||||
storage?: string
|
||||
// 文件路径
|
||||
file_path?: string
|
||||
// 媒体服务器名称
|
||||
server?: string
|
||||
// 媒体服务器类型:emby、jellyfin、plex 等
|
||||
server_type?: string
|
||||
// 媒体服务器条目 ID
|
||||
itemid?: string
|
||||
}
|
||||
|
||||
// 订阅集详情
|
||||
@@ -1195,9 +1504,9 @@ export interface TransferForm {
|
||||
// 历史ID
|
||||
logid: number
|
||||
// 目标存储
|
||||
target_storage: string
|
||||
target_storage: string | null
|
||||
// 目标路径
|
||||
target_path: string
|
||||
target_path: string | null
|
||||
// TMDB ID
|
||||
tmdbid?: number
|
||||
// 豆瓣 ID
|
||||
@@ -1207,7 +1516,7 @@ export interface TransferForm {
|
||||
// 类型
|
||||
type_name?: string
|
||||
// 整理方式
|
||||
transfer_type: string
|
||||
transfer_type: string | null
|
||||
// 自定义格式
|
||||
episode_format?: string
|
||||
// 指定集数
|
||||
@@ -1219,15 +1528,95 @@ export interface TransferForm {
|
||||
// 最小文件大小
|
||||
min_filesize: number
|
||||
// 刮削
|
||||
scrape: boolean
|
||||
scrape: boolean | null
|
||||
// 复用历史识别信息
|
||||
from_history: boolean
|
||||
// 媒体库类型子目录
|
||||
library_type_folder?: boolean
|
||||
library_type_folder?: boolean | null
|
||||
// 媒体库类别子目录
|
||||
library_category_folder?: boolean
|
||||
library_category_folder?: boolean | null
|
||||
// 剧集组编号
|
||||
episode_group?: string
|
||||
episode_group?: string | null
|
||||
// 预览模式
|
||||
preview?: boolean
|
||||
}
|
||||
|
||||
// 手动整理请求
|
||||
export interface ManualTransferPayload extends Omit<TransferForm, 'fileitem'> {
|
||||
// 文件项
|
||||
fileitem?: FileItem
|
||||
// 多选文件批量请求
|
||||
fileitems?: FileItem[]
|
||||
}
|
||||
|
||||
// 手动整理目的路径匹配结果
|
||||
export interface ManualTransferTargetPathData {
|
||||
// 目标存储
|
||||
target_storage?: string | null
|
||||
// 目标路径
|
||||
target_path?: string | null
|
||||
// 整理方式
|
||||
transfer_type?: string | null
|
||||
// 刮削
|
||||
scrape?: boolean | null
|
||||
// 媒体库类型子目录
|
||||
library_type_folder?: boolean | null
|
||||
// 媒体库类别子目录
|
||||
library_category_folder?: boolean | null
|
||||
}
|
||||
|
||||
// 手动整理预览统计
|
||||
export interface ManualTransferPreviewSummary {
|
||||
// 总数
|
||||
total: number
|
||||
// 成功数
|
||||
success: number
|
||||
// 失败数
|
||||
failed: number
|
||||
}
|
||||
|
||||
// 手动整理预览项
|
||||
export interface ManualTransferPreviewItem {
|
||||
// 原始路径
|
||||
source?: string
|
||||
// 目标路径
|
||||
target?: string
|
||||
// 目标目录
|
||||
target_dir?: string
|
||||
// 是否成功
|
||||
success?: boolean
|
||||
// 提示信息
|
||||
message?: string
|
||||
// 媒体类型
|
||||
type?: string
|
||||
// 媒体标题
|
||||
title?: string
|
||||
// 季
|
||||
season?: number | string
|
||||
// 开始集
|
||||
episode?: number | string
|
||||
// 结束集
|
||||
episode_end?: number | string
|
||||
// Part
|
||||
part?: string
|
||||
// 原始识别字符串
|
||||
org_string?: string
|
||||
// 应用的自定义识别词
|
||||
apply_words?: string[]
|
||||
// 制作组/字幕组
|
||||
resource_team?: string
|
||||
// 自定义占位符
|
||||
customization?: string
|
||||
}
|
||||
|
||||
// 手动整理预览数据
|
||||
export interface ManualTransferPreviewData {
|
||||
// 统计信息
|
||||
summary: ManualTransferPreviewSummary
|
||||
// 预览结果
|
||||
items: ManualTransferPreviewItem[]
|
||||
// 额外消息
|
||||
message?: string
|
||||
}
|
||||
|
||||
// 整理队列
|
||||
@@ -1290,6 +1679,10 @@ export interface Workflow {
|
||||
description?: string
|
||||
// 定时器
|
||||
timer?: string
|
||||
// 触发类型:timer-定时触发 event-事件触发 manual-手动触发
|
||||
trigger_type?: string
|
||||
// 事件类型(当trigger_type为event时使用)
|
||||
event_type?: string
|
||||
// 状态
|
||||
state?: string
|
||||
// 当前执行动作
|
||||
@@ -1302,6 +1695,10 @@ export interface Workflow {
|
||||
actions?: any[]
|
||||
// 动作流
|
||||
flows?: any[]
|
||||
// 工作流执行配置
|
||||
execution_config?: { [key: string]: any }
|
||||
// 工作流结构化执行状态
|
||||
execution_state?: { [key: string]: any }
|
||||
// 创建时间
|
||||
add_time?: string
|
||||
// 最后执行时间
|
||||
@@ -1353,3 +1750,70 @@ export interface TorrentCacheData {
|
||||
// 缓存数据
|
||||
data: TorrentCacheItem[]
|
||||
}
|
||||
|
||||
// 媒体识别缓存项
|
||||
export interface RecognitionCacheItem {
|
||||
// 缓存键
|
||||
key: string
|
||||
// TMDB ID,0 表示未识别
|
||||
tmdb_id?: number
|
||||
// 豆瓣 ID,0 表示未识别
|
||||
douban_id?: string | number
|
||||
// 当前识别数据源对应的统一 ID
|
||||
recognition_id?: string
|
||||
// 识别后的标题
|
||||
title: string
|
||||
// 识别后的年份
|
||||
year: string
|
||||
// 媒体类型
|
||||
media_type: string
|
||||
// TMDB 海报相对路径
|
||||
poster_path?: string
|
||||
// TMDB 背景图相对路径
|
||||
backdrop_path?: string
|
||||
}
|
||||
|
||||
// 媒体识别缓存数据
|
||||
export interface RecognitionCacheData {
|
||||
// 缓存总数
|
||||
count: number
|
||||
// 已识别数量
|
||||
recognized: number
|
||||
// 未识别数量
|
||||
unrecognized: number
|
||||
// 缓存数据
|
||||
data: RecognitionCacheItem[]
|
||||
}
|
||||
|
||||
// 订阅分享统计
|
||||
export interface SubscribeShareStatistics {
|
||||
// 分享人
|
||||
share_user?: string
|
||||
// 分享数量
|
||||
share_count?: number
|
||||
// 总复用人次
|
||||
total_reuse_count?: number
|
||||
}
|
||||
|
||||
// 通用API响应
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
message?: string
|
||||
message_i18n?: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 分类规则
|
||||
export interface CategoryRule {
|
||||
genre_ids?: string
|
||||
original_language?: string
|
||||
production_countries?: string
|
||||
origin_country?: string
|
||||
release_year?: string
|
||||
}
|
||||
|
||||
// 分类配置
|
||||
export interface CategoryConfig {
|
||||
movie?: { [key: string]: CategoryRule }
|
||||
tv?: { [key: string]: CategoryRule }
|
||||
}
|
||||
|
||||
BIN
src/assets/images/logos/clawbot.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/assets/images/logos/discord.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
src/assets/images/logos/feishu.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/images/logos/qq.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/logos/rtorrent.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/logos/ugreen.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/images/logos/zspace.webp
Normal file
|
After Width: | Height: | Size: 7.2 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 |
@@ -1,249 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
interface Props {
|
||||
errorCode?: string
|
||||
errorTitle?: string
|
||||
errorDescription?: string
|
||||
icon?: string
|
||||
iconColor?: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<div class="error-title">
|
||||
{{ props.errorTitle || t('common.noData') }}
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div class="error-description">
|
||||
{{ props.errorDescription || t('common.noContent') }}
|
||||
</div>
|
||||
|
||||
<!-- 按钮插槽 -->
|
||||
<div class="actions-container">
|
||||
<slot name="button" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.no-data-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: 100%;
|
||||
min-block-size: 300px;
|
||||
padding-block: 3rem;
|
||||
padding-inline: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.icon-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
block-size: 100px;
|
||||
inline-size: 100px;
|
||||
margin-block: 0 2rem;
|
||||
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;
|
||||
margin-block-end: 0.75rem;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 5%);
|
||||
}
|
||||
|
||||
.error-title::after {
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
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;
|
||||
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;
|
||||
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>
|
||||
1864
src/components/agent/AgentAssistantEntry.vue
Normal file
3392
src/components/agent/AgentAssistantPanel.vue
Normal file
28
src/components/agent/AgentAssistantWidget.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import AgentAssistantEntry from './AgentAssistantEntry.vue'
|
||||
import AgentAssistantPanel from './AgentAssistantPanel.vue'
|
||||
|
||||
type AgentAssistantEntryRef = InstanceType<typeof AgentAssistantEntry>
|
||||
|
||||
const panelOpen = ref(false)
|
||||
const thinking = ref(false)
|
||||
const entryRef = ref<AgentAssistantEntryRef | null>(null)
|
||||
|
||||
// 打开 Agent 面板并清空入口预览气泡。
|
||||
function openPanel() {
|
||||
panelOpen.value = true
|
||||
entryRef.value?.clearBubbles()
|
||||
}
|
||||
|
||||
// 在面板关闭时展示助手回复预览。
|
||||
function handleAssistantPreview(value: string) {
|
||||
if (panelOpen.value) return
|
||||
|
||||
entryRef.value?.showAssistantReplyPreview(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AgentAssistantEntry ref="entryRef" :active="!panelOpen" :thinking="thinking" @open="openPanel" />
|
||||
<AgentAssistantPanel v-model="panelOpen" @assistant-preview="handleAssistantPreview" @thinking-change="thinking = $event" />
|
||||
</template>
|
||||
28
src/components/agent/pet/AgentPetStage.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import CssRobotRenderer from './renderers/CssRobotRenderer.vue'
|
||||
import type { AgentPetActionName, AgentPetIntent, AgentPetRendererKind } from './types'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
action?: AgentPetActionName | null
|
||||
intent?: AgentPetIntent
|
||||
renderer?: AgentPetRendererKind
|
||||
thinking?: boolean
|
||||
}>(),
|
||||
{
|
||||
action: null,
|
||||
intent: 'idle',
|
||||
renderer: 'css-robot',
|
||||
thinking: false,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CssRobotRenderer
|
||||
v-if="props.renderer === 'css-robot'"
|
||||
:action="props.action"
|
||||
:intent="props.intent"
|
||||
:thinking="props.thinking"
|
||||
/>
|
||||
</template>
|
||||
156
src/components/agent/pet/agentPetActions.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { AgentPetActionDefinition, AgentPetActionName } from './types'
|
||||
|
||||
export const AGENT_PET_RANDOM_ACTION_MIN_DELAY = 8000
|
||||
export const AGENT_PET_RANDOM_ACTION_MAX_DELAY = 18000
|
||||
|
||||
export const AGENT_PET_RANDOM_ACTIONS = [
|
||||
'wave',
|
||||
'sit',
|
||||
'eye-roll',
|
||||
'faint',
|
||||
'disassemble',
|
||||
'happy-jump',
|
||||
'sleep',
|
||||
'stretch',
|
||||
'peek',
|
||||
'scan',
|
||||
'charge',
|
||||
'spin-cheer',
|
||||
'shy',
|
||||
'confused',
|
||||
] as const
|
||||
|
||||
export const AGENT_PET_ACTIONS: Record<AgentPetActionName, AgentPetActionDefinition> = {
|
||||
wave: {
|
||||
name: 'wave',
|
||||
intent: 'reaction',
|
||||
clip: 'agent-fab-action-wave',
|
||||
duration: 2450,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
sit: {
|
||||
name: 'sit',
|
||||
intent: 'idle',
|
||||
clip: 'agent-fab-action-sit',
|
||||
duration: 4200,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
'eye-roll': {
|
||||
name: 'eye-roll',
|
||||
intent: 'reaction',
|
||||
clip: 'agent-fab-action-eye-roll',
|
||||
duration: 1900,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
faint: {
|
||||
name: 'faint',
|
||||
intent: 'reaction',
|
||||
clip: 'agent-fab-action-faint',
|
||||
duration: 4800,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
disassemble: {
|
||||
name: 'disassemble',
|
||||
intent: 'reaction',
|
||||
clip: 'agent-fab-action-disassemble',
|
||||
duration: 6200,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
'happy-jump': {
|
||||
name: 'happy-jump',
|
||||
intent: 'success',
|
||||
clip: 'agent-fab-action-happy-jump',
|
||||
duration: 5200,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
sleep: {
|
||||
name: 'sleep',
|
||||
intent: 'sleeping',
|
||||
clip: 'agent-fab-action-sleep',
|
||||
duration: 5800,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
stretch: {
|
||||
name: 'stretch',
|
||||
intent: 'idle',
|
||||
clip: 'agent-fab-action-stretch',
|
||||
duration: 4400,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
peek: {
|
||||
name: 'peek',
|
||||
intent: 'reaction',
|
||||
clip: 'agent-fab-action-peek',
|
||||
duration: 2800,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
scan: {
|
||||
name: 'scan',
|
||||
intent: 'thinking',
|
||||
clip: 'agent-fab-action-scan',
|
||||
duration: 3200,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
charge: {
|
||||
name: 'charge',
|
||||
intent: 'thinking',
|
||||
clip: 'agent-fab-action-charge',
|
||||
duration: 4800,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
'spin-cheer': {
|
||||
name: 'spin-cheer',
|
||||
intent: 'success',
|
||||
clip: 'agent-fab-action-spin-cheer',
|
||||
duration: 3600,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
shy: {
|
||||
name: 'shy',
|
||||
intent: 'reaction',
|
||||
clip: 'agent-fab-action-shy',
|
||||
duration: 3000,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
confused: {
|
||||
name: 'confused',
|
||||
intent: 'reaction',
|
||||
clip: 'agent-fab-action-confused',
|
||||
duration: 3400,
|
||||
priority: 1,
|
||||
interruptible: true,
|
||||
},
|
||||
}
|
||||
|
||||
/** 获取指定宠物动作的播放时长。 */
|
||||
export function getAgentPetActionDuration(action: AgentPetActionName) {
|
||||
return AGENT_PET_ACTIONS[action].duration
|
||||
}
|
||||
|
||||
/** 生成下一次空闲趣味动作的随机等待时间。 */
|
||||
export function getAgentPetRandomActionDelay() {
|
||||
return (
|
||||
AGENT_PET_RANDOM_ACTION_MIN_DELAY +
|
||||
Math.round(Math.random() * (AGENT_PET_RANDOM_ACTION_MAX_DELAY - AGENT_PET_RANDOM_ACTION_MIN_DELAY))
|
||||
)
|
||||
}
|
||||
|
||||
/** 从动作池中选择一个不同于上一次的趣味动作。 */
|
||||
export function pickAgentPetRandomAction(lastAction: AgentPetActionName | null): AgentPetActionName {
|
||||
const candidates = AGENT_PET_RANDOM_ACTIONS.filter(action => action !== lastAction)
|
||||
|
||||
return candidates[Math.floor(Math.random() * candidates.length)] || AGENT_PET_RANDOM_ACTIONS[0]
|
||||
}
|
||||
44
src/components/agent/pet/renderers/CssRobotRenderer.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { AgentPetActionName, AgentPetIntent } from '../types'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
action?: AgentPetActionName | null
|
||||
intent?: AgentPetIntent
|
||||
thinking?: boolean
|
||||
}>(),
|
||||
{
|
||||
action: null,
|
||||
intent: 'idle',
|
||||
thinking: false,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="agent-assistant-fab__bot"
|
||||
:data-agent-pet-action="props.action || undefined"
|
||||
:data-agent-pet-intent="props.intent"
|
||||
:data-agent-pet-thinking="props.thinking ? 'true' : undefined"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="agent-assistant-fab__antenna" />
|
||||
<span class="agent-assistant-fab__head">
|
||||
<span class="agent-assistant-fab__face">
|
||||
<span class="agent-assistant-fab__eye agent-assistant-fab__eye--left" />
|
||||
<span class="agent-assistant-fab__eye agent-assistant-fab__eye--right" />
|
||||
<span class="agent-assistant-fab__smile" />
|
||||
</span>
|
||||
</span>
|
||||
<span class="agent-assistant-fab__body">
|
||||
<span class="agent-assistant-fab__core" />
|
||||
</span>
|
||||
<span class="agent-assistant-fab__arm agent-assistant-fab__arm--left" />
|
||||
<span class="agent-assistant-fab__arm agent-assistant-fab__arm--right" />
|
||||
<span class="agent-assistant-fab__leg agent-assistant-fab__leg--left" />
|
||||
<span class="agent-assistant-fab__leg agent-assistant-fab__leg--right" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style src="./cssRobot.scss" lang="scss"></style>
|
||||
100
src/components/agent/pet/renderers/actions/_charge.scss
Normal file
@@ -0,0 +1,100 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable no-duplicate-selectors */
|
||||
|
||||
.agent-assistant-fab.is-action-charge .agent-assistant-fab__bot {
|
||||
animation: agent-fab-action-charge-bot 4.8s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-charge .agent-assistant-fab__antenna {
|
||||
animation: agent-fab-action-charge-antenna 4.8s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-charge .agent-assistant-fab__face {
|
||||
animation: agent-fab-action-charge-face 4.8s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-charge .agent-assistant-fab__core {
|
||||
animation: agent-fab-action-charge-core 4.8s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-charge .agent-assistant-fab__arm--left,
|
||||
.agent-assistant-fab.is-action-charge .agent-assistant-fab__arm--right {
|
||||
animation: agent-fab-action-charge-arm 4.8s ease-in-out both;
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-charge-bot {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(var(--agent-assistant-bot-scale)) rotate(var(--agent-assistant-robot-tilt));
|
||||
}
|
||||
|
||||
18%,
|
||||
42%,
|
||||
66% {
|
||||
transform: translateY(0.1rem) scale(var(--agent-assistant-bot-scale))
|
||||
rotate(calc(var(--agent-assistant-robot-tilt) - 2deg));
|
||||
}
|
||||
|
||||
30%,
|
||||
54%,
|
||||
78% {
|
||||
transform: translateY(-0.12rem) scale(var(--agent-assistant-bot-scale))
|
||||
rotate(calc(var(--agent-assistant-robot-tilt) + 2deg));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-charge-antenna {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y)) rotate(22deg);
|
||||
}
|
||||
|
||||
30%,
|
||||
54%,
|
||||
78% {
|
||||
transform: translate(var(--agent-assistant-head-x), calc(var(--agent-assistant-head-y) - 0.16rem)) rotate(-20deg)
|
||||
scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-charge-face {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: inset 0 0.1rem 0 rgba(255, 255, 255, 8%);
|
||||
}
|
||||
|
||||
20%,
|
||||
84% {
|
||||
box-shadow:
|
||||
inset 0 0.1rem 0 rgba(255, 255, 255, 8%),
|
||||
0 0 0.9rem rgba(166, 118, 255, 56%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-charge-core {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.78;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
20%,
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
opacity: 1;
|
||||
transform: scale(1.32);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-charge-arm {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
22%,
|
||||
82% {
|
||||
transform: translateY(0.12rem) scaleY(0.9);
|
||||
}
|
||||
}
|
||||
101
src/components/agent/pet/renderers/actions/_confused.scss
Normal file
@@ -0,0 +1,101 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable no-duplicate-selectors */
|
||||
|
||||
.agent-assistant-fab.is-action-confused .agent-assistant-fab__antenna {
|
||||
animation: agent-fab-action-confused-antenna 3.4s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-confused .agent-assistant-fab__head {
|
||||
animation: agent-fab-action-confused-head 3.4s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-confused .agent-assistant-fab__eye {
|
||||
animation:
|
||||
agent-fab-blink 4.8s ease-in-out infinite,
|
||||
agent-fab-action-confused-eye 0.85s ease-in-out 4;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-confused .agent-assistant-fab__arm--left {
|
||||
animation: agent-fab-action-confused-arm-left 3.4s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-confused .agent-assistant-fab__arm--right {
|
||||
animation: agent-fab-action-confused-arm-right 3.4s ease-in-out both;
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-confused-antenna {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y)) rotate(22deg);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y)) rotate(58deg);
|
||||
}
|
||||
|
||||
48% {
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y)) rotate(-28deg);
|
||||
}
|
||||
|
||||
74% {
|
||||
transform: translate(var(--agent-assistant-head-x), calc(var(--agent-assistant-head-y) - 0.06rem)) rotate(42deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-confused-head {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y)) rotate(0deg);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(var(--agent-assistant-head-x), calc(var(--agent-assistant-head-y) - 0.06rem)) rotate(-10deg);
|
||||
}
|
||||
|
||||
48% {
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y)) rotate(10deg);
|
||||
}
|
||||
|
||||
74% {
|
||||
transform: translate(var(--agent-assistant-head-x), calc(var(--agent-assistant-head-y) - 0.03rem)) rotate(-6deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-confused-eye {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(var(--agent-assistant-eye-x), var(--agent-assistant-eye-y));
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translate(0.22rem, -0.16rem);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translate(-0.22rem, 0.08rem);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-confused-arm-left {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(17deg);
|
||||
}
|
||||
|
||||
24%,
|
||||
74% {
|
||||
transform: translate(-0.12rem, -0.12rem) rotate(88deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-confused-arm-right {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-17deg);
|
||||
}
|
||||
|
||||
24%,
|
||||
74% {
|
||||
transform: translate(0.16rem, -0.08rem) rotate(-42deg);
|
||||
}
|
||||
}
|
||||
195
src/components/agent/pet/renderers/actions/_disassemble.scss
Normal file
@@ -0,0 +1,195 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable no-duplicate-selectors */
|
||||
|
||||
.agent-assistant-fab.is-action-disassemble .agent-assistant-fab__bot {
|
||||
animation: agent-fab-action-disassemble-bot 6.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-disassemble .agent-assistant-fab__antenna {
|
||||
animation: agent-fab-action-disassemble-antenna 6.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-disassemble .agent-assistant-fab__head {
|
||||
animation: agent-fab-action-disassemble-head 6.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-disassemble .agent-assistant-fab__body {
|
||||
animation: agent-fab-action-disassemble-body 6.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-disassemble .agent-assistant-fab__arm--left {
|
||||
animation: agent-fab-action-disassemble-arm-left 6.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-disassemble .agent-assistant-fab__arm--right {
|
||||
animation: agent-fab-action-disassemble-arm-right 6.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-disassemble .agent-assistant-fab__leg--left {
|
||||
animation: agent-fab-action-disassemble-leg-left 6.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-action-disassemble .agent-assistant-fab__leg--right {
|
||||
animation: agent-fab-action-disassemble-leg-right 6.2s ease-in-out both;
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-disassemble-bot {
|
||||
/* 34%-70% 让散落状态停留片刻,再用 84% 的轻微过冲表现回装吸附感。 */
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(var(--agent-assistant-bot-scale)) rotate(var(--agent-assistant-robot-tilt));
|
||||
}
|
||||
|
||||
12% {
|
||||
transform: translateY(-0.08rem) scale(var(--agent-assistant-bot-scale)) rotate(-5deg);
|
||||
}
|
||||
|
||||
24%,
|
||||
72% {
|
||||
transform: translateY(0.18rem) scale(var(--agent-assistant-bot-scale)) rotate(0deg);
|
||||
}
|
||||
|
||||
86% {
|
||||
transform: translateY(-0.04rem) scale(var(--agent-assistant-bot-scale)) rotate(3deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-disassemble-antenna {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y)) rotate(22deg);
|
||||
}
|
||||
|
||||
16% {
|
||||
transform: translate(0.02rem, -0.14rem) rotate(44deg);
|
||||
}
|
||||
|
||||
32%,
|
||||
70% {
|
||||
transform: translate(0.96rem, 3.42rem) rotate(268deg);
|
||||
}
|
||||
|
||||
84% {
|
||||
transform: translate(-0.05rem, -0.08rem) rotate(8deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-disassemble-head {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y)) rotate(0deg);
|
||||
}
|
||||
|
||||
16% {
|
||||
transform: translate(var(--agent-assistant-head-x), calc(var(--agent-assistant-head-y) - 0.24rem)) rotate(-8deg);
|
||||
}
|
||||
|
||||
32%,
|
||||
70% {
|
||||
transform: translate(-1.18rem, 2.72rem) rotate(-46deg);
|
||||
}
|
||||
|
||||
84% {
|
||||
transform: translate(0.08rem, -0.1rem) rotate(6deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-disassemble-body {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(var(--agent-assistant-body-x), var(--agent-assistant-body-y));
|
||||
}
|
||||
|
||||
18% {
|
||||
transform: translate(var(--agent-assistant-body-x), calc(var(--agent-assistant-body-y) + 0.08rem)) rotate(4deg);
|
||||
}
|
||||
|
||||
34%,
|
||||
70% {
|
||||
transform: translate(0.26rem, 1.56rem) rotate(18deg);
|
||||
}
|
||||
|
||||
84% {
|
||||
transform: translate(-0.05rem, -0.08rem) rotate(-5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-disassemble-arm-left {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(17deg);
|
||||
}
|
||||
|
||||
18% {
|
||||
transform: translate(-0.16rem, -0.08rem) rotate(78deg);
|
||||
}
|
||||
|
||||
34%,
|
||||
70% {
|
||||
transform: translate(-1.72rem, 1.84rem) rotate(248deg);
|
||||
}
|
||||
|
||||
84% {
|
||||
transform: translate(0.08rem, -0.04rem) rotate(-4deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-disassemble-arm-right {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-17deg);
|
||||
}
|
||||
|
||||
18% {
|
||||
transform: translate(0.16rem, -0.08rem) rotate(-78deg);
|
||||
}
|
||||
|
||||
34%,
|
||||
70% {
|
||||
transform: translate(1.58rem, 1.72rem) rotate(-238deg);
|
||||
}
|
||||
|
||||
84% {
|
||||
transform: translate(-0.08rem, -0.04rem) rotate(4deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-disassemble-leg-left {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(-0.12rem, 0.18rem) rotate(34deg);
|
||||
}
|
||||
|
||||
34%,
|
||||
70% {
|
||||
transform: translate(-0.98rem, 0.96rem) rotate(112deg);
|
||||
}
|
||||
|
||||
84% {
|
||||
transform: translate(0.07rem, -0.08rem) rotate(-8deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-fab-action-disassemble-leg-right {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(0.12rem, 0.18rem) rotate(-34deg);
|
||||
}
|
||||
|
||||
34%,
|
||||
70% {
|
||||
transform: translate(0.92rem, 1.04rem) rotate(-118deg);
|
||||
}
|
||||
|
||||
84% {
|
||||
transform: translate(-0.07rem, -0.08rem) rotate(8deg);
|
||||
}
|
||||
}
|
||||