Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9cf111a86 | ||
|
|
99f8914830 | ||
|
|
6da877709d | ||
|
|
c4948dc11f | ||
|
|
3f243129a4 | ||
|
|
eb2cae7a77 | ||
|
|
a2bbcd3939 | ||
|
|
0650c4bc26 | ||
|
|
7c32bf97ae | ||
|
|
70aecc1f0c | ||
|
|
5bd5c7d2c5 | ||
|
|
d6dce4021d | ||
|
|
ccfdb99348 | ||
|
|
2ca712435a | ||
|
|
0af8e34a8f | ||
|
|
f2583e152a | ||
|
|
cf70bd2dc1 | ||
|
|
8f50af917f | ||
|
|
4bdde365cf | ||
|
|
7f1bc33eed | ||
|
|
d0d24aba4e | ||
|
|
0d68c0d683 | ||
|
|
e1ab735823 | ||
|
|
bb67a8cbbb | ||
|
|
c8a7ac7dc6 | ||
|
|
788a294200 | ||
|
|
f34935a634 | ||
|
|
a7331256fc | ||
|
|
8fa3941db9 | ||
|
|
ada120c1b9 | ||
|
|
72f7168893 | ||
|
|
75b70a26bd | ||
|
|
4433e5d8e5 | ||
|
|
351b2e1417 | ||
|
|
3c7dc5a49e | ||
|
|
9895231696 | ||
|
|
3fa3281c51 | ||
|
|
bbf11d0374 | ||
|
|
04ee696f6a | ||
|
|
ddbbe92a85 | ||
|
|
32ba5e1802 | ||
|
|
a1c6eb177a | ||
|
|
14d60264c7 | ||
|
|
f4d6abace7 | ||
|
|
a5e1717138 | ||
|
|
472a4d400b | ||
|
|
fda4786737 | ||
|
|
c3686579b2 | ||
|
|
f2dfb2f32d | ||
|
|
60ee9f1a15 | ||
|
|
f43d1af6a7 | ||
|
|
b631b4c7b7 | ||
|
|
e3aba0529f | ||
|
|
cf682feae9 | ||
|
|
03d798760c | ||
|
|
51450c2b02 | ||
|
|
751ddbb3f3 | ||
|
|
36b3620d0c | ||
|
|
fd3eb513ff | ||
|
|
af0aaddbfd | ||
|
|
b167170aa9 | ||
|
|
642ac3ebc4 | ||
|
|
69cceb4fc7 | ||
|
|
ac32e23e87 | ||
|
|
04de4ff606 | ||
|
|
27761566c9 | ||
|
|
3d93eda92d | ||
|
|
6943571a6d | ||
|
|
28994cb9da | ||
|
|
5866634b3f | ||
|
|
e3d91f1cc7 | ||
|
|
8ac06259f5 | ||
|
|
c8933a6669 | ||
|
|
6c2da264db | ||
|
|
993f01a301 | ||
|
|
9e29e7283c | ||
|
|
7da106798d | ||
|
|
b2e9cbb44a | ||
|
|
143f6df2c1 | ||
|
|
a64505fcc1 | ||
|
|
0aaf919827 | ||
|
|
bf8e988488 | ||
|
|
d26ea28028 | ||
|
|
abdd5e08a6 | ||
|
|
47a5ef915b | ||
|
|
5861508153 | ||
|
|
85754ccfb5 | ||
|
|
cbeea23295 | ||
|
|
e2d79a6802 | ||
|
|
c68d85096b | ||
|
|
fcafa83111 | ||
|
|
47d7418eb1 | ||
|
|
8751400ae3 | ||
|
|
3889c1fa71 | ||
|
|
a4d1d3b16c | ||
|
|
adfdf41e85 | ||
|
|
a6825f7ee3 | ||
|
|
830ee45161 | ||
|
|
5f7df3f871 | ||
|
|
34af730934 | ||
|
|
645801fe86 | ||
|
|
547f49a978 | ||
|
|
fd3d7a6f29 | ||
|
|
6be7140a47 | ||
|
|
ca7dc02d88 | ||
|
|
1924c45358 | ||
|
|
be92f92991 | ||
|
|
a9b909cca1 | ||
|
|
175b83dd68 | ||
|
|
df85284a60 | ||
|
|
17ffa93393 | ||
|
|
73c525127f | ||
|
|
3e76d7c321 | ||
|
|
9295f9cef3 | ||
|
|
9d1e6df8f0 | ||
|
|
0805d70c07 | ||
|
|
0133524f09 | ||
|
|
2c5100b2ed | ||
|
|
51296cdc15 | ||
|
|
f261bec248 | ||
|
|
97c7087f6c | ||
|
|
109906d083 | ||
|
|
81833c4938 | ||
|
|
1b6cfdbdc4 | ||
|
|
281d8bddc1 | ||
|
|
0eaa3aeeb7 | ||
|
|
4dcb0bcfed | ||
|
|
e69b7dfd6a | ||
|
|
38d63c71ee | ||
|
|
6880fd3f5f | ||
|
|
1771ca363a | ||
|
|
c896c88c23 | ||
|
|
5ef733729b | ||
|
|
f73e29937c | ||
|
|
8383547943 | ||
|
|
8070d3f1e6 | ||
|
|
a161fa953e | ||
|
|
471e1ae96f | ||
|
|
ed28bcdfb0 | ||
|
|
31ea81c211 | ||
|
|
283189a17c | ||
|
|
40020df865 | ||
|
|
aa6b5ec24f | ||
|
|
f53fe042a9 | ||
|
|
034a54bf4b | ||
|
|
894dd1e353 | ||
|
|
ca99db5a97 | ||
|
|
61e506010c | ||
|
|
4b4343a9fd | ||
|
|
9efacefba3 | ||
|
|
f288c9a9ad | ||
|
|
ec45f23485 | ||
|
|
1c11e73ff8 | ||
|
|
174f93fe11 | ||
|
|
6340f1fb54 | ||
|
|
febdc5b8e2 | ||
|
|
b5044b8753 | ||
|
|
00a60b8948 | ||
|
|
17ec70f839 | ||
|
|
ac03a93c64 | ||
|
|
5e8692eae5 | ||
|
|
2e56e93d83 | ||
|
|
11d0ff2ec3 | ||
|
|
8a59025132 | ||
|
|
5488a8ea82 | ||
|
|
18cb82a0fe | ||
|
|
f951358db4 | ||
|
|
a23162117c | ||
|
|
b13e7b89e2 | ||
|
|
2416fd1c71 | ||
|
|
2b7eb832ed | ||
|
|
1532f47da1 | ||
|
|
2fa20de631 | ||
|
|
b51e16bba8 | ||
|
|
8ae6c4fd7a | ||
|
|
53b51f0740 | ||
|
|
92b4be90d0 | ||
|
|
80a6658769 | ||
|
|
f0f4bd8bf2 | ||
|
|
e05b323489 | ||
|
|
7f198b64f1 | ||
|
|
1c87fef28d | ||
|
|
816c22979f | ||
|
|
42f9a27851 | ||
|
|
6a2d2dfdaf | ||
|
|
efd2088745 | ||
|
|
56e8e84eaf | ||
|
|
eccebae596 | ||
|
|
edc36283d9 | ||
|
|
2409cfefc9 | ||
|
|
fae73913de | ||
|
|
69f398c594 | ||
|
|
fe52238ac2 | ||
|
|
a3501e5417 | ||
|
|
52add26905 | ||
|
|
954b034b8b | ||
|
|
e5aa5ace15 | ||
|
|
a6c5ec8581 | ||
|
|
beb395ce32 | ||
|
|
bcfa80a2d5 | ||
|
|
96e5398ad1 | ||
|
|
4b748ee43a | ||
|
|
838e0f9910 | ||
|
|
548e1378f5 | ||
|
|
fb90d79eb3 | ||
|
|
3fb3381a19 | ||
|
|
60d0750695 | ||
|
|
ce9c31f368 | ||
|
|
d4d7148f16 | ||
|
|
b5aba6e007 | ||
|
|
2564fa7382 | ||
|
|
c07be7386c | ||
|
|
c400003ce9 | ||
|
|
d4ffd8e8e4 | ||
|
|
9a6b907c11 | ||
|
|
84d4c7a6f2 | ||
|
|
0497e2052d | ||
|
|
dc8e9e3fef | ||
|
|
518a593c2a | ||
|
|
f7168cc814 | ||
|
|
18217dadc3 | ||
|
|
559a4969c1 | ||
|
|
7bad6cd20a | ||
|
|
e3987c8aa3 | ||
|
|
a46286872d | ||
|
|
9bb7a125e3 | ||
|
|
3c196abccb | ||
|
|
2e8899dfef | ||
|
|
8337768c5a | ||
|
|
034b371ad4 | ||
|
|
1e68b75c68 | ||
|
|
ac1bf7c9bf | ||
|
|
d60e052f05 | ||
|
|
43b71c7532 | ||
|
|
8ee0a99323 | ||
|
|
50febc342c | ||
|
|
c98667245e | ||
|
|
76fdf8e501 | ||
|
|
6270226ead | ||
|
|
ed5afff5d7 | ||
|
|
5d8d38bdf0 | ||
|
|
88af879d31 | ||
|
|
91095f14fb | ||
|
|
65077088b6 | ||
|
|
47666b7193 | ||
|
|
353f73ed0b | ||
|
|
51ed9dab04 | ||
|
|
c8b0faf05d | ||
|
|
3d7609e3f5 | ||
|
|
d9611948d7 | ||
|
|
67053db9ba | ||
|
|
c831c19848 | ||
|
|
ced45bec65 | ||
|
|
13ded5595a | ||
|
|
dfc4a8d713 | ||
|
|
31b9bfa590 | ||
|
|
f93b3c7424 | ||
|
|
3d32ec7210 | ||
|
|
b108d4e287 | ||
|
|
d8ea86fde1 | ||
|
|
58e979bc45 | ||
|
|
d5acf92f8a | ||
|
|
ddd9b9ae97 | ||
|
|
f77bf37be8 | ||
|
|
fb8fcc5028 | ||
|
|
f80daf6cd4 | ||
|
|
092aeceb9d | ||
|
|
7542ac9d87 | ||
|
|
b27ebaaccf | ||
|
|
5b20a7edb8 | ||
|
|
48580cb5b0 | ||
|
|
7c79accda7 | ||
|
|
1182fdeb43 | ||
|
|
694ffb0a81 | ||
|
|
fe8ff2b180 | ||
|
|
ade1db5215 | ||
|
|
5269cbf862 | ||
|
|
d62e43790c | ||
|
|
5766a50bc2 | ||
|
|
c09503aad8 | ||
|
|
f6bc183972 | ||
|
|
a087677c6e | ||
|
|
529a2ecd81 | ||
|
|
3843d1bd3e | ||
|
|
6be7f6ad68 | ||
|
|
30c4fb8032 | ||
|
|
a7dee45720 | ||
|
|
f7c597d957 | ||
|
|
b89e7c6c3d | ||
|
|
0f7a619a34 | ||
|
|
5c1cb120e1 | ||
|
|
9f24e765be | ||
|
|
a23f9881ee | ||
|
|
80e85a571e | ||
|
|
82a412093e | ||
|
|
8986e84dfe | ||
|
|
b6136a092d | ||
|
|
939f46c7e6 | ||
|
|
dd8059693d | ||
|
|
afacd2a8a0 | ||
|
|
4755cf6a25 | ||
|
|
d3e58c85cf | ||
|
|
89a6d6a4c1 | ||
|
|
ecbfeb608b | ||
|
|
f8a8314476 | ||
|
|
b1d7bb9fd7 | ||
|
|
28a84a90cf | ||
|
|
067119eac7 | ||
|
|
3fa19cf977 | ||
|
|
5cbb5e7d40 | ||
|
|
f5c4749954 | ||
|
|
a3ee010286 | ||
|
|
7e8b5f1f63 | ||
|
|
eff88528d3 | ||
|
|
f83e49bd3d | ||
|
|
bd6c689d91 | ||
|
|
f9469a1ea3 | ||
|
|
c0d92fbdd5 | ||
|
|
2079fc1801 | ||
|
|
59d01a6bd9 | ||
|
|
4f29fe5040 | ||
|
|
455ae8aa05 | ||
|
|
db417e6a5f | ||
|
|
8d444f24f8 | ||
|
|
618cd667fc | ||
|
|
f4d5419ba1 | ||
|
|
0d80269e2d | ||
|
|
861e95673d | ||
|
|
997e4a1edd | ||
|
|
bfdf02f065 | ||
|
|
f658b9d6d9 | ||
|
|
671fce2961 | ||
|
|
93c9ffe109 | ||
|
|
17cd414dde | ||
|
|
3bc10291c4 | ||
|
|
8f6c1d0e47 | ||
|
|
f882fe2c32 | ||
|
|
b25644c590 | ||
|
|
626e29a001 | ||
|
|
dca7a6f270 | ||
|
|
05ef2b8eaa | ||
|
|
1819649a3a | ||
|
|
b8f58b2cc1 | ||
|
|
2ceefab18c | ||
|
|
254dd75cd0 | ||
|
|
6e87d66932 | ||
|
|
3a05d74a36 | ||
|
|
4b5f08ccc2 | ||
|
|
cc20050dc8 | ||
|
|
25cf439f03 | ||
|
|
2beae9ea5b | ||
|
|
64a1ab5c60 | ||
|
|
00ef7ab6ca | ||
|
|
08b3c9d1ac | ||
|
|
2287196746 | ||
|
|
9ac679d437 | ||
|
|
db0a5875fa | ||
|
|
38ff16f3cd | ||
|
|
ccbc36f991 | ||
|
|
b47b99bd28 | ||
|
|
55a922427b | ||
|
|
a5525c065c | ||
|
|
840b3c2a84 | ||
|
|
3cef5f0bff | ||
|
|
fa1463939e | ||
|
|
b31099c94d | ||
|
|
fc0899d9d3 | ||
|
|
a7170a4eee | ||
|
|
e04874d074 | ||
|
|
0fc570b138 | ||
|
|
5edbc1a7b5 | ||
|
|
a7cfa05504 | ||
|
|
c0988da176 | ||
|
|
12707793b1 | ||
|
|
06d6732ab2 | ||
|
|
ab6e98aae2 | ||
|
|
a2ef9d781d | ||
|
|
106ee43171 | ||
|
|
36d626c833 | ||
|
|
b0ac85d81d | ||
|
|
25d70b7b49 | ||
|
|
4c5946e8da | ||
|
|
4de1a38c00 | ||
|
|
caadc02fb4 | ||
|
|
3e691aa31f | ||
|
|
fd7ce9027a | ||
|
|
3fdde5f8c4 | ||
|
|
4c5eb721fb | ||
|
|
f3bdb03cae | ||
|
|
9b6112d206 | ||
|
|
beab0fa6f6 | ||
|
|
fa60cab587 | ||
|
|
05da6b3c50 | ||
|
|
0b12f4e6fd | ||
|
|
b9ddf29289 | ||
|
|
10e47b8305 | ||
|
|
0e1c9487f0 | ||
|
|
60f569dbf9 | ||
|
|
bdb803db21 | ||
|
|
634fab4d9f | ||
|
|
34a14865eb | ||
|
|
abcd3a30ce | ||
|
|
9f40bf6f5d | ||
|
|
8a98aef28c | ||
|
|
bb47b4475e | ||
|
|
1bb6c5fbfc | ||
|
|
35fe459420 | ||
|
|
d75ac45c33 | ||
|
|
9927da5088 | ||
|
|
295b4b3b72 | ||
|
|
8464e0ea3f | ||
|
|
4b73457f7f | ||
|
|
eedcf6f1fe | ||
|
|
5b130d9500 | ||
|
|
600f457bc2 | ||
|
|
e1959f74aa | ||
|
|
0f7d04e0b9 | ||
|
|
f91311144b | ||
|
|
529b3cb18f | ||
|
|
bcbefe29e7 | ||
|
|
1dd7037aa0 | ||
|
|
2c2e9396c4 | ||
|
|
69feb6a4f1 | ||
|
|
911b4b9138 | ||
|
|
21cd4b67f0 | ||
|
|
8c7eebe76c | ||
|
|
bb8b00e547 | ||
|
|
f8c7f88ac2 | ||
|
|
c6cc5810ca | ||
|
|
a89ff584fd | ||
|
|
11d824a88d | ||
|
|
f1ab927a7e | ||
|
|
9e0634f449 | ||
|
|
a9006f2954 | ||
|
|
3a021e7879 | ||
|
|
6ea8cc9986 | ||
|
|
4c77151877 | ||
|
|
ea34431cf7 | ||
|
|
7c546ed6b8 | ||
|
|
db53920153 | ||
|
|
c60015bd09 | ||
|
|
d2a3388058 | ||
|
|
384a75622f | ||
|
|
68a62d689c | ||
|
|
8f7968302d | ||
|
|
35a5733e82 | ||
|
|
f6bbbc56ba | ||
|
|
d1d601f028 | ||
|
|
bbb133d0f2 | ||
|
|
9671f57b65 | ||
|
|
d3212c8b49 | ||
|
|
1f01a5b1cc | ||
|
|
95f7f5535f | ||
|
|
8ae031dbbd | ||
|
|
4498f069f0 | ||
|
|
349b895ac5 | ||
|
|
794f27a3c8 | ||
|
|
1def9a6b19 | ||
|
|
cbf1e42c73 | ||
|
|
7c960bb899 | ||
|
|
1a1807df96 | ||
|
|
2d03f17bde | ||
|
|
8cbaa6b281 | ||
|
|
2c0ae66143 | ||
|
|
a54c160d83 | ||
|
|
61fb7b0d86 | ||
|
|
b3beae44df | ||
|
|
d1369e765a | ||
|
|
6185485c68 | ||
|
|
db733ac9ce | ||
|
|
f94fcf58a7 | ||
|
|
7487841385 | ||
|
|
289c4b497d | ||
|
|
c49bfb97dd | ||
|
|
4643dbce17 | ||
|
|
82e8c01b95 | ||
|
|
47ced11e03 | ||
|
|
04d35a3a83 | ||
|
|
1df3a0b8cb | ||
|
|
faa13f58e0 | ||
|
|
58fcc90417 | ||
|
|
0690612477 | ||
|
|
c0344b385a | ||
|
|
214c5998c6 | ||
|
|
b14c01f9f7 | ||
|
|
4a838e7bdb | ||
|
|
4a748e894d | ||
|
|
b576491156 | ||
|
|
17da790651 | ||
|
|
c599cab14a | ||
|
|
4240bad627 | ||
|
|
0017de7b21 | ||
|
|
53a4ebb66e | ||
|
|
5bfca0f86f | ||
|
|
9553aa944a | ||
|
|
35991ff5d6 | ||
|
|
42d703ca1d | ||
|
|
e089ddb223 | ||
|
|
c323b6a8a9 | ||
|
|
bf461f59de | ||
|
|
9974fa831c | ||
|
|
73e41b0de0 | ||
|
|
9aa297a804 | ||
|
|
5c1bcdbd74 | ||
|
|
8c4997fa37 | ||
|
|
d099a22bd9 | ||
|
|
0de0426f72 | ||
|
|
04d5b692b8 | ||
|
|
da6cdcef84 | ||
|
|
fbf2671eaf | ||
|
|
67e050bd6f | ||
|
|
8282f48cdb | ||
|
|
5d903de2a2 | ||
|
|
78deee2de3 | ||
|
|
a09403328e | ||
|
|
45e92e7b7a | ||
|
|
ebe8a40202 | ||
|
|
bd0747ff6f | ||
|
|
b37ec98e17 | ||
|
|
a93db0a038 | ||
|
|
08f9029bf8 | ||
|
|
fb9d2d99d4 | ||
|
|
a374d4d40f | ||
|
|
fb4666195c | ||
|
|
39b6c00c6d | ||
|
|
decb3ef2f1 | ||
|
|
2a9c501ee2 | ||
|
|
60d25a18fb | ||
|
|
39a71d51cf | ||
|
|
5c401f3858 | ||
|
|
cc45ec85fd | ||
|
|
1d66becff5 | ||
|
|
2a4ebcaf9c | ||
|
|
1a792254ca | ||
|
|
d9319f5734 | ||
|
|
81d96fe3a9 | ||
|
|
39fff0e57d | ||
|
|
95058f7890 | ||
|
|
472ef1a231 | ||
|
|
d7de302393 | ||
|
|
e4b1dfc77a | ||
|
|
aed7267253 | ||
|
|
a24b7433a5 | ||
|
|
0b423a6fe0 | ||
|
|
cb98812444 | ||
|
|
b4e30565d2 | ||
|
|
9f40e2c71e | ||
|
|
fb0a2798e7 | ||
|
|
9912b5037d | ||
|
|
bca56bfaad | ||
|
|
542f048a67 | ||
|
|
91c5e7e06c | ||
|
|
972ee69fac | ||
|
|
00c9ef47e9 | ||
|
|
dcad5f4b5f | ||
|
|
e74cda3c0f | ||
|
|
bafbb39883 | ||
|
|
02391e66dc | ||
|
|
762ab30c71 | ||
|
|
6ce6a932e1 | ||
|
|
e7de1896ea | ||
|
|
4391705c9a | ||
|
|
b40bf76270 | ||
|
|
f44aee0af4 | ||
|
|
57a1af9392 | ||
|
|
b16ae64105 | ||
|
|
0912d583c7 | ||
|
|
6eff89aca1 | ||
|
|
6e5e195a30 | ||
|
|
e3efda2764 | ||
|
|
5360c3dbaf | ||
|
|
f6427c50f2 | ||
|
|
22035d1383 | ||
|
|
85949c5927 | ||
|
|
dde746a5fe | ||
|
|
c2ae30575e | ||
|
|
cb5d9f626e | ||
|
|
c6d82409a8 | ||
|
|
fef5d0e688 | ||
|
|
f64e696f7a | ||
|
|
acf99a6a9e | ||
|
|
a76560c38a | ||
|
|
e1eec66e04 | ||
|
|
96d1ee2347 | ||
|
|
2a06463d07 | ||
|
|
899862dc57 | ||
|
|
c8d585889b | ||
|
|
d044423330 | ||
|
|
273a9a5e66 | ||
|
|
0294e0d2b3 | ||
|
|
38daa0826e | ||
|
|
b9e8dea5d9 | ||
|
|
241930cefe | ||
|
|
bed3684a0c | ||
|
|
e1bb684c4c | ||
|
|
4c75a5b421 | ||
|
|
20f809740c | ||
|
|
83294abaf0 | ||
|
|
687dabc777 | ||
|
|
605d6f0f9b | ||
|
|
b1d13f5a71 | ||
|
|
50c740d219 | ||
|
|
a7456d1182 | ||
|
|
804cbb8c56 | ||
|
|
cb13f66b3d | ||
|
|
ec2cd29096 | ||
|
|
412c6dadca | ||
|
|
1ac3768468 | ||
|
|
e622636686 | ||
|
|
2ddeca8f9d | ||
|
|
d1a80e1a64 | ||
|
|
4cfe41ca9e | ||
|
|
cd1d3005e9 | ||
|
|
435161854c | ||
|
|
890287ce72 | ||
|
|
0cc2a40f26 | ||
|
|
849150c9ab | ||
|
|
75ac74ca5e | ||
|
|
9822479d24 | ||
|
|
c456f63e8c | ||
|
|
51ae3a16bd | ||
|
|
bd418b23dc | ||
|
|
af789ee886 | ||
|
|
324bd57e3d | ||
|
|
146bcdcf1c | ||
|
|
60c92b8a98 | ||
|
|
230120fff5 | ||
|
|
ba76498acc | ||
|
|
5e0772fae9 | ||
|
|
7fcd005f06 | ||
|
|
961d420c95 | ||
|
|
a42df69a7f | ||
|
|
59d2c5ae3c | ||
|
|
a6f0f554f0 | ||
|
|
c8c22b3f22 | ||
|
|
67febd3e25 | ||
|
|
dde937f460 | ||
|
|
c3473673da | ||
|
|
f24aef300b | ||
|
|
84e0c46711 | ||
|
|
16e2ac5b6c | ||
|
|
01214077d3 | ||
|
|
1e3957bdd5 | ||
|
|
8e7fa486db | ||
|
|
a72088b7a8 | ||
|
|
419f1e07d1 | ||
|
|
e05ec3d2d5 | ||
|
|
22f90a505c | ||
|
|
66e506db07 | ||
|
|
fa4041e560 | ||
|
|
f8a8225b8c | ||
|
|
75636b2575 | ||
|
|
e44cc4c145 | ||
|
|
343ada135d | ||
|
|
93a6a77c2b | ||
|
|
8bc67b323c | ||
|
|
f684a3b0ae | ||
|
|
5f2115fff6 | ||
|
|
9d654bcd66 | ||
|
|
5f8e72c6ae | ||
|
|
5dde5cae50 | ||
|
|
f730548c8f | ||
|
|
a185bd9463 | ||
|
|
abdc78e123 | ||
|
|
ba49fa6c88 | ||
|
|
870ffa9dc1 | ||
|
|
8afaf1a60e | ||
|
|
6974ac8e14 | ||
|
|
9f274ee17e | ||
|
|
673bcde719 | ||
|
|
6eaf5bb463 | ||
|
|
b463eedb3f | ||
|
|
90350f99ad | ||
|
|
8576015633 | ||
|
|
f5edf39027 | ||
|
|
751781e3e5 | ||
|
|
bc9129bcbe | ||
|
|
c442a33d5e | ||
|
|
902bb06353 | ||
|
|
a0a1b8b3e8 | ||
|
|
eb5cf9da8d | ||
|
|
57386eeb55 | ||
|
|
33a21f2aaa | ||
|
|
d6fd498489 | ||
|
|
386f2969eb | ||
|
|
2fea3a4b40 | ||
|
|
b9c9fe7a97 | ||
|
|
594c4cd670 | ||
|
|
e10c5e28b5 | ||
|
|
fbefb2014d | ||
|
|
909da7edb5 | ||
|
|
349676d908 | ||
|
|
a47f7cc0ce | ||
|
|
8fbdb884c7 | ||
|
|
ec68ce082e | ||
|
|
2b61e3dfd3 | ||
|
|
4c3dddc486 | ||
|
|
ecc683a6bd | ||
|
|
7b2098cf33 | ||
|
|
3784d86344 | ||
|
|
2b22481443 | ||
|
|
c041d768b2 | ||
|
|
5df6154abe | ||
|
|
1fed02d23f | ||
|
|
2d34d07da4 | ||
|
|
ea25291d75 | ||
|
|
5f77a0082c | ||
|
|
3978ec683d | ||
|
|
458f467985 | ||
|
|
69ffdd4604 | ||
|
|
5c2503b2a8 | ||
|
|
bc921ff835 | ||
|
|
197488bff6 | ||
|
|
e48d21c8cb | ||
|
|
7e992fad99 | ||
|
|
49c0467428 | ||
|
|
7e985991cd | ||
|
|
cc7d2ded6c | ||
|
|
f813f56c1f | ||
|
|
a698c65a07 | ||
|
|
3cd96ba2e8 | ||
|
|
0652f850d6 | ||
|
|
3ab29f9e01 | ||
|
|
b041cdc766 | ||
|
|
c04b9342b8 | ||
|
|
1abaa164f5 | ||
|
|
4f4f00f0c8 | ||
|
|
2e5f0d9210 | ||
|
|
af077c4706 | ||
|
|
a5cebb88ef | ||
|
|
6baf8f08c8 |
@@ -1,2 +1,4 @@
|
|||||||
PORT=9000
|
PORT=9000
|
||||||
UMI_DEV_SERVER_COMPRESS=none
|
UMI_DEV_SERVER_COMPRESS=none
|
||||||
|
DID_YOU_KNOW=none
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
src/components/icon-font/iconfont/iconfont.js
|
||||||
@@ -3,7 +3,13 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'react/no-unstable-nested-components': 1,
|
'react/no-unstable-nested-components': 1,
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
|
'no-undef': 'error',
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
'@typescript-eslint/class-name-casing': 'off'
|
'@typescript-eslint/class-name-casing': 'off'
|
||||||
}
|
},
|
||||||
|
globals: {
|
||||||
|
Global: 'readonly',
|
||||||
|
React: 'readonly'
|
||||||
|
},
|
||||||
|
ignorePatterns: ['public/static/']
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
|
- 'v*-dev'
|
||||||
tags:
|
tags:
|
||||||
- '*.*.*'
|
- '*.*.*'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
|
- 'v*-dev'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: '21'
|
NODE_VERSION: '21'
|
||||||
@@ -72,11 +74,6 @@ jobs:
|
|||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
version: ['latest', '${{ github.ref_name }}']
|
|
||||||
exclude:
|
|
||||||
- version: 'main'
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -92,10 +89,23 @@ jobs:
|
|||||||
${{ github.workspace }}/node_modules
|
${{ github.workspace }}/node_modules
|
||||||
- name: Build
|
- name: Build
|
||||||
run: scripts/build
|
run: scripts/build
|
||||||
|
- name: Determine Version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
|
||||||
|
echo "version=latest" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
||||||
|
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${GITHUB_REF}" == refs/heads/v*-dev ]]; then
|
||||||
|
echo "version=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "Not a publish target"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
- name: Package
|
- name: Package
|
||||||
run: scripts/package
|
run: scripts/package
|
||||||
env:
|
env:
|
||||||
VERSION: '${{ matrix.version }}'
|
VERSION: '${{ steps.version.outputs.version }}'
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: TencentCloud/cos-action@b0aa648235fb35a1bdd6a77f529eb0ac4c2f1c25
|
uses: TencentCloud/cos-action@b0aa648235fb35a1bdd6a77f529eb0ac4c2f1c25
|
||||||
with:
|
with:
|
||||||
@@ -103,7 +113,7 @@ jobs:
|
|||||||
secret_key: ${{ secrets.CI_TECENTCOS_SECRET_KEY }}
|
secret_key: ${{ secrets.CI_TECENTCOS_SECRET_KEY }}
|
||||||
cos_bucket: ${{ secrets.COS_BUCKET }}
|
cos_bucket: ${{ secrets.COS_BUCKET }}
|
||||||
cos_region: ${{ secrets.COS_REGION }}
|
cos_region: ${{ secrets.COS_REGION }}
|
||||||
local_path: dist/${{ matrix.version }}.tar.gz
|
local_path: dist/${{ steps.version.outputs.version }}.tar.gz
|
||||||
remote_path: releases/${{ matrix.version }}.tar.gz
|
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
|
||||||
accelerate: true
|
accelerate: true
|
||||||
clean: false
|
clean: false
|
||||||
|
|||||||
@@ -12,3 +12,4 @@
|
|||||||
/.mfsu
|
/.mfsu
|
||||||
.swc
|
.swc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.idea
|
||||||
@@ -1 +1,4 @@
|
|||||||
npx --no-install lint-staged --quiet
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
{
|
{
|
||||||
"*.{md,json}": [
|
"*.{md,json}": ["prettier --cache --write"],
|
||||||
"prettier --cache --write"
|
"*.{js,jsx}": ["max lint --fix --eslint-only", "prettier --cache --write"],
|
||||||
],
|
|
||||||
"*.{js,jsx}": [
|
|
||||||
"max lint --fix --eslint-only",
|
|
||||||
"prettier --cache --write"
|
|
||||||
],
|
|
||||||
"*.{css,less}": [
|
"*.{css,less}": [
|
||||||
"max lint --fix --stylelint-only",
|
"max lint --fix --stylelint-only",
|
||||||
"prettier --cache --write"
|
"prettier --cache --write"
|
||||||
@@ -13,5 +8,6 @@
|
|||||||
"*.ts?(x)": [
|
"*.ts?(x)": [
|
||||||
"max lint --fix --eslint-only",
|
"max lint --fix --eslint-only",
|
||||||
"prettier --cache --parser=typescript --write"
|
"prettier --cache --parser=typescript --write"
|
||||||
]
|
],
|
||||||
|
"src/locales/**/*.ts": ["npx tsx src/locales/check.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.umi
|
.umi
|
||||||
.umi-production
|
.umi-production
|
||||||
|
public/static/*.js
|
||||||
|
public/static/*.css
|
||||||
|
src/components/icon-font/iconfont/iconfont.js
|
||||||
|
src/components/icon-font/iconfont/*.css
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ module.exports = {
|
|||||||
extends: require.resolve('@umijs/max/stylelint'),
|
extends: require.resolve('@umijs/max/stylelint'),
|
||||||
rules: {
|
rules: {
|
||||||
'selector-class-pattern': null
|
'selector-class-pattern': null
|
||||||
}
|
},
|
||||||
|
ignoreFiles: ['public/static/*.css']
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright (c) 2024 The GPUStack authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# GPUStack UI
|
||||||
|
|
||||||
|
UI for [GPUStack](https://github.com/gpustack/gpustack).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. [Nodejs](https://nodejs.org/en) 16.0+(with NPM)
|
1. [Nodejs](https://nodejs.org/en) 16.0+(with NPM)
|
||||||
@@ -40,3 +44,12 @@ npm run dev
|
|||||||
```
|
```
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Bugs & Issues
|
||||||
|
|
||||||
|
- Please submit [bugs and issues](https://github.com/gpustack/gpustack/issues) with a label `ui`
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Official website: https://gpustack.ai/
|
||||||
|
- Documents: https://docs.gpustack.ai/latest/overview/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from '@umijs/max';
|
import { defineConfig } from '@umijs/max';
|
||||||
|
import keepAlive from './keep-alive';
|
||||||
import proxy from './proxy';
|
import proxy from './proxy';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import theme from './theme';
|
|
||||||
import { getBranchInfo } from './utils';
|
import { getBranchInfo } from './utils';
|
||||||
const CompressionWebpackPlugin = require('compression-webpack-plugin');
|
const CompressionWebpackPlugin = require('compression-webpack-plugin');
|
||||||
|
|
||||||
@@ -14,11 +14,23 @@ const isProduction = env === 'production';
|
|||||||
const t = Date.now();
|
const t = Date.now();
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
proxy: {
|
proxy: {
|
||||||
...proxy('http://192.168.50.166:8080')
|
...proxy(process.env.PROXY_HOST)
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
type: 'hash'
|
type: 'hash'
|
||||||
},
|
},
|
||||||
|
analyze: {
|
||||||
|
analyzerMode: 'server',
|
||||||
|
analyzerPort: 8888,
|
||||||
|
openAnalyzer: true,
|
||||||
|
generateStatsFile: false,
|
||||||
|
statsFilename: 'stats.json',
|
||||||
|
logLevel: 'info',
|
||||||
|
defaultSizes: 'parsed' // stat // gzip
|
||||||
|
},
|
||||||
|
mfsu: {
|
||||||
|
exclude: ['lodash', 'ml-pca']
|
||||||
|
},
|
||||||
base: process.env.npm_config_base || '/',
|
base: process.env.npm_config_base || '/',
|
||||||
...(isProduction
|
...(isProduction
|
||||||
? {
|
? {
|
||||||
@@ -65,17 +77,14 @@ export default defineConfig({
|
|||||||
presets: ['umi-presets-pro'],
|
presets: ['umi-presets-pro'],
|
||||||
clickToComponent: {},
|
clickToComponent: {},
|
||||||
antd: {
|
antd: {
|
||||||
style: 'less',
|
style: 'less'
|
||||||
configProvider: {
|
|
||||||
componentSize: 'large',
|
|
||||||
theme
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
hash: true,
|
hash: true,
|
||||||
access: {},
|
access: {},
|
||||||
model: {},
|
model: {},
|
||||||
initialState: {},
|
initialState: {},
|
||||||
request: {},
|
request: {},
|
||||||
|
keepalive: keepAlive,
|
||||||
locale: {
|
locale: {
|
||||||
antd: true,
|
antd: true,
|
||||||
baseNavigator: true,
|
baseNavigator: true,
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const keepAliveRoutes = {
|
||||||
|
text2images: '/playground/text-to-image',
|
||||||
|
speech: '/playground/speech'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default [keepAliveRoutes.text2images, keepAliveRoutes.speech];
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
const proxyTableList = ['cli', 'v1', 'auth', 'v1-openai', 'version'];
|
const proxyTableList = [
|
||||||
|
'cli',
|
||||||
|
'v1',
|
||||||
|
'auth',
|
||||||
|
'v1-openai',
|
||||||
|
'version',
|
||||||
|
'proxy',
|
||||||
|
'update'
|
||||||
|
];
|
||||||
|
|
||||||
// @ts-ingore
|
// @ts-ingore
|
||||||
export default function createProxyTable(target?: string) {
|
export default function createProxyTable(target?: string) {
|
||||||
@@ -10,6 +18,7 @@ export default function createProxyTable(target?: string) {
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
ws: true,
|
ws: true,
|
||||||
|
log: 'debug',
|
||||||
pathRewrite: (pth: string) => pth.replace(`/^/${api}`, `/${api}`),
|
pathRewrite: (pth: string) => pth.replace(`/^/${api}`, `/${api}`),
|
||||||
headers: {
|
headers: {
|
||||||
origin: newTarget,
|
origin: newTarget,
|
||||||
|
|||||||
@@ -1,58 +1,183 @@
|
|||||||
|
import { keepAliveRoutes } from './keep-alive';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
key: 'dashboard',
|
key: 'dashboard',
|
||||||
icon: 'AppstoreOutlined',
|
icon: 'icon-dashboard',
|
||||||
|
selectedIcon: 'icon-dashboard-filled',
|
||||||
|
defaultIcon: 'icon-dashboard',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeAdmin',
|
||||||
component: './dashboard'
|
component: './dashboard',
|
||||||
|
routes: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'playground',
|
name: 'playground',
|
||||||
title: 'Playground',
|
icon: 'icon-experiment',
|
||||||
|
selectedIcon: 'icon-experiment-filled',
|
||||||
|
defaultIcon: 'icon-experiment',
|
||||||
path: '/playground',
|
path: '/playground',
|
||||||
key: 'playground',
|
key: 'playground',
|
||||||
icon: 'Comment',
|
routes: [
|
||||||
component: './playground'
|
{
|
||||||
|
path: '/playground',
|
||||||
|
redirect: '/playground/chat'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'chat',
|
||||||
|
title: 'Chat',
|
||||||
|
path: '/playground/chat',
|
||||||
|
key: 'chat',
|
||||||
|
icon: 'icon-chat',
|
||||||
|
selectedIcon: 'icon-chat-filled',
|
||||||
|
defaultIcon: 'icon-chat',
|
||||||
|
component: './playground/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'embedding',
|
||||||
|
title: 'embedding',
|
||||||
|
path: '/playground/embedding',
|
||||||
|
key: 'embedding',
|
||||||
|
icon: 'icon-embedding',
|
||||||
|
selectedIcon: 'icon-embedding-filled',
|
||||||
|
defaultIcon: 'icon-embedding',
|
||||||
|
component: './playground/embedding'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rerank',
|
||||||
|
title: 'Rerank',
|
||||||
|
path: '/playground/rerank',
|
||||||
|
key: 'rerank',
|
||||||
|
icon: 'icon-reranker',
|
||||||
|
selectedIcon: 'icon-reranker-filled',
|
||||||
|
defaultIcon: 'icon-reranker',
|
||||||
|
component: './playground/rerank'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'text2images',
|
||||||
|
title: 'Text2Images',
|
||||||
|
path: keepAliveRoutes.text2images,
|
||||||
|
key: 'text2images',
|
||||||
|
icon: 'icon-image1',
|
||||||
|
selectedIcon: 'icon-image-filled',
|
||||||
|
defaultIcon: 'icon-image1',
|
||||||
|
component: './playground/images'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'speech',
|
||||||
|
title: 'Speech',
|
||||||
|
path: keepAliveRoutes.speech,
|
||||||
|
key: 'speech',
|
||||||
|
icon: 'icon-audio1',
|
||||||
|
selectedIcon: 'icon-audio-filled',
|
||||||
|
defaultIcon: 'icon-audio1',
|
||||||
|
component: './playground/speech'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'models',
|
name: 'models',
|
||||||
path: '/models',
|
path: '/models',
|
||||||
key: 'models',
|
key: 'models',
|
||||||
icon: 'Block',
|
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeAdmin',
|
||||||
component: './llmodels'
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/models',
|
||||||
|
redirect: '/models/deployments'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'modelCatalog',
|
||||||
|
path: '/models/catalog',
|
||||||
|
key: 'modelsCatalog',
|
||||||
|
icon: 'icon-layers',
|
||||||
|
selectedIcon: 'icon-layers-filled',
|
||||||
|
defaultIcon: 'icon-layers',
|
||||||
|
access: 'canSeeAdmin',
|
||||||
|
component: './llmodels/catalog'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deployment',
|
||||||
|
path: '/models/deployments',
|
||||||
|
key: 'modelDeployments',
|
||||||
|
icon: 'icon-rocket-launch1',
|
||||||
|
selectedIcon: 'icon-rocket-launch-fill',
|
||||||
|
defaultIcon: 'icon-rocket-launch1',
|
||||||
|
access: 'canSeeAdmin',
|
||||||
|
component: './llmodels/index'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'resources',
|
name: 'resources',
|
||||||
path: '/resources',
|
path: '/resources',
|
||||||
key: 'resources',
|
key: 'resources',
|
||||||
icon: 'CloudServer',
|
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeAdmin',
|
||||||
component: './resources'
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/resources',
|
||||||
|
redirect: '/resources/workers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'workers',
|
||||||
|
path: '/resources/workers',
|
||||||
|
key: 'workers',
|
||||||
|
icon: 'icon-resources',
|
||||||
|
selectedIcon: 'icon-resources-filled',
|
||||||
|
defaultIcon: 'icon-resources',
|
||||||
|
component: './resources/components/workers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gpus',
|
||||||
|
path: '/resources/gpus',
|
||||||
|
key: 'gpus',
|
||||||
|
icon: 'icon-gpu1',
|
||||||
|
selectedIcon: 'icon-gpu-filled',
|
||||||
|
defaultIcon: 'icon-gpu1',
|
||||||
|
component: './resources/components/gpus'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'modelfiles',
|
||||||
|
path: '/resources/modelfiles',
|
||||||
|
key: 'modelfiles',
|
||||||
|
icon: 'icon-files',
|
||||||
|
selectedIcon: 'icon-files-filled',
|
||||||
|
defaultIcon: 'icon-files',
|
||||||
|
component: './resources/components/model-files'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'accessControl',
|
||||||
|
path: '/access-control',
|
||||||
|
key: 'accessControl',
|
||||||
|
access: 'canSeeAdmin',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/access-control',
|
||||||
|
redirect: '/access-control/users'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'users',
|
||||||
|
path: '/access-control/users',
|
||||||
|
key: 'users',
|
||||||
|
icon: 'icon-users',
|
||||||
|
selectedIcon: 'icon-users-filled',
|
||||||
|
defaultIcon: 'icon-users',
|
||||||
|
component: './users'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// name: 'usage',
|
|
||||||
// path: '/usage',
|
|
||||||
// key: 'usage',
|
|
||||||
// icon: 'BarChartOutlined',
|
|
||||||
// component: './usage'
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
name: 'apikeys',
|
name: 'apikeys',
|
||||||
path: '/api-keys',
|
path: '/api-keys',
|
||||||
key: 'apikeys',
|
key: 'apikeys',
|
||||||
icon: 'LockOutlined',
|
hideInMenu: true,
|
||||||
|
selectedIcon: 'icon-key-filled',
|
||||||
|
icon: 'icon-key',
|
||||||
|
defaultIcon: 'icon-key',
|
||||||
component: './api-keys'
|
component: './api-keys'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'users',
|
|
||||||
path: '/users',
|
|
||||||
key: 'users',
|
|
||||||
icon: 'Team',
|
|
||||||
access: 'canSeeAdmin',
|
|
||||||
component: './users'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
@@ -73,7 +198,7 @@ export default [
|
|||||||
name: '404',
|
name: '404',
|
||||||
path: '*',
|
path: '*',
|
||||||
key: '404',
|
key: '404',
|
||||||
layout: false,
|
hideInMenu: true,
|
||||||
component: './404'
|
component: './404'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
export default {
|
|
||||||
'root-entry-name': 'variable',
|
|
||||||
cssVar: true,
|
|
||||||
hashed: false,
|
|
||||||
components: {
|
|
||||||
Input: {
|
|
||||||
inputFontSize: 12
|
|
||||||
},
|
|
||||||
Table: {
|
|
||||||
cellFontSize: 12,
|
|
||||||
rowSelectedHoverBg: 'rgba(230, 230, 230, 0.88)',
|
|
||||||
rowSelectedBg: 'rgb(244, 245, 244)'
|
|
||||||
},
|
|
||||||
Menu: {
|
|
||||||
iconSize: 16,
|
|
||||||
iconMarginInlineEnd: 12,
|
|
||||||
itemHeight: 44,
|
|
||||||
itemColor: 'rgba(0,0,0,1)',
|
|
||||||
itemHoverBg: 'rgba(0,0,0,0.04)',
|
|
||||||
itemActiveBg: 'rgba(0,0,0,0.04)'
|
|
||||||
},
|
|
||||||
Progress: {
|
|
||||||
lineBorderRadius: 4
|
|
||||||
},
|
|
||||||
Select: {
|
|
||||||
itemSelectedBg: 'rgba(0,0,0,0.06)'
|
|
||||||
},
|
|
||||||
Message: {
|
|
||||||
contentPadding: '12px 16px'
|
|
||||||
},
|
|
||||||
Slider: {
|
|
||||||
handleSize: 8,
|
|
||||||
handleSizeHover: 8,
|
|
||||||
trackBg: 'rgba(0,0,0,0.15)',
|
|
||||||
handleColor: 'rgba(0,0,0,0.2)',
|
|
||||||
trackHoverBg: 'rgba(0,0,0,0.25)',
|
|
||||||
handleActiveColor: 'rgba(0,0,0,0.3)',
|
|
||||||
dotActiveBorderColor: 'rgba(0,0,0,0.25)',
|
|
||||||
handleActiveOutlineColor: 'rgba(0,0,0,0.25)',
|
|
||||||
dotBorderColor: 'rgba(0,0,0,0.25)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
token: {
|
|
||||||
colorPrimary: '#007BFF',
|
|
||||||
borderRadius: 8,
|
|
||||||
fontSize: 12,
|
|
||||||
motion: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1 +1,28 @@
|
|||||||
// only for third party libraries styles
|
// only for third party libraries styles
|
||||||
|
|
||||||
|
.simplebar-scrollbar.simplebar-visible::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplebar-scrollbar::before {
|
||||||
|
background: var(--scrollbar-handle-bg);
|
||||||
|
width: var(--scrollbar-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplebar-scrollbar.simplebar-visible.simplebar-hover {
|
||||||
|
&::before {
|
||||||
|
background: var(--scrollbar-handle-hover-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplebar-scrollbar.scrollbar-handle-light {
|
||||||
|
&::before {
|
||||||
|
background-color: var(--scrollbar-handle-light-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplebar-scrollbar.simplebar-visible.simplebar-hover.scrollbar-handle-light {
|
||||||
|
&::before {
|
||||||
|
background: var(--scrollbar-handle-light-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,55 +1,88 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "jialin <jialinkuang@126.com>",
|
"author": "jialin",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "max build",
|
"build": "max build",
|
||||||
|
"check:locales": "npx tsx ./src/locales/check.ts",
|
||||||
"dev": "max dev",
|
"dev": "max dev",
|
||||||
"format": "prettier --cache --write .",
|
"format": "prettier --cache --write .",
|
||||||
"postinstall": "max setup",
|
"postinstall": "max setup",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
"preview": "max preview",
|
||||||
"setup": "max setup",
|
"setup": "max setup",
|
||||||
"start": "npm run dev"
|
"start": "npm run dev"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"immer": "^9.0.6"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.3.7",
|
"@ant-design/icons": "^5.5.1",
|
||||||
"@ant-design/pro-components": "^2.7.1",
|
"@ant-design/pro-components": "^2.7.19",
|
||||||
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@orcid/bibtex-parse-js": "^0.0.25",
|
||||||
"@react-hook/resize-observer": "^2.0.2",
|
"@react-hook/resize-observer": "^2.0.2",
|
||||||
"@types/lodash": "^4.17.4",
|
"@types/lodash": "^4.17.4",
|
||||||
"@umijs/max": "^4.2.11",
|
"@umijs/max": "^4.2.11",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"ahooks": "^3.8.5",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"antd": "^5.18.3",
|
"antd": "^5.21.6",
|
||||||
"antd-style": "^3.6.2",
|
"antd-style": "^3.6.2",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.8.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
|
"driver.js": "^1.3.1",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
|
"epubjs": "^0.3.93",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"has-ansi": "^5.0.1",
|
"has-ansi": "^5.0.1",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
"jotai": "^2.8.4",
|
"jotai": "^2.8.4",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"katex": "^0.16.21",
|
||||||
|
"lamejs": "github:zhuker/lamejs",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"mammoth": "^1.8.0",
|
||||||
"marked": "^14.1.0",
|
"marked": "^14.1.0",
|
||||||
|
"ml-dataset-iris": "^1.2.1",
|
||||||
|
"ml-pca": "^4.1.1",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
|
"overlayscrollbars": "^2.10.0",
|
||||||
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
|
"pdfjs-dist": "^4.7.76",
|
||||||
"query-string": "^9.0.0",
|
"query-string": "^9.0.0",
|
||||||
|
"rc-resize-observer": "^1.4.0",
|
||||||
|
"rc-virtual-list": "^3.14.8",
|
||||||
|
"re-resizable": "^6.10.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hotkeys-hook": "^4.5.0",
|
"react-hotkeys-hook": "^4.5.0",
|
||||||
|
"react-markdown": "^9.0.3",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"remark-breaks": "^4.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
"simplebar-react": "^3.2.6",
|
"simplebar-react": "^3.2.6",
|
||||||
"umi-presets-pro": "^2.0.3"
|
"styled-components": "^6.1.15",
|
||||||
|
"umi-presets-pro": "^2.0.3",
|
||||||
|
"wavesurfer.js": "^7.8.8",
|
||||||
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/marked": "^6.0.0",
|
"@types/marked": "^6.0.0",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.1",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@umijs/case-sensitive-paths-webpack-plugin": "^1.0.1",
|
"@umijs/case-sensitive-paths-webpack-plugin": "^1.0.1",
|
||||||
|
"@umijs/plugins": "^4.4.11",
|
||||||
"babel-plugin-named-asset-import": "^0.3.8",
|
"babel-plugin-named-asset-import": "^0.3.8",
|
||||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||||
"compression-webpack-plugin": "^11.1.0",
|
"compression-webpack-plugin": "^11.1.0",
|
||||||
@@ -66,7 +99,15 @@
|
|||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"prettier-plugin-packagejson": "^2.5.0",
|
"prettier-plugin-packagejson": "^2.5.0",
|
||||||
"prettier-plugin-two-style-order": "^1.0.1",
|
"prettier-plugin-two-style-order": "^1.0.1",
|
||||||
|
"tsx": "^4.19.3",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"url-loader": "^4.1.1"
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
|
"worker-loader": "^3.0.8"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"elliptic": "^6.6.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ export default (api: IApi) => {
|
|||||||
api.modifyHTML(($) => {
|
api.modifyHTML(($) => {
|
||||||
const info = JSON.parse(process.env.VERSION || '{}');
|
const info = JSON.parse(process.env.VERSION || '{}');
|
||||||
const env = process.env.NODE_ENV;
|
const env = process.env.NODE_ENV;
|
||||||
|
|
||||||
|
$('html').attr('data-env', env);
|
||||||
|
|
||||||
$('html').attr(
|
$('html').attr(
|
||||||
'data-version',
|
'data-version',
|
||||||
env === 'production'
|
env === 'production' ? info.version || info.commitId : `${info.commitId}`
|
||||||
? info.version || info.commitId
|
|
||||||
: `dev-${info.commitId}`
|
|
||||||
);
|
);
|
||||||
if (env === 'production') {
|
if (env === 'production') {
|
||||||
$('script[src^="/js/umi"]').first?.().remove?.();
|
$('script[src^="/js/umi"]').first?.().remove?.();
|
||||||
|
|||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -1,14 +1,33 @@
|
|||||||
import { GPUStackVersionAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
||||||
import { setAtomStorage } from '@/atoms/utils';
|
import { setAtomStorage } from '@/atoms/utils';
|
||||||
import { requestConfig } from '@/request-config';
|
import { requestConfig } from '@/request-config';
|
||||||
import {
|
import {
|
||||||
queryCurrentUserState,
|
queryCurrentUserState,
|
||||||
queryVersionInfo
|
queryVersionInfo,
|
||||||
|
updateCheck
|
||||||
} from '@/services/profile/apis';
|
} from '@/services/profile/apis';
|
||||||
|
import { isOnline } from '@/utils';
|
||||||
|
import {
|
||||||
|
IS_FIRST_LOGIN,
|
||||||
|
readState,
|
||||||
|
writeState
|
||||||
|
} from '@/utils/localstore/index';
|
||||||
import { RequestConfig, history } from '@umijs/max';
|
import { RequestConfig, history } from '@umijs/max';
|
||||||
|
|
||||||
const loginPath = '/login';
|
const loginPath = '/login';
|
||||||
|
|
||||||
|
// only for the first login and access from http://localhost
|
||||||
|
|
||||||
|
const checkDefaultPage = async (userInfo: any) => {
|
||||||
|
const isFirstLogin = await readState(IS_FIRST_LOGIN);
|
||||||
|
if (isFirstLogin === null && isOnline()) {
|
||||||
|
writeState(IS_FIRST_LOGIN, true);
|
||||||
|
if (userInfo && userInfo?.is_admin) {
|
||||||
|
history.push('/models/deployments');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// runtime configuration
|
// runtime configuration
|
||||||
export async function getInitialState(): Promise<{
|
export async function getInitialState(): Promise<{
|
||||||
fetchUserInfo: () => Promise<Global.UserInfo>;
|
fetchUserInfo: () => Promise<Global.UserInfo>;
|
||||||
@@ -16,11 +35,27 @@ export async function getInitialState(): Promise<{
|
|||||||
}> {
|
}> {
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
|
|
||||||
|
const getUpdateCheck = async () => {
|
||||||
|
try {
|
||||||
|
const data = await updateCheck();
|
||||||
|
|
||||||
|
setAtomStorage(UpdateCheckAtom, {
|
||||||
|
...data
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('updateCheck error', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchUserInfo = async (): Promise<Global.UserInfo> => {
|
const fetchUserInfo = async (): Promise<Global.UserInfo> => {
|
||||||
try {
|
try {
|
||||||
const data = await queryCurrentUserState({
|
const data = await queryCurrentUserState({
|
||||||
skipErrorHandler: true
|
skipErrorHandler: true
|
||||||
});
|
});
|
||||||
|
if (data.is_admin) {
|
||||||
|
getUpdateCheck();
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
history.push(loginPath);
|
history.push(loginPath);
|
||||||
@@ -31,7 +66,11 @@ export async function getInitialState(): Promise<{
|
|||||||
const getAppVersionInfo = async () => {
|
const getAppVersionInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await queryVersionInfo();
|
const data = await queryVersionInfo();
|
||||||
setAtomStorage(GPUStackVersionAtom, data);
|
const isProduction = data.version?.indexOf('0.0.0') === -1;
|
||||||
|
setAtomStorage(GPUStackVersionAtom, {
|
||||||
|
...data,
|
||||||
|
isProduction
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('queryVersionInfo error', error);
|
console.error('queryVersionInfo error', error);
|
||||||
}
|
}
|
||||||
@@ -41,7 +80,7 @@ export async function getInitialState(): Promise<{
|
|||||||
|
|
||||||
if (![loginPath].includes(location.pathname)) {
|
if (![loginPath].includes(location.pathname)) {
|
||||||
const userInfo = await fetchUserInfo();
|
const userInfo = await fetchUserInfo();
|
||||||
|
checkDefaultPage(userInfo);
|
||||||
return {
|
return {
|
||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
currentUser: userInfo
|
currentUser: userInfo
|
||||||
|
|||||||
|
After Width: | Height: | Size: 726 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 118 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755020187776" class="icon" viewBox="0 0 1105 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7392" xmlns:xlink="http://www.w3.org/1999/xlink" width="138.125" height="128"><path d="M507.2896 133.44768l130.98496-71.4496v820.9408l-130.97984 69.71392V133.44768z" fill="#F38019" p-id="7393"></path><path d="M479.39072 323.968a724.30592 724.30592 0 0 0-77.056 16.6144C118.35904 417.01376 37.77536 603.8016 67.95776 701.696c30.11584 97.8944 161.03424 263.2192 439.2704 250.19904 0.13824-27.41248 0.13824-57.73824 0-90.89536-200.49408-26.37824-302.67904-88.82688-306.55488-187.40736-5.74464-147.74272 134.1696-216.90368 217.87136-229.0176 16.82432-2.49344 37.66272-5.67808 61.19936-8.6528l-0.27648-111.9488h-0.07168z m185.12384 107.65312c52.1984 5.6832 103.57248 18.69312 147.0464 44.44672-10.65984 5.19168-34.26816 19.73248-70.82496 43.47904l290.49344 66.39104-16.54272-206.58176-78.37184 44.30336c-116.10112-57.8048-205.19936-91.0336-267.29984-99.6864l-4.50048 107.648z" fill="#ADAFB3" p-id="7394"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M140-180v-88.92q0-29.39 15.96-54.43 15.96-25.03 42.66-38.49 59.3-29.08 119.65-43.62Q378.62-420 440-420q21.06 0 42.12 1.89 21.05 1.88 42.11 5.65-1.69 51.46 20.81 96.81Q567.54-270.31 609-240v60H140ZM753.85-72.31l-53.35-53.14v-164.73q-39.11-11.51-63.85-43.97-24.73-32.46-24.73-74.7 0-51.46 36.38-87.84t87.85-36.38q51.46 0 87.66 36.39Q860-460.28 860-408.79q0 39.94-22.42 70.71-22.43 30.77-57.2 44.23l44.23 44.23-53.07 53.2 53.07 53.19-70.76 70.92ZM440-484.62q-57.75 0-98.87-41.12Q300-566.86 300-624.61q0-57.75 41.13-98.88 41.12-41.12 98.87-41.12 57.75 0 98.87 41.12Q580-682.36 580-624.61q0 57.75-41.13 98.87-41.12 41.12-98.87 41.12Zm296.15 93.47q14.7 0 25.04-10.54 10.35-10.54 10.35-25.23 0-14.69-10.35-25.04-10.34-10.35-25.04-10.35-14.69 0-25.23 10.35-10.54 10.35-10.54 25.04t10.54 25.23q10.54 10.54 25.23 10.54Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 934 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||||
|
␍<g fill="#C22E33">
|
||||||
|
␍<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
|
||||||
|
␍<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
|
||||||
|
␍<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
|
||||||
|
␍</g>
|
||||||
|
␍</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -6,6 +6,10 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m-b-6 {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.m-b-5 {
|
.m-b-5 {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
@@ -38,6 +42,22 @@
|
|||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m-l-40 {
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-t-16 {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-t-10 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-0 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.m-r-10 {
|
.m-r-10 {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
@@ -66,9 +86,18 @@
|
|||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-l-8 {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-t-6 {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// space-between
|
// space-between
|
||||||
.flex-between {
|
.flex-between {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -79,12 +108,27 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.justify-start {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-end {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
// align-items: center
|
// align-items: center
|
||||||
.flex-center {
|
.flex-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-gap-2 {
|
.flex-gap-2 {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
@@ -98,6 +142,11 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-end {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-column {
|
.flex-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -115,14 +164,34 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gap-20 {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-6 {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-24 {
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.relative {
|
.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-size-0 {
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.font-size-12 {
|
.font-size-12 {
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-size-13 {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.font-size-14 {
|
.font-size-14 {
|
||||||
font-size: var(--font-size-middle);
|
font-size: var(--font-size-middle);
|
||||||
}
|
}
|
||||||
@@ -131,6 +200,34 @@
|
|||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-size-20 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-18 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-24 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-32 {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-b-0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-b-16 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-r-16 {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.font-400 {
|
.font-400 {
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
}
|
}
|
||||||
@@ -139,6 +236,10 @@
|
|||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-500 {
|
||||||
|
font-weight: var(--font-weight-500);
|
||||||
|
}
|
||||||
|
|
||||||
.font-700 {
|
.font-700 {
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
}
|
}
|
||||||
@@ -150,3 +251,71 @@
|
|||||||
.text-tertiary {
|
.text-tertiary {
|
||||||
color: var(--ant-color-text-tertiary);
|
color: var(--ant-color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tips-desc-list {
|
||||||
|
list-style-type: decimal;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-16 {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-white-tertiary {
|
||||||
|
color: var(--color-white-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-white-quaternary {
|
||||||
|
color: var(--color-white-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-white-light-1 {
|
||||||
|
color: var(--color-white-light-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-white-light-2 {
|
||||||
|
color: var(--color-white-light-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-white-light-3 {
|
||||||
|
color: var(--color-white-light-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-white-light-4 {
|
||||||
|
color: var(--color-white-light-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
padding-inline: 6px 0;
|
||||||
|
padding-block: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea::-webkit-scrollbar {
|
||||||
|
width: var(--scrollbar-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-scrollbar-thumb);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold-text {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-result-extra {
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.driver-popover {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
|
||||||
|
.driver-popover-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
.ant-pro-layout {
|
.ant-pro-layout {
|
||||||
.ant-pro-sider {
|
.ant-pro-sider {
|
||||||
// &.ant-layout-sider {
|
|
||||||
// background: var(--color-white-1);
|
|
||||||
// }
|
|
||||||
.ant-layout-sider-children {
|
.ant-layout-sider-children {
|
||||||
background-color: var(--color-fill-1);
|
|
||||||
border-inline: none;
|
border-inline: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
// padding-inline: 16px;
|
// padding-inline: 16px;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { atom, getDefaultStore } from 'jotai';
|
||||||
|
|
||||||
|
// models expand keys: create, update , delete,
|
||||||
|
export const modelsExpandKeysAtom = atom<string[]>([]);
|
||||||
|
|
||||||
|
export const requestIdAtom = atom<number>(0);
|
||||||
|
|
||||||
|
export const setRquestId = () => {
|
||||||
|
const store = getDefaultStore();
|
||||||
|
const id = Date.now();
|
||||||
|
store.set(requestIdAtom, id);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRequestId = () => {
|
||||||
|
const store = getDefaultStore();
|
||||||
|
return store.get(requestIdAtom);
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { atom, getDefaultStore } from 'jotai';
|
||||||
|
|
||||||
|
export const routeCacheAtom = atom<any>(new Map());
|
||||||
|
|
||||||
|
export const setRouteCache = (key: string, value: any) => {
|
||||||
|
const store = getDefaultStore();
|
||||||
|
const cache = store.get(routeCacheAtom);
|
||||||
|
cache.set(key, value);
|
||||||
|
store.set(routeCacheAtom, cache);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteRouteCache = (key: string) => {
|
||||||
|
const store = getDefaultStore();
|
||||||
|
const cache = store.get(routeCacheAtom);
|
||||||
|
cache.delete(key);
|
||||||
|
store.set(routeCacheAtom, cache);
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { colorPrimary } from '@/config/theme';
|
||||||
|
import { atom } from 'jotai';
|
||||||
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
|
type UserSettings = {
|
||||||
|
theme: 'light' | 'realDark';
|
||||||
|
mode: 'light' | 'realDark' | 'auto';
|
||||||
|
colorPrimary: string;
|
||||||
|
isDarkTheme: boolean;
|
||||||
|
collapsed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSettings: UserSettings = {
|
||||||
|
theme: 'light',
|
||||||
|
mode: 'auto',
|
||||||
|
isDarkTheme: false,
|
||||||
|
colorPrimary: colorPrimary,
|
||||||
|
collapsed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStorageUserSettings = () => {
|
||||||
|
if (typeof window === 'undefined') return defaultSettings;
|
||||||
|
try {
|
||||||
|
const savedSettings = JSON.parse(
|
||||||
|
localStorage.getItem('userSettings') || '{}'
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...defaultSettings,
|
||||||
|
...savedSettings
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return defaultSettings;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userSettingsAtom = atomWithStorage<UserSettings>('userSettings', {
|
||||||
|
...getStorageUserSettings()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSettingsHelperAtom = atom(
|
||||||
|
(get) => get(userSettingsAtom),
|
||||||
|
(get, set, update: Partial<UserSettings>) => {
|
||||||
|
const prev = get(userSettingsAtom);
|
||||||
|
const newSettings = {
|
||||||
|
...prev,
|
||||||
|
...(update || {})
|
||||||
|
};
|
||||||
|
set(userSettingsAtom, {
|
||||||
|
...newSettings,
|
||||||
|
colorPrimary: newSettings.colorPrimary || colorPrimary,
|
||||||
|
isDarkTheme: newSettings.theme === 'realDark'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { getDefaultStore } from 'jotai';
|
||||||
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
|
export const tabActiveAtom = atomWithStorage<Map<string, any>>(
|
||||||
|
'tabActiveStatus',
|
||||||
|
new Map()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setActiveStatus = (key: string, value: any) => {
|
||||||
|
const store = getDefaultStore();
|
||||||
|
const cache = store.get(tabActiveAtom);
|
||||||
|
cache.set(key, value);
|
||||||
|
store.set(tabActiveAtom, cache);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActiveStatus = (key: string) => {
|
||||||
|
const store = getDefaultStore();
|
||||||
|
const cache = store.get(tabActiveAtom);
|
||||||
|
return cache.get(key);
|
||||||
|
};
|
||||||
@@ -6,9 +6,17 @@ export const userAtom = atomWithStorage<any>('userInfo', null);
|
|||||||
export const GPUStackVersionAtom = atom<{
|
export const GPUStackVersionAtom = atom<{
|
||||||
version: string;
|
version: string;
|
||||||
git_commit: string;
|
git_commit: string;
|
||||||
|
isProduction: boolean;
|
||||||
}>({
|
}>({
|
||||||
version: '',
|
version: '',
|
||||||
git_commit: ''
|
git_commit: '',
|
||||||
|
isProduction: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateCheckAtom = atom<{
|
||||||
|
latest_version: string;
|
||||||
|
}>({
|
||||||
|
latest_version: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
export const initialPasswordAtom = atomWithStorage<string>(
|
export const initialPasswordAtom = atomWithStorage<string>(
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
interface WatchRequest {
|
||||||
|
id: number;
|
||||||
|
token: any;
|
||||||
|
cancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__GPUSTACK_WATCH_REQUEST_CLEAR__: {
|
||||||
|
watchIDValue: number;
|
||||||
|
requestList: WatchRequest[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__GPUSTACK_WATCH_REQUEST_CLEAR__ = {
|
||||||
|
watchIDValue: 0,
|
||||||
|
requestList: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateWatchIDValue = () => {
|
||||||
|
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.watchIDValue =
|
||||||
|
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.watchIDValue + 1;
|
||||||
|
return window.__GPUSTACK_WATCH_REQUEST_CLEAR__.watchIDValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateWatchRequest = (watchToken: WatchRequest) => {
|
||||||
|
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList.push(watchToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cancelWatchRequest = (n: number) => {
|
||||||
|
// cancel the before n requests
|
||||||
|
const requestList = window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList;
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
requestList[i]?.cancel?.();
|
||||||
|
}
|
||||||
|
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList = requestList.slice(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearWatchRequestId = (id: number) => {
|
||||||
|
const requestList = window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList;
|
||||||
|
const newRequestList = requestList.filter((item) => item.id !== id);
|
||||||
|
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList = newRequestList;
|
||||||
|
};
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { WarningFilled } from '@ant-design/icons';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import OverlayScroller, { OverlayScrollerOptions } from '../overlay-scroller';
|
||||||
|
interface AlertInfoProps {
|
||||||
|
type: Global.MessageType;
|
||||||
|
message: React.ReactNode;
|
||||||
|
rows?: number;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
ellipsis?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
contentStyle?: React.CSSProperties;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
maxHeight?: number;
|
||||||
|
overlayScrollerProps?: OverlayScrollerOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ token, css }) => {
|
||||||
|
return {
|
||||||
|
alertBlockInfo: css`
|
||||||
|
padding-block: 6px;
|
||||||
|
padding-inline: 10px 16px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 32px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
.ant-typography {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
border-color: ${token.colorErrorBorder};
|
||||||
|
background-color: ${token.colorErrorBg};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
border-color: ${token.colorWarningBorder};
|
||||||
|
background-color: ${token.colorWarningBg};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.transition {
|
||||||
|
color: ${token.geekblue7};
|
||||||
|
background: ${token.geekblue1};
|
||||||
|
border-color: ${token.geekblue3};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
border: 1px solid ${token.colorSuccess};
|
||||||
|
color: ${token.colorSuccessText};
|
||||||
|
background: ${token.colorSuccessBg};
|
||||||
|
|
||||||
|
.content.success {
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: ${token.borderRadius}px ${token.borderRadius}px 0 0;
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
&.danger {
|
||||||
|
color: ${token.colorErrorText};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
color: ${token.colorWarningText};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.transition {
|
||||||
|
color: ${token.geekblue7};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
color: ${token.colorSuccessText};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const TitleWrapper = styled.div`
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ContentWrapper = styled.div<{ $hasTitle: boolean }>`
|
||||||
|
word-break: break-word;
|
||||||
|
color: ${(props) =>
|
||||||
|
props.$hasTitle
|
||||||
|
? 'var(--ant-color-text-secondary)'
|
||||||
|
: 'var(--ant-color-text)'};
|
||||||
|
font-weight: var(--font-weight-500);
|
||||||
|
white-space: pre-line;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
||||||
|
const {
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
rows = 1,
|
||||||
|
ellipsis,
|
||||||
|
style,
|
||||||
|
title,
|
||||||
|
contentStyle,
|
||||||
|
icon,
|
||||||
|
maxHeight = 86,
|
||||||
|
overlayScrollerProps = {}
|
||||||
|
} = props;
|
||||||
|
const { styles } = useStyles();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{message ? (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.alertBlockInfo, type)}
|
||||||
|
style={{ ...style }}
|
||||||
|
>
|
||||||
|
<Typography.Paragraph
|
||||||
|
ellipsis={
|
||||||
|
ellipsis ?? {
|
||||||
|
rows: rows,
|
||||||
|
tooltip: message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={classNames('title', type)}>
|
||||||
|
<span className={classNames('info-icon', type)}>
|
||||||
|
{icon ?? <WarningFilled />}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{title && (
|
||||||
|
<TitleWrapper className="title-text">{title}</TitleWrapper>
|
||||||
|
)}
|
||||||
|
<OverlayScroller
|
||||||
|
maxHeight={maxHeight}
|
||||||
|
style={{ ...contentStyle }}
|
||||||
|
{...overlayScrollerProps}
|
||||||
|
>
|
||||||
|
<ContentWrapper
|
||||||
|
$hasTitle={!!title}
|
||||||
|
className={classNames('content', type)}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</ContentWrapper>
|
||||||
|
</OverlayScroller>
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(AlertInfo);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { WarningOutlined } from '@ant-design/icons';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface AlertInfoProps {
|
||||||
|
type: 'danger' | 'warning';
|
||||||
|
message: string;
|
||||||
|
rows?: number;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
ellipsis?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
||||||
|
const { message, type, rows = 1, ellipsis, style } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{message ? (
|
||||||
|
<Typography.Paragraph
|
||||||
|
type={type}
|
||||||
|
ellipsis={
|
||||||
|
ellipsis !== undefined
|
||||||
|
? ellipsis
|
||||||
|
: {
|
||||||
|
rows: rows,
|
||||||
|
tooltip: message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '2px 5px',
|
||||||
|
borderRadius: 'var(--border-radius-base)',
|
||||||
|
margin: 0,
|
||||||
|
backgroundColor: 'var(--ant-color-error-bg)',
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WarningOutlined className="m-r-8" />
|
||||||
|
{message}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(AlertInfo);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.canvas-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroller-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import useResizeObserver from '@/components/logs-viewer/use-size';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
interface AudioAnimationProps {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
scaleFactor?: number;
|
||||||
|
maxBarCount?: number;
|
||||||
|
amplitude?: number;
|
||||||
|
fixedHeight?: boolean;
|
||||||
|
analyserData: {
|
||||||
|
data: Uint8Array;
|
||||||
|
analyser: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const AudioAnimation: React.FC<AudioAnimationProps> = (props) => {
|
||||||
|
const {
|
||||||
|
scaleFactor = 1.2,
|
||||||
|
maxBarCount = 128,
|
||||||
|
amplitude = 40,
|
||||||
|
maxWidth,
|
||||||
|
fixedHeight = true,
|
||||||
|
analyserData,
|
||||||
|
width: initialWidth,
|
||||||
|
height: initialHeight
|
||||||
|
} = props;
|
||||||
|
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||||
|
const animationId = React.useRef<number>(0);
|
||||||
|
const isScaled = React.useRef<boolean>(false);
|
||||||
|
const oscillationOffset = React.useRef(0);
|
||||||
|
const direction = React.useRef(1);
|
||||||
|
const scrollerRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [width, setWidth] = useState(initialWidth);
|
||||||
|
const [height, setHeight] = useState(initialHeight);
|
||||||
|
const containerRef = React.useRef<any>(null);
|
||||||
|
|
||||||
|
const size = useResizeObserver(scrollerRef);
|
||||||
|
|
||||||
|
const calculateJitter = (
|
||||||
|
i: number,
|
||||||
|
timestamp: number,
|
||||||
|
baseHeight: number,
|
||||||
|
minJitter: number,
|
||||||
|
jitterAmplitude: number
|
||||||
|
) => {
|
||||||
|
//
|
||||||
|
const jitterFactor = Math.sin(timestamp / 200 + i) * 0.5 + 0.5;
|
||||||
|
const jitter =
|
||||||
|
minJitter +
|
||||||
|
jitterFactor * (jitterAmplitude - minJitter) * (baseHeight / maxBarCount);
|
||||||
|
return jitter;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startAudioVisualization = () => {
|
||||||
|
if (!canvasRef.current || !analyserData.data?.length) return;
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const canvasCtx = canvas.getContext('2d');
|
||||||
|
if (!canvasCtx) return;
|
||||||
|
|
||||||
|
const WIDTH = (canvas.width = width * 2);
|
||||||
|
const HEIGHT = (canvas.height = height * 2);
|
||||||
|
|
||||||
|
if (!isScaled.current) {
|
||||||
|
canvasCtx.scale(2, 2);
|
||||||
|
isScaled.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const barWidth = 4;
|
||||||
|
const barSpacing = 6;
|
||||||
|
const centerLine = Math.floor(HEIGHT / 2);
|
||||||
|
|
||||||
|
const jitterAmplitude = amplitude;
|
||||||
|
const minJitter = 10;
|
||||||
|
|
||||||
|
let lastFrameTime = 0;
|
||||||
|
|
||||||
|
const gradient = canvasCtx.createLinearGradient(0, 0, 0, HEIGHT);
|
||||||
|
gradient.addColorStop(0, '#007BFF');
|
||||||
|
gradient.addColorStop(1, '#0069DA');
|
||||||
|
canvasCtx.fillStyle = gradient;
|
||||||
|
|
||||||
|
const draw = (timestamp: number) => {
|
||||||
|
const elapsed = timestamp - lastFrameTime;
|
||||||
|
if (elapsed < 16) {
|
||||||
|
animationId.current = requestAnimationFrame(draw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastFrameTime = timestamp;
|
||||||
|
|
||||||
|
analyserData.analyser?.current?.getByteFrequencyData(analyserData.data);
|
||||||
|
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
|
||||||
|
|
||||||
|
const barCount = Math.min(maxBarCount, analyserData.data.length);
|
||||||
|
const totalWidth = barCount * (barWidth + barSpacing) - barSpacing;
|
||||||
|
let x = WIDTH / 2 - totalWidth / 2 + oscillationOffset.current;
|
||||||
|
|
||||||
|
oscillationOffset.current += direction.current;
|
||||||
|
if (Math.abs(oscillationOffset.current) > 20) {
|
||||||
|
direction.current *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < barCount; i++) {
|
||||||
|
const baseHeight = Math.floor(analyserData.data[i] / 2) * scaleFactor;
|
||||||
|
|
||||||
|
const jitter = calculateJitter(
|
||||||
|
i,
|
||||||
|
timestamp,
|
||||||
|
baseHeight,
|
||||||
|
minJitter,
|
||||||
|
jitterAmplitude
|
||||||
|
);
|
||||||
|
const barHeight = baseHeight + Math.round(jitter);
|
||||||
|
|
||||||
|
const topY = Math.round(centerLine - barHeight / 2);
|
||||||
|
const bottomY = Math.round(centerLine + barHeight / 2);
|
||||||
|
|
||||||
|
canvasCtx.beginPath();
|
||||||
|
canvasCtx.moveTo(x, bottomY);
|
||||||
|
canvasCtx.lineTo(x, topY + 2);
|
||||||
|
canvasCtx.arcTo(x + barWidth, topY + 2, x + barWidth, bottomY, 2);
|
||||||
|
canvasCtx.lineTo(x + barWidth, bottomY);
|
||||||
|
canvasCtx.closePath();
|
||||||
|
canvasCtx.fill();
|
||||||
|
|
||||||
|
x += barWidth + barSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
animationId.current = requestAnimationFrame(draw);
|
||||||
|
};
|
||||||
|
|
||||||
|
draw(performance.now());
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (size) {
|
||||||
|
if (maxWidth) {
|
||||||
|
setWidth(Math.min(size.width, maxWidth));
|
||||||
|
} else {
|
||||||
|
setWidth(size?.width || 0);
|
||||||
|
}
|
||||||
|
if (!fixedHeight) {
|
||||||
|
setHeight(size?.height || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [size, maxWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canvasRef.current) return;
|
||||||
|
const clearCanvas = () => {
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
|
if (ctx) ctx.clearRect(0, 0, width * 2, height * 2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!analyserData.data?.length || !analyserData.analyser?.current) {
|
||||||
|
clearCanvas();
|
||||||
|
cancelAnimationFrame(animationId.current);
|
||||||
|
animationId.current = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startAudioVisualization();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationId.current);
|
||||||
|
clearCanvas();
|
||||||
|
};
|
||||||
|
}, [analyserData, width, height]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="scroller-wrapper"
|
||||||
|
ref={scrollerRef}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="canvas-wrap"
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
>
|
||||||
|
<canvas ref={canvasRef} style={{ display: 'block' }}></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(AudioAnimation);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const AudioWrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AudioElement: React.FC<any> = (props) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AudioWrapper>
|
||||||
|
<audio {...props} controls></audio>
|
||||||
|
</AudioWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AudioElement;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export type AudioEvent =
|
||||||
|
| 'play'
|
||||||
|
| 'playing'
|
||||||
|
| 'pause'
|
||||||
|
| 'timeupdate'
|
||||||
|
| 'ended'
|
||||||
|
| 'loadedmetadata'
|
||||||
|
| 'audioprocess'
|
||||||
|
| 'canplay'
|
||||||
|
| 'ended'
|
||||||
|
| 'loadeddata'
|
||||||
|
| 'seeked'
|
||||||
|
| 'seeking'
|
||||||
|
| 'volumechange';
|
||||||
|
|
||||||
|
export interface AudioPlayerProps {
|
||||||
|
controls?: boolean;
|
||||||
|
autoplay?: boolean;
|
||||||
|
url: string;
|
||||||
|
speed?: number;
|
||||||
|
ref?: any;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
duration?: number;
|
||||||
|
onPlay?: () => void;
|
||||||
|
onPlaying?: () => void;
|
||||||
|
onPause?: () => void;
|
||||||
|
onTimeUpdate?: () => void;
|
||||||
|
onEnded?: () => void;
|
||||||
|
onLoadedMetadata?: (duration: number) => void;
|
||||||
|
onAudioProcess?: (current: number) => void;
|
||||||
|
onCanPlay?: () => void;
|
||||||
|
onLoadedData?: () => void;
|
||||||
|
onSeeked?: () => void;
|
||||||
|
onSeeking?: () => void;
|
||||||
|
onVolumeChange?: () => void;
|
||||||
|
onReady?: (duration: number) => void;
|
||||||
|
onAnalyse?: (analyseData: any, frequencyBinCount: any) => void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
.player-wrap {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
// background-color: var(--ant-color-fill-quaternary);
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.player-ui {
|
||||||
|
padding: 8px 16px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn {
|
||||||
|
margin-inline: 30px;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.backward,
|
||||||
|
.forward {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.slider-inner {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
width: 52px;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
margin-inline: 10px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
line-height: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-slider-horizontal {
|
||||||
|
margin-block: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.speaker {
|
||||||
|
margin-left: 10px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
import { formatTime } from '@/utils/index';
|
||||||
|
import {
|
||||||
|
FastBackwardOutlined,
|
||||||
|
FastForwardOutlined,
|
||||||
|
PauseCircleFilled,
|
||||||
|
PlayCircleFilled
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button, Slider, Tooltip } from 'antd';
|
||||||
|
import { round } from 'lodash';
|
||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle
|
||||||
|
} from 'react';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
interface AudioPlayerProps {
|
||||||
|
autoplay?: boolean;
|
||||||
|
url: string;
|
||||||
|
speed?: number;
|
||||||
|
ref?: any;
|
||||||
|
name: string;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
duration?: number;
|
||||||
|
extra?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const speedOptions = [
|
||||||
|
{ label: '1x', value: 1 },
|
||||||
|
{ label: '2x', value: 2 },
|
||||||
|
{ label: '3x', value: 3 },
|
||||||
|
{ label: '4x', value: 4 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const speedConfig = {
|
||||||
|
min: 0.5,
|
||||||
|
max: 2,
|
||||||
|
step: 0.25
|
||||||
|
};
|
||||||
|
|
||||||
|
const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { autoplay = false, speed: defaultSpeed = 1, extra } = props;
|
||||||
|
const audioRef = React.useRef<HTMLAudioElement>(null);
|
||||||
|
const [audioState, setAudioState] = React.useState<{
|
||||||
|
currentTime: number;
|
||||||
|
duration: number;
|
||||||
|
}>({
|
||||||
|
currentTime: 0,
|
||||||
|
duration: 0
|
||||||
|
});
|
||||||
|
const [playOn, setPlayOn] = React.useState<boolean>(false);
|
||||||
|
const [speakerOn, setSpeakerOn] = React.useState<boolean>(false);
|
||||||
|
const [volume, setVolume] = React.useState<number>(1);
|
||||||
|
const [speed, setSpeed] = React.useState<number>(defaultSpeed);
|
||||||
|
const timer = React.useRef<any>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
play: () => {
|
||||||
|
audioRef.current?.play();
|
||||||
|
},
|
||||||
|
pause: () => {
|
||||||
|
audioRef.current?.pause();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const handleShowVolume = useCallback(() => {
|
||||||
|
setSpeakerOn(!speakerOn);
|
||||||
|
}, [speakerOn]);
|
||||||
|
|
||||||
|
const handleSeepdChange = useCallback((value: number | string) => {
|
||||||
|
setSpeed(value as number);
|
||||||
|
audioRef.current!.playbackRate = value as number;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAudioOnPlay = useCallback(() => {
|
||||||
|
console.log('audio play');
|
||||||
|
timer.current = setInterval(() => {
|
||||||
|
setAudioState((prestate) => {
|
||||||
|
return {
|
||||||
|
currentTime: Math.ceil(audioRef.current?.currentTime || 0),
|
||||||
|
duration:
|
||||||
|
prestate.duration || Math.ceil(audioRef.current?.duration || 0)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (audioRef.current?.paused || audioRef.current?.ended) {
|
||||||
|
clearInterval(timer.current);
|
||||||
|
setPlayOn(false);
|
||||||
|
setAudioState((prestate: any) => {
|
||||||
|
return {
|
||||||
|
currentTime: audioRef.current?.ended ? 0 : prestate.currentTime,
|
||||||
|
duration: prestate.duration
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePlay = useCallback(() => {
|
||||||
|
setPlayOn(!playOn);
|
||||||
|
if (playOn) {
|
||||||
|
audioRef.current?.pause();
|
||||||
|
} else {
|
||||||
|
audioRef.current?.play();
|
||||||
|
}
|
||||||
|
}, [playOn]);
|
||||||
|
|
||||||
|
const handleFormatVolume = (val?: number) => {
|
||||||
|
if (val === undefined) {
|
||||||
|
return `${round(volume * 100)}%`;
|
||||||
|
}
|
||||||
|
return `${round(val * 100)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVolumeChange = useCallback((value: number) => {
|
||||||
|
audioRef.current!.volume = round(value, 2);
|
||||||
|
setVolume(round(value, 2));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initPlayerConfig = () => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current!.volume = volume;
|
||||||
|
audioRef.current!.playbackRate = speed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadedMetadata = useCallback(
|
||||||
|
(data: any) => {
|
||||||
|
const duration = Math.ceil(audioRef.current?.duration || 0);
|
||||||
|
setAudioState({
|
||||||
|
currentTime: 0,
|
||||||
|
duration:
|
||||||
|
duration && duration !== Infinity ? duration : props.duration || 0
|
||||||
|
});
|
||||||
|
setPlayOn(autoplay);
|
||||||
|
},
|
||||||
|
[autoplay, props.duration]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCurrentChange = useCallback((val: number) => {
|
||||||
|
audioRef.current!.currentTime = val;
|
||||||
|
setAudioState((prestate) => {
|
||||||
|
return {
|
||||||
|
currentTime: val,
|
||||||
|
duration: prestate.duration
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleReduceSpeed = () => {
|
||||||
|
setSpeed((pre) => {
|
||||||
|
if (pre - speedConfig.step < speedConfig.min) {
|
||||||
|
return speedConfig.min;
|
||||||
|
}
|
||||||
|
const next = pre - speedConfig.step;
|
||||||
|
audioRef.current!.playbackRate = next;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddSpeed = () => {
|
||||||
|
setSpeed((pre) => {
|
||||||
|
if (pre + speedConfig.step > speedConfig.max) {
|
||||||
|
return speedConfig.max;
|
||||||
|
}
|
||||||
|
const next = pre + speedConfig.step;
|
||||||
|
audioRef.current!.playbackRate = next;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnLoad = (e: any) => {
|
||||||
|
console.log('onload', e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDownload = useCallback(() => {
|
||||||
|
const url = props.url || '';
|
||||||
|
const filename = props.name;
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}, [props.url, props.name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
initPlayerConfig();
|
||||||
|
}
|
||||||
|
}, [audioRef.current]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="player-wrap" style={{ width: props.width || '100%' }}>
|
||||||
|
<div className="player-ui">
|
||||||
|
<div className="play-content">
|
||||||
|
<div className="progress-bar">
|
||||||
|
<span className="file-name">{props.name}</span>
|
||||||
|
<div className="slider">
|
||||||
|
{/* <span className="time current">
|
||||||
|
{' '}
|
||||||
|
{formatTime(audioState.currentTime)}
|
||||||
|
</span> */}
|
||||||
|
<div className="slider-inner">
|
||||||
|
<Slider
|
||||||
|
tooltip={{ open: false }}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
styles={{
|
||||||
|
rail: {
|
||||||
|
// height: 6
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
max={audioState.duration}
|
||||||
|
value={audioState.currentTime}
|
||||||
|
onChange={handleCurrentChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* <span className="time">{formatTime(audioState.duration)}</span> */}
|
||||||
|
</div>
|
||||||
|
<div className="controls">
|
||||||
|
<div className="audio-control flex-center">
|
||||||
|
<span className="time current">
|
||||||
|
{' '}
|
||||||
|
{formatTime(audioState.currentTime)}
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'playground.audio.button.slow'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
className="backward"
|
||||||
|
disabled={
|
||||||
|
speed === speedConfig.min || speed < speedConfig.min
|
||||||
|
}
|
||||||
|
onClick={handleReduceSpeed}
|
||||||
|
>
|
||||||
|
<FastBackwardOutlined className="font-size-20" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<span className="play-btn">
|
||||||
|
<Button
|
||||||
|
size="middle"
|
||||||
|
type="text"
|
||||||
|
onClick={handlePlay}
|
||||||
|
disabled={!audioState?.duration}
|
||||||
|
icon={
|
||||||
|
!playOn ? (
|
||||||
|
<PlayCircleFilled
|
||||||
|
style={{ fontSize: '22px' }}
|
||||||
|
></PlayCircleFilled>
|
||||||
|
) : (
|
||||||
|
<PauseCircleFilled
|
||||||
|
style={{ fontSize: '22px' }}
|
||||||
|
></PauseCircleFilled>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></Button>
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'playground.audio.button.fast'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
className="forward"
|
||||||
|
disabled={
|
||||||
|
speed === speedConfig.max || speed > speedConfig.max
|
||||||
|
}
|
||||||
|
onClick={handleAddSpeed}
|
||||||
|
>
|
||||||
|
<FastForwardOutlined className="font-size-20" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<span className="time">{formatTime(audioState.duration)}</span>
|
||||||
|
</div>
|
||||||
|
{extra}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<audio
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
autoPlay={autoplay}
|
||||||
|
src={props.url}
|
||||||
|
ref={audioRef}
|
||||||
|
preload="metadata"
|
||||||
|
style={{ opacity: 0, position: 'absolute', left: '-9999px' }}
|
||||||
|
onPlay={handleAudioOnPlay}
|
||||||
|
onLoadedMetadata={handleLoadedMetadata}
|
||||||
|
></audio>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default React.memo(AudioPlayer);
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef
|
||||||
|
} from 'react';
|
||||||
|
import { AudioPlayerProps } from './config/type';
|
||||||
|
|
||||||
|
const RawAudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
|
||||||
|
const { autoplay = false } = props;
|
||||||
|
const audioRef = React.useRef<HTMLAudioElement>(null);
|
||||||
|
|
||||||
|
// =================== audio context ======================
|
||||||
|
const audioContext = useRef<any>(null);
|
||||||
|
const analyser = useRef<any>(null);
|
||||||
|
const dataArray = useRef<any>(null);
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
const initAudioContext = useCallback(() => {
|
||||||
|
audioContext.current = new (window.AudioContext ||
|
||||||
|
window.webkitAudioContext)();
|
||||||
|
|
||||||
|
analyser.current = audioContext.current.createAnalyser();
|
||||||
|
analyser.current.fftSize = 512;
|
||||||
|
dataArray.current = new Uint8Array(analyser.current.frequencyBinCount);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const generateVisualData = useCallback(() => {
|
||||||
|
const source = audioContext.current.createMediaElementSource(
|
||||||
|
audioRef.current
|
||||||
|
);
|
||||||
|
source.connect(analyser.current);
|
||||||
|
analyser.current.connect(audioContext.current.destination);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initEnvents = () => {
|
||||||
|
if (!audioRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('complete', () => {});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('play', () => {
|
||||||
|
props.onAnalyse?.(dataArray.current, analyser);
|
||||||
|
props.onPlay?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('pause', () => {
|
||||||
|
props.onAnalyse?.(dataArray.current, analyser);
|
||||||
|
props.onPause?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('timeupdate', () => {
|
||||||
|
props.onTimeUpdate?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('ended', () => {
|
||||||
|
props.onEnded?.();
|
||||||
|
});
|
||||||
|
// add all other events
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('canplay', () => {
|
||||||
|
props.onCanPlay?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('loadeddata', () => {
|
||||||
|
initEnvents();
|
||||||
|
if (!audioContext.current) {
|
||||||
|
initAudioContext();
|
||||||
|
generateVisualData();
|
||||||
|
}
|
||||||
|
props.onLoadedData?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('seeked', () => {
|
||||||
|
props.onSeeked?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('seeking', () => {
|
||||||
|
props.onSeeking?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('volumechange', () => {
|
||||||
|
props.onVolumeChange?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('audioprocess', () => {
|
||||||
|
const current = audioRef.current?.currentTime || 0;
|
||||||
|
props.onAudioProcess?.(current);
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('playing', () => {
|
||||||
|
props.onPlaying?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('loadedmetadata', () => {
|
||||||
|
const duration = audioRef.current?.duration || 0;
|
||||||
|
props.onLoadedMetadata?.(duration);
|
||||||
|
props.onReady?.(duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('ended', () => {
|
||||||
|
props.onEnded?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioRef.current.addEventListener('loadeddata', () => {
|
||||||
|
props.onLoadedData?.();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
play: () => {
|
||||||
|
audioRef.current?.play();
|
||||||
|
},
|
||||||
|
pause: () => {
|
||||||
|
audioRef.current?.pause();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
console.log('audioRef.current', audioRef.current, props.url);
|
||||||
|
initEnvents();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (audioContext.current) {
|
||||||
|
audioContext.current.close();
|
||||||
|
}
|
||||||
|
// remove all events
|
||||||
|
|
||||||
|
audioRef.current?.removeEventListener('play', () => {});
|
||||||
|
audioRef.current?.removeEventListener('pause', () => {});
|
||||||
|
audioRef.current?.removeEventListener('timeupdate', () => {});
|
||||||
|
audioRef.current?.removeEventListener('ended', () => {});
|
||||||
|
audioRef.current?.removeEventListener('canplay', () => {});
|
||||||
|
audioRef.current?.removeEventListener('loadeddata', () => {});
|
||||||
|
audioRef.current?.removeEventListener('seeked', () => {});
|
||||||
|
audioRef.current?.removeEventListener('seeking', () => {});
|
||||||
|
audioRef.current?.removeEventListener('volumechange', () => {});
|
||||||
|
audioRef.current?.removeEventListener('audioprocess', () => {});
|
||||||
|
audioRef.current?.removeEventListener('playing', () => {});
|
||||||
|
audioRef.current?.removeEventListener('loadedmetadata', () => {});
|
||||||
|
audioRef.current?.removeEventListener('ended', () => {});
|
||||||
|
audioRef.current?.removeEventListener('loadeddata', () => {});
|
||||||
|
};
|
||||||
|
}, [audioRef.current]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<audio
|
||||||
|
controls
|
||||||
|
autoPlay={autoplay}
|
||||||
|
src={props.url}
|
||||||
|
ref={audioRef}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '-9999px',
|
||||||
|
opacity: 0
|
||||||
|
}}
|
||||||
|
preload="metadata"
|
||||||
|
></audio>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default RawAudioPlayer;
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
import { formatTime } from '@/utils/index';
|
||||||
|
import { DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button, Dropdown, Slider, type MenuProps } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { round } from 'lodash';
|
||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo
|
||||||
|
} from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import AutoTooltip from '../auto-tooltip';
|
||||||
|
import IconFont from '../icon-font';
|
||||||
|
|
||||||
|
type ActionItem = 'download' | 'delete' | 'speed';
|
||||||
|
|
||||||
|
interface AudioPlayerProps {
|
||||||
|
autoplay?: boolean;
|
||||||
|
url: string;
|
||||||
|
speed?: number;
|
||||||
|
ref?: any;
|
||||||
|
name: string;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
duration?: number;
|
||||||
|
actions?: ActionItem[];
|
||||||
|
onDelete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SliderWrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
.ant-slider {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const isDarkMode = token.darkMode as boolean;
|
||||||
|
return {
|
||||||
|
wrapper: css`
|
||||||
|
position: relative;
|
||||||
|
min-width: 360px;
|
||||||
|
height: 54px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background-color: ${isDarkMode
|
||||||
|
? 'var(--ant-color-fill-secondary)'
|
||||||
|
: '#F1F3F4'};
|
||||||
|
border-radius: 28px;
|
||||||
|
.inner {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex: 1;
|
||||||
|
gap: 8px;
|
||||||
|
.slider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
.ant-slider {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
.ant-slider-handle {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus-within {
|
||||||
|
.ant-slider-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-slider-handle {
|
||||||
|
opacity: 0;
|
||||||
|
&::before {
|
||||||
|
background-color: var(--ant-color-bg-spotlight);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const sliderStyles = {
|
||||||
|
rail: {
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: 'var(--ant-color-fill-secondary)'
|
||||||
|
},
|
||||||
|
track: {
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: 'var(--ant-color-bg-spotlight)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const speedOptions = [
|
||||||
|
{ label: '1x', value: 1 },
|
||||||
|
{ label: '2x', value: 2 },
|
||||||
|
{ label: '3x', value: 3 },
|
||||||
|
{ label: '4x', value: 4 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const speedConfig = {
|
||||||
|
min: 0.5,
|
||||||
|
max: 2,
|
||||||
|
step: 0.25
|
||||||
|
};
|
||||||
|
|
||||||
|
const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const {
|
||||||
|
autoplay = false,
|
||||||
|
speed: defaultSpeed = 1,
|
||||||
|
actions = ['delete'],
|
||||||
|
name,
|
||||||
|
onDelete
|
||||||
|
} = props;
|
||||||
|
const audioRef = React.useRef<HTMLAudioElement>(null);
|
||||||
|
const [audioState, setAudioState] = React.useState<{
|
||||||
|
currentTime: number;
|
||||||
|
duration: number;
|
||||||
|
}>({
|
||||||
|
currentTime: 0,
|
||||||
|
duration: 0
|
||||||
|
});
|
||||||
|
console.log('audioState', name);
|
||||||
|
const [playOn, setPlayOn] = React.useState<boolean>(false);
|
||||||
|
const [speakerOn, setSpeakerOn] = React.useState<boolean>(false);
|
||||||
|
const [volume, setVolume] = React.useState<number>(1);
|
||||||
|
const [speed, setSpeed] = React.useState<number>(defaultSpeed);
|
||||||
|
const timer = React.useRef<any>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
play: () => {
|
||||||
|
audioRef.current?.play();
|
||||||
|
},
|
||||||
|
pause: () => {
|
||||||
|
audioRef.current?.pause();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const handleShowVolume = useCallback(() => {
|
||||||
|
setSpeakerOn(!speakerOn);
|
||||||
|
}, [speakerOn]);
|
||||||
|
|
||||||
|
const handleSeepdChange = useCallback((value: number | string) => {
|
||||||
|
setSpeed(value as number);
|
||||||
|
audioRef.current!.playbackRate = value as number;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAudioOnPlay = useCallback(() => {
|
||||||
|
timer.current = setInterval(() => {
|
||||||
|
setAudioState((prestate) => {
|
||||||
|
return {
|
||||||
|
currentTime: Math.ceil(audioRef.current?.currentTime || 0),
|
||||||
|
duration:
|
||||||
|
prestate.duration || Math.ceil(audioRef.current?.duration || 0)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (audioRef.current?.paused || audioRef.current?.ended) {
|
||||||
|
clearInterval(timer.current);
|
||||||
|
setPlayOn(false);
|
||||||
|
setAudioState((prestate: any) => {
|
||||||
|
return {
|
||||||
|
currentTime: audioRef.current?.ended ? 0 : prestate.currentTime,
|
||||||
|
duration: prestate.duration
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePlay = useCallback(() => {
|
||||||
|
setPlayOn(!playOn);
|
||||||
|
if (playOn) {
|
||||||
|
audioRef.current?.pause();
|
||||||
|
} else {
|
||||||
|
audioRef.current?.play();
|
||||||
|
}
|
||||||
|
}, [playOn]);
|
||||||
|
|
||||||
|
const handleFormatVolume = (val?: number) => {
|
||||||
|
if (val === undefined) {
|
||||||
|
return `${round(volume * 100)}%`;
|
||||||
|
}
|
||||||
|
return `${round(val * 100)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVolumeChange = useCallback((value: number) => {
|
||||||
|
audioRef.current!.volume = round(value, 2);
|
||||||
|
setVolume(round(value, 2));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initPlayerConfig = () => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current!.volume = volume;
|
||||||
|
audioRef.current!.playbackRate = speed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadedMetadata = useCallback(
|
||||||
|
(data: any) => {
|
||||||
|
const duration = Math.ceil(audioRef.current?.duration || 0);
|
||||||
|
setAudioState({
|
||||||
|
currentTime: 0,
|
||||||
|
duration:
|
||||||
|
duration && duration !== Infinity ? duration : props.duration || 0
|
||||||
|
});
|
||||||
|
setPlayOn(autoplay);
|
||||||
|
},
|
||||||
|
[autoplay, props.duration]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCurrentChange = useCallback((val: number) => {
|
||||||
|
audioRef.current!.currentTime = val;
|
||||||
|
setAudioState((prestate) => {
|
||||||
|
return {
|
||||||
|
currentTime: val,
|
||||||
|
duration: prestate.duration
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleReduceSpeed = () => {
|
||||||
|
setSpeed((pre) => {
|
||||||
|
if (pre - speedConfig.step < speedConfig.min) {
|
||||||
|
return speedConfig.min;
|
||||||
|
}
|
||||||
|
const next = pre - speedConfig.step;
|
||||||
|
audioRef.current!.playbackRate = next;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddSpeed = () => {
|
||||||
|
setSpeed((pre) => {
|
||||||
|
if (pre + speedConfig.step > speedConfig.max) {
|
||||||
|
return speedConfig.max;
|
||||||
|
}
|
||||||
|
const next = pre + speedConfig.step;
|
||||||
|
audioRef.current!.playbackRate = next;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnLoad = (e: any) => {
|
||||||
|
console.log('onload', e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDownload = useCallback(() => {
|
||||||
|
const url = props.url || '';
|
||||||
|
const filename = props.name;
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename || 'audio.mp3'; // Default filename
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}, [props.url, props.name]);
|
||||||
|
|
||||||
|
const items: MenuProps['items'] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'download',
|
||||||
|
label: intl.formatMessage({ id: 'common.button.download' }),
|
||||||
|
icon: <DownloadOutlined />,
|
||||||
|
onClick: onDownload
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'speed',
|
||||||
|
label: intl.formatMessage({ id: 'playground.params.speed' }),
|
||||||
|
icon: <IconFont type="icon-play-speed"></IconFont>,
|
||||||
|
children: speedOptions.map((item) => ({
|
||||||
|
key: item.value,
|
||||||
|
label: item.label,
|
||||||
|
onClick: () => handleSeepdChange(item.value)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: intl.formatMessage({ id: 'common.button.delete' }),
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
danger: true,
|
||||||
|
onClick: () => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
audioRef.current.src = '';
|
||||||
|
audioRef.current.load();
|
||||||
|
}
|
||||||
|
setAudioState({ currentTime: 0, duration: 0 });
|
||||||
|
setPlayOn(false);
|
||||||
|
onDelete?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].filter((item) => actions.includes(item.key as ActionItem));
|
||||||
|
}, [actions, intl, onDownload, onDelete, handleSeepdChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
initPlayerConfig();
|
||||||
|
}
|
||||||
|
}, [audioRef.current]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.wrapper}
|
||||||
|
style={{
|
||||||
|
width: props.width || '100%',
|
||||||
|
height: props.height || '60px',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="inner">
|
||||||
|
<Button
|
||||||
|
size="middle"
|
||||||
|
type="text"
|
||||||
|
onClick={handlePlay}
|
||||||
|
shape="circle"
|
||||||
|
disabled={!audioState?.duration}
|
||||||
|
icon={
|
||||||
|
!playOn ? (
|
||||||
|
<IconFont
|
||||||
|
type="icon-playcircle-fill"
|
||||||
|
style={{ fontSize: '24px' }}
|
||||||
|
></IconFont>
|
||||||
|
) : (
|
||||||
|
<IconFont
|
||||||
|
type="icon-stopcircle-fill"
|
||||||
|
style={{ fontSize: '24px' }}
|
||||||
|
></IconFont>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></Button>
|
||||||
|
|
||||||
|
<div className="slider">
|
||||||
|
<div className="flex-center flex-between file-name">
|
||||||
|
<AutoTooltip ghost maxWidth={200}>
|
||||||
|
<span>{name}</span>
|
||||||
|
</AutoTooltip>
|
||||||
|
</div>
|
||||||
|
<SliderWrapper>
|
||||||
|
<span className="time">{formatTime(audioState.currentTime)}</span>
|
||||||
|
<Slider
|
||||||
|
tooltip={{ open: false }}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
styles={sliderStyles}
|
||||||
|
max={audioState.duration}
|
||||||
|
value={audioState.currentTime}
|
||||||
|
onChange={handleCurrentChange}
|
||||||
|
/>
|
||||||
|
</SliderWrapper>
|
||||||
|
</div>
|
||||||
|
<Dropdown menu={{ items }} trigger={['click']}>
|
||||||
|
<Button
|
||||||
|
icon={<IconFont type="icon-more"></IconFont>}
|
||||||
|
type="text"
|
||||||
|
size="middle"
|
||||||
|
shape="circle"
|
||||||
|
></Button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<audio
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
autoPlay={autoplay}
|
||||||
|
src={props.url}
|
||||||
|
ref={audioRef}
|
||||||
|
preload="metadata"
|
||||||
|
style={{ opacity: 0, position: 'absolute', left: '-9999px' }}
|
||||||
|
onPlay={handleAudioOnPlay}
|
||||||
|
onLoadedMetadata={handleLoadedMetadata}
|
||||||
|
></audio>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default React.memo(AudioPlayer);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.toolbar-wrapper {
|
||||||
|
padding: 0 24px;
|
||||||
|
color: rgba(255, 255, 255, 65%);
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: rgba(0, 0, 0, 10%);
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-wrapper .anticon {
|
||||||
|
padding: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-wrapper .anticon[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-wrapper .anticon:hover {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import fallbackImg from '@/assets/images/img_fallback.png';
|
||||||
|
import {
|
||||||
|
DownloadOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
RotateLeftOutlined,
|
||||||
|
RotateRightOutlined,
|
||||||
|
SwapOutlined,
|
||||||
|
UndoOutlined,
|
||||||
|
ZoomInOutlined,
|
||||||
|
ZoomOutOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Image as AntImage, ImageProps, Space } from 'antd';
|
||||||
|
import { round } from 'lodash';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const AutoImage: React.FC<
|
||||||
|
ImageProps & {
|
||||||
|
height: number | string;
|
||||||
|
width?: number | string;
|
||||||
|
autoSize?: boolean;
|
||||||
|
preview?: boolean;
|
||||||
|
onLoad?: () => void;
|
||||||
|
}
|
||||||
|
> = (props) => {
|
||||||
|
const { height = 100, width: w, autoSize, preview = true, ...rest } = props;
|
||||||
|
const [width, setWidth] = useState(w || 0);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const getImgRatio = useCallback((url: string): Promise<{ ratio: number }> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
resolve({ ratio: round(img.width / img.height, 2) });
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
resolve({ ratio: 1 });
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOnLoad = useCallback(async () => {
|
||||||
|
if (autoSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { ratio } = await getImgRatio(props.src || '');
|
||||||
|
|
||||||
|
if (typeof height === 'number') {
|
||||||
|
setWidth(height * ratio);
|
||||||
|
} else {
|
||||||
|
throw new Error('Height must be a number');
|
||||||
|
}
|
||||||
|
}, [getImgRatio, height, props.src]);
|
||||||
|
|
||||||
|
const onDownload = useCallback(() => {
|
||||||
|
const url = props.src || '';
|
||||||
|
const filename = Date.now() + '';
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}, [props.src]);
|
||||||
|
|
||||||
|
const handleImgLoad = useCallback(() => {
|
||||||
|
props.onLoad?.();
|
||||||
|
setIsError(false);
|
||||||
|
}, [props.onLoad]);
|
||||||
|
|
||||||
|
const handleOnError = useCallback(() => {
|
||||||
|
setIsError(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleOnLoad();
|
||||||
|
}, [handleOnLoad]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setWidth(w || 0);
|
||||||
|
}, [w]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AntImage
|
||||||
|
{...rest}
|
||||||
|
height={isError ? 'auto' : height}
|
||||||
|
width={isError ? '100%' : width}
|
||||||
|
onError={handleOnError}
|
||||||
|
onLoad={handleImgLoad}
|
||||||
|
fallback={fallbackImg}
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
preview={
|
||||||
|
preview &&
|
||||||
|
!isError && {
|
||||||
|
mask: <EyeOutlined />,
|
||||||
|
toolbarRender: (
|
||||||
|
_,
|
||||||
|
{
|
||||||
|
transform: { scale },
|
||||||
|
actions: {
|
||||||
|
onFlipY,
|
||||||
|
onFlipX,
|
||||||
|
onRotateLeft,
|
||||||
|
onRotateRight,
|
||||||
|
onZoomOut,
|
||||||
|
onZoomIn,
|
||||||
|
onReset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) => (
|
||||||
|
<Space size={12} className="toolbar-wrapper">
|
||||||
|
<DownloadOutlined onClick={onDownload} />
|
||||||
|
<SwapOutlined rotate={90} onClick={onFlipY} />
|
||||||
|
<SwapOutlined onClick={onFlipX} />
|
||||||
|
<RotateLeftOutlined onClick={onRotateLeft} />
|
||||||
|
<RotateRightOutlined onClick={onRotateRight} />
|
||||||
|
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
|
||||||
|
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
|
||||||
|
<UndoOutlined onClick={onReset} />
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(AutoImage);
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.img-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-wrapper .auto-image {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-wrapper .progress-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-square {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-square-bg {
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-square-fg {
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: square;
|
||||||
|
stroke-dasharray: 400;
|
||||||
|
stroke-dashoffset: 400;
|
||||||
|
transition: stroke-dashoffset 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
position: absolute;
|
||||||
|
color: black;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
.thumb-img {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0 8px;
|
||||||
|
background-color: var(--ant-geekblue-1);
|
||||||
|
z-index: 10;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-progress-wrap {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(0, 0, 0, 30%);
|
||||||
|
|
||||||
|
.ant-progress-text {
|
||||||
|
color: var(--color-white-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
display: flex;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
cursor: pointer;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -2px;
|
||||||
|
font-size: var(--font-size-middle);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color-white-1);
|
||||||
|
display: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.ant-image .ant-image-mask {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity var(--ant-motion-duration-slow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-image {
|
||||||
|
// height: 100%;
|
||||||
|
width: inherit;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.auto-bg-color {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
filter: blur(100px);
|
||||||
|
backdrop-filter: blur(100px);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-img {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-image {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { Progress, ProgressProps } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { round } from 'lodash';
|
||||||
|
import ResizeObserver from 'rc-resize-observer';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import AutoImage from './index';
|
||||||
|
import './single-image.less';
|
||||||
|
interface SingleImageProps {
|
||||||
|
loading?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
progress?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
dataUrl: string;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
uid: number;
|
||||||
|
preview?: boolean;
|
||||||
|
autoSize?: boolean;
|
||||||
|
onDelete: (uid: number) => void;
|
||||||
|
onClick?: (item: any) => void;
|
||||||
|
autoBgColor?: boolean;
|
||||||
|
editable?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
loadingSize?: ProgressProps['size'];
|
||||||
|
progressType?: 'line' | 'circle' | 'dashboard';
|
||||||
|
progressColor?: string;
|
||||||
|
progressWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SingleImage: React.FC<SingleImageProps> = (props) => {
|
||||||
|
const {
|
||||||
|
editable,
|
||||||
|
onDelete: handleOnDelete,
|
||||||
|
onClick,
|
||||||
|
autoSize,
|
||||||
|
uid,
|
||||||
|
loading,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
progress,
|
||||||
|
maxHeight,
|
||||||
|
maxWidth,
|
||||||
|
dataUrl = '',
|
||||||
|
label,
|
||||||
|
style,
|
||||||
|
autoBgColor,
|
||||||
|
preview = true,
|
||||||
|
loadingSize = 'default'
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const imgWrapper = React.useRef<HTMLSpanElement>(null);
|
||||||
|
const [imgSize, setImgSize] = React.useState({
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
});
|
||||||
|
|
||||||
|
const thumImgWrapStyle = React.useMemo(() => {
|
||||||
|
return loading ? { width: '100%', height: '100%' } : {};
|
||||||
|
}, [loading, imgSize]);
|
||||||
|
|
||||||
|
const handleOnClick = useCallback(() => {
|
||||||
|
onClick?.(props);
|
||||||
|
}, [onClick, props]);
|
||||||
|
|
||||||
|
const handleResize = useCallback(
|
||||||
|
(size: { width: number; height: number }) => {
|
||||||
|
if (!autoSize || !size.width || !size.height) return;
|
||||||
|
|
||||||
|
const { width: containerWidth, height: containerHeight } = size;
|
||||||
|
const { width: originalWidth, height: originalHeight } = props;
|
||||||
|
|
||||||
|
if (!originalWidth || !originalHeight) return;
|
||||||
|
|
||||||
|
const widthRatio = containerWidth / originalWidth;
|
||||||
|
const heightRatio = containerHeight / originalHeight;
|
||||||
|
|
||||||
|
const scale = Math.min(widthRatio, heightRatio, 1);
|
||||||
|
|
||||||
|
const newWidth = originalWidth * scale;
|
||||||
|
const newHeight = originalHeight * scale;
|
||||||
|
|
||||||
|
if (newWidth === imgSize.width && newHeight === imgSize.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setImgSize({
|
||||||
|
width: newWidth,
|
||||||
|
height: newHeight
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[autoSize, props.width, props.height]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOnLoad = React.useCallback(async () => {}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResizeObserver onResize={handleResize}>
|
||||||
|
<div
|
||||||
|
style={{ ...style }}
|
||||||
|
key={uid}
|
||||||
|
className={classNames('single-image', {
|
||||||
|
'auto-bg-color': autoBgColor,
|
||||||
|
'auto-size': autoSize,
|
||||||
|
loading: loading
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{autoBgColor && (
|
||||||
|
<div
|
||||||
|
className="mask"
|
||||||
|
style={{
|
||||||
|
background: `url(${dataUrl}) center center / cover no-repeat`
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className="thumb-img"
|
||||||
|
style={{ ...thumImgWrapStyle }}
|
||||||
|
ref={imgWrapper}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{label && <div className="label">{label}</div>}
|
||||||
|
{loading ? (
|
||||||
|
<span
|
||||||
|
className="progress-wrap"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
border: '1px solid var(--ant-color-split)',
|
||||||
|
borderRadius: 'var(--border-radius-base)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '10px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Progress
|
||||||
|
percent={round(progress, 0)}
|
||||||
|
type="dashboard"
|
||||||
|
size={loadingSize}
|
||||||
|
steps={{ count: 50, gap: 2 }}
|
||||||
|
format={() => (
|
||||||
|
<span className="font-size-20">{round(progress, 0)}%</span>
|
||||||
|
)}
|
||||||
|
trailColor="var(--ant-color-fill-secondary)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
onClick={handleOnClick}
|
||||||
|
className="img"
|
||||||
|
style={{
|
||||||
|
maxHeight: `min(${maxHeight}, 100%)`,
|
||||||
|
maxWidth: `min(${maxWidth}, 100%)`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AutoImage
|
||||||
|
style={{ objectFit: 'cover' }}
|
||||||
|
preview={preview}
|
||||||
|
autoSize={autoSize}
|
||||||
|
src={dataUrl}
|
||||||
|
width={imgSize.width || '100%'}
|
||||||
|
height={imgSize.height || 100}
|
||||||
|
onLoad={handleOnLoad}
|
||||||
|
/>
|
||||||
|
{progress && progress < 100 && (
|
||||||
|
<span className="small-progress-wrap">
|
||||||
|
<Progress
|
||||||
|
percent={round(progress, 0)}
|
||||||
|
type="dashboard"
|
||||||
|
size="small"
|
||||||
|
steps={{ count: 25, gap: 3 }}
|
||||||
|
format={() => (
|
||||||
|
<span className="font-size-12">
|
||||||
|
{round(progress, 0)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
strokeColor="var(--color-white-secondary)"
|
||||||
|
trailColor="var(--ant-color-fill-secondary)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
|
||||||
|
{editable && (
|
||||||
|
<span className="del" onClick={() => handleOnDelete(uid)}>
|
||||||
|
<CloseCircleOutlined />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ResizeObserver>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SingleImage;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Tag, Tooltip } from 'antd';
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
import debounce from 'lodash/debounce';
|
import { Tag, Tooltip, type TagProps } from 'antd';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -7,46 +8,83 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { TooltipOverlayScroller } from '../overlay-scroller';
|
||||||
|
|
||||||
interface AutoTooltipProps extends React.ComponentProps<typeof Tag> {
|
// type TagProps = React.ComponentProps<typeof Tag>;
|
||||||
|
|
||||||
|
interface AutoTooltipProps extends Omit<TagProps, 'title'> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
|
minWidth?: number | string;
|
||||||
color?: string;
|
color?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
ghost?: boolean;
|
ghost?: boolean;
|
||||||
|
title?: React.ReactNode;
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
radius?: number | string;
|
||||||
|
filled?: boolean;
|
||||||
|
tooltipProps?: React.ComponentProps<typeof Tooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledTag = styled(Tag)`
|
||||||
|
&.tag-filled {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--ant-color-fill-secondary);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
||||||
children,
|
children,
|
||||||
maxWidth = '100%',
|
maxWidth = '100%',
|
||||||
|
minWidth,
|
||||||
ghost = false,
|
ghost = false,
|
||||||
|
title,
|
||||||
showTitle = false,
|
showTitle = false,
|
||||||
|
tooltipProps,
|
||||||
|
radius = 12,
|
||||||
|
filled = false,
|
||||||
...tagProps
|
...tagProps
|
||||||
}) => {
|
}) => {
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||||
|
const resizeObserver = useRef<ResizeObserver | null>(null);
|
||||||
|
|
||||||
const checkOverflow = useCallback(() => {
|
const checkOverflow = useCallback(() => {
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
const { scrollWidth, clientWidth } = contentRef.current;
|
const { scrollWidth, clientWidth } = contentRef.current;
|
||||||
setIsOverflowing(scrollWidth > clientWidth);
|
setIsOverflowing(scrollWidth > clientWidth);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [contentRef.current]);
|
||||||
|
|
||||||
const debouncedCheckOverflow = useMemo(
|
|
||||||
() => debounce(checkOverflow, 200),
|
|
||||||
[checkOverflow]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const element = contentRef.current;
|
||||||
|
if (!element) return;
|
||||||
|
resizeObserver.current?.disconnect();
|
||||||
|
resizeObserver.current = new ResizeObserver(() => {
|
||||||
checkOverflow();
|
checkOverflow();
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.current?.observe(element);
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
checkOverflow();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.current?.disconnect();
|
||||||
|
resizeObserver.current = null;
|
||||||
|
};
|
||||||
|
}, [checkOverflow]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const debouncedCheckOverflow = throttle(checkOverflow, 200);
|
||||||
window.addEventListener('resize', debouncedCheckOverflow);
|
window.addEventListener('resize', debouncedCheckOverflow);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', debouncedCheckOverflow);
|
window.removeEventListener('resize', debouncedCheckOverflow);
|
||||||
debouncedCheckOverflow.cancel();
|
debouncedCheckOverflow.cancel();
|
||||||
};
|
};
|
||||||
}, [checkOverflow, debouncedCheckOverflow]);
|
}, [checkOverflow]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkOverflow();
|
checkOverflow();
|
||||||
@@ -55,6 +93,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
|||||||
const tagStyle = useMemo(
|
const tagStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
maxWidth,
|
maxWidth,
|
||||||
|
minWidth,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap' as const,
|
whiteSpace: 'nowrap' as const,
|
||||||
@@ -64,17 +103,46 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={isOverflowing || showTitle ? children : ''}>
|
<TooltipOverlayScroller
|
||||||
|
toolTipProps={{
|
||||||
|
...tooltipProps,
|
||||||
|
destroyTooltipOnHide: false
|
||||||
|
}}
|
||||||
|
title={isOverflowing || showTitle ? title || children : false}
|
||||||
|
>
|
||||||
{ghost ? (
|
{ghost ? (
|
||||||
<div ref={contentRef} style={tagStyle}>
|
<div ref={contentRef} style={tagStyle} data-overflow={isOverflowing}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Tag {...tagProps} ref={contentRef} style={tagStyle}>
|
<StyledTag
|
||||||
|
{...tagProps}
|
||||||
|
className={`${tagProps.className || ''} ${filled ? 'tag-filled' : ''}`}
|
||||||
|
ref={contentRef}
|
||||||
|
style={{
|
||||||
|
...tagStyle,
|
||||||
|
paddingInline: tagProps.closable ? '8px 22px' : 8,
|
||||||
|
borderRadius: radius
|
||||||
|
}}
|
||||||
|
closeIcon={
|
||||||
|
tagProps.closable ? (
|
||||||
|
<CloseOutlined
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 8,
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Tag>
|
</StyledTag>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</TooltipOverlayScroller>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { OverlayScroller } from '@/components/overlay-scroller';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TitleTipProps {
|
||||||
|
isOverflowing: boolean;
|
||||||
|
showTitle: boolean;
|
||||||
|
title: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TitleTip: React.FC<TitleTipProps> = (props) => {
|
||||||
|
const { isOverflowing, showTitle, title, children } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayScroller maxHeight={200}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 'fit-content',
|
||||||
|
maxWidth: 'var(--width-tooltip-max)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isOverflowing || showTitle ? title || children : ''}
|
||||||
|
</div>
|
||||||
|
</OverlayScroller>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TitleTip);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import bibtexParse from '@orcid/bibtex-parse-js';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/*
|
||||||
|
@inproceedings{Lysenko:2010:GMC:1839778.1839781,\
|
||||||
|
author = {Lysenko, Mikola and Nelaturi, Saigopal and Shapiro, Vadim},\
|
||||||
|
title = {Group morphology with convolution algebras},\
|
||||||
|
booktitle = {Proceedings of the 14th ACM Symposium on Solid and Physical Modeling},\
|
||||||
|
series = {SPM '10},\
|
||||||
|
year = {2010},\
|
||||||
|
isbn = {978-1-60558-984-8},\
|
||||||
|
location = {Haifa, Israel},\
|
||||||
|
pages = {11--22},\
|
||||||
|
numpages = {12},\
|
||||||
|
url = {http://doi.acm.org/10.1145/1839778.1839781},\
|
||||||
|
doi = {10.1145/1839778.1839781},\
|
||||||
|
acmid = {1839781},\
|
||||||
|
publisher = {ACM},\
|
||||||
|
address = {New York, NY, USA},\
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BibTeXViewer: React.FC<{ data: string }> = ({ data }) => {
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const dataList = bibtexParse.toJSON(data);
|
||||||
|
return (
|
||||||
|
<ol>
|
||||||
|
{dataList.map((item: any, index: number) => (
|
||||||
|
<li key={index} style={{ lineHeight: 2 }}>
|
||||||
|
<Typography.Link href={item.entryTags?.url} target="_blank">
|
||||||
|
{item.entryTags?.title}.{' '}
|
||||||
|
</Typography.Link>
|
||||||
|
<Typography.Text>{item.entryTags?.author}. </Typography.Text>
|
||||||
|
<Typography.Text>[{item.entryTags?.year}] </Typography.Text>
|
||||||
|
{item.entryTags?.journal && (
|
||||||
|
<Typography.Text>.({item.entryTags?.journal})</Typography.Text>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BibTeXViewer;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { DoubleRightOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
interface MoreButtonProps {
|
||||||
|
show: boolean;
|
||||||
|
loadMore: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MoreWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-block: 16px;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
&.loading {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MoreButton: React.FC<MoreButtonProps> = (props) => {
|
||||||
|
const { show, loading, loadMore } = props;
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{show ? (
|
||||||
|
<MoreWrapper className={loading ? 'loading' : ''}>
|
||||||
|
<Button
|
||||||
|
onClick={loadMore}
|
||||||
|
size="middle"
|
||||||
|
type="text"
|
||||||
|
icon={<DoubleRightOutlined rotate={90} />}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: 'common.button.more' })}
|
||||||
|
</Button>
|
||||||
|
</MoreWrapper>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MoreButton;
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
background-color: var(--color-white-1);
|
background-color: var(--color-white-1);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid var(--color-border-1);
|
border: 1px solid var(--ant-color-border);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const SimpleCardItemWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
gap: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => ({
|
||||||
|
wrapper: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: ${token.colorBgContainer};
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
padding: ${token.padding}px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${token.padding}px;
|
||||||
|
&.bordered {
|
||||||
|
border: 1px solid ${token.colorBorder};
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: ${token.fontSize}px;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: ${token.fontSize}px;
|
||||||
|
color: ${token.colorTextSecondary};
|
||||||
|
gap: 8px;
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
&.roundRect {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
&.circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}));
|
||||||
|
export const SimpleCardItem: React.FC<{
|
||||||
|
title?: string;
|
||||||
|
content?: React.ReactNode;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
bordered?: boolean;
|
||||||
|
color?: string;
|
||||||
|
iconType?: string;
|
||||||
|
}> = (props) => {
|
||||||
|
const { styles, cx } = useStyles();
|
||||||
|
|
||||||
|
const { title, content, style, bordered, iconType, color } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx({ bordered: bordered }, styles.wrapper)} style={style}>
|
||||||
|
<div className="title">{title}</div>
|
||||||
|
<div className="content">
|
||||||
|
<span
|
||||||
|
className={cx([iconType], 'icon')}
|
||||||
|
style={{
|
||||||
|
backgroundColor: color || 'transparent'
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
<span>{content}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SimpleCard: React.FC<{
|
||||||
|
dataList: {
|
||||||
|
label: string;
|
||||||
|
value: React.ReactNode;
|
||||||
|
color: string;
|
||||||
|
iconType: string;
|
||||||
|
}[];
|
||||||
|
height?: string | number;
|
||||||
|
bordered?: boolean;
|
||||||
|
}> = (props) => {
|
||||||
|
const { dataList, bordered } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleCardItemWrapper style={{ height: props.height || '100%' }}>
|
||||||
|
{dataList.map((item, index) => (
|
||||||
|
<SimpleCardItem
|
||||||
|
key={index}
|
||||||
|
title={item.label}
|
||||||
|
content={item.value}
|
||||||
|
bordered={bordered}
|
||||||
|
color={item.color}
|
||||||
|
iconType={item.iconType}
|
||||||
|
></SimpleCardItem>
|
||||||
|
))}
|
||||||
|
</SimpleCardItemWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CheckButtonsProps {
|
||||||
|
options: Global.BaseOption<string | number>[];
|
||||||
|
onChange: (value: string | number) => void;
|
||||||
|
cancelable?: boolean;
|
||||||
|
size?: 'small' | 'middle' | 'large';
|
||||||
|
type?: 'text' | 'primary' | 'default' | 'dashed' | 'link' | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckButtons: React.FC<CheckButtonsProps> = (props) => {
|
||||||
|
const [type, setType] = React.useState(props.type || 'text');
|
||||||
|
const [active, setActive] = React.useState<string | number | null>(null);
|
||||||
|
const handleChange = (value: string | number) => {
|
||||||
|
props.onChange(value);
|
||||||
|
if (props.cancelable && active === value) {
|
||||||
|
setActive(null);
|
||||||
|
} else {
|
||||||
|
setActive(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex-center gap-6">
|
||||||
|
{props.options?.map?.((option, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size={props.size}
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleChange(option.value)}
|
||||||
|
variant="filled"
|
||||||
|
color={active === option.value ? 'default' : undefined}
|
||||||
|
type={type}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(CheckButtons);
|
||||||
@@ -2,25 +2,41 @@ import { CheckCircleFilled, CopyOutlined } from '@ant-design/icons';
|
|||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Tooltip, message } from 'antd';
|
import { Button, Tooltip, message } from 'antd';
|
||||||
import ClipboardJS from 'clipboard';
|
import ClipboardJS from 'clipboard';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
type CopyButtonProps = {
|
type CopyButtonProps = {
|
||||||
|
children?: React.ReactNode;
|
||||||
text: string;
|
text: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
fontSize?: string;
|
fontSize?: string;
|
||||||
type?: 'text' | 'primary' | 'dashed' | 'link' | 'default';
|
type?: 'text' | 'primary' | 'dashed' | 'link' | 'default';
|
||||||
size?: 'small' | 'middle' | 'large';
|
size?: 'small' | 'middle' | 'large';
|
||||||
shape?: 'circle' | 'round' | 'default';
|
shape?: 'circle' | 'round' | 'default';
|
||||||
|
tips?: string;
|
||||||
|
placement?:
|
||||||
|
| 'top'
|
||||||
|
| 'left'
|
||||||
|
| 'right'
|
||||||
|
| 'bottom'
|
||||||
|
| 'topLeft'
|
||||||
|
| 'topRight'
|
||||||
|
| 'bottomLeft'
|
||||||
|
| 'bottomRight';
|
||||||
|
btnStyle?: React.CSSProperties;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CopyButton: React.FC<CopyButtonProps> = ({
|
const CopyButton: React.FC<CopyButtonProps> = ({
|
||||||
|
children,
|
||||||
|
tips,
|
||||||
text,
|
text,
|
||||||
disabled,
|
disabled,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
shape = 'circle',
|
shape = 'default',
|
||||||
fontSize = '14px',
|
fontSize = '14px',
|
||||||
style,
|
style,
|
||||||
|
btnStyle,
|
||||||
|
placement,
|
||||||
size = 'middle'
|
size = 'middle'
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -47,6 +63,13 @@ const CopyButton: React.FC<CopyButtonProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tipTitle = useMemo(() => {
|
||||||
|
if (copied) {
|
||||||
|
return intl.formatMessage({ id: 'common.button.copied' });
|
||||||
|
}
|
||||||
|
return tips ?? intl.formatMessage({ id: 'common.button.copy' });
|
||||||
|
}, [copied, intl, tips]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initClipboard();
|
initClipboard();
|
||||||
return () => {
|
return () => {
|
||||||
@@ -55,7 +78,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
|
|||||||
}, [buttonRef]);
|
}, [buttonRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={intl.formatMessage({ id: 'common.button.copy' })}>
|
<Tooltip title={tipTitle} placement={placement}>
|
||||||
<Button
|
<Button
|
||||||
className="copy-button"
|
className="copy-button"
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
@@ -64,6 +87,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
|
|||||||
size={size}
|
size={size}
|
||||||
disabled={!!disabled}
|
disabled={!!disabled}
|
||||||
data-clipboard-text={text}
|
data-clipboard-text={text}
|
||||||
|
style={{ ...btnStyle }}
|
||||||
icon={
|
icon={
|
||||||
copied ? (
|
copied ? (
|
||||||
<CheckCircleFilled
|
<CheckCircleFilled
|
||||||
@@ -73,7 +97,9 @@ const CopyButton: React.FC<CopyButtonProps> = ({
|
|||||||
<CopyOutlined style={{ fontSize: fontSize, ...style }} />
|
<CopyOutlined style={{ fontSize: fontSize, ...style }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></Button>
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { getStorageUserSettings } from '@/atoms/settings';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import styled, { keyframes } from 'styled-components';
|
||||||
|
|
||||||
|
const duration = 1;
|
||||||
|
|
||||||
|
const fadeOut = keyframes`
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Mask = styled.div<{ $isHiding: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(59, 59, 59, 1);
|
||||||
|
z-index: 9999;
|
||||||
|
animation: ${({ $isHiding }) => ($isHiding ? fadeOut : 'none')} ${duration}s
|
||||||
|
ease forwards;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DarkModeMask = () => {
|
||||||
|
const { theme: savedTheme } = getStorageUserSettings();
|
||||||
|
const [visible, setVisible] = useState(savedTheme === 'realDark');
|
||||||
|
const [isHiding, setIsHiding] = useState(false);
|
||||||
|
const maskRef = useRef<HTMLDivElement>(null);
|
||||||
|
const timerRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const initTheme = () => {
|
||||||
|
const html = document.querySelector('html');
|
||||||
|
if (html) {
|
||||||
|
html.setAttribute('data-theme', savedTheme);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (savedTheme === 'realDark') {
|
||||||
|
setVisible(true);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsHiding(true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setIsHiding(true);
|
||||||
|
}
|
||||||
|
}, [savedTheme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = maskRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const handleAnimationEnd = () => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener('animationend', handleAnimationEnd);
|
||||||
|
|
||||||
|
if (isHiding) {
|
||||||
|
timerRef.current = setTimeout(
|
||||||
|
() => {
|
||||||
|
setVisible(false);
|
||||||
|
},
|
||||||
|
duration * 1000 + 100
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return () => el.removeEventListener('animationend', handleAnimationEnd);
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return visible ? <Mask ref={maskRef} $isHiding={isHiding}></Mask> : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DarkModeMask;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
:local(.delete-modal-content) {
|
|
||||||
display: flex;
|
|
||||||
font-size: var(--font-size-middle);
|
|
||||||
|
|
||||||
:global {
|
|
||||||
.anticon {
|
|
||||||
font-size: 20px;
|
|
||||||
margin-right: 10px;
|
|
||||||
color: var(--ant-color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.content) {
|
|
||||||
padding-top: 15px;
|
|
||||||
padding-left: 30px;
|
|
||||||
}
|
|
||||||
@@ -1,43 +1,135 @@
|
|||||||
|
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Modal, Space, message, type ModalFuncProps } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
message,
|
||||||
|
type ModalFuncProps
|
||||||
|
} from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||||
import Styles from './index.less';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css }) => ({
|
||||||
|
'delete-modal-content': css`
|
||||||
|
display: flex;
|
||||||
|
font-size: var(--font-size-middle);
|
||||||
|
.anticon {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
color: var(--ant-color-warning);
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: var(--font-weight-500);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
content: css`
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-left: 30px;
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
white-space: pre-line;
|
||||||
|
word-break: break-all;
|
||||||
|
span {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
display: flex;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CheckboxWrapper = styled.div`
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-left: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
.check-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ant-color-warning);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface DataOptions {
|
||||||
|
content?: string;
|
||||||
|
selection?: boolean;
|
||||||
|
name?: string;
|
||||||
|
okText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
title?: string;
|
||||||
|
operation: string;
|
||||||
|
checkConfig?: {
|
||||||
|
checkText: string;
|
||||||
|
defautlChecked: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Configuration {
|
||||||
|
checked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const DeleteModal = forwardRef((props, ref) => {
|
const DeleteModal = forwardRef((props, ref) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [config, setConfig] = useState<
|
const [configuration, setConfiguration] = useState<Configuration>({
|
||||||
ModalFuncProps & { content: string; selection?: boolean; name?: string }
|
checked: false
|
||||||
>({});
|
});
|
||||||
|
const [config, setConfig] = useState<ModalFuncProps & DataOptions>({} as any);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
const show = (data: ModalFuncProps & DataOptions) => {
|
||||||
show: (
|
saveScrollHeight();
|
||||||
data: ModalFuncProps & {
|
|
||||||
content: string;
|
|
||||||
selection?: boolean;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
setConfig(data);
|
setConfig(data);
|
||||||
|
setConfiguration({
|
||||||
|
checked: data.checkConfig?.defautlChecked || false
|
||||||
|
});
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
},
|
};
|
||||||
hide: () => {
|
|
||||||
|
const hide = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}
|
restoreScrollHeight();
|
||||||
}));
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
config.onCancel?.();
|
config.onCancel?.();
|
||||||
|
restoreScrollHeight();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
await config.onOk?.();
|
try {
|
||||||
|
const res = await config.onOk?.();
|
||||||
|
const isArray = Array.isArray(res);
|
||||||
|
if (isArray) {
|
||||||
|
const allSuccess = res.every(
|
||||||
|
(item: any) => item?.status === 'fulfilled'
|
||||||
|
);
|
||||||
|
if (allSuccess) {
|
||||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Handle error if needed
|
||||||
|
} finally {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
|
restoreScrollHeight();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
configuration
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
style={{
|
style={{
|
||||||
@@ -46,49 +138,73 @@ const DeleteModal = forwardRef((props, ref) => {
|
|||||||
open={visible}
|
open={visible}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
destroyOnClose={true}
|
destroyOnClose={false}
|
||||||
closeIcon={false}
|
closeIcon={false}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
keyboard={false}
|
keyboard={false}
|
||||||
width={460}
|
width={460}
|
||||||
styles={{}}
|
styles={{
|
||||||
|
footer: {
|
||||||
|
marginTop: '20px'
|
||||||
|
}
|
||||||
|
}}
|
||||||
footer={
|
footer={
|
||||||
<Space size={20}>
|
<Space size={20}>
|
||||||
<Button onClick={handleCancel} size="middle">
|
<Button onClick={handleCancel} size="middle">
|
||||||
{intl.formatMessage({ id: 'common.button.cancel' })}
|
{config.cancelText
|
||||||
|
? intl.formatMessage({ id: config.cancelText })
|
||||||
|
: intl.formatMessage({ id: 'common.button.cancel' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="primary" onClick={handleOk} size="middle">
|
<Button type="primary" onClick={handleOk} size="middle">
|
||||||
{intl.formatMessage({ id: 'common.button.delete' })}
|
{config.okText
|
||||||
|
? intl.formatMessage({ id: config.okText })
|
||||||
|
: intl.formatMessage({ id: 'common.button.delete' })}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={Styles['delete-modal-content']}>
|
<div className={styles['delete-modal-content']}>
|
||||||
<span className="title">
|
<span className="title">
|
||||||
<ExclamationCircleFilled />
|
<ExclamationCircleFilled />
|
||||||
<span>
|
<span>
|
||||||
{intl.formatMessage({ id: 'common.title.delete.confirm' })}
|
{config.title
|
||||||
|
? intl.formatMessage({ id: config.title })
|
||||||
|
: intl.formatMessage({ id: 'common.title.delete.confirm' })}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={Styles['content']}
|
className={styles['content']}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html:
|
__html: config.content
|
||||||
config.content &&
|
? intl.formatMessage(
|
||||||
intl.formatMessage(
|
|
||||||
{
|
{
|
||||||
id: config.selection
|
id: config.operation || ''
|
||||||
? 'common.delete.confirm'
|
|
||||||
: 'common.delete.single.confirm'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: intl.formatMessage({ id: config.content }),
|
type: intl.formatMessage({ id: config.content }),
|
||||||
name: config.name
|
name: config.name
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
: ''
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
{config.checkConfig && (
|
||||||
|
<CheckboxWrapper>
|
||||||
|
<Checkbox
|
||||||
|
checked={configuration.checked}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConfiguration({
|
||||||
|
checked: e.target.checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="check-text">
|
||||||
|
{intl.formatMessage({ id: config.checkConfig?.checkText })}
|
||||||
|
</span>
|
||||||
|
</Checkbox>
|
||||||
|
</CheckboxWrapper>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
import * as icons from '@ant-design/icons';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Dropdown, Space } from 'antd';
|
import { Dropdown, DropDownProps } from 'antd';
|
||||||
import type { MenuProps } from 'antd/es/menu';
|
import _ from 'lodash';
|
||||||
import react from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
type DropdownProps = {
|
const DropDownActions: React.FC<DropDownProps> = (props) => {
|
||||||
trigger?: Array<'click' | 'hover' | 'contextMenu'>;
|
const {
|
||||||
items: MenuProps['items'];
|
menu,
|
||||||
onClick: (e: any) => void;
|
trigger = ['hover'],
|
||||||
};
|
placement = 'bottomRight',
|
||||||
const renderIcon = (icon: string) => {
|
children,
|
||||||
if (!icon) {
|
...rest
|
||||||
return null;
|
} = props;
|
||||||
}
|
const intl = useIntl();
|
||||||
// @ts-ignore
|
|
||||||
const Icon = icons[icon] as React.FC;
|
const items = useMemo(() => {
|
||||||
return react.createElement(Icon);
|
return menu?.items?.map((item: any) => ({
|
||||||
};
|
..._.omit(item, 'locale'),
|
||||||
const DropDownActions: React.FC<DropdownProps> = (props) => {
|
label: item.locale ? intl.formatMessage({ id: item.label }) : item.label
|
||||||
const { trigger, items, onClick } = props;
|
}));
|
||||||
|
}, [menu?.items, intl]);
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Dropdown
|
||||||
<Dropdown menu={{ items, onClick }} trigger={trigger}>
|
menu={{
|
||||||
<Button
|
items: items,
|
||||||
onClick={(e) => e.preventDefault()}
|
onClick: menu?.onClick
|
||||||
style={{ fontSize: '14px' }}
|
}}
|
||||||
type="text"
|
trigger={trigger}
|
||||||
icon={renderIcon('MoreOutlined')}
|
placement={placement}
|
||||||
></Button>
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Space>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.dropdown-button.middle {
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
}
|
||||||
@@ -1,23 +1,39 @@
|
|||||||
import { MoreOutlined } from '@ant-design/icons';
|
import { MoreOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Dropdown, Tooltip, type MenuProps } from 'antd';
|
import { Button, Dropdown, Tooltip, type MenuProps } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { memo } from 'react';
|
import React from 'react';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
type Trigger = 'click' | 'hover';
|
||||||
interface DropdownButtonsProps {
|
interface DropdownButtonsProps {
|
||||||
items: MenuProps['items'];
|
items: MenuProps['items'];
|
||||||
size?: 'small' | 'middle' | 'large';
|
size?: 'small' | 'middle' | 'large';
|
||||||
|
trigger?: Trigger[];
|
||||||
|
showText?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
variant?: 'filled' | 'outlined';
|
||||||
|
color?: string;
|
||||||
|
extra?: React.ReactNode;
|
||||||
onSelect: (val: any, item?: any) => void;
|
onSelect: (val: any, item?: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
||||||
items,
|
items,
|
||||||
size = 'small',
|
size = 'middle',
|
||||||
|
trigger = ['hover'],
|
||||||
|
showText,
|
||||||
|
disabled,
|
||||||
|
variant,
|
||||||
|
color,
|
||||||
|
extra,
|
||||||
onSelect
|
onSelect
|
||||||
}) => {
|
}) => {
|
||||||
|
const headItem = _.head(items);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleMenuClick = (item: any) => {
|
const handleMenuClick = (item: any) => {
|
||||||
console.log('menu click', item.key);
|
|
||||||
const selectItem = _.find(items, { key: item.key });
|
const selectItem = _.find(items, { key: item.key });
|
||||||
onSelect(item.key, selectItem);
|
onSelect(item.key, selectItem);
|
||||||
};
|
};
|
||||||
@@ -34,23 +50,26 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{items?.length === 1 ? (
|
{items?.length === 1 ? (
|
||||||
<Tooltip title={intl.formatMessage({ id: _.head(items)?.label })}>
|
<Tooltip title={intl.formatMessage({ id: headItem?.label })}>
|
||||||
<Button
|
<Button
|
||||||
{..._.head(items)}
|
className={classNames('dropdown-button', size)}
|
||||||
icon={_.get(items, '0.icon')}
|
icon={headItem?.icon}
|
||||||
size={size}
|
size={size}
|
||||||
{..._.get(items, '0.props')}
|
{...headItem?.props}
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
></Button>
|
></Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown.Button
|
<Dropdown.Button
|
||||||
|
disabled={disabled}
|
||||||
|
trigger={trigger}
|
||||||
|
type="primary"
|
||||||
dropdownRender={(menus: any) => {
|
dropdownRender={(menus: any) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-column "
|
className="flex flex-column "
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-white-1)',
|
backgroundColor: 'var(--ant-color-bg-elevated)',
|
||||||
padding: 5,
|
padding: 5,
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
borderRadius: 'var(--border-radius-base)',
|
borderRadius: 'var(--border-radius-base)',
|
||||||
@@ -62,9 +81,10 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
{...item.props}
|
{...item.props}
|
||||||
type="text"
|
type="text"
|
||||||
size="middle"
|
size={size}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
key={item.key}
|
key={item.key}
|
||||||
|
disabled={item.disabled}
|
||||||
onClick={() => handleMenuClick(item)}
|
onClick={() => handleMenuClick(item)}
|
||||||
style={{ width: '100%', justifyContent: 'flex-start' }}
|
style={{ width: '100%', justifyContent: 'flex-start' }}
|
||||||
>
|
>
|
||||||
@@ -76,17 +96,47 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
buttonsRender={([leftButton, rightButton]) => [
|
buttonsRender={([leftButton, rightButton]) => [
|
||||||
|
<>
|
||||||
|
{showText ? (
|
||||||
|
<Button
|
||||||
|
{...headItem?.props}
|
||||||
|
disabled={headItem?.disabled || disabled}
|
||||||
|
className={classNames('dropdown-button', size)}
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
size={size}
|
||||||
|
icon={headItem?.icon}
|
||||||
|
variant={variant}
|
||||||
|
color={color}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: headItem?.label
|
||||||
|
})}
|
||||||
|
{extra}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={intl.formatMessage({ id: _.head(items)?.label })}
|
title={intl.formatMessage({ id: headItem?.label })}
|
||||||
key="leftButton"
|
key="leftButton"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
{...headItem?.props}
|
||||||
|
className={classNames('dropdown-button', size)}
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
size={size}
|
size={size}
|
||||||
icon={_.head(items)?.icon}
|
icon={headItem?.icon}
|
||||||
|
disabled={headItem?.disabled}
|
||||||
|
></Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>,
|
||||||
|
<Button
|
||||||
|
icon={<MoreOutlined />}
|
||||||
|
size={size}
|
||||||
|
key="menu"
|
||||||
|
variant={variant}
|
||||||
|
color="default"
|
||||||
|
className={classNames('dropdown-button', size)}
|
||||||
></Button>
|
></Button>
|
||||||
</Tooltip>,
|
|
||||||
<Button icon={<MoreOutlined />} size={size} key="menu"></Button>
|
|
||||||
]}
|
]}
|
||||||
></Dropdown.Button>
|
></Dropdown.Button>
|
||||||
)}
|
)}
|
||||||
@@ -94,4 +144,4 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(DropdownButtons);
|
export default DropdownButtons;
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
import Chart from '@/components/echarts/chart';
|
||||||
import {
|
import useChartConfig from '@/components/echarts/config';
|
||||||
barItemConfig,
|
|
||||||
grid,
|
|
||||||
legend,
|
|
||||||
title as titleConfig,
|
|
||||||
tooltip,
|
|
||||||
xAxis,
|
|
||||||
yAxis
|
|
||||||
} from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
import EmptyData from '@/components/empty-data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { memo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import { ChartProps } from './types';
|
import { ChartProps } from './types';
|
||||||
|
|
||||||
const BarChart: React.FC<ChartProps> = (props) => {
|
const BarChart: React.FC<ChartProps> = (props) => {
|
||||||
@@ -23,11 +15,17 @@ const BarChart: React.FC<ChartProps> = (props) => {
|
|||||||
legendData,
|
legendData,
|
||||||
title
|
title
|
||||||
} = props;
|
} = props;
|
||||||
|
const {
|
||||||
|
barItemConfig,
|
||||||
|
grid,
|
||||||
|
legend,
|
||||||
|
title: titleConfig,
|
||||||
|
tooltip,
|
||||||
|
xAxis,
|
||||||
|
yAxis
|
||||||
|
} = useChartConfig();
|
||||||
|
|
||||||
if (!seriesData.length) {
|
const dataOptions = useMemo((): any => {
|
||||||
return <EmptyData height={height} title={title}></EmptyData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
title: {
|
title: {
|
||||||
text: ''
|
text: ''
|
||||||
@@ -35,9 +33,6 @@ const BarChart: React.FC<ChartProps> = (props) => {
|
|||||||
grid,
|
grid,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
...tooltip
|
...tooltip
|
||||||
// axisPointer: {
|
|
||||||
// type: 'shadow'
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
...xAxis,
|
...xAxis,
|
||||||
@@ -55,7 +50,6 @@ const BarChart: React.FC<ChartProps> = (props) => {
|
|||||||
|
|
||||||
series: []
|
series: []
|
||||||
};
|
};
|
||||||
const setDataOptions = () => {
|
|
||||||
const data = _.map(seriesData, (item: any) => {
|
const data = _.map(seriesData, (item: any) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@@ -82,16 +76,31 @@ const BarChart: React.FC<ChartProps> = (props) => {
|
|||||||
},
|
},
|
||||||
series: data
|
series: data
|
||||||
};
|
};
|
||||||
};
|
}, [
|
||||||
|
seriesData,
|
||||||
const dataOptions: any = setDataOptions();
|
xAxisData,
|
||||||
|
title,
|
||||||
|
labelFormatter,
|
||||||
|
tooltip,
|
||||||
|
grid,
|
||||||
|
xAxis,
|
||||||
|
yAxis,
|
||||||
|
legend,
|
||||||
|
barItemConfig
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{!seriesData.length ? (
|
||||||
|
<EmptyData height={height} title={title}></EmptyData>
|
||||||
|
) : (
|
||||||
<Chart
|
<Chart
|
||||||
height={height}
|
height={height}
|
||||||
options={dataOptions}
|
options={dataOptions}
|
||||||
width={width || '100%'}
|
width={width || '100%'}
|
||||||
></Chart>
|
></Chart>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const TooltipWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: rgba(255, 255, 255, 80%);
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 360px;
|
||||||
|
|
||||||
|
.tooltip-x-name {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-item {
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.tooltip-item-title {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-value {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ItemSymbol = styled.span<{ $color: string }>`
|
||||||
|
background-color: ${(props) => props.$color};
|
||||||
|
display: inline-block;
|
||||||
|
marginright: 5px;
|
||||||
|
borderradius: 8px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ChartTooltipProps {
|
||||||
|
params: any[];
|
||||||
|
callback?: (val: any) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartTooltip: React.FC<ChartTooltipProps> = (props) => {
|
||||||
|
const { params, callback } = props;
|
||||||
|
console.log('params====', params);
|
||||||
|
return (
|
||||||
|
<TooltipWrapper>
|
||||||
|
<span className="tooltip-x-name">{params[0]?.axisValue}</span>
|
||||||
|
<>
|
||||||
|
{params.map((item: any, index: number) => {
|
||||||
|
let value = callback?.(item.data.value) || item.data.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="tooltip-item" key={index}>
|
||||||
|
<span className="tooltip-item-name">
|
||||||
|
<ItemSymbol $color={item.color}></ItemSymbol>
|
||||||
|
<span className="tooltip-title">{item.seriesName}</span>:
|
||||||
|
</span>
|
||||||
|
<span className="tooltip-value">{value}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</TooltipWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ChartTooltip;
|
||||||
@@ -1,58 +1,148 @@
|
|||||||
import { throttle } from 'echarts/core';
|
import _, { throttle } from 'lodash';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef
|
||||||
|
} from 'react';
|
||||||
import echarts, { ECOption } from '.';
|
import echarts, { ECOption } from '.';
|
||||||
|
|
||||||
const Chart: React.FC<{
|
const Chart: React.FC<{
|
||||||
options: ECOption;
|
options: ECOption;
|
||||||
height: number | string;
|
height: number | string;
|
||||||
width: number | string;
|
width: number | string;
|
||||||
}> = ({ options, width, height }) => {
|
ref?: any;
|
||||||
|
}> = forwardRef(({ options, width, height }, ref) => {
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
const chart = useRef<echarts.EChartsType>();
|
const chart = useRef<echarts.EChartsType>();
|
||||||
|
const resizeable = useRef(false);
|
||||||
|
const resizeObserver = useRef<ResizeObserver>();
|
||||||
|
const finished = useRef(false);
|
||||||
|
|
||||||
const init = useCallback(() => {
|
useImperativeHandle(ref, () => {
|
||||||
|
return {
|
||||||
|
chart: chart.current
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
if (container.current) {
|
if (container.current) {
|
||||||
chart.current?.clear();
|
chart.current?.clear();
|
||||||
chart.current = echarts.init(container.current);
|
chart.current = echarts.init(container.current);
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const resize = useCallback(() => {
|
const setOption = (options: ECOption) => {
|
||||||
chart.current?.resize();
|
console.log('setOption', options);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setOption = useCallback((options: ECOption) => {
|
|
||||||
chart.current?.clear();
|
chart.current?.clear();
|
||||||
chart.current?.setOption(options, {
|
chart.current?.setOption(options, {
|
||||||
notMerge: true,
|
notMerge: true,
|
||||||
lazyUpdate: true
|
lazyUpdate: true
|
||||||
});
|
});
|
||||||
|
if (Array.isArray(options.yAxis) && options.yAxis.length > 1) {
|
||||||
|
chart.current?.resize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOnFinished = () => {
|
||||||
|
if (!chart.current || finished.current) return;
|
||||||
|
|
||||||
|
const currentChart = chart.current;
|
||||||
|
const optionsYAxis = currentChart.getOption()?.yAxis;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!optionsYAxis ||
|
||||||
|
!Array.isArray(optionsYAxis) ||
|
||||||
|
optionsYAxis.length < 2
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
// @ts-ignore
|
||||||
|
const model = currentChart.getModel();
|
||||||
|
|
||||||
|
const yAxisModels = [
|
||||||
|
model.getComponent('yAxis', 0),
|
||||||
|
model.getComponent('yAxis', 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!yAxisModels[0] || !yAxisModels[1]) return;
|
||||||
|
|
||||||
|
const axes = yAxisModels.map((m) => m.axis);
|
||||||
|
|
||||||
|
const intervals = axes.map((axis) => axis.scale.getInterval());
|
||||||
|
const ticksList = axes.map((axis) => axis.scale.getTicks());
|
||||||
|
const counts = ticksList.map((t) => t.length);
|
||||||
|
|
||||||
|
const unifiedCount = Math.max(counts[0], counts[1]);
|
||||||
|
|
||||||
|
const newMax0 = intervals[0] * (unifiedCount - 1);
|
||||||
|
const newMax1 = intervals[1] * (unifiedCount - 1);
|
||||||
|
|
||||||
|
// if newMax0 equal to maxValue0, and newMax1 equal to maxValue1, do not update yAxis
|
||||||
|
if (counts[0] === counts[1]) return;
|
||||||
|
|
||||||
|
const yAxis: any[] = [{}, {}];
|
||||||
|
|
||||||
|
if (counts[0] < unifiedCount) {
|
||||||
|
yAxis[0].max = _.round(newMax0, 2);
|
||||||
|
yAxis[0].interval = intervals[0];
|
||||||
|
yAxis[0].splitNumber = unifiedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counts[1] < unifiedCount) {
|
||||||
|
yAxis[1].max = _.round(newMax1, 2);
|
||||||
|
yAxis[1].interval = intervals[1];
|
||||||
|
yAxis[1].splitNumber = unifiedCount;
|
||||||
|
}
|
||||||
|
finished.current = true;
|
||||||
|
|
||||||
|
currentChart.setOption({
|
||||||
|
yAxis: yAxis
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (container.current) {
|
||||||
|
init();
|
||||||
|
chart.current?.on('finished', handleOnFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
chart.current?.dispose();
|
||||||
|
chart.current?.off('finished', handleOnFinished);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
init();
|
resizeable.current = false;
|
||||||
return () => {
|
finished.current = false;
|
||||||
chart.current?.dispose();
|
|
||||||
};
|
|
||||||
}, [init]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
resize();
|
|
||||||
setOption(options);
|
setOption(options);
|
||||||
|
resizeable.current = true;
|
||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
// resize on window resize
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = throttle(() => {
|
const handleResize = throttle(() => {
|
||||||
resize();
|
if (resizeable.current) {
|
||||||
|
chart.current?.resize();
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
};
|
|
||||||
}, [resize]);
|
|
||||||
|
|
||||||
return <div ref={container} style={{ width: width, height }}></div>;
|
if (container.current) {
|
||||||
|
resizeObserver.current = new ResizeObserver(handleResize);
|
||||||
|
|
||||||
|
resizeObserver.current.observe(container.current);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (container.current) {
|
||||||
|
resizeObserver.current?.unobserve(container.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chart-wrapper" style={{ width: width, height }}>
|
||||||
|
<div ref={container} style={{ width: width, height }}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default Chart;
|
export default Chart;
|
||||||
|
|||||||
@@ -1,45 +1,76 @@
|
|||||||
|
import useUserSettings from '@/hooks/use-user-settings';
|
||||||
|
import { formatLargeNumber } from '@/utils';
|
||||||
|
import { theme } from 'antd';
|
||||||
import { isFunction } from 'lodash';
|
import { isFunction } from 'lodash';
|
||||||
const chartColorMap = {
|
import { useMemo } from 'react';
|
||||||
tickLineColor: 'rgba(217,217,217,0.5)',
|
|
||||||
axislabelColor: 'rgba(0, 0, 0, 0.4)'
|
export const grid = {
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 20,
|
||||||
|
containLabel: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tooltip = {
|
export default function useChartConfig() {
|
||||||
|
const { userSettings, isDarkTheme } = useUserSettings();
|
||||||
|
const { useToken } = theme;
|
||||||
|
const { token } = useToken();
|
||||||
|
|
||||||
|
console.log('token:', token);
|
||||||
|
|
||||||
|
const chartColorMap = useMemo(() => {
|
||||||
|
return {
|
||||||
|
titleColor: token.colorText,
|
||||||
|
splitLineColor: token.colorBorder,
|
||||||
|
tickLineColor: token.colorSplit,
|
||||||
|
axislabelColor: token.colorTextTertiary,
|
||||||
|
gaugeBgColor: token.colorFillSecondary,
|
||||||
|
gaugeSplitLineColor: isDarkTheme
|
||||||
|
? 'rgba(255,255,255,.3)'
|
||||||
|
: 'rgba(255, 255, 255, 1)',
|
||||||
|
gaugeSplitLineColor2: isDarkTheme
|
||||||
|
? 'rgba(255,255,255,.5)'
|
||||||
|
: 'rgba(255, 255, 255, 1)',
|
||||||
|
colorBgContainerHover: isDarkTheme ? '#424242' : '#fff'
|
||||||
|
};
|
||||||
|
}, [userSettings.theme, isDarkTheme]);
|
||||||
|
|
||||||
|
const tooltip = {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
// axisPointer: {
|
backgroundColor: chartColorMap.colorBgContainerHover,
|
||||||
// type: 'shadow'
|
borderColor: 'transparent',
|
||||||
// }
|
|
||||||
formatter(params: any, callback?: (val: any) => any) {
|
formatter(params: any, callback?: (val: any) => any) {
|
||||||
let result = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
|
let result = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
|
||||||
|
|
||||||
params.forEach((item: any) => {
|
params.forEach((item: any) => {
|
||||||
let value = isFunction(callback)
|
let value = isFunction(callback)
|
||||||
? callback?.(item.data.value)
|
? callback?.(item.data.value)
|
||||||
: item.data.value;
|
: item.data.value;
|
||||||
|
|
||||||
|
const borderRadius = item.seriesType === 'bar' ? '2px' : '8px';
|
||||||
|
|
||||||
result += `<span class="tooltip-item">
|
result += `<span class="tooltip-item">
|
||||||
<span class="tooltip-item-name">
|
<span class="tooltip-item-name">
|
||||||
<span style="display:inline-block;margin-right:5px;border-radius:8px;width:8px;height:8px;background-color:${item.color};"></span>
|
<span style="display:inline-block;margin-right:5px;border-radius:${borderRadius};width:8px;height:8px;background-color:${item.color};"></span>
|
||||||
<span class="tooltip-title">${item.seriesName}</span>:
|
<span class="tooltip-title">${item.seriesName}</span>:
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip-value">${value}</span>
|
<span class="tooltip-value">${value}</span>
|
||||||
</span>`;
|
</span>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return `<div class="tooltip-wrapper">${result}</div>`;
|
return `<div class="tooltip-wrapper">${result}</div>`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const grid = {
|
const legend = {
|
||||||
left: 0,
|
|
||||||
right: 20,
|
|
||||||
bottom: 20,
|
|
||||||
containLabel: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export const legend = {
|
|
||||||
itemWidth: 8,
|
itemWidth: 8,
|
||||||
itemHeight: 8
|
itemHeight: 8,
|
||||||
|
textStyle: {
|
||||||
|
color: chartColorMap.axislabelColor
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const xAxis = {
|
const xAxis = {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: true,
|
show: true,
|
||||||
@@ -56,21 +87,21 @@ export const xAxis = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const yAxis = {
|
const yAxis = {
|
||||||
// max: 100,
|
|
||||||
// min: 0,
|
|
||||||
nameTextStyle: {
|
nameTextStyle: {
|
||||||
padding: [0, 0, 0, -20]
|
padding: [0, 0, 0, -20]
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: true,
|
show: true,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
type: 'dashed'
|
type: 'dashed',
|
||||||
|
color: chartColorMap.splitLineColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: chartColorMap.axislabelColor,
|
color: chartColorMap.axislabelColor,
|
||||||
fontSize: 12
|
fontSize: 12,
|
||||||
|
formatter: formatLargeNumber
|
||||||
},
|
},
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false
|
show: false
|
||||||
@@ -78,17 +109,17 @@ export const yAxis = {
|
|||||||
type: 'value'
|
type: 'value'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const title = {
|
const title = {
|
||||||
show: true,
|
show: true,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#000'
|
color: chartColorMap.titleColor
|
||||||
},
|
},
|
||||||
text: ''
|
text: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export const barItemConfig = {
|
const barItemConfig = {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
barMaxWidth: 20,
|
barMaxWidth: 20,
|
||||||
barMinWidth: 8,
|
barMinWidth: 8,
|
||||||
@@ -97,7 +128,7 @@ export const barItemConfig = {
|
|||||||
barCategoryGap: '50%'
|
barCategoryGap: '50%'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lineItemConfig = {
|
const lineItemConfig = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: true,
|
smooth: true,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
@@ -108,7 +139,7 @@ export const lineItemConfig = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gaugeItemConfig = {
|
const gaugeItemConfig = {
|
||||||
type: 'gauge',
|
type: 'gauge',
|
||||||
radius: '88%',
|
radius: '88%',
|
||||||
center: ['50%', '65%'],
|
center: ['50%', '65%'],
|
||||||
@@ -133,7 +164,6 @@ export const gaugeItemConfig = {
|
|||||||
roundCap: false,
|
roundCap: false,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 12,
|
width: 12,
|
||||||
// color: [[1, 'rgba(221, 221, 221, 0.5)']],
|
|
||||||
color: [
|
color: [
|
||||||
[0.5, 'rgba(84, 204, 152, 80%)'],
|
[0.5, 'rgba(84, 204, 152, 80%)'],
|
||||||
[0.8, 'rgba(250, 173, 20, 80%)'],
|
[0.8, 'rgba(250, 173, 20, 80%)'],
|
||||||
@@ -142,25 +172,25 @@ export const gaugeItemConfig = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
axisTick: {
|
axisTick: {
|
||||||
distance: -12,
|
distance: -11,
|
||||||
length: 6,
|
length: 6,
|
||||||
splitNumber: 5,
|
splitNumber: 5,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
color: 'rgba(255, 255, 255, 1)'
|
color: chartColorMap.gaugeSplitLineColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
distance: -12,
|
distance: -5,
|
||||||
length: 12,
|
length: 5,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
color: 'rgba(255, 255, 255, 1)'
|
color: chartColorMap.gaugeSplitLineColor2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
distance: 14,
|
distance: 14,
|
||||||
color: 'rgba(0, 0, 0, .5)',
|
color: chartColorMap.axislabelColor,
|
||||||
fontSize: 12
|
fontSize: 12
|
||||||
},
|
},
|
||||||
detail: {
|
detail: {
|
||||||
@@ -169,7 +199,7 @@ export const gaugeItemConfig = {
|
|||||||
offsetCenter: [5, 30],
|
offsetCenter: [5, 30],
|
||||||
valueAnimation: false,
|
valueAnimation: false,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
color: 'rgba(0, 0, 0, 0.88)',
|
color: chartColorMap.titleColor,
|
||||||
formatter(value: any) {
|
formatter(value: any) {
|
||||||
return '{value|' + value + '}{unit|%}';
|
return '{value|' + value + '}{unit|%}';
|
||||||
},
|
},
|
||||||
@@ -177,14 +207,29 @@ export const gaugeItemConfig = {
|
|||||||
value: {
|
value: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: 'rgba(0, 0, 0, 0.88)'
|
color: chartColorMap.titleColor
|
||||||
},
|
},
|
||||||
unit: {
|
unit: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: 'rgba(0, 0, 0, 0.88)',
|
color: chartColorMap.titleColor,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
padding: [0, 0, 0, 2]
|
padding: [0, 0, 0, 2]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
tooltip,
|
||||||
|
grid,
|
||||||
|
legend,
|
||||||
|
xAxis,
|
||||||
|
yAxis,
|
||||||
|
title,
|
||||||
|
chartColorMap,
|
||||||
|
barItemConfig,
|
||||||
|
lineItemConfig,
|
||||||
|
gaugeItemConfig,
|
||||||
|
isDark: isDarkTheme
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
import Chart from '@/components/echarts/chart';
|
||||||
import {
|
import useChartConfig from '@/components/echarts/config';
|
||||||
gaugeItemConfig,
|
|
||||||
title as titleConfig
|
|
||||||
} from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
import EmptyData from '@/components/empty-data';
|
||||||
import { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { ChartProps } from './types';
|
import { ChartProps } from './types';
|
||||||
|
|
||||||
const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
||||||
props
|
props
|
||||||
) => {
|
) => {
|
||||||
|
const {
|
||||||
|
gaugeItemConfig,
|
||||||
|
title: titleConfig,
|
||||||
|
chartColorMap
|
||||||
|
} = useChartConfig();
|
||||||
const { value, height, width, labelFormatter, title, color } = props;
|
const { value, height, width, labelFormatter, title, color } = props;
|
||||||
if (!value && value !== 0) {
|
if (!value && value !== 0) {
|
||||||
return <EmptyData height={height} title={title}></EmptyData>;
|
return <EmptyData height={height} title={title}></EmptyData>;
|
||||||
@@ -32,7 +34,7 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
|||||||
...gaugeItemConfig.axisLine.lineStyle,
|
...gaugeItemConfig.axisLine.lineStyle,
|
||||||
color: [
|
color: [
|
||||||
[value / 100, color],
|
[value / 100, color],
|
||||||
[1, 'rgba(221, 221, 221, 0.5)']
|
[1, chartColorMap.gaugeBgColor]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
import Chart from '@/components/echarts/chart';
|
||||||
import {
|
import useChartConfig from '@/components/echarts/config';
|
||||||
grid,
|
|
||||||
legend,
|
|
||||||
title as titleConfig,
|
|
||||||
tooltip,
|
|
||||||
xAxis,
|
|
||||||
yAxis
|
|
||||||
} from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
import EmptyData from '@/components/empty-data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { memo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import { ChartProps } from './types';
|
import { ChartProps } from './types';
|
||||||
|
|
||||||
const BarChart: React.FC<ChartProps> = (props) => {
|
const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
||||||
const {
|
const {
|
||||||
seriesData,
|
seriesData,
|
||||||
xAxisData,
|
xAxisData,
|
||||||
@@ -20,15 +13,31 @@ const BarChart: React.FC<ChartProps> = (props) => {
|
|||||||
width,
|
width,
|
||||||
labelFormatter,
|
labelFormatter,
|
||||||
legendData,
|
legendData,
|
||||||
|
maxItems,
|
||||||
title
|
title
|
||||||
} = props;
|
} = props;
|
||||||
if (!seriesData.length) {
|
const {
|
||||||
return <EmptyData height={height} title={title}></EmptyData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
title: titleConfig,
|
|
||||||
grid,
|
grid,
|
||||||
|
legend,
|
||||||
|
title: titleConfig,
|
||||||
|
tooltip,
|
||||||
|
xAxis,
|
||||||
|
yAxis
|
||||||
|
} = useChartConfig();
|
||||||
|
|
||||||
|
const dataOptions = useMemo((): any => {
|
||||||
|
const options = {
|
||||||
|
title: {
|
||||||
|
...titleConfig,
|
||||||
|
left: 'start'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
...grid,
|
||||||
|
top: 0,
|
||||||
|
bottom: maxItems
|
||||||
|
? `${(1 / maxItems) * (maxItems - xAxisData.length) * 100}%`
|
||||||
|
: 0
|
||||||
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
...tooltip
|
...tooltip
|
||||||
},
|
},
|
||||||
@@ -39,7 +48,15 @@ const BarChart: React.FC<ChartProps> = (props) => {
|
|||||||
formatter: labelFormatter
|
formatter: labelFormatter
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
yAxis,
|
yAxis: {
|
||||||
|
...yAxis,
|
||||||
|
axisLabel: {
|
||||||
|
...yAxis.axisLabel,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 75,
|
||||||
|
ellipsis: '...'
|
||||||
|
}
|
||||||
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legend,
|
...legend,
|
||||||
data: []
|
data: []
|
||||||
@@ -47,8 +64,6 @@ const BarChart: React.FC<ChartProps> = (props) => {
|
|||||||
|
|
||||||
series: []
|
series: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const setDataOptions = () => {
|
|
||||||
const data = _.map(seriesData, (item: any) => {
|
const data = _.map(seriesData, (item: any) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@@ -69,7 +84,6 @@ const BarChart: React.FC<ChartProps> = (props) => {
|
|||||||
animation: false,
|
animation: false,
|
||||||
title: {
|
title: {
|
||||||
...options.title,
|
...options.title,
|
||||||
left: 'start',
|
|
||||||
text: title
|
text: title
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
@@ -103,16 +117,36 @@ const BarChart: React.FC<ChartProps> = (props) => {
|
|||||||
},
|
},
|
||||||
series: data
|
series: data
|
||||||
};
|
};
|
||||||
};
|
}, [
|
||||||
|
seriesData,
|
||||||
|
xAxisData,
|
||||||
|
title,
|
||||||
|
labelFormatter,
|
||||||
|
tooltip,
|
||||||
|
grid,
|
||||||
|
xAxis,
|
||||||
|
yAxis,
|
||||||
|
legend
|
||||||
|
]);
|
||||||
|
|
||||||
const dataOptions: any = setDataOptions();
|
const isEmpty = useMemo(() => {
|
||||||
|
return seriesData?.every?.((item: any) => {
|
||||||
|
return !item?.data?.length;
|
||||||
|
});
|
||||||
|
}, [seriesData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{isEmpty ? (
|
||||||
|
<EmptyData height={height} title={title}></EmptyData>
|
||||||
|
) : (
|
||||||
<Chart
|
<Chart
|
||||||
height={height}
|
height={height}
|
||||||
options={dataOptions}
|
options={dataOptions}
|
||||||
width={width || '100%'}
|
width={width || '100%'}
|
||||||
></Chart>
|
></Chart>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
import type {
|
import type {
|
||||||
// 系列类型的定义后缀都为 SeriesOption
|
|
||||||
BarSeriesOption,
|
BarSeriesOption,
|
||||||
GaugeSeriesOption,
|
GaugeSeriesOption,
|
||||||
LineSeriesOption
|
LineSeriesOption,
|
||||||
|
ScatterSeriesOption
|
||||||
} from 'echarts/charts';
|
} from 'echarts/charts';
|
||||||
import { BarChart, GaugeChart, LineChart } from 'echarts/charts';
|
import { BarChart, GaugeChart, LineChart, ScatterChart } from 'echarts/charts';
|
||||||
import type {
|
import type {
|
||||||
DatasetComponentOption,
|
DatasetComponentOption,
|
||||||
GridComponentOption,
|
GridComponentOption,
|
||||||
// 组件类型的定义后缀都为 ComponentOption
|
|
||||||
TitleComponentOption,
|
TitleComponentOption,
|
||||||
TooltipComponentOption
|
TooltipComponentOption
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
import {
|
import {
|
||||||
// 数据集组件
|
DataZoomComponent,
|
||||||
DatasetComponent,
|
DatasetComponent,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
TitleComponent,
|
TitleComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
// 内置数据转换器组件 (filter, sort)
|
// (filter, sort)
|
||||||
TransformComponent
|
TransformComponent
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
import type { ComposeOption } from 'echarts/core';
|
import type { ComposeOption } from 'echarts/core';
|
||||||
@@ -27,7 +26,6 @@ import * as echarts from 'echarts/core';
|
|||||||
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
|
||||||
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
|
|
||||||
type ECOption = ComposeOption<
|
type ECOption = ComposeOption<
|
||||||
| BarSeriesOption
|
| BarSeriesOption
|
||||||
| LineSeriesOption
|
| LineSeriesOption
|
||||||
@@ -36,9 +34,10 @@ type ECOption = ComposeOption<
|
|||||||
| GridComponentOption
|
| GridComponentOption
|
||||||
| DatasetComponentOption
|
| DatasetComponentOption
|
||||||
| GaugeSeriesOption
|
| GaugeSeriesOption
|
||||||
|
| ScatterSeriesOption
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// 注册必须的组件
|
// register components and charts
|
||||||
echarts.use([
|
echarts.use([
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
TitleComponent,
|
TitleComponent,
|
||||||
@@ -46,8 +45,10 @@ echarts.use([
|
|||||||
GridComponent,
|
GridComponent,
|
||||||
DatasetComponent,
|
DatasetComponent,
|
||||||
TransformComponent,
|
TransformComponent,
|
||||||
|
DataZoomComponent,
|
||||||
BarChart,
|
BarChart,
|
||||||
LineChart,
|
LineChart,
|
||||||
|
ScatterChart,
|
||||||
GaugeChart,
|
GaugeChart,
|
||||||
LabelLayout,
|
LabelLayout,
|
||||||
UniversalTransition,
|
UniversalTransition,
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
import Chart from '@/components/echarts/chart';
|
import Chart from '@/components/echarts/chart';
|
||||||
import {
|
import useChartConfig from '@/components/echarts/config';
|
||||||
grid,
|
|
||||||
legend,
|
|
||||||
lineItemConfig,
|
|
||||||
title as titleConfig,
|
|
||||||
tooltip,
|
|
||||||
xAxis,
|
|
||||||
yAxis
|
|
||||||
} from '@/components/echarts/config';
|
|
||||||
import EmptyData from '@/components/empty-data';
|
import EmptyData from '@/components/empty-data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { memo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import { ChartProps } from './types';
|
import { ChartProps } from './types';
|
||||||
|
|
||||||
const LineChart: React.FC<ChartProps> = (props) => {
|
const LineChart: React.FC<ChartProps> = (props) => {
|
||||||
@@ -26,9 +18,25 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
|||||||
smooth,
|
smooth,
|
||||||
title
|
title
|
||||||
} = props;
|
} = props;
|
||||||
if (!seriesData.length) {
|
const {
|
||||||
return <EmptyData height={height} title={title}></EmptyData>;
|
grid,
|
||||||
|
legend,
|
||||||
|
lineItemConfig,
|
||||||
|
title: titleConfig,
|
||||||
|
tooltip,
|
||||||
|
xAxis,
|
||||||
|
yAxis
|
||||||
|
} = useChartConfig();
|
||||||
|
|
||||||
|
const axisLabelFormatter = (value: string, index: number) => {
|
||||||
|
if (labelFormatter) {
|
||||||
|
return labelFormatter(value, index);
|
||||||
}
|
}
|
||||||
|
if (index === xAxisData.length - 1) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
title: {
|
title: {
|
||||||
@@ -47,19 +55,24 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
|||||||
...xAxis,
|
...xAxis,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
...xAxis.axisLabel,
|
...xAxis.axisLabel,
|
||||||
formatter: labelFormatter
|
formatter: axisLabelFormatter
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
yAxis,
|
yAxis,
|
||||||
legend: {
|
legend: {
|
||||||
...legend,
|
...legend,
|
||||||
data: legendData
|
data: legendData.map((item: any) => {
|
||||||
|
return {
|
||||||
|
name: item,
|
||||||
|
icon: 'circle'
|
||||||
|
};
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
series: []
|
series: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateOptions = (): any => {
|
const dataOptions = useMemo((): any => {
|
||||||
const data = _.map(seriesData, (item: any) => {
|
const data = _.map(seriesData, (item: any) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@@ -75,7 +88,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const optionsConfig = {
|
return {
|
||||||
...options,
|
...options,
|
||||||
animation: false,
|
animation: false,
|
||||||
title: {
|
title: {
|
||||||
@@ -84,7 +97,11 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
|||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
...options.yAxis,
|
...options.yAxis,
|
||||||
name: yAxisName
|
name: yAxisName,
|
||||||
|
nameTextStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
align: 'right'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
...options.xAxis,
|
...options.xAxis,
|
||||||
@@ -92,15 +109,20 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
|||||||
},
|
},
|
||||||
series: data
|
series: data
|
||||||
};
|
};
|
||||||
return optionsConfig;
|
}, [seriesData, xAxisData, yAxisName, title, smooth, legendData, options]);
|
||||||
};
|
|
||||||
const dataOptions = generateOptions();
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{!seriesData.length ? (
|
||||||
|
<EmptyData height={height} title={title}></EmptyData>
|
||||||
|
) : (
|
||||||
<Chart
|
<Chart
|
||||||
height={height}
|
height={height}
|
||||||
options={dataOptions}
|
options={dataOptions}
|
||||||
width={width || '100%'}
|
width={width || '100%'}
|
||||||
></Chart>
|
></Chart>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import Chart from '@/components/echarts/chart';
|
||||||
|
import useChartConfig from '@/components/echarts/config';
|
||||||
|
import EmptyData from '@/components/empty-data';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { ChartProps } from './types';
|
||||||
|
|
||||||
|
const MixLineBarChart: React.FC<
|
||||||
|
ChartProps & {
|
||||||
|
chartData: {
|
||||||
|
line: any[];
|
||||||
|
bar: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
> = (props) => {
|
||||||
|
const {
|
||||||
|
seriesData,
|
||||||
|
xAxisData,
|
||||||
|
yAxisName,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
labelFormatter,
|
||||||
|
tooltipValueFormatter = null,
|
||||||
|
legendData = [],
|
||||||
|
smooth,
|
||||||
|
title,
|
||||||
|
chartData
|
||||||
|
} = props;
|
||||||
|
const {
|
||||||
|
grid,
|
||||||
|
legend,
|
||||||
|
lineItemConfig,
|
||||||
|
barItemConfig,
|
||||||
|
title: titleConfig,
|
||||||
|
tooltip,
|
||||||
|
xAxis,
|
||||||
|
yAxis
|
||||||
|
} = useChartConfig();
|
||||||
|
|
||||||
|
const { line: lineSeriesData, bar: barSeriesData } = chartData;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
title: {
|
||||||
|
text: ''
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
...grid,
|
||||||
|
right: 0,
|
||||||
|
top: 20,
|
||||||
|
bottom: 10
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
...tooltip,
|
||||||
|
formatter(params: any) {
|
||||||
|
return tooltipValueFormatter
|
||||||
|
? tooltip.formatter(params, tooltipValueFormatter)
|
||||||
|
: tooltip.formatter(params);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
...xAxis,
|
||||||
|
axisLabel: {
|
||||||
|
...xAxis.axisLabel,
|
||||||
|
formatter: labelFormatter
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis,
|
||||||
|
legend: {
|
||||||
|
...legend,
|
||||||
|
data: legendData,
|
||||||
|
itemGap: 20,
|
||||||
|
bottom: 5,
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
|
||||||
|
series: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataOptions = useMemo((): any => {
|
||||||
|
const linedata = _.map(lineSeriesData, (item: any) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...lineItemConfig,
|
||||||
|
smooth: smooth,
|
||||||
|
itemStyle: {
|
||||||
|
...lineItemConfig.itemStyle,
|
||||||
|
color: item.color
|
||||||
|
},
|
||||||
|
yAxisIndex: 1,
|
||||||
|
lineStyle: {
|
||||||
|
...lineItemConfig.lineStyle,
|
||||||
|
color: item.color
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const barData = _.map(barSeriesData, (item: any) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...barItemConfig,
|
||||||
|
stack: 'total',
|
||||||
|
yAxisIndex: 0,
|
||||||
|
itemStyle: {
|
||||||
|
...item.itemStyle,
|
||||||
|
color: item.color
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
animation: false,
|
||||||
|
title: {
|
||||||
|
...titleConfig,
|
||||||
|
text: title
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
...options.yAxis
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...options.yAxis,
|
||||||
|
nameTextStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
align: 'right'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
...options.xAxis,
|
||||||
|
data: xAxisData
|
||||||
|
},
|
||||||
|
series: [...barData, ...linedata]
|
||||||
|
};
|
||||||
|
}, [seriesData, xAxisData, yAxisName, title, smooth, legendData, options]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!lineSeriesData.length && !barSeriesData.length ? (
|
||||||
|
<EmptyData height={height} title={title}></EmptyData>
|
||||||
|
) : (
|
||||||
|
<Chart
|
||||||
|
height={height}
|
||||||
|
options={dataOptions}
|
||||||
|
width={width || '100%'}
|
||||||
|
></Chart>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MixLineBarChart;
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import Chart from '@/components/echarts/chart';
|
||||||
|
import useChartConfig from '@/components/echarts/config';
|
||||||
|
import EmptyData from '@/components/empty-data';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
|
import { ChartProps } from './types';
|
||||||
|
|
||||||
|
const Scatter: React.FC<ChartProps> = (props) => {
|
||||||
|
const { grid, title: titleConfig, isDark, chartColorMap } = useChartConfig();
|
||||||
|
const { seriesData, xAxisData, height, width, showEmpty, title } = props;
|
||||||
|
|
||||||
|
const chart = useRef<any>(null);
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
const colorMap = isDark
|
||||||
|
? {
|
||||||
|
split: chartColorMap.splitLineColor,
|
||||||
|
axis: chartColorMap.axislabelColor,
|
||||||
|
label: chartColorMap.axislabelColor
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
split: '#F2F2F2',
|
||||||
|
axis: '#dcdcdc',
|
||||||
|
label: '#dcdcdc'
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
animation: false,
|
||||||
|
grid: {
|
||||||
|
...grid,
|
||||||
|
right: 10,
|
||||||
|
top: 10,
|
||||||
|
bottom: 2,
|
||||||
|
left: 2,
|
||||||
|
containLabel: true,
|
||||||
|
borderRadius: 4
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
min: -1,
|
||||||
|
max: 1,
|
||||||
|
scale: false,
|
||||||
|
slient: true,
|
||||||
|
splitNumber: 15,
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: colorMap.split
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: colorMap.axis
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
show: true,
|
||||||
|
color: colorMap.label
|
||||||
|
},
|
||||||
|
boundaryGap: [0.05, 0.05]
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
min: -1,
|
||||||
|
max: 1,
|
||||||
|
scale: false,
|
||||||
|
slient: true,
|
||||||
|
splitNumber: 10,
|
||||||
|
boundaryGap: [0.05, 0.05],
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: colorMap.split
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: colorMap.axis
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
show: true,
|
||||||
|
color: colorMap.label
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
symbol: 'roundRect',
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
shadowColor: 'none',
|
||||||
|
textBorderColor: 'none',
|
||||||
|
formatter: (params: any) => {
|
||||||
|
return params.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: []
|
||||||
|
};
|
||||||
|
}, [isDark]);
|
||||||
|
|
||||||
|
const findOverlappingPoints = useCallback(
|
||||||
|
(data: any[], currentPoint: any) => {
|
||||||
|
const overlappingPoints = [];
|
||||||
|
const symbolRadius = 16;
|
||||||
|
|
||||||
|
const [x1, y1] = chart.current.chart?.convertToPixel(
|
||||||
|
'grid',
|
||||||
|
currentPoint.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const pixelPoints = data.map((point) => {
|
||||||
|
return {
|
||||||
|
...point,
|
||||||
|
value: chart.current.chart?.convertToPixel('grid', point.value)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let j = 0; j < pixelPoints.length; j++) {
|
||||||
|
if (currentPoint.name === pixelPoints[j].name) {
|
||||||
|
overlappingPoints.push({ ...pixelPoints[j] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [x2, y2] = pixelPoints[j].value;
|
||||||
|
|
||||||
|
const distance = Math.sqrt(
|
||||||
|
Math.pow(_.round(x2 - x1, 2), 2) + Math.pow(_.round(y2 - y1, 2), 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance <= symbolRadius) {
|
||||||
|
overlappingPoints.push({ ...pixelPoints[j] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return overlappingPoints;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderNameInTooltip = useCallback((dataList: any[]) => {
|
||||||
|
if (!dataList.length || dataList.length < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const renderText = (item: any) => {
|
||||||
|
return `<span class="tooltip-item-name">
|
||||||
|
<span style="display:flex;justify-content:center;align-items: center;color:#fff;
|
||||||
|
margin-right:0;border-radius:4px;width:14px;
|
||||||
|
height:14px;background-color:${item?.itemStyle?.color};"
|
||||||
|
>${item.name}</span>
|
||||||
|
</span>`;
|
||||||
|
};
|
||||||
|
return renderText;
|
||||||
|
}, []);
|
||||||
|
const dataOptions = useMemo((): any => {
|
||||||
|
const seriseDataList = seriesData.map((item: any, index: number) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#5470c6'
|
||||||
|
},
|
||||||
|
symbolSize: 16
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
borderWidth: 0,
|
||||||
|
backgroundColor: chartColorMap.colorBgContainerHover,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
formatter(params: any, callback?: (val: any) => any) {
|
||||||
|
const dataList = findOverlappingPoints(seriseDataList, params.data);
|
||||||
|
let result = '';
|
||||||
|
const renderText: any = renderNameInTooltip(dataList);
|
||||||
|
dataList.forEach((item: any) => {
|
||||||
|
result += `
|
||||||
|
<span class="tooltip-item" style="justify-content: flex-start;">
|
||||||
|
${renderText ? renderText(item) : ''}
|
||||||
|
<span class="tooltip-value">${item.text}</span>
|
||||||
|
</span>`;
|
||||||
|
});
|
||||||
|
return `<div class="tooltip-wrapper scatter">${result}</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
...titleConfig,
|
||||||
|
text: title
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
type: 'scatter',
|
||||||
|
labelLayout: {
|
||||||
|
hideOverlap: true
|
||||||
|
},
|
||||||
|
data: seriseDataList
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [seriesData, xAxisData, title, options, findOverlappingPoints]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!seriesData.length && showEmpty ? (
|
||||||
|
<EmptyData height={height} title={title}></EmptyData>
|
||||||
|
) : (
|
||||||
|
<Chart
|
||||||
|
ref={chart}
|
||||||
|
height={height}
|
||||||
|
options={dataOptions}
|
||||||
|
width={width || '100%'}
|
||||||
|
></Chart>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Scatter);
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import type { LegendComponentOption } from 'echarts/components';
|
||||||
export interface ChartProps {
|
export interface ChartProps {
|
||||||
seriesData: any[];
|
seriesData: any[];
|
||||||
|
showEmpty?: boolean;
|
||||||
xAxisData: string[];
|
xAxisData: string[];
|
||||||
legendData?: string[];
|
legendData?: LegendComponentOption['data'];
|
||||||
labelFormatter?: (val?: any) => string;
|
labelFormatter?: (val?: any, index?: number) => string;
|
||||||
tooltipValueFormatter?: (val?: any) => string;
|
tooltipValueFormatter?: (val: any) => string;
|
||||||
height: string | number;
|
height: string | number;
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
@@ -1,29 +1,21 @@
|
|||||||
.editor-wrap {
|
.editor-wrap {
|
||||||
border-radius: var(--border-radius-mini);
|
border-radius: var(--border-radius-mini);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
font-size: 0;
|
||||||
|
|
||||||
|
.code-pre {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-header {
|
.editor-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 6px 10px;
|
padding-block: 0;
|
||||||
|
padding-inline: 12px 10px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: rgb(56, 56, 56);
|
background-color: var(--color-editor-header-bg);
|
||||||
|
|
||||||
.ant-select-selector {
|
|
||||||
background-color: rgba(255, 255, 255, 9%) !important;
|
|
||||||
color: rgba(255, 255, 255, 70%);
|
|
||||||
|
|
||||||
.ant-select-selection-item {
|
|
||||||
color: rgba(255, 255, 255, 70%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-arrow {
|
|
||||||
color: rgba(255, 255, 255, 70%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// set scrollbar style
|
// set scrollbar style
|
||||||
|
|
||||||
.scrollbar {
|
.scrollbar {
|
||||||
.slider {
|
.slider {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -1,27 +1,40 @@
|
|||||||
import { Select } from 'antd';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
import CopyButton from '../copy-button';
|
import CopyButton from '../copy-button';
|
||||||
|
import SegmentLine from '../segment-line';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
|
const Wrapper = styled.div<{ $height: number }>`
|
||||||
|
height: ${(props) => props.$height}px;
|
||||||
|
`;
|
||||||
|
|
||||||
interface EditorwrapProps {
|
interface EditorwrapProps {
|
||||||
|
headerHeight?: number;
|
||||||
header?: React.ReactNode;
|
header?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
copyText: string;
|
copyText: string;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
langOptions?: { label: string; value: string }[];
|
langOptions?: Global.BaseOption<string | number>[];
|
||||||
onChangeLang?: (value: string) => void;
|
styles?: {
|
||||||
|
wrapper?: React.CSSProperties;
|
||||||
|
header?: React.CSSProperties;
|
||||||
|
content?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
onChangeLang?: (value: string | number) => void;
|
||||||
}
|
}
|
||||||
const EditorWrap: React.FC<EditorwrapProps> = ({
|
const EditorWrap: React.FC<EditorwrapProps> = ({
|
||||||
|
headerHeight = 40,
|
||||||
header,
|
header,
|
||||||
children,
|
children,
|
||||||
copyText,
|
copyText,
|
||||||
langOptions,
|
langOptions = [],
|
||||||
onChangeLang,
|
onChangeLang,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
styles = {},
|
||||||
showHeader = true
|
showHeader = true
|
||||||
}) => {
|
}) => {
|
||||||
const handleChangeLang = (value: string) => {
|
const handleChangeLang = (value: string | number) => {
|
||||||
onChangeLang?.(value);
|
onChangeLang?.(value);
|
||||||
};
|
};
|
||||||
const renderHeader = () => {
|
const renderHeader = () => {
|
||||||
@@ -30,15 +43,14 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
|
|||||||
}
|
}
|
||||||
if (showHeader) {
|
if (showHeader) {
|
||||||
return (
|
return (
|
||||||
<div className="editor-header">
|
<Wrapper className="editor-header" $height={headerHeight}>
|
||||||
<Select
|
<SegmentLine
|
||||||
|
height={headerHeight}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
style={{ width: 120 }}
|
size="small"
|
||||||
size="middle"
|
|
||||||
variant="filled"
|
|
||||||
options={langOptions}
|
options={langOptions}
|
||||||
onChange={handleChangeLang}
|
onChange={handleChangeLang}
|
||||||
></Select>
|
></SegmentLine>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
text={copyText}
|
text={copyText}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -46,13 +58,13 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
|
|||||||
color: 'rgba(255,255,255,.7)'
|
color: 'rgba(255,255,255,.7)'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="editor-wrap">
|
<div className="editor-wrap" style={{ ...styles.wrapper }}>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
<div className="editor-content">{children}</div>
|
<div className="editor-content">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import Editor from '@monaco-editor/react';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import EditorWrap from '../editor-wrap';
|
||||||
|
|
||||||
|
interface ViewerProps {
|
||||||
|
lang: string;
|
||||||
|
defaultLang?: string;
|
||||||
|
langOptions?: Global.BaseOption<string>[];
|
||||||
|
config?: any;
|
||||||
|
value: string;
|
||||||
|
height?: string | number;
|
||||||
|
theme?: string;
|
||||||
|
showHeader?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewerEditor: React.FC<ViewerProps> = (props) => {
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const {
|
||||||
|
lang,
|
||||||
|
value,
|
||||||
|
config,
|
||||||
|
langOptions,
|
||||||
|
defaultLang,
|
||||||
|
height = 380,
|
||||||
|
showHeader
|
||||||
|
} = props;
|
||||||
|
const [langType, setLangType] = useState(defaultLang);
|
||||||
|
|
||||||
|
const handleBeforeMount = (monaco: any) => {
|
||||||
|
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||||
|
noSemanticValidation: false,
|
||||||
|
noSyntaxValidation: false,
|
||||||
|
diagnosticCodesToIgnore: [80001]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorDidMount = (editor: any, monaco: any) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
console.log('loaded====', editor, monaco);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnChangeLang = (value: string) => {
|
||||||
|
setLangType(value);
|
||||||
|
};
|
||||||
|
const formatCode = () => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorRef.current
|
||||||
|
?.getAction?.('editor.action.formatDocument')
|
||||||
|
?.run()
|
||||||
|
.then(() => {
|
||||||
|
console.log('format success');
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formatCode();
|
||||||
|
setTimeout(() => {
|
||||||
|
const lineCount = editorRef.current?.getModel().getLineCount(); // 获取总行数
|
||||||
|
editorRef.current?.revealLine(lineCount); // 滚动到最后一行
|
||||||
|
}, 100); // 可以调整延时,确保编辑器完全加载
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditorWrap
|
||||||
|
copyText={value}
|
||||||
|
langOptions={langOptions}
|
||||||
|
defaultValue={lang}
|
||||||
|
showHeader={showHeader}
|
||||||
|
onChangeLang={handleOnChangeLang}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
height={height}
|
||||||
|
theme="vs-dark"
|
||||||
|
className="monaco-editor"
|
||||||
|
defaultLanguage={defaultLang}
|
||||||
|
language={langType}
|
||||||
|
value={value}
|
||||||
|
options={config}
|
||||||
|
beforeMount={handleBeforeMount}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
/>
|
||||||
|
</EditorWrap>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ViewerEditor);
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// footer all styles
|
|
||||||
.footer {
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 20px 0;
|
|
||||||
text-align: center;
|
|
||||||
font-size: var(--font-size-middle);
|
|
||||||
color: var(--color-text-2);
|
|
||||||
|
|
||||||
.footer-content-left-text {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,30 +3,60 @@ import { getAtomStorage } from '@/atoms/utils';
|
|||||||
import VersionInfo, { modalConfig } from '@/components/version-info';
|
import VersionInfo, { modalConfig } from '@/components/version-info';
|
||||||
import externalLinks from '@/constants/external-links';
|
import externalLinks from '@/constants/external-links';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Modal, Space } from 'antd';
|
import { Button, Divider, Modal } from 'antd';
|
||||||
import './index.less';
|
import { createStyles } from 'antd-style';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const CompanyWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding-inline: 0 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
|
footer: css`
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-size-middle);
|
||||||
|
color: ${token.colorTextTertiary};
|
||||||
|
`,
|
||||||
|
'footer-content-left-text': {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
|
const { styles } = useStyles();
|
||||||
|
|
||||||
const showVersion = () => {
|
const showVersion = () => {
|
||||||
Modal.info({
|
modal.info({
|
||||||
...modalConfig,
|
...modalConfig,
|
||||||
|
width: 460,
|
||||||
content: <VersionInfo intl={intl} />
|
content: <VersionInfo intl={intl} />
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="footer">
|
<>
|
||||||
|
{contextHolder}
|
||||||
|
<div className={styles.footer}>
|
||||||
<div className="footer-content">
|
<div className="footer-content">
|
||||||
<div className="footer-content-left">
|
<div className="footer-content-left">
|
||||||
<div className="footer-content-left-text">
|
<div className={styles['footer-content-left-text']}>
|
||||||
<Space size={4}>
|
<CompanyWrapper>
|
||||||
<span>©</span>
|
<span>©</span>
|
||||||
<span> {new Date().getFullYear()}</span>
|
<span> {new Date().getFullYear()}</span>
|
||||||
<span> {intl.formatMessage({ id: 'settings.company' })}</span>
|
<span> {intl.formatMessage({ id: 'settings.company' })}</span>
|
||||||
</Space>
|
</CompanyWrapper>
|
||||||
<Space size={8} style={{ marginLeft: 18 }}>
|
<Divider type="vertical" />
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -35,14 +65,15 @@ const Footer: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage({ id: 'common.button.help' })}
|
{intl.formatMessage({ id: 'common.button.help' })}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Divider type="vertical" />
|
||||||
<Button type="link" size="small" onClick={showVersion}>
|
<Button type="link" size="small" onClick={showVersion}>
|
||||||
{getAtomStorage(GPUStackVersionAtom)?.version}
|
{getAtomStorage(GPUStackVersionAtom)?.version}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,31 @@ import './styles/dark.less';
|
|||||||
|
|
||||||
interface CodeViewerProps {
|
interface CodeViewerProps {
|
||||||
code: string;
|
code: string;
|
||||||
|
copyValue?: string;
|
||||||
lang: string;
|
lang: string;
|
||||||
autodetect?: boolean;
|
autodetect?: boolean;
|
||||||
ignoreIllegals?: boolean;
|
ignoreIllegals?: boolean;
|
||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
|
height?: string | number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
||||||
const { code, lang, autodetect, ignoreIllegals, copyable } = props || {};
|
const {
|
||||||
|
code,
|
||||||
|
copyValue,
|
||||||
|
lang,
|
||||||
|
autodetect,
|
||||||
|
ignoreIllegals,
|
||||||
|
copyable,
|
||||||
|
height = 'auto'
|
||||||
|
} = props || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
|
style={props.style}
|
||||||
|
height={height}
|
||||||
code={code}
|
code={code}
|
||||||
|
copyValue={copyValue}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
autodetect={autodetect}
|
autodetect={autodetect}
|
||||||
|
|||||||
@@ -4,17 +4,32 @@ import './styles/light.less';
|
|||||||
|
|
||||||
interface CodeViewerProps {
|
interface CodeViewerProps {
|
||||||
code: string;
|
code: string;
|
||||||
|
copyValue?: string;
|
||||||
lang: string;
|
lang: string;
|
||||||
autodetect?: boolean;
|
autodetect?: boolean;
|
||||||
ignoreIllegals?: boolean;
|
ignoreIllegals?: boolean;
|
||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
|
height?: string | number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
||||||
const { code, lang, autodetect, ignoreIllegals, copyable } = props || {};
|
const {
|
||||||
|
code,
|
||||||
|
copyValue,
|
||||||
|
lang,
|
||||||
|
autodetect,
|
||||||
|
ignoreIllegals,
|
||||||
|
copyable,
|
||||||
|
style,
|
||||||
|
height = 'auto'
|
||||||
|
} = props || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
|
style={style}
|
||||||
|
height={height}
|
||||||
code={code}
|
code={code}
|
||||||
|
copyValue={copyValue}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
theme="light"
|
theme="light"
|
||||||
autodetect={autodetect}
|
autodetect={autodetect}
|
||||||
|
|||||||
@@ -1,24 +1,86 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
import CopyButton from '../copy-button';
|
import CopyButton from '../copy-button';
|
||||||
import { escapeHtml } from './utils';
|
import { escapeHtml } from './utils';
|
||||||
|
|
||||||
interface CodeViewerProps {
|
interface CodeViewerProps {
|
||||||
code: string;
|
code: string;
|
||||||
|
copyValue?: string;
|
||||||
lang: string;
|
lang: string;
|
||||||
autodetect?: boolean;
|
autodetect?: boolean;
|
||||||
ignoreIllegals?: boolean;
|
ignoreIllegals?: boolean;
|
||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
|
height?: string | number;
|
||||||
theme?: 'light' | 'dark';
|
theme?: 'light' | 'dark';
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CodeHeaderProps {
|
||||||
|
copyValue: string;
|
||||||
|
copyable: boolean;
|
||||||
|
lang: string;
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeHeaderWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
&.dark {
|
||||||
|
background-color: var(--color-editor-header-bg);
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
border-radius: var(--border-radius-mini);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CodeHeader: React.FC<CodeHeaderProps> = ({
|
||||||
|
copyValue,
|
||||||
|
lang,
|
||||||
|
theme,
|
||||||
|
copyable
|
||||||
|
}) => {
|
||||||
|
if (!copyable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<CodeHeaderWrapper
|
||||||
|
className={classNames({
|
||||||
|
dark: theme === 'dark',
|
||||||
|
light: theme === 'light'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span>{lang}</span>
|
||||||
|
<CopyButton
|
||||||
|
text={copyValue}
|
||||||
|
size="small"
|
||||||
|
style={{ color: '#abb2bf' }}
|
||||||
|
></CopyButton>
|
||||||
|
</CodeHeaderWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
code,
|
code = '',
|
||||||
|
copyValue,
|
||||||
lang,
|
lang,
|
||||||
autodetect = true,
|
autodetect = true,
|
||||||
ignoreIllegals = true,
|
ignoreIllegals = true,
|
||||||
copyable = true
|
copyable = true,
|
||||||
|
height = 'auto',
|
||||||
|
style
|
||||||
} = props || {};
|
} = props || {};
|
||||||
|
|
||||||
const highlightedCode = useMemo(() => {
|
const highlightedCode = useMemo(() => {
|
||||||
@@ -57,27 +119,39 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
}, [code, lang, autodetect, ignoreIllegals]);
|
}, [code, lang, autodetect, ignoreIllegals]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<CodeHeader
|
||||||
|
copyValue={copyValue || code}
|
||||||
|
lang={lang}
|
||||||
|
copyable={copyable}
|
||||||
|
theme={props.theme || 'light'}
|
||||||
|
></CodeHeader>
|
||||||
<pre
|
<pre
|
||||||
className={classNames('code-pre', {
|
className={classNames(
|
||||||
|
'code-pre custome-scrollbar custom-scrollbar-horizontal ',
|
||||||
|
{
|
||||||
dark: props.theme === 'dark',
|
dark: props.theme === 'dark',
|
||||||
light: props.theme === 'light',
|
light: props.theme === 'light'
|
||||||
copyable: copyable
|
}
|
||||||
})}
|
)}
|
||||||
|
style={{
|
||||||
|
marginBottom: 0,
|
||||||
|
height: height,
|
||||||
|
...style
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<code
|
<code
|
||||||
className={highlightedCode.className}
|
style={{ minHeight: height }}
|
||||||
|
className={classNames(highlightedCode.className, {
|
||||||
|
dark: props.theme === 'dark',
|
||||||
|
light: props.theme === 'light'
|
||||||
|
})}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: highlightedCode.value
|
__html: highlightedCode.value
|
||||||
}}
|
}}
|
||||||
></code>
|
></code>
|
||||||
{copyable && (
|
|
||||||
<CopyButton
|
|
||||||
text={code}
|
|
||||||
size="small"
|
|
||||||
style={{ color: '#abb2bf' }}
|
|
||||||
></CopyButton>
|
|
||||||
)}
|
|
||||||
</pre>
|
</pre>
|
||||||
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import useUserSettings from '@/hooks/use-user-settings';
|
||||||
|
import React from 'react';
|
||||||
import CodeViewerDark from './code-viewer-dark';
|
import CodeViewerDark from './code-viewer-dark';
|
||||||
import CodeViewerLight from './code-viewer-light';
|
import CodeViewerLight from './code-viewer-light';
|
||||||
import './styles/index.less';
|
import './styles/index.less';
|
||||||
@@ -7,15 +9,48 @@ const HighlightCode: React.FC<{
|
|||||||
lang?: string;
|
lang?: string;
|
||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
theme?: 'light' | 'dark';
|
theme?: 'light' | 'dark';
|
||||||
|
fixedTheme?: 'light' | 'dark';
|
||||||
|
height?: string | number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
copyValue?: string;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { code, lang = 'bash', copyable = true, theme = 'dark' } = props;
|
const {
|
||||||
|
style,
|
||||||
|
code,
|
||||||
|
copyValue,
|
||||||
|
lang = 'bash',
|
||||||
|
copyable = true,
|
||||||
|
theme,
|
||||||
|
height = 'auto'
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { userSettings } = useUserSettings();
|
||||||
|
|
||||||
|
const currentTheme = React.useMemo(() => {
|
||||||
|
const res = theme || userSettings.theme === 'realDark' ? 'dark' : 'light';
|
||||||
|
return res;
|
||||||
|
}, [theme, userSettings.theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="high-light-wrapper">
|
<div className="high-light-wrapper hj-wrapper">
|
||||||
{theme === 'dark' ? (
|
{currentTheme === 'dark' ? (
|
||||||
<CodeViewerDark lang={lang} code={code} copyable={copyable} />
|
<CodeViewerDark
|
||||||
|
lang={lang}
|
||||||
|
code={code}
|
||||||
|
copyValue={copyValue}
|
||||||
|
copyable={copyable}
|
||||||
|
height={height}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CodeViewerLight lang={lang} code={code} copyable={copyable} />
|
<CodeViewerLight
|
||||||
|
style={style}
|
||||||
|
lang={lang}
|
||||||
|
code={code}
|
||||||
|
copyValue={copyValue}
|
||||||
|
copyable={copyable}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ hue-6-2: #e6c07b
|
|||||||
.code-pre.dark {
|
.code-pre.dark {
|
||||||
.hljs {
|
.hljs {
|
||||||
color: #abb2bf;
|
color: #abb2bf;
|
||||||
background: #282c34;
|
background: var(--color-editor-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-comment,
|
.hljs-comment,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
.high-light-wrapper {
|
.high-light-wrapper {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
font-size: var(--font-size-code);
|
||||||
|
|
||||||
.hljs {
|
.hljs {
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
@@ -7,27 +8,46 @@
|
|||||||
padding-block: 1.2em;
|
padding-block: 1.2em;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
height: 8px;
|
height: var(--scrollbar-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(211, 211, 220, 50%);
|
background-color: transparent;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(190, 190, 190, 100%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-scrollbar-thumb);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--scrollbar-handle-light-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-pre {
|
.code-pre {
|
||||||
padding-inline: 12px 12px;
|
padding-inline: 12px 12px;
|
||||||
|
border-radius: 0 0 var(--border-radius-mini) var(--border-radius-mini);
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--border-radius-mini);
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
code {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
&.copyable {
|
&.copyable {
|
||||||
padding-inline: 12px 32px;
|
padding-inline: 12px 32px;
|
||||||
@@ -40,11 +60,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
background-color: #282c34;
|
background-color: var(--color-editor-dark);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--scrollbar-handle-light-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.light {
|
&.light {
|
||||||
background-color: rgb(250, 250, 250);
|
background-color: rgb(250, 250, 250);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-scrollbar-thumb);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ hue-6-2: #c18401
|
|||||||
.code-pre.light {
|
.code-pre.light {
|
||||||
.hljs {
|
.hljs {
|
||||||
color: #383a42;
|
color: #383a42;
|
||||||
background: #fafafa;
|
background: var(--color-editor-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-comment,
|
.hljs-comment,
|
||||||
|
|||||||
@@ -0,0 +1,591 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: iconfont; /* Project id 4613488 */
|
||||||
|
src: url('iconfont.woff2?t=1755160439217') format('woff2'),
|
||||||
|
url('iconfont.woff?t=1755160439217') format('woff'),
|
||||||
|
url('iconfont.ttf?t=1755160439217') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-family: iconfont !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-admin-user::before {
|
||||||
|
content: "\e69d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-user::before {
|
||||||
|
content: "\e69e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op::before {
|
||||||
|
content: "\e69b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-docker::before {
|
||||||
|
content: "\e69c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-digitalocean::before {
|
||||||
|
content: "\eb79";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-rocket-launch1::before {
|
||||||
|
content: "\e699";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-rocket-launch-fill::before {
|
||||||
|
content: "\e69a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-credential-filled::before {
|
||||||
|
content: "\e697";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-credential-outline::before {
|
||||||
|
content: "\e698";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-k8s-filled::before {
|
||||||
|
content: "\e63e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-k8s-outline::before {
|
||||||
|
content: "\e64b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-huggingface1::before {
|
||||||
|
content: "\e605";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-modelscope_light::before {
|
||||||
|
content: "\e696";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-catalog1::before {
|
||||||
|
content: "\e691";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chat-filled::before {
|
||||||
|
content: "\e692";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chat::before {
|
||||||
|
content: "\e693";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-files-filled::before {
|
||||||
|
content: "\e694";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-files::before {
|
||||||
|
content: "\e695";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-models-filled::before {
|
||||||
|
content: "\e682";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-image-filled::before {
|
||||||
|
content: "\e683";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-audio1::before {
|
||||||
|
content: "\e684";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-image1::before {
|
||||||
|
content: "\e685";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-audio-filled::before {
|
||||||
|
content: "\e686";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-reranker-filled::before {
|
||||||
|
content: "\e687";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-embedding-filled::before {
|
||||||
|
content: "\e68a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-models::before {
|
||||||
|
content: "\e68b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-reranker::before {
|
||||||
|
content: "\e68c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-embedding::before {
|
||||||
|
content: "\e68d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gpu-filled::before {
|
||||||
|
content: "\e68e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-catalog-filled::before {
|
||||||
|
content: "\e68f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gpu1::before {
|
||||||
|
content: "\e690";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-language::before {
|
||||||
|
content: "\e679";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-help::before {
|
||||||
|
content: "\e67a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-key-filled::before {
|
||||||
|
content: "\e67b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-key::before {
|
||||||
|
content: "\e67d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-resources::before {
|
||||||
|
content: "\e67e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-users::before {
|
||||||
|
content: "\e67f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-resources-filled::before {
|
||||||
|
content: "\e680";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-users-filled::before {
|
||||||
|
content: "\e681";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-model::before {
|
||||||
|
content: "\e676";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-model-filled::before {
|
||||||
|
content: "\e678";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-layers-filled::before {
|
||||||
|
content: "\e671";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-layers::before {
|
||||||
|
content: "\e673";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-experiment::before {
|
||||||
|
content: "\e674";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-experiment-filled::before {
|
||||||
|
content: "\e675";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dashboard::before {
|
||||||
|
content: "\e66d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dashboard-filled::before {
|
||||||
|
content: "\e66e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-expand-left::before {
|
||||||
|
content: "\ea48";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-expand-right::before {
|
||||||
|
content: "\ea49";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-users-fill::before {
|
||||||
|
content: "\e677";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-layers-fill::before {
|
||||||
|
content: "\e89d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-key-fill::before {
|
||||||
|
content: "\e80e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-server-fill::before {
|
||||||
|
content: "\e7a3";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-model-fill::before {
|
||||||
|
content: "\e7b7";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-left_panel_close::before {
|
||||||
|
content: "\e668";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-left_panel_open::before {
|
||||||
|
content: "\e669";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-playcircle-fill::before {
|
||||||
|
content: "\e665";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-stopcircle-fill::before {
|
||||||
|
content: "\e666";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-play-speed::before {
|
||||||
|
content: "\e856";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-more::before {
|
||||||
|
content: "\e62e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-play::before {
|
||||||
|
content: "\e9f9";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-pause::before {
|
||||||
|
content: "\e713";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dark_theme::before {
|
||||||
|
content: "\e646";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-theme-auto-1::before {
|
||||||
|
content: "\e660";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-theme-auto::before {
|
||||||
|
content: "\e663";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-auto-theme-1::before {
|
||||||
|
content: "\e664";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-auto-theme::before {
|
||||||
|
content: "\e662";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cols_3::before {
|
||||||
|
content: "\e65c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cols_6::before {
|
||||||
|
content: "\e65d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cols_2::before {
|
||||||
|
content: "\e65e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cols_4::before {
|
||||||
|
content: "\e65f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-a-save1::before {
|
||||||
|
content: "\e65b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-uncollapse_all::before {
|
||||||
|
content: "\e657";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-collapse::before {
|
||||||
|
content: "\e656";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-rocket-launch::before {
|
||||||
|
content: "\e689";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-user-filled::before {
|
||||||
|
content: "\e625";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-assistant::before {
|
||||||
|
content: "\e62d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-assistant-filled::before {
|
||||||
|
content: "\e847";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-save3::before {
|
||||||
|
content: "\e655";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-fankuifaqs::before {
|
||||||
|
content: "\e7bf";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-issues::before {
|
||||||
|
content: "\e816";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-neicun::before {
|
||||||
|
content: "\e688";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-collapse_all::before {
|
||||||
|
content: "\e66f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-down::before {
|
||||||
|
content: "\e654";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-fenxiang::before {
|
||||||
|
content: "\e604";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-mosaic-2::before {
|
||||||
|
content: "\e64f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-stars::before {
|
||||||
|
content: "\e8a8";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-mosaic::before {
|
||||||
|
content: "\e636";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-outline-play::before {
|
||||||
|
content: "\e653";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-SelectionInverse::before {
|
||||||
|
content: "\eace";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-justice1::before {
|
||||||
|
content: "\e652";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-New_img::before {
|
||||||
|
content: "\e733";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-new_release_outlined::before {
|
||||||
|
content: "\e66c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-new-releases::before {
|
||||||
|
content: "\e60f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-catalog::before {
|
||||||
|
content: "\e62b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-save2::before {
|
||||||
|
content: "\e635";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-left-template::before {
|
||||||
|
content: "\e62a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-logs::before {
|
||||||
|
content: "\e6ec";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gpu::before {
|
||||||
|
content: "\e71e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-filled-gpu::before {
|
||||||
|
content: "\e6de";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-outline-gpu::before {
|
||||||
|
content: "\e641";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ts-tubiao_webserver::before {
|
||||||
|
content: "\e716";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-server::before {
|
||||||
|
content: "\e66a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-host::before {
|
||||||
|
content: "\e7c6";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-playcircle::before {
|
||||||
|
content: "\e80f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-recreate::before {
|
||||||
|
content: "\e6a5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-save1::before {
|
||||||
|
content: "\e647";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-save::before {
|
||||||
|
content: "\e67c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-upload_image::before {
|
||||||
|
content: "\e613";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-sound-wave::before {
|
||||||
|
content: "\e619";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-rank1::before {
|
||||||
|
content: "\e7cb";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cube::before {
|
||||||
|
content: "\e769";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-speaker-slash::before {
|
||||||
|
content: "\ebb6";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-random::before {
|
||||||
|
content: "\e603";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-suijisenlin::before {
|
||||||
|
content: "\e60e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-stop2::before {
|
||||||
|
content: "\e8db";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-stop::before {
|
||||||
|
content: "\e60b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-image::before {
|
||||||
|
content: "\e62c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-SpeakerSlash::before {
|
||||||
|
content: "\e661";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-SpeakerHigh::before {
|
||||||
|
content: "\e670";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-user_voice::before {
|
||||||
|
content: "\e667";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-audio::before {
|
||||||
|
content: "\e985";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-hard-disk::before {
|
||||||
|
content: "\eb1d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-new::before {
|
||||||
|
content: "\e612";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-tu2::before {
|
||||||
|
content: "\e607";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-robot::before {
|
||||||
|
content: "\e634";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-robot1::before {
|
||||||
|
content: "\e602";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-aizhineng::before {
|
||||||
|
content: "\e672";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-copy::before {
|
||||||
|
content: "\e720";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-AIzhineng::before {
|
||||||
|
content: "\e608";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-clear::before {
|
||||||
|
content: "\e60a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-keyboard::before {
|
||||||
|
content: "\e61b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-networkerror::before {
|
||||||
|
content: "\e624";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-external-link::before {
|
||||||
|
content: "\e66b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-huggingface::before {
|
||||||
|
content: "\e7d1";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ollama::before {
|
||||||
|
content: "\e601";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-a-layout6-line::before {
|
||||||
|
content: "\e9ef";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-a-Layout5::before {
|
||||||
|
content: "\e610";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-English::before {
|
||||||
|
content: "\e8b3";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chinese::before {
|
||||||
|
content: "\e611";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-yingguo::before {
|
||||||
|
content: "\e606";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-code::before {
|
||||||
|
content: "\e84f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-stop1::before {
|
||||||
|
content: "\e783";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-command::before {
|
||||||
|
content: "\e600";
|
||||||
|
}
|
||||||
|
|
||||||