Compare commits
1387 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7a7c56ce4 | ||
|
|
2753265edb | ||
|
|
ac82726e12 | ||
|
|
dbe7b7aa7c | ||
|
|
16a700459e | ||
|
|
3aa7ecf8c0 | ||
|
|
b5b40e8bf3 | ||
|
|
7793093d0a | ||
|
|
5182529a58 | ||
|
|
0643e78a79 | ||
|
|
5517457929 | ||
|
|
711783baf5 | ||
|
|
11ad2df6f3 | ||
|
|
751543ab7a | ||
|
|
2f6187d2fa | ||
|
|
42e597349a | ||
|
|
949ec2ec6a | ||
|
|
0a7604f9ab | ||
|
|
fd38980751 | ||
|
|
0264644c2a | ||
|
|
10396c9ac9 | ||
|
|
b83ee46978 | ||
|
|
99251dcef8 | ||
|
|
4c8174d75d | ||
|
|
06a7a7be5c | ||
|
|
93387f64e0 | ||
|
|
fa5185952d | ||
|
|
3df3bcd9a5 | ||
|
|
c49dc76a04 | ||
|
|
1daae21236 | ||
|
|
63963b5ee6 | ||
|
|
ec45da57cf | ||
|
|
1775f0d45a | ||
|
|
3a4a09eedb | ||
|
|
ea226961bd | ||
|
|
7f5b96b88f | ||
|
|
2815287c2e | ||
|
|
03ae8c07b6 | ||
|
|
a3bda0ccac | ||
|
|
1cd7027e1a | ||
|
|
84c5085955 | ||
|
|
c9bd563fed | ||
|
|
0df622bbfd | ||
|
|
148962cbc2 | ||
|
|
a3179fae9d | ||
|
|
beb13bb280 | ||
|
|
5b0766d2b4 | ||
|
|
448662cce7 | ||
|
|
a6ba0af9f8 | ||
|
|
17ac4da44a | ||
|
|
9614a3138d | ||
|
|
b5facefde8 | ||
|
|
48fc5a7a7d | ||
|
|
cd7d00b4f8 | ||
|
|
f9d6c3034f | ||
|
|
175f9942e1 | ||
|
|
3e0cc92cfc | ||
|
|
db85605352 | ||
|
|
434d09183b | ||
|
|
c553198d5a | ||
|
|
c05fe80b89 | ||
|
|
700f3ab5d2 | ||
|
|
4a8ef1aec8 | ||
|
|
82bc96884a | ||
|
|
745d14c93f | ||
|
|
b838ae8fc1 | ||
|
|
06ca494e02 | ||
|
|
4ed9821c5d | ||
|
|
f0ba9b387d | ||
|
|
12cd0808bc | ||
|
|
07bfc890c2 | ||
|
|
67dfc0a87c | ||
|
|
ffd66d0f5e | ||
|
|
afe4dbd131 | ||
|
|
1f46065ebc | ||
|
|
597941fbd1 | ||
|
|
ec2d4957e4 | ||
|
|
5c1463ffa9 | ||
|
|
6ec8df94dc | ||
|
|
a72772d5ee | ||
|
|
ac47668016 | ||
|
|
3f7e1a4af6 | ||
|
|
f40649a534 | ||
|
|
99cbabbaaf | ||
|
|
04747667b5 | ||
|
|
9eaedd2c28 | ||
|
|
667a4ea656 | ||
|
|
2e856b1922 | ||
|
|
e403704ab3 | ||
|
|
d1c926205a | ||
|
|
b8fa23b6a5 | ||
|
|
96b828b1bf | ||
|
|
9ddcf79bac | ||
|
|
65fa2986b1 | ||
|
|
69e7c282ca | ||
|
|
109e9fef37 | ||
|
|
8ce1455555 | ||
|
|
69bdce481f | ||
|
|
10527ecfa8 | ||
|
|
82c15699cc | ||
|
|
092161316e | ||
|
|
faf907b61b | ||
|
|
2ecf9e331d | ||
|
|
1537b06278 | ||
|
|
bcbc92ad57 | ||
|
|
41ffb8a220 | ||
|
|
1d11f01115 | ||
|
|
1deebf3f5c | ||
|
|
ae1120f1e3 | ||
|
|
b597ed2275 | ||
|
|
1ecfacce49 | ||
|
|
b20f00be53 | ||
|
|
168fff56c1 | ||
|
|
689705ef70 | ||
|
|
6629a9ad14 | ||
|
|
af79d1e860 | ||
|
|
38379fac65 | ||
|
|
7534dd79fa | ||
|
|
f956fc949b | ||
|
|
442be3b02c | ||
|
|
208c25b2f7 | ||
|
|
dd4b43c10e | ||
|
|
519a9b1d1b | ||
|
|
f0c278a35f | ||
|
|
ab5885f2a0 | ||
|
|
9bc1d7d7bf | ||
|
|
4d8fd96190 | ||
|
|
7f7a5357a0 | ||
|
|
28adf38f2f | ||
|
|
b2abe9f570 | ||
|
|
5aa8657640 | ||
|
|
44dbe20b03 | ||
|
|
3a0621aab8 | ||
|
|
28f4479d3c | ||
|
|
e356ae67c7 | ||
|
|
6b31889261 | ||
|
|
16aec83f02 | ||
|
|
9fe0f9f677 | ||
|
|
ee4dcb1b8b | ||
|
|
5dfaba943b | ||
|
|
bac66de046 | ||
|
|
9b8f3b1b2b | ||
|
|
56d5f0b5a3 | ||
|
|
51a731ea68 | ||
|
|
002f03e037 | ||
|
|
a250819d87 | ||
|
|
135e58f1e2 | ||
|
|
58b6cd2340 | ||
|
|
b21f40942e | ||
|
|
dd9fa3d0f2 | ||
|
|
430b8eee91 | ||
|
|
cec64b889b | ||
|
|
443ef6ad2d | ||
|
|
3e8b607948 | ||
|
|
9eeaaa70c4 | ||
|
|
80ae205cb3 | ||
|
|
cd38e0eedd | ||
|
|
66d217d5f1 | ||
|
|
2b128195bf | ||
|
|
e469537ac5 | ||
|
|
b2e84a2259 | ||
|
|
228d59402f | ||
|
|
ea2dd8cab2 | ||
|
|
efcbb06eb2 | ||
|
|
61e720c247 | ||
|
|
88418386a4 | ||
|
|
f096e07ec8 | ||
|
|
046b00404e | ||
|
|
9bb6390f9a | ||
|
|
cb612746a6 | ||
|
|
8beb2cb6fe | ||
|
|
aabd8037a6 | ||
|
|
408c6a2029 | ||
|
|
ba0ebcf273 | ||
|
|
0457528dd0 | ||
|
|
e6037ccfd5 | ||
|
|
ce1c434ea8 | ||
|
|
b4c813311b | ||
|
|
08d2a811b7 | ||
|
|
256afaee12 | ||
|
|
198c62081b | ||
|
|
53b679aeb6 | ||
|
|
64358df1c1 | ||
|
|
b0d9481cdb | ||
|
|
db0985504a | ||
|
|
82f9b9371a | ||
|
|
d9ee7e9402 | ||
|
|
b609eeb0eb | ||
|
|
203e8c8eaf | ||
|
|
2c90f6f33c | ||
|
|
88bc7ef3cd | ||
|
|
5ae0d997d9 | ||
|
|
879396e91c | ||
|
|
b8754f6174 | ||
|
|
302617cd81 | ||
|
|
79fd133821 | ||
|
|
4f2b34522f | ||
|
|
853a7f32d2 | ||
|
|
eea18cc0c7 | ||
|
|
0e173668c5 | ||
|
|
abdcff813c | ||
|
|
0ac84bd033 | ||
|
|
4c12619f0e | ||
|
|
dc1f194bcc | ||
|
|
7a89b80c8f | ||
|
|
7a4f40de54 | ||
|
|
988d7fcdd3 | ||
|
|
236ef09538 | ||
|
|
d914f097c0 | ||
|
|
c37e5aca35 | ||
|
|
5e198d1021 | ||
|
|
cc91552c36 | ||
|
|
f208bbdb9a | ||
|
|
20f567da37 | ||
|
|
9b45ebea0e | ||
|
|
7a0ae54705 | ||
|
|
3f5a2189ce | ||
|
|
36d6f94177 | ||
|
|
c046bdab30 | ||
|
|
0c8f1d6419 | ||
|
|
f8810f5199 | ||
|
|
bc6421b1af | ||
|
|
2f00ee176b | ||
|
|
44da1feed3 | ||
|
|
66c8623138 | ||
|
|
e12fbad859 | ||
|
|
183df2cac3 | ||
|
|
7f1fda40dc | ||
|
|
d42a2df286 | ||
|
|
1a345fff79 | ||
|
|
7724d60a05 | ||
|
|
1cb7903e37 | ||
|
|
d8183909ca | ||
|
|
7f7d5d5c39 | ||
|
|
72cf3fd81c | ||
|
|
65167be7f8 | ||
|
|
7dda34a779 | ||
|
|
bde1d36005 | ||
|
|
631e8d00e2 | ||
|
|
3720987997 | ||
|
|
8e4ce2aee2 | ||
|
|
08fc18da86 | ||
|
|
b2760cde0e | ||
|
|
61926503cf | ||
|
|
84a51b8f91 | ||
|
|
4f8a69a511 | ||
|
|
4537305dfc | ||
|
|
be901a92f6 | ||
|
|
1067a15916 | ||
|
|
2a41d88ce3 | ||
|
|
80027e8fa3 | ||
|
|
df893296ba | ||
|
|
77e9f763cd | ||
|
|
c531957ce8 | ||
|
|
257ca5014b | ||
|
|
1c3e567b9a | ||
|
|
4aacb7bcb6 | ||
|
|
e7fef596d4 | ||
|
|
5d95ca6f53 | ||
|
|
3cd6bf9dba | ||
|
|
67a4bd6e3a | ||
|
|
c33c62aa6d | ||
|
|
581e09d45b | ||
|
|
688a200999 | ||
|
|
cc89df7957 | ||
|
|
198e0434b3 | ||
|
|
55fe2b46df | ||
|
|
c7e934f23d | ||
|
|
7752dd136f | ||
|
|
af2f32da4a | ||
|
|
81859dfacb | ||
|
|
3e145ca123 | ||
|
|
c9a5d3091f | ||
|
|
4374ad5036 | ||
|
|
1ca2ca7058 | ||
|
|
9fe15b1bec | ||
|
|
a557319b3e | ||
|
|
f06eb0e9d7 | ||
|
|
1be091522e | ||
|
|
63df35b79c | ||
|
|
2697da15aa | ||
|
|
ced0f993de | ||
|
|
55629a8ce4 | ||
|
|
923c6a5904 | ||
|
|
b63e87d0cf | ||
|
|
96caa71f03 | ||
|
|
b9b1877e04 | ||
|
|
81daa8cfdc | ||
|
|
903ea005e3 | ||
|
|
580e9f4f73 | ||
|
|
51e08d1b2a | ||
|
|
3708f5a61e | ||
|
|
58377b4670 | ||
|
|
5bb5aa3d5a | ||
|
|
71def21369 | ||
|
|
1ea565fbdf | ||
|
|
56a9a015f1 | ||
|
|
0708b5a134 | ||
|
|
242065d298 | ||
|
|
b5d6214521 | ||
|
|
9c5bccb23c | ||
|
|
5f5b567749 | ||
|
|
354d6c4f62 | ||
|
|
2c37314c4f | ||
|
|
1dbdee155e | ||
|
|
2939b63998 | ||
|
|
ff7097f2d9 | ||
|
|
33cd4de3c5 | ||
|
|
0bc6c3a22a | ||
|
|
44edfe7a81 | ||
|
|
d3fae3bbd0 | ||
|
|
11a4ec8c67 | ||
|
|
75aab08a72 | ||
|
|
2cc2184002 | ||
|
|
57c398f537 | ||
|
|
7d9f94984c | ||
|
|
8659049ae9 | ||
|
|
1ca5b30c15 | ||
|
|
b9bc0a71a9 | ||
|
|
13ae8bd76f | ||
|
|
31fdabaa15 | ||
|
|
43e9930b45 | ||
|
|
a77192ebc2 | ||
|
|
181a792d0a | ||
|
|
43f6a061e1 | ||
|
|
05bba1a461 | ||
|
|
97fc9959bb | ||
|
|
ad2d1c27ab | ||
|
|
97144e4ebe | ||
|
|
0cc30e5cb1 | ||
|
|
eebb73f49e | ||
|
|
72626e3367 | ||
|
|
07a44e104d | ||
|
|
e4a8975baa | ||
|
|
23de3cac7e | ||
|
|
0244871fc1 | ||
|
|
5691d78004 | ||
|
|
9e76d39707 | ||
|
|
5087190816 | ||
|
|
ce47856c81 | ||
|
|
a03c162962 | ||
|
|
e658a32244 | ||
|
|
637dcddee2 | ||
|
|
5c7940538d | ||
|
|
499b36b03b | ||
|
|
9e101e924e | ||
|
|
10f876f2f6 | ||
|
|
ed4d305165 | ||
|
|
a5cc4e5e18 | ||
|
|
65da14032b | ||
|
|
7e2dd2914d | ||
|
|
644dd8912d | ||
|
|
d7e08fe420 | ||
|
|
cd27506388 | ||
|
|
78de6c92b7 | ||
|
|
cd41190663 | ||
|
|
bbdfdc136b | ||
|
|
5a52b56d89 | ||
|
|
d8d1003d7a | ||
|
|
c801490c2f | ||
|
|
30b33ec974 | ||
|
|
4140e6d3d7 | ||
|
|
1786dec900 | ||
|
|
bda57a84e8 | ||
|
|
deac306f69 | ||
|
|
43b60a0049 | ||
|
|
ab2ee050de | ||
|
|
21de87ca32 | ||
|
|
782031cbc6 | ||
|
|
6c9551be05 | ||
|
|
36eb67a217 | ||
|
|
7dcbcfacc1 | ||
|
|
2c9bda734e | ||
|
|
d296d019e5 | ||
|
|
ccf947c64d | ||
|
|
3faee0d8be | ||
|
|
fe12f78c1d | ||
|
|
a2b0041490 | ||
|
|
235e3ed994 | ||
|
|
4baa82449c | ||
|
|
84ced0c32a | ||
|
|
beabe1c191 | ||
|
|
719ed98996 | ||
|
|
b0a60e428c | ||
|
|
16a8656b49 | ||
|
|
7fdb993f57 | ||
|
|
0e56f0808c | ||
|
|
bd45a9252a | ||
|
|
5a5160b92d | ||
|
|
3e78122659 | ||
|
|
57d85765c3 | ||
|
|
067ae0fd10 | ||
|
|
bf421e1a49 | ||
|
|
339f06e630 | ||
|
|
367a250fba | ||
|
|
73110acb37 | ||
|
|
0912a0067e | ||
|
|
5faf83e437 | ||
|
|
3f76f37a4c | ||
|
|
f0574ef555 | ||
|
|
304059bef7 | ||
|
|
c60e361aa3 | ||
|
|
273ea73b1b | ||
|
|
faae2906b2 | ||
|
|
4db65c2cbf | ||
|
|
bb2b681ae0 | ||
|
|
e9aa500800 | ||
|
|
6eeabe4203 | ||
|
|
caef9d7d46 | ||
|
|
f7c0679abc | ||
|
|
fb9e61aafe | ||
|
|
316cac8149 | ||
|
|
4ca75d4907 | ||
|
|
7d9f9fbd08 | ||
|
|
95eed69b66 | ||
|
|
8fccc61e38 | ||
|
|
1582e22756 | ||
|
|
4684e880b1 | ||
|
|
227fcae39d | ||
|
|
8d5869e6c2 | ||
|
|
eca5549592 | ||
|
|
01bf7ee271 | ||
|
|
240743c563 | ||
|
|
94fa7a8279 | ||
|
|
5715849de5 | ||
|
|
dbe4ee692c | ||
|
|
b3924cdb71 | ||
|
|
c41d8125da | ||
|
|
58243d9c17 | ||
|
|
3200f09836 | ||
|
|
93b4834539 | ||
|
|
268da6e47c | ||
|
|
a74f9b9557 | ||
|
|
dfe4ff076d | ||
|
|
a6ee45f3e3 | ||
|
|
cec0eff031 | ||
|
|
ae4a165b07 | ||
|
|
bfaf21a180 | ||
|
|
75e35f52ab | ||
|
|
eb5dbe5066 | ||
|
|
5da3bc3000 | ||
|
|
d6a003c3e2 | ||
|
|
f28f9049d5 | ||
|
|
e9994bcd8a | ||
|
|
b1b68f4397 | ||
|
|
87d1e1c44f | ||
|
|
bb72038e2b | ||
|
|
2f804c7d40 | ||
|
|
5a287a42ac | ||
|
|
d940d36123 | ||
|
|
a8b0add92b | ||
|
|
49d8c4f818 | ||
|
|
674bbe1c3e | ||
|
|
daf504d5fd | ||
|
|
d722c03744 | ||
|
|
6eddf2f6db | ||
|
|
02398e9ced | ||
|
|
35c9369d3d | ||
|
|
018a6162da | ||
|
|
b46c808aa8 | ||
|
|
921ce713fa | ||
|
|
cf75ee4150 | ||
|
|
a997734c5c | ||
|
|
f9c08b6091 | ||
|
|
8daa0a5d5c | ||
|
|
a3af9de4a9 | ||
|
|
e467a8e6e0 | ||
|
|
46c67e3bfd | ||
|
|
a6e39c32c1 | ||
|
|
fa802b50ad | ||
|
|
e163ada783 | ||
|
|
888e2b376a | ||
|
|
adc22c7d19 | ||
|
|
ac605baa24 | ||
|
|
05c5915e7b | ||
|
|
a4060fec1b | ||
|
|
469ab4a085 | ||
|
|
367a76a54f | ||
|
|
998e65d98d | ||
|
|
a8ba45c08a | ||
|
|
055b300c5a | ||
|
|
baf389ec65 | ||
|
|
ca7a2e954f | ||
|
|
eada5fe8b1 | ||
|
|
fe347965ff | ||
|
|
8d017ed2cb | ||
|
|
eafca9ae85 | ||
|
|
990c7a2693 | ||
|
|
da87f9a865 | ||
|
|
515c85e600 | ||
|
|
6b3948d454 | ||
|
|
e3afa24795 | ||
|
|
903a29ed09 | ||
|
|
6a64e3854c | ||
|
|
0d1d082d9e | ||
|
|
8ae8e66a3a | ||
|
|
f684c4cb3f | ||
|
|
44a8fbe0c9 | ||
|
|
9c5e253d75 | ||
|
|
5fb15592ff | ||
|
|
da8bcb04c9 | ||
|
|
1862b11c0d | ||
|
|
942f08bbf2 | ||
|
|
175c78c3d0 | ||
|
|
0decc34593 | ||
|
|
18a7055a4e | ||
|
|
5638d8c66a | ||
|
|
1644e478c7 | ||
|
|
d1c35b480f | ||
|
|
3d8c656224 | ||
|
|
364a52b553 | ||
|
|
8cff6750f9 | ||
|
|
d01c91e6ee | ||
|
|
e1893e1506 | ||
|
|
ae4b592bb6 | ||
|
|
c65001399f | ||
|
|
494ccbbe5e | ||
|
|
8b28194d00 | ||
|
|
3ff3e40d8e | ||
|
|
77072594d2 | ||
|
|
3c2fec2122 | ||
|
|
f58070495f | ||
|
|
51c60c55b0 | ||
|
|
781662026a | ||
|
|
422fed2719 | ||
|
|
27a108027b | ||
|
|
5f1a1e9f0e | ||
|
|
aa3c07a0b2 | ||
|
|
e409618b7a | ||
|
|
7d8573b52a | ||
|
|
28cbcea4f9 | ||
|
|
e1d4f678ab | ||
|
|
bb6453d3a2 | ||
|
|
1b02666531 | ||
|
|
3b82908e8a | ||
|
|
8941b87e03 | ||
|
|
1f081cdddc | ||
|
|
dc58109829 | ||
|
|
3b36b19d01 | ||
|
|
26af225fff | ||
|
|
1c9f986a97 | ||
|
|
7056e00fcc | ||
|
|
5c86c2fbf2 | ||
|
|
7f4bc0487e | ||
|
|
75e62cce72 | ||
|
|
a5b1861e7d | ||
|
|
68a5350c19 | ||
|
|
81f225d9cb | ||
|
|
268aedbb6e | ||
|
|
c2dce8344b | ||
|
|
ef45bedf83 | ||
|
|
35b38e8542 | ||
|
|
c3f1433868 | ||
|
|
f137bd2c07 | ||
|
|
5b9ae2b805 | ||
|
|
eef514bea1 | ||
|
|
9be85c4c2d | ||
|
|
17e81cafc2 | ||
|
|
b6b1d581a3 | ||
|
|
7db48facbd | ||
|
|
0958c5f9c7 | ||
|
|
7475dbd07c | ||
|
|
19734415f0 | ||
|
|
252d02e7b9 | ||
|
|
64ad480bbb | ||
|
|
4df670d1c9 | ||
|
|
52f4a2b1e1 | ||
|
|
527d5341db | ||
|
|
a775596719 | ||
|
|
3fbf54df99 | ||
|
|
fbb78ebd50 | ||
|
|
87bf284943 | ||
|
|
d319264349 | ||
|
|
3230bbe2e5 | ||
|
|
fc5ae31b2b | ||
|
|
8bf6cbe1d4 | ||
|
|
1083822513 | ||
|
|
db22100846 | ||
|
|
d751bbfcd4 | ||
|
|
ceffea4c5b | ||
|
|
a716315311 | ||
|
|
edf5abcec9 | ||
|
|
d26e5a324d | ||
|
|
0275eea6e7 | ||
|
|
7091adb6a0 | ||
|
|
f11bc7f686 | ||
|
|
3b0b83aae1 | ||
|
|
038c4149d8 | ||
|
|
811ebff52f | ||
|
|
8908da5baf | ||
|
|
204791075e | ||
|
|
da63930f5e | ||
|
|
892e82c1d6 | ||
|
|
806df16a0b | ||
|
|
077f4fd488 | ||
|
|
0b30cc3ae7 | ||
|
|
b31e1a47f0 | ||
|
|
7c36100553 | ||
|
|
f2c25927eb | ||
|
|
6e2fc369e2 | ||
|
|
57f2e0d649 | ||
|
|
381eb37f1a | ||
|
|
2c46cb956f | ||
|
|
31c7980a9a | ||
|
|
b067134ae7 | ||
|
|
be5a848342 | ||
|
|
1f3d39a1bd | ||
|
|
e96c65c1ab | ||
|
|
80495c3594 | ||
|
|
1667e5240d | ||
|
|
afb8ba0ef3 | ||
|
|
ddc3e855d4 | ||
|
|
d8f5771dcb | ||
|
|
5313bc6a18 | ||
|
|
ff7b583f07 | ||
|
|
e1fce63876 | ||
|
|
85149e5af0 | ||
|
|
713956aa4f | ||
|
|
0da8a75fbe | ||
|
|
4a946062b3 | ||
|
|
5f0c1c8b97 | ||
|
|
0e6b885095 | ||
|
|
f5480bdc58 | ||
|
|
5102f02083 | ||
|
|
e38b0fde51 | ||
|
|
340f074f01 | ||
|
|
cb0256c820 | ||
|
|
ed43e67047 | ||
|
|
a923615190 | ||
|
|
d8074502e0 | ||
|
|
1bd3883b18 | ||
|
|
0ae2265c50 | ||
|
|
9fa09bb743 | ||
|
|
c90628a71f | ||
|
|
7b3df95152 | ||
|
|
372847f398 | ||
|
|
f9dd2c6bd6 | ||
|
|
34083441d4 | ||
|
|
8796ec5fbc | ||
|
|
c6639a8259 | ||
|
|
3b063ff64c | ||
|
|
ef71d83321 | ||
|
|
a73481808c | ||
|
|
8ff912a4a7 | ||
|
|
c22ab933c7 | ||
|
|
31354272e7 | ||
|
|
0b50724b2f | ||
|
|
43209882e4 | ||
|
|
5ea49ef6f1 | ||
|
|
7699397bda | ||
|
|
31217c6fd7 | ||
|
|
acabbf30da | ||
|
|
adc5dd1742 | ||
|
|
0b398b372e | ||
|
|
1d5d46c9ce | ||
|
|
0beeb15245 | ||
|
|
43b63908b5 | ||
|
|
c40d3fe044 | ||
|
|
52c93b80f6 | ||
|
|
30fd27e570 | ||
|
|
6d7c5202c2 | ||
|
|
4e63e533d3 | ||
|
|
e0feb6dff1 | ||
|
|
8e9313eafa | ||
|
|
fa602f3efa | ||
|
|
f96377ff12 | ||
|
|
7a01efaa52 | ||
|
|
1e1b65684d | ||
|
|
e506334689 | ||
|
|
d2ab632f2f | ||
|
|
5acdd13966 | ||
|
|
b3bf342320 | ||
|
|
27dfb17e70 | ||
|
|
6fa17a8f81 | ||
|
|
0b48be15aa | ||
|
|
0159a246ac | ||
|
|
e00bbf576a | ||
|
|
88653c1656 | ||
|
|
31dad88078 | ||
|
|
ebc24432ff | ||
|
|
664d86838d | ||
|
|
58dfbe3084 | ||
|
|
5a1c0321a6 | ||
|
|
1cc5605291 | ||
|
|
fb0cce836d | ||
|
|
55be0c18e2 | ||
|
|
a9680d2464 | ||
|
|
7060bcce01 | ||
|
|
3c89d312c6 | ||
|
|
8e9b2920ed | ||
|
|
1b2a6d5acd | ||
|
|
c32ff2cf29 | ||
|
|
424fc518ff | ||
|
|
ac5c445b6f | ||
|
|
59115fbf69 | ||
|
|
7560389e35 | ||
|
|
74b39ac480 | ||
|
|
7ed368a137 | ||
|
|
a8a9cf76df | ||
|
|
fe82be5f21 | ||
|
|
65fe93421b | ||
|
|
f0cd287f9a | ||
|
|
4c2c3db052 | ||
|
|
3a90b1d163 | ||
|
|
0eb6716263 | ||
|
|
92242bb8ff | ||
|
|
8742560e6c | ||
|
|
fbfe0ba1fd | ||
|
|
7b39e68363 | ||
|
|
8f0c6c30dc | ||
|
|
1423b28663 | ||
|
|
1b15125bee | ||
|
|
c6adee4502 | ||
|
|
51a7f4f2ee | ||
|
|
0c846a86b9 | ||
|
|
0fe7f86ad6 | ||
|
|
69613450c7 | ||
|
|
a9b769e510 | ||
|
|
deb0eb85b5 | ||
|
|
ce1eddad68 | ||
|
|
a9c41aa9c3 | ||
|
|
b0d5068192 | ||
|
|
2bd82fc6ef | ||
|
|
17279390d4 | ||
|
|
91a95e20a2 | ||
|
|
cb7a37fe9e | ||
|
|
4498f3542b | ||
|
|
fab5204c56 | ||
|
|
24bb2adc70 | ||
|
|
75230dd3b8 | ||
|
|
4440da92bf | ||
|
|
07c22e06ba | ||
|
|
ef839f65c1 | ||
|
|
ce6e752905 | ||
|
|
9c1cfda40b | ||
|
|
d87d3ed9e1 | ||
|
|
c21387874b | ||
|
|
5b35786c5c | ||
|
|
3829da428c | ||
|
|
d8ef4d522c | ||
|
|
26b7eb7cd8 | ||
|
|
f69659309d | ||
|
|
18f2ebf455 | ||
|
|
c625e12075 | ||
|
|
ed58b824ac | ||
|
|
6fa89dbdf4 | ||
|
|
8dc19a522f | ||
|
|
dd18b32a09 | ||
|
|
b916180ccd | ||
|
|
593e6c4670 | ||
|
|
8a7a717801 | ||
|
|
a53460b4cc | ||
|
|
f0f64dd118 | ||
|
|
ac53861a85 | ||
|
|
85c73a5069 | ||
|
|
94d40308d2 | ||
|
|
e9fbebdfad | ||
|
|
f58b95cd95 | ||
|
|
d110c97424 | ||
|
|
5b5a05d8c5 | ||
|
|
dc93c7f9f4 | ||
|
|
065b516bd7 | ||
|
|
7935936da1 | ||
|
|
bedd05c6ae | ||
|
|
a32d0781fc | ||
|
|
15a17b5e1d | ||
|
|
527a76c480 | ||
|
|
02c94a12d5 | ||
|
|
706be3efd1 | ||
|
|
3477b5d575 | ||
|
|
680c16c7da | ||
|
|
c9eb19a064 | ||
|
|
8b2c24d7a1 | ||
|
|
18b2735f80 | ||
|
|
d11ae4365c | ||
|
|
a251697813 | ||
|
|
f9f798417b | ||
|
|
4dc377c16e | ||
|
|
e528d8dc51 | ||
|
|
81e0f23075 | ||
|
|
a7124c800e | ||
|
|
93f0220a71 | ||
|
|
1d94368579 | ||
|
|
21396f5a1d | ||
|
|
4d69c8e570 | ||
|
|
16e3ccada8 | ||
|
|
a723b2801e | ||
|
|
f577dbdd70 | ||
|
|
d84c11a459 | ||
|
|
5c5059681d | ||
|
|
c88431b564 | ||
|
|
d802b9da1f | ||
|
|
c91e54370f | ||
|
|
27f0532491 | ||
|
|
d95e07285c | ||
|
|
2b3c41ce36 | ||
|
|
410a43e6a1 | ||
|
|
a270b8ce3e | ||
|
|
62e8039138 | ||
|
|
9f01ad0665 | ||
|
|
268afb7209 | ||
|
|
fb362a56af | ||
|
|
e5bf979682 | ||
|
|
cb05b5beb3 | ||
|
|
7cf6f1b9fa | ||
|
|
5c42c8ad3c | ||
|
|
06c8b43950 | ||
|
|
7937fea5c3 | ||
|
|
4a48bec42d | ||
|
|
575e0f2924 | ||
|
|
7749947138 | ||
|
|
66ddb75c13 | ||
|
|
625705eaf0 | ||
|
|
287c2637ed | ||
|
|
f640733276 | ||
|
|
cc432ba18f | ||
|
|
fe03a4e9c1 | ||
|
|
4c3c32d5a4 | ||
|
|
0be87ee2db | ||
|
|
7d3b5dbbe9 | ||
|
|
2e3ac230ca | ||
|
|
55286bdfe5 | ||
|
|
8b64c1ab0d | ||
|
|
02b864d657 | ||
|
|
ca1eefb906 | ||
|
|
f2fb543f93 | ||
|
|
d3ee0bde5a | ||
|
|
1ccdfa3e12 | ||
|
|
9816d7ec18 | ||
|
|
a2a0cb8461 | ||
|
|
76e5004830 | ||
|
|
14d9790b5c | ||
|
|
2480f79a34 | ||
|
|
084dd23bc2 | ||
|
|
b989c19a8a | ||
|
|
caa57b3e8b | ||
|
|
f6b0e7b0d9 | ||
|
|
4d6b516662 | ||
|
|
1caf567f10 | ||
|
|
1e34ecffc2 | ||
|
|
dd2f58dc78 | ||
|
|
1977aa50c2 | ||
|
|
60173a0efb | ||
|
|
fbfce10afb | ||
|
|
2c539f4316 | ||
|
|
db9e4eb004 | ||
|
|
bea5f44565 | ||
|
|
9b3181576f | ||
|
|
648a61d09a | ||
|
|
18fbf79fa8 | ||
|
|
ff97490a2b | ||
|
|
dc60dacb02 | ||
|
|
b69ad40013 | ||
|
|
8d386d9bdf | ||
|
|
32f50d676e | ||
|
|
ecb5a741bf | ||
|
|
19977cc536 | ||
|
|
51312a3a2a | ||
|
|
919f0dc120 | ||
|
|
23c54af27d | ||
|
|
9ed298bf3e | ||
|
|
8507b640b2 | ||
|
|
3a66a3af3f | ||
|
|
95303d2960 | ||
|
|
ad60e684fc | ||
|
|
0f1edf3f9c | ||
|
|
51b1918f72 | ||
|
|
46d6641259 | ||
|
|
984b9af719 | ||
|
|
42e74a05f5 | ||
|
|
4364546bc9 | ||
|
|
2bcaadbeb6 | ||
|
|
4385995742 | ||
|
|
2c7d90e49d | ||
|
|
bc34346301 | ||
|
|
5619d2b00e | ||
|
|
56a4711189 | ||
|
|
4b84d5f013 | ||
|
|
c6304d75cf | ||
|
|
8fb8e83632 | ||
|
|
390d88e0e9 | ||
|
|
e08c290a66 | ||
|
|
f60bf294c1 | ||
|
|
ae4fcbad67 | ||
|
|
543e274737 | ||
|
|
98a9aa7701 | ||
|
|
04589154ca | ||
|
|
03056186c6 | ||
|
|
514b077da4 | ||
|
|
c4866e3865 | ||
|
|
d0953585a7 | ||
|
|
6782f188f5 | ||
|
|
cc58e4420e | ||
|
|
ef95bcb8bc | ||
|
|
a5064263ba | ||
|
|
7fcde76981 | ||
|
|
24c99c7b95 | ||
|
|
575aa833b5 | ||
|
|
c9f9530d09 | ||
|
|
e76e5629bd | ||
|
|
7853a3ff97 | ||
|
|
983dbf0009 | ||
|
|
f724e91421 | ||
|
|
f0bf8f737f | ||
|
|
9a25812b81 | ||
|
|
79ccb20135 | ||
|
|
fbc40bb4be | ||
|
|
cb44735eec | ||
|
|
c5bc67771f | ||
|
|
eb073b04ac | ||
|
|
2b48dd8722 | ||
|
|
9fa647e40e | ||
|
|
fe5e7b1633 | ||
|
|
6c6d0f328e | ||
|
|
9a8af03932 | ||
|
|
6c8becb201 | ||
|
|
cc180dcb65 | ||
|
|
d98b056b57 | ||
|
|
527402425f | ||
|
|
dba6ae164c | ||
|
|
97cb2b072b | ||
|
|
f13d623d14 | ||
|
|
4e8c728457 | ||
|
|
e960c0863f | ||
|
|
79c1a539bb | ||
|
|
bd6f1064fb | ||
|
|
57ceae97a1 | ||
|
|
4fa7834510 | ||
|
|
8ac5413765 | ||
|
|
524eef9a54 | ||
|
|
a4d19c8fc9 | ||
|
|
d62701fa85 | ||
|
|
59da085676 | ||
|
|
1305bfac59 | ||
|
|
b84a3255c6 | ||
|
|
8682cdab51 | ||
|
|
2033faff08 | ||
|
|
7f5daa0f85 | ||
|
|
b2db8e8f5a | ||
|
|
1c99eee96b | ||
|
|
689e6d18df | ||
|
|
c4ba459b20 | ||
|
|
ae1ef52113 | ||
|
|
03145afa4f | ||
|
|
b03b457e7e | ||
|
|
f31941425d | ||
|
|
18ec2752ba | ||
|
|
bbf0bee19b | ||
|
|
e3bf844420 | ||
|
|
afa58712cf | ||
|
|
f76029c2d9 | ||
|
|
25da77a3b1 | ||
|
|
70256b252c | ||
|
|
e7c109eab1 | ||
|
|
66241f32af | ||
|
|
2e7f890218 | ||
|
|
d31edeef65 | ||
|
|
55ff880993 | ||
|
|
7c9ae9e339 | ||
|
|
f1a2ee6170 | ||
|
|
25a77db3c6 | ||
|
|
914538d2ed | ||
|
|
9c9b982a56 | ||
|
|
0d18623473 | ||
|
|
876b4e4a5d | ||
|
|
a6134bb455 | ||
|
|
711159aa2b | ||
|
|
451d737d90 | ||
|
|
12d8c39ccf | ||
|
|
54464efdcd | ||
|
|
923f0e99b9 | ||
|
|
2d78c2a37a | ||
|
|
37da3e8350 | ||
|
|
890e3c338b | ||
|
|
8d9d069267 | ||
|
|
3c68a603af | ||
|
|
1f0ef3c64d | ||
|
|
df0bbddbd5 | ||
|
|
7bd7bd3564 | ||
|
|
30cafc5619 | ||
|
|
7e5617bd80 | ||
|
|
533c16be3f | ||
|
|
ff96312d53 | ||
|
|
0bddc4dc5b | ||
|
|
9cdfe552e0 | ||
|
|
c27642bc12 | ||
|
|
a3ba45626d | ||
|
|
ef853d2736 | ||
|
|
8b1387a8ed | ||
|
|
647e2a5f7e | ||
|
|
d3ae45a360 | ||
|
|
48880bed0d | ||
|
|
c6da2062f2 | ||
|
|
414660a0a5 | ||
|
|
2995755cda | ||
|
|
0d6ac88dea | ||
|
|
eb63dc1d8d | ||
|
|
22560a5ce2 | ||
|
|
4c5e049c16 | ||
|
|
92e9b69348 | ||
|
|
b08086e3f5 | ||
|
|
70877c8c96 | ||
|
|
e35403eaf4 | ||
|
|
8d8068f5d6 | ||
|
|
fd82dfc5e0 | ||
|
|
d5e609aa37 | ||
|
|
b3f6d4f6e7 | ||
|
|
4fd8c1f5bf | ||
|
|
45c496313d | ||
|
|
97bb1c1fa9 | ||
|
|
316a237297 | ||
|
|
efa34a1ac6 | ||
|
|
c146796987 | ||
|
|
a67ca442a0 | ||
|
|
e7b795f744 | ||
|
|
20658c3ca5 | ||
|
|
cfa8182789 | ||
|
|
a867f25481 | ||
|
|
9906f14446 | ||
|
|
cca999a18d | ||
|
|
3198959cb7 | ||
|
|
832be374b5 | ||
|
|
aec0bad35f | ||
|
|
f77e8709a8 | ||
|
|
f05efe464a | ||
|
|
3cb10e217b | ||
|
|
52430d6466 | ||
|
|
64c75772bb | ||
|
|
5437a47e37 | ||
|
|
1605b96173 | ||
|
|
09e78277b5 | ||
|
|
58ad5b3254 | ||
|
|
14813013a7 | ||
|
|
343c142d87 | ||
|
|
25b983a9ae | ||
|
|
8e16067cf3 | ||
|
|
b9d88160a2 | ||
|
|
b780a4db58 | ||
|
|
0975995486 | ||
|
|
14f0a6d7f3 | ||
|
|
136e0f6225 | ||
|
|
8581432bf4 | ||
|
|
5bdd299dad | ||
|
|
d4336ed447 | ||
|
|
fa1b2b329f | ||
|
|
13cb684abd | ||
|
|
f6837f7819 | ||
|
|
bf2e017ca0 | ||
|
|
1defb30632 | ||
|
|
b3aa52cd91 | ||
|
|
06e0ce5106 | ||
|
|
46c31ac653 | ||
|
|
778f1a3a11 | ||
|
|
fb10e43055 | ||
|
|
2deb80fdbf | ||
|
|
1d731c0006 | ||
|
|
3dba61f9d9 | ||
|
|
2ba9616f49 | ||
|
|
27aa23179f | ||
|
|
5a23174e8b | ||
|
|
e80a31905e | ||
|
|
c8d63bf93a | ||
|
|
02b394f31b | ||
|
|
a5dcb64a10 | ||
|
|
db4bc8651d | ||
|
|
8e177534f4 | ||
|
|
49a94318ac | ||
|
|
9fc2dee5d7 | ||
|
|
d89682027e | ||
|
|
bc7cc6810d | ||
|
|
bdc94d853d | ||
|
|
190f2df15e | ||
|
|
7a4831e200 | ||
|
|
db2133a4b0 | ||
|
|
26e519bec0 | ||
|
|
bcc2022826 | ||
|
|
faefefe186 | ||
|
|
519f66f46d | ||
|
|
bc6d85acca | ||
|
|
d878a1fcaa | ||
|
|
b4eb2919b1 | ||
|
|
64b5518309 | ||
|
|
e2ff3dd7c5 | ||
|
|
7430f4deb8 | ||
|
|
b89860bc93 | ||
|
|
87a1002aa5 | ||
|
|
ca44dab358 | ||
|
|
53a78e6744 | ||
|
|
e050da41e5 | ||
|
|
0a6f892ba8 | ||
|
|
f7b28d876d | ||
|
|
b0496e309e | ||
|
|
31d75be68e | ||
|
|
1053d4125f | ||
|
|
fdc6c15d88 | ||
|
|
ee5d7fef31 | ||
|
|
640daf2431 | ||
|
|
703656d3c4 | ||
|
|
3c1337b213 | ||
|
|
57f59b8827 | ||
|
|
3bf91cedc9 | ||
|
|
93f4f1dc5a | ||
|
|
35c5cf60d4 | ||
|
|
0d5e34cfdd | ||
|
|
29e6bf8582 | ||
|
|
79ccd8580e | ||
|
|
968480669e | ||
|
|
1334942fc0 | ||
|
|
08dc74cb67 | ||
|
|
3645075473 | ||
|
|
a26b465ff6 | ||
|
|
c7b15896ac | ||
|
|
f7a27c7c63 | ||
|
|
c12633b2b1 | ||
|
|
83ae92b2b6 | ||
|
|
465c63ed1c | ||
|
|
812cb97713 | ||
|
|
f099bb5420 | ||
|
|
0b1872cc02 | ||
|
|
b3cf0243dd | ||
|
|
b32e6cac0c | ||
|
|
32ea075e90 | ||
|
|
5ce77e0adc | ||
|
|
647a053ccc | ||
|
|
d726be7d66 | ||
|
|
d0bd7d4cd7 | ||
|
|
eadb7a8f3e | ||
|
|
cfb752509a | ||
|
|
8e89c5ba73 | ||
|
|
61e1ffc6c4 | ||
|
|
ad5fc0bb52 | ||
|
|
0681c69d67 | ||
|
|
3f4101a87d | ||
|
|
87aea20056 | ||
|
|
3d944770f1 | ||
|
|
1d93368eb0 | ||
|
|
fcc4805220 | ||
|
|
d93165bfac | ||
|
|
640f6fca02 | ||
|
|
82c2300496 | ||
|
|
d2560784a2 | ||
|
|
16fae82c17 | ||
|
|
f9be0fad2d | ||
|
|
50db041021 | ||
|
|
a6945d1dc9 | ||
|
|
9c2f275534 | ||
|
|
bd317d8fc0 | ||
|
|
eda7f00453 | ||
|
|
b6cef907f9 | ||
|
|
0a792a5406 | ||
|
|
0468531655 | ||
|
|
dac3d51f88 | ||
|
|
273ba8073c | ||
|
|
96e22b14aa | ||
|
|
1b1bd7536d | ||
|
|
60e5a682bd | ||
|
|
8af3ad3549 | ||
|
|
7dd20b0a9b | ||
|
|
85f4ca9744 | ||
|
|
c7e90fc917 | ||
|
|
639d7d3cb6 | ||
|
|
504220f630 | ||
|
|
94196b1fe3 | ||
|
|
9cd285223b | ||
|
|
25ae2458f0 | ||
|
|
a66f9b01eb | ||
|
|
695ccdd177 | ||
|
|
d17ed6e5c3 | ||
|
|
5f58a1a2c4 | ||
|
|
7be8ff2e94 | ||
|
|
27b9ef910c | ||
|
|
10c78470b3 | ||
|
|
566ca54fcf | ||
|
|
db4e2f9d43 | ||
|
|
0e71a6701d | ||
|
|
b4384c4329 | ||
|
|
3c71a2be96 | ||
|
|
907451df48 | ||
|
|
94df1469c0 | ||
|
|
e53f31487e | ||
|
|
f5dd5e8191 | ||
|
|
2c509d5aa1 | ||
|
|
5ef40510ce | ||
|
|
140c0ed7e0 | ||
|
|
e2dfd44874 | ||
|
|
f10af2741e | ||
|
|
720686fb88 | ||
|
|
f0e06e4197 | ||
|
|
22cc5755fa | ||
|
|
4a70635ae4 | ||
|
|
6380b8752b | ||
|
|
46ce2f7e8d | ||
|
|
13577c302d | ||
|
|
4e78eb6c5e | ||
|
|
628d940a3e | ||
|
|
949930f2cf | ||
|
|
aa583e6748 | ||
|
|
be9e07cfea | ||
|
|
c847eff9fc | ||
|
|
746d3af943 | ||
|
|
2693e573f6 | ||
|
|
9170857d8a | ||
|
|
240c749806 | ||
|
|
88a007de7c | ||
|
|
14c882ff08 | ||
|
|
67fac4b2f2 | ||
|
|
0baf472473 | ||
|
|
faf8a6e234 | ||
|
|
f6b620383c | ||
|
|
d40710de3c | ||
|
|
1750dc13eb | ||
|
|
19bce0bfa1 | ||
|
|
05333b4345 | ||
|
|
c62c06a072 | ||
|
|
1b8f64b4fb | ||
|
|
863a4fd31f | ||
|
|
1b7f8bae6c | ||
|
|
239e518676 | ||
|
|
60465774d4 | ||
|
|
9c3bc5200a | ||
|
|
dd4446fd8c | ||
|
|
ea4e36d759 | ||
|
|
4a5f9579b4 | ||
|
|
69e2acd156 | ||
|
|
cdd5d88e2d | ||
|
|
d8effe2dcb | ||
|
|
bea245ced9 | ||
|
|
90f29da343 | ||
|
|
1d5441b84a | ||
|
|
8c9a660afd | ||
|
|
0af7969fcc | ||
|
|
dc296336e0 | ||
|
|
82a8c8b374 | ||
|
|
2432f7817c | ||
|
|
14b1230f72 | ||
|
|
e0c0c59e73 | ||
|
|
18c57904bb | ||
|
|
c0982905dd | ||
|
|
227ddbb776 | ||
|
|
cef477dfee | ||
|
|
ee82732880 | ||
|
|
9dbd2d2a0c | ||
|
|
0abddfbfb3 | ||
|
|
8749a59957 | ||
|
|
b7d94b56f0 | ||
|
|
d785f11aff | ||
|
|
a06e691420 | ||
|
|
98b3d0dff7 | ||
|
|
08d851ec81 | ||
|
|
a339dd898b | ||
|
|
b06a0e16b0 | ||
|
|
bb06d3e4f8 | ||
|
|
504e8ff9b6 | ||
|
|
67c76d8b04 | ||
|
|
662a192b81 | ||
|
|
b33231b3b4 | ||
|
|
f592784c11 | ||
|
|
ab32093caf | ||
|
|
f78cac645c | ||
|
|
cd88e44464 | ||
|
|
94c484dbe4 | ||
|
|
7a2553940c | ||
|
|
d4c10de30a | ||
|
|
7ebece0491 | ||
|
|
f59ce8809d | ||
|
|
b53929cb22 | ||
|
|
070d6b70d6 | ||
|
|
2deddf6fd4 | ||
|
|
a1ae5e9364 | ||
|
|
ed99243890 | ||
|
|
302670ed99 | ||
|
|
ff801d2169 | ||
|
|
dca72fac8c | ||
|
|
57a32e252d | ||
|
|
982b2c024b | ||
|
|
ebff15fbed | ||
|
|
9b1cfb6e03 | ||
|
|
e63441c254 | ||
|
|
192c8c3c0a | ||
|
|
2ef2aff18f | ||
|
|
a4be3d534f | ||
|
|
7621b91f15 | ||
|
|
38f5fcf360 | ||
|
|
f2a10e249f | ||
|
|
e36f602e6a | ||
|
|
d838108025 | ||
|
|
02a42dd4b2 | ||
|
|
6ed19f7a84 | ||
|
|
7f5da492ee | ||
|
|
eedd687e47 | ||
|
|
73407ceb8d | ||
|
|
fe62708aa3 | ||
|
|
9ef72679b3 | ||
|
|
05783db2b1 | ||
|
|
65f80c55b0 | ||
|
|
947f5c0f6b | ||
|
|
4967f31791 | ||
|
|
ae4cbd46dc | ||
|
|
56c9c24937 | ||
|
|
688e3faacd | ||
|
|
91ddc42667 | ||
|
|
fca202fd76 | ||
|
|
2e01757519 | ||
|
|
fb543f99ba | ||
|
|
de0f8fafda | ||
|
|
fddda27ca8 | ||
|
|
325d53cad6 | ||
|
|
b59ddea3b6 | ||
|
|
3cc48d33e6 | ||
|
|
165349172b | ||
|
|
fa36c21672 | ||
|
|
efec6d6857 | ||
|
|
ecddbf2226 | ||
|
|
031558c483 | ||
|
|
02d0031079 | ||
|
|
101676cef3 | ||
|
|
9193bda39f | ||
|
|
c5d4e80e7c | ||
|
|
599d67141f | ||
|
|
529e5d3b83 | ||
|
|
68fe9946d2 | ||
|
|
f4df55b9e1 | ||
|
|
3c4d0fcb5f | ||
|
|
1c154c6f48 | ||
|
|
c4b41daf6a | ||
|
|
2577fd81eb | ||
|
|
75987baef8 | ||
|
|
e3ccf61fd2 | ||
|
|
f430718e5f | ||
|
|
9815134536 | ||
|
|
78e2105a62 | ||
|
|
c78134f05c | ||
|
|
0693f5de74 | ||
|
|
f78af9e77c | ||
|
|
6638e24901 | ||
|
|
4a4e9a611f | ||
|
|
51495a93ff | ||
|
|
0059c3ba3b | ||
|
|
035c0602b5 | ||
|
|
e4b1cf4a15 | ||
|
|
0941124ab6 | ||
|
|
89a7c12fa0 | ||
|
|
2da76dfcff | ||
|
|
62ceca1d34 | ||
|
|
dd2db73e9d | ||
|
|
2824e6582c | ||
|
|
d2d9e53655 | ||
|
|
54b84f8bf6 | ||
|
|
e45344a1c3 | ||
|
|
2b978997c7 | ||
|
|
834ccea786 | ||
|
|
13a940b8c2 | ||
|
|
a4f63f0d70 | ||
|
|
3995ef55e1 | ||
|
|
92fa2975c2 | ||
|
|
ea2336fe1e | ||
|
|
d9f2e134b1 | ||
|
|
922d1357fa | ||
|
|
c1cc74c2f3 | ||
|
|
dce6910371 | ||
|
|
10dd9869d4 | ||
|
|
600e2a52ad | ||
|
|
dcb1c95215 | ||
|
|
1c1062b235 | ||
|
|
d1d8391e1d | ||
|
|
63df76bf01 | ||
|
|
d277cd6518 | ||
|
|
8a06e6504d | ||
|
|
f6beb38502 | ||
|
|
da11868d48 | ||
|
|
7e9b8e2341 | ||
|
|
8d2dcad9ca | ||
|
|
6d768f3a97 | ||
|
|
3a444a29b9 | ||
|
|
232d4834be | ||
|
|
e1ed0e5dff | ||
|
|
a9306bb8ba | ||
|
|
9facfe2ff8 | ||
|
|
c8742de888 | ||
|
|
14e397ab26 | ||
|
|
cbc8c4a501 | ||
|
|
ed5ff02b91 | ||
|
|
4608e23b60 | ||
|
|
0cee80e6d7 | ||
|
|
2234fdf172 | ||
|
|
a243e66746 | ||
| cea4a01a4a |
16
.codegraph/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# CodeGraph data files
|
||||
# These are local to each machine and should not be committed
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
|
||||
# Cache
|
||||
cache/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Hook markers
|
||||
.dirty
|
||||
6
.codegraph/daemon.pid
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"pid": 74205,
|
||||
"version": "0.9.9",
|
||||
"socketPath": "/Users/karuo/Documents/开发/2、私域银行/cunkebao_v3/.codegraph/daemon.sock",
|
||||
"startedAt": 1780473089935
|
||||
}
|
||||
21
.cursor/rules/codegraph.mdc
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: CodeGraph F21a — 经卡若AI 全局 MCP 解析代码,不依赖本仓科室配置
|
||||
alwaysApply: true
|
||||
---
|
||||
<!-- CODEGRAPH_START · 卡若AI F21a 引用 -->
|
||||
## CodeGraph(全局 · 卡若AI F21a)
|
||||
|
||||
结构读/写代码 **只走卡若AI F21a**,MCP 已在 `~/.cursor/mcp.json` 由 `codegraph_mcp_serve.sh` 启动。
|
||||
|
||||
**真源**:`/Users/karuo/Documents/个人/卡若AI/04_卡火(火)/火种_知识模型/CodeGraph_MCP语义代码图谱/SKILL.md`
|
||||
|
||||
| 场景 | 工具 |
|
||||
|:---|:---|
|
||||
| 任务上下文 | `codegraph_context` → `codegraph_explore` |
|
||||
| 找符号 | `codegraph_search` |
|
||||
| 调用链 | `codegraph_callers` / `codegraph_callees` |
|
||||
| 改 API 影响 | `codegraph_impact` |
|
||||
|
||||
无 `.codegraph/`:`bash …/CodeGraph_MCP语义代码图谱/脚本/codegraph_init_repo.sh "$(pwd)"`
|
||||
**勿**在本仓新增 `.cursor/mcp.json` 的 codegraph 块。
|
||||
<!-- CODEGRAPH_END -->
|
||||
40
.cursor/rules/cunkebao-deploy-autofix.mdc
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
description: 存客宝 v3 部署——Agent 自主执行、自主修错、验收至首页可访问(覆盖「部署失败再问用户」)
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 存客宝 v3 · 部署自主处理
|
||||
|
||||
> **生效**:2026-05-26。用户说「部署」时,**不得**只跑脚本报成功就结束;必须验收到**首页有内容**。
|
||||
|
||||
## 强制流程
|
||||
|
||||
1. **执行**:`bash 开发文档/8、部署/scripts/deploy_local_docker.sh --build`(改前端/首次/用户抱怨空白页时必须 `--build`)。
|
||||
2. **脚本失败时 Agent 自行排查修复**,不要停下来问用户,常见顺序:
|
||||
- Docker 未响应 → `open -a Docker`,等就绪后重试
|
||||
- 全量 `--build` 中 `server` 镜像失败(composer/php 拉取)→ 改为 `docker compose build cunkebao-web touchkebao-web superadmin-web && docker compose up -d`
|
||||
- `vendor` 缺失 → 重建 `server` 或 `docker compose up -d --build server`
|
||||
- MySQL 未 healthy → `COMPOSE_PROFILES=embedded-mysql docker compose up -d mysql`,确认 3308
|
||||
- 3100 HTTP 200 但首页空白 → 查 `pageerror`(如 `homeIcons` 缺 import)、`PermissionRoute` hydration、index.html 缓存;修代码后 **必须 rebuild cunkebao-web**
|
||||
3. **验收(全绿才算部署成功)**:
|
||||
- HTTP:3100 / 3101 = 200
|
||||
- API:登录 code 200(经 `/api` 与 8082 任一)
|
||||
- **首页 smoke**:`node 开发文档/8、部署/scripts/verify_home_smoke.mjs` → 页面含「设备数量」「场景获客」,`pageerror` 为空
|
||||
- `index.html` 响应头含 `Cache-Control: no-cache`
|
||||
4. **失败则循环**:修 → rebuild 前端 → 再 deploy → 再 smoke,直到通过或明确环境级阻塞(仅此时向用户说明)。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 仅 `curl` 首页 200 就宣称部署成功
|
||||
- 部署失败后只给用户命令让用户自己跑
|
||||
- 未 rebuild 就声称「已更新前端」
|
||||
|
||||
## 脚本与文档
|
||||
|
||||
- 本地一键:`开发文档/8、部署/scripts/deploy_local_docker.sh`
|
||||
- 首页 smoke:`开发文档/8、部署/scripts/verify_home_smoke.mjs`
|
||||
- 腾讯云:`开发文档/8、部署/scripts/deploy_tencent_baota.sh`(用户明确要求线上时)
|
||||
|
||||
## 与「仅本地不 push」关系
|
||||
|
||||
部署修错可改本地代码与 `开发文档/`;**不自动 git push**(见 `cunkebao-local-only-no-github.mdc`),除非用户本轮明确说提交/推送。
|
||||
28
.cursor/rules/cunkebao-four-end-http-only.mdc
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
description: 四端只 HTTP 对接、不串仓硬性铁律。改触客宝/AI数智员工/存客宝/超管任意一端时强制遵守。
|
||||
globs:
|
||||
- "**/*"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 四端对接铁律 · 只 HTTP · 不串仓
|
||||
|
||||
> 真源:`开发文档/2、架构/四端对接铁律_只HTTP不串仓.md`
|
||||
|
||||
## 强制
|
||||
|
||||
1. **各端各自仓库、各自职责** — 存客宝(3100) · 触客宝(3101) · AI数智员工(3104) · SuperAdmin(3103) **不互相改业务代码**。
|
||||
2. **协作只走 HTTP API** — JWT `/v1/*` · Open `X-API-KEY` · 禁止跨项目 import 源码、禁止直连 MySQL/Mongo。
|
||||
3. **数据范围** — 请求必须带当前项目 `companyId`;禁止跨项目读写。
|
||||
4. **新能力** — 先更 `开发文档/5、接口/` 索引,再改 `route.php`,最后写 UI。
|
||||
5. **工作手机/神射手** — 只经存客宝 BFF 或 documented HTTP;AI数智员工 **禁止** `/v1/kefu/*`。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 在触客宝/AI数智员工仓修改 `Server/application/cunkebao`(除非用户本轮明确「改存客宝 Server」)
|
||||
- copy 兄弟项目 React 业务组件进本仓(设计 token 可对齐)
|
||||
- 触客宝直连 `traffic_pool` 表
|
||||
|
||||
## 接口索引
|
||||
|
||||
`开发文档/5、接口/SuperAdmin_四端对接接口索引.md` · `02-存客宝对接/触客宝PC接口对接索引.md`
|
||||
89
.cursor/rules/cunkebao-karuo-dialogue.mdc
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
description: 存客宝 v3 · 卡若AI 对话强制规则(复盘五块 + 本轮文档可打开路径)
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 存客宝 v3 · 卡若AI 对话强制规则
|
||||
|
||||
> **生效**:2026-05-31 · 优先级:与本仓 `cunkebao-v3-devdoc-first.mdc`、`cunkebao-local-only-no-github.mdc` 并列。
|
||||
|
||||
## 一、身份与启动(每轮)
|
||||
|
||||
1. **读卡若AI**:`/Users/karuo/Documents/个人/卡若AI/BOOTSTRAP.md` → `SKILL_REGISTRY.md`(热技能命中则读对应 `SKILL.md`)。
|
||||
2. **回廊洗字**:`运营中枢/参考资料/卡若闽南口音_ASR纠错库.json`(卡路/卡洛/卡罗拉→**卡若**)。
|
||||
3. **执行前公示**(默认):正文最前 **理解 ≤200 字** + **本次要做** 有序列表;用户说「直接做」可跳过列表,**复盘仍必须**。
|
||||
4. **存客宝开发**:叠加 `开发文档/开发铁律_先读文档再编码.md` Phase 0。
|
||||
|
||||
## 二、对话结束 · 强制复盘(卡若 v5/v6)
|
||||
|
||||
**每条回复的最后一块**必须是完整 **[卡若复盘](YYYY-MM-DD HH:mm)**,真源:
|
||||
|
||||
`卡若AI/运营中枢/参考资料/卡若复盘格式_固定规则.md` · 速查 `04_卡火(火)/火炬_全栈消息/卡若复盘格式/SKILL.md`(F01d)
|
||||
|
||||
| 顺序 | 块 |
|
||||
|:---:|:---|
|
||||
| 1 | **🎯 目标·结果·达成率**(单行 ≤50 字,含 **0~100%**) |
|
||||
| 2 | **📌 过程**(1、2、3 分点,可验收数据) |
|
||||
| 3 | **💡 反思**(1~3 点) |
|
||||
| 4 | **📝 总结** |
|
||||
| 5 | **▶ 下一步执行** |
|
||||
|
||||
复盘块内**禁止表格**;禁止省略复盘或写「见上文」。
|
||||
|
||||
## 三、复盘之后 · 本轮文档(Cursor 可点 · 强制)
|
||||
|
||||
复盘五块写完后、回复结束前,追加 **📎 本轮文档**(**只列本轮对话新建/修改的文件**,通常 1~3 个;**禁止**贴历史轮次、索引 README、无关专文)。
|
||||
|
||||
### 3.1 Cursor 聊天内(默认 · 必用)
|
||||
|
||||
**禁止**在 Cursor 复盘末尾使用下列形式(点击无反应):
|
||||
|
||||
- `obsidian://open?...`
|
||||
- `file://` / `vscode://`
|
||||
- 一长串与本轮无关的 MD 链接列表
|
||||
|
||||
**必须**使用 **代码引用块**(点块内路径可在 Cursor 打开文件)+ **一行绝对路径**:
|
||||
|
||||
````markdown
|
||||
**📎 本轮文档(Cursor:点下面代码块即可打开)**
|
||||
|
||||
- **卡若对话强制规则**
|
||||
|
||||
```1:3:.cursor/rules/cunkebao-karuo-dialogue.mdc
|
||||
---
|
||||
description: 存客宝 v3 · 卡若AI
|
||||
```
|
||||
|
||||
`/Users/karuo/Documents/开发/2、私域银行/cunkebao_v3/.cursor/rules/cunkebao-karuo-dialogue.mdc`
|
||||
````
|
||||
|
||||
- 路径用**仓库相对路径**写在 code fence 第三段:`行号:行号:相对路径`
|
||||
- 块下再写一行 **反引号包裹的绝对路径** `/Users/karuo/...`(备用,部分环境可点)
|
||||
- 真源对齐卡若AI 需求整理 §8.1:**禁止** `file://` 超链接
|
||||
|
||||
### 3.2 生成方式
|
||||
|
||||
```bash
|
||||
python3 "开发文档/scripts/build_obsidian_doc_link.py" \
|
||||
".cursor/rules/cunkebao-karuo-dialogue.mdc" \
|
||||
--title "卡若对话强制规则"
|
||||
```
|
||||
|
||||
**只传入本轮动过的路径**;将 stdout **原样**贴在复盘正下方。
|
||||
|
||||
### 3.3 Obsidian / 星图阁(非 Cursor 聊天)
|
||||
|
||||
- 用户明确说「Obsidian 打开」→ 脚本加 `--obsidian --open`(终端外开,**不要**把 `obsidian://` 贴进 Cursor 聊天)
|
||||
- 工作日志库「个人」→ `卡若AI/.../build_obsidian_work_log_link.py`
|
||||
|
||||
## 四、与其它规则
|
||||
|
||||
| 规则 | 关系 |
|
||||
|:---|:---|
|
||||
| `cunkebao-local-only-no-github.mdc` | 不自动 push |
|
||||
| `cunkebao-v3-devdoc-first.mdc` | 改接口/需求先读文档 |
|
||||
| 卡若AI `karuo-ai.mdc` | 复盘五块一致;路径输出以本条 §3 为准 |
|
||||
|
||||
## 五、进度(可选)
|
||||
|
||||
用户要求时,在 **📌** 或 **📝** 中带 **本任务完成度 X%**(分子/分母说清)。
|
||||
26
.cursor/rules/cunkebao-local-only-no-github.mdc
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: 存客宝 v3 仅本地开发——禁止 Agent 自动 commit/push GitHub(覆盖全局 User Rules)
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 存客宝 v3 · 仅本地、不自动同步 GitHub
|
||||
|
||||
> **生效**:2026-05-25 起,卡若明确要求暂停「对话结束自动推 GitHub」。
|
||||
|
||||
## 强制
|
||||
|
||||
1. **所有代码与文档改动仅保存在本机工作区**,不自动 `git commit`、不自动 `git push`、不自动同步 Gitea/GitHub/远程服务器。
|
||||
2. **仅当卡若在本轮对话中明确说**「提交」「commit」「推 GitHub」「push」「同步远程」等时,才可执行对应 Git 远程操作。
|
||||
3. 对话结束时:**列出本次修改的文件路径即可**,不要执行 push;**禁止**提示「要不要同步 GitHub」「已同步 GitHub」等。
|
||||
4. 沙盒、未授权远程、CI 触发型部署:**一律不主动触发**。
|
||||
5. 开发文档可写本地 `开发文档/` 目录,**不要**为进度文档单独 commit/push。
|
||||
|
||||
## 与全局 User Rules 的优先级
|
||||
|
||||
若全局 User Rules 含「边做边同步 GitHub / 每次对话完成推到 GitHub」,**以本规则为准**——存客宝 v3 会话内忽略该条,直至卡若另行恢复。
|
||||
|
||||
## 仍允许
|
||||
|
||||
- 本地 `git status` / `git diff` 用于自查
|
||||
- 卡若显式授权后的 commit / push
|
||||
- 开发文档文件夹内记录进度(本地 MD,不推远程)
|
||||
47
.cursor/rules/cunkebao-mobile-first.mdc
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
description: 存客宝 v3 · 手机端优先适配铁律(开发 /mobile 必守)
|
||||
globs:
|
||||
- Cunkebao/src/pages/mobile/**
|
||||
- Cunkebao/src/components/Layout/**
|
||||
- Cunkebao/index.html
|
||||
---
|
||||
|
||||
# 存客宝 · 手机端优先(Mobile First)
|
||||
|
||||
> **产品定位**:存客宝 **以手机端界面为主**;PC 为同套 `Layout frame=mobile` 全屏,不是桌面专属布局。
|
||||
> **壳层参照**:`Cunkebao/src/components/Layout/` · `Cunkebao/index.html` · 触客宝 `.cursor/skills/触客宝手机端前端壳/SKILL.md`(键盘/100dvh 同源)
|
||||
|
||||
## 一、结构(强制)
|
||||
|
||||
| 层级 | 规则 |
|
||||
|:---|:---|
|
||||
| 页面壳 | 用 `Layout`:`header` 顶 · `main` **唯一纵向滚动** · `footer` 底 |
|
||||
| 多步表单 | **`embedded`**:底栏内嵌在 flex 底,**禁止**只靠 Portal 让用户上拉找「下一步」 |
|
||||
| 内容区 | **禁止** `min-height: 100vh` / `padding-bottom: 100px` 撑高;滚动只发生在 `Layout.main` |
|
||||
|
||||
## 二、选项与排版(禁止挤压)
|
||||
|
||||
| 场景 | 做法 |
|
||||
|:---|:---|
|
||||
| ≤3 个短选项 | `radioGroupSegment` 等分横排 |
|
||||
| ≥4 个或文案较长 | `radioGroupWrap` **两列换行**,禁止 `radioGroupHorizontal` 塞 5 项导致**竖排单字** |
|
||||
| 长说明 | 副文案单独一行(`radioDesc`),不挤在 Radio 同一窄列 |
|
||||
| 触控 | 可点区域高度 **≥ 44px**;字号 **≥ 14px**(输入 **≥ 16px** 防 iOS 放大) |
|
||||
|
||||
## 三、底栏按钮(下一步 / 提交)
|
||||
|
||||
- 固定在视口**最下**(`embedded` 的 `inlineFooter` 或 `TabBarPortal`)
|
||||
- 主按钮 **block / flex:1**,与「上一步」并排时 `footer-btn-group`
|
||||
- 主体 `main` 预留 `padding-bottom` ≥ 底栏高 + safe-area(`Layout` 已处理 `embedded` 时勿重复加大留白)
|
||||
|
||||
## 四、新页 / 改页 Checklist
|
||||
|
||||
- [ ] 已读本条 + Layout 源码
|
||||
- [ ] 无 `100vh` 撑高 body
|
||||
- [ ] 多选项未竖排挤字
|
||||
- [ ] 底栏无需上拉即可点「下一步」
|
||||
- [ ] 真机或 375px 模拟器目视
|
||||
|
||||
## 五、需求落盘
|
||||
|
||||
手机适配类需求写入 `开发文档/1、需求/修改/存客宝_进行中_{YYYYMMDD}.md`(REQ-CKB-11x),验收:**375px 宽无挤字、底栏固定可见**。
|
||||
43
.cursor/rules/cunkebao-requirement-images-must-read.mdc
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
description: 存客宝 v3 · 读需求表必先看图(强制)
|
||||
globs:
|
||||
- "开发文档/1、需求/**/*.md"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 存客宝 v3 · 读需求表必先看图
|
||||
|
||||
> **生效**:2026-05-26 起。卡若明确要求:每次读 `开发文档/1、需求/修改/存-*.md` 或类似需求 MD 时,**必须**确认能看到 MD 引用的所有图片,再开始开发。
|
||||
|
||||
## 强制流程(开发前 Phase 0)
|
||||
|
||||
1. **先读需求 MD**(`Read` 工具读完整文件)。
|
||||
2. **列出所有 `` 引用**(用 `Grep` 模式 `../../10、项目管理/images/[^)]+\.png`)。
|
||||
3. **`ls images/`** 确认本地文件均存在;若缺图必须在对话里**明确报告缺失清单**,不要假装能看到。
|
||||
4. **逐张 `Read` 关键截图**(至少所有 P0/P1 模块对应的截图与「参考样式」「标注」类图片)。Read 工具支持 PNG/JPG/GIF/WebP,会作为图像加载到上下文。
|
||||
5. **复述**截图中看到的元素(按钮文字、颜色、布局、标注红框),与 MD 文字描述对照,若有冲突以**截图为准**并在回复中标注。
|
||||
6. **图片缺失或读取失败时**:先用 `GenerateImage` 不可、改用 `WebFetch` 也不可。立刻向用户报告,**不**继续编码。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 仅凭 MD 文字描述就开始编码,跳过截图。
|
||||
- 用 `cat`/`head` 读 PNG(无效)。
|
||||
- 复述 MD 时把截图当作文字一并描述但实际未 Read(伪装看图)。
|
||||
|
||||
## 适用范围
|
||||
|
||||
- `开发文档/1、需求/修改/*.md`
|
||||
- `开发文档/1、需求/草稿/*.md`
|
||||
- 任何外部传入的 PDF 需求(先 `Read` PDF,再 `Read` 拆出的 PNG 页)
|
||||
|
||||
## 复盘要求
|
||||
|
||||
每次按本规则开发后,在复盘里说明:
|
||||
- 本轮 Read 了哪些 PNG(路径)
|
||||
- 截图中验证/纠正了哪些 MD 文字描述
|
||||
- 是否有图片缺失或读取失败
|
||||
|
||||
## 与其他规则关系
|
||||
|
||||
- 与 `cunkebao-v3-devdoc-first.mdc` 协同:先读 1、需求图 → 再读 5、接口/2、架构 → 再编码。
|
||||
- 与 `cunkebao-local-only-no-github.mdc` 不冲突:仍只本地,不自动推。
|
||||
54
.cursor/rules/cunkebao-v3-devdoc-first.mdc
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
description: 存客宝 v3 开发铁律——先读开发文档再编码,迭代结束回写文档(含 5、接口)
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 存客宝 v3 · 开发文档先行
|
||||
|
||||
本项目代码变更**必须**遵循 `开发文档/开发铁律_先读文档再编码.md`。
|
||||
|
||||
## 动手前(Phase 0)
|
||||
|
||||
1. 读 `开发文档/开发铁律_先读文档再编码.md` 的 Phase 0 表,按任务类型读完对应 MD
|
||||
2. **目录/需求维护** 必读 `开发文档/开发文档目录结构铁律.md`(十大目录 · modify 收口 · 已完成进 10、项目管理)
|
||||
2. **接口/BFF/对接** 必读:
|
||||
- `开发文档/5、接口/README.md`
|
||||
- `开发文档/5、接口/07-接口分类总览(Obsidian配色).md`
|
||||
- `开发文档/5、接口/存客宝API全量索引.md`
|
||||
- 涉及 workphone 时再读 `5、接口/02-存客宝与工作手机对接/` 下 05/06/03
|
||||
3. **架构/新模块** 必读 `开发文档/2、架构/README.md` → `系统架构.md`
|
||||
4. 对话中先列出「本次已读文档路径」(≤5 条),再改代码
|
||||
|
||||
## 开发中(Phase 1)
|
||||
|
||||
- 路由真源:`Server/application/*/config/route.php`
|
||||
- 不改原有业务 URL;设备写操作走 `/v1/workphone/*`
|
||||
- 对照 `5、接口/02-存客宝与工作手机对接/06-业务接口与工作手机一对一映射总表.md`
|
||||
- **边开发边标注**:只改 `开发文档/1、需求/修改/{端}_进行中_{YYYYMMDD}.md`(⬜→🟡→✅);**禁止**同日另开 `{端}_{主题}_{date}.md`;关单节迁入 `开发文档/10、项目管理/已完成/`
|
||||
- **需求落盘 Skill**:`.cursor/skills/需求协调与归档/SKILL.md` §九(一项目一文件)
|
||||
|
||||
### ⛳ 强制打勾铁律(卡若 2026-05-31 · 最高优先)
|
||||
|
||||
> **每完成一项,立即在开发文档对应行打 ✅** —— 不攒批、不等收尾、不口头代替。
|
||||
|
||||
1. **触发即更新**:任一需求/子项验收通过的**当下**,立刻把进行中 MD 对应行 `⬜/🟡 → ✅`,并在验收列补**可验收证据**(HTTP 码 / 条数 / 脚本 exit / 截图路径)。
|
||||
2. **粒度到子项**:表格每行、清单每条 `- [ ] → - [x]`;一个功能含多子项的,完成几项打几个勾。
|
||||
3. **顺序固定**:`改代码 → 验证 → 立即打勾 ✅ + 证据 → 再继续下一项`;严禁"全部做完再统一打勾"。
|
||||
4. **复盘对齐**:复盘 📌 引用的完成项,必须与 MD 中已 ✅ 的行一致;不一致以 MD 为准并补勾。
|
||||
5. **跨文档同步**:接口变更同步打勾 `开发进度_接口文档.md`;其它相关文档同样"完成一项打一勾"。
|
||||
6. **未完成保留**:⬜/🟡/🔒 原样留下,不删行;整节全 ✅ 才迁 `10、项目管理/已完成/` 并留索引行。
|
||||
|
||||
## 收尾(Phase 2)
|
||||
|
||||
- 路由变更 → 同步 `5、接口/存客宝API全量索引.md`
|
||||
- BFF/SDK 变更 → 同步 02-对接目录 + `开发文档/开发进度_接口文档.md`
|
||||
- 新增 MD → 运行 `开发文档/scripts/generate_doc_header_banners.py`
|
||||
- 复盘须写:读了哪些文档、更新了哪些文档
|
||||
|
||||
## Skill
|
||||
|
||||
| Skill | 路径 |
|
||||
|:---|:---|
|
||||
| 卡若AI 开发文档先行 | `01_卡资(金)/金盾_数据安全/存客宝/开发文档先行/SKILL.md` |
|
||||
| 需求整理 W05b | `.cursor/skills/开发需求文档整理/SKILL.md` |
|
||||
| 需求协调 W05c | `.cursor/skills/需求协调与归档/SKILL.md` |
|
||||
122
.cursor/skills/cunkebao-architecture-five-diagrams/SKILL.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: cunkebao-architecture-five-diagrams
|
||||
description: >-
|
||||
存客宝 v3 项目架构五图:对话内 GenerateImage 生成高级架构/流程图,写入
|
||||
开发文档/2、架构/images/architecture-overview/,并维护 08-项目架构五图读懂指南.md。
|
||||
触发:架构五图、五图读懂、项目架构图、生成架构图、一图读懂存客宝、
|
||||
PM架构图、不用Python生成图片、架构 infographic。禁止用 Pillow/Python 画架构总览图。
|
||||
---
|
||||
|
||||
# 存客宝 · 架构五图(对话内 AI 出图)
|
||||
|
||||
> **铁律**:架构总览 / 业务流程 / 板块关系图 → **只用 Cursor `GenerateImage`** 在对话里生成;**禁止**用 `generate_doc_header_banners.py` 或 Pillow 替代。
|
||||
> **文档说明小头图**(单篇 MD 顶栏)仍走 F09a「开发文档说明头图」Skill。
|
||||
|
||||
---
|
||||
|
||||
## 一、何时触发
|
||||
|
||||
- 用户要「几张图看懂整个项目」「给 PM 的架构图」「五图读懂」
|
||||
- 存客宝大版本架构变更后需更新总览图
|
||||
- 用户明确说「不要用 Python 生成图片」
|
||||
|
||||
---
|
||||
|
||||
## 二、五图标准套装(固定 5 张)
|
||||
|
||||
| 序号 | 文件名 | 标题(中文) | 内容要点 |
|
||||
|:----:|--------|-------------|----------|
|
||||
| 1 | `01-系统四层架构总览.png` | 存客宝 v3 · 系统四层架构总览 | 客户端层 → Nginx → Server/Moncter/WebSocket → MongoDB/MySQL/Redis + 工作手机 SDK |
|
||||
| 2 | `02-核心业务数据流.png` | 存客宝 · 核心业务数据流 | 工作手机 ↔ SDK ↔ ThinkPHP ↔ 三库;WebSocket ↔ React 前端 |
|
||||
| 3 | `03-六大业务板块关系.png` | 存客宝 · 六大业务板块 | 场景获客/工作台/设备/微信号/流量池/内容库 → 中心 BFF,route.php 真源 |
|
||||
| 4 | `04-场景获客与好友添加流程.png` | 场景获客 + 好友添加 · 完整流程 | 建计划→task_customer→Workerman→健康分→SDK→状态0~5→Webhook |
|
||||
| 5 | `05-定时任务与自动化调度.png` | 存客宝 · 定时任务与自动化调度 | task_scheduler 高/中/低频任务,Command→Queue→Job |
|
||||
|
||||
**输出目录**(项目内):
|
||||
|
||||
```
|
||||
开发文档/2、架构/images/architecture-overview/
|
||||
```
|
||||
|
||||
**配套文档**(必须同步更新或新建):
|
||||
|
||||
```
|
||||
开发文档/2、架构/08-项目架构五图读懂指南.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、GenerateImage 提示词规范
|
||||
|
||||
### 视觉风格(统一)
|
||||
|
||||
- 比例:**16:9 横版**
|
||||
- 风格:**dark glassmorphism**,深蓝/紫渐变底,毛玻璃卡片,细线箭头
|
||||
- 字体:**中文为主**,大号标题,PM 可读
|
||||
- 质量:Keynote / 企业 SaaS 发布会级别,**不要**代码截图、不要 ASCII
|
||||
|
||||
### 每张图调用模板
|
||||
|
||||
```
|
||||
Professional enterprise architecture infographic, 16:9 landscape,
|
||||
premium dark glassmorphism, subtle blue-purple gradient.
|
||||
Chinese title: "{标题}".
|
||||
{具体内容 bullet,英文描述布局即可}
|
||||
Clean sans-serif Chinese typography, Apple keynote quality, footer: CKB v3
|
||||
```
|
||||
|
||||
### 并行生成
|
||||
|
||||
5 张图 **同一轮对话并行** 调用 5 次 `GenerateImage`,不要串行等待。
|
||||
|
||||
### 落盘
|
||||
|
||||
GenerateImage 默认写到 Cursor assets;**必须 cp 到**:
|
||||
|
||||
```bash
|
||||
开发文档/2、架构/images/architecture-overview/
|
||||
```
|
||||
|
||||
(用 Shell 复制,不要用 Python 画图。)
|
||||
|
||||
---
|
||||
|
||||
## 四、文档维护 checklist
|
||||
|
||||
生成图片后按序执行:
|
||||
|
||||
1. [ ] 5 张 PNG 已在 `architecture-overview/`
|
||||
2. [ ] `08-项目架构五图读懂指南.md` 内 5 处 ``
|
||||
3. [ ] `2、架构/README.md` 阅读顺序增加「08-五图读懂」
|
||||
4. [ ] `5、接口/开发进度_接口文档.md` 或架构变更记录加一行
|
||||
5. [ ] **不**运行 `generate_doc_header_banners.py` 为这五张图生成 doc-header
|
||||
|
||||
---
|
||||
|
||||
## 五、内容真源(生成前必读)
|
||||
|
||||
| 主题 | 文档 / 代码 |
|
||||
|------|-------------|
|
||||
| 四层架构 | `开发文档/2、架构/系统架构.md` |
|
||||
| 数据流 | `开发文档/2、架构/核心业务链路.md` |
|
||||
| 六板块 | `开发文档/5、接口/03-业务板块接口/README.md` |
|
||||
| 获客加友 | `开发文档/2、架构/07-业务板块核心算法.md` |
|
||||
| 定时任务 | `开发文档/2、架构/自动化工作流.md` · `Server/config/task_scheduler.php` |
|
||||
|
||||
---
|
||||
|
||||
## 六、与 F09a 头图 Skill 的分工
|
||||
|
||||
| 类型 | 工具 | Skill |
|
||||
|------|------|-------|
|
||||
| 单篇 MD 顶栏小 banner | GenerateImage(首选) | F09a 开发文档说明头图 |
|
||||
| **项目级架构五图** | GenerateImage(强制) | **本 Skill** |
|
||||
| 离线无 AI 的小色条 | Pillow fallback | F09a 仅 fallback,不用于五图 |
|
||||
|
||||
---
|
||||
|
||||
## 七、变更记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| 1.0 | 2026-05-25 | 首版;替代 Python 批量画架构总览 |
|
||||
193
.cursor/skills/存客宝官网开发/SKILL.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
name: 存客宝官网开发
|
||||
description: 存客宝对外官网(/官网/)需求、SEO/GEO、产品矩阵截图、留资单接口、超管接口文档第9类。触发:存客宝官网、官网开发、官网SEO、产品矩阵、官网留资、对外接口、官网需求、GEO、私域官网。
|
||||
triggers: 存客宝官网,官网开发,官网SEO,产品矩阵,官网留资,对外接口,GEO,私域官网,触客宝官网
|
||||
owner: 火炬
|
||||
group: 火
|
||||
version: "1.0"
|
||||
updated: "2026-05-31"
|
||||
---
|
||||
|
||||
# 存客宝官网开发 Skill(F01e · 项目内)
|
||||
|
||||
> **代码**:`/Users/karuo/Documents/开发/2、私域银行/cunkebao_v3/官网/`(静态站 · 零构建)
|
||||
> **当日需求真源(唯一)**:`开发文档/1、需求/修改/官网_进行中_{YYYYMMDD}.md` — **新需求只追加此文件章节**,禁止再开 `官网_{主题}_*.md` 于 `修改/`
|
||||
> **鉴定/索引**:`开发文档/1、需求/官网/00~13` 为已整理专文,**非**当日追加口(见 [需求协调与归档](../需求协调与归档/SKILL.md) §九)
|
||||
> **对话规则**:`.cursor/rules/cunkebao-karuo-dialogue.mdc`(卡若复盘 + 本轮文档代码块)
|
||||
|
||||
---
|
||||
|
||||
## 一、何时启用
|
||||
|
||||
| 触发 | 动作 |
|
||||
|:---|:---|
|
||||
| 改官网页面/样式/留资 | 读 **`官网_进行中_{date}.md`**(REQ/E 序)· 专文仅作参考 → 改 `官网/` → **回勾进行中(⬜→🟡→✅)** → `03-研发迭代日志` |
|
||||
| 运营后台/访客/Webhook | 读 `_进行中` **§ REQ-WEB-60** → `scripts/analytics-store.mjs` + `admin/` → 子项打勾 |
|
||||
| **AI 客服对外(新)** | **先读 14 专文 §十一** · **勿抢先写代码** · Server `official-ai/chat` → 再官网挂件 |
|
||||
| 留资/对接 API | 只读 [07-官网统一留资接口](../../开发文档/1、需求/官网/03-接口与对接/07-官网统一留资接口_20260531.md),**禁止再造第二 URL** |
|
||||
| SEO/关键词 | 读 [09](../../开发文档/1、需求/官网/04-SEO与增长/09-官网SEO与GEO全站优化_20260531.md);要做后台读 [12](../../开发文档/1、需求/官网/04-SEO与增长/12-官网SEO运营后台与自动挂链_20260531.md) |
|
||||
| 产品矩阵配图 | 读 [08-产品矩阵界面与截图](../../开发文档/1、需求/官网/02-页面与视觉/08-产品矩阵界面与截图_20260531.md) |
|
||||
| 超管接口汇总 | 同步 `SuperAdmin/lib/api-docs-index.ts` + `5、接口/SuperAdmin_四端对接接口索引.md` |
|
||||
|
||||
---
|
||||
|
||||
## 二、文档地图(按编号)
|
||||
|
||||
| 编号 | 文件 | 用途 |
|
||||
|:---|:---|:---|
|
||||
| **进行中** | `修改/官网_进行中_20260531.md` | **REQ-WEB 总表 · 待改文件 · E1–E7** |
|
||||
| 00 | 00-官网需求执行总表_20260531.md | 进度 % · 速查 |
|
||||
| 01 | 01-需求与验收_v2.md | IA、价格、验收 smoke |
|
||||
| 06 | 06-首页Hero三版_20260531.md | Hero 配色/真截图 |
|
||||
| 07 | 07-官网统一留资接口_20260531.md | **鉴定版** POST /v1/api/scenarios |
|
||||
| 08 | 08-产品矩阵界面与截图_20260531.md | 三端截图规范 |
|
||||
| 09 | 09-官网SEO与GEO全站优化_20260531.md | 关键词矩阵 + Schema + 工具 |
|
||||
| 10 | 10-官网对外接口文档_20260531.md | 改造官网索引 |
|
||||
| 11 | 11-超管统一接口文档中心_20260531.md | 超管 9 类汇总 |
|
||||
| 12 | 12-官网SEO运营后台与自动挂链_20260531.md | **⬜** SEO 运营中心需求(挂链/发帖/收录) |
|
||||
| 13 | 13-官网AI数智员工单设备硬件专题_20260531.md | 单手机 mockup · 流程图 · `../../10、项目管理/images/官网_20260531/` |
|
||||
| **14** | **14-官网AI客服获客场景_20260531.md** | **P0 · 仅文档 · 开发按 §十一(Server→官网)** |
|
||||
| **15** | **15-官网后台用户旅程与Webhook推送_20260531.md** | **P1 · 有手机优先 · 一键推 Webhook** |
|
||||
|
||||
---
|
||||
|
||||
## 三、常用命令
|
||||
|
||||
```bash
|
||||
# 本地预览 + smoke
|
||||
bash 开发文档/8、部署/scripts/deploy_official_site.sh --smoke
|
||||
# → http://127.0.0.1:8090/
|
||||
|
||||
# 复盘末尾生成本轮文档链接(Cursor 可点)
|
||||
python3 开发文档/scripts/build_obsidian_doc_link.py \
|
||||
"开发文档/1、需求/官网/02-页面与视觉/08-产品矩阵界面与截图_20260531.md" \
|
||||
--title "产品矩阵规范"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、留资铁律(踩坑合集)
|
||||
|
||||
| 坑 | 正确做法 |
|
||||
|:---|:---|
|
||||
| 再造 `/v1/open/*` 或第二个留资 URL | **唯一** `POST /v1/api/scenarios` + apiKey/sign |
|
||||
| 只传 `portrait.tags` | 必须传顶层 **`tags` + `siteTags`** 逗号串(`lead.js` 已补) |
|
||||
| `portrait` 参与签名 | 签名时 **删除** portrait、apiKey、sign |
|
||||
| 页脚明文微信/电话 | 只留 **data-lead** 弹窗 |
|
||||
| planId 写 URL | 用 **apiKey** 绑 plan **779** |
|
||||
|
||||
**配置**:`官网/assets/js/site-config.js` · **提交**:`lead.js` · **轨迹**:`track.js`
|
||||
|
||||
### 4.1 访客追踪 + Webhook(REQ-WEB-60)
|
||||
|
||||
| 项 | 说明 |
|
||||
|:---|:---|
|
||||
| 追踪 API | `POST /api/track`(公开)· 数据 `data/analytics.json` |
|
||||
| 留资推送 | `POST /api/lead-local`(有手机号才推飞书)· **勿**浏览器直连飞书 |
|
||||
| 飞书格式 | `{ msg_type:"text", content:{ text:"…" } }` · 成功 `code===0` |
|
||||
| 合并键 | `phone` > `fingerprint`(canvas+UA,非真 IMEI/MAC)> `visitorId` |
|
||||
| 后台 | 概览/获客/旅程 · 右上 ⏰ `GET /admin/api/alerts` |
|
||||
| 边做边勾 | 完成子项即改 `_进行中` REQ-WEB-60 / **REQ-WEB-61** 表状态 ✅ |
|
||||
| 开发顺序 | **REQ-CKB-66 阶段1(Server)→ 阶段2(计划UI)→ REQ-WEB-61 阶段3(官网)** |
|
||||
|
||||
---
|
||||
|
||||
## 五、SEO / GEO 要点(2026)
|
||||
|
||||
1. **SEO 地基优先**:76% Google AI 摘要 URL 在自然 Top10 — 先 meta、sitemap、Schema、内链。
|
||||
2. **五类词**:核心词 / 场景词(连锁·AI客服)/ 长尾 FAQ / 蒸馏词 / 品牌词 → 见 09 §三。
|
||||
3. **Schema**:index 已有 SoftwareApplication + ItemList(产品矩阵);pricing FAQPage。
|
||||
4. **工具**:Screaming Frog 全站爬、Google Rich Results Test、百度搜索资源平台。
|
||||
5. **GEO**:E-E-A-T + 真实案例数字 + 站外知乎/百家号同实体 NAP。
|
||||
6. **勿迷信单独 GEO 外包**:与 SEO 同一套内容工程。
|
||||
|
||||
---
|
||||
|
||||
## 六、产品矩阵 UI
|
||||
|
||||
| 端 | 截图路径 | 替换方式 |
|
||||
|:---|:---|:---|
|
||||
| 存客宝 | `官网/assets/img/matrix-cunkebao.png` | 3100 真机截图覆盖 |
|
||||
| 触客宝 | `官网/assets/img/matrix-touchkebao.png` | 3101 真机截图覆盖 |
|
||||
| AI数智员工 | `官网/assets/img/matrix-aistaff.png` | 3104 真机截图覆盖 |
|
||||
|
||||
- HTML:`index.html#matrix` · class `matrix-shot`
|
||||
- 标题:**三端入口** + **存客宝产品矩阵**(对齐设计稿)
|
||||
|
||||
---
|
||||
|
||||
## 七、超管「接口文档」同步(第 9 类)
|
||||
|
||||
```
|
||||
改留资接口说明
|
||||
→ 07/10 官网 MD
|
||||
→ SuperAdmin_四端对接接口索引.md §九
|
||||
→ SuperAdmin/lib/api-docs-index.ts id: official-site
|
||||
→ 超管 /dashboard/ai-center?tab=docs 自测
|
||||
```
|
||||
|
||||
**注意**:Tab 挂在 AI 中心下,**内容是全平台 9 类**,文案勿写「仅 AI 接口」。
|
||||
|
||||
---
|
||||
|
||||
## 八、复盘与链接(Cursor 专用 · 必记)
|
||||
|
||||
| 错误 | 正确 |
|
||||
|:---|:---|
|
||||
| 复盘贴 `obsidian://` | **点不开**;用 **代码引用块** ` ```1:3:相对路径 ` + 绝对路径一行 |
|
||||
| 贴 6 个历史 MD 链接 | **只列本轮**新建/修改 1~3 个文件 |
|
||||
| 用 `file://` 超链接 | 卡若需求整理 §8.1 **禁止** |
|
||||
|
||||
脚本:`开发文档/scripts/build_obsidian_doc_link.py`(默认 Cursor 块)
|
||||
|
||||
---
|
||||
|
||||
## 九、与兄弟 Skill 关系
|
||||
|
||||
| Skill | 关系 |
|
||||
|:---|:---|
|
||||
| [开发需求文档整理](../开发需求文档整理/SKILL.md) | 四端需求 MD;官网专文走 **本 Skill** |
|
||||
| [需求协调与归档](../需求协调与归档/SKILL.md) | 跨端 COORD |
|
||||
| [存客宝宝塔部署](../存客宝宝塔部署/SKILL.md) | 公网部署官网时 |
|
||||
| 卡若AI F01d 复盘格式 | 每轮收尾五块 |
|
||||
|
||||
---
|
||||
|
||||
## 十、验收清单(每轮)
|
||||
|
||||
- [ ] `node 官网/smoke.mjs` 全绿
|
||||
- [ ] 矩阵三图不裂、alt 有意义
|
||||
- [ ] 留资弹窗可提交(本地需 apiKey)
|
||||
- [ ] 文档 03 迭代日志 + 相关专文变更记录
|
||||
- [ ] 若动接口:超管 api-docs-index 已同步
|
||||
|
||||
---
|
||||
|
||||
## 十一、SEO 运营后台(需求 · 12 号文档)
|
||||
|
||||
| 要点 | 说明 |
|
||||
|:---|:---|
|
||||
| 放哪 | **SuperAdmin 平级「SEO 运营」**,不要塞 AI 中心 |
|
||||
| 自动挂链 | **站内**内链脚本 + **矩阵站**互链表;**站外**发稿任务单(知乎等多为草稿+人工) |
|
||||
| 申请 | P1 接百度站长、GSC、IndexNow API |
|
||||
| 禁止 | 小旋风类黑帽站群、泛目录、买链轮链 |
|
||||
| 开源参考 | GitHub `SEO-Interlinking-Tool`、`schema-sentry` |
|
||||
| 本轮 | **只写需求**,不改 `官网/` |
|
||||
|
||||
---
|
||||
|
||||
## 十二、经验沉淀(evolution)
|
||||
|
||||
| 日期 | 经验 |
|
||||
|:---|:---|
|
||||
| 2026-05-31 | Cursor 聊天内 obsidian:// 无效 → 复盘用代码引用块 |
|
||||
| 2026-05-31 | 官网留资已有单接口,文档 07 鉴定即可 |
|
||||
| 2026-05-31 | 超管接口文档 = 9 类全平台,官网为第 9 类 |
|
||||
| 2026-05-31 | 产品矩阵需 UI 截图,非纯文字卡 |
|
||||
| 2026-05-31 | SEO+GEO:关键词矩阵在 09,ItemList Schema 上 index |
|
||||
| 2026-05-31 | SEO 后台要自建;站群 SaaS 黑帽不做;发帖 API 稀缺→任务单 |
|
||||
| 2026-05-31 | 本机素材:`9、手册/使用手册/截图/AI数智员工-登录页.png`;海报参考在 `个人/4、工作日志/会议纪要/参考/` |
|
||||
| 2026-05-31 | 单设备叙事≠海报获客;硬件专题见 13 号 REQ-WEB-50~54 |
|
||||
| 2026-05-31 | **当日官网需求只写 `官网_进行中`**;`官网/0x` 专文=鉴定索引,勿在 `modify/` 再开主题 MD |
|
||||
| 2026-05-31 | 官网 AI systemPrompt:**Server 七层合并**(存客宝 §6)· 默认循序渐进留资见 REQ-WEB-63 |
|
||||
| 2026-05-31 | REQ-WEB-60:访客 analytics + 飞书 Webhook + 后台 Karuo UI;开发完须在 `_进行中` 打勾 |
|
||||
120
.cursor/skills/存客宝宝塔部署/SKILL.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
name: 存客宝宝塔部署
|
||||
description: 存客宝 v3 部署到腾讯云宝塔 42.194.245.239 的标准流程、验收与踩坑防治。触发词:存客宝部署、宝塔部署、kr-op部署、ckbapi部署、腾讯云部署、部署踩坑、Query.php、场景获客失败、工作台500、二维码不显示。
|
||||
---
|
||||
|
||||
# 存客宝宝塔部署 Skill
|
||||
|
||||
> 生产:kr-op / kr-kf / ckbapi · 服务器 42.194.245.239:22022
|
||||
> 详细踩坑:`开发文档/8、部署/宝塔腾讯云部署踩坑清单.md`
|
||||
|
||||
## 一、部署前必读(防治清单)
|
||||
|
||||
| 序号 | 风险 | 防治 |
|
||||
|------|------|------|
|
||||
| 1 | 登录 405 | 构建 `VITE_API_BASE_URL=https://ckbapi.quwanzhi.com` + kr-op `/api` 反代 |
|
||||
| 2 | 登录 10501 / where 全挂 | **禁止**覆盖 `thinkphp/library/think/db/Query.php` 为 TP5.1 精简版;脚本 2b 强制同步仓库版 |
|
||||
| 3 | JWT 500 | 控制器用 `JwtAuthTrait::authUser()`,勿读 `$request->userInfo` |
|
||||
| 4 | 工作台 500 | `setAttr('config')` 勿用 `$item->config` / `$item->data[]` |
|
||||
| 5 | 流量池分页 1 条 | count 用 `(clone $query)->count()` |
|
||||
| 6 | 场景设置保存 500 | 先跑 `20260525_acquisition_global_settings.sql` |
|
||||
| 7 | 场景列表空白 / 401 | 前端 token 每次请求从 store 读;401 跳登录 |
|
||||
| 8 | **海报二维码 img 404** | URL **勿**带 `.png` 后缀(Nginx 当静态文件 404);用 `/v1/frontend/poster/qrcode/{id}` |
|
||||
| 9 | **海报弹窗误显 H5 码** | 主二维码 = **小程序码**(`poster_qrcode_mode=miniprogram`);H5 表单仅下方链接区,两者隔离 |
|
||||
| 10 | **小程序码进表单/错 AppId** | 落地页 **`pages/poster/index2`**(非 `input2`);AppId **`wx789850448e26c91d`**(存客宝);`.env` 勿重复 `[weChat]` 段 |
|
||||
| 11 | **授权手机号后无入池** | `decryptphone` 勿用 EasyWeChat `phone_number`;用 `MiniProgramQrCodeService::getUserPhoneNumber` curl 直连微信 |
|
||||
| 12 | rsync 勿删 `.env` / `vendor` / `runtime` | 脚本已 exclude |
|
||||
|
||||
## 二、标准命令(项目根 cunkebao_v3)
|
||||
|
||||
```bash
|
||||
# 1. 数据库迁移(有 migrations 时先跑)
|
||||
bash 开发文档/8、部署/scripts/run_db_migrations_tencent.sh
|
||||
|
||||
# 2. 全量部署
|
||||
bash 开发文档/8、部署/scripts/deploy_tencent_baota.sh
|
||||
|
||||
# 3. 仅前端
|
||||
bash 开发文档/8、部署/scripts/deploy_tencent_baota.sh --skip-server
|
||||
|
||||
# 4. 验收(必须 17/17;扩展含 AI/支付/海报;线上全链路)
|
||||
bash 开发文档/8、部署/scripts/acceptance_tencent_prod.sh
|
||||
bash 开发文档/8、部署/scripts/acceptance_tencent_extended.sh
|
||||
bash 开发文档/8、部署/scripts/acceptance_tencent_online_full.sh
|
||||
|
||||
# 5. 仅同步支付 overlay + 证书
|
||||
bash 开发文档/8、部署/scripts/sync_payment_certs_tencent.sh
|
||||
```
|
||||
|
||||
## 三、上线顺序
|
||||
|
||||
**迁移 SQL → rsync 后端(含 Query.php 2b)→ 构建前端 → 发布 dist → 清 cache → 验收**
|
||||
|
||||
## 四、快速诊断
|
||||
|
||||
```bash
|
||||
# 登录
|
||||
curl -X POST https://ckbapi.quwanzhi.com/v1/auth/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"account":"karuo","password":"***","typeId":1}'
|
||||
|
||||
# 场景(需 Bearer)
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
'https://ckbapi.quwanzhi.com/v1/plan/scenes?page=1&limit=5'
|
||||
|
||||
# 海报二维码(应 200 image/png,勿测 .png 后缀)
|
||||
curl -I 'https://ckbapi.quwanzhi.com/v1/frontend/poster/qrcode/709'
|
||||
|
||||
# Query.php 版本(应为 375f1093… 一类,newQuery 须复制 name/table)
|
||||
ssh -p 22022 root@42.194.245.239 \
|
||||
"md5sum /www/wwwroot/ckbapi.quwanzhi.com/thinkphp/library/think/db/Query.php"
|
||||
```
|
||||
|
||||
## 五、路径速查
|
||||
|
||||
| 组件 | 路径 |
|
||||
|------|------|
|
||||
| API | `/www/wwwroot/ckbapi.quwanzhi.com` |
|
||||
| 存客宝 dist | `/www/wwwroot/auto-devlop/ckb-operation/dist` |
|
||||
| 触客宝 dist | `/www/wwwroot/auto-devlop/tkb-wechat/dist` |
|
||||
| monorepo | `/www/wwwroot/mckb_quwanzhi_com` |
|
||||
| MySQL | 10.0.0.4:3306 / cunkebao_v3 / ck_ |
|
||||
|
||||
## 六、复盘归档
|
||||
|
||||
每次部署后更新:`开发文档/8、部署/04-部署复盘/`
|
||||
|
||||
---
|
||||
|
||||
## 七、超管 SuperAdmin(本地 Docker vs 宝塔)
|
||||
|
||||
| 项 | 本地 Docker | 宝塔生产 |
|
||||
|----|-------------|----------|
|
||||
| 访问 | http://localhost:**3103** | **kr-admin.quwanzhi.com**(Node/PM2 单独部署,未进 ckb dist) |
|
||||
| API 反代 | Next `rewrites` → `cunkebao-server` 或 `/api/v1/admin` | 需 Nginx 反代到 **ckbapi.quwanzhi.com/v1/admin** 或同域 `/api` |
|
||||
| 构建 | `docker compose build superadmin-web`,build-arg `SUPERADMIN_API_PROXY_TARGET=http://cunkebao-server` | `npm run build` + `next start`;`NEXT_PUBLIC_API_BASE_URL=/api/v1/admin` |
|
||||
| 设备二维码 | 依赖 `api.guid` + `api.deviceSocketHost`(`.env.docker`) | 同左,生产 `.env` 必须配置;缺则添加设备报「环境配置不完整」 |
|
||||
| S2 同步设备 | 需本机/内网能访问 **rpc.API_BASE_URL**(工作手机 SDK) | 宝塔 server 需能连 S2;本地 Docker 常连 `workphone-sdk:8899` 或线上 kf |
|
||||
| 客户池列表 | 37 万+ 数据,首屏约 **3~5s**(V2 表);勿用旧 `traffic_source_v1` JOIN | 同架构;生产库更大时需加索引/缓存 |
|
||||
| 端口冲突 | 登记 **3103**(勿与 website 3102 混用) | 宝塔单独 Node 项目,不占 3100/3101 |
|
||||
|
||||
### 超管常见报错
|
||||
|
||||
| 现象 | 原因 | 处理 |
|
||||
|------|------|------|
|
||||
| 添加设备网络错误 | 前端打到错误路径 `/v1/api/device/add` | 用 `POST /v1/admin/devices/qrcode` |
|
||||
| 二维码环境不完整 | 缺 guid / deviceSocketHost | 补 `Server/.env` `[api]` 或 `config/device_bind.php` |
|
||||
| 客户池一直 loading | 旧 trafficPool 全表 JOIN 超时 | 已改 V2;确认部署含新 `GetPoolListController` |
|
||||
| 转移设备失败 | 目标项目无 s2_accountId | 项目编辑页确认触客宝账号已创建 |
|
||||
| 本地 3105/3103 混用 | 多个 next dev 实例 | 统一 Docker **3103** 或登记端口 |
|
||||
|
||||
### 超管验收(本地)
|
||||
|
||||
```bash
|
||||
bash 开发文档/8、部署/scripts/acceptance_superadmin_docker.sh
|
||||
# 扩展:overview + devices
|
||||
TOKEN=$(curl -s -X POST http://localhost:8082/v1/admin/auth/login -H 'Content-Type: application/json' \
|
||||
-d '{"account":"admin","password":"e10adc3949ba59abbe56e057f20f883e"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")
|
||||
curl -s -H "Authorization: Bearer $TOKEN" 'http://localhost:8082/v1/admin/dashboard/overview?limit=5' | python3 -m json.tool | head -30
|
||||
curl -s -H "Authorization: Bearer $TOKEN" 'http://localhost:8082/v1/admin/devices/list?limit=5' | python3 -m json.tool | head -20
|
||||
```
|
||||
180
.cursor/skills/开发需求文档整理/SKILL.md
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
name: 开发需求文档整理
|
||||
description: 将录屏转写、标注截图、口述草稿合并为存客宝标准需求 MD。命名 项目_功能主题_日期;modify 扁平无子目录;四端分文件。触发词:需求文档、录屏需求、截图需求、需求拆解、整理需求、开发需求文档、存客宝需求、触客宝需求、超管需求。
|
||||
---
|
||||
|
||||
# 开发需求文档整理 Skill(存客宝 v3 · W05b 项目内)
|
||||
|
||||
> **卡若AI 母 Skill**:`/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水泉_规划拆解/需求整理/SKILL.md`(通用)
|
||||
> **本 Skill**:存客宝 **录屏全量包** + **模块拆解**;落盘纪律以本文为准。
|
||||
> **协调归档**:配合 [需求协调与归档](../需求协调与归档/SKILL.md)(W05c 项目内)
|
||||
|
||||
---
|
||||
|
||||
## 一、目录与命名(强制 · **2026-05-31 修订**)
|
||||
|
||||
> **当日进行中 = 一项目一文件**。主题需求写在文件 **内章节**,不另开 `{项目}_{主题}.md`。
|
||||
> 纪律真源:[需求协调与归档/SKILL.md](../需求协调与归档/SKILL.md) §二、§九
|
||||
|
||||
```
|
||||
开发文档/1、需求/
|
||||
├── 索引.md
|
||||
├── 修改/
|
||||
│ ├── YYYYMMDD_需求总览.md ← 唯一横表(待办+协同+审计)
|
||||
│ ├── 存客宝_进行中_{YYYYMMDD}.md ← 存客宝当日唯一真源(多节)
|
||||
│ ├── 触客宝_进行中_{YYYYMMDD}.md
|
||||
│ ├── 官网_进行中_{YYYYMMDD}.md
|
||||
│ ├── AI数智员工_进行中_{YYYYMMDD}.md
|
||||
│ ├── images/
|
||||
│ └── (无 images/ · 无 已完成/)
|
||||
|
||||
开发文档/10、项目管理/
|
||||
├── images/ ← 需求配图
|
||||
└── 已完成/ ← 关单批次
|
||||
```
|
||||
|
||||
| 规则 | 说明 |
|
||||
|:---|:---|
|
||||
| **进行中文件名** | **仅** `{端}_进行中_{YYYYMMDD}.md` |
|
||||
| **禁止** | 根目录 `存客宝_设置与我的页_*`、`触客宝_手机_*`、`SuperAdmin_*_{主题}_*` 等与 `_进行中` 并存 |
|
||||
| **超管/协同** | SuperAdmin、触客宝登录/BFF 等 **写入 `存客宝_进行中_{YYYYMMDD}.md` 新 §**,不另开 `SuperAdmin_*.md` |
|
||||
| **文内结构** | `## §X · {功能主题}`:问题 / 根因 / 规则 / 接口 / REQ / 验收 |
|
||||
| **端名称** | **触客宝**、**存客宝**、**SuperAdmin**、**AI数智员工** |
|
||||
| **禁止误称** | 不写「初客宝 / 初刻宝 / 纯客宝」 |
|
||||
|
||||
**当日示例(20260531)**:
|
||||
|
||||
| 端 | 唯一文件 | 节示例 |
|
||||
|:---|:---|:---|
|
||||
| 存客宝 | `存客宝_进行中_20260531.md` | §三 设置/我的 · §一 总表 dedupe |
|
||||
| 触客宝 | `触客宝_进行中_20260531.md` | 功能中心二版 |
|
||||
| 官网 | `官网_进行中_20260531.md` | REQ-WEB · E1~E9 |
|
||||
|
||||
---
|
||||
|
||||
## 二、完成目标优先
|
||||
|
||||
**标题下第一句**必须是:
|
||||
|
||||
```markdown
|
||||
完成目标:【一句话:交付什么、谁受益、如何验收】。
|
||||
```
|
||||
|
||||
再写来源、时间轴、模块、排期。禁止先堆口述原文。
|
||||
|
||||
---
|
||||
|
||||
## 三、何时启用
|
||||
|
||||
| 素材 | 动作 |
|
||||
|:---|:---|
|
||||
| 录屏 TXT + 截图 + 草稿 | **追加**对应端 `{项目}_进行中_{日期}.md` 新 §,不新建主题 MD |
|
||||
| 口述「整理需求」 | 读本文 + 项目经验 `卡若AI/.../项目经验/存客宝.md` |
|
||||
| 跨端需求 | 拆到四端各一篇 + 总表;COORD 写入各端「跨项目协调」节 |
|
||||
|
||||
---
|
||||
|
||||
## 四、执行流程
|
||||
|
||||
```
|
||||
素材收集 → 完成目标 → 识别端(四端之一) → 图片入 10、项目管理/images/ → 模块拆解 → 落盘 → 更新索引
|
||||
```
|
||||
|
||||
### Step 1 · 确定端与主题
|
||||
|
||||
| 端 | 端口 | 主题示例 |
|
||||
|:---|:---|:---|
|
||||
| 存客宝 | 3100 H5 | 微信号、场景获客、迁移、设备 |
|
||||
| 触客宝 | 3101 PC | AI获客、工作台、Webhook |
|
||||
| SuperAdmin | 3103 | 超管后台、客户池、项目 |
|
||||
| AI数智员工 | 3104 | 扫码、飞书、协同对接 |
|
||||
|
||||
### Step 2 · 图片
|
||||
|
||||
- 进行中:``(相对 `modify/*.md`)
|
||||
- 命名建议:`YYYYMMDD-模块-序号.png`
|
||||
- Obsidian:`attachmentFolderPath` = `./images`
|
||||
|
||||
### Step 3 · 模块拆解(每端 MD 内)
|
||||
|
||||
| 块 | 内容 |
|
||||
|:---|:---|
|
||||
| 需求总表 | 优先级 + 功能块 + 状态 |
|
||||
| 分模块章节 | 说明 / 表 / 验收 / 代码路径 |
|
||||
| **跨项目协调(本端 · W05c)** | 见 [需求协调与归档](../需求协调与归档/SKILL.md) |
|
||||
|
||||
### Step 4 · 录屏全量包(可选)
|
||||
|
||||
大录屏可另存 `10、项目管理/已完成/` 或保留详细节;**当日进行中**仍只维护该端 **一篇** 主题 MD。
|
||||
|
||||
---
|
||||
|
||||
## 五、标准章节
|
||||
|
||||
见 [template.md](template.md)。必含:完成目标、需求总表、模块章节、开发计划、验收清单、变更记录。
|
||||
|
||||
---
|
||||
|
||||
## 六、与卡若AI / 项目规范
|
||||
|
||||
| 环节 | 动作 |
|
||||
|:---|:---|
|
||||
| W05b 通用 | 卡若AI `需求整理/SKILL.md`;存客宝以 **本节目录纪律** 为准 |
|
||||
| W05c 协调 | 共享 `/Users/karuo/Documents/开发/_共享/跨项目协调/索引.md` |
|
||||
| COORD 分配 | 001/005/006→存客宝;001/003→触客宝;001/002/003→SuperAdmin;001/002/004→AI数智员工 |
|
||||
| 开发前 | `开发文档/开发铁律_先读文档再编码.md` Phase 0 |
|
||||
| Git | `cunkebao-local-only-no-github.mdc`,不自动 push |
|
||||
|
||||
---
|
||||
|
||||
## 七、质量门禁
|
||||
|
||||
- [ ] 当日需求已写入 `{端}_进行中_{日期}.md`(无第二份同端 MD)
|
||||
- [ ] 文内含 `完成目标` 或 § 内问题/根因/验收表
|
||||
- [ ] 第一句 `完成目标:...`
|
||||
- [ ] 无「初刻宝」等误称
|
||||
- [ ] 图片 `../../10、项目管理/images/` 存在
|
||||
- [ ] 本端 COORD 表已写入(有则写,无则注「当前无」)
|
||||
- [ ] `1、需求/索引.md` 与当日总表已更新
|
||||
|
||||
---
|
||||
|
||||
## 八、验收后归档
|
||||
|
||||
1. 迁入 `10、项目管理/已完成/{项目}_{功能主题}_{日期}/` + `../../10、项目管理/images/`
|
||||
2. 运行 `python3 开发文档/1、需求/scripts/flatten_requirements.py`(可选)
|
||||
3. 更新 `YYYYMMDD_需求总览.md`
|
||||
|
||||
---
|
||||
|
||||
## 九、存客宝官网(独立 Skill)
|
||||
|
||||
对外品牌站 **不走四端 `{项目}_{主题}_{日期}.md`**,走专题目录:
|
||||
|
||||
| 项 | 路径 |
|
||||
|:---|:---|
|
||||
| Skill | [.cursor/skills/存客宝官网开发/SKILL.md](../存客宝官网开发/SKILL.md) |
|
||||
| **进行中真源** | [`修改/官网_进行中_20260531.md`](../修改/官网_进行中_20260531.md) |
|
||||
| 文档 | `开发文档/1、需求/官网/`(00~12 编号专文) |
|
||||
| 代码 | `官网/` 静态站 |
|
||||
|
||||
触发词:官网需求、官网SEO、产品矩阵、官网留资 → **先读官网 Skill**,再落盘 `官网/` 或 `1、需求/官网/*.md`。
|
||||
|
||||
**复盘链接**:官网任务同样遵守 `cunkebao-karuo-dialogue.mdc`(代码块路径,禁止 obsidian:// 贴聊天)。
|
||||
|
||||
---
|
||||
|
||||
## 十、经验沉淀
|
||||
|
||||
| 日期 | 说明 |
|
||||
|:---|:---|
|
||||
| 2026-05-31 | §一 修订:**进行中仅 `{端}_进行中_{date}.md`**;与 W05c 需求协调 Skill 对齐 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、延伸阅读
|
||||
|
||||
- [template.md](template.md) · [examples.md](examples.md)
|
||||
- [需求协调与归档/SKILL.md](../需求协调与归档/SKILL.md)
|
||||
- [存客宝官网开发/SKILL.md](../存客宝官网开发/SKILL.md)
|
||||
- 卡若AI:`02_卡人(水)/水泉_规划拆解/需求整理/references/项目经验/存客宝.md`
|
||||
37
.cursor/skills/开发需求文档整理/examples.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 标杆示例 · 2026-05-26 全量需求
|
||||
|
||||
## 文档路径
|
||||
|
||||
`开发文档/1、需求/修改/存-20260526_超级后台设备客服与流量池全量需求.md`
|
||||
|
||||
## 素材来源对照
|
||||
|
||||
| 类型 | 原始路径 | 处理方式 |
|
||||
|------|----------|----------|
|
||||
| 录屏 | `/Users/karuo/Downloads/存客宝开发 20260526.txt` | → 第一章时间轴 14 行 |
|
||||
| 截图夹 | `/Users/karuo/Downloads/20260526/*.png` | → 复制为 `../../10、项目管理/images/20260526-*.png` |
|
||||
| 草稿 | `修改/存-20260526.md` | → 并入模块 D(微信号/流量池) |
|
||||
|
||||
## 结构亮点(可复制)
|
||||
|
||||
1. **时间轴先行**:开发先懂「口述顺序」,再按端看章节。
|
||||
2. **mermaid 产品关系**:多系统(超管/存客宝/触客宝/工作手机)必读。
|
||||
3. **每节固定块**:需求 + 图 + 表 + 验收 + 代码路径。
|
||||
4. **P0 扫码绑 IMEI**:步骤表 + 链到 20260525 设备管理文档。
|
||||
5. **无接口原则**:设备列表「无接口不展示」写进表。
|
||||
6. **Phase 0 六条**:可直接开 sprint。
|
||||
7. **验收 20+ checkbox**:测试无需再问产品。
|
||||
|
||||
## 图片命名对照(节选)
|
||||
|
||||
| 标准名 | 原文件名 |
|
||||
|--------|----------|
|
||||
| `20260526-superadmin-01.png` | 超级管理后台标注_20260526204146.png |
|
||||
| `20260526-device-02.png` | 存客宝-我的-设备管理标注_20260526205229.png |
|
||||
| `20260526-kefu-module-02.png` | 存客宝-我的-新增客户版块标注_20260526210031.png |
|
||||
|
||||
## 触发本 Skill 的用户话术示例
|
||||
|
||||
- 「把录屏和截图整理成需求文档,放修改目录」
|
||||
- 「按上次 0526 的格式再做一份需求」
|
||||
- 「需求要能直接在 Obsidian 看图,开发能照着做」
|
||||
185
.cursor/skills/开发需求文档整理/template.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# 需求文档标准模板(复制后替换占位符)
|
||||
|
||||
> 进行中文件名:`{项目名称}_{功能主题}_{YYYYMMDD}.md`(例 `存客宝_微信号场景迁移与设备_20260529.md`)
|
||||
> 位置:`开发文档/1、需求/修改/`(扁平,无子目录)
|
||||
> 图片:`开发文档/10、项目管理/images/` · 已完成:`10、项目管理/已完成/{项目}_{主题}_{日期}/images/`
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# {项目名称}_{功能主题}_{YYYYMMDD}
|
||||
|
||||
完成目标:【一句话写清本轮最终交付、边界和验收结果。例:把某账号/项目/功能链路整理到可开发、可测试、可验收的稳定状态。】
|
||||
|
||||
> **来源**:录屏口述 `【录屏文件名】.txt`(【时长】)+ 标注截图 `【文件夹或说明】` + 草稿 `【草稿文件名,无则删】`
|
||||
> **日期**:YYYY-MM-DD
|
||||
> **状态**:待开发
|
||||
> **适用端**:【例:超级管理后台 3103 · 存客宝 H5 3100 · 触客宝】
|
||||
|
||||
---
|
||||
|
||||
## 一、录屏时间轴(与口述一一对应)
|
||||
|
||||
| 时间点 | 模块 | 口述要点 |
|
||||
|--------|------|----------|
|
||||
| MM:SS | 【模块名】 | 【一句话需求,**加粗**关键动作】 |
|
||||
| MM:SS~MM:SS | 【模块名】 | 【跨段合并要点】 |
|
||||
| — | 草稿补充 | 【仅来自草稿、录屏未提的项】 |
|
||||
|
||||
---
|
||||
|
||||
## 二、目标拆解(先定目标,再拆需求)
|
||||
|
||||
| 目标层级 | 完成目标 | 验收标准 |
|
||||
|----------|----------|----------|
|
||||
| 总目标 | 【一句话总目标】 | 【最终验收标准】 |
|
||||
| 子目标 1 | 【模块/数据/页面目标】 | 【可测试结果】 |
|
||||
| 子目标 2 | 【模块/数据/页面目标】 | 【可测试结果】 |
|
||||
|
||||
---
|
||||
|
||||
## 三、产品关系(开发必读)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph A["【系统A】"]
|
||||
A1[模块]
|
||||
end
|
||||
subgraph B["【系统B】"]
|
||||
B1[模块]
|
||||
end
|
||||
A1 --> B1
|
||||
```
|
||||
|
||||
- **【产品甲】**:【与产品乙的关系一句】
|
||||
- **【关键链路】**:【例:扫码 → IMEI → 入库 → 超管可见】
|
||||
|
||||
---
|
||||
|
||||
## 四、模块 A · 【端名称 · 如超级管理后台】
|
||||
|
||||
### A1. 【功能名】(P0/P1)
|
||||
|
||||
**完成目标**:【本小节完成后达到什么状态】
|
||||
|
||||
**需求**:【一句话】
|
||||
|
||||

|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 业务规则 | 【规则】 |
|
||||
| 验收 | 【可测试句子】 |
|
||||
| 建议改动 | 【组件/文件,无则写待勘察】 |
|
||||
| 接口 | 【有则链 5、接口;无则「无接口不展示」】 |
|
||||
|
||||
---
|
||||
|
||||
### A2. 【功能名】(P0)
|
||||
|
||||
【重复 A1 结构:说明 + 图 + 表 + 验收 + 代码入口】
|
||||
|
||||
---
|
||||
|
||||
## 五、模块 B · 【端名称】
|
||||
|
||||
### B1. 【功能名】(P0)
|
||||
|
||||
**完成目标**:【本小节完成后达到什么状态】
|
||||
|
||||

|
||||
|
||||
| 功能点 | 详细说明 | 优先级 |
|
||||
|--------|----------|--------|
|
||||
| 【点1】 | 【说明】 | P0 |
|
||||
|
||||
**代码入口**:`【仓库相对路径】`
|
||||
|
||||
---
|
||||
|
||||
## 六、模块 C · 【可选】
|
||||
|
||||
【无则写:本期录屏未涉及,略。】
|
||||
|
||||
---
|
||||
|
||||
## 七、开发计划(建议排期)
|
||||
|
||||
### Phase 0 · 本周必做
|
||||
|
||||
| 编号 | 任务 | 端 | 估时 |
|
||||
|------|------|-----|------|
|
||||
| P0-1 | 【任务】 | 【SuperAdmin/Cunkebao/Server】 | 【0.5d】 |
|
||||
|
||||
### Phase 1 · 下周
|
||||
|
||||
| 编号 | 任务 | 端 |
|
||||
|------|------|-----|
|
||||
| P1-1 | 【任务】 | 【端】 |
|
||||
|
||||
### Phase 2 · 迭代
|
||||
|
||||
| 编号 | 任务 |
|
||||
|------|------|
|
||||
| P2-1 | 【任务】 |
|
||||
|
||||
---
|
||||
|
||||
## 八、验收清单(测试可直接勾选)
|
||||
|
||||
### 【端名称】
|
||||
|
||||
- [ ] 【验收项1】
|
||||
- [ ] 【验收项2】
|
||||
|
||||
---
|
||||
|
||||
## 九、代码与文档索引(开发定位)
|
||||
|
||||
| 模块 | 路径 |
|
||||
|------|------|
|
||||
| 【模块】 | `【文件路径】` |
|
||||
| 接口真源 | `开发文档/5、接口/存客宝API全量索引.md` |
|
||||
|
||||
---
|
||||
|
||||
## 十、素材清单(本目录 images)
|
||||
|
||||
| 文件名 | 用途 |
|
||||
|--------|------|
|
||||
| `YYYYMMDD-xxx-01.png` | 【对应章节】 |
|
||||
|
||||
> **Obsidian**:`` 与本文同级 `../../10、项目管理/images/` 目录。
|
||||
|
||||
---
|
||||
|
||||
## 十一、进度
|
||||
|
||||
| 维度 | 状态 |
|
||||
|------|------|
|
||||
| 需求拆解 | ✅ 已完成 |
|
||||
| UI 素材归档 | ✅ / ⏳ |
|
||||
| 开发 | ⏳ 待排期 |
|
||||
| 接口文档回写 | ⏳ 开发后 |
|
||||
|
||||
**维护**:变更写在下方表。
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| YYYY-MM-DD | 初版:录屏 + 截图 + 草稿合并 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模块章节编号约定
|
||||
|
||||
| 章 | 内容 |
|
||||
|----|------|
|
||||
| 二 | 目标拆解(总目标与子目标) |
|
||||
| 三 | 产品关系 |
|
||||
| 四 | 模块 A(通常超管) |
|
||||
| 五 | 模块 B(通常存客宝主功能) |
|
||||
| 六 | 模块 C(我的/客服等) |
|
||||
| 七~十一 | 固定:计划、验收、索引、素材、进度 |
|
||||
|
||||
模块超过四个时继续「六、模块 E」,**不要**打乱七~十一的固定编号。
|
||||
BIN
.cursor/skills/开发需求文档整理/存客宝-需求到交付流程图.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
177
.cursor/skills/触客宝手机端前端壳/SKILL.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: 触客宝手机端前端壳
|
||||
description: 触客宝 Touchkebao 手机 H5 壳层开发铁律:顶底固定、禁缩放、键盘不乱动、仅中间滚动。对齐存客宝 index.html+Layout 与 AI数智员工 mobile-terminal。触发词:触客宝手机端、mobile shell、viewport、键盘、顶栏固定、底栏Tab、user-scalable、visualViewport、100dvh、乱动、壳固定。
|
||||
---
|
||||
|
||||
# 触客宝 · 手机端前端壳 Skill
|
||||
|
||||
> **需求真源**:[触客宝_进行中 §98 / TKB-M30](../../开发文档/1、需求/修改/触客宝_进行中_20260602.md)(§十五~§十九 见 [0530归档](../../开发文档/10、项目管理/已完成/触客宝_0530进行中全文归档_20260530/))
|
||||
> **参照实现**:
|
||||
> - 存客宝 `Cunkebao/index.html` · `Cunkebao/src/components/Layout/`
|
||||
> - AI数智员工 `AI数智员工/app/layout.tsx` · `globals.css` · `mobile-app-shell.tsx`
|
||||
> **路由范围**:`/mobile/**` · 组件根 `Touchkebao/src/pages/mobile/`
|
||||
|
||||
---
|
||||
|
||||
## 一、何时启用
|
||||
|
||||
| 场景 | 必须读本 Skill |
|
||||
|:---|:---|
|
||||
| 新建/改 `/mobile/` 任意页面 | ✅ |
|
||||
| 改 `Layout.tsx` · TabBar · 顶栏 | ✅ |
|
||||
| 改聊天输入区 · 键盘交互 · 发送/AI 不遮挡 | ✅ §十八 |
|
||||
| 改顶栏 · 同步态 · 算力 | ✅ §十六 |
|
||||
| 改进房预加载 · Dexie 缓存 | ✅ §十七 · **PC+手机双端同一套** |
|
||||
| 改 ErrorBoundary / 登录跳转 | ✅ §十九 |
|
||||
| 改 `Touchkebao/index.html` | ✅ |
|
||||
| 仅改 PC `/pc/` | ❌ 不必 |
|
||||
|
||||
---
|
||||
|
||||
## 二、壳三层(固定结构)
|
||||
|
||||
```
|
||||
html.tkb-mobile(锁滚)
|
||||
└─ #root
|
||||
└─ .shell(fixed · 100dvh · overflow hidden · max-width 420px)
|
||||
├─ .topBar ← flex-shrink:0 · z-50 · safe-area-top
|
||||
├─ .body ← flex:1 · min-height:0 · 唯一纵向滚动
|
||||
└─ .tabBar ← flex-shrink:0 · z-50 · safe-area-bottom
|
||||
```
|
||||
|
||||
**全屏子页**(聊天室):`.room` 覆盖 Tab · 结构 `topBar + record(scroll) + enter(fixed bottom)`。
|
||||
|
||||
---
|
||||
|
||||
## 三、index.html 清单(强制)
|
||||
|
||||
```html
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
```
|
||||
|
||||
**内联 `--real-vh` 脚本**(与存客宝一致):
|
||||
|
||||
```javascript
|
||||
function syncVh() {
|
||||
var vv = window.visualViewport;
|
||||
var h = (vv && vv.height) || window.innerHeight || document.documentElement.clientHeight;
|
||||
document.documentElement.style.setProperty("--real-vh", h * 0.01 + "px");
|
||||
}
|
||||
syncVh();
|
||||
window.addEventListener("resize", syncVh);
|
||||
window.addEventListener("orientationchange", syncVh);
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener("resize", syncVh);
|
||||
}
|
||||
```
|
||||
|
||||
**触控锁滚**(存客宝同款):
|
||||
|
||||
```css
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
html, body { position: fixed; width: 100%; height: 100%; overflow: hidden; touch-action: manipulation; }
|
||||
#root { width: 100%; height: 100%; overflow: hidden; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、CSS 变量与高度(强制)
|
||||
|
||||
```scss
|
||||
.shell {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100dvh;
|
||||
height: calc(var(--real-vh, 1vh) * 100);
|
||||
max-height: calc(var(--real-vh, 1vh) * 100);
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-y: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、键盘 / 输入(强制)
|
||||
|
||||
| 规则 | 说明 |
|
||||
|:---|:---|
|
||||
| 输入字号 **≥ 16px** | 防 iOS focus 自动 zoom |
|
||||
| **不**滚 `body` | 只滚 `.body` 或 `.record` |
|
||||
| `visualViewport` 只更新 `--real-vh` | 禁止改 body top 造成跳变 |
|
||||
| 输入区 `touch-action: auto` | 壳层 `touch-action: manipulation` |
|
||||
| 获焦不 unmount 父 Layout | 避免 Tab 栏闪烁 |
|
||||
|
||||
---
|
||||
|
||||
## 六、禁止清单
|
||||
|
||||
- ❌ `user-scalable=yes` 或缺少 `maximum-scale=1`
|
||||
- ❌ 整页 `html/body { overflow-y: auto }`
|
||||
- ❌ 只用 `100vh` 不用 `--real-vh` / `100dvh`
|
||||
- ❌ 顶栏 `position: sticky` 代替 flex 壳
|
||||
- ❌ 多个并列 `position:fixed` 全屏滚动区
|
||||
- ❌ 键盘弹出时隐藏 TabBar 再显示(闪屏)
|
||||
|
||||
---
|
||||
|
||||
## 七、新页面开发 Checklist
|
||||
|
||||
开发任意 `/mobile/` 页面前 **逐项打勾**:
|
||||
|
||||
- [ ] 内容在 `.body` 内 · 不在 shell 外再包一层可滚 `min-h-screen`
|
||||
- [ ] 未新增 `100vh` 撑高 body
|
||||
- [ ] 弹层 Drawer/Modal 用 `embedded` 或 Portal · 不破坏 shell overflow
|
||||
- [ ] 输入框字号 ≥ 16px
|
||||
- [ ] 长列表在 `.body` 内滚 · 不绑 `window` scroll
|
||||
- [ ] iPhone 真机或 390 smoke:捏合不缩放 · 键盘不整页乱跳 · **发送+AI 可见**(§十八)
|
||||
- [ ] 顶栏:**圆形头像** · 同步时头像闪 · **「· 聊天」保留**(§十六)
|
||||
- [ ] 需求变更已写入 `触客宝_进行中_20260530.md`(一项目一文件)
|
||||
|
||||
---
|
||||
|
||||
## 八、验收命令
|
||||
|
||||
```bash
|
||||
node 开发文档/8、部署/scripts/verify_touchkebao_mobile.mjs
|
||||
# 扩展后应含:viewport user-scalable=no · sessionRows · 无横向溢出
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、与需求协调 Skill 关系
|
||||
|
||||
| Skill | 职责 |
|
||||
|:---|:---|
|
||||
| [需求协调与归档](../需求协调与归档/SKILL.md) | 需求落盘 · 一项目一 `_进行中` |
|
||||
| **本文** | 手机 H5 **壳层与键盘** 编码铁律 |
|
||||
| [开发需求文档整理](../开发需求文档整理/SKILL.md) | 录屏→需求 MD |
|
||||
|
||||
---
|
||||
|
||||
## 十、参照文件速查
|
||||
|
||||
| 项目 | 文件 |
|
||||
|:---|:---|
|
||||
| 存客宝 viewport | `cunkebao_v3/Cunkebao/index.html` |
|
||||
| 存客宝 Layout | `Cunkebao/src/components/Layout/Layout.tsx` · `layout.module.scss` |
|
||||
| AI 终端 globals | `AI数智员工/app/globals.css` → `.mobile-terminal` · `.mobile-app-frame` |
|
||||
| AI Shell 组件 | `AI数智员工/app/components/mobile-app-shell.tsx` |
|
||||
| 触客宝工作台 | `Touchkebao/src/pages/mobile/workbench/Layout.module.scss` |
|
||||
| 触客宝聊天室 | `Touchkebao/src/pages/mobile/workbench/ChatRoom/index.module.scss` |
|
||||
197
.cursor/skills/需求协调与归档/SKILL.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
name: 需求协调与归档
|
||||
description: 存客宝四端需求维护与文档收敛:一项目一进行中MD、已完成只进10、项目管理/已完成/、关单分拣、COORD、0530维护经验。触发词:需求协调、需求归档、四端汇总、已完成、进行中、修改目录整理、一项目一文件、关单、需求维护、项目管理。
|
||||
---
|
||||
|
||||
# 需求协调与归档 Skill(存客宝 v3 · 0602 维护版)
|
||||
|
||||
> **🚫 不再写需求总览(卡若 0602 定)**:需求**按项目、按当天**直接写进 `{项目}_进行中_{YYYYMMDD}.md`,谁的子项目就写谁的文件,简单记录;**禁止**再建/维护 `YYYYMMDD_需求总览.md` 横表(旧文件留作历史索引)。
|
||||
> **🚫 不再写未完成项审计(卡若 0603 定)**:禁止 `YYYYMMDD_未完成项审计*.md`;**完成度表 + §待卡若验收 + §P0 开发顺序** 写在各端当日 `_进行中`。非当天未完 **迁入当天文件** 后 **删除** 旧 `_进行中`(`modify/` 只留当天,详文备份在 `10、项目管理/已完成/`)。
|
||||
> **端真源(唯一)**:`开发文档/1、需求/修改/{项目}_进行中_{YYYYMMDD}.md`(**每端最多 1 个**)
|
||||
> **已完成总汇**:`开发文档/10、项目管理/已完成/YYYYMMDD_四端已完成与全量检测总汇/`(**不在根目录、不在 modify/**)
|
||||
> **跨端协同**:写在**发起方**项目 `_进行中`「跨项目协调」节(标 `协同 XXX`),不再单列总览。
|
||||
> **术语铁律**:统一「知识库」「流量池」;**无「流量词」概念**(ASR 误识,纠错库 `卡若AI/运营中枢/参考资料/卡若闽南口音_ASR纠错库.json` 已收 流量词→流量池)。
|
||||
> **卡若AI 母 Skill**:`/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水泉_规划拆解/需求协调/SKILL.md`
|
||||
> **单篇整理**:[开发需求文档整理/SKILL.md](../开发需求文档整理/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 一、何时启用
|
||||
|
||||
| 场景 | 动作 |
|
||||
|:---|:---|
|
||||
| 新 P0/P1 需求(**当日/本轮**) | **只追加** `修改/{端}_进行中_{YYYYMMDD}.md` 新 § 节 · **禁止**根目录另开 `{端}_{主题}_{date}.md` |
|
||||
| 需求写完仅文档 | 在 `_进行中` 内写 **固定规则+验收** · 状态标 ⬜ |
|
||||
| **官网边开发边勾** | 子项完成即改 `官网_进行中` **⬜→🟡→✅**(REQ-WEB-60/61);**AI 客服**按 [14 专文 §十一](../../开发文档/1、需求/官网/05-AI与硬件/14-官网AI客服获客场景_20260531.md) 阶段打勾 |
|
||||
| **存客宝 AI 客服** | 同步 `存客宝_进行中` **REQ-CKB-66** · Server 先于官网 |
|
||||
| 验收通过 | 迁入 `10、项目管理/已完成/{项目}_{主题}_{date}/` · 更新 **已完成总汇** §索引 |
|
||||
| 端 100% 关单 | **删除** 根目录 `{端}_进行中` · 总汇保留链接 |
|
||||
| 日终 / 分拣 | 各端 `_进行中` 内更新状态 · 已完成项 **移出** 根目录(**不维护总览**) |
|
||||
| 跨端依赖 | COORD 写入 **各端 `_进行中`**「跨项目协调」小节 |
|
||||
|
||||
---
|
||||
|
||||
## 二、目录纪律(2026-06-02 · 已完成迁入项目管理)
|
||||
|
||||
```
|
||||
开发文档/1、需求/修改/
|
||||
├── README.md
|
||||
├── 存客宝_进行中_YYYYMMDD.md ← 需求只写各端进行中(不写总览)
|
||||
├── 触客宝_进行中_YYYYMMDD.md
|
||||
├── 官网_进行中_YYYYMMDD.md
|
||||
└── AI数智员工_进行中_YYYYMMDD.md (有未完才建)
|
||||
# YYYYMMDD_需求总览.md ← 已停用 · 仅历史索引,勿再维护
|
||||
|
||||
开发文档/10、项目管理/
|
||||
├── README.md
|
||||
├── images/ ← 需求截图(禁止 modify/images)
|
||||
├── 已完成/ ← 关单批次唯一落点
|
||||
│ ├── README.md
|
||||
│ └── {项目}_{功能主题}_{YYYYMMDD}/
|
||||
├── 开发进度_*.md
|
||||
└── scripts/
|
||||
```
|
||||
|
||||
### 禁止(本轮维护教训)
|
||||
|
||||
| 禁止 | 正确做法 |
|
||||
|:---|:---|
|
||||
| `modify/已完成/` · `modify/images/` | `10、项目管理/已完成/` · `10、项目管理/images/` |
|
||||
| 写「需求总览/横表」 | **不写总览**;需求按项目当天写进 `{项目}_进行中_{YYYYMMDD}.md` |
|
||||
| 文档里写「流量词」 | 统一「**流量池**」(ASR 误识,无流量词概念) |
|
||||
| 根目录 `四端已完成*.md` | 只放 `10、项目管理/已完成/…/` |
|
||||
| 新建 `10、使用手册/` | 只写 `9、手册/使用手册/…` |
|
||||
| 同项目两个 MD(`_进行中` + 专文) | P0 全文 **并入** `_进行中` 新章节 · 删/归档专文 |
|
||||
| 根目录 `SuperAdmin_{主题}_{date}.md` | 写入 **`存客宝_进行中_{date}.md` §** |
|
||||
| 文件名 初刻宝/初客宝/触刻版 | 统一 **触客宝** |
|
||||
| COORD 只写横表 | **必须**写入各端 `_进行中` |
|
||||
|
||||
---
|
||||
|
||||
## 三、0530 维护期经验(固定写法)
|
||||
|
||||
### 3.1 需求文档结构(P0 块)
|
||||
|
||||
每个 P0 在 `_进行中` 内必须含:
|
||||
|
||||
1. **问题表**(卡若原话 / 截图路径)
|
||||
2. **根因**(代码文件 · 现状 vs 期望)
|
||||
3. **固定规则**(归一化、合并键、禁止项 · **不可写抽象**)
|
||||
4. **接口清单**(不变 URL)
|
||||
5. **代码锚点**(待建/已有路径)
|
||||
6. **验收表**(可数字 · V1~Vn)
|
||||
|
||||
### 3.2 只写文档不开发
|
||||
|
||||
- 状态标 **「P0 · 仅文档 · 待开发」**
|
||||
- 同步 `开发文档/5、接口/开发进度_接口文档.md` 顶部一节
|
||||
- **不**为专文单独占根目录文件名
|
||||
|
||||
### 3.3 已完成 vs 进行中边界
|
||||
|
||||
| 状态 | 放哪 |
|
||||
|:---|:---|
|
||||
| 代码 ✅ · 待目视 🟡 | 进行中留一行 · 细节进总汇或 `10、项目管理/已完成/` 批次 |
|
||||
| 代码 ✅ · 验收 ✅ | 移入 `10、项目管理/已完成/` · 从 `_进行中` **删**该行 |
|
||||
| 运营 P2(签字/Key 轮换) | 写横表 · **不**占 P0 开发 slot |
|
||||
| 全端 100% | 无 `_进行中` · 链总汇 §二 |
|
||||
|
||||
### 3.4 截图与 images
|
||||
|
||||
- 进行中:`10、项目管理/images/{项目}_{现象}_{YYYYMMDD}.png`
|
||||
- 关单迁 `10、项目管理/已完成/{批次}/images/` 时 **改相对路径**
|
||||
|
||||
---
|
||||
|
||||
## 四、COORD 分配(写入各端 `_进行中`)
|
||||
|
||||
| COORD | 简述 | 写入文档 |
|
||||
|:---|:---|:---|
|
||||
| 001 | SuperAdmin 开放 API | 存客宝、SuperAdmin、触客宝、AI数智员工 |
|
||||
| 006 | 设备/微信项目同源 | **存客宝、触客宝** |
|
||||
|
||||
(其余见历史表 · 各端「跨项目协调」小节)
|
||||
|
||||
---
|
||||
|
||||
## 五、关单流程(逐步)
|
||||
|
||||
```
|
||||
1. 开发/文档完成 → 更新 {端}_进行中 状态 ✅
|
||||
2. 全文批次 → 10、项目管理/已完成/{项目}_{主题}_{date}/同名.md
|
||||
3. 更新 已完成/…/四端已完成总汇.md §索引
|
||||
4. 从 _进行中 删除已完成行;端无未完 → 删除 _进行中 文件
|
||||
5. 更新 YYYYMMDD_需求总览.md
|
||||
6. 更新 5、接口/开发进度_接口文档.md
|
||||
7. 更新 10、项目管理/已完成/README.md 表格一行
|
||||
8. 禁止 git push(除非卡若本轮明确说提交)
|
||||
```
|
||||
|
||||
### 脚本(可选)
|
||||
|
||||
```bash
|
||||
python3 开发文档/1、需求/scripts/flatten_requirements.py
|
||||
python3 开发文档/1、需求/scripts/reorganize_requirement_docs.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、开发前检查
|
||||
|
||||
1. 读待开发横表 + **本端唯一** `_进行中`
|
||||
2. 读 **跨项目协调** 表
|
||||
3. 对话列出:已读文档 ≤5 条
|
||||
4. 确认 **没有** 第二份同端 MD
|
||||
5. 关单目标路径为 **`10、项目管理/已完成/`**
|
||||
|
||||
---
|
||||
|
||||
## 七、质量门禁
|
||||
|
||||
- [ ] 根目录无 `*已完成*` MD(总汇只在 `10、项目管理/已完成/`)
|
||||
- [ ] 每端 **≤1** 个 `_进行中` 在 `modify/`
|
||||
- [ ] **禁止** 向 `10、项目管理/已完成/` 新建批次(仅跳转 README)
|
||||
- [ ] P0 块含:问题 / 根因 / 固定规则 / 验收
|
||||
- [ ] 已完成批次在 `10、项目管理/已完成/{项目}_{主题}_{date}/` 且图片不断链
|
||||
- [ ] `10、项目管理/README.md` 与 `已完成/README.md` 已更新
|
||||
|
||||
---
|
||||
|
||||
## 八、与卡若AI 分工
|
||||
|
||||
| Skill | 职责 |
|
||||
|:---|:---|
|
||||
| **本文** | 一项目一文件 · **已完成进 10、项目管理** · 关单分拣 |
|
||||
| 开发需求文档整理 | 录屏→全量 MD · 落盘纪律以本文为准 |
|
||||
|
||||
---
|
||||
|
||||
## 九、当日需求落盘(Agent 必背)
|
||||
|
||||
```
|
||||
卡若提新需求
|
||||
→ 打开 modify/{端}_进行中_{YYYYMMDD}.md 新 §
|
||||
→ 更新 待开发清单 一行
|
||||
→ 截图 → 10、项目管理/images/
|
||||
→ 禁止 新建 modify/{端}_{主题}_{YYYYMMDD}.md
|
||||
关单后
|
||||
→ 迁入 10、项目管理/已完成/{项目}_{主题}_{date}/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、经验沉淀
|
||||
|
||||
| 日期 | 教训 |
|
||||
|:---|:---|
|
||||
| 2026-06-02 | 十大目录收口:已完成+配图→`10、项目管理` · 三表→`需求总览` · `10、使用手册`→`9、手册/使用手册` |
|
||||
| 2026-06-02 | CKB-90:流量分发多子账号轮询 + 客服选择 UI 与好友分配一致 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、延伸阅读
|
||||
|
||||
- [修改/README.md](../../开发文档/1、需求/修改/README.md)
|
||||
- [10、项目管理/README.md](../../开发文档/10、项目管理/README.md)
|
||||
- [已完成/README.md](../../开发文档/10、项目管理/已完成/README.md)
|
||||
- [四端对接铁律](../../开发文档/2、架构/四端对接铁律_只HTTP不串仓.md)
|
||||
20
.gitignore
vendored
@@ -1,12 +1,20 @@
|
||||
Store_vue/unpackage/
|
||||
*.js
|
||||
Store_vue/node_modules/
|
||||
Server/.specstory/
|
||||
Cunkebao/.next/
|
||||
Cunkebao/.specstory/
|
||||
.idea/
|
||||
Cunkebao/.next/
|
||||
Store_vue/node_modules/
|
||||
*.zip
|
||||
Cunkebao/.specstory/
|
||||
*.cursorindexingignore
|
||||
Server/.specstory/
|
||||
Server/thinkphp/
|
||||
Store_vue/.specstory/
|
||||
Store_vue/unpackage/
|
||||
Store_vue/.vscode/
|
||||
SuperAdmin/.specstory/
|
||||
Cunkebao/dist
|
||||
Touchkebao/.specstory/
|
||||
Serverruntime/
|
||||
Moncter/提示词/
|
||||
*.log
|
||||
*.code-workspace
|
||||
项目整体分析报告.md
|
||||
Server/docs/traffic_pool_design.md
|
||||
|
||||
13
.obsidian/app.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"pdfExportSettings": {
|
||||
"pageSize": "Letter",
|
||||
"landscape": false,
|
||||
"margin": "0",
|
||||
"downscalePercent": 100
|
||||
},
|
||||
"promptDelete": false,
|
||||
"attachmentFolderPath": "./images",
|
||||
"useMarkdownLinks": true,
|
||||
"newLinkFormat": "relative",
|
||||
"alwaysUpdateLinks": true
|
||||
}
|
||||
5
.obsidian/appearance.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"enabledCssSnippets": [
|
||||
"cunkebao-devdoc"
|
||||
]
|
||||
}
|
||||
33
.obsidian/core-plugins.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"file-explorer": true,
|
||||
"global-search": true,
|
||||
"switcher": true,
|
||||
"graph": true,
|
||||
"backlink": true,
|
||||
"canvas": true,
|
||||
"outgoing-link": true,
|
||||
"tag-pane": true,
|
||||
"footnotes": false,
|
||||
"properties": true,
|
||||
"page-preview": true,
|
||||
"daily-notes": true,
|
||||
"templates": true,
|
||||
"note-composer": true,
|
||||
"command-palette": true,
|
||||
"slash-command": false,
|
||||
"editor-status": true,
|
||||
"bookmarks": true,
|
||||
"markdown-importer": false,
|
||||
"zk-prefixer": false,
|
||||
"random-note": false,
|
||||
"outline": true,
|
||||
"word-count": true,
|
||||
"slides": false,
|
||||
"audio-recorder": false,
|
||||
"workspaces": false,
|
||||
"file-recovery": true,
|
||||
"publish": false,
|
||||
"sync": true,
|
||||
"bases": true,
|
||||
"webviewer": false
|
||||
}
|
||||
93
.obsidian/graph.json
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"collapse-filter": false,
|
||||
"search": "path:开发文档",
|
||||
"showTags": true,
|
||||
"showAttachments": false,
|
||||
"hideUnresolved": false,
|
||||
"showOrphans": false,
|
||||
"collapse-color-groups": false,
|
||||
"colorGroups": [
|
||||
{
|
||||
"query": "path:开发文档/2、架构",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 14010688
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:开发文档/1、需求",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 6178001
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:开发文档/3、原型",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 6178001
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:开发文档/4、前端",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 162529
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:开发文档/5、接口",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 162529
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:开发文档/6、后端",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 162529
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:开发文档/7、数据库",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 5606192
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:开发文档/8、部署",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 5606192
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:开发文档/9、手册",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 14010688
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:开发文档/10、使用手册",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 12976168
|
||||
}
|
||||
}
|
||||
],
|
||||
"collapse-display": true,
|
||||
"showArrow": true,
|
||||
"textFadeMultiplier": 0,
|
||||
"nodeSizeMultiplier": 1,
|
||||
"lineSizeMultiplier": 1,
|
||||
"collapse-forces": true,
|
||||
"centerStrength": 0.5,
|
||||
"repelStrength": 10,
|
||||
"linkStrength": 1,
|
||||
"linkDistance": 200,
|
||||
"scale": 0.17544920373570672,
|
||||
"close": true
|
||||
}
|
||||
124
.obsidian/snippets/cunkebao-devdoc.css
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
/* 存客宝 v3 · 开发文档 Obsidian 目录配色
|
||||
* 启用:Settings → Appearance → CSS snippets → cunkebao-devdoc
|
||||
* 依赖:Obsidian 文件资源管理器 nav-folder-title data-path 属性
|
||||
* 规范:与工作手机 workphone-devdoc.css 四类分类色对齐
|
||||
*/
|
||||
|
||||
/* ── 一、需求与架构 ── */
|
||||
.nav-folder-title[data-path="开发文档/1、需求"] > .nav-folder-title-content {
|
||||
color: #5E35B1 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/2、架构"] > .nav-folder-title-content {
|
||||
color: #1565C0 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/3、原型"] > .nav-folder-title-content {
|
||||
color: #5E35B1 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── 二、开发与接口 ── */
|
||||
.nav-folder-title[data-path="开发文档/4、前端"] > .nav-folder-title-content {
|
||||
color: #0277BD !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/4、前端/UI"] > .nav-folder-title-content {
|
||||
color: #0288D1 !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/5、接口"] > .nav-folder-title-content {
|
||||
color: #0277BD !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path^="开发文档/5、接口/0"] > .nav-folder-title-content {
|
||||
color: #0288D1 !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/5、接口/images"] > .nav-folder-title-content {
|
||||
color: #78909C !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/5、接口/02-存客宝与工作手机对接"] > .nav-folder-title-content {
|
||||
color: #FFB300 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path^="开发文档/5、接口/02-存客宝与工作手机对接/04-CodeGraph"] > .nav-folder-title-content {
|
||||
color: #78909C !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/6、后端"] > .nav-folder-title-content {
|
||||
color: #0277BD !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── 三、部署与数据 ── */
|
||||
.nav-folder-title[data-path="开发文档/7、数据库"] > .nav-folder-title-content {
|
||||
color: #558B2F !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/8、部署"] > .nav-folder-title-content {
|
||||
color: #558B2F !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/8、部署/scripts"] > .nav-folder-title-content {
|
||||
color: #689F38 !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/8、部署/01-本地环境"] > .nav-folder-title-content {
|
||||
color: #689F38 !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/8、部署/02-数据库同步"] > .nav-folder-title-content {
|
||||
color: #558B2F !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/8、部署/03-运维与CI"] > .nav-folder-title-content {
|
||||
color: #558B2F !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/8、部署/04-部署复盘"] > .nav-folder-title-content {
|
||||
color: #689F38 !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/8、部署/05-测试验收"] > .nav-folder-title-content {
|
||||
color: #F9A825 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path^="开发文档/8、部署/05-测试验收/"] > .nav-folder-title-content {
|
||||
color: #FFB300 !important;
|
||||
}
|
||||
|
||||
/* ── 四、手册与使用 ── */
|
||||
.nav-folder-title[data-path="开发文档/9、手册"] > .nav-folder-title-content {
|
||||
color: #1565C0 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/9、手册/提示词"] > .nav-folder-title-content {
|
||||
color: #0288D1 !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/10、使用手册"] > .nav-folder-title-content {
|
||||
color: #C62828 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/10、使用手册/存客宝"] > .nav-folder-title-content {
|
||||
color: #E65100 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/10、使用手册/存客宝/工作台"] > .nav-folder-title-content {
|
||||
color: #FF6F00 !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/10、使用手册/截图"] > .nav-folder-title-content {
|
||||
color: #78909C !important;
|
||||
}
|
||||
|
||||
/* ── 辅助目录 ── */
|
||||
.nav-folder-title[data-path="开发文档/工作手机对接"] > .nav-folder-title-content {
|
||||
color: #90A4AE !important;
|
||||
}
|
||||
.nav-folder-title[data-path="开发文档/scripts"] > .nav-folder-title-content {
|
||||
color: #546E7A !important;
|
||||
}
|
||||
|
||||
/* 文档说明头图 */
|
||||
.markdown-preview-view img[alt="文档说明"],
|
||||
.markdown-source-view img[alt="文档说明"] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 920px;
|
||||
margin: 0.5em 0 1.2em;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 12px rgba(15, 23, 42, 0.08);
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
}
|
||||
235
.obsidian/workspace.json
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"main": {
|
||||
"id": "413f86c90d0909cc",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "984914435f6a9181",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "4301052b31068085",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "开发文档/1、需求/修改/存客宝_进行中_20260603.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "存客宝_进行中_20260603"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
},
|
||||
"left": {
|
||||
"id": "28b5f5be79ea5491",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "03d49e20527f98d9",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "b4bd2ff5d3f2ec66",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "file-explorer",
|
||||
"state": {
|
||||
"sortOrder": "alphabetical",
|
||||
"autoReveal": false
|
||||
},
|
||||
"icon": "lucide-folder-closed",
|
||||
"title": "文件列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a8ab05904e453c81",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "tag:#存客宝",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-search",
|
||||
"title": "搜索"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1b4c70b94399d9a5",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "bookmarks",
|
||||
"state": {},
|
||||
"icon": "lucide-bookmark",
|
||||
"title": "书签"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 276.5004348754883
|
||||
},
|
||||
"right": {
|
||||
"id": "213cf3c58fffb77e",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "b9b3f94b382e2801",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "7fb9573cc08dd33f",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "开发文档/1、需求/修改/20260530_四端已完成与全量检测总汇.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "20260530_四端已完成与全量检测总汇 的反向链接列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "6e4f013221e7c63d",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "开发文档/项目文档生成器_核心提示词.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "项目文档生成器_核心提示词 的出链列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7b4a28550b01a9ca",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "tag",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"useHierarchy": true,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-tags",
|
||||
"title": "标签"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b9587677fcf19822",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "all-properties",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-archive",
|
||||
"title": "添加笔记属性"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "9a63f6e4237e033b",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "开发文档/项目文档生成器_核心提示词.md",
|
||||
"followCursor": false,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "项目文档生成器_核心提示词 的大纲"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300,
|
||||
"collapsed": true
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"switcher:打开快速切换": false,
|
||||
"graph:查看关系图谱": false,
|
||||
"canvas:新建白板": false,
|
||||
"daily-notes:打开/创建今天的日记": false,
|
||||
"templates:插入模板": false,
|
||||
"command-palette:打开命令面板": false,
|
||||
"bases:新建数据库": false
|
||||
}
|
||||
},
|
||||
"active": "4301052b31068085",
|
||||
"lastOpenFiles": [
|
||||
"Touchkebao/dist/xiaobao-ai.png",
|
||||
"Touchkebao/dist/websdk.js",
|
||||
"Touchkebao/dist/manifest.json",
|
||||
"Touchkebao/dist/logo.png",
|
||||
"Touchkebao/dist/index.html",
|
||||
"Touchkebao/dist/assets/vendor-fZy-8e4I.js",
|
||||
"Touchkebao/dist/assets/utils-Bc0vVQOI.js",
|
||||
"Touchkebao/dist/assets/ui-DsBy32W5.js",
|
||||
"Touchkebao/dist/assets/ui-DmZQDU-f.css",
|
||||
"Touchkebao/dist/assets/other/wither.png",
|
||||
"Touchkebao/dist/assets/other/sun.png",
|
||||
"Touchkebao/dist/assets/other/rose.png",
|
||||
"Touchkebao/dist/assets/other/poop.png",
|
||||
"Touchkebao/dist/assets/other/moon.png",
|
||||
"Touchkebao/dist/assets/other/lips.png",
|
||||
"Touchkebao/dist/assets/other/knife.png",
|
||||
"Touchkebao/dist/assets/other/heart.png",
|
||||
"Touchkebao/dist/assets/other/coffee.png",
|
||||
"Touchkebao/dist/assets/other",
|
||||
"Touchkebao/dist/assets/index-DfxITDxP.css",
|
||||
"Touchkebao/dist/assets/index-DHDH-NpT.js",
|
||||
"开发文档/8、部署/开发进度_五端全量验收_20260603.md",
|
||||
"开发文档/1、需求/修改/官网_进行中_20260603.md",
|
||||
"开发文档/1、需求/修改/未完成项与跨仓验收汇总.md",
|
||||
"开发文档/10、项目管理/已完成/触客宝_0603日终复盘_20260603.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_203119_10_转化话术C_3.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_203119_10_引流玩法B_2.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_203119_10_测试套餐A_1.md",
|
||||
"开发文档/6、后端/核心算法清单.md",
|
||||
"开发文档/6、后端/核心算法图解手册.md",
|
||||
"开发文档/10、项目管理/账号密码与登录环境一览.md",
|
||||
"开发文档/10、项目管理/开发进度.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_184809_10_转化话术C_3.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_184809_10_引流玩法B_2.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_184809_10_测试套餐A_1.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_184041_10_转化话术C_3.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_184041_10_引流玩法B_2.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_184041_10_测试套餐A_1.md",
|
||||
"开发文档/1、需求/修改/AI数智员工_进行中_20260603.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_183530_10_转化话术C_3.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_183530_10_引流玩法B_2.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_183530_10_测试套餐A_1.md",
|
||||
"开发文档/1、需求/修改/存客宝_进行中_20260603.md",
|
||||
"开发文档/1、需求/修改/触客宝_进行中_20260603.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_172928_13_转化话术C_3.md",
|
||||
"Server/public/uploads/knowledge/914/ingest/20260603_172928_13_测试套餐A_1.md"
|
||||
]
|
||||
}
|
||||
9
Cunkebao/.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
.DS_Store
|
||||
coverage
|
||||
.vite
|
||||
12
Cunkebao/.env.development
Normal file
@@ -0,0 +1,12 @@
|
||||
# 开发环境直接使用线上 API(腾讯云数据库)
|
||||
# 如需使用本地后端,改为 VITE_API_BASE_URL=/api 并确保 vite.config.ts 代理配置正确
|
||||
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
VITE_API_BASE_URL2=https://kf.quwanzhi.com:9991
|
||||
VITE_API_WS_URL=wss://kf.quwanzhi.com:9993
|
||||
VITE_APP_TITLE=存客宝
|
||||
|
||||
# 开发环境自动登录配置(仅开发环境使用,生产环境禁止使用)
|
||||
# WARNING: 此配置仅用于开发调试,正式环境部署时必须删除或禁用
|
||||
VITE_DEV_AUTO_LOGIN=true
|
||||
VITE_DEV_ACCOUNT=admin
|
||||
VITE_DEV_PASSWORD=k123456
|
||||
1
Cunkebao/.env.local
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_BASE_URL= http://yishi.com
|
||||
6
Cunkebao/.env.production
Normal file
@@ -0,0 +1,6 @@
|
||||
# 基础环境变量示例
|
||||
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
VITE_API_BASE_URL2=https://kf.quwanzhi.com:9991
|
||||
VITE_API_WS_URL=wss://kf.quwanzhi.com:9993
|
||||
# VITE_API_BASE_URL=http://www.yishi.com
|
||||
VITE_APP_TITLE=存客宝
|
||||
64
Cunkebao/.eslintrc.js
Normal file
@@ -0,0 +1,64 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended", // 这个配置会自动处理大部分冲突
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["react", "react-hooks", "@typescript-eslint", "prettier"],
|
||||
rules: {
|
||||
"prettier/prettier": "error",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "warn",
|
||||
"react/prop-types": "off",
|
||||
"linebreak-style": "off",
|
||||
"eol-last": "off",
|
||||
"no-empty": "warn",
|
||||
"prefer-const": "warn",
|
||||
// 确保与 Prettier 完全兼容
|
||||
"comma-dangle": "off",
|
||||
"comma-spacing": "off",
|
||||
"comma-style": "off",
|
||||
"object-curly-spacing": "off",
|
||||
"array-bracket-spacing": "off",
|
||||
indent: "off",
|
||||
quotes: "off",
|
||||
semi: "off",
|
||||
"arrow-parens": "off",
|
||||
"no-multiple-empty-lines": "off",
|
||||
"max-len": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"space-before-blocks": "off",
|
||||
"keyword-spacing": "off",
|
||||
"space-infix-ops": "off",
|
||||
"space-in-parens": "off",
|
||||
"space-in-brackets": "off",
|
||||
"object-property-newline": "off",
|
||||
"array-element-newline": "off",
|
||||
"function-paren-newline": "off",
|
||||
"object-curly-newline": "off",
|
||||
"array-bracket-newline": "off",
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
};
|
||||
27
Cunkebao/.gitattributes
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# 设置默认行为,如果core.autocrlf没有设置,Git会自动处理行尾符
|
||||
* text=auto
|
||||
|
||||
# 明确指定文本文件使用LF
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.json text eol=lf
|
||||
*.css text eol=lf
|
||||
*.scss text eol=lf
|
||||
*.html text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# 二进制文件
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.svg binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
32
Cunkebao/.gitignore
vendored
@@ -1,23 +1,11 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
yarn.lock
|
||||
.env
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
dist/*
|
||||
.cursorindexingignore
|
||||
*.zip
|
||||
.idea/
|
||||
.next/
|
||||
|
||||
13
Cunkebao/.prettierrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": false,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"endOfLine": "lf",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "as-needed"
|
||||
}
|
||||
11
Cunkebao/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"christian-kohler.path-intellisense",
|
||||
"ms-vscode.vscode-json"
|
||||
]
|
||||
}
|
||||
45
Cunkebao/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.format.enable": false,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"typescript.suggest.autoImports": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false
|
||||
}
|
||||
23
Cunkebao/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
# 存客宝前端 - React + Vite
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
ARG VITE_API_BASE_URL=
|
||||
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,266 +0,0 @@
|
||||
# 内客宝 - 智能获客管理平台
|
||||
|
||||
## 📋 项目简介
|
||||
|
||||
内客宝是一个专业的微信获客和流量管理平台,基于 React 技术栈构建。平台提供智能化的客户获取、管理和运营解决方案,集成了多种自动化工具,帮助企业高效管理微信营销活动。
|
||||
|
||||
## 🚀 技术栈详解
|
||||
|
||||
### 核心框架
|
||||
- **React 18.2.0** - 现代化的用户界面库
|
||||
- **TypeScript 4.9.5** - 类型安全的 JavaScript 超集
|
||||
- **Create React App (CRA) 5.0.1** - React 应用脚手架
|
||||
- **React Router DOM 6.20.0** - 客户端路由管理
|
||||
|
||||
### 构建工具
|
||||
- **CRACO 7.1.0** - Create React App Configuration Override
|
||||
- 支持自定义 webpack 配置
|
||||
- 路径别名配置
|
||||
- 构建优化
|
||||
|
||||
### UI 组件库
|
||||
- **Radix UI** - 无样式的可访问组件库
|
||||
- 完整的组件生态系统(30+ 组件)
|
||||
- 优秀的无障碍访问支持
|
||||
- 高度可定制
|
||||
- **Tailwind CSS 3.4.17** - 实用优先的 CSS 框架
|
||||
- 响应式设计支持
|
||||
- 自定义主题配置
|
||||
- 原子化 CSS 类
|
||||
|
||||
### 图标和样式
|
||||
- **Lucide React 0.454.0** - 精美的图标库
|
||||
- **Tailwind CSS Animate** - CSS 动画库
|
||||
- **Class Variance Authority** - 组件变体管理
|
||||
- **Tailwind Merge** - Tailwind 类名合并工具
|
||||
|
||||
### 状态管理和表单
|
||||
- **React Hook Form 7.54.1** - 高性能表单库
|
||||
- **Zod 3.24.1** - TypeScript 优先的模式验证
|
||||
- **@hookform/resolvers 3.9.1** - 表单验证解析器
|
||||
|
||||
### 数据可视化
|
||||
- **Recharts** - 基于 React 的图表库
|
||||
- **Chart.js 4.5.0** - 灵活的图表库
|
||||
- **@ant-design/plots** - Ant Design 图表组件
|
||||
|
||||
### HTTP 请求和数据处理
|
||||
- **Axios 1.6.0** - HTTP 客户端
|
||||
- **Crypto-js 4.2.0** - 加密库
|
||||
- **Date-fns** - 日期处理库
|
||||
- **XLSX 0.18.5** - Excel 文件处理
|
||||
|
||||
### 通知和反馈
|
||||
- **React Hot Toast 2.5.2** - 轻量级通知库
|
||||
- **Sonner 1.7.4** - 现代化 Toast 组件
|
||||
|
||||
### 高级组件
|
||||
- **@tanstack/react-table** - 功能强大的表格组件
|
||||
- **Embla Carousel React 8.5.1** - 轮播组件
|
||||
- **React Resizable Panels 2.1.7** - 可调整大小的面板
|
||||
- **Vaul 0.9.6** - 抽屉组件
|
||||
- **Input OTP 1.4.1** - OTP 输入组件
|
||||
- **React Day Picker** - 日期选择器
|
||||
|
||||
### 开发工具
|
||||
- **PostCSS 8** - CSS 后处理器
|
||||
- **Autoprefixer 10.4.20** - CSS 前缀自动添加
|
||||
- **ESLint** - 代码质量检查
|
||||
- **Jest** - 单元测试框架
|
||||
- **Testing Library** - React 测试工具
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
nkebao/
|
||||
├── public/ # 静态资源
|
||||
├── src/ # 源代码
|
||||
│ ├── api/ # API 接口封装
|
||||
│ ├── components/ # 全局组件
|
||||
│ │ ├── ui/ # UI 基础组件
|
||||
│ │ └── icons/ # 图标组件
|
||||
│ ├── config/ # 配置文件
|
||||
│ ├── contexts/ # React Context
|
||||
│ ├── hooks/ # 自定义 Hooks
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── workspace/ # 工作台模块
|
||||
│ │ │ ├── auto-like/ # 自动点赞
|
||||
│ │ │ ├── auto-group/ # 自动建群
|
||||
│ │ │ ├── group-push/ # 群消息推送
|
||||
│ │ │ ├── moments-sync/ # 朋友圈同步
|
||||
│ │ │ ├── ai-assistant/ # AI 对话助手
|
||||
│ │ │ └── traffic-distribution/ # 流量分发
|
||||
│ │ ├── devices/ # 设备管理
|
||||
│ │ ├── scenarios/ # 场景管理
|
||||
│ │ ├── content/ # 内容管理
|
||||
│ │ └── ...
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── App.tsx # 应用根组件
|
||||
│ └── index.tsx # 应用入口
|
||||
├── craco.config.js # CRACO 配置
|
||||
├── tailwind.config.js # Tailwind CSS 配置
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
└── package.json # 项目依赖
|
||||
```
|
||||
|
||||
## 🎯 核心功能模块
|
||||
|
||||
### 工作台 (Workspace)
|
||||
- **自动点赞** - 智能点赞管理和配置
|
||||
- **自动建群** - 群组自动化创建和管理
|
||||
- **群消息推送** - 群组消息批量发送
|
||||
- **朋友圈同步** - 内容同步和发布
|
||||
- **AI 对话助手** - 智能客服和对话管理
|
||||
- **流量分发** - 流量分配和策略管理
|
||||
|
||||
### 设备管理 (Devices)
|
||||
- 设备状态监控和配置
|
||||
- 设备性能分析
|
||||
- 设备权限管理
|
||||
|
||||
### 场景管理 (Scenarios)
|
||||
- 营销场景配置
|
||||
- 自动化流程设计
|
||||
- 场景效果分析
|
||||
|
||||
### 内容管理 (Content)
|
||||
- 内容创建与编辑
|
||||
- 内容模板管理
|
||||
- 内容发布调度
|
||||
|
||||
### 其他模块
|
||||
- 用户管理 (Users)
|
||||
- 订单管理 (Orders)
|
||||
- 流量池管理 (Traffic Pool)
|
||||
- 联系人导入 (Contact Import)
|
||||
|
||||
## 🛠️ 开发指南
|
||||
|
||||
### 环境要求
|
||||
- **Node.js** 16+
|
||||
- **npm** 或 **yarn**
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
# 使用 npm
|
||||
npm install
|
||||
|
||||
# 使用 yarn
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 开发环境启动
|
||||
```bash
|
||||
# 使用 npm
|
||||
npm start
|
||||
|
||||
# 使用 yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### 构建生产版本
|
||||
```bash
|
||||
# 使用 npm
|
||||
npm run build
|
||||
|
||||
# 使用 yarn
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
```bash
|
||||
# 使用 npm
|
||||
npm test
|
||||
|
||||
# 使用 yarn
|
||||
yarn test
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 路径别名配置
|
||||
项目使用 CRACO 配置了路径别名:
|
||||
```javascript
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'@/components': path.resolve(__dirname, 'src/components'),
|
||||
'@/api': path.resolve(__dirname, 'src/api'),
|
||||
'@/types': path.resolve(__dirname, 'src/types'),
|
||||
'@/hooks': path.resolve(__dirname, 'src/hooks'),
|
||||
'@/utils': path.resolve(__dirname, 'src/utils'),
|
||||
'@/styles': path.resolve(__dirname, 'src/styles'),
|
||||
'@/pages': path.resolve(__dirname, 'src/pages'),
|
||||
```
|
||||
|
||||
### Tailwind CSS 配置
|
||||
- 自定义字体大小和间距
|
||||
- 响应式断点配置
|
||||
- 主题颜色系统
|
||||
|
||||
### TypeScript 配置
|
||||
- 严格模式启用
|
||||
- 路径映射配置
|
||||
- JSX 支持
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
项目采用移动优先的响应式设计:
|
||||
- 支持桌面端、平板端、移动端
|
||||
- 自适应布局组件
|
||||
- 触摸友好的交互设计
|
||||
|
||||
## 🎨 UI 设计系统
|
||||
|
||||
### 设计原则
|
||||
- 简洁现代的设计风格
|
||||
- 一致的用户体验
|
||||
- 无障碍访问支持
|
||||
|
||||
### 组件库特点
|
||||
- 基于 Radix UI 的高质量组件
|
||||
- 完整的表单组件系统
|
||||
- 数据展示组件
|
||||
- 导航和布局组件
|
||||
|
||||
## 🔒 安全特性
|
||||
|
||||
- 身份验证和授权
|
||||
- API 请求拦截
|
||||
- 数据验证和清理
|
||||
- 加密功能支持
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
- 代码分割和懒加载
|
||||
- 组件优化
|
||||
- 缓存策略
|
||||
- 包大小优化
|
||||
|
||||
## 🧪 测试策略
|
||||
|
||||
- 单元测试 (Jest + Testing Library)
|
||||
- 组件测试
|
||||
- 集成测试支持
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证。
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
如有问题或建议,请联系开发团队。
|
||||
|
||||
---
|
||||
|
||||
**项目名称**: 内客宝 (nkebao2)
|
||||
**版本**: 0.1.0
|
||||
**技术栈**: React + TypeScript + CRA + Tailwind CSS
|
||||
**最后更新**: 2024年12月
|
||||
@@ -1,16 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'@/components': path.resolve(__dirname, 'src/components'),
|
||||
'@/api': path.resolve(__dirname, 'src/api'),
|
||||
'@/types': path.resolve(__dirname, 'src/types'),
|
||||
'@/hooks': path.resolve(__dirname, 'src/hooks'),
|
||||
'@/utils': path.resolve(__dirname, 'src/utils'),
|
||||
'@/styles': path.resolve(__dirname, 'src/styles'),
|
||||
'@/pages': path.resolve(__dirname, 'src/pages'),
|
||||
},
|
||||
},
|
||||
};
|
||||
95
Cunkebao/devlop.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import os
|
||||
import zipfile
|
||||
import paramiko
|
||||
|
||||
# 配置
|
||||
local_dir = './dist' # 本地要打包的目录
|
||||
zip_name = 'dist.zip'
|
||||
# 上传到服务器的 zip 路径
|
||||
remote_path = '/www/wwwroot/auto-devlop/ckb-operation/dist.zip' # 服务器上的临时zip路径
|
||||
server_ip = '42.194.245.239'
|
||||
server_port = 6523
|
||||
server_user = 'yongpxu'
|
||||
server_pwd = 'Aa123456789.'
|
||||
# 服务器 dist 相关目录
|
||||
remote_base_dir = '/www/wwwroot/auto-devlop/ckb-operation'
|
||||
dist_dir = f'{remote_base_dir}/dist'
|
||||
dist1_dir = f'{remote_base_dir}/dist1'
|
||||
dist2_dir = f'{remote_base_dir}/dist2'
|
||||
|
||||
# 美化输出用的函数
|
||||
from datetime import datetime
|
||||
|
||||
def info(msg):
|
||||
print(f"\033[36m[INFO {datetime.now().strftime('%H:%M:%S')}] {msg}\033[0m")
|
||||
|
||||
def success(msg):
|
||||
print(f"\033[32m[SUCCESS] {msg}\033[0m")
|
||||
|
||||
def error(msg):
|
||||
print(f"\033[31m[ERROR] {msg}\033[0m")
|
||||
|
||||
def step(msg):
|
||||
print(f"\n\033[35m==== {msg} ====" + "\033[0m")
|
||||
|
||||
# 1. 先运行 pnpm build
|
||||
step('Step 1: 构建项目 (pnpm build)')
|
||||
info('开始执行 pnpm build...')
|
||||
ret = os.system('pnpm build')
|
||||
if ret != 0:
|
||||
error('pnpm build 失败,终止部署!')
|
||||
exit(1)
|
||||
success('pnpm build 完成')
|
||||
|
||||
# 2. 打包
|
||||
step('Step 2: 打包 dist 目录为 zip')
|
||||
info('开始打包 dist 目录...')
|
||||
with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(local_dir):
|
||||
for file in files:
|
||||
filepath = os.path.join(root, file)
|
||||
arcname = os.path.relpath(filepath, local_dir)
|
||||
zipf.write(filepath, arcname)
|
||||
success('本地打包完成')
|
||||
|
||||
# 3. 上传
|
||||
step('Step 3: 上传 zip 包到服务器')
|
||||
info('开始上传 zip 包...')
|
||||
transport = paramiko.Transport((server_ip, server_port))
|
||||
transport.connect(username=server_user, password=server_pwd)
|
||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||
sftp.put(zip_name, remote_path)
|
||||
sftp.close()
|
||||
transport.close()
|
||||
success('上传到服务器完成')
|
||||
|
||||
# 删除本地 dist.zip
|
||||
try:
|
||||
os.remove(zip_name)
|
||||
success('本地 dist.zip 已删除')
|
||||
except Exception as e:
|
||||
error(f'本地 dist.zip 删除失败: {e}')
|
||||
|
||||
# 4. 远程解压并覆盖
|
||||
step('Step 4: 服务器端解压、切换目录')
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(server_ip, server_port, server_user, server_pwd)
|
||||
commands = [
|
||||
f'unzip -oq {remote_path} -d {dist2_dir}', # 静默解压
|
||||
f'rm {remote_path}',
|
||||
f'if [ -d {dist_dir} ]; then mv {dist_dir} {dist1_dir}; fi',
|
||||
f'mv {dist2_dir} {dist_dir}',
|
||||
f'rm -rf {dist1_dir}'
|
||||
]
|
||||
for i, cmd in enumerate(commands, 1):
|
||||
info(f'执行第{i}步: {cmd}')
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
out, err = stdout.read().decode(), stderr.read().decode()
|
||||
# 只打印非 unzip 命令的输出
|
||||
if i != 1 and out.strip():
|
||||
print(out.strip())
|
||||
if err.strip():
|
||||
error(err.strip())
|
||||
ssh.close()
|
||||
success('服务器解压并覆盖完成,部署成功!')
|
||||
48
Cunkebao/docker/nginx.conf
Normal file
@@ -0,0 +1,48 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
client_max_body_size 200M;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json;
|
||||
gzip_comp_level 6;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
}
|
||||
|
||||
location = /.vite/manifest.json {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://cunkebao-server/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||
48
Cunkebao/docker/nginx.local.conf
Normal file
@@ -0,0 +1,48 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
client_max_body_size 200M;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json;
|
||||
gzip_comp_level 6;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
}
|
||||
|
||||
location = /.vite/manifest.json {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://cunkebao-server/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||
196
Cunkebao/docs/自动点赞-自动评论功能开发说明.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 存客宝 · 自动点赞扩展「自动评论」开发说明
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
在现有「工作台 · 自动点赞」能力上,增加**自动评论**子能力。评论方式分为两种:
|
||||
|
||||
| 模式 | 说明 |
|
||||
|------|------|
|
||||
| **固定评论** | 用户预先配置**多条**文案;执行时**随机选取一条**发出。 |
|
||||
| **AI 评论** | 根据朋友圈正文 / 可见描述等上下文,由大模型生成短评论后再发出。 |
|
||||
|
||||
本文档供前后端与设备协议联调使用,并约定配置字段、接口与任务执行顺序。
|
||||
|
||||
---
|
||||
|
||||
## 2. 需求摘要
|
||||
|
||||
### 2.1 产品硬性约定(本次补充)
|
||||
|
||||
1. **自动评论总开关(必选)**
|
||||
- 每个自动点赞任务必须提供**独立的「自动评论」开关**(开 / 关)。
|
||||
- **关**:不发起任何自动评论逻辑,行为与当前线上「仅自动点赞」一致。
|
||||
- **开**:再根据下方「评论模式」走固定评论或 AI 评论。
|
||||
- 对应配置字段:`enableAutoComment`(0=关,1=开);前后端表单、列表展示均需体现该开关状态。
|
||||
|
||||
2. **固定评论:多条配置、随机一条**
|
||||
- 固定评论模式下,用户可配置**多条**文案(数组,至少 1 条)。
|
||||
- 每次对某条朋友圈执行自动评论时,从 `fixedComments` 中**随机抽取一条**作为本次评论内容(均匀随机即可;不要求去重轮播,除非后续产品另定)。
|
||||
- 仅 1 条配置时,行为等价于「每次都用该条」。
|
||||
|
||||
### 2.2 其他需求说明
|
||||
|
||||
- 自动评论依附于**现有自动点赞工作台**(`type = 1`),在同一任务配置中通过上述**总开关** + **评论模式**管理。
|
||||
- **固定评论**:除「多条 + 随机一条」外,仍需校验每条长度(建议与微信单条评论上限对齐,具体以客户端协议为准)。
|
||||
- **AI 评论**:用户可编辑「补充提示词」;**默认提示词**(新建任务时表单默认值、后端缺省回填)为:
|
||||
|
||||
```
|
||||
1、模仿好友自然语气,简洁走心,贴合朋友圈内容,口语化不生硬,适配日常互动
|
||||
2、轻松接地气,简短真诚,不浮夸,适配风景 / 日常 / 自拍等多类朋友圈
|
||||
3、可适当在句末加一个微信内置表情(客户端识别的方括号写法),例如:[微笑]、[666]、[强]、[玫瑰]、[愉快],与语气相符即可,最多一个,勿堆砌;不要用一串 Unicode 小黄脸代替
|
||||
```
|
||||
|
||||
- 业务上需明确:**仅评论**、**先赞后评**、**仅赞不评** 的组合策略(见第 5 节建议默认)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 现状与代码入口(便于对接)
|
||||
|
||||
### 3.1 存客宝(`F:\karuo\yi-shi\Cunkebao`)
|
||||
|
||||
- 路由:`/workspace/auto-like`、`/workspace/auto-like/new`、`/workspace/auto-like/edit/:id`、`/workspace/auto-like/record/:id`
|
||||
- 列表 / 详情 API 封装:`src/pages/mobile/workspace/auto-like/list/api.ts`(`/v1/workbench/list`、`/v1/workbench/detail`、`/v1/workbench/create`、`/v1/workbench/update` 等,`type: 1`)
|
||||
- 任务与表单类型:`src/pages/mobile/workspace/auto-like/new/data.ts` 等处的 `LikeTask`、`CreateLikeTaskData`
|
||||
|
||||
### 3.2 服务端(`F:\karuo\yi-shi\Server`)
|
||||
|
||||
- 配置表:`ck_workbench_auto_like`(模型 `app\cunkebao\model\WorkbenchAutoLike`)
|
||||
- 执行记录:`ck_workbench_auto_like_item`
|
||||
- 创建 / 更新配置:`application\cunkebao\controller\workbench\WorkbenchController.php` 中 `TYPE_AUTO_LIKE` 分支
|
||||
- 校验:`application\cunkebao\validate\Workbench.php` 中 `type = 1` 相关规则
|
||||
- 定时 / 队列任务:`application\job\WorkbenchAutoLikeJob.php`,点赞调用 `WebSocketController::momentInteract()`
|
||||
- 当前 `momentInteract` 仅下发 `momentInteractType => 1`(点赞),**评论能力需在设备指令层扩展**(见第 6 节)
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置项设计(建议字段)
|
||||
|
||||
在 `workbench_auto_like` 上扩展存储(**新增列**或 **`json` 扩展字段**二选一,推荐新增列便于查询与默认值):
|
||||
|
||||
| 字段(示例命名) | 类型 | 说明 |
|
||||
|------------------|------|------|
|
||||
| `enableAutoComment` | tinyint(1) | **自动评论总开关**:0 关 / 1 开(关则完全不评论) |
|
||||
| `commentMode` | tinyint(1) | 仅当 `enableAutoComment=1` 时有效:1=固定评论,2=AI 评论 |
|
||||
| `fixedComments` | json / text | 固定评论文案**数组**(≥1 条);执行时**随机取一条**评论,如 `["真好","赞"]` |
|
||||
| `aiCommentPrompt` | varchar(2000) 或 text | 用户自定义 AI 提示词;**空则使用产品默认拼接** |
|
||||
| `commentAfterLike` | tinyint(1) | 可选;1=先赞后评,0=仅评论不强制点赞(与 `enableAutoComment`、原点赞开关组合) |
|
||||
|
||||
**默认行为建议:**
|
||||
|
||||
- `enableAutoComment = 0`:**自动评论总开关关闭**,与线上一致,只点赞、不评论。
|
||||
- `enableAutoComment = 1`:必须带有效 `commentMode`;固定模式下 `fixedComments` 至少 1 条,执行时随机一条。
|
||||
- `commentMode = 2` 且 `aiCommentPrompt` 为空:服务端保存/返回时回填为第 2 节中的两段默认文案(可存库,也可仅前端默认 + 后端生成时用常量)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务执行逻辑(`WorkbenchAutoLikeJob`)
|
||||
|
||||
建议流程(可在 PRD 确认后微调):
|
||||
|
||||
1. 沿用现有:时间窗、`maxLikes`、内容类型、好友分页、点赞间隔等。
|
||||
2. 对「目标朋友圈」在点赞成功后:
|
||||
- **仅当 `enableAutoComment = 1`(总开关开)** 且当日已成功评论次数(按 `commentTime` 统计、与点赞同一时段窗口)**未达到 `maxLikes` 所允许的条数**时,才进入评论逻辑(评论与点赞**各自计数**,**上限数值均取配置中的 `maxLikes`**);总开关为关则跳过整段评论。
|
||||
- `commentMode = 1`(固定评论):从 `fixedComments` 数组中 **随机选取一条** 作为本次评论文案并下发。
|
||||
- `commentMode = 2`(AI 评论):拉取朋友圈文本上下文 → 调用 AI 服务 → 得到短句 → 截断与敏感词过滤(如有)→ 下发评论指令。
|
||||
3. 评论与点赞的间隔:建议在 `interval` 之外增加**独立评论间隔**字段 `commentInterval`(秒),避免操作过快触发风控(可选,产品定)。
|
||||
|
||||
**去重:** 若同一条朋友圈既要赞又要评,`workbench_auto_like_item` 需能区分「已赞」「已评」或增加评论记录表,避免重复评论(见第 7 节)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 设备 / WebSocket 协议(必排期)
|
||||
|
||||
当前 `application\api\controller\WebSocketController.php` 中 `momentInteract` 固定为点赞类型。需与**终端 / 协议文档**确认:
|
||||
|
||||
- 朋友圈评论是否仍使用 `CmdMomentInteract`,通过不同 `momentInteractType` 或附加 `commentContent` 等字段区分;
|
||||
- 或独立命令如 `CmdMomentComment`(以实际协议为准)。
|
||||
|
||||
**后端交付物:** 新增或扩展方法(例如 `momentComment($snsId, $wechatAccountId, $wechatFriendId, $content)`),由 `WorkbenchAutoLikeJob` 在生成文案后调用。
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据与统计
|
||||
|
||||
### 7.1 记录表扩展
|
||||
|
||||
任选一种:
|
||||
|
||||
- **方案 A**:在 `ck_workbench_auto_like_item` 增加 `commentContent`、`commentTime`、`commentStatus` 等;同一条动态赞+评可同一行更新或两行(需唯一约束说明)。
|
||||
- **方案 B**:新建 `ck_workbench_auto_comment_item`,与 `momentsId` / `snsId` / `workbenchId` 关联,结构更清晰。
|
||||
|
||||
列表页若需展示「评论内容 / 是否 AI」,需同步扩展 `WorkbenchAutoLikeController` / `WorkbenchController` 的 records 接口字段。
|
||||
|
||||
### 7.2 统计
|
||||
|
||||
与现有 `todayLikeCount`、`totalLikeCount` 并列,可增加 `todayCommentCount`、`totalCommentCount`(在 `WorkbenchController` 组装 `config` 时查询)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 接口与校验(`WorkbenchController` + `Workbench` 校验器)
|
||||
|
||||
- `create` / `update`(`type = 1`)增加对上述字段的读写。
|
||||
- 校验规则示例:
|
||||
- **`enableAutoComment`**:必填布尔或 0/1;为 1 时下列规则生效。
|
||||
- `enableAutoComment = 1` 时:`commentMode` 必填(1 或 2)。
|
||||
- `commentMode = 1`:`fixedComments` 为**非空数组**(至少一条文案),每条长度 ≤ 限制;服务端/Job 执行时**随机取一条**。
|
||||
- `commentMode = 2`:`aiCommentPrompt` 可为空(空则用默认常量);若限制长度建议 ≥ 500 字以容纳用户扩展。
|
||||
|
||||
存客宝侧:`CreateLikeTaskData` / `LikeTask` 增加对应 TS 字段;新建/编辑页增加表单项(**自动评论总开关**、模式单选、**固定评论多条录入 + 说明「随机一条」**、AI 提示词多行文本默认值)。
|
||||
|
||||
---
|
||||
|
||||
## 9. AI 评论实现建议(服务端)
|
||||
|
||||
- 复用项目内已有 AI 调用模式(例如内容库 `ContentLibraryController::aiRewrite` 或 `application\ai\controller\OpenAi.php` 所走通道),封装 **`generateMomentComment(context, userPrompt)`**:
|
||||
- **System / 前置说明**:强调输出**一条**短评论、无引号包裹、无多余解释。
|
||||
- **User 内容**:拼接「朋友圈正文/摘要」+ 默认两段提示 + 用户自定义 `aiCommentPrompt`。
|
||||
- 失败策略:记录日志;可选降级为「不发评论」或「随机一条固定兜底文案」(产品定)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 存客宝前端交互要点
|
||||
|
||||
- 在「新建/编辑自动点赞」页增加区块:**自动评论**,顶部为**总开关**(关则隐藏或禁用下方模式与文案配置,提交时 `enableAutoComment=0`)。
|
||||
- 总开关**开**时展示子选项:
|
||||
- **固定评论**:支持录入**多条**文案(列表、多标签或换行拆分等交互任选);文案旁或区块说明:**「每次评论随机抽取其中一条」**。
|
||||
- **AI 评论**:多行 `TextArea`,占位符或默认值展示第 2 节两段文案。
|
||||
- 列表 / 记录页:如需区分展示,可增加「评论数」或记录列(依赖接口)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 测试清单(提测用)
|
||||
|
||||
- [ ] 仅点赞:老数据无新字段或**自动评论总开关关**时,行为与线上一致。
|
||||
- [ ] **总开关**:关不评论;开才评论。
|
||||
- [ ] 固定评论:**多条配置**、多次执行观察为**随机一条**、单条边界长度、仅文字朋友圈 / 带图朋友圈。
|
||||
- [ ] AI 评论:默认提示词、自定义提示词、AI 超时 / 失败。
|
||||
- [ ] 当日评论次数达到 `maxLikes` 后不再发评论(与点赞各自计数、上限数值均为 `maxLikes`);点赞是否仍执行(按产品设计)。
|
||||
- [ ] 先赞后评顺序与间隔;重复调度不产生重复评论。
|
||||
- [ ] 多端设备协议联调通过。
|
||||
|
||||
---
|
||||
|
||||
## 12. 交付物清单
|
||||
|
||||
| 层级 | 交付物 |
|
||||
|------|--------|
|
||||
| DB | `workbench_auto_like`(及可选新表)迁移 SQL |
|
||||
| Server | `WorkbenchController`、`WorkbenchValidate`、`WorkbenchAutoLikeJob`、`WebSocketController`(或新评论方法)、AI 封装 |
|
||||
| Cunkebao | `auto-like` 表单/类型/API 字段、文案与校验提示 |
|
||||
| 联调 | 设备侧评论指令说明与抓包用例 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 附录:默认 AI 提示词(常量)
|
||||
|
||||
以下为产品约定默认值,建议定义为后端常量 + 前端表单初始值,保持完全一致:
|
||||
|
||||
```
|
||||
1、模仿好友自然语气,简洁走心,贴合朋友圈内容,口语化不生硬,适配日常互动
|
||||
2、轻松接地气,简短真诚,不浮夸,适配风景 / 日常 / 自拍等多类朋友圈
|
||||
3、可适当在句末加一个微信内置表情(客户端识别的方括号写法),例如:[微笑]、[666]、[强]、[玫瑰]、[愉快],与语气相符即可,最多一个,勿堆砌;不要用一串 Unicode 小黄脸代替
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*文档版本:v1.2 · 与仓库路径 `Cunkebao`、`Server` 当前结构对应,实施时以当时代码为准微调字段命名与表结构。*
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
66
Cunkebao/index.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<title>存客宝</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png" />
|
||||
<link rel="apple-touch-icon" href="/logo-192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#F5A623" />
|
||||
<style>
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: auto;
|
||||
}
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
html,
|
||||
body {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
function syncVh() {
|
||||
var vv = window.visualViewport;
|
||||
var h = (vv && vv.height) || window.innerHeight || document.documentElement.clientHeight;
|
||||
document.documentElement.style.setProperty("--real-vh", h * 0.01 + "px");
|
||||
}
|
||||
syncVh();
|
||||
window.addEventListener("resize", syncVh);
|
||||
window.addEventListener("orientationchange", syncVh);
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener("resize", syncVh);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!-- 引入 uni-app web-view SDK(必须) -->
|
||||
<script type="text/javascript" src="/websdk.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
6
Cunkebao/next-env.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
21347
Cunkebao/package-lock.json
generated
@@ -1,106 +1,56 @@
|
||||
{
|
||||
"name": "nkebao2",
|
||||
"version": "0.1.0",
|
||||
"name": "cunkebao",
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/plots": "latest",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "latest",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
||||
"@radix-ui/react-avatar": "latest",
|
||||
"@radix-ui/react-checkbox": "latest",
|
||||
"@radix-ui/react-collapsible": "latest",
|
||||
"@radix-ui/react-context-menu": "^2.2.4",
|
||||
"@radix-ui/react-dialog": "latest",
|
||||
"@radix-ui/react-dropdown-menu": "latest",
|
||||
"@radix-ui/react-hover-card": "^1.1.4",
|
||||
"@radix-ui/react-icons": "latest",
|
||||
"@radix-ui/react-label": "latest",
|
||||
"@radix-ui/react-menubar": "^1.1.4",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||
"@radix-ui/react-popover": "latest",
|
||||
"@radix-ui/react-progress": "latest",
|
||||
"@radix-ui/react-radio-group": "latest",
|
||||
"@radix-ui/react-scroll-area": "latest",
|
||||
"@radix-ui/react-select": "latest",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "latest",
|
||||
"@radix-ui/react-tabs": "latest",
|
||||
"@radix-ui/react-toast": "latest",
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "latest",
|
||||
"@tanstack/react-table": "latest",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^18.19.34",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.6.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"antd": "^5.13.1",
|
||||
"antd-mobile": "^5.39.1",
|
||||
"antd-mobile-icons": "^0.3.0",
|
||||
"axios": "^1.6.7",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"input-otp": "1.4.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "latest",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.54.1",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"recharts": "latest",
|
||||
"regenerator-runtime": "latest",
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tdesign-mobile-react": "^0.16.0",
|
||||
"vaul": "^0.9.6",
|
||||
"web-vitals": "^2.1.4",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.24.1"
|
||||
"react-window": "^1.8.11",
|
||||
"vconsole": "^3.15.1",
|
||||
"xmldom": "^0.6.0",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^4.9.5"
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^24.0.14",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-pxtorem": "^6.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.75.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^7.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"dev": "pnpm vite",
|
||||
"build": "pnpm vite build",
|
||||
"build:check": "tsc && pnpm vite build",
|
||||
"preview": "pnpm vite preview",
|
||||
"lint": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,scss,css}\"",
|
||||
"lint:check": "eslint src --ext .js,.jsx,.ts,.tsx",
|
||||
"format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,json,scss,css}\""
|
||||
}
|
||||
}
|
||||
|
||||
5261
Cunkebao/pnpm-lock.yaml
generated
Normal file
4
Cunkebao/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': set this to true or false
|
||||
core-js: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
@@ -1,6 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
plugins: {
|
||||
'postcss-pxtorem': {
|
||||
rootValue: 16,
|
||||
propList: ['*'],
|
||||
},
|
||||
},
|
||||
};
|
||||
BIN
Cunkebao/public/favicon-16.png
Normal file
|
After Width: | Height: | Size: 342 B |
BIN
Cunkebao/public/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
Cunkebao/public/logo-192.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
Cunkebao/public/logo-512.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
Cunkebao/public/logo.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"name": "存客宝",
|
||||
"short_name": "存客宝",
|
||||
"description": "存客宝私域管理系统",
|
||||
"theme_color": "#F5A623",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
@@ -8,18 +15,16 @@
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"src": "logo-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"src": "logo-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
308
Cunkebao/public/websdk.js
Normal file
@@ -0,0 +1,308 @@
|
||||
!(function (e, n) {
|
||||
"object" == typeof exports && "undefined" != typeof module
|
||||
? (module.exports = n())
|
||||
: "function" == typeof define && define.amd
|
||||
? define(n)
|
||||
: ((e = e || self).uni = n());
|
||||
})(this, function () {
|
||||
"use strict";
|
||||
try {
|
||||
var e = {};
|
||||
(Object.defineProperty(e, "passive", {
|
||||
get: function () {
|
||||
!0;
|
||||
},
|
||||
}),
|
||||
window.addEventListener("test-passive", null, e));
|
||||
} catch (e) {}
|
||||
var n = Object.prototype.hasOwnProperty;
|
||||
function i(e, i) {
|
||||
return n.call(e, i);
|
||||
}
|
||||
var t = [];
|
||||
function o() {
|
||||
return window.__dcloud_weex_postMessage || window.__dcloud_weex_;
|
||||
}
|
||||
function a() {
|
||||
return window.__uniapp_x_postMessage || window.__uniapp_x_;
|
||||
}
|
||||
var r = function (e, n) {
|
||||
var i = { options: { timestamp: +new Date() }, name: e, arg: n };
|
||||
if (a()) {
|
||||
if ("postMessage" === e) {
|
||||
var r = { data: n };
|
||||
return window.__uniapp_x_postMessage
|
||||
? window.__uniapp_x_postMessage(r)
|
||||
: window.__uniapp_x_.postMessage(JSON.stringify(r));
|
||||
}
|
||||
var d = {
|
||||
type: "WEB_INVOKE_APPSERVICE",
|
||||
args: { data: i, webviewIds: t },
|
||||
};
|
||||
window.__uniapp_x_postMessage
|
||||
? window.__uniapp_x_postMessageToService(d)
|
||||
: window.__uniapp_x_.postMessageToService(JSON.stringify(d));
|
||||
} else if (o()) {
|
||||
if ("postMessage" === e) {
|
||||
var s = { data: [n] };
|
||||
return window.__dcloud_weex_postMessage
|
||||
? window.__dcloud_weex_postMessage(s)
|
||||
: window.__dcloud_weex_.postMessage(JSON.stringify(s));
|
||||
}
|
||||
var w = {
|
||||
type: "WEB_INVOKE_APPSERVICE",
|
||||
args: { data: i, webviewIds: t },
|
||||
};
|
||||
window.__dcloud_weex_postMessage
|
||||
? window.__dcloud_weex_postMessageToService(w)
|
||||
: window.__dcloud_weex_.postMessageToService(JSON.stringify(w));
|
||||
} else {
|
||||
if (!window.plus)
|
||||
return window.parent.postMessage(
|
||||
{ type: "WEB_INVOKE_APPSERVICE", data: i, pageId: "" },
|
||||
"*",
|
||||
);
|
||||
if (0 === t.length) {
|
||||
var u = plus.webview.currentWebview();
|
||||
if (!u) throw new Error("plus.webview.currentWebview() is undefined");
|
||||
var g = u.parent(),
|
||||
v = "";
|
||||
((v = g ? g.id : u.id), t.push(v));
|
||||
}
|
||||
if (plus.webview.getWebviewById("__uniapp__service"))
|
||||
plus.webview.postMessageToUniNView(
|
||||
{ type: "WEB_INVOKE_APPSERVICE", args: { data: i, webviewIds: t } },
|
||||
"__uniapp__service",
|
||||
);
|
||||
else {
|
||||
var c = JSON.stringify(i);
|
||||
plus.webview
|
||||
.getLaunchWebview()
|
||||
.evalJS(
|
||||
'UniPlusBridge.subscribeHandler("'
|
||||
.concat("WEB_INVOKE_APPSERVICE", '",')
|
||||
.concat(c, ",")
|
||||
.concat(JSON.stringify(t), ");"),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
d = {
|
||||
navigateTo: function () {
|
||||
var e =
|
||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
||||
n = e.url;
|
||||
r("navigateTo", { url: encodeURI(n) });
|
||||
},
|
||||
navigateBack: function () {
|
||||
var e =
|
||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
||||
n = e.delta;
|
||||
r("navigateBack", { delta: parseInt(n) || 1 });
|
||||
},
|
||||
switchTab: function () {
|
||||
var e =
|
||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
||||
n = e.url;
|
||||
r("switchTab", { url: encodeURI(n) });
|
||||
},
|
||||
reLaunch: function () {
|
||||
var e =
|
||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
||||
n = e.url;
|
||||
r("reLaunch", { url: encodeURI(n) });
|
||||
},
|
||||
redirectTo: function () {
|
||||
var e =
|
||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
||||
n = e.url;
|
||||
r("redirectTo", { url: encodeURI(n) });
|
||||
},
|
||||
getEnv: function (e) {
|
||||
a()
|
||||
? e({ uvue: !0 })
|
||||
: o()
|
||||
? e({ nvue: !0 })
|
||||
: window.plus
|
||||
? e({ plus: !0 })
|
||||
: e({ h5: !0 });
|
||||
},
|
||||
postMessage: function () {
|
||||
var e =
|
||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {};
|
||||
r("postMessage", e.data || {});
|
||||
},
|
||||
},
|
||||
s = /uni-app/i.test(navigator.userAgent),
|
||||
w = /Html5Plus/i.test(navigator.userAgent),
|
||||
u = /complete|loaded|interactive/;
|
||||
var g =
|
||||
window.my &&
|
||||
navigator.userAgent.indexOf(
|
||||
["t", "n", "e", "i", "l", "C", "y", "a", "p", "i", "l", "A"]
|
||||
.reverse()
|
||||
.join(""),
|
||||
) > -1;
|
||||
var v =
|
||||
window.swan && window.swan.webView && /swan/i.test(navigator.userAgent);
|
||||
var c =
|
||||
window.qq &&
|
||||
window.qq.miniProgram &&
|
||||
/QQ/i.test(navigator.userAgent) &&
|
||||
/miniProgram/i.test(navigator.userAgent);
|
||||
var p =
|
||||
window.tt &&
|
||||
window.tt.miniProgram &&
|
||||
/toutiaomicroapp/i.test(navigator.userAgent);
|
||||
var _ =
|
||||
window.wx &&
|
||||
window.wx.miniProgram &&
|
||||
/micromessenger/i.test(navigator.userAgent) &&
|
||||
/miniProgram/i.test(navigator.userAgent);
|
||||
var m = window.qa && /quickapp/i.test(navigator.userAgent);
|
||||
var f =
|
||||
window.ks &&
|
||||
window.ks.miniProgram &&
|
||||
/micromessenger/i.test(navigator.userAgent) &&
|
||||
/miniProgram/i.test(navigator.userAgent);
|
||||
var l =
|
||||
window.tt &&
|
||||
window.tt.miniProgram &&
|
||||
/Lark|Feishu/i.test(navigator.userAgent);
|
||||
var E =
|
||||
window.jd && window.jd.miniProgram && /jdmp/i.test(navigator.userAgent);
|
||||
var x =
|
||||
window.xhs &&
|
||||
window.xhs.miniProgram &&
|
||||
/xhsminiapp/i.test(navigator.userAgent);
|
||||
for (
|
||||
var S,
|
||||
h = function () {
|
||||
((window.UniAppJSBridge = !0),
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("UniAppJSBridgeReady", {
|
||||
bubbles: !0,
|
||||
cancelable: !0,
|
||||
}),
|
||||
));
|
||||
},
|
||||
y = [
|
||||
function (e) {
|
||||
if (s || w)
|
||||
return (
|
||||
window.__uniapp_x_postMessage ||
|
||||
window.__uniapp_x_ ||
|
||||
window.__dcloud_weex_postMessage ||
|
||||
window.__dcloud_weex_
|
||||
? document.addEventListener("DOMContentLoaded", e)
|
||||
: window.plus && u.test(document.readyState)
|
||||
? setTimeout(e, 0)
|
||||
: document.addEventListener("plusready", e),
|
||||
d
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
if (_)
|
||||
return (
|
||||
window.WeixinJSBridge && window.WeixinJSBridge.invoke
|
||||
? setTimeout(e, 0)
|
||||
: document.addEventListener("WeixinJSBridgeReady", e),
|
||||
window.wx.miniProgram
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
if (c)
|
||||
return (
|
||||
window.QQJSBridge && window.QQJSBridge.invoke
|
||||
? setTimeout(e, 0)
|
||||
: document.addEventListener("QQJSBridgeReady", e),
|
||||
window.qq.miniProgram
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
if (g) {
|
||||
document.addEventListener("DOMContentLoaded", e);
|
||||
var n = window.my;
|
||||
return {
|
||||
navigateTo: n.navigateTo,
|
||||
navigateBack: n.navigateBack,
|
||||
switchTab: n.switchTab,
|
||||
reLaunch: n.reLaunch,
|
||||
redirectTo: n.redirectTo,
|
||||
postMessage: n.postMessage,
|
||||
getEnv: n.getEnv,
|
||||
};
|
||||
}
|
||||
},
|
||||
function (e) {
|
||||
if (v)
|
||||
return (
|
||||
document.addEventListener("DOMContentLoaded", e),
|
||||
window.swan.webView
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
if (p)
|
||||
return (
|
||||
document.addEventListener("DOMContentLoaded", e),
|
||||
window.tt.miniProgram
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
if (m) {
|
||||
window.QaJSBridge && window.QaJSBridge.invoke
|
||||
? setTimeout(e, 0)
|
||||
: document.addEventListener("QaJSBridgeReady", e);
|
||||
var n = window.qa;
|
||||
return {
|
||||
navigateTo: n.navigateTo,
|
||||
navigateBack: n.navigateBack,
|
||||
switchTab: n.switchTab,
|
||||
reLaunch: n.reLaunch,
|
||||
redirectTo: n.redirectTo,
|
||||
postMessage: n.postMessage,
|
||||
getEnv: n.getEnv,
|
||||
};
|
||||
}
|
||||
},
|
||||
function (e) {
|
||||
if (f)
|
||||
return (
|
||||
window.WeixinJSBridge && window.WeixinJSBridge.invoke
|
||||
? setTimeout(e, 0)
|
||||
: document.addEventListener("WeixinJSBridgeReady", e),
|
||||
window.ks.miniProgram
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
if (l)
|
||||
return (
|
||||
document.addEventListener("DOMContentLoaded", e),
|
||||
window.tt.miniProgram
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
if (E)
|
||||
return (
|
||||
window.JDJSBridgeReady && window.JDJSBridgeReady.invoke
|
||||
? setTimeout(e, 0)
|
||||
: document.addEventListener("JDJSBridgeReady", e),
|
||||
window.jd.miniProgram
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
if (x) return window.xhs.miniProgram;
|
||||
},
|
||||
function (e) {
|
||||
return (document.addEventListener("DOMContentLoaded", e), d);
|
||||
},
|
||||
],
|
||||
M = 0;
|
||||
M < y.length && !(S = y[M](h));
|
||||
M++
|
||||
);
|
||||
S || (S = {});
|
||||
var P = "undefined" != typeof uni ? uni : {};
|
||||
if (!P.navigateTo) for (var b in S) i(S, b) && (P[b] = S[b]);
|
||||
return ((P.webView = S), P);
|
||||
});
|
||||
BIN
Cunkebao/public/xiaobao-ai.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
@@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,190 +1,48 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { AuthProvider } from "./contexts/AuthContext";
|
||||
import { WechatAccountProvider } from "./contexts/WechatAccountContext";
|
||||
import ProtectedRoute from "./components/ProtectedRoute";
|
||||
import LayoutWrapper from "./components/LayoutWrapper";
|
||||
import { initInterceptors } from "./api";
|
||||
import Home from "./pages/Home";
|
||||
import Login from "./pages/login/Login";
|
||||
import Devices from "./pages/devices/Devices";
|
||||
import DeviceDetail from "./pages/devices/DeviceDetail";
|
||||
import WechatAccounts from "./pages/wechat-accounts/WechatAccounts";
|
||||
import WechatAccountDetail from "./pages/wechat-accounts/WechatAccountDetail";
|
||||
import Workspace from "./pages/workspace/Workspace";
|
||||
import AutoLike from "./pages/workspace/auto-like/AutoLike";
|
||||
import NewAutoLike from "./pages/workspace/auto-like/NewAutoLike";
|
||||
import AutoLikeDetail from "./pages/workspace/auto-like/AutoLikeDetail";
|
||||
import NewDistribution from "./pages/workspace/traffic-distribution/NewDistribution";
|
||||
import AutoGroup from "./pages/workspace/auto-group/AutoGroup";
|
||||
import AutoGroupDetail from "./pages/workspace/auto-group/Detail";
|
||||
import GroupPush from "./pages/workspace/group-push/GroupPush";
|
||||
import MomentsSync from "./pages/workspace/moments-sync/MomentsSync";
|
||||
import MomentsSyncDetail from "./pages/workspace/moments-sync/Detail";
|
||||
import NewMomentsSync from "./pages/workspace/moments-sync/new";
|
||||
import AIAssistant from "./pages/workspace/ai-assistant/AIAssistant";
|
||||
import TrafficDistribution from "./pages/workspace/traffic-distribution/TrafficDistribution";
|
||||
import TrafficDistributionDetail from "./pages/workspace/traffic-distribution/Detail";
|
||||
import Scenarios from "./pages/scenarios/Scenarios";
|
||||
import NewPlan from "./pages/scenarios/new/page";
|
||||
import ScenarioList from "./pages/scenarios/ScenarioList";
|
||||
import Profile from "./pages/profile/Profile";
|
||||
import Plans from "./pages/plans/Plans";
|
||||
import PlanDetail from "./pages/plans/PlanDetail";
|
||||
import Orders from "./pages/orders/Orders";
|
||||
import TrafficPool from "./pages/traffic-pool/TrafficPool";
|
||||
import ContactImport from "./pages/contact-import/ContactImport";
|
||||
import Content from "./pages/content/Content";
|
||||
import TrafficPoolDetail from "./pages/traffic-pool/TrafficPoolDetail";
|
||||
import NewContent from "./pages/content/NewContent";
|
||||
import Materials from "./pages/content/materials/List";
|
||||
import MaterialsNew from "./pages/content/materials/New";
|
||||
import NewGroupPush from './pages/workspace/group-push/new';
|
||||
// 占位导入(如未实现可后续补充)
|
||||
// import GroupPushDetail from './pages/workspace/group-push/GroupPushDetail';
|
||||
// import EditGroupPush from './pages/workspace/group-push/EditGroupPush';
|
||||
// import NewAutoGroup from './pages/workspace/auto-group/NewAutoGroup';
|
||||
// import EditAutoGroup from './pages/workspace/auto-group/EditAutoGroup';
|
||||
import React from "react";
|
||||
import AppRouter from "@/router";
|
||||
import UpdateNotification from "@/components/UpdateNotification";
|
||||
import { prefetchHomeOverview } from "@/store/module/homeOverview";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
|
||||
function App() {
|
||||
// 初始化HTTP拦截器
|
||||
useEffect(() => {
|
||||
const cleanup = initInterceptors();
|
||||
return cleanup;
|
||||
React.useEffect(() => {
|
||||
const syncViewport = () => {
|
||||
const h = window.visualViewport?.height ?? window.innerHeight;
|
||||
document.documentElement.style.setProperty("--real-vh", `${h * 0.01}px`);
|
||||
};
|
||||
syncViewport();
|
||||
window.addEventListener("resize", syncViewport);
|
||||
window.visualViewport?.addEventListener("resize", syncViewport);
|
||||
window.visualViewport?.addEventListener("scroll", syncViewport);
|
||||
return () => {
|
||||
window.removeEventListener("resize", syncViewport);
|
||||
window.visualViewport?.removeEventListener("resize", syncViewport);
|
||||
window.visualViewport?.removeEventListener("scroll", syncViewport);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
prefetchHomeOverview();
|
||||
}
|
||||
return useUserStore.subscribe((state, prev) => {
|
||||
if (state.isLoggedIn && !prev.isLoggedIn) {
|
||||
prefetchHomeOverview();
|
||||
}
|
||||
if (!state.isLoggedIn && prev.isLoggedIn) {
|
||||
import("@/store/module/homeOverview").then(m =>
|
||||
m.useHomeOverviewStore.getState().clear(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BrowserRouter
|
||||
future={{ v7_startTransition: true, v7_relativeSplatPath: true }}
|
||||
>
|
||||
<AuthProvider>
|
||||
<WechatAccountProvider>
|
||||
<ProtectedRoute>
|
||||
<LayoutWrapper>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/devices" element={<Devices />} />
|
||||
<Route path="/devices/:id" element={<DeviceDetail />} />
|
||||
<Route path="/wechat-accounts" element={<WechatAccounts />} />
|
||||
<Route
|
||||
path="/wechat-accounts/:id"
|
||||
element={<WechatAccountDetail />}
|
||||
/>
|
||||
<Route path="/workspace" element={<Workspace />} />
|
||||
<Route path="/workspace/auto-like" element={<AutoLike />} />
|
||||
<Route
|
||||
path="/workspace/auto-like/new"
|
||||
element={<NewAutoLike />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/auto-like/:id"
|
||||
element={<AutoLikeDetail />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/auto-like/:id/edit"
|
||||
element={<NewAutoLike />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/traffic-distribution"
|
||||
element={<TrafficDistribution />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/traffic-distribution/new"
|
||||
element={<NewDistribution />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/traffic-distribution/edit/:id"
|
||||
element={<NewDistribution />}
|
||||
/>
|
||||
<Route path="/workspace/auto-group" element={<AutoGroup />} />
|
||||
<Route
|
||||
path="/workspace/auto-group/:id"
|
||||
element={<AutoGroupDetail />}
|
||||
/>
|
||||
<Route path="/workspace/group-push" element={<GroupPush />} />
|
||||
<Route
|
||||
path="/workspace/group-push/new"
|
||||
element={<NewGroupPush />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/group-push/:id"
|
||||
element={<div>群发推送详情页(待实现GroupPushDetail组件)</div>}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/group-push/:id/edit"
|
||||
element={<div>编辑群发推送任务页(待实现EditGroupPush组件)</div>}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/moments-sync"
|
||||
element={<MomentsSync />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/moments-sync/new"
|
||||
element={<NewMomentsSync />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/moments-sync/:id"
|
||||
element={<MomentsSyncDetail />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/moments-sync/edit/:id"
|
||||
element={<NewMomentsSync />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/ai-assistant"
|
||||
element={<AIAssistant />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/traffic-distribution"
|
||||
element={<TrafficDistribution />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/traffic-distribution/:id"
|
||||
element={<TrafficDistributionDetail />}
|
||||
/>
|
||||
{/* 场景计划开始 */}
|
||||
<Route path="/scenarios" element={<Scenarios />} />
|
||||
<Route path="/scenarios/new" element={<NewPlan />} />
|
||||
<Route
|
||||
path="/scenarios/new/:scenarioId"
|
||||
element={<NewPlan />}
|
||||
/>
|
||||
<Route path="/scenarios/edit/:planId" element={<NewPlan />} />
|
||||
<Route
|
||||
path="/scenarios/list/:scenarioId/:scenarioName"
|
||||
element={<ScenarioList />}
|
||||
/>
|
||||
{/* 场景计划结束 */}
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/plans" element={<Plans />} />
|
||||
<Route path="/plans/:planId" element={<PlanDetail />} />
|
||||
<Route path="/orders" element={<Orders />} />
|
||||
<Route path="/traffic-pool" element={<TrafficPool />} />
|
||||
<Route
|
||||
path="/traffic-pool/:id"
|
||||
element={<TrafficPoolDetail />}
|
||||
/>
|
||||
<Route path="/contact-import" element={<ContactImport />} />
|
||||
<Route path="/content" element={<Content />} />
|
||||
<Route path="/content/new" element={<NewContent />} />
|
||||
<Route path="/content/edit/:id" element={<NewContent />} />
|
||||
<Route path="/content/materials/:id" element={<Materials />} />
|
||||
<Route
|
||||
path="/content/materials/new/:id"
|
||||
element={<MaterialsNew />}
|
||||
/>
|
||||
<Route
|
||||
path="/content/materials/edit/:id/:materialId"
|
||||
element={<MaterialsNew />}
|
||||
/>
|
||||
{/* 你可以继续添加更多路由 */}
|
||||
</Routes>
|
||||
</LayoutWrapper>
|
||||
</ProtectedRoute>
|
||||
</WechatAccountProvider>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
<>
|
||||
<AppRouter />
|
||||
<UpdateNotification position="top" autoReload={false} showToast={true} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
350
Cunkebao/src/android-polyfills.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
// Android 专用 polyfill - 解决Android 7等低版本系统的兼容性问题
|
||||
|
||||
// 检测是否为Android设备
|
||||
const isAndroid = () => {
|
||||
return /Android/i.test(navigator.userAgent);
|
||||
};
|
||||
|
||||
// 检测Android版本
|
||||
const getAndroidVersion = () => {
|
||||
const match = navigator.userAgent.match(/Android\s+(\d+)/);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
};
|
||||
|
||||
// 检测是否为低版本Android
|
||||
const isLowVersionAndroid = () => {
|
||||
const version = getAndroidVersion();
|
||||
return version <= 7; // Android 7及以下版本
|
||||
};
|
||||
|
||||
// 只在Android设备上执行polyfill
|
||||
if (isAndroid() && isLowVersionAndroid()) {
|
||||
console.log("检测到低版本Android系统,启用兼容性polyfill");
|
||||
|
||||
// 修复Array.prototype.includes在Android WebView中的问题
|
||||
if (!Array.prototype.includes) {
|
||||
Array.prototype.includes = function (searchElement, fromIndex) {
|
||||
if (this == null) {
|
||||
throw new TypeError('"this" is null or not defined');
|
||||
}
|
||||
var o = Object(this);
|
||||
var len = o.length >>> 0;
|
||||
if (len === 0) {
|
||||
return false;
|
||||
}
|
||||
var n = fromIndex | 0;
|
||||
var k = Math.max(n >= 0 ? n : len + n, 0);
|
||||
while (k < len) {
|
||||
if (o[k] === searchElement) {
|
||||
return true;
|
||||
}
|
||||
k++;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// 修复String.prototype.includes在Android WebView中的问题
|
||||
if (!String.prototype.includes) {
|
||||
String.prototype.includes = function (search, start) {
|
||||
if (typeof start !== "number") {
|
||||
start = 0;
|
||||
}
|
||||
if (start + search.length > this.length) {
|
||||
return false;
|
||||
} else {
|
||||
return this.indexOf(search, start) !== -1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 修复String.prototype.startsWith在Android WebView中的问题
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function (searchString, position) {
|
||||
position = position || 0;
|
||||
return this.substr(position, searchString.length) === searchString;
|
||||
};
|
||||
}
|
||||
|
||||
// 修复String.prototype.endsWith在Android WebView中的问题
|
||||
if (!String.prototype.endsWith) {
|
||||
String.prototype.endsWith = function (searchString, length) {
|
||||
if (length === undefined || length > this.length) {
|
||||
length = this.length;
|
||||
}
|
||||
return (
|
||||
this.substring(length - searchString.length, length) === searchString
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 修复Array.prototype.find在Android WebView中的问题
|
||||
if (!Array.prototype.find) {
|
||||
Array.prototype.find = function (predicate) {
|
||||
if (this == null) {
|
||||
throw new TypeError("Array.prototype.find called on null or undefined");
|
||||
}
|
||||
if (typeof predicate !== "function") {
|
||||
throw new TypeError("predicate must be a function");
|
||||
}
|
||||
var list = Object(this);
|
||||
var length = parseInt(list.length) || 0;
|
||||
var thisArg = arguments[1];
|
||||
for (var i = 0; i < length; i++) {
|
||||
var element = list[i];
|
||||
if (predicate.call(thisArg, element, i, list)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
// 修复Array.prototype.findIndex在Android WebView中的问题
|
||||
if (!Array.prototype.findIndex) {
|
||||
Array.prototype.findIndex = function (predicate) {
|
||||
if (this == null) {
|
||||
throw new TypeError(
|
||||
"Array.prototype.findIndex called on null or undefined",
|
||||
);
|
||||
}
|
||||
if (typeof predicate !== "function") {
|
||||
throw new TypeError("predicate must be a function");
|
||||
}
|
||||
var list = Object(this);
|
||||
var length = parseInt(list.length) || 0;
|
||||
var thisArg = arguments[1];
|
||||
for (var i = 0; i < length; i++) {
|
||||
var element = list[i];
|
||||
if (predicate.call(thisArg, element, i, list)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
// 修复Object.assign在Android WebView中的问题
|
||||
if (typeof Object.assign !== "function") {
|
||||
Object.assign = function (target) {
|
||||
if (target == null) {
|
||||
throw new TypeError("Cannot convert undefined or null to object");
|
||||
}
|
||||
var to = Object(target);
|
||||
for (var index = 1; index < arguments.length; index++) {
|
||||
var nextSource = arguments[index];
|
||||
if (nextSource != null) {
|
||||
for (var nextKey in nextSource) {
|
||||
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
|
||||
to[nextKey] = nextSource[nextKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return to;
|
||||
};
|
||||
}
|
||||
|
||||
// 修复Array.from在Android WebView中的问题
|
||||
if (!Array.from) {
|
||||
Array.from = (function () {
|
||||
var toStr = Object.prototype.toString;
|
||||
var isCallable = function (fn) {
|
||||
return (
|
||||
typeof fn === "function" || toStr.call(fn) === "[object Function]"
|
||||
);
|
||||
};
|
||||
var toInteger = function (value) {
|
||||
var number = Number(value);
|
||||
if (isNaN(number)) {
|
||||
return 0;
|
||||
}
|
||||
if (number === 0 || !isFinite(number)) {
|
||||
return number;
|
||||
}
|
||||
return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
|
||||
};
|
||||
var maxSafeInteger = Math.pow(2, 53) - 1;
|
||||
var toLength = function (value) {
|
||||
var len = toInteger(value);
|
||||
return Math.min(Math.max(len, 0), maxSafeInteger);
|
||||
};
|
||||
return function from(arrayLike) {
|
||||
var C = this;
|
||||
var items = Object(arrayLike);
|
||||
if (arrayLike == null) {
|
||||
throw new TypeError(
|
||||
"Array.from requires an array-like object - not null or undefined",
|
||||
);
|
||||
}
|
||||
var mapFunction = arguments.length > 1 ? arguments[1] : void undefined;
|
||||
var T;
|
||||
if (typeof mapFunction !== "undefined") {
|
||||
if (typeof mapFunction !== "function") {
|
||||
throw new TypeError(
|
||||
"Array.from: when provided, the second argument must be a function",
|
||||
);
|
||||
}
|
||||
if (arguments.length > 2) {
|
||||
T = arguments[2];
|
||||
}
|
||||
}
|
||||
var len = toLength(items.length);
|
||||
var A = isCallable(C) ? Object(new C(len)) : new Array(len);
|
||||
var k = 0;
|
||||
var kValue;
|
||||
while (k < len) {
|
||||
kValue = items[k];
|
||||
if (mapFunction) {
|
||||
A[k] =
|
||||
typeof T === "undefined"
|
||||
? mapFunction(kValue, k)
|
||||
: mapFunction.call(T, kValue, k);
|
||||
} else {
|
||||
A[k] = kValue;
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
A.length = len;
|
||||
return A;
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
||||
// 修复requestAnimationFrame在Android WebView中的问题
|
||||
if (!window.requestAnimationFrame) {
|
||||
window.requestAnimationFrame = function (callback) {
|
||||
return setTimeout(function () {
|
||||
callback(Date.now());
|
||||
}, 1000 / 60);
|
||||
} as unknown as typeof window.requestAnimationFrame;
|
||||
}
|
||||
|
||||
if (!window.cancelAnimationFrame) {
|
||||
window.cancelAnimationFrame = function (id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
|
||||
// 修复IntersectionObserver在Android WebView中的问题
|
||||
if (!window.IntersectionObserver) {
|
||||
window.IntersectionObserver = function (callback, options) {
|
||||
this.callback = callback;
|
||||
this.options = options || {};
|
||||
this.observers = [];
|
||||
|
||||
this.observe = function (element) {
|
||||
this.observers.push(element);
|
||||
// 简单的实现,实际项目中可能需要更复杂的逻辑
|
||||
setTimeout(() => {
|
||||
this.callback([
|
||||
{
|
||||
target: element,
|
||||
isIntersecting: true,
|
||||
intersectionRatio: 1,
|
||||
},
|
||||
]);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
this.unobserve = function (element) {
|
||||
var index = this.observers.indexOf(element);
|
||||
if (index > -1) {
|
||||
this.observers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
this.disconnect = function () {
|
||||
this.observers = [];
|
||||
};
|
||||
} as unknown as typeof window.IntersectionObserver;
|
||||
}
|
||||
|
||||
// 修复ResizeObserver在Android WebView中的问题
|
||||
if (!window.ResizeObserver) {
|
||||
window.ResizeObserver = function (callback) {
|
||||
this.callback = callback;
|
||||
this.observers = [];
|
||||
|
||||
this.observe = function (element) {
|
||||
this.observers.push(element);
|
||||
};
|
||||
|
||||
this.unobserve = function (element) {
|
||||
var index = this.observers.indexOf(element);
|
||||
if (index > -1) {
|
||||
this.observers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
this.disconnect = function () {
|
||||
this.observers = [];
|
||||
};
|
||||
} as unknown as typeof window.ResizeObserver;
|
||||
}
|
||||
|
||||
// 修复URLSearchParams在Android WebView中的问题
|
||||
if (!window.URLSearchParams) {
|
||||
window.URLSearchParams = function (init) {
|
||||
this.params = {};
|
||||
|
||||
if (init) {
|
||||
if (typeof init === "string") {
|
||||
if (init.charAt(0) === "?") {
|
||||
init = init.slice(1);
|
||||
}
|
||||
var pairs = init.split("&");
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var pair = pairs[i].split("=");
|
||||
var key = decodeURIComponent(pair[0]);
|
||||
var value = decodeURIComponent(pair[1] || "");
|
||||
this.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.append = function (name, value) {
|
||||
if (!this.params[name]) {
|
||||
this.params[name] = [];
|
||||
}
|
||||
this.params[name].push(value);
|
||||
};
|
||||
|
||||
this.get = function (name) {
|
||||
return this.params[name] ? this.params[name][0] : null;
|
||||
};
|
||||
|
||||
this.getAll = function (name) {
|
||||
return this.params[name] || [];
|
||||
};
|
||||
|
||||
this.has = function (name) {
|
||||
return !!this.params[name];
|
||||
};
|
||||
|
||||
this.set = function (name, value) {
|
||||
this.params[name] = [value];
|
||||
};
|
||||
|
||||
this.delete = function (name) {
|
||||
delete this.params[name];
|
||||
};
|
||||
|
||||
this.toString = function () {
|
||||
var pairs = [];
|
||||
for (var key in this.params) {
|
||||
if (this.params.hasOwnProperty(key)) {
|
||||
for (var i = 0; i < this.params[key].length; i++) {
|
||||
pairs.push(
|
||||
encodeURIComponent(key) +
|
||||
"=" +
|
||||
encodeURIComponent(this.params[key][i]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return pairs.join("&");
|
||||
};
|
||||
} as unknown as typeof window.URLSearchParams;
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { request } from './request';
|
||||
import type { ApiResponse } from '@/types/common';
|
||||
|
||||
// 登录响应数据类型
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
token_expired: string;
|
||||
member: {
|
||||
id: number;
|
||||
username: string;
|
||||
account: string;
|
||||
avatar?: string;
|
||||
s2_accountId: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 验证码响应类型
|
||||
export interface VerificationCodeResponse {
|
||||
code: string;
|
||||
expire_time: string;
|
||||
}
|
||||
|
||||
// 认证相关API
|
||||
export const authApi = {
|
||||
// 账号密码登录
|
||||
login: async (account: string, password: string) => {
|
||||
const response = await request.post<ApiResponse<LoginResponse>>('/v1/auth/login', {
|
||||
account,
|
||||
password,
|
||||
typeId: 1 // 默认使用用户类型1
|
||||
});
|
||||
return response as unknown as ApiResponse<LoginResponse>;
|
||||
},
|
||||
|
||||
// 验证码登录
|
||||
loginWithCode: async (account: string, code: string) => {
|
||||
const response = await request.post<ApiResponse<LoginResponse>>('/v1/auth/login/code', {
|
||||
account,
|
||||
code,
|
||||
typeId: 1
|
||||
});
|
||||
return response as unknown as ApiResponse<LoginResponse>;
|
||||
},
|
||||
|
||||
// 发送验证码
|
||||
sendVerificationCode: async (account: string) => {
|
||||
const response = await request.post<ApiResponse<VerificationCodeResponse>>('/v1/auth/send-code', {
|
||||
account,
|
||||
type: 'login' // 登录验证码
|
||||
});
|
||||
return response as unknown as ApiResponse<VerificationCodeResponse>;
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo: async () => {
|
||||
const response = await request.get<ApiResponse<any>>('/v1/auth/info');
|
||||
return response as unknown as ApiResponse<any>;
|
||||
},
|
||||
|
||||
// 刷新Token
|
||||
refreshToken: async () => {
|
||||
const response = await request.post<ApiResponse<{ token: string; token_expired: string }>>('/v1/auth/refresh', {});
|
||||
return response as unknown as ApiResponse<{ token: string; token_expired: string }>;
|
||||
},
|
||||
|
||||
// 微信登录
|
||||
wechatLogin: async (code: string) => {
|
||||
const response = await request.post<ApiResponse<LoginResponse>>('/v1/auth/wechat', {
|
||||
code
|
||||
});
|
||||
return response as unknown as ApiResponse<LoginResponse>;
|
||||
},
|
||||
|
||||
// Apple登录
|
||||
appleLogin: async (identityToken: string, authorizationCode: string) => {
|
||||
const response = await request.post<ApiResponse<LoginResponse>>('/v1/auth/apple', {
|
||||
identity_token: identityToken,
|
||||
authorization_code: authorizationCode
|
||||
});
|
||||
return response as unknown as ApiResponse<LoginResponse>;
|
||||
},
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import { get, post, del } from './request';
|
||||
import {
|
||||
LikeTask,
|
||||
CreateLikeTaskData,
|
||||
UpdateLikeTaskData,
|
||||
LikeRecord,
|
||||
ApiResponse,
|
||||
PaginatedResponse
|
||||
} from '@/types/auto-like';
|
||||
|
||||
// 获取自动点赞任务列表
|
||||
export async function fetchAutoLikeTasks(): Promise<LikeTask[]> {
|
||||
try {
|
||||
const res = await get<ApiResponse<PaginatedResponse<LikeTask>>>('/v1/workbench/list?type=1&page=1&limit=100');
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data.list || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('获取自动点赞任务失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个任务详情
|
||||
export async function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
|
||||
try {
|
||||
console.log(`Fetching task detail for id: ${id}`);
|
||||
// 使用any类型来处理可能的不同响应结构
|
||||
const res = await get<any>(`/v1/workbench/detail?id=${id}`);
|
||||
console.log('Task detail API response:', res);
|
||||
|
||||
if (res.code === 200) {
|
||||
// 检查响应中的data字段
|
||||
if (res.data) {
|
||||
// 如果data是对象,直接返回
|
||||
if (typeof res.data === 'object') {
|
||||
return res.data;
|
||||
} else {
|
||||
console.error('Task detail API response data is not an object:', res.data);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
console.error('Task detail API response missing data field:', res);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Task detail API error:', res.msg || 'Unknown error');
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建自动点赞任务
|
||||
export async function createAutoLikeTask(data: CreateLikeTaskData): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/create', {
|
||||
...data,
|
||||
type: 1 // 自动点赞类型
|
||||
});
|
||||
}
|
||||
|
||||
// 更新自动点赞任务
|
||||
export async function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/update', {
|
||||
...data,
|
||||
type: 1 // 自动点赞类型
|
||||
});
|
||||
}
|
||||
|
||||
// 删除自动点赞任务
|
||||
export async function deleteAutoLikeTask(id: string): Promise<ApiResponse> {
|
||||
return del('/v1/workbench/delete', { params: { id } });
|
||||
}
|
||||
|
||||
// 切换任务状态
|
||||
export async function toggleAutoLikeTask(id: string, status: string): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/update-status', { id, status });
|
||||
}
|
||||
|
||||
// 复制自动点赞任务
|
||||
export async function copyAutoLikeTask(id: string): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/copy', { id });
|
||||
}
|
||||
|
||||
// 获取点赞记录
|
||||
export async function fetchLikeRecords(
|
||||
workbenchId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
keyword?: string
|
||||
): Promise<PaginatedResponse<LikeRecord>> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
workbenchId,
|
||||
page: page.toString(),
|
||||
limit: limit.toString()
|
||||
});
|
||||
|
||||
if (keyword) {
|
||||
params.append('keyword', keyword);
|
||||
}
|
||||
|
||||
const res = await get<ApiResponse<PaginatedResponse<LikeRecord>>>(`/v1/workbench/like-records?${params.toString()}`);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return { list: [], total: 0, page, limit };
|
||||
} catch (error) {
|
||||
console.error('获取点赞记录失败:', error);
|
||||
return { list: [], total: 0, page, limit };
|
||||
}
|
||||
}
|
||||
|
||||
export type { LikeTask, LikeRecord, CreateLikeTaskData };
|
||||
30
Cunkebao/src/api/common.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
/**
|
||||
* 通用文件上传(走统一 request 拦截器:401 跳转登录、code/msg 解析)
|
||||
*/
|
||||
export async function uploadFile(
|
||||
file: File,
|
||||
uploadUrl: string = "/v1/attachment/upload",
|
||||
): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const data = await request(
|
||||
uploadUrl,
|
||||
formData,
|
||||
"POST",
|
||||
{
|
||||
skipErrorToast: true,
|
||||
timeout: 30000,
|
||||
} as Parameters<typeof request>[3],
|
||||
0,
|
||||
);
|
||||
|
||||
if (typeof data === "string" && data) return data;
|
||||
const url = data?.url || data?.source || "";
|
||||
if (!url) {
|
||||
throw new Error("上传失败:未返回文件地址");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { get, post, put, del } from './request';
|
||||
import type { ApiResponse, PaginatedResponse } from '@/types/common';
|
||||
|
||||
// 内容库类型定义
|
||||
export interface ContentLibrary {
|
||||
id: string;
|
||||
name: string;
|
||||
sourceType: number;
|
||||
creatorName: string;
|
||||
updateTime: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
// 内容库列表响应
|
||||
export interface ContentLibraryListResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: {
|
||||
list: ContentLibrary[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 获取内容库列表
|
||||
export const fetchContentLibraryList = async (
|
||||
page: number = 1,
|
||||
limit: number = 100,
|
||||
keyword?: string
|
||||
): Promise<ContentLibraryListResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page.toString());
|
||||
params.append('limit', limit.toString());
|
||||
|
||||
if (keyword) {
|
||||
params.append('keyword', keyword);
|
||||
}
|
||||
|
||||
return get<ContentLibraryListResponse>(`/v1/content/library/list?${params.toString()}`);
|
||||
};
|
||||
|
||||
// 内容库API对象
|
||||
export const contentLibraryApi = {
|
||||
// 获取内容库列表
|
||||
async getList(page: number = 1, limit: number = 100, keyword?: string): Promise<ContentLibraryListResponse> {
|
||||
return fetchContentLibraryList(page, limit, keyword);
|
||||
},
|
||||
|
||||
// 创建内容库
|
||||
async create(params: { name: string; sourceType: number }): Promise<ApiResponse<ContentLibrary>> {
|
||||
return post<ApiResponse<ContentLibrary>>('/v1/content/library', params);
|
||||
},
|
||||
|
||||
// 更新内容库
|
||||
async update(id: string, params: Partial<ContentLibrary>): Promise<ApiResponse<ContentLibrary>> {
|
||||
return put<ApiResponse<ContentLibrary>>(`/v1/content/library/${id}`, params);
|
||||
},
|
||||
|
||||
// 删除内容库
|
||||
async delete(id: string): Promise<ApiResponse<void>> {
|
||||
return del<ApiResponse<void>>(`/v1/content/library/${id}`);
|
||||
},
|
||||
|
||||
// 获取内容库详情
|
||||
async getById(id: string): Promise<ApiResponse<ContentLibrary>> {
|
||||
return get<ApiResponse<ContentLibrary>>(`/v1/content/library/${id}`);
|
||||
},
|
||||
};
|
||||
@@ -1,200 +0,0 @@
|
||||
import { get, post, put, del } from './request';
|
||||
import type { ApiResponse, PaginatedResponse } from '@/types/common';
|
||||
import type {
|
||||
Device,
|
||||
DeviceStats,
|
||||
DeviceTaskRecord,
|
||||
QueryDeviceParams,
|
||||
CreateDeviceParams,
|
||||
UpdateDeviceParams,
|
||||
DeviceStatus,
|
||||
ServerDevicesResponse
|
||||
} from '@/types/device';
|
||||
|
||||
const API_BASE = "/devices";
|
||||
|
||||
// 获取设备列表 - 连接到服务器/v1/devices接口
|
||||
export const fetchDeviceList = async (page: number = 1, limit: number = 20, keyword?: string): Promise<ServerDevicesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page.toString());
|
||||
params.append('limit', limit.toString());
|
||||
|
||||
if (keyword) {
|
||||
params.append('keyword', keyword);
|
||||
}
|
||||
|
||||
return get<ServerDevicesResponse>(`/v1/devices?${params.toString()}`);
|
||||
};
|
||||
|
||||
// 获取设备详情 - 连接到服务器/v1/devices/:id接口
|
||||
export const fetchDeviceDetail = async (id: string | number): Promise<ApiResponse<any>> => {
|
||||
return get<ApiResponse<any>>(`/v1/devices/${id}`);
|
||||
};
|
||||
|
||||
// 获取设备关联的微信账号
|
||||
export const fetchDeviceRelatedAccounts = async (id: string | number): Promise<ApiResponse<any>> => {
|
||||
return get<ApiResponse<any>>(`/v1/wechats/related-device/${id}`);
|
||||
};
|
||||
|
||||
// 获取设备操作记录
|
||||
export const fetchDeviceHandleLogs = async (id: string | number, page: number = 1, limit: number = 10): Promise<ApiResponse<any>> => {
|
||||
return get<ApiResponse<any>>(`/v1/devices/${id}/handle-logs?page=${page}&limit=${limit}`);
|
||||
};
|
||||
|
||||
// 更新设备任务配置
|
||||
export const updateDeviceTaskConfig = async (
|
||||
config: {
|
||||
deviceId: string | number;
|
||||
autoAddFriend?: boolean;
|
||||
autoReply?: boolean;
|
||||
momentsSync?: boolean;
|
||||
aiChat?: boolean;
|
||||
}
|
||||
): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>(`/v1/devices/task-config`, config);
|
||||
};
|
||||
|
||||
// 删除设备
|
||||
export const deleteDevice = async (id: number): Promise<ApiResponse<any>> => {
|
||||
return del<ApiResponse<any>>(`/v1/devices/${id}`);
|
||||
};
|
||||
|
||||
// 设备管理API
|
||||
export const devicesApi = {
|
||||
// 获取设备列表
|
||||
async getList(page: number = 1, limit: number = 20, keyword?: string): Promise<ServerDevicesResponse> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page.toString());
|
||||
params.append('limit', limit.toString());
|
||||
|
||||
if (keyword) {
|
||||
params.append('keyword', keyword);
|
||||
}
|
||||
|
||||
return get<ServerDevicesResponse>(`/v1/devices?${params.toString()}`);
|
||||
},
|
||||
|
||||
// 获取设备二维码
|
||||
async getQRCode(accountId: string): Promise<ApiResponse<{ qrCode: string }>> {
|
||||
return post<ApiResponse<{ qrCode: string }>>('/v1/api/device/add', { accountId });
|
||||
},
|
||||
|
||||
// 通过IMEI添加设备
|
||||
async addByImei(imei: string, name: string): Promise<ApiResponse<any>> {
|
||||
return post<ApiResponse<any>>('/v1/api/device/add-by-imei', { imei, name });
|
||||
},
|
||||
|
||||
// 创建设备
|
||||
async create(params: CreateDeviceParams): Promise<ApiResponse<Device>> {
|
||||
return post<ApiResponse<Device>>(`${API_BASE}`, params);
|
||||
},
|
||||
|
||||
// 更新设备
|
||||
async update(params: UpdateDeviceParams): Promise<ApiResponse<Device>> {
|
||||
return put<ApiResponse<Device>>(`${API_BASE}/${params.id}`, params);
|
||||
},
|
||||
|
||||
// 获取设备详情
|
||||
async getById(id: string): Promise<ApiResponse<Device>> {
|
||||
return get<ApiResponse<Device>>(`${API_BASE}/${id}`);
|
||||
},
|
||||
|
||||
// 查询设备列表
|
||||
async query(params: QueryDeviceParams): Promise<ApiResponse<PaginatedResponse<Device>>> {
|
||||
// 创建一个新对象,用于构建URLSearchParams
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
// 按需将params中的属性添加到queryParams
|
||||
if (params.keyword) queryParams.keyword = params.keyword;
|
||||
if (params.status) queryParams.status = params.status;
|
||||
if (params.type) queryParams.type = params.type;
|
||||
if (params.page) queryParams.page = params.page.toString();
|
||||
if (params.pageSize) queryParams.pageSize = params.pageSize.toString();
|
||||
|
||||
// 特殊处理需要JSON序列化的属性
|
||||
if (params.tags) queryParams.tags = JSON.stringify(params.tags);
|
||||
if (params.dateRange) queryParams.dateRange = JSON.stringify(params.dateRange);
|
||||
|
||||
// 构建查询字符串
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
return get<ApiResponse<PaginatedResponse<Device>>>(`${API_BASE}?${queryString}`);
|
||||
},
|
||||
|
||||
// 删除设备(旧版本)
|
||||
async deleteById(id: string): Promise<ApiResponse<void>> {
|
||||
return del<ApiResponse<void>>(`${API_BASE}/${id}`);
|
||||
},
|
||||
|
||||
// 删除设备(新版本)
|
||||
async delete(id: number): Promise<ApiResponse<any>> {
|
||||
return del<ApiResponse<any>>(`/v1/devices/${id}`);
|
||||
},
|
||||
|
||||
// 重启设备
|
||||
async restart(id: string): Promise<ApiResponse<void>> {
|
||||
return post<ApiResponse<void>>(`${API_BASE}/${id}/restart`);
|
||||
},
|
||||
|
||||
// 解绑设备
|
||||
async unbind(id: string): Promise<ApiResponse<void>> {
|
||||
return post<ApiResponse<void>>(`${API_BASE}/${id}/unbind`);
|
||||
},
|
||||
|
||||
// 获取设备统计数据
|
||||
async getStats(id: string): Promise<ApiResponse<DeviceStats>> {
|
||||
return get<ApiResponse<DeviceStats>>(`${API_BASE}/${id}/stats`);
|
||||
},
|
||||
|
||||
// 获取设备任务记录
|
||||
async getTaskRecords(id: string, page = 1, pageSize = 20): Promise<ApiResponse<PaginatedResponse<DeviceTaskRecord>>> {
|
||||
return get<ApiResponse<PaginatedResponse<DeviceTaskRecord>>>(`${API_BASE}/${id}/tasks?page=${page}&pageSize=${pageSize}`);
|
||||
},
|
||||
|
||||
// 批量更新设备标签
|
||||
async updateTags(ids: string[], tags: string[]): Promise<ApiResponse<void>> {
|
||||
return post<ApiResponse<void>>(`${API_BASE}/tags`, { deviceIds: ids, tags });
|
||||
},
|
||||
|
||||
// 批量导出设备数据
|
||||
async exportDevices(ids: string[]): Promise<Blob> {
|
||||
const response = await fetch(`${process.env.REACT_APP_API_BASE || 'http://localhost:3000/api'}${API_BASE}/export`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ deviceIds: ids }),
|
||||
});
|
||||
return response.blob();
|
||||
},
|
||||
|
||||
// 检查设备在线状态
|
||||
async checkStatus(ids: string[]): Promise<ApiResponse<Record<string, DeviceStatus>>> {
|
||||
return post<ApiResponse<Record<string, DeviceStatus>>>(`${API_BASE}/status`, { deviceIds: ids });
|
||||
},
|
||||
|
||||
// 获取设备关联的微信账号
|
||||
async getRelatedAccounts(id: string | number): Promise<ApiResponse<any>> {
|
||||
return get<ApiResponse<any>>(`/v1/wechats/related-device/${id}`);
|
||||
},
|
||||
|
||||
// 获取设备操作记录
|
||||
async getHandleLogs(id: string | number, page: number = 1, limit: number = 10): Promise<ApiResponse<any>> {
|
||||
return get<ApiResponse<any>>(`/v1/devices/${id}/handle-logs?page=${page}&limit=${limit}`);
|
||||
},
|
||||
|
||||
// 更新设备任务配置
|
||||
async updateTaskConfig(config: {
|
||||
deviceId: string | number;
|
||||
autoAddFriend?: boolean;
|
||||
autoReply?: boolean;
|
||||
momentsSync?: boolean;
|
||||
aiChat?: boolean;
|
||||
}): Promise<ApiResponse<any>> {
|
||||
return post<ApiResponse<any>>(`/v1/devices/task-config`, config);
|
||||
},
|
||||
|
||||
// 获取设备任务配置
|
||||
async getTaskConfig(id: string | number): Promise<ApiResponse<any>> {
|
||||
return get<ApiResponse<any>>(`/v1/devices/${id}/task-config`);
|
||||
},
|
||||
};
|
||||
@@ -1,201 +0,0 @@
|
||||
import { get, post, put, del } from './request';
|
||||
|
||||
// 群发推送任务类型定义
|
||||
export interface GroupPushTask {
|
||||
id: string;
|
||||
name: string;
|
||||
status: number; // 1: 运行中, 2: 已暂停
|
||||
deviceCount: number;
|
||||
targetGroups: string[];
|
||||
pushCount: number;
|
||||
successCount: number;
|
||||
lastPushTime: string;
|
||||
createTime: string;
|
||||
creator: string;
|
||||
pushInterval: number;
|
||||
maxPushPerDay: number;
|
||||
timeRange: { start: string; end: string };
|
||||
messageType: 'text' | 'image' | 'video' | 'link';
|
||||
messageContent: string;
|
||||
targetTags: string[];
|
||||
pushMode: 'immediate' | 'scheduled';
|
||||
scheduledTime?: string;
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群发推送任务列表
|
||||
*/
|
||||
export async function fetchGroupPushTasks(): Promise<GroupPushTask[]> {
|
||||
try {
|
||||
const response = await get<ApiResponse<GroupPushTask[]>>('/v1/workspace/group-push/tasks');
|
||||
|
||||
if (response.code === 200 && Array.isArray(response.data)) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 如果API不可用,返回模拟数据
|
||||
return getMockGroupPushTasks();
|
||||
} catch (error) {
|
||||
console.error('获取群发推送任务失败:', error);
|
||||
// 返回模拟数据作为降级方案
|
||||
return getMockGroupPushTasks();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除群发推送任务
|
||||
*/
|
||||
export async function deleteGroupPushTask(id: string): Promise<ApiResponse> {
|
||||
try {
|
||||
const response = await del<ApiResponse>(`/v1/workspace/group-push/tasks/${id}`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('删除群发推送任务失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换群发推送任务状态
|
||||
*/
|
||||
export async function toggleGroupPushTask(id: string, status: string): Promise<ApiResponse> {
|
||||
try {
|
||||
const response = await post<ApiResponse>(`/v1/workspace/group-push/tasks/${id}/toggle`, {
|
||||
status
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('切换群发推送任务状态失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制群发推送任务
|
||||
*/
|
||||
export async function copyGroupPushTask(id: string): Promise<ApiResponse> {
|
||||
try {
|
||||
const response = await post<ApiResponse>(`/v1/workspace/group-push/tasks/${id}/copy`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('复制群发推送任务失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建群发推送任务
|
||||
*/
|
||||
export async function createGroupPushTask(taskData: Partial<GroupPushTask>): Promise<ApiResponse> {
|
||||
try {
|
||||
const response = await post<ApiResponse>('/v1/workspace/group-push/tasks', taskData);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('创建群发推送任务失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新群发推送任务
|
||||
*/
|
||||
export async function updateGroupPushTask(id: string, taskData: Partial<GroupPushTask>): Promise<ApiResponse> {
|
||||
try {
|
||||
const response = await put<ApiResponse>(`/v1/workspace/group-push/tasks/${id}`, taskData);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('更新群发推送任务失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群发推送任务详情
|
||||
*/
|
||||
export async function getGroupPushTaskDetail(id: string): Promise<GroupPushTask> {
|
||||
try {
|
||||
const response = await get<ApiResponse<GroupPushTask>>(`/v1/workspace/group-push/tasks/${id}`);
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw new Error(response.message || '获取任务详情失败');
|
||||
} catch (error) {
|
||||
console.error('获取群发推送任务详情失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟数据 - 当API不可用时使用
|
||||
*/
|
||||
function getMockGroupPushTasks(): GroupPushTask[] {
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
name: '产品推广群发',
|
||||
deviceCount: 2,
|
||||
targetGroups: ['VIP客户群', '潜在客户群'],
|
||||
pushCount: 156,
|
||||
successCount: 142,
|
||||
lastPushTime: '2025-02-06 13:12:35',
|
||||
createTime: '2024-11-20 19:04:14',
|
||||
creator: 'admin',
|
||||
status: 1, // 运行中
|
||||
pushInterval: 60,
|
||||
maxPushPerDay: 200,
|
||||
timeRange: { start: '09:00', end: '21:00' },
|
||||
messageType: 'text',
|
||||
messageContent: '新品上市,限时优惠!点击查看详情...',
|
||||
targetTags: ['VIP客户', '高意向'],
|
||||
pushMode: 'immediate',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '活动通知推送',
|
||||
deviceCount: 1,
|
||||
targetGroups: ['活动群', '推广群'],
|
||||
pushCount: 89,
|
||||
successCount: 78,
|
||||
lastPushTime: '2024-03-04 14:09:35',
|
||||
createTime: '2024-03-04 14:29:04',
|
||||
creator: 'manager',
|
||||
status: 2, // 已暂停
|
||||
pushInterval: 120,
|
||||
maxPushPerDay: 100,
|
||||
timeRange: { start: '10:00', end: '20:00' },
|
||||
messageType: 'image',
|
||||
messageContent: '活动海报.jpg',
|
||||
targetTags: ['活跃用户', '中意向'],
|
||||
pushMode: 'scheduled',
|
||||
scheduledTime: '2024-03-05 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '新客户欢迎消息',
|
||||
deviceCount: 3,
|
||||
targetGroups: ['新客户群', '体验群'],
|
||||
pushCount: 234,
|
||||
successCount: 218,
|
||||
lastPushTime: '2025-02-06 15:30:22',
|
||||
createTime: '2024-12-01 09:15:30',
|
||||
creator: 'admin',
|
||||
status: 1, // 运行中
|
||||
pushInterval: 30,
|
||||
maxPushPerDay: 300,
|
||||
timeRange: { start: '08:00', end: '22:00' },
|
||||
messageType: 'text',
|
||||
messageContent: '欢迎加入我们的大家庭!这里有最新的产品信息和优惠活动...',
|
||||
targetTags: ['新客户', '欢迎'],
|
||||
pushMode: 'immediate',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// 导出所有API相关的内容
|
||||
export * from './auth';
|
||||
export * from './utils';
|
||||
export * from './interceptors';
|
||||
export * from './request';
|
||||
|
||||
// 导出现有的API模块
|
||||
export * from './devices';
|
||||
export * from './scenarios';
|
||||
export * from './wechat-accounts';
|
||||
export * from './trafficDistribution';
|
||||
|
||||
// 默认导出request实例
|
||||
export { default as request } from './request';
|
||||
@@ -1,152 +0,0 @@
|
||||
import { refreshAuthToken, isTokenExpiringSoon, clearToken } from './utils';
|
||||
|
||||
// Token过期处理
|
||||
export const handleTokenExpired = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// 清除本地存储
|
||||
clearToken();
|
||||
|
||||
// 跳转到登录页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 显示API错误,但不会重定向
|
||||
export const showApiError = (error: any, defaultMessage: string = '请求失败') => {
|
||||
if (typeof window === 'undefined') return; // 服务端不处理
|
||||
|
||||
let errorMessage = defaultMessage;
|
||||
|
||||
// 尝试从各种可能的错误格式中获取消息
|
||||
if (error) {
|
||||
if (typeof error === 'string') {
|
||||
errorMessage = error;
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message || defaultMessage;
|
||||
} else if (typeof error === 'object') {
|
||||
// 尝试从API响应中获取错误消息
|
||||
errorMessage = error.msg || error.message || error.error || defaultMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
console.error('API错误:', errorMessage);
|
||||
|
||||
// 这里可以集成toast系统
|
||||
// 由于toast context在组件层级,这里暂时用console
|
||||
// 实际项目中可以通过事件系统或其他方式集成
|
||||
};
|
||||
|
||||
// 请求拦截器 - 检查token是否需要刷新
|
||||
export const requestInterceptor = async (): Promise<boolean> => {
|
||||
if (typeof window === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查token是否即将过期
|
||||
if (isTokenExpiringSoon()) {
|
||||
try {
|
||||
console.log('Token即将过期,尝试刷新...');
|
||||
const success = await refreshAuthToken();
|
||||
if (!success) {
|
||||
console.log('Token刷新失败,需要重新登录');
|
||||
handleTokenExpired();
|
||||
return false;
|
||||
}
|
||||
console.log('Token刷新成功');
|
||||
} catch (error) {
|
||||
console.error('Token刷新过程中出错:', error);
|
||||
handleTokenExpired();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 响应拦截器 - 处理常见错误
|
||||
export const responseInterceptor = (response: any, result: any) => {
|
||||
// 处理401未授权
|
||||
if (response?.status === 401 || (result && result.code === 401)) {
|
||||
handleTokenExpired();
|
||||
throw new Error('登录已过期,请重新登录');
|
||||
}
|
||||
|
||||
// 处理403禁止访问
|
||||
if (response?.status === 403 || (result && result.code === 403)) {
|
||||
throw new Error('没有权限访问此资源');
|
||||
}
|
||||
|
||||
// 处理404未找到
|
||||
if (response?.status === 404 || (result && result.code === 404)) {
|
||||
throw new Error('请求的资源不存在');
|
||||
}
|
||||
|
||||
// 处理500服务器错误
|
||||
if (response?.status >= 500 || (result && result.code >= 500)) {
|
||||
throw new Error('服务器内部错误,请稍后重试');
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 错误拦截器 - 统一错误处理
|
||||
export const errorInterceptor = (error: any) => {
|
||||
console.error('API请求错误:', error);
|
||||
|
||||
let errorMessage = '网络请求失败,请稍后重试';
|
||||
|
||||
if (error) {
|
||||
if (typeof error === 'string') {
|
||||
errorMessage = error;
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
errorMessage = '网络连接失败,请检查网络设置';
|
||||
} else if (error.name === 'AbortError') {
|
||||
errorMessage = '请求已取消';
|
||||
}
|
||||
}
|
||||
|
||||
showApiError(error, errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
};
|
||||
|
||||
// 网络状态监听
|
||||
export const setupNetworkListener = () => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const handleOnline = () => {
|
||||
console.log('网络已连接');
|
||||
// 可以在这里添加网络恢复后的处理逻辑
|
||||
};
|
||||
|
||||
const handleOffline = () => {
|
||||
console.log('网络已断开');
|
||||
showApiError(null, '网络连接已断开,请检查网络设置');
|
||||
};
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化拦截器
|
||||
export const initInterceptors = () => {
|
||||
// 设置网络监听
|
||||
const cleanupNetwork = setupNetworkListener();
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
if (cleanupNetwork) {
|
||||
cleanupNetwork();
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,111 +0,0 @@
|
||||
import { get, post, del } from './request';
|
||||
import {
|
||||
MomentsSyncTask,
|
||||
CreateMomentsSyncData,
|
||||
UpdateMomentsSyncData,
|
||||
SyncRecord,
|
||||
ApiResponse,
|
||||
PaginatedResponse
|
||||
} from '@/types/moments-sync';
|
||||
|
||||
// 获取朋友圈同步任务列表
|
||||
export async function fetchMomentsSyncTasks(): Promise<MomentsSyncTask[]> {
|
||||
try {
|
||||
const res = await get<ApiResponse<PaginatedResponse<MomentsSyncTask>>>('/v1/workbench/list?type=2&page=1&limit=100');
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data.list || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('获取朋友圈同步任务失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个任务详情
|
||||
export async function fetchMomentsSyncTaskDetail(id: string): Promise<MomentsSyncTask | null> {
|
||||
try {
|
||||
const res = await get<ApiResponse<MomentsSyncTask>>(`/v1/workbench/detail?id=${id}`);
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建朋友圈同步任务
|
||||
export async function createMomentsSyncTask(data: CreateMomentsSyncData): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/create', {
|
||||
...data,
|
||||
type: 2 // 朋友圈同步类型
|
||||
});
|
||||
}
|
||||
|
||||
// 更新朋友圈同步任务
|
||||
export async function updateMomentsSyncTask(data: UpdateMomentsSyncData): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/update', {
|
||||
...data,
|
||||
type: 2 // 朋友圈同步类型
|
||||
});
|
||||
}
|
||||
|
||||
// 删除朋友圈同步任务
|
||||
export async function deleteMomentsSyncTask(id: string): Promise<ApiResponse> {
|
||||
return del('/v1/workbench/delete', { params: { id } });
|
||||
}
|
||||
|
||||
// 切换任务状态
|
||||
export async function toggleMomentsSyncTask(id: string, status: string): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/update-status', { id, status });
|
||||
}
|
||||
|
||||
// 复制朋友圈同步任务
|
||||
export async function copyMomentsSyncTask(id: string): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/copy', { id });
|
||||
}
|
||||
|
||||
// 获取同步记录
|
||||
export async function fetchSyncRecords(
|
||||
workbenchId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
keyword?: string
|
||||
): Promise<PaginatedResponse<SyncRecord>> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
workbenchId,
|
||||
page: page.toString(),
|
||||
limit: limit.toString()
|
||||
});
|
||||
|
||||
if (keyword) {
|
||||
params.append('keyword', keyword);
|
||||
}
|
||||
|
||||
const res = await get<ApiResponse<PaginatedResponse<SyncRecord>>>(`/v1/workbench/sync-records?${params.toString()}`);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return { list: [], total: 0, page, limit };
|
||||
} catch (error) {
|
||||
console.error('获取同步记录失败:', error);
|
||||
return { list: [], total: 0, page, limit };
|
||||
}
|
||||
}
|
||||
|
||||
// 手动同步
|
||||
export async function syncMoments(id: string): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/sync', { id });
|
||||
}
|
||||
|
||||
// 同步所有任务
|
||||
export async function syncAllMoments(): Promise<ApiResponse> {
|
||||
return post('/v1/workbench/sync-all', { type: 2 });
|
||||
}
|
||||
|
||||
export type { MomentsSyncTask, SyncRecord, CreateMomentsSyncData };
|
||||
@@ -1,73 +1,107 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { requestInterceptor, responseInterceptor, errorInterceptor } from './interceptors';
|
||||
import axios, {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
Method,
|
||||
AxiosResponse,
|
||||
} from "axios";
|
||||
import { Toast } from "antd-mobile";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
const DEFAULT_DEBOUNCE_GAP = 1000;
|
||||
const debounceMap = new Map<string, number>();
|
||||
|
||||
// 创建axios实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL || 'https://ckbapi.quwanzhi.com',
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api",
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
async (config) => {
|
||||
// 检查token是否需要刷新
|
||||
if (config.headers.Authorization) {
|
||||
const shouldContinue = await requestInterceptor();
|
||||
if (!shouldContinue) {
|
||||
throw new Error('请求被拦截,需要重新登录');
|
||||
instance.interceptors.request.use((config: any) => {
|
||||
const token =
|
||||
useUserStore.getState().token ||
|
||||
localStorage.getItem("token") ||
|
||||
"";
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(res: AxiosResponse) => {
|
||||
const { code, success, msg } = res.data || {};
|
||||
if (code === 200 || success) {
|
||||
return res.data.data ?? res.data;
|
||||
}
|
||||
if (code === 401) {
|
||||
localStorage.removeItem("token");
|
||||
useUserStore.getState().logout?.();
|
||||
const currentPath = window.location.pathname + window.location.search;
|
||||
if (currentPath === "/login") {
|
||||
window.location.href = "/login";
|
||||
} else {
|
||||
window.location.href = `/login?returnUrl=${encodeURIComponent(currentPath)}`;
|
||||
}
|
||||
return Promise.reject(msg || "未授权,请重新登录");
|
||||
}
|
||||
|
||||
// 添加token到请求头
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
if (code === 402) {
|
||||
const payload = { code: 402, msg, data: res.data?.data };
|
||||
if (!(res.config as any)?.skipErrorToast) {
|
||||
Toast.show({ content: msg || "算力不足", position: "top" });
|
||||
}
|
||||
return Promise.reject(payload);
|
||||
}
|
||||
|
||||
return config;
|
||||
if (!(res.config as any)?.skipErrorToast) {
|
||||
Toast.show({ content: msg || "接口错误", position: "top" });
|
||||
}
|
||||
return Promise.reject(msg || "接口错误");
|
||||
},
|
||||
err => {
|
||||
// 网络错误静默处理,不弹出 Toast(根据用户需求)
|
||||
// 如需恢复错误提示,取消下面一行注释:
|
||||
// Toast.show({ content: err.message || "网络异常", position: "top" });
|
||||
console.warn("[v0] Network error (silent):", err.message);
|
||||
return Promise.reject(err);
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 处理响应数据
|
||||
const result = response.data;
|
||||
const processedResult = responseInterceptor(response, result);
|
||||
return processedResult;
|
||||
},
|
||||
(error) => {
|
||||
// 统一错误处理
|
||||
return errorInterceptor(error);
|
||||
export function request(
|
||||
url: string,
|
||||
data?: any,
|
||||
method: Method = "GET",
|
||||
config?: AxiosRequestConfig & { skipErrorToast?: boolean },
|
||||
debounceGap?: number,
|
||||
): Promise<any> {
|
||||
const gap =
|
||||
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||||
const now = Date.now();
|
||||
const last = debounceMap.get(key) || 0;
|
||||
if (gap > 0 && now - last < gap) {
|
||||
// Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
||||
return Promise.reject("请求过于频繁,请稍后再试");
|
||||
}
|
||||
);
|
||||
debounceMap.set(key, now);
|
||||
|
||||
// 封装GET请求
|
||||
export const get = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.get(url, config);
|
||||
};
|
||||
const axiosConfig: AxiosRequestConfig = {
|
||||
url,
|
||||
method,
|
||||
...config,
|
||||
} as AxiosRequestConfig & { skipErrorToast?: boolean };
|
||||
|
||||
// 封装POST请求
|
||||
export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.post(url, data, config);
|
||||
};
|
||||
// 如果是FormData,不设置Content-Type,让浏览器自动设置
|
||||
if (data instanceof FormData) {
|
||||
delete axiosConfig.headers?.["Content-Type"];
|
||||
}
|
||||
|
||||
// 封装PUT请求
|
||||
export const put = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.put(url, data, config);
|
||||
};
|
||||
if (method.toUpperCase() === "GET") {
|
||||
axiosConfig.params = data;
|
||||
} else {
|
||||
axiosConfig.data = data;
|
||||
}
|
||||
return instance(axiosConfig);
|
||||
}
|
||||
|
||||
// 封装DELETE请求
|
||||
export const del = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.delete(url, config);
|
||||
};
|
||||
|
||||
// 导出request实例
|
||||
export { request };
|
||||
export default request;
|
||||
export default request;
|
||||
|
||||
89
Cunkebao/src/api/request2.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import axios, {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
Method,
|
||||
AxiosResponse,
|
||||
} from "axios";
|
||||
import { Toast } from "antd-mobile";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
const DEFAULT_DEBOUNCE_GAP = 1000;
|
||||
const debounceMap = new Map<string, number>();
|
||||
|
||||
interface RequestConfig extends AxiosRequestConfig {
|
||||
headers: {
|
||||
Client?: string;
|
||||
"Content-Type"?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: (import.meta as any).env?.VITE_API_BASE_URL2 || "/api",
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Client: "kefu-client",
|
||||
},
|
||||
});
|
||||
|
||||
instance.interceptors.request.use((config: any) => {
|
||||
// 在每次请求时动态获取最新的 token2
|
||||
const { token2 } = useUserStore.getState();
|
||||
if (token2) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers["Authorization"] = `bearer ${token2}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(res: AxiosResponse) => {
|
||||
return res.data;
|
||||
},
|
||||
err => {
|
||||
// 处理401错误,跳转到登录页面
|
||||
if (err.response && err.response.status === 401) {
|
||||
Toast.show({ content: "登录已过期,请重新登录", position: "top" });
|
||||
// 获取当前路径,用于登录后跳回
|
||||
const currentPath = window.location.pathname + window.location.search;
|
||||
window.location.href = `/login?returnUrl=${encodeURIComponent(currentPath)}`;
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
Toast.show({ content: err.message || "网络异常", position: "top" });
|
||||
return Promise.reject(err);
|
||||
},
|
||||
);
|
||||
|
||||
export function request(
|
||||
url: string,
|
||||
data?: any,
|
||||
method: Method = "GET",
|
||||
config?: RequestConfig,
|
||||
debounceGap?: number,
|
||||
): Promise<any> {
|
||||
const gap =
|
||||
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||||
const now = Date.now();
|
||||
const last = debounceMap.get(key) || 0;
|
||||
if (gap > 0 && now - last < gap) {
|
||||
// Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
||||
return Promise.reject("请求过于频繁,请稍后再试");
|
||||
}
|
||||
debounceMap.set(key, now);
|
||||
|
||||
const axiosConfig: RequestConfig = {
|
||||
url,
|
||||
method,
|
||||
...config,
|
||||
};
|
||||
|
||||
if (method.toUpperCase() === "GET") {
|
||||
axiosConfig.params = data;
|
||||
} else {
|
||||
axiosConfig.data = data;
|
||||
}
|
||||
return instance(axiosConfig);
|
||||
}
|
||||
|
||||
export default request;
|
||||
@@ -1,327 +0,0 @@
|
||||
import { get, del, post,put } from './request';
|
||||
import type { ApiResponse } from '@/types/common';
|
||||
|
||||
// 服务器返回的场景数据类型
|
||||
export interface SceneItem {
|
||||
id: number;
|
||||
name: string;
|
||||
image: string;
|
||||
status: number;
|
||||
createTime: number;
|
||||
updateTime: number | null;
|
||||
deleteTime: number | null;
|
||||
}
|
||||
|
||||
// 前端使用的场景数据类型
|
||||
export interface Channel {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
stats: {
|
||||
daily: number;
|
||||
growth: number;
|
||||
};
|
||||
link?: string;
|
||||
plans?: Plan[];
|
||||
}
|
||||
|
||||
// 计划类型
|
||||
export interface Plan {
|
||||
id: string;
|
||||
name: string;
|
||||
isNew?: boolean;
|
||||
status: "active" | "paused" | "completed";
|
||||
acquisitionCount: number;
|
||||
}
|
||||
|
||||
// 任务类型
|
||||
export interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
status: number;
|
||||
stats: {
|
||||
devices: number;
|
||||
acquired: number;
|
||||
added: number;
|
||||
};
|
||||
lastUpdated: string;
|
||||
executionTime: string;
|
||||
nextExecutionTime: string;
|
||||
trend: { date: string; customers: number }[];
|
||||
}
|
||||
|
||||
// 消息内容类型
|
||||
export interface MessageContent {
|
||||
id: string;
|
||||
type: string; // "text" | "image" | "video" | "file" | "miniprogram" | "link" | "group" 等
|
||||
content?: string;
|
||||
intervalUnit?: "seconds" | "minutes";
|
||||
sendInterval?: number;
|
||||
// 其他可选字段
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 每天的消息计划
|
||||
export interface MessagePlan {
|
||||
day: number;
|
||||
messages: MessageContent[];
|
||||
}
|
||||
|
||||
// 海报类型
|
||||
export interface Poster {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
preview: string;
|
||||
}
|
||||
|
||||
// 标签类型
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// textUrl类型
|
||||
export interface TextUrl {
|
||||
apiKey: string;
|
||||
originalString?: string;
|
||||
sign?: string;
|
||||
fullUrl: string;
|
||||
}
|
||||
|
||||
// 计划详情类型
|
||||
export interface PlanDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
scenario: number;
|
||||
scenarioTags: Tag[];
|
||||
customTags: Tag[];
|
||||
posters: Poster[];
|
||||
device: string[];
|
||||
enabled: boolean;
|
||||
addInterval: number;
|
||||
remarkFormat: string;
|
||||
endTime: string;
|
||||
greeting: string;
|
||||
startTime: string;
|
||||
remarkType: string;
|
||||
addFriendInterval: number;
|
||||
messagePlans: MessagePlan[];
|
||||
sceneId: number | string;
|
||||
userId: number;
|
||||
companyId: number;
|
||||
status: number;
|
||||
apiKey: string;
|
||||
wxMinAppSrc?: any;
|
||||
textUrl: TextUrl;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取获客场景列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @returns 获客场景列表
|
||||
*/
|
||||
export const fetchScenes = async (params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
} = {}): Promise<ApiResponse<SceneItem[]>> => {
|
||||
const { page = 1, limit = 10, keyword = "" } = params;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append("page", String(page));
|
||||
queryParams.append("limit", String(limit));
|
||||
|
||||
if (keyword) {
|
||||
queryParams.append("keyword", keyword);
|
||||
}
|
||||
|
||||
try {
|
||||
return await get<ApiResponse<SceneItem[]>>(`/v1/plan/scenes?${queryParams.toString()}`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching scenes:", error);
|
||||
// 返回一个错误响应
|
||||
return {
|
||||
code: 500,
|
||||
msg: "获取场景列表失败",
|
||||
data: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取场景详情
|
||||
*
|
||||
* @param id 场景ID
|
||||
* @returns 场景详情
|
||||
*/
|
||||
export const fetchSceneDetail = async (id: string | number): Promise<ApiResponse<SceneItem>> => {
|
||||
try {
|
||||
return await get<ApiResponse<SceneItem>>(`/v1/plan/scenes/${id}`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching scene detail:", error);
|
||||
return {
|
||||
code: 500,
|
||||
msg: "获取场景详情失败",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取场景名称
|
||||
*
|
||||
* @param channel 场景标识
|
||||
* @returns 场景名称
|
||||
*/
|
||||
export const fetchSceneName = async (channel: string): Promise<ApiResponse<{ name: string }>> => {
|
||||
try {
|
||||
return await get<ApiResponse<{ name: string }>>(`/v1/plan/scenes-detail?id=${channel}`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching scene name:", error);
|
||||
return {
|
||||
code: 500,
|
||||
msg: "获取场景名称失败",
|
||||
data: { name: channel }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取计划列表
|
||||
*
|
||||
* @param channel 场景标识
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @returns 计划列表
|
||||
*/
|
||||
export const fetchPlanList = async (
|
||||
channel: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 10
|
||||
): Promise<ApiResponse<{ list: Task[]; total: number }>> => {
|
||||
try {
|
||||
return await get<ApiResponse<{ list: Task[]; total: number }>>(
|
||||
`/v1/plan/list?sceneId=${channel}&page=${page}&pageSize=${pageSize}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching plan list:", error);
|
||||
return {
|
||||
code: 500,
|
||||
msg: "获取计划列表失败",
|
||||
data: { list: [], total: 0 }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 复制计划
|
||||
*
|
||||
* @param planId 计划ID
|
||||
* @returns 复制结果
|
||||
*/
|
||||
export const copyPlan = async (planId: string): Promise<ApiResponse<any>> => {
|
||||
try {
|
||||
return await get<ApiResponse<any>>(`/v1/plan/copy?planId=${planId}`);
|
||||
} catch (error) {
|
||||
console.error("Error copying plan:", error);
|
||||
return {
|
||||
code: 500,
|
||||
msg: "复制计划失败",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除计划
|
||||
*
|
||||
* @param planId 计划ID
|
||||
* @returns 删除结果
|
||||
*/
|
||||
export const deletePlan = async (planId: string): Promise<ApiResponse<any>> => {
|
||||
try {
|
||||
return await del<ApiResponse<any>>(`/v1/plan/delete?planId=${planId}`);
|
||||
} catch (error) {
|
||||
console.error("Error deleting plan:", error);
|
||||
return {
|
||||
code: 500,
|
||||
msg: "删除计划失败",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取计划详情
|
||||
*
|
||||
* @param planId 计划ID
|
||||
* @returns 计划详情
|
||||
*/
|
||||
export const fetchPlanDetail = async (planId: string): Promise<ApiResponse<PlanDetail>> => {
|
||||
try {
|
||||
return await get<ApiResponse<PlanDetail>>(`/v1/plan/detail?planId=${planId}`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching plan detail:", error);
|
||||
return {
|
||||
code: 500,
|
||||
msg: "获取计划详情失败",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将服务器返回的场景数据转换为前端展示需要的格式
|
||||
*
|
||||
* @param item 服务器返回的场景数据
|
||||
* @returns 前端展示的场景数据
|
||||
*/
|
||||
export const transformSceneItem = (item: SceneItem): Channel => {
|
||||
// 为每个场景生成随机的"今日"数据和"增长百分比"
|
||||
const dailyCount = Math.floor(Math.random() * 100);
|
||||
const growthPercent = Math.floor(Math.random() * 40) - 10; // -10% 到 30% 的随机值
|
||||
|
||||
// 默认图标(如果服务器没有返回)
|
||||
const defaultIcon = "/assets/icons/poster-icon.svg";
|
||||
|
||||
return {
|
||||
id: String(item.id),
|
||||
name: item.name,
|
||||
icon: item.image || defaultIcon,
|
||||
stats: {
|
||||
daily: dailyCount,
|
||||
growth: growthPercent
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getPlanScenes = () => get<any>('/v1/plan/scenes');
|
||||
|
||||
export async function createScenarioPlan(data: any) {
|
||||
return post('/v1/plan/create', data);
|
||||
}
|
||||
|
||||
// 编辑计划
|
||||
export async function updateScenarioPlan(planId: number | string, data: any) {
|
||||
return await put(`/v1/plan/update?planId=${planId}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取计划小程序二维码
|
||||
* @param taskid 任务ID
|
||||
* @returns base64二维码
|
||||
*/
|
||||
export const getWxMinAppCode = async (taskId: string): Promise<{ code: number; data?: string; msg?: string }> => {
|
||||
try {
|
||||
return await get<{ code: number; data?: string; msg?: string }>(
|
||||
`/v1/plan/getWxMinAppCode?taskId=${ taskId }`,
|
||||
|
||||
);
|
||||
} catch (error) {
|
||||
return { code: 500, msg: '获取小程序二维码失败' };
|
||||
}
|
||||
};
|
||||
@@ -1,227 +0,0 @@
|
||||
import { get, post, put, del } from './request';
|
||||
import type { ApiResponse } from '@/types/common';
|
||||
|
||||
// 工作台任务类型
|
||||
export enum WorkbenchTaskType {
|
||||
MOMENTS_SYNC = 1, // 朋友圈同步
|
||||
GROUP_PUSH = 2, // 社群推送
|
||||
AUTO_LIKE = 3, // 自动点赞
|
||||
AUTO_GROUP = 4, // 自动建群
|
||||
TRAFFIC_DISTRIBUTION = 5, // 流量分发
|
||||
}
|
||||
|
||||
// 工作台任务状态
|
||||
export enum WorkbenchTaskStatus {
|
||||
PENDING = 0, // 待处理
|
||||
RUNNING = 1, // 运行中
|
||||
PAUSED = 2, // 已暂停
|
||||
COMPLETED = 3, // 已完成
|
||||
FAILED = 4, // 失败
|
||||
}
|
||||
|
||||
// 账号类型
|
||||
export interface Account {
|
||||
id: string;
|
||||
userName: string;
|
||||
realName: string;
|
||||
nickname: string;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
// 账号列表响应类型
|
||||
export interface AccountListResponse {
|
||||
list: Account[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
// 流量池类型
|
||||
export interface TrafficPool {
|
||||
id: string;
|
||||
name: string;
|
||||
count: number;
|
||||
description?: string;
|
||||
deviceIds: string[];
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
// 流量池列表响应类型
|
||||
export interface TrafficPoolListResponse {
|
||||
list: TrafficPool[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// 流量分发规则类型
|
||||
export interface DistributionRule {
|
||||
id: number;
|
||||
name: string;
|
||||
type: number;
|
||||
status: number;
|
||||
autoStart: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
companyId: number;
|
||||
config?: {
|
||||
id: number;
|
||||
workbenchId: number;
|
||||
distributeType: number; // 1-均分配, 2-优先级分配, 3-比例分配
|
||||
maxPerDay: number; // 每日最大分配量
|
||||
timeType: number; // 1-全天, 2-自定义时间段
|
||||
startTime: string; // 开始时间
|
||||
endTime: string; // 结束时间
|
||||
account: string[]; // 账号列表
|
||||
devices: string[]; // 设备列表
|
||||
pools: string[]; // 流量池列表
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
lastUpdated: string;
|
||||
total: {
|
||||
dailyAverage: number; // 日均分发量
|
||||
totalAccounts: number; // 分发账户总数
|
||||
deviceCount: number; // 分发设备数量
|
||||
poolCount: number; // 流量池数量
|
||||
totalUsers: number; // 总用户数
|
||||
};
|
||||
};
|
||||
auto_like?: any;
|
||||
moments_sync?: any;
|
||||
group_push?: any;
|
||||
}
|
||||
|
||||
// 流量分发列表响应类型
|
||||
export interface TrafficDistributionListResponse {
|
||||
list: DistributionRule[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号列表
|
||||
* @param params 查询参数
|
||||
* @returns 账号列表
|
||||
*/
|
||||
export const fetchAccountList = async (params: {
|
||||
page?: number; // 页码
|
||||
limit?: number; // 每页数量
|
||||
keyword?: string; // 搜索关键词
|
||||
} = {}): Promise<ApiResponse<AccountListResponse>> => {
|
||||
const { page = 1, limit = 10, keyword = "" } = params;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('page', page.toString());
|
||||
queryParams.append('limit', limit.toString());
|
||||
|
||||
if (keyword) {
|
||||
queryParams.append('keyword', keyword);
|
||||
}
|
||||
|
||||
return get<ApiResponse<AccountListResponse>>(`/v1/workbench/account-list?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取设备标签(流量池)列表
|
||||
* @param params 查询参数
|
||||
* @returns 流量池列表
|
||||
*/
|
||||
export const fetchDeviceLabels = async (params: {
|
||||
deviceIds: string[]; // 设备ID列表
|
||||
page?: number; // 页码
|
||||
pageSize?: number; // 每页数量
|
||||
keyword?: string; // 搜索关键词
|
||||
}): Promise<ApiResponse<TrafficPoolListResponse>> => {
|
||||
const { deviceIds, page = 1, pageSize = 10, keyword = "" } = params;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('deviceIds', deviceIds.join(','));
|
||||
queryParams.append('page', page.toString());
|
||||
queryParams.append('pageSize', pageSize.toString());
|
||||
|
||||
if (keyword) {
|
||||
queryParams.append('keyword', keyword);
|
||||
}
|
||||
|
||||
return get<ApiResponse<TrafficPoolListResponse>>(`/v1/workbench/device-labels?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取流量分发规则列表
|
||||
* @param params 查询参数
|
||||
* @returns 流量分发规则列表
|
||||
*/
|
||||
export const fetchDistributionRules = async (params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
} = {}): Promise<ApiResponse<TrafficDistributionListResponse>> => {
|
||||
const { page = 1, limit = 10, keyword = "" } = params;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('type', WorkbenchTaskType.TRAFFIC_DISTRIBUTION.toString());
|
||||
queryParams.append('page', page.toString());
|
||||
queryParams.append('limit', limit.toString());
|
||||
|
||||
if (keyword) {
|
||||
queryParams.append('keyword', keyword);
|
||||
}
|
||||
|
||||
return get<ApiResponse<TrafficDistributionListResponse>>(`/v1/workbench/list?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取流量分发规则详情
|
||||
* @param id 规则ID
|
||||
* @returns 流量分发规则详情
|
||||
*/
|
||||
export const fetchDistributionRuleDetail = async (id: string): Promise<ApiResponse<DistributionRule>> => {
|
||||
return get<ApiResponse<DistributionRule>>(`/v1/workbench/detail?id=${id}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建流量分发规则
|
||||
* @param params 创建参数
|
||||
* @returns 创建结果
|
||||
*/
|
||||
export const createDistributionRule = async (params: any): Promise<ApiResponse<{ id: string }>> => {
|
||||
return post<ApiResponse<{ id: string }>>('/v1/workbench/create', {
|
||||
...params,
|
||||
type: WorkbenchTaskType.TRAFFIC_DISTRIBUTION
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新流量分发规则
|
||||
* @param id 规则ID
|
||||
* @param params 更新参数
|
||||
* @returns 更新结果
|
||||
*/
|
||||
export const updateDistributionRule = async (id : string, params: any): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>(`/v1/workbench/update`, {
|
||||
id: id,
|
||||
...params,
|
||||
type: WorkbenchTaskType.TRAFFIC_DISTRIBUTION
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除流量分发规则
|
||||
* @param id 规则ID
|
||||
* @returns 删除结果
|
||||
*/
|
||||
export const deleteDistributionRule = async (id: string): Promise<ApiResponse<any>> => {
|
||||
return del<ApiResponse<any>>(`/v1/workbench/delete?id=${id}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 启动/暂停流量分发规则
|
||||
* @param id 规则ID
|
||||
* @param status 状态:1-启动,0-暂停
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export const toggleDistributionRuleStatus = async (id: string, status: 0 | 1): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/v1/workbench/update-status', { id, status });
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { request } from './request';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
|
||||
// 上传图片,返回图片地址
|
||||
export async function uploadImage(file: File): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const response: AxiosResponse<any> = await request.post('/v1/attachment/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
const res = response.data || response;
|
||||
if (res?.url) {
|
||||
return res.url;
|
||||
}
|
||||
throw new Error(res?.msg || '图片上传失败');
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
import { authApi } from './auth';
|
||||
import { get, post, put, del } from './request';
|
||||
import type { ApiResponse, PaginatedResponse } from '@/types/common';
|
||||
// 设置token到localStorage
|
||||
export const setToken = (token: string) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取token
|
||||
export const getToken = (): string | null => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 清除token
|
||||
export const clearToken = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
localStorage.removeItem('token_expired');
|
||||
localStorage.removeItem('s2_accountId');
|
||||
}
|
||||
};
|
||||
|
||||
// 验证token是否有效
|
||||
export const validateToken = async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await authApi.getUserInfo();
|
||||
return response.code === 200;
|
||||
} catch (error) {
|
||||
console.error('Token验证失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新令牌
|
||||
export const refreshAuthToken = async (): Promise<boolean> => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authApi.refreshToken();
|
||||
if (response.code === 200 && response.data?.token) {
|
||||
setToken(response.data.token);
|
||||
// 更新过期时间
|
||||
if (response.data.token_expired) {
|
||||
localStorage.setItem('token_expired', response.data.token_expired);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('刷新Token失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查token是否即将过期
|
||||
export const isTokenExpiringSoon = (): boolean => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tokenExpired = localStorage.getItem('token_expired');
|
||||
if (!tokenExpired) return true;
|
||||
|
||||
try {
|
||||
const expiredTime = new Date(tokenExpired).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
// 提前10分钟认为即将过期
|
||||
return currentTime >= (expiredTime - 10 * 60 * 1000);
|
||||
} catch (error) {
|
||||
console.error('解析token过期时间失败:', error);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查token是否已过期
|
||||
export const isTokenExpired = (): boolean => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tokenExpired = localStorage.getItem('token_expired');
|
||||
if (!tokenExpired) return true;
|
||||
|
||||
try {
|
||||
const expiredTime = new Date(tokenExpired).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
// 提前5分钟认为过期,给刷新留出时间
|
||||
return currentTime >= (expiredTime - 5 * 60 * 1000);
|
||||
} catch (error) {
|
||||
console.error('解析token过期时间失败:', error);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// 请求去重器
|
||||
class RequestDeduplicator {
|
||||
private pendingRequests = new Map<string, Promise<any>>();
|
||||
|
||||
async deduplicate<T>(key: string, requestFn: () => Promise<T>): Promise<T> {
|
||||
if (this.pendingRequests.has(key)) {
|
||||
return this.pendingRequests.get(key)!;
|
||||
}
|
||||
|
||||
const promise = requestFn();
|
||||
this.pendingRequests.set(key, promise);
|
||||
|
||||
try {
|
||||
const result = await promise;
|
||||
return result;
|
||||
} finally {
|
||||
this.pendingRequests.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
getPendingCount(): number {
|
||||
return this.pendingRequests.size;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.pendingRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 请求取消管理器
|
||||
class RequestCancelManager {
|
||||
private abortControllers = new Map<string, AbortController>();
|
||||
|
||||
createController(key: string): AbortController {
|
||||
// 取消之前的请求
|
||||
this.cancelRequest(key);
|
||||
|
||||
const controller = new AbortController();
|
||||
this.abortControllers.set(key, controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
cancelRequest(key: string): void {
|
||||
const controller = this.abortControllers.get(key);
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
this.abortControllers.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
cancelAllRequests(): void {
|
||||
this.abortControllers.forEach(controller => controller.abort());
|
||||
this.abortControllers.clear();
|
||||
}
|
||||
|
||||
getController(key: string): AbortController | undefined {
|
||||
return this.abortControllers.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const requestDeduplicator = new RequestDeduplicator();
|
||||
export const requestCancelManager = new RequestCancelManager();
|
||||
|
||||
/**
|
||||
* 通用文件上传方法(支持图片、文件)
|
||||
* @param {File} file - 要上传的文件对象
|
||||
* @param {string} [uploadUrl='/v1/attachment/upload'] - 上传接口地址
|
||||
* @returns {Promise<string>} - 上传成功后返回文件url
|
||||
*/
|
||||
export async function uploadFile(file: File, uploadUrl: string = '/v1/attachment/upload'): Promise<string> {
|
||||
try {
|
||||
// 创建 FormData 对象用于文件上传
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 使用 post 方法上传文件,设置正确的 Content-Type
|
||||
const res = await post(uploadUrl, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
// 检查响应结果
|
||||
if (res?.code === 200 && res?.data?.url) {
|
||||
return res.data.url;
|
||||
} else {
|
||||
throw new Error(res?.msg || '文件上传失败');
|
||||
}
|
||||
} catch (e: any) {
|
||||
throw new Error(e?.message || '文件上传失败');
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
import { get, post, put } from './request';
|
||||
import type { ApiResponse } from '@/types/common';
|
||||
|
||||
// 添加接口返回数据类型定义
|
||||
interface WechatAccountSummary {
|
||||
accountAge: string;
|
||||
activityLevel: {
|
||||
allTimes: number;
|
||||
dayTimes: number;
|
||||
};
|
||||
accountWeight: {
|
||||
scope: number;
|
||||
ageWeight: number;
|
||||
activityWeigth: number;
|
||||
restrictWeight: number;
|
||||
realNameWeight: number;
|
||||
};
|
||||
statistics: {
|
||||
todayAdded: number;
|
||||
addLimit: number;
|
||||
};
|
||||
restrictions: {
|
||||
id: number;
|
||||
level: string;
|
||||
reason: string;
|
||||
date: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface QueryWechatAccountParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
sort?: string;
|
||||
order?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信账号列表
|
||||
* @param params 查询参数
|
||||
* @returns 微信账号列表响应
|
||||
*/
|
||||
export const fetchWechatAccountList = async (params: QueryWechatAccountParams = {}): Promise<ApiResponse<{
|
||||
list: any[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}>> => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
// 添加查询参数
|
||||
if (params.page) queryParams.append('page', params.page.toString());
|
||||
if (params.limit) queryParams.append('limit', params.limit.toString());
|
||||
if (params.keyword) queryParams.append('nickname', params.keyword); // 使用nickname作为关键词搜索参数
|
||||
if (params.sort) queryParams.append('sort', params.sort);
|
||||
if (params.order) queryParams.append('order', params.order);
|
||||
|
||||
// 发起API请求
|
||||
return get<ApiResponse<{
|
||||
list: any[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}>>(`/v1/wechats?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新微信账号状态
|
||||
* @returns 刷新结果
|
||||
*/
|
||||
export const refreshWechatAccounts = async (): Promise<ApiResponse<any>> => {
|
||||
return put<ApiResponse<any>>('/v1/wechats/refresh', {});
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行微信好友转移
|
||||
* @param sourceId 源微信账号ID
|
||||
* @param targetId 目标微信账号ID
|
||||
* @returns 转移结果
|
||||
*/
|
||||
export const transferWechatFriends = async (sourceId: string | number, targetId: string | number): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/v1/wechats/transfer-friends', {
|
||||
source_id: sourceId,
|
||||
target_id: targetId
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 将服务器返回的微信账号数据转换为前端使用的格式
|
||||
* @param serverAccount 服务器返回的微信账号数据
|
||||
* @returns 前端使用的微信账号数据
|
||||
*/
|
||||
export const transformWechatAccount = (serverAccount: any): any => {
|
||||
// 从deviceInfo中提取设备信息
|
||||
let deviceName = '';
|
||||
|
||||
if (serverAccount.deviceInfo) {
|
||||
// 尝试解析设备信息字符串
|
||||
const deviceInfo = serverAccount.deviceInfo.split(' ');
|
||||
if (deviceInfo.length > 0) {
|
||||
// 提取设备名称
|
||||
if (deviceInfo.length > 1) {
|
||||
deviceName = deviceInfo[1] ? deviceInfo[1].replace(/[()]/g, '').trim() : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果没有设备名称,使用备用名称
|
||||
if (!deviceName) {
|
||||
deviceName = serverAccount.deviceMemo || '未命名设备';
|
||||
}
|
||||
|
||||
// 假设每天最多可添加20个好友
|
||||
const maxDailyAdds = 20;
|
||||
const todayAdded = serverAccount.todayNewFriendCount || 0;
|
||||
|
||||
return {
|
||||
id: serverAccount.id.toString(),
|
||||
avatar: serverAccount.avatar || '',
|
||||
nickname: serverAccount.nickname || serverAccount.accountNickname || '未命名',
|
||||
wechatId: serverAccount.wechatId || '',
|
||||
deviceId: serverAccount.deviceId || '',
|
||||
deviceName,
|
||||
friendCount: serverAccount.totalFriend || 0,
|
||||
todayAdded,
|
||||
remainingAdds: serverAccount.canAddFriendCount || (maxDailyAdds - todayAdded),
|
||||
maxDailyAdds,
|
||||
status: serverAccount.wechatStatus === 1 ? "normal" : "abnormal" as "normal" | "abnormal",
|
||||
lastActive: new Date().toLocaleString() // 服务端未提供,使用当前时间
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取微信好友列表
|
||||
* @param wechatId 微信账号ID
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param searchQuery 搜索关键词
|
||||
* @returns 好友列表数据
|
||||
*/
|
||||
export const fetchWechatFriends = async (wechatId: string, page: number = 1, pageSize: number = 20, searchQuery: string = ''): Promise<ApiResponse<{
|
||||
list: any[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}>> => {
|
||||
try {
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('page', page.toString());
|
||||
queryParams.append('limit', pageSize.toString());
|
||||
if (searchQuery) {
|
||||
queryParams.append('search', searchQuery);
|
||||
}
|
||||
|
||||
return get<ApiResponse<{
|
||||
list: any[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}>>(`/v1/wechats/${wechatId}/friends?${queryParams.toString()}`);
|
||||
} catch (error) {
|
||||
console.error("获取好友列表失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取微信账号概览信息
|
||||
* @param id 微信账号ID
|
||||
* @returns 微信账号概览信息
|
||||
*/
|
||||
export const fetchWechatAccountSummary = async (wechatId: string): Promise<ApiResponse<WechatAccountSummary>> => {
|
||||
try {
|
||||
return get<ApiResponse<WechatAccountSummary>>(`/v1/wechats/${wechatId}/summary`);
|
||||
} catch (error) {
|
||||
console.error("获取账号概览失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取好友详情信息
|
||||
* @param wechatId 微信账号ID
|
||||
* @param friendId 好友ID
|
||||
* @returns 好友详情信息
|
||||
*/
|
||||
export interface WechatFriendDetail {
|
||||
id: number;
|
||||
avatar: string;
|
||||
nickname: string;
|
||||
region: string;
|
||||
wechatId: string;
|
||||
addDate: string;
|
||||
tags: string[];
|
||||
memo: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export const fetchWechatFriendDetail = async (wechatId: string): Promise<ApiResponse<WechatFriendDetail>> => {
|
||||
try {
|
||||
return get<ApiResponse<WechatFriendDetail>>(`/v1/wechats/${wechatId}/friend-detail`);
|
||||
} catch (error) {
|
||||
console.error("获取好友详情失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/* 弹层在底栏之下(底栏 10200),弹层本体 10150,为 Tab 留出空间 */
|
||||
.popup {
|
||||
--z-index: 10150;
|
||||
}
|
||||
|
||||
.popupBody {
|
||||
bottom: calc(
|
||||
var(--ckb-tabbar-h, 64px) + env(safe-area-inset-bottom, 0px)
|
||||
) !important;
|
||||
height: calc(
|
||||
100dvh - var(--ckb-tabbar-h, 64px) - env(safe-area-inset-bottom, 0px) - 8vh
|
||||
) !important;
|
||||
max-height: calc(
|
||||
100dvh - var(--ckb-tabbar-h, 64px) - env(safe-area-inset-bottom, 0px) - 8vh
|
||||
) !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 20px 20px 0 0;
|
||||
}
|
||||
|
||||
.panelWrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Popup } from "antd-mobile";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useAIAssistantStore } from "@/store/module/aiAssistant";
|
||||
import { AIAssistantPanel } from "@/pages/mobile/workspace/ai-assistant/AIAssistant";
|
||||
import styles from "./AIAssistantGlobalSheet.module.scss";
|
||||
|
||||
/** 底栏切换路由时自动收起小宝 AI 弹层 */
|
||||
const AIAssistantRouteSync: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const { visible, close } = useAIAssistantStore();
|
||||
const prevPathRef = React.useRef(location.pathname);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevPathRef.current !== location.pathname) {
|
||||
if (visible) close();
|
||||
prevPathRef.current = location.pathname;
|
||||
}
|
||||
}, [location.pathname, visible, close]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const AIAssistantGlobalSheet: React.FC = () => {
|
||||
const { visible, initialPrompt, openId, close } = useAIAssistantStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AIAssistantRouteSync />
|
||||
<Popup
|
||||
visible={visible}
|
||||
onMaskClick={close}
|
||||
position="bottom"
|
||||
destroyOnClose
|
||||
className={styles.popup}
|
||||
bodyClassName={styles.popupBody}
|
||||
style={
|
||||
{
|
||||
"--z-index": 10150,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
maskStyle={{ zIndex: 10140 }}
|
||||
>
|
||||
{visible && (
|
||||
<div className={styles.panelWrap}>
|
||||
<AIAssistantPanel
|
||||
key={`ai-panel-${openId}`}
|
||||
embedded
|
||||
initialPrompt={initialPrompt}
|
||||
onClose={close}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIAssistantGlobalSheet;
|
||||
10
Cunkebao/src/components/AccountSelection/api.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
// 获取好友列表
|
||||
export function getAccountList(params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
keyword?: string;
|
||||
}) {
|
||||
return request("/v1/workbench/account-list", params, "GET");
|
||||
}
|
||||
35
Cunkebao/src/components/AccountSelection/data.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// 账号对象类型
|
||||
export interface AccountItem {
|
||||
id: number;
|
||||
userName: string;
|
||||
realName: string;
|
||||
departmentName: string;
|
||||
avatar?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
//弹窗的
|
||||
export interface SelectionPopupProps {
|
||||
visible: boolean;
|
||||
onVisibleChange: (visible: boolean) => void;
|
||||
selectedOptions: AccountItem[];
|
||||
onSelect: (options: AccountItem[]) => void;
|
||||
readonly?: boolean;
|
||||
onConfirm?: (selectedOptions: AccountItem[]) => void;
|
||||
}
|
||||
|
||||
// 组件属性接口
|
||||
export interface AccountSelectionProps {
|
||||
selectedOptions: AccountItem[];
|
||||
onSelect: (options: AccountItem[]) => void;
|
||||
accounts?: AccountItem[]; // 可选:用于在外层显示已选账号详情
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
visible?: boolean;
|
||||
onVisibleChange?: (visible: boolean) => void;
|
||||
selectedListMaxHeight?: number;
|
||||
showInput?: boolean;
|
||||
showSelectedList?: boolean;
|
||||
readonly?: boolean;
|
||||
onConfirm?: (selectedOptions: AccountItem[]) => void;
|
||||
accountGroups?: any[]; // 传递账号组数据
|
||||
}
|
||||
233
Cunkebao/src/components/AccountSelection/index.module.scss
Normal file
@@ -0,0 +1,233 @@
|
||||
.inputWrapper {
|
||||
position: relative;
|
||||
}
|
||||
.inputIcon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #bdbdbd;
|
||||
font-size: 20px;
|
||||
}
|
||||
.input {
|
||||
padding-left: 38px !important;
|
||||
height: 48px;
|
||||
border-radius: 16px !important;
|
||||
border: 1px solid #e5e6eb !important;
|
||||
font-size: 16px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.popupContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
.popupHeader {
|
||||
padding: 24px;
|
||||
}
|
||||
.popupTitle {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.searchWrapper {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.searchInput {
|
||||
padding-left: 40px !important;
|
||||
padding-top: 8px !important;
|
||||
padding-bottom: 8px !important;
|
||||
border-radius: 24px !important;
|
||||
border: 1px solid #e5e6eb !important;
|
||||
font-size: 15px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.searchIcon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #bdbdbd;
|
||||
font-size: 16px;
|
||||
}
|
||||
.clearBtn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border-radius: 50%;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.friendList {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.friendListInner {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.friendItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
&:hover {
|
||||
background: #f5f6fa;
|
||||
}
|
||||
}
|
||||
.radioWrapper {
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.radioSelected {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #1890ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.radioUnselected {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e5e6eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.radioDot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #1890ff;
|
||||
}
|
||||
.friendInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
.friendAvatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
}
|
||||
.avatarImg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.friendDetail {
|
||||
flex: 1;
|
||||
}
|
||||
.friendName {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: #222;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.friendId {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.friendCustomer {
|
||||
font-size: 13px;
|
||||
color: #bdbdbd;
|
||||
}
|
||||
|
||||
.loadingBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.loadingText {
|
||||
color: #888;
|
||||
font-size: 15px;
|
||||
}
|
||||
.emptyBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.emptyText {
|
||||
color: #888;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.paginationRow {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
}
|
||||
.totalCount {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
.paginationControls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.pageBtn {
|
||||
padding: 0 8px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
}
|
||||
.pageInfo {
|
||||
font-size: 14px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.popupFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
}
|
||||
.selectedCount {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
.footerBtnGroup {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.cancelBtn {
|
||||
padding: 0 24px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid #e5e6eb;
|
||||
}
|
||||
.confirmBtn {
|
||||
padding: 0 24px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
139
Cunkebao/src/components/AccountSelection/index.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React, { useState } from "react";
|
||||
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import { Button, Input } from "antd";
|
||||
import style from "./index.module.scss";
|
||||
import SelectionPopup from "./selectionPopup";
|
||||
import { AccountItem, AccountSelectionProps } from "./data";
|
||||
|
||||
export default function AccountSelection({
|
||||
selectedOptions,
|
||||
onSelect,
|
||||
accounts: propAccounts = [],
|
||||
placeholder = "选择账号",
|
||||
className = "",
|
||||
visible,
|
||||
onVisibleChange,
|
||||
selectedListMaxHeight = 300,
|
||||
showInput = true,
|
||||
showSelectedList = true,
|
||||
readonly = false,
|
||||
onConfirm,
|
||||
}: AccountSelectionProps) {
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
|
||||
// 受控弹窗逻辑
|
||||
const realVisible = visible !== undefined ? visible : popupVisible;
|
||||
const setRealVisible = (v: boolean) => {
|
||||
if (onVisibleChange) onVisibleChange(v);
|
||||
if (visible === undefined) setPopupVisible(v);
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openPopup = () => {
|
||||
if (readonly) return;
|
||||
setRealVisible(true);
|
||||
};
|
||||
|
||||
// 获取显示文本
|
||||
const getDisplayText = () => {
|
||||
if (selectedOptions.length === 0) return "";
|
||||
return `已选择 ${selectedOptions.length} 个账号`;
|
||||
};
|
||||
|
||||
// 删除已选账号
|
||||
const handleRemoveAccount = (id: number) => {
|
||||
if (readonly) return;
|
||||
onSelect(selectedOptions.filter(d => d.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 输入框 */}
|
||||
{showInput && (
|
||||
<div className={`${style.inputWrapper} ${className}`}>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={getDisplayText()}
|
||||
onClick={openPopup}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear={!readonly}
|
||||
size="large"
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
style={
|
||||
readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 已选账号列表窗口 */}
|
||||
{showSelectedList && selectedOptions.length > 0 && (
|
||||
<div
|
||||
className={style.selectedListWindow}
|
||||
style={{
|
||||
maxHeight: selectedListMaxHeight,
|
||||
overflowY: "auto",
|
||||
marginTop: 8,
|
||||
border: "1px solid #e5e6eb",
|
||||
borderRadius: 8,
|
||||
background: "#fff",
|
||||
}}
|
||||
>
|
||||
{selectedOptions.map(acc => (
|
||||
<div
|
||||
key={acc.id}
|
||||
className={style.selectedListRow}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "4px 8px",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
【{acc.realName}】 {acc.userName}
|
||||
</div>
|
||||
{!readonly && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
color: "#ff4d4f",
|
||||
border: "none",
|
||||
background: "none",
|
||||
minWidth: 24,
|
||||
height: 24,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onClick={() => handleRemoveAccount(acc.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* 弹窗 */}
|
||||
<SelectionPopup
|
||||
visible={realVisible}
|
||||
onVisibleChange={setRealVisible}
|
||||
selectedOptions={selectedOptions}
|
||||
onSelect={onSelect}
|
||||
readonly={readonly}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
248
Cunkebao/src/components/AccountSelection/selectionPopup.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Popup } from "antd-mobile";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import PopupHeader from "@/components/PopuLayout/header";
|
||||
import PopupFooter from "@/components/PopuLayout/footer";
|
||||
import {
|
||||
selectionPopupBodyStyle,
|
||||
selectionPopupLayoutStyle,
|
||||
selectionPopupStyle,
|
||||
} from "@/components/PopuLayout/popupShell";
|
||||
import style from "./index.module.scss";
|
||||
import { getAccountList } from "./api";
|
||||
import { AccountItem, SelectionPopupProps } from "./data";
|
||||
|
||||
export default function SelectionPopup({
|
||||
visible,
|
||||
onVisibleChange,
|
||||
selectedOptions,
|
||||
onSelect,
|
||||
readonly = false,
|
||||
onConfirm,
|
||||
}: SelectionPopupProps) {
|
||||
const [accounts, setAccounts] = useState<AccountItem[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalAccounts, setTotalAccounts] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tempSelectedOptions, setTempSelectedOptions] = useState<AccountItem[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
// 累积已加载过的账号,确保确认时能返回更完整的对象
|
||||
const loadedAccountMapRef = useRef<Map<number, AccountItem>>(new Map());
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
const fetchAccounts = async (page: number, keyword: string = "") => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = { page, limit: pageSize };
|
||||
if (keyword.trim()) params.keyword = keyword.trim();
|
||||
|
||||
const response = await getAccountList(params);
|
||||
if (response && response.list) {
|
||||
setAccounts(response.list);
|
||||
const total: number = response.total || response.list.length || 0;
|
||||
setTotalAccounts(total);
|
||||
setTotalPages(Math.max(1, Math.ceil(total / pageSize)));
|
||||
|
||||
// 累积到映射表
|
||||
response.list.forEach((acc: AccountItem) => {
|
||||
loadedAccountMapRef.current.set(acc.id, acc);
|
||||
});
|
||||
} else {
|
||||
setAccounts([]);
|
||||
setTotalAccounts(0);
|
||||
setTotalPages(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取账号列表失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountToggle = (account: AccountItem) => {
|
||||
if (readonly || !onSelect) return;
|
||||
const isSelected = tempSelectedOptions.some(opt => opt.id === account.id);
|
||||
const next = isSelected
|
||||
? tempSelectedOptions.filter(opt => opt.id !== account.id)
|
||||
: tempSelectedOptions.concat(account);
|
||||
setTempSelectedOptions(next);
|
||||
};
|
||||
|
||||
// 全选当前页
|
||||
const handleSelectAllCurrentPage = (checked: boolean) => {
|
||||
if (readonly) return;
|
||||
|
||||
if (checked) {
|
||||
// 全选:添加当前页面所有未选中的账号
|
||||
const currentPageAccounts = accounts.filter(
|
||||
account => !tempSelectedOptions.some(a => a.id === account.id),
|
||||
);
|
||||
setTempSelectedOptions(prev => [...prev, ...currentPageAccounts]);
|
||||
} else {
|
||||
// 取消全选:移除当前页面的所有账号
|
||||
const currentPageAccountIds = accounts.map(a => a.id);
|
||||
setTempSelectedOptions(prev =>
|
||||
prev.filter(a => !currentPageAccountIds.includes(a.id)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 检查当前页是否全选
|
||||
const isCurrentPageAllSelected =
|
||||
accounts.length > 0 &&
|
||||
accounts.every(account =>
|
||||
tempSelectedOptions.some(a => a.id === account.id),
|
||||
);
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (onConfirm) {
|
||||
onConfirm(tempSelectedOptions);
|
||||
}
|
||||
if (onSelect) {
|
||||
onSelect(tempSelectedOptions);
|
||||
}
|
||||
onVisibleChange(false);
|
||||
};
|
||||
|
||||
// 弹窗打开时初始化数据
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setCurrentPage(1);
|
||||
setSearchQuery("");
|
||||
loadedAccountMapRef.current.clear();
|
||||
// 复制一份selectedOptions到临时变量
|
||||
setTempSelectedOptions([...selectedOptions]);
|
||||
fetchAccounts(1, "");
|
||||
}
|
||||
}, [visible, selectedOptions]);
|
||||
|
||||
// 搜索防抖
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
if (searchQuery === "") return;
|
||||
const timer = setTimeout(() => {
|
||||
setCurrentPage(1);
|
||||
fetchAccounts(1, searchQuery);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchQuery, visible]);
|
||||
|
||||
// 页码变化
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
fetchAccounts(currentPage, searchQuery);
|
||||
}, [currentPage, visible, searchQuery]);
|
||||
|
||||
const selectedIdSet = useMemo(
|
||||
() => new Set(tempSelectedOptions.map(opt => opt.id)),
|
||||
[tempSelectedOptions],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible && !readonly}
|
||||
onMaskClick={() => onVisibleChange(false)}
|
||||
position="bottom"
|
||||
style={selectionPopupStyle}
|
||||
bodyStyle={selectionPopupBodyStyle}
|
||||
closeOnMaskClick
|
||||
destroyOnClose
|
||||
>
|
||||
<Layout
|
||||
embedded
|
||||
style={selectionPopupLayoutStyle}
|
||||
header={
|
||||
<PopupHeader
|
||||
title="选择账号"
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
searchPlaceholder="搜索账号"
|
||||
loading={loading}
|
||||
onRefresh={() => fetchAccounts(currentPage, searchQuery)}
|
||||
onClose={() => onVisibleChange(false)}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<PopupFooter
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
loading={loading}
|
||||
selectedCount={tempSelectedOptions.length}
|
||||
onPageChange={setCurrentPage}
|
||||
onCancel={() => onVisibleChange(false)}
|
||||
onConfirm={handleConfirm}
|
||||
isAllSelected={isCurrentPageAllSelected}
|
||||
onSelectAll={handleSelectAllCurrentPage}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style.friendList}>
|
||||
{loading ? (
|
||||
<div className={style.loadingBox}>
|
||||
<div className={style.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : accounts.length > 0 ? (
|
||||
<div className={style.friendListInner}>
|
||||
{accounts.map(acc => (
|
||||
<label
|
||||
key={acc.id}
|
||||
className={style.friendItem}
|
||||
onClick={() => !readonly && handleAccountToggle(acc)}
|
||||
>
|
||||
<div className={style.radioWrapper}>
|
||||
<div
|
||||
className={
|
||||
selectedIdSet.has(acc.id)
|
||||
? style.radioSelected
|
||||
: style.radioUnselected
|
||||
}
|
||||
>
|
||||
{selectedIdSet.has(acc.id) && (
|
||||
<div className={style.radioDot}></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.friendInfo}>
|
||||
<div className={style.friendAvatar}>
|
||||
{acc.avatar ? (
|
||||
<img
|
||||
src={acc.avatar}
|
||||
alt={acc.userName}
|
||||
className={style.avatarImg}
|
||||
/>
|
||||
) : (
|
||||
(acc.userName?.charAt(0) ?? "?")
|
||||
)}
|
||||
</div>
|
||||
<div className={style.friendDetail}>
|
||||
<div className={style.friendName}>{acc.userName}</div>
|
||||
<div className={style.friendId}>
|
||||
真实姓名: {acc.realName}
|
||||
</div>
|
||||
<div className={style.friendId}>
|
||||
部门: {acc.departmentName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={style.emptyBox}>
|
||||
<div className={style.emptyText}>
|
||||
{searchQuery
|
||||
? `没有找到包含"${searchQuery}"的账号`
|
||||
: "没有找到账号"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
228
Cunkebao/src/components/AndroidCompatibilityCheck.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface AndroidCompatibilityInfo {
|
||||
isAndroid: boolean;
|
||||
androidVersion: number;
|
||||
chromeVersion: number;
|
||||
webViewVersion: number;
|
||||
issues: string[];
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
const AndroidCompatibilityCheck: React.FC = () => {
|
||||
const [compatibility, setCompatibility] = useState<AndroidCompatibilityInfo>({
|
||||
isAndroid: false,
|
||||
androidVersion: 0,
|
||||
chromeVersion: 0,
|
||||
webViewVersion: 0,
|
||||
issues: [],
|
||||
suggestions: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const checkAndroidCompatibility = () => {
|
||||
const ua = navigator.userAgent;
|
||||
const issues: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
let isAndroid = false;
|
||||
let androidVersion = 0;
|
||||
let chromeVersion = 0;
|
||||
let webViewVersion = 0;
|
||||
|
||||
// 检测Android系统
|
||||
if (ua.indexOf("Android") > -1) {
|
||||
isAndroid = true;
|
||||
const androidMatch = ua.match(/Android\s+(\d+)/);
|
||||
if (androidMatch) {
|
||||
androidVersion = parseInt(androidMatch[1]);
|
||||
}
|
||||
|
||||
// 检测Chrome版本
|
||||
const chromeMatch = ua.match(/Chrome\/(\d+)/);
|
||||
if (chromeMatch) {
|
||||
chromeVersion = parseInt(chromeMatch[1]);
|
||||
}
|
||||
|
||||
// 检测WebView版本
|
||||
const webViewMatch = ua.match(/Version\/\d+\.\d+/);
|
||||
if (webViewMatch) {
|
||||
const versionMatch = webViewMatch[0].match(/\d+/);
|
||||
if (versionMatch) {
|
||||
webViewVersion = parseInt(versionMatch[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Android 7 (API 24) 兼容性检查
|
||||
if (androidVersion === 7) {
|
||||
issues.push("Android 7 系统对ES6+特性支持不完整");
|
||||
suggestions.push("建议升级到Android 8+或使用最新版Chrome");
|
||||
}
|
||||
|
||||
// Android 6 (API 23) 兼容性检查
|
||||
if (androidVersion === 6) {
|
||||
issues.push("Android 6 系统对现代Web特性支持有限");
|
||||
suggestions.push("强烈建议升级系统或使用最新版Chrome");
|
||||
}
|
||||
|
||||
// Chrome版本检查
|
||||
if (chromeVersion > 0 && chromeVersion < 50) {
|
||||
issues.push(`Chrome版本过低 (${chromeVersion}),建议升级到50+`);
|
||||
suggestions.push("请在Google Play商店更新Chrome浏览器");
|
||||
}
|
||||
|
||||
// WebView版本检查
|
||||
if (webViewVersion > 0 && webViewVersion < 50) {
|
||||
issues.push(`WebView版本过低 (${webViewVersion}),可能影响应用功能`);
|
||||
suggestions.push("建议使用Chrome浏览器或更新系统WebView");
|
||||
}
|
||||
|
||||
// 检测特定问题
|
||||
const features = {
|
||||
Promise: typeof Promise !== "undefined",
|
||||
fetch: typeof fetch !== "undefined",
|
||||
"Array.from": typeof Array.from !== "undefined",
|
||||
"Object.assign": typeof Object.assign !== "undefined",
|
||||
"String.includes": typeof String.prototype.includes !== "undefined",
|
||||
"Array.includes": typeof Array.prototype.includes !== "undefined",
|
||||
requestAnimationFrame: typeof requestAnimationFrame !== "undefined",
|
||||
IntersectionObserver: typeof IntersectionObserver !== "undefined",
|
||||
ResizeObserver: typeof ResizeObserver !== "undefined",
|
||||
URLSearchParams: typeof URLSearchParams !== "undefined",
|
||||
TextEncoder: typeof TextEncoder !== "undefined",
|
||||
AbortController: typeof AbortController !== "undefined",
|
||||
};
|
||||
|
||||
Object.entries(features).forEach(([feature, supported]) => {
|
||||
if (!supported) {
|
||||
issues.push(`${feature} 特性不支持`);
|
||||
}
|
||||
});
|
||||
|
||||
// 微信内置浏览器检测
|
||||
if (ua.indexOf("MicroMessenger") > -1) {
|
||||
issues.push("微信内置浏览器对某些Web特性支持有限");
|
||||
suggestions.push("建议在系统浏览器中打开以获得最佳体验");
|
||||
}
|
||||
|
||||
// QQ内置浏览器检测
|
||||
if (ua.indexOf("QQ/") > -1) {
|
||||
issues.push("QQ内置浏览器对某些Web特性支持有限");
|
||||
suggestions.push("建议在系统浏览器中打开以获得最佳体验");
|
||||
}
|
||||
}
|
||||
|
||||
setCompatibility({
|
||||
isAndroid,
|
||||
androidVersion,
|
||||
chromeVersion,
|
||||
webViewVersion,
|
||||
issues,
|
||||
suggestions,
|
||||
});
|
||||
};
|
||||
|
||||
checkAndroidCompatibility();
|
||||
}, []);
|
||||
|
||||
if (!compatibility.isAndroid || compatibility.issues.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: "#fff3cd",
|
||||
border: "1px solid #ffeaa7",
|
||||
padding: "15px",
|
||||
zIndex: 9999,
|
||||
textAlign: "center",
|
||||
fontSize: "14px",
|
||||
maxHeight: "50vh",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ fontWeight: "bold", marginBottom: "10px", color: "#856404" }}
|
||||
>
|
||||
🚨 Android 兼容性警告
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "8px", fontSize: "12px" }}>
|
||||
系统版本: Android {compatibility.androidVersion}
|
||||
{compatibility.chromeVersion > 0 &&
|
||||
` | Chrome: ${compatibility.chromeVersion}`}
|
||||
{compatibility.webViewVersion > 0 &&
|
||||
` | WebView: ${compatibility.webViewVersion}`}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
<div
|
||||
style={{ fontWeight: "bold", marginBottom: "5px", color: "#856404" }}
|
||||
>
|
||||
检测到的问题:
|
||||
</div>
|
||||
<div style={{ color: "#856404", fontSize: "12px" }}>
|
||||
{compatibility.issues.map((issue, index) => (
|
||||
<div key={index} style={{ marginBottom: "3px" }}>
|
||||
• {issue}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{compatibility.suggestions.length > 0 && (
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
marginBottom: "5px",
|
||||
color: "#155724",
|
||||
}}
|
||||
>
|
||||
建议解决方案:
|
||||
</div>
|
||||
<div style={{ color: "#155724", fontSize: "12px" }}>
|
||||
{compatibility.suggestions.map((suggestion, index) => (
|
||||
<div key={index} style={{ marginBottom: "3px" }}>
|
||||
• {suggestion}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ fontSize: "11px", color: "#6c757d", marginTop: "10px" }}>
|
||||
💡 应用已启用兼容模式,但建议升级系统以获得最佳体验
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const element = document.querySelector(
|
||||
'[style*="position: fixed"][style*="top: 0"]',
|
||||
) as HTMLElement;
|
||||
if (element) {
|
||||
element.style.display = "none";
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "5px",
|
||||
right: "10px",
|
||||
background: "none",
|
||||
border: "none",
|
||||
fontSize: "18px",
|
||||
cursor: "pointer",
|
||||
color: "#856404",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AndroidCompatibilityCheck;
|
||||
@@ -1,92 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ChevronLeft, ArrowLeft } from 'lucide-react';
|
||||
|
||||
interface BackButtonProps {
|
||||
/** 返回按钮的样式变体 */
|
||||
variant?: 'icon' | 'button' | 'text';
|
||||
/** 自定义返回逻辑,如果不提供则使用navigate(-1) */
|
||||
onBack?: () => void;
|
||||
/** 按钮文本,仅在button和text变体时使用 */
|
||||
text?: string;
|
||||
/** 自定义CSS类名 */
|
||||
className?: string;
|
||||
/** 图标大小 */
|
||||
iconSize?: number;
|
||||
/** 是否显示图标 */
|
||||
showIcon?: boolean;
|
||||
/** 自定义图标 */
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用返回上一页按钮组件
|
||||
* 使用React Router的navigate方法实现返回功能
|
||||
*/
|
||||
export const BackButton: React.FC<BackButtonProps> = ({
|
||||
variant = 'icon',
|
||||
onBack,
|
||||
text = '返回',
|
||||
className = '',
|
||||
iconSize = 6,
|
||||
showIcon = true,
|
||||
icon
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleBack = () => {
|
||||
if (onBack) {
|
||||
onBack();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultIcon = variant === 'icon' ? (
|
||||
<ChevronLeft className={`h-${iconSize} w-${iconSize}`} />
|
||||
) : (
|
||||
<ArrowLeft className={`h-${iconSize} w-${iconSize}`} />
|
||||
);
|
||||
|
||||
const buttonIcon = icon || (showIcon ? defaultIcon : null);
|
||||
|
||||
switch (variant) {
|
||||
case 'icon':
|
||||
return (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className={`p-2 hover:bg-gray-100 rounded-lg transition-colors ${className}`}
|
||||
title="返回上一页"
|
||||
>
|
||||
{buttonIcon}
|
||||
</button>
|
||||
);
|
||||
|
||||
case 'button':
|
||||
return (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className={`flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors ${className}`}
|
||||
>
|
||||
{buttonIcon}
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
|
||||
case 'text':
|
||||
return (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className={`flex items-center gap-2 text-blue-600 hover:text-blue-700 transition-colors ${className}`}
|
||||
>
|
||||
{buttonIcon}
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default BackButton;
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Home, Users, LayoutGrid, User } from 'lucide-react';
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
id: "home",
|
||||
name: "首页",
|
||||
href: "/",
|
||||
icon: Home,
|
||||
active: (pathname: string) => pathname === "/",
|
||||
},
|
||||
{
|
||||
id: "scenarios",
|
||||
name: "场景获客",
|
||||
href: "/scenarios",
|
||||
icon: Users,
|
||||
active: (pathname: string) => pathname.startsWith("/scenarios"),
|
||||
},
|
||||
{
|
||||
id: "workspace",
|
||||
name: "工作台",
|
||||
href: "/workspace",
|
||||
icon: LayoutGrid,
|
||||
active: (pathname: string) => pathname.startsWith("/workspace"),
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
name: "我的",
|
||||
href: "/profile",
|
||||
icon: User,
|
||||
active: (pathname: string) => pathname.startsWith("/profile"),
|
||||
},
|
||||
];
|
||||
|
||||
interface BottomNavProps {
|
||||
activeTab?: string;
|
||||
}
|
||||
|
||||
export default function BottomNav({ activeTab }: BottomNavProps) {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 safe-area-pb">
|
||||
<div className="flex justify-around items-center h-16 max-w-md mx-auto">
|
||||
{navItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
const isActive = activeTab ? activeTab === item.id : item.active(location.pathname);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
className={`flex flex-col items-center justify-center flex-1 h-full transition-colors ${
|
||||
isActive ? "text-blue-500" : "text-gray-500 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<IconComponent className="w-5 h-5" />
|
||||
<span className="text-xs mt-1">{item.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
Cunkebao/src/components/CompatibilityCheck.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface CompatibilityInfo {
|
||||
isCompatible: boolean;
|
||||
browser: string;
|
||||
version: string;
|
||||
issues: string[];
|
||||
}
|
||||
|
||||
const CompatibilityCheck: React.FC = () => {
|
||||
const [compatibility, setCompatibility] = useState<CompatibilityInfo>({
|
||||
isCompatible: true,
|
||||
browser: "",
|
||||
version: "",
|
||||
issues: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const checkCompatibility = () => {
|
||||
const ua = navigator.userAgent;
|
||||
const issues: string[] = [];
|
||||
let browser = "Unknown";
|
||||
let version = "Unknown";
|
||||
|
||||
// 检测浏览器类型和版本
|
||||
if (ua.indexOf("Chrome") > -1) {
|
||||
browser = "Chrome";
|
||||
const match = ua.match(/Chrome\/(\d+)/);
|
||||
version = match ? match[1] : "Unknown";
|
||||
if (parseInt(version) < 50) {
|
||||
issues.push("Chrome版本过低,建议升级到50+");
|
||||
}
|
||||
} else if (ua.indexOf("Firefox") > -1) {
|
||||
browser = "Firefox";
|
||||
const match = ua.match(/Firefox\/(\d+)/);
|
||||
version = match ? match[1] : "Unknown";
|
||||
if (parseInt(version) < 50) {
|
||||
issues.push("Firefox版本过低,建议升级到50+");
|
||||
}
|
||||
} else if (ua.indexOf("Safari") > -1 && ua.indexOf("Chrome") === -1) {
|
||||
browser = "Safari";
|
||||
const match = ua.match(/Version\/(\d+)/);
|
||||
version = match ? match[1] : "Unknown";
|
||||
if (parseInt(version) < 10) {
|
||||
issues.push("Safari版本过低,建议升级到10+");
|
||||
}
|
||||
} else if (ua.indexOf("MSIE") > -1 || ua.indexOf("Trident") > -1) {
|
||||
browser = "Internet Explorer";
|
||||
const match = ua.match(/(?:MSIE |rv:)(\d+)/);
|
||||
version = match ? match[1] : "Unknown";
|
||||
issues.push("Internet Explorer不受支持,建议使用现代浏览器");
|
||||
} else if (ua.indexOf("Edge") > -1) {
|
||||
browser = "Edge";
|
||||
const match = ua.match(/Edge\/(\d+)/);
|
||||
version = match ? match[1] : "Unknown";
|
||||
if (parseInt(version) < 12) {
|
||||
issues.push("Edge版本过低,建议升级到12+");
|
||||
}
|
||||
}
|
||||
|
||||
// 检测ES6+特性支持
|
||||
const features = {
|
||||
Promise: typeof Promise !== "undefined",
|
||||
fetch: typeof fetch !== "undefined",
|
||||
"Array.from": typeof Array.from !== "undefined",
|
||||
"Object.assign": typeof Object.assign !== "undefined",
|
||||
"String.includes": typeof String.prototype.includes !== "undefined",
|
||||
"Array.includes": typeof Array.prototype.includes !== "undefined",
|
||||
};
|
||||
|
||||
Object.entries(features).forEach(([feature, supported]) => {
|
||||
if (!supported) {
|
||||
issues.push(`${feature} 特性不支持`);
|
||||
}
|
||||
});
|
||||
|
||||
setCompatibility({
|
||||
isCompatible: issues.length === 0,
|
||||
browser,
|
||||
version,
|
||||
issues,
|
||||
});
|
||||
};
|
||||
|
||||
checkCompatibility();
|
||||
}, []);
|
||||
|
||||
if (compatibility.isCompatible) {
|
||||
return null; // 兼容时不需要显示
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: "#fff3cd",
|
||||
border: "1px solid #ffeaa7",
|
||||
padding: "10px",
|
||||
zIndex: 9999,
|
||||
textAlign: "center",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "5px" }}>
|
||||
浏览器兼容性警告
|
||||
</div>
|
||||
<div style={{ marginBottom: "5px" }}>
|
||||
当前浏览器: {compatibility.browser} {compatibility.version}
|
||||
</div>
|
||||
<div style={{ color: "#856404" }}>
|
||||
{compatibility.issues.map((issue, index) => (
|
||||
<div key={index}>{issue}</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: "10px", fontSize: "12px" }}>
|
||||
建议使用 Chrome 50+、Firefox 50+、Safari 10+ 或 Edge 12+
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompatibilityCheck;
|
||||
@@ -1,210 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Search, RefreshCw, Loader2 } from "lucide-react";
|
||||
import { fetchContentLibraryList } from "@/api/content";
|
||||
import { ContentLibrary } from "@/api/content";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
|
||||
interface ContentLibrarySelectionDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
selectedLibraries: string[];
|
||||
onSelect: (libraries: string[]) => void;
|
||||
}
|
||||
|
||||
export function ContentLibrarySelectionDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedLibraries,
|
||||
onSelect,
|
||||
}: ContentLibrarySelectionDialogProps) {
|
||||
const { toast } = useToast();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [libraries, setLibraries] = useState<ContentLibrary[]>([]);
|
||||
const [tempSelected, setTempSelected] = useState<string[]>([]);
|
||||
|
||||
// 获取内容库列表
|
||||
const fetchLibraries = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetchContentLibraryList(1, 100, searchQuery);
|
||||
if (response.code === 200 && response.data) {
|
||||
setLibraries(response.data.list);
|
||||
} else {
|
||||
toast({
|
||||
title: "获取内容库列表失败",
|
||||
description: response.msg,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取内容库列表失败:", error);
|
||||
toast({
|
||||
title: "获取内容库列表失败",
|
||||
description: "请检查网络连接",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchQuery, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchLibraries();
|
||||
setTempSelected(selectedLibraries);
|
||||
}
|
||||
}, [open, selectedLibraries, fetchLibraries]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchLibraries();
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (tempSelected.length === libraries.length) {
|
||||
setTempSelected([]);
|
||||
} else {
|
||||
setTempSelected(libraries.map((lib) => lib.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleLibraryToggle = (libraryId: string) => {
|
||||
setTempSelected((prev) =>
|
||||
prev.includes(libraryId)
|
||||
? prev.filter((id) => id !== libraryId)
|
||||
: [...prev, libraryId]
|
||||
);
|
||||
};
|
||||
|
||||
const handleDialogOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setTempSelected(selectedLibraries);
|
||||
}
|
||||
onOpenChange(open);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
onSelect(tempSelected);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||
<DialogContent className="flex flex-col bg-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择内容库</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex items-center space-x-2 my-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索内容库"
|
||||
className="pl-9"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="text-sm text-gray-500">
|
||||
已选择 {tempSelected.length} 个内容库
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSelectAll}
|
||||
disabled={loading || libraries.length === 0}
|
||||
>
|
||||
{tempSelected.length === libraries.length ? "取消全选" : "全选"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto -mx-6 px-6 max-h-[400px]">
|
||||
<div className="space-y-2">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
加载中...
|
||||
</div>
|
||||
) : libraries.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
暂无数据
|
||||
</div>
|
||||
) : (
|
||||
libraries.map((library) => (
|
||||
<label
|
||||
key={library.id}
|
||||
className="flex items-start space-x-3 p-4 rounded-lg hover:bg-gray-50 cursor-pointer border"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tempSelected.includes(library.id)}
|
||||
onChange={() => handleLibraryToggle(library.id)}
|
||||
className="mt-1 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{library.name}</span>
|
||||
<Badge variant="outline">
|
||||
{library.sourceType === 1
|
||||
? "文本"
|
||||
: library.sourceType === 2
|
||||
? "图片"
|
||||
: "视频"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
<div>创建人: {library.creatorName || "-"}</div>
|
||||
<div>
|
||||
更新时间:{" "}
|
||||
{new Date(library.updateTime).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mt-4 pt-4 border-t">
|
||||
<div className="text-sm text-gray-500">
|
||||
已选择 {tempSelected.length} 个内容库
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleConfirm}>
|
||||
确定{tempSelected.length > 0 ? ` (${tempSelected.length})` : ""}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
5
Cunkebao/src/components/ContentSelection/api.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
export function getContentLibraryList(params: any) {
|
||||
return request("/v1/content/library/list", { ...params, formType: 0 }, "GET");
|
||||
}
|
||||
21
Cunkebao/src/components/ContentSelection/data.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// 内容库接口类型
|
||||
export interface ContentItem {
|
||||
id: number;
|
||||
name: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 组件属性接口
|
||||
export interface ContentSelectionProps {
|
||||
selectedOptions: ContentItem[];
|
||||
onSelect: (selectedItems: ContentItem[]) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
visible?: boolean;
|
||||
onVisibleChange?: (visible: boolean) => void;
|
||||
selectedListMaxHeight?: number;
|
||||
showInput?: boolean;
|
||||
showSelectedList?: boolean;
|
||||
readonly?: boolean;
|
||||
onConfirm?: (selectedItems: ContentItem[]) => void;
|
||||
}
|
||||