mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-08-23 15:57:14 +00:00
Compare commits
1148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b18b8ccb02 | |||
| f8a01cb9a2 | |||
| 64832a2a04 | |||
| b1dd781a3d | |||
| 71dc082e40 | |||
| 10836bbb4a | |||
| d8237ae301 | |||
| 3974f1be87 | |||
| 126c9921a8 | |||
| c84754fc0c | |||
| 172807d326 | |||
| 72608af77e | |||
| 7879459c3f | |||
| ad482dad7f | |||
| 58e99976b3 | |||
| 567803ebeb | |||
| 8e3d67cc7f | |||
| b463608ab7 | |||
| eefd36562d | |||
| ebf4497b34 | |||
| 194b0343bf | |||
| adb1dfbded | |||
| 9e689f6022 | |||
| 499f5c915f | |||
| a9f11d79a9 | |||
| db5e9ea2d0 | |||
| 31fbf7e48a | |||
| e2e24078c5 | |||
| 39ef92e1ce | |||
| f63ba187dd | |||
| 23d1ac1021 | |||
| f08329eef4 | |||
| e993a582cd | |||
| 1524e189dc | |||
| 2b5165324c | |||
| 8f67826072 | |||
| ac9a31f049 | |||
| ed2cdbbc31 | |||
| 8bd2feaf7e | |||
| 4eccfe6d2c | |||
| 5ba2e000a1 | |||
| fe9adadb3a | |||
| 805c2a045e | |||
| fd7b196134 | |||
| 1b1c327c35 | |||
| a228f603d2 | |||
| 7d0a05ee11 | |||
| 5f0a62b63e | |||
| 825a1b1027 | |||
| 38dde1a373 | |||
| 950e7d1b00 | |||
| 89ded74345 | |||
| d8f9f48278 | |||
| 818bb38617 | |||
| bdd76addf3 | |||
| 0202d75ff4 | |||
| 2936f21f12 | |||
| a0831ec6cd | |||
| 705ca4d20a | |||
| 2926aab7a4 | |||
| 6a066f1b8e | |||
| 775fb1f853 | |||
| b7d137247a | |||
| 22278c40bf | |||
| 4373642ebd | |||
| e29b32bd2d | |||
| 5bf90920f5 | |||
| fa6bb5e46a | |||
| edcbb3e226 | |||
| cebf8497a4 | |||
| 5213bdf08b | |||
| cded4bdcc7 | |||
| cf817fd8ea | |||
| 596b700d63 | |||
| a2481ff1cf | |||
| 59dfa95bf8 | |||
| bc7d06d3e5 | |||
| f6b5a94b29 | |||
| 8e81dfa12a | |||
| 188fb23f08 | |||
| 0ff76f0f21 | |||
| 5347a12035 | |||
| 787caa84c8 | |||
| 691d453f41 | |||
| c2503e663a | |||
| 5b7f2603ae | |||
| 405a88862b | |||
| 3fd7f810f0 | |||
| 296eabe09a | |||
| e04a48623d | |||
| 2e1bad387c | |||
| 54b45ec2ff | |||
| 5c18a50330 | |||
| c434f85045 | |||
| d78d3ffe02 | |||
| 4d10279870 | |||
| 44240f65d5 | |||
| 97e81a7dcc | |||
| 9e8f1ed6bf | |||
| 4dbfdab50d | |||
| c39814ce2b | |||
| 95a071014c | |||
| 6e03f4b363 | |||
| 9de9489673 | |||
| bb8644dea0 | |||
| ff9142ddd4 | |||
| 9814fec930 | |||
| 35f469fb82 | |||
| 53ba731159 | |||
| 705ad58a8f | |||
| c96f86fbbf | |||
| 4b3b64e9e2 | |||
| 2f2e146951 | |||
| a09c529414 | |||
| a8d1d58e95 | |||
| 6695be815e | |||
| fbd3478772 | |||
| 2102e1afbb | |||
| 155c56f502 | |||
| 37a9b0e485 | |||
| 52a3f13f1e | |||
| a678a11c33 | |||
| d34b785238 | |||
| 1086e1d631 | |||
| b2f57aa483 | |||
| 3094b9c8fd | |||
| 4c2dba1004 | |||
| 0f0a4c0a7e | |||
| c374126f69 | |||
| 8498cd71dc | |||
| bf30517393 | |||
| 495f86e7fc | |||
| 471017657f | |||
| 4861dd75be | |||
| 342ceea371 | |||
| 880c34dfee | |||
| a1e487100d | |||
| 77948b1e16 | |||
| e28a12a1ee | |||
| 00a8bc6784 | |||
| 8fffa60569 | |||
| 2debe7e927 | |||
| 478bc32ddd | |||
| dfd2be1265 | |||
| 33ff2e29e5 | |||
| 4df1c7a136 | |||
| c522248d39 | |||
| 246d96ee63 | |||
| 909ae4aa00 | |||
| 26e3ababcf | |||
| 4d9f89f630 | |||
| b989199edb | |||
| 0d81fe6c8e | |||
| be45f41e34 | |||
| e9ac58b1ef | |||
| 59d9ae96ac | |||
| 3cff6f7189 | |||
| 14aee28289 | |||
| 8c1b4d4516 | |||
| 068df8fa15 | |||
| e371310d02 | |||
| 96b8121210 | |||
| 1960a85ead | |||
| 69cb479e01 | |||
| db90131bd6 | |||
| c18d272413 | |||
| cd0952e170 | |||
| e6a6d0485d | |||
| a642ae0332 | |||
| 67f508c7da | |||
| 2225f09797 | |||
| fe7fa46a0c | |||
| 6d8901fa21 | |||
| 7750cb0021 | |||
| 327d68a347 | |||
| 7c272b0efd | |||
| 5eb4ed157e | |||
| d4e00b960c | |||
| 79adc871ef | |||
| af0eff88b8 | |||
| c755befa28 | |||
| 3f6890da27 | |||
| 8144fada25 | |||
| 08319391ea | |||
| ae7f1c03e2 | |||
| 151017ed47 | |||
| 941d15b635 | |||
| d8058400dd | |||
| 5a1ed953de | |||
| 58d127b602 | |||
| 13bf73e6cf | |||
| aa46fdb113 | |||
| 4a09da3a25 | |||
| d70fcbb9f3 | |||
| a16ef6476d | |||
| dae75343d0 | |||
| d7564127d2 | |||
| c6bf54fd9d | |||
| 8d47072e1c | |||
| d601226187 | |||
| 4801832d9d | |||
| 6b0f42d0b8 | |||
| 08379c21ac | |||
| 2499bd9ad3 | |||
| ad6da0d320 | |||
| a2583c0591 | |||
| 0ea4a29152 | |||
| 7fe50788a5 | |||
| c4a68076d6 | |||
| 9e365895af | |||
| 9ece25e17d | |||
| 2140eff51b | |||
| dfba3c30a5 | |||
| 4b717109d2 | |||
| e54e908fbc | |||
| 7a11a9ef15 | |||
| c9d0700fd9 | |||
| f9b809801d | |||
| cc551ba266 | |||
| 754ba02263 | |||
| 23787ff462 | |||
| 7ddf57ae06 | |||
| 9f603c5e77 | |||
| 3d720bdd81 | |||
| cc5180a6f7 | |||
| a8eccbe43b | |||
| 2ac77ac39f | |||
| 4eaa518cf3 | |||
| 5622f94fc8 | |||
| 95457b7dcd | |||
| 6a9de72c78 | |||
| 088a614160 | |||
| 013ee98f53 | |||
| dc46ed0e05 | |||
| 2a0c657ca3 | |||
| a0aee80c63 | |||
| 4910be3403 | |||
| 1541d74c84 | |||
| 34c9151dc1 | |||
| 6c69770ed6 | |||
| 5b7c38c67f | |||
| b7bec8ecb7 | |||
| 82fe9c3596 | |||
| 9f44c34d34 | |||
| afd84516b0 | |||
| 024f00b73c | |||
| 24a21bf2ee | |||
| f22c6bf658 | |||
| 0df700ec18 | |||
| 46141f87b8 | |||
| eecce10018 | |||
| b793b81768 | |||
| c8506e3d3b | |||
| bddd611cc1 | |||
| b399fc557a | |||
| 233f6e00f0 | |||
| b20ea734fd | |||
| b7dba68549 | |||
| 7ab0acd6a4 | |||
| 1d60ccc516 | |||
| 2a8d2213b1 | |||
| a27ce36a32 | |||
| 3fdcc895ed | |||
| 88e510d07a | |||
| 2526feb0d9 | |||
| 4171b2bdfb | |||
| 0611f8de6c | |||
| df9e495dc5 | |||
| 8a48476105 | |||
| 9d4a1705a5 | |||
| 871e5733c7 | |||
| bdea12c51a | |||
| e8a663e3c7 | |||
| a27d9ea259 | |||
| 422d439627 | |||
| 7cd824c284 | |||
| edeff3c169 | |||
| e27d95e2b5 | |||
| 13076421a1 | |||
| 3943e7f05f | |||
| 6839827db0 | |||
| cd0d9dad98 | |||
| 857da34b9c | |||
| ccad7e7bb5 | |||
| b8de15d66e | |||
| c02661ea29 | |||
| 3c70c8ae59 | |||
| d6a04f96fe | |||
| b985b62068 | |||
| 297b760293 | |||
| 5f820b9dc1 | |||
| 088abfe7ab | |||
| 17340ae0ac | |||
| fe687af8ca | |||
| 7e26029268 | |||
| 03f7f2a53e | |||
| 296260bf6a | |||
| dd07acd21a | |||
| 89f6402fbf | |||
| 6788edbe9d | |||
| 853059c814 | |||
| 3895305882 | |||
| 5a0876f8dc | |||
| 2e32238652 | |||
| c2acbaaa94 | |||
| ad1a99aa44 | |||
| 24e4be019a | |||
| 13c917ad7e | |||
| 0a8cf6870f | |||
| 061291cebb | |||
| b8e0d7760b | |||
| 962de0183c | |||
| 627396dbf7 | |||
| 6c300bb018 | |||
| 9273df4af2 | |||
| 4a99be2f15 | |||
| 669d784f54 | |||
| 5253d657b6 | |||
| 27c816cf3b | |||
| c2548c5007 | |||
| 0d81776212 | |||
| bdec823148 | |||
| a6de958daa | |||
| cccab31c0f | |||
| 50c25d4574 | |||
| 68100f7f24 | |||
| 38777ea285 | |||
| 00c8f08179 | |||
| 45d6579fb6 | |||
| dad9254128 | |||
| 4ddf3bf2bf | |||
| 9934143f00 | |||
| f617bde81b | |||
| b2771b7b3f | |||
| 44b3237dd6 | |||
| b987a2d365 | |||
| 1cf299f090 | |||
| c87d78c666 | |||
| 3d37a3d367 | |||
| 79c8d90049 | |||
| 73d8236697 | |||
| 1afc3e5e1c | |||
| 87e8bbfa41 | |||
| 3e3d3e162e | |||
| cc80b87fee | |||
| b614ecccc2 | |||
| 8251c6589b | |||
| 4edf2ea02a | |||
| 12dd4659cd | |||
| 5d4f40c004 | |||
| 3c34e8e0e7 | |||
| 7441a3a717 | |||
| 3eef9a836b | |||
| 922202734a | |||
| fffd7e375f | |||
| d87f09c957 | |||
| b29884f8df | |||
| b270960a04 | |||
| c0ee2c2d8e | |||
| 5c4899df6e | |||
| be1580e949 | |||
| 6424eb871c | |||
| 8a8c43c7a5 | |||
| 9a503ddc72 | |||
| b130466c8f | |||
| 2f0215ac87 | |||
| 3ab5459778 | |||
| dd5cc206e5 | |||
| 4eb1bf2de2 | |||
| 6c47551b03 | |||
| 657ecccee3 | |||
| b4ba70c0f3 | |||
| c2bf5e845a | |||
| 8900e72e45 | |||
| 0cb192135d | |||
| c1ca687630 | |||
| 29cc24508b | |||
| 3db55cbd48 | |||
| c0a7f41747 | |||
| 1ba08bbfa6 | |||
| 51a8f42d89 | |||
| 3b081ff0f4 | |||
| 7f31a301e3 | |||
| 039d949523 | |||
| 5b16dc6ee7 | |||
| 34648c704f | |||
| 1966e69460 | |||
| 74b1b07b31 | |||
| 2e023cb8dc | |||
| 118c0e1ff1 | |||
| e933f32d9c | |||
| 81d075c028 | |||
| 514dd6c76a | |||
| 0b2501c1d8 | |||
| ddc323a8c3 | |||
| 2933c057a2 | |||
| c1d892069e | |||
| 20ed6d1d3d | |||
| 90f5275201 | |||
| fe64b203da | |||
| 9eb1353455 | |||
| 8a1c55c731 | |||
| 9059bebd07 | |||
| d18f79fe74 | |||
| 3a8f0be28a | |||
| 60cf380f96 | |||
| ab8240613e | |||
| d6e9dc6839 | |||
| 6891bdde53 | |||
| bda335212d | |||
| ecadfeab19 | |||
| 06f4cdc649 | |||
| a38d547200 | |||
| a15f431a7f | |||
| 3bb5b66e8b | |||
| 9bf7fa4081 | |||
| d9ff3242c6 | |||
| cd562ab8ed | |||
| 3a8a69ac2e | |||
| 4ca9dfd9c0 | |||
| 0d5b4936bd | |||
| b5f6eaf159 | |||
| adfee8bf58 | |||
| 25c80921e0 | |||
| fbfa2a71a9 | |||
| 5c51ed48c3 | |||
| 22febfc42a | |||
| de482dd9f4 | |||
| 74af67a41f | |||
| 170b41441b | |||
| 4991e50e16 | |||
| 1f236ef929 | |||
| 7f86c5984d | |||
| 6fcee49b19 | |||
| ac527851a0 | |||
| 64bbeaedcb | |||
| 3e984a1bcb | |||
| a7237fe62f | |||
| 79165214d7 | |||
| c3c454b7d7 | |||
| 3f7855dd6e | |||
| d4d708d44b | |||
| f0610281e5 | |||
| 7f0b6a3a46 | |||
| e87e7e2164 | |||
| c2a7c089d2 | |||
| 1a3ad390fd | |||
| df5bd4df60 | |||
| c383367e9f | |||
| 79b6010104 | |||
| 7a9f45c96b | |||
| 97b0a98793 | |||
| 7277cb289f | |||
| 86b2d5eff2 | |||
| 7873847616 | |||
| 5e46f149d9 | |||
| 574fc52332 | |||
| 8c924ca98f | |||
| a9adc0c1a2 | |||
| ed286c8894 | |||
| d900a3d08e | |||
| a55da15202 | |||
| cdf5b66729 | |||
| 9728118f94 | |||
| 9cc2ea412b | |||
| 3f1ad4b7dc | |||
| 56c225bf20 | |||
| 8b68bfb28c | |||
| a5299b52d4 | |||
| 9e9bc52737 | |||
| 76106e4504 | |||
| a6c00c42fa | |||
| 4e23005b5b | |||
| f27e2de220 | |||
| eefcabcff3 | |||
| 63e4669e3f | |||
| 7d5428f775 | |||
| ab3c4562fa | |||
| 13f89bf335 | |||
| e6a18445c3 | |||
| c8ab209426 | |||
| 360fea4085 | |||
| 9794d67eaa | |||
| b60a639312 | |||
| 870706c4ff | |||
| 491471fa10 | |||
| 6dbf61d4e4 | |||
| 81545d192b | |||
| 37cdc26160 | |||
| fb6a35ebe4 | |||
| f7a565bb80 | |||
| bf5e72b7e0 | |||
| ec6186596d | |||
| dece19cec6 | |||
| 6aa1d27711 | |||
| 264e77f383 | |||
| 0cbc284e42 | |||
| 9e42a334fa | |||
| 49438d2ee1 | |||
| 735aa24020 | |||
| 14f63a203d | |||
| e11c0a3633 | |||
| ca83f139f7 | |||
| 58e44b7d12 | |||
| 8f47474edd | |||
| c5200aada8 | |||
| 4e39b93673 | |||
| f215643f2e | |||
| aa72be6ff3 | |||
| b567e56b60 | |||
| 17431c1707 | |||
| 7c9ebef5e9 | |||
| 72b0e11543 | |||
| 9e2af9dbca | |||
| b7cc987132 | |||
| 8476c73b0c | |||
| e76b49afd2 | |||
| c712b95e0d | |||
| 1ee1a8d6d9 | |||
| f01c764589 | |||
| 6df66d150b | |||
| 6132f94eff | |||
| ac5e67ea73 | |||
| 143d2b44d0 | |||
| ef130fe377 | |||
| d7d1e2100c | |||
| af5afd7700 | |||
| a64df5cf0c | |||
| 3866319373 | |||
| ec5ad809f2 | |||
| 268acb174d | |||
| 3733877be1 | |||
| 6d5579a8d6 | |||
| 306cd2f945 | |||
| 5d4dd1e66f | |||
| 5f371bdc4a | |||
| aa58ba392e | |||
| 0485610818 | |||
| 7cb8becc09 | |||
| 8fa535a01b | |||
| 5f202e03a2 | |||
| ef2dfe330b | |||
| fcd86fbebd | |||
| 960d294aa2 | |||
| 27f8e63f3d | |||
| e8d7ad58be | |||
| f4d537e0f5 | |||
| d6c93dd537 | |||
| aa5fff47de | |||
| c920d8423d | |||
| b8fcfe2e7c | |||
| 072c96e1ec | |||
| 38c8ece642 | |||
| 9bf1576fb4 | |||
| d648e177c4 | |||
| e8f62af7f6 | |||
| e746aafa2f | |||
| 71f7a1b166 | |||
| 678f570a90 | |||
| 4bc1f195db | |||
| b2ff49ee94 | |||
| d9558f13ad | |||
| 8dc8f2567e | |||
| 316636f83c | |||
| e3c2fbf82a | |||
| 1b6175e598 | |||
| 26dc479596 | |||
| dca25dbb78 | |||
| c55275872e | |||
| d4d1fb26f6 | |||
| a48cb7bc34 | |||
| 8af6e9290f | |||
| 14b277d813 | |||
| 20915038a3 | |||
| 15723457cb | |||
| be8a0ec184 | |||
| ec79fc933c | |||
| b02e3aad95 | |||
| 3ed5a420a7 | |||
| 08eca511ad | |||
| 711e701933 | |||
| c34e911596 | |||
| e6bbd83c6b | |||
| 8a452c3072 | |||
| b99b310306 | |||
| 13bfb14107 | |||
| e3c46016e0 | |||
| 4188b0969e | |||
| eb739fcccb | |||
| 0c27795a10 | |||
| 9a94505f43 | |||
| d05693c5c1 | |||
| 7c5c3d8a3c | |||
| c0b2063b38 | |||
| 7027f9a55b | |||
| 4d183747b1 | |||
| cf3d8af260 | |||
| 08fe1b2f75 | |||
| 4ea7352674 | |||
| db3e8a267e | |||
| 6e6885849b | |||
| 8fc62682c4 | |||
| 827f2b9739 | |||
| 75031914a3 | |||
| 54d70623ab | |||
| a4c9fdd95a | |||
| ed819e8c79 | |||
| 6a9bfeb5aa | |||
| 94680c4411 | |||
| e654766f60 | |||
| 8bcbb68438 | |||
| 0ef6955f96 | |||
| b9f31d42f4 | |||
| b4501557c9 | |||
| dc2b768c6b | |||
| a2ed99e6cb | |||
| 60df732db7 | |||
| 6bd6bb3885 | |||
| 6d85d128cc | |||
| 399cf65fc9 | |||
| 96785ca037 | |||
| 24906a6df1 | |||
| 7cfc747653 | |||
| d772bbebe6 | |||
| 05f911628a | |||
| 14988853a3 | |||
| 18b718a328 | |||
| 7b3f16ac9f | |||
| 6d9b9bd3f7 | |||
| 82b2755c18 | |||
| 04e990aeb3 | |||
| cbbcd4c571 | |||
| 5ad9adeb4a | |||
| 13c9594be0 | |||
| e4f5f78bfa | |||
| bfa0208692 | |||
| 64994cdeda | |||
| 9796a841d9 | |||
| 404b493d23 | |||
| c04f9eb993 | |||
| 825da42208 | |||
| 721a8f1ecb | |||
| 586a174d6c | |||
| bec6966542 | |||
| b1f4e1d4d8 | |||
| d7d64e8259 | |||
| 08b498fccd | |||
| ecc5a52232 | |||
| 7327b90255 | |||
| 6ce7a9c16f | |||
| eb2658a91c | |||
| 9f59a18756 | |||
| ef06f0da98 | |||
| 4dec0e2c69 | |||
| 5f36f1af11 | |||
| 5a1a596098 | |||
| 7a25c6e062 | |||
| a73ed71b9d | |||
| 2abbb95ea2 | |||
| 1f80ba9463 | |||
| 4327fdac12 | |||
| a6f05a5874 | |||
| 3fb74a1598 | |||
| 2ac44cdeb6 | |||
| 4e4dc4cb73 | |||
| 855a953010 | |||
| 3264fda06e | |||
| 2c7d472069 | |||
| 8182e6797f | |||
| 99cb48ce88 | |||
| e391bfca80 | |||
| 0181ad3715 | |||
| b95dff0751 | |||
| 668d4c9c64 | |||
| 66c0d1b2f7 | |||
| bef0996375 | |||
| 391a1e4cf0 | |||
| 38facf517a | |||
| 5c9b1e8764 | |||
| dbcf14b566 | |||
| cc59f0c761 | |||
| f263fc067a | |||
| 6635b88baa | |||
| 87ea2a611f | |||
| ea8a2135cd | |||
| 76d86395ef | |||
| e268870636 | |||
| 49e9f41ef2 | |||
| 68cda968a1 | |||
| f6826fcefc | |||
| 56a91db7e7 | |||
| 8f4d94f046 | |||
| 9446bf5f98 | |||
| 23e353b18b | |||
| 46daaa4ba1 | |||
| f66d59531d | |||
| b76cd1d5ae | |||
| 55ba0d08a5 | |||
| cc78780a8e | |||
| 4a00809061 | |||
| cd760bbd84 | |||
| 84bdc6be7f | |||
| 8dd6bf8933 | |||
| 08f170d217 | |||
| 20da6a1c20 | |||
| 8586931630 | |||
| 667fc79a6f | |||
| ac7430dded | |||
| 9c73e16560 | |||
| c605e222a7 | |||
| 578f26a738 | |||
| 24218e7570 | |||
| 16f946550e | |||
| 9960a31485 | |||
| 437632f859 | |||
| 98f0f442eb | |||
| 3b999dc5e2 | |||
| 20943ad3d3 | |||
| 3b1544b5e4 | |||
| b317d597ac | |||
| 3bb19811e7 | |||
| 065a01de48 | |||
| 09fb2f6557 | |||
| 9ca97bf4bc | |||
| 0c59503edb | |||
| 6fa08861f8 | |||
| 72b95151a1 | |||
| 378e6ec9af | |||
| 27b68c1174 | |||
| 1ae8eabe99 | |||
| 72726c1cf9 | |||
| 6203d9a150 | |||
| 861f11af66 | |||
| 39fdfae541 | |||
| 9f57fb1421 | |||
| 059f57db2d | |||
| 13c8be8d06 | |||
| e0dbfb0488 | |||
| 722ed128a1 | |||
| cc628821e6 | |||
| 857af61af1 | |||
| f6fc484cb3 | |||
| ba07946f1e | |||
| 600f668c63 | |||
| 8a8990b12b | |||
| fe85c1bbb9 | |||
| a89ff72308 | |||
| a99e58184b | |||
| d981bc8fd0 | |||
| ff22480d89 | |||
| 8ec85e2829 | |||
| 706aacd8e6 | |||
| 803bc2d9fc | |||
| 7cc67cf8f0 | |||
| 9909c3a0ed | |||
| 60a3751839 | |||
| ce87e0c40b | |||
| 71cd548c00 | |||
| 1ddd05302b | |||
| ea3301c75c | |||
| 4b1c4f7ccc | |||
| 21f2622a4b | |||
| 79f6a43019 | |||
| 3d75093b2c | |||
| 023dc89c3e | |||
| b5641be30d | |||
| ff4515bbf1 | |||
| 5dc0fe05af | |||
| 25bba912f6 | |||
| cb6e323596 | |||
| cf4b04e047 | |||
| 243b5be31c | |||
| c3d753cf38 | |||
| 70c46d098b | |||
| 816e4a096e | |||
| 1d1e819817 | |||
| 342daf1be8 | |||
| f6bb9b582a | |||
| aad62bc976 | |||
| 38a49a7f37 | |||
| f603bf6be7 | |||
| 6cc05e9e27 | |||
| c00e67542f | |||
| 22660362a3 | |||
| b1ab9975b7 | |||
| 7ca89d8b54 | |||
| 998b07ab8c | |||
| 4936896ff7 | |||
| 18b7484c5b | |||
| 6754c8e85e | |||
| 57243ff010 | |||
| 7c4dfe96ee | |||
| bf19120c27 | |||
| 3d37087916 | |||
| 6949f1328c | |||
| b63090c20f | |||
| b15ebda948 | |||
| 252eef2e5e | |||
| 3bea8f9706 | |||
| dfe808bee7 | |||
| 58f3dd5336 | |||
| b00f16648f | |||
| fc66c31e89 | |||
| a26c849532 | |||
| d2991e60b6 | |||
| 10ba1430f9 | |||
| 6d71f24f75 | |||
| 8f4d20e411 | |||
| cf758d773e | |||
| c012f0c4c5 | |||
| 9e85900057 | |||
| 68e3b787f9 | |||
| 6706704ad6 | |||
| 1067fa015e | |||
| 411335ebcb | |||
| f19c15491e | |||
| 0da81a4d10 | |||
| 049839a5c7 | |||
| a901927844 | |||
| 07ab0bc5a1 | |||
| a6025e6fab | |||
| e841a61bf0 | |||
| 2f1c2110c6 | |||
| 8e573eea4c | |||
| 149765764e | |||
| 45d6444c7e | |||
| aa65f49190 | |||
| 1bda41ff67 | |||
| 5a799139cd | |||
| eb36d0742a | |||
| d8a9123852 | |||
| 19031c2197 | |||
| 7a2ffdf39c | |||
| ce8fa79206 | |||
| b05c63549d | |||
| 9f98660423 | |||
| 7b0fa862d2 | |||
| bfe2d4d573 | |||
| 26950c5673 | |||
| 86788362a5 | |||
| deedc5fb2e | |||
| f4fbe67db9 | |||
| 599ce0eade | |||
| 11f3ab8dc7 | |||
| 75b4a6dd46 | |||
| 86bbea337d | |||
| e63a30064b | |||
| 222b1ddbd9 | |||
| a88dd88a07 | |||
| a47c01fd41 | |||
| 35e385f7a7 | |||
| 191e3b7d2c | |||
| 13fbbc190a | |||
| 61c6a6e7f3 | |||
| 911f218f0b | |||
| af41350eb9 | |||
| 8f63e348b3 | |||
| 8901cf8e81 | |||
| e11736e1d2 | |||
| c062fba69c | |||
| 4628095fe7 | |||
| 787ed9bc0f | |||
| daaff8974e | |||
| 4fcbd511c0 | |||
| 64d6bff6d9 | |||
| 998cbace92 | |||
| 2204038951 | |||
| 998f67ac5d | |||
| be981a2c9b | |||
| da3cf0d4a3 | |||
| 7124b9a36c | |||
| 712a6b6c39 | |||
| ed0a1c57d6 | |||
| 7858228505 | |||
| 2525a22d78 | |||
| 7b4ef8fc31 | |||
| ce14cd02e4 | |||
| 05f501af52 | |||
| e94fa4844f | |||
| e0965aae5e | |||
| 414c1de963 | |||
| b589102be8 | |||
| df367e0d47 | |||
| 680219ebcb | |||
| ef87487f60 | |||
| 8bafe72434 | |||
| 76dcc69e44 | |||
| 682ef22194 | |||
| ee8dd41605 | |||
| d0b8d666e4 | |||
| 4a81826d19 | |||
| 848a10c3a8 | |||
| a4baa29678 | |||
| a9b5280691 | |||
| 264706210b | |||
| 7677ae254f | |||
| 7ccb4c5f06 | |||
| 01205af018 | |||
| 5b5150e6d4 | |||
| 9d8e3f5049 | |||
| 99ede83bdc | |||
| 61488e840d | |||
| a80d01209c | |||
| d2a8d655c8 | |||
| 80e2b34abc | |||
| fb3d43d2d5 | |||
| ebdec4fa44 | |||
| ead8dbbaa5 | |||
| 95615a5501 | |||
| 75487b1f58 | |||
| c7daaa00cd | |||
| 606f24e981 | |||
| c83d9287a7 | |||
| fa8092b2cd | |||
| b2b07a2be6 | |||
| a21628e350 | |||
| 9fcd686fda | |||
| 1759fd4cf9 | |||
| 12e7837602 | |||
| ff349f2edf | |||
| 088d8ca207 | |||
| c37902d3a4 | |||
| da38684cdd | |||
| cb2cb72333 | |||
| d53c8f9830 | |||
| c8136d4231 | |||
| e4b8380530 | |||
| fbd599194c | |||
| 0270ec26fb | |||
| 79e342be57 | |||
| c89f23b018 | |||
| f971ec5d34 | |||
| c5776ce41f | |||
| 75c5ebbffa | |||
| d51a724ade | |||
| c1143d7a6d | |||
| f3697431a4 | |||
| e3d6e5f420 | |||
| af6f7a7146 | |||
| c18d95e89a | |||
| fa42d03a65 | |||
| 22f7de2c09 | |||
| b4b9df81cb | |||
| 2a71c2b0e7 | |||
| c37b3d3df5 | |||
| ae135b89d6 | |||
| 4112426848 | |||
| 638b399b31 | |||
| 8a5713ef7c | |||
| 382d7bfaa1 | |||
| 3daf2f7738 | |||
| 944d35971d | |||
| 9ce8af0b82 | |||
| d825b9ec26 | |||
| 9afe4aea4b | |||
| 3cc8c3284a | |||
| e2c18c4e1e | |||
| 81fe768f6a | |||
| 8923779ab0 | |||
| 583218a045 | |||
| 153b2bfa53 | |||
| 0a9b325360 | |||
| af9e6f2c46 | |||
| 0e31726fd3 | |||
| 590d9c2e61 | |||
| c12fe9f25e | |||
| 9bd83f3acf | |||
| aada15fc41 | |||
| 6a2d383be6 | |||
| 545c8fa482 | |||
| e3d18643b8 | |||
| 2f318bbe9f | |||
| fac7eb61b3 | |||
| 4450d03a54 | |||
| e4e7868b30 | |||
| 3d7fa680cb | |||
| c38035e25e | |||
| 8d4fdaf902 | |||
| 2820adad53 | |||
| f7a427d2c0 | |||
| 59ed8c9660 | |||
| 71562ab0e5 | |||
| 278ddf037f | |||
| ac3151af92 | |||
| 1189ad7862 | |||
| 88c13e7b9a | |||
| 596baac62e | |||
| 5a8503ec14 | |||
| e44f95724d | |||
| dec0d0dea7 | |||
| 46f96e94ec | |||
| 89b30bcf58 | |||
| 473d8f5f85 | |||
| 08d8d65599 | |||
| 2964c1d82e | |||
| 75ab8552d9 | |||
| b78d5b3d03 | |||
| 064081c771 | |||
| 3525d35d15 | |||
| 9d9ee7c585 | |||
| e65c32c4c7 | |||
| 113769791f | |||
| 5d2a1d21d5 | |||
| 6e40f92aaf | |||
| 302cb8a5be | |||
| 0b27890484 | |||
| 5db247e632 | |||
| c1155a4338 | |||
| da806b9492 | |||
| 3d6d46b30d | |||
| 57c69738ba | |||
| 0c8157dbc0 | |||
| d7b278f2f7 | |||
| 18d74f1057 | |||
| e04856f794 | |||
| d7bbfb0fc3 | |||
| a0857817de | |||
| 8d39234fd0 | |||
| 0fce1c0fd8 | |||
| c23c285f82 | |||
| 81b560d791 | |||
| 55459a4f42 | |||
| 9eafd3e6ca | |||
| e7ac26ff5a | |||
| fad78641d6 | |||
| a32cb0142b | |||
| 4a3c133152 | |||
| ae79c34508 | |||
| 7eb69c1ad7 | |||
| 8aaff0af89 | |||
| 4b77280c65 | |||
| a0df84e4f2 | |||
| 8876e6a098 | |||
| 9bee2a90f9 | |||
| cd6a811bbd | |||
| 84f9a83f55 | |||
| 2efc669ab2 | |||
| 5444ed77ad | |||
| dd71fe80a5 | |||
| b73092eb64 | |||
| aa28f87b0c | |||
| 1e1bcd4a30 | |||
| d24b3c46bf | |||
| 37222f07d9 | |||
| 49646a79e5 | |||
| 9940e1210e | |||
| 10892812c3 | |||
| d8ff5987dd | |||
| d014d418e9 | |||
| cc1b56501d | |||
| a5ad9648bf | |||
| 3630099234 | |||
| 941a24c75b | |||
| 50bdc01484 | |||
| d9e340ddc4 | |||
| beee1e91d6 | |||
| 35935d2bac | |||
| 5d0b0ad33e | |||
| d1a6ac531f | |||
| ae255a3bd9 | |||
| fa733afa51 | |||
| b60c723457 | |||
| 2bb7ff3c13 | |||
| 68b4cf00e2 | |||
| 440169ff60 | |||
| 1fc361242e | |||
| 72cc6f3d75 | |||
| a2b1924e00 | |||
| 7e926167f0 | |||
| 32484bd32e | |||
| 04ade1008b | |||
| 4603de0e70 | |||
| 12c1ff65e9 | |||
| 7b8cbf7f86 | |||
| c198e83f70 | |||
| e60863a290 | |||
| 946563d3b2 | |||
| 3f2ef1d54e | |||
| 0d2b60b905 | |||
| 161b11e428 | |||
| f7748d51df | |||
| 5d13b4b705 | |||
| 92e52a2284 | |||
| 3efd5fb77a | |||
| c3d62bb8d8 | |||
| c099e843d5 | |||
| 5f3a5871f1 | |||
| f01fdd0070 | |||
| d20cc367b8 | |||
| eadb9a733f | |||
| 32ac454b5b | |||
| 2cadd6af44 | |||
| 3c8b5cb313 | |||
| fcf2387794 | |||
| f9783b4806 | |||
| cbdc532f83 | |||
| 124554bae5 | |||
| 2725536d7d | |||
| beeef7a4f7 | |||
| 7f2ebb6aeb | |||
| c8e30383ba | |||
| a7d5a6ccb9 | |||
| be3380aaf3 | |||
| 40a4ab5410 | |||
| 54a2181960 | |||
| e490e15bc4 | |||
| 469844d97b | |||
| 892ea29ba8 | |||
| acde1c6742 | |||
| 07da11d852 | |||
| 502b8c2270 | |||
| af3f7ac810 | |||
| 935c6caf96 | |||
| 90bce1d437 | |||
| 831dd3e2e0 | |||
| b8e208eb32 | |||
| 542c908e30 | |||
| 08529242bf | |||
| 1ae9449d92 | |||
| 7fd0b1fa08 | |||
| 111572e3f2 | |||
| c9875d24b4 | |||
| 94a75603c3 | |||
| d55925bccd | |||
| 0b5adcbfab | |||
| 8d84562f32 | |||
| 136d159833 | |||
| e3026062d8 | |||
| 8a69c69b7f | |||
| 197714a57a | |||
| 567cdc6f8d | |||
| be6e8c7713 | |||
| f667b35403 | |||
| 942e482ca4 | |||
| 10b381965e | |||
| 79c3a99a38 | |||
| 463d3c8e97 | |||
| da007729c6 | |||
| b7b42b5fd4 | |||
| 735f9bd053 | |||
| 6096b65374 | |||
| 727d342eb3 | |||
| 58815ba8bc | |||
| 49d0502775 | |||
| 0b6c6e6d2c | |||
| 45ede6047a | |||
| 8f469d4ebb | |||
| f3264d056d |
@@ -14,3 +14,4 @@ logs
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
miniprogram
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# 更新日志
|
||||
|
||||
## v4.2.8
|
||||
|
||||
- Bug 修复:修复管理后台邮件配置报“参数错误”问题
|
||||
- 功能优化:手机端也不显示后端禁用的菜单功能页面
|
||||
- 功能优化:**聊天页面支持 Sora2 文生视频/图生视频功能** 🔥🔥🔥
|
||||
- 功能优化:**重构路由代码,优化路由加载逻辑,支持关闭手机站功能** 🔥🔥🔥
|
||||
- Bug 修复:修复用户算力扣减日志记录逻辑错误问题
|
||||
- 功能优化:优化 404 页面样式,兼容 PC 端和移动端
|
||||
- Bug 修复:ipv6 地址登录报错:Data too long for column 'last_login_ip'
|
||||
- Bug 修复:移动端登录页面输入密码的时候会覆盖在确认按钮上层
|
||||
- 功能优化:**移动端的 DALL-E 绘图页面支持上传参考图** 🔥🔥🔥
|
||||
- 功能优化:优化 WebFooter 组件,如果配置了备案号,则显示备案号,否则不显示
|
||||
- 功能优化:启动时自动同步 model 与数据表字段,缺列自动新建、多余列自动删除
|
||||
|
||||
## v4.2.7
|
||||
|
||||
- Bug 修复:修复超级管理员无法修改密码的 Bug
|
||||
|
||||
@@ -16,6 +16,7 @@ tmp
|
||||
bin
|
||||
data
|
||||
config.toml
|
||||
config.toml.*.bak
|
||||
static/upload
|
||||
static/audio
|
||||
storage.json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Listen = "0.0.0.0:5678"
|
||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||
MysqlDns = "root:12345678@tcp(localhost:3306)/geekai?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||
StaticDir = "./static" # 静态资源的目录
|
||||
StaticUrl = "/static" # 静态资源访问 URL
|
||||
TikaHost = "http://tika:9998"
|
||||
|
||||
@@ -44,11 +44,11 @@ func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types
|
||||
}
|
||||
|
||||
func (s *AppServer) Init(client *redis.Client) {
|
||||
s.Engine.Use(middleware.ThumbMiddleware())
|
||||
s.Engine.Use(middleware.ParameterHandlerMiddleware())
|
||||
s.Engine.Use(errorHandler)
|
||||
// 添加静态资源访问
|
||||
s.Engine.Static("/static", s.Config.StaticDir)
|
||||
s.Engine.Use(middleware.StaticMiddleware())
|
||||
}
|
||||
|
||||
func (s *AppServer) Run(db *gorm.DB) error {
|
||||
|
||||
@@ -85,15 +85,6 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
|
||||
logger.Error("load system config error: ", err)
|
||||
}
|
||||
|
||||
// 加载许可证配置
|
||||
var license types.License
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyLicense).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &license)
|
||||
if err != nil {
|
||||
logger.Error("load license config error: ", err)
|
||||
}
|
||||
|
||||
// 加载验证码配置
|
||||
var captchaConfig types.CaptchaConfig
|
||||
sysConfig.Id = 0
|
||||
@@ -168,7 +159,6 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
|
||||
|
||||
return &types.SystemConfig{
|
||||
Base: baseConfig,
|
||||
License: license,
|
||||
SMS: smsConfig,
|
||||
OSS: ossConfig,
|
||||
SMTP: smtpConfig,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
@@ -13,6 +14,8 @@ import (
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
// 前端用户授权验证
|
||||
func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// 静态资源中间件
|
||||
func StaticMiddleware() gin.HandlerFunc {
|
||||
// 缩略图中间件
|
||||
func ThumbMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
url := c.Request.URL.String()
|
||||
@@ -37,22 +37,6 @@ type RedisConfig struct {
|
||||
DB int
|
||||
}
|
||||
|
||||
// LicenseKey 存储许可证书的 KEY
|
||||
const LicenseKey = "Geek-AI-License"
|
||||
|
||||
type License struct {
|
||||
Key string `json:"key"` // 许可证书密钥
|
||||
MachineId string `json:"machine_id"` // 机器码
|
||||
ExpiredAt int64 `json:"expired_at"` // 过期时间
|
||||
IsActive bool `json:"is_active"` // 是否激活
|
||||
Configs LicenseConfig `json:"configs"`
|
||||
}
|
||||
|
||||
type LicenseConfig struct {
|
||||
UserNum int `json:"user_num"` // 用户数量
|
||||
DeCopy bool `json:"de_copy"` // 去版权
|
||||
}
|
||||
|
||||
func (c RedisConfig) Url() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
@@ -97,6 +81,8 @@ type BaseConfig struct {
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
|
||||
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
||||
|
||||
EnableMobileSite bool `json:"enable_mobile_site"` // 是否开启手机站点
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
@@ -108,7 +94,6 @@ type SystemConfig struct {
|
||||
Captcha CaptchaConfig
|
||||
WxLogin WxLoginConfig
|
||||
Jimeng JimengConfig
|
||||
License License
|
||||
Moderation ModerationConfig
|
||||
}
|
||||
|
||||
@@ -121,7 +106,6 @@ const (
|
||||
ConfigKeyMarkMap = "mark_map"
|
||||
ConfigKeyCaptcha = "captcha"
|
||||
ConfigKeyWxLogin = "wx_login"
|
||||
ConfigKeyLicense = "license"
|
||||
ConfigKeySms = "sms"
|
||||
ConfigKeySmtp = "smtp"
|
||||
ConfigKeyOss = "oss"
|
||||
|
||||
@@ -15,7 +15,8 @@ type JimengConfig struct {
|
||||
type JMTaskStatus string
|
||||
|
||||
const (
|
||||
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务已提交
|
||||
JMTaskStatusSubmited = JMTaskStatus("submited") // 任务已提交
|
||||
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务排队中
|
||||
JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
|
||||
JMTaskStatusDone = JMTaskStatus("done") // 处理完成
|
||||
JMTaskStatusNotFound = JMTaskStatus("not_found") // 任务未找到
|
||||
|
||||
@@ -72,14 +72,13 @@ type SdTaskParams struct {
|
||||
type DallTask struct {
|
||||
ModelId uint `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
ModelValue string `json:"model_value"`
|
||||
Image []string `json:"image,omitempty"`
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
AspectRatio string `json:"aspect_ratio"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
Power int `json:"power"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
|
||||
type ConfigHandler struct {
|
||||
handler.BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
sysConfig *types.SystemConfig
|
||||
alipayService *payment.AlipayService
|
||||
wxpayService *payment.WxPayService
|
||||
@@ -41,7 +40,6 @@ type ConfigHandler struct {
|
||||
func NewConfigHandler(
|
||||
app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
licenseService *service.LicenseService,
|
||||
sysConfig *types.SystemConfig,
|
||||
alipayService *payment.AlipayService,
|
||||
wxpayService *payment.WxPayService,
|
||||
@@ -54,7 +52,6 @@ func NewConfigHandler(
|
||||
) *ConfigHandler {
|
||||
return &ConfigHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
licenseService: licenseService,
|
||||
sysConfig: sysConfig,
|
||||
alipayService: alipayService,
|
||||
wxpayService: wxpayService,
|
||||
@@ -87,8 +84,6 @@ func (h *ConfigHandler) RegisterRoutes() {
|
||||
rg.POST("update/oss", h.UpdateOss)
|
||||
rg.POST("update/smtp", h.UpdateStmp)
|
||||
rg.GET("get", h.Get)
|
||||
rg.POST("license/active", h.Active)
|
||||
rg.GET("license/get", h.GetLicense)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,19 +96,6 @@ func (h *ConfigHandler) UpdateBase(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 未授权的话不允许修改版权
|
||||
license := h.licenseService.GetLicense()
|
||||
if !license.IsActive && data.Copyright != h.sysConfig.Base.Copyright {
|
||||
resp.ERROR(c, "未授权系统不允许修改版权信息")
|
||||
return
|
||||
}
|
||||
|
||||
// 未授权的话不允许修改 Logo
|
||||
if !license.IsActive && data.Logo != h.sysConfig.Base.Logo {
|
||||
resp.ERROR(c, "未授权系统不允许修改 Logo")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeySystem, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
@@ -407,36 +389,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// Active 激活系统
|
||||
func (h *ConfigHandler) Active(c *gin.Context) {
|
||||
var data struct {
|
||||
License string `json:"license"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.licenseService.ActiveLicense(data.License)
|
||||
license := h.licenseService.GetLicense()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.Update(types.ConfigKeyLicense, license); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 更新系统配置
|
||||
h.sysConfig.License = *license
|
||||
|
||||
resp.SUCCESS(c, license.MachineId)
|
||||
|
||||
}
|
||||
|
||||
// GetLicense 获取 License 信息
|
||||
func (h *ConfigHandler) GetLicense(c *gin.Context) {
|
||||
license := h.licenseService.GetLicense()
|
||||
resp.SUCCESS(c, license)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
@@ -28,12 +27,11 @@ import (
|
||||
|
||||
type UserHandler struct {
|
||||
handler.BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
redis *redis.Client
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService, redisCli *redis.Client) *UserHandler {
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService, redis: redisCli}
|
||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, redisCli *redis.Client) *UserHandler {
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, redis: redisCli}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
@@ -114,13 +112,6 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 检测最大注册人数
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
var user = model.User{}
|
||||
var res *gorm.DB
|
||||
var userVo vo.User
|
||||
@@ -198,11 +189,7 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||
}
|
||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
} else {
|
||||
u.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
||||
}
|
||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
res = h.DB.Create(&u)
|
||||
_ = utils.CopyObject(u, &userVo)
|
||||
userVo.Id = u.Id
|
||||
|
||||
+54
-24
@@ -19,6 +19,7 @@ import (
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sora"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
@@ -65,36 +66,39 @@ type ChatHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
userLocks *types.UserLockManager
|
||||
soraService *sora.SoraService
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService, moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||
func NewChatHandler(app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
redis *redis.Client,
|
||||
manager *oss.UploaderManager,
|
||||
userService *service.UserService,
|
||||
soraService *sora.SoraService,
|
||||
moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
userLocks: types.NewUserLockManager(),
|
||||
soraService: soraService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/chat/")
|
||||
|
||||
// 聊天接口不需要授权(已在authConfig中配置)
|
||||
group.Any("message", h.Chat)
|
||||
|
||||
// 其他接口需要用户授权
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.Any("message", h.Chat)
|
||||
group.GET("list", h.List)
|
||||
group.GET("detail", h.Detail)
|
||||
group.POST("update", h.Update)
|
||||
@@ -340,14 +344,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
||||
for _, file := range input.Files {
|
||||
logger.Debugf("detected file: %+v", file.URL)
|
||||
// 处理图片
|
||||
if isImageURL(file.URL) {
|
||||
if isImageURL(file.URL) || isVideoURL(file.URL) {
|
||||
imgList = append(imgList, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": file.URL,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
} else if isTextURL(file.URL) {
|
||||
// 处理文件,提取文件内容
|
||||
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
@@ -408,23 +412,43 @@ func isImageURL(url string) bool {
|
||||
".svg": true,
|
||||
".ico": true,
|
||||
}
|
||||
return validImageExts[ext]
|
||||
}
|
||||
|
||||
if !validImageExts[ext] {
|
||||
// 判断是个链接是否是文本链接
|
||||
func isTextURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 发送HEAD请求检查Content-Type
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validTextExts := map[string]bool{
|
||||
".txt": true,
|
||||
".doc": true,
|
||||
".docx": true,
|
||||
".pdf": true,
|
||||
}
|
||||
resp, err := client.Head(url)
|
||||
if err != nil {
|
||||
return validTextExts[ext]
|
||||
}
|
||||
|
||||
// 判断是个链接是否是视频
|
||||
func isVideoURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
return strings.HasPrefix(contentType, "image/")
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validVideoExts := map[string]bool{
|
||||
".mp4": true,
|
||||
".avi": true,
|
||||
".mov": true,
|
||||
".wmv": true,
|
||||
}
|
||||
return validVideoExts[ext]
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
@@ -505,11 +529,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
|
||||
// ONLY allow apiURL in blank list
|
||||
err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Debugf("对话请求消息体:%+v", req)
|
||||
var apiURL string
|
||||
p, _ := url.Parse(apiKey.ApiURL)
|
||||
@@ -644,6 +663,16 @@ func (h *ChatHandler) saveChatHistory(
|
||||
replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
||||
totalTokens = replyTokens + getTotalTokens(req)
|
||||
}
|
||||
|
||||
// 如果是 Sora 相关模型,则下载视频
|
||||
files := make([]vo.File, 0)
|
||||
if strings.HasPrefix(req.Model, "sora") {
|
||||
video, err := h.soraService.DownloadVideoURL(message.Content)
|
||||
if err == nil {
|
||||
files = append(files, *video)
|
||||
}
|
||||
}
|
||||
|
||||
historyReplyMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
@@ -652,7 +681,7 @@ func (h *ChatHandler) saveChatHistory(
|
||||
Icon: input.ChatRole.Icon,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: message.Content,
|
||||
Files: input.Files,
|
||||
Files: files,
|
||||
}),
|
||||
Tokens: replyTokens,
|
||||
TotalTokens: totalTokens,
|
||||
@@ -676,6 +705,7 @@ func (h *ChatHandler) saveChatHistory(
|
||||
if err != nil {
|
||||
content.Text = historyReplyMsg.Content
|
||||
}
|
||||
content.Files = files
|
||||
messageVo.Content = content
|
||||
messageVo.CreatedAt = historyReplyMsg.CreatedAt.Unix()
|
||||
messageVo.UpdatedAt = historyReplyMsg.UpdatedAt.Unix()
|
||||
|
||||
@@ -212,6 +212,14 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
} else {
|
||||
errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
contents = append(contents, errMsg)
|
||||
// 扣减算力
|
||||
if function.Power > 0 {
|
||||
h.userService.DecreasePower(userVo.Id, function.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: function.Name,
|
||||
Remark: fmt.Sprintf("函数调用,%s(%s)", function.Label, function.Name),
|
||||
})
|
||||
}
|
||||
}
|
||||
pushMessage(c, "text", errMsg)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
@@ -20,11 +19,10 @@ import (
|
||||
|
||||
type ConfigHandler struct {
|
||||
BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *ConfigHandler {
|
||||
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService}
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
|
||||
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
@@ -33,7 +31,6 @@ func (h *ConfigHandler) RegisterRoutes() {
|
||||
|
||||
// 无需授权的接口
|
||||
group.GET("get", h.Get)
|
||||
group.GET("license", h.License)
|
||||
}
|
||||
|
||||
// Get 获取指定的系统配置
|
||||
@@ -55,9 +52,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// License 获取 License 配置
|
||||
func (h *ConfigHandler) License(c *gin.Context) {
|
||||
license := h.licenseService.GetLicense()
|
||||
resp.SUCCESS(c, license.Configs)
|
||||
}
|
||||
|
||||
@@ -117,12 +117,12 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
task := types.DallTask{
|
||||
UserId: uint(userId),
|
||||
ModelId: chatModel.Id,
|
||||
ModelName: chatModel.Value,
|
||||
ModelName: chatModel.Name,
|
||||
ModelValue: chatModel.Value,
|
||||
Image: data.Image,
|
||||
Prompt: data.Prompt,
|
||||
Quality: data.Quality,
|
||||
AspectRatio: data.AspectRatio,
|
||||
Size: data.Size,
|
||||
Style: data.Style,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Value,
|
||||
Model: chatModel.Name,
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -215,12 +215,9 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
ModelId: chatModel.Id,
|
||||
ModelName: chatModel.Value,
|
||||
ModelValue: chatModel.Value,
|
||||
ModelName: chatModel.Name,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
N: 1,
|
||||
Quality: "standard",
|
||||
Size: "1024x1024",
|
||||
Style: "vivid",
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
job := model.DallJob{
|
||||
|
||||
@@ -247,30 +247,12 @@ func (h *JimengHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
||||
if err := h.DB.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
||||
logger.Errorf("delete jimeng job failed: %v", err)
|
||||
resp.ERROR(c, "删除任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 失败任务删除后退回算力
|
||||
if job.Status == types.JMTaskStatusFailed {
|
||||
logger.Infof("delete jimeng job failed, refund power: %d", job.Power)
|
||||
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: job.ReqKey,
|
||||
Remark: fmt.Sprintf("删除任务,退回%d算力", job.Power),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "退回算力失败")
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
resp.SUCCESS(c, gin.H{})
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||
func (h *MenuHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/menu/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("list/all", h.ListAll)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
@@ -54,3 +55,20 @@ func (h *MenuHandler) List(c *gin.Context) {
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
// ListAll 获取所有的菜单列表
|
||||
func (h *MenuHandler) ListAll(c *gin.Context) {
|
||||
var items []model.Menu
|
||||
var list = make([]vo.Menu, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Menu
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
list = append(list, product)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ func (h *PaymentHandler) SyncOrders() error {
|
||||
}
|
||||
case payment.PayChannelAL:
|
||||
res, err = h.alipayService.Query(order.OrderNo)
|
||||
logger.Debugf("支付宝支付订单状态:%+v", res)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query order info: %v", err)
|
||||
continue
|
||||
|
||||
@@ -35,7 +35,6 @@ type UserHandler struct {
|
||||
searcher *xdb.Searcher
|
||||
redis *redis.Client
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
captchaService *service.CaptchaService
|
||||
userService *service.UserService
|
||||
wxLoginService *service.WxLoginService
|
||||
@@ -51,15 +50,13 @@ func NewUserHandler(
|
||||
captcha *service.CaptchaService,
|
||||
userService *service.UserService,
|
||||
wxLoginService *service.WxLoginService,
|
||||
ipSearcher *xdb.Searcher,
|
||||
licenseService *service.LicenseService) *UserHandler {
|
||||
ipSearcher *xdb.Searcher) *UserHandler {
|
||||
return &UserHandler{
|
||||
BaseHandler: BaseHandler{DB: db, App: app},
|
||||
searcher: searcher,
|
||||
redis: client,
|
||||
levelDB: levelDB,
|
||||
captchaService: captcha,
|
||||
licenseService: licenseService,
|
||||
userService: userService,
|
||||
wxLoginService: wxLoginService,
|
||||
ipSearcher: ipSearcher,
|
||||
@@ -132,14 +129,6 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检测最大注册人数
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
var key string
|
||||
if data.RegWay == "email" {
|
||||
@@ -327,7 +316,7 @@ func (h *UserHandler) GetWxLoginState(c *gin.Context) {
|
||||
}
|
||||
|
||||
// createNewUser 创建新用户
|
||||
func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.User, error) {
|
||||
func (h *UserHandler) createNewUser(user model.User, code string) (model.User, error) {
|
||||
if user.OpenId != "" {
|
||||
user.Platform = "wechat"
|
||||
user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6))
|
||||
@@ -353,19 +342,22 @@ func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.U
|
||||
// 创建用户
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return user, err
|
||||
}
|
||||
|
||||
// 记录邀请关系
|
||||
if inviteCode != "" {
|
||||
inviteCode := model.InviteCode{}
|
||||
err := h.DB.Where("code = ?", inviteCode).First(&inviteCode).Error
|
||||
if code != "" {
|
||||
var inviteCode model.InviteCode
|
||||
err := h.DB.Where("code = ?", code).First(&inviteCode).Error
|
||||
if err != nil {
|
||||
logger.Error("无效的邀请码:", err.Error())
|
||||
tx.Rollback()
|
||||
return user, fmt.Errorf("无效的邀请码")
|
||||
}
|
||||
|
||||
// 增加邀请数量
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", inviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.Base.InvitePower > 0 {
|
||||
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
|
||||
Type: types.PowerInvite,
|
||||
|
||||
+6
-12
@@ -24,6 +24,7 @@ import (
|
||||
"geekai/service/payment"
|
||||
"geekai/service/sd"
|
||||
"geekai/service/sms"
|
||||
"geekai/service/sora"
|
||||
"geekai/service/suno"
|
||||
"geekai/service/video"
|
||||
"geekai/store"
|
||||
@@ -166,12 +167,6 @@ func main() {
|
||||
|
||||
// 邮件服务
|
||||
fx.Provide(service.NewSmtpService),
|
||||
// License 服务
|
||||
fx.Provide(service.NewLicenseService),
|
||||
fx.Invoke(func(licenseService *service.LicenseService) {
|
||||
licenseService.SyncLicense()
|
||||
}),
|
||||
|
||||
// Dalle 服务
|
||||
fx.Provide(dalle.NewService),
|
||||
fx.Invoke(func(s *dalle.Service) {
|
||||
@@ -189,6 +184,9 @@ func main() {
|
||||
s.DownloadImages()
|
||||
}),
|
||||
|
||||
// Sora service
|
||||
fx.Provide(sora.NewSoraService),
|
||||
|
||||
// Stable Diffusion 机器人
|
||||
fx.Provide(sd.NewService),
|
||||
fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
|
||||
@@ -222,12 +220,8 @@ func main() {
|
||||
fx.Provide(sms.NewAliYunSmsService),
|
||||
fx.Provide(sms.NewBaoSmsService),
|
||||
fx.Provide(sms.NewSmsManager),
|
||||
fx.Provide(func(config *types.SystemConfig) *service.CaptchaService {
|
||||
return service.NewCaptchaService(config.Captcha)
|
||||
}),
|
||||
fx.Provide(func(config *types.SystemConfig, client *redis.Client) *service.WxLoginService {
|
||||
return service.NewWxLoginService(config.WxLogin, client)
|
||||
}),
|
||||
fx.Provide(service.NewCaptchaService),
|
||||
fx.Provide(service.NewWxLoginService),
|
||||
|
||||
// 支付服务
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
|
||||
@@ -20,9 +20,9 @@ type CaptchaService struct {
|
||||
client *req.Client
|
||||
}
|
||||
|
||||
func NewCaptchaService(captchaConfig types.CaptchaConfig) *CaptchaService {
|
||||
func NewCaptchaService(sysConfig *types.SystemConfig) *CaptchaService {
|
||||
return &CaptchaService{
|
||||
config: captchaConfig,
|
||||
config: sysConfig.Captcha,
|
||||
client: req.C().SetTimeout(10 * time.Second),
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func (s *CaptchaService) GetConfig() types.CaptchaConfig {
|
||||
return s.config
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Get() (interface{}, error) {
|
||||
func (s *CaptchaService) Get() (any, error) {
|
||||
url := fmt.Sprintf("%s/api/captcha/get", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
@@ -98,10 +97,8 @@ type imgReq struct {
|
||||
Model string `json:"model"`
|
||||
Image []string `json:"image,omitempty"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n,omitempty"`
|
||||
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
}
|
||||
|
||||
@@ -116,10 +113,10 @@ type imgRes struct {
|
||||
|
||||
type ErrRes struct {
|
||||
Error struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param interface{} `json:"param"`
|
||||
Type string `json:"type"`
|
||||
Code any `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param any `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
@@ -130,7 +127,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
if task.ModelId > 0 {
|
||||
s.db.Where("id", task.ModelId).First(&chatModel)
|
||||
} else {
|
||||
s.db.Where("value", task.ModelName).First(&chatModel)
|
||||
s.db.Where("value", task.ModelValue).First(&chatModel)
|
||||
}
|
||||
|
||||
// get image generation API KEY
|
||||
@@ -153,16 +150,16 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
|
||||
reqBody := imgReq{
|
||||
Model: chatModel.Value,
|
||||
Prompt: task.Prompt,
|
||||
N: 1,
|
||||
Size: task.Size,
|
||||
Style: task.Style,
|
||||
Quality: task.Quality,
|
||||
Model: chatModel.Value,
|
||||
Prompt: task.Prompt,
|
||||
AspectRatio: task.AspectRatio,
|
||||
Size: task.Size,
|
||||
ResponseFormat: "url",
|
||||
}
|
||||
|
||||
// 图片编辑
|
||||
if len(task.Image) > 0 {
|
||||
reqBody.Prompt = fmt.Sprintf("%s, %s", strings.Join(task.Image, " "), task.Prompt)
|
||||
reqBody.Image = task.Image
|
||||
}
|
||||
|
||||
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
|
||||
@@ -185,7 +182,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
// update the api key last use time
|
||||
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
var imgURL string
|
||||
var data = map[string]interface{}{
|
||||
var data = map[string]any{
|
||||
"progress": 100,
|
||||
"prompt": task.Prompt,
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
@@ -23,29 +24,31 @@ var logger = logger2.GetLogger()
|
||||
|
||||
// Service 即梦服务(合并了消费者功能)
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
taskQueue *store.RedisQueue
|
||||
client *Client
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
running bool
|
||||
uploader *oss.UploaderManager
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
taskQueue *store.RedisQueue
|
||||
client *Client
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
running bool
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
// NewService 创建即梦服务
|
||||
func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client) *Service {
|
||||
func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client, userService *service.UserService) *Service {
|
||||
taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Service{
|
||||
db: db,
|
||||
redis: redisCli,
|
||||
taskQueue: taskQueue,
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
running: false,
|
||||
uploader: uploader,
|
||||
db: db,
|
||||
redis: redisCli,
|
||||
taskQueue: taskQueue,
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
running: false,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +118,7 @@ func (s *Service) CreateTask(userId uint, req *types.JimengTaskRequest) (*model.
|
||||
ReqKey: req.ReqKey,
|
||||
Prompt: req.Prompt,
|
||||
Params: utils.JsonEncode(req),
|
||||
Status: types.JMTaskStatusInQueue,
|
||||
Status: types.JMTaskStatusSubmited,
|
||||
Power: req.Power,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
@@ -216,6 +219,7 @@ func (s *Service) ProcessTask(jobId uint) error {
|
||||
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||
"task_id": resp.Data.TaskId,
|
||||
"raw_data": string(rawData),
|
||||
"status": types.JMTaskStatusInQueue, // 把任务状态改成排队中,以便开启轮询
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
logger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
@@ -277,12 +281,10 @@ func (s *Service) pollTaskStatus() {
|
||||
|
||||
for {
|
||||
var jobs []model.JimengJob
|
||||
s.db.Where("status IN (?)", []types.JMTaskStatus{types.JMTaskStatusGenerating, types.JMTaskStatusInQueue}).Find(&jobs)
|
||||
if len(jobs) == 0 {
|
||||
logger.Debugf("no jimeng task to poll, sleep 10s")
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
// 找出排队中和处理中的任务进行轮询
|
||||
s.db.Where("status IN (?)", []types.JMTaskStatus{
|
||||
types.JMTaskStatusGenerating,
|
||||
types.JMTaskStatusInQueue}).Find(&jobs)
|
||||
|
||||
for _, job := range jobs {
|
||||
// 任务超时处理
|
||||
@@ -366,6 +368,21 @@ func (s *Service) pollTaskStatus() {
|
||||
|
||||
}
|
||||
|
||||
// 找出失败的任务,并恢复其扣减算力
|
||||
s.db.Where("status = ?", types.JMTaskStatusFailed).Where("power > ?", 0).Find(&jobs)
|
||||
for _, job := range jobs {
|
||||
err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: job.ReqKey,
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d", job.Id),
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 更新任务状态
|
||||
s.db.Model(&job).UpdateColumn("power", 0)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
package service
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LicenseService struct {
|
||||
license *types.License
|
||||
urlWhiteList []string
|
||||
machineId string
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLicenseService(sysConfig *types.SystemConfig, db *gorm.DB) *LicenseService {
|
||||
var machineId string
|
||||
info, err := host.Info()
|
||||
if err == nil {
|
||||
machineId = info.HostID
|
||||
}
|
||||
logger.Infof("License: %+v", sysConfig.License)
|
||||
return &LicenseService{
|
||||
license: &sysConfig.License,
|
||||
machineId: machineId,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
type License struct {
|
||||
Name string `json:"name"`
|
||||
License string `json:"license"`
|
||||
MachineId string `json:"mid"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
UserNum int `json:"user_num"`
|
||||
Configs types.LicenseConfig `json:"configs"`
|
||||
}
|
||||
|
||||
// ActiveLicense 激活 License
|
||||
func (s *LicenseService) ActiveLicense(license string) error {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data License `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/active")
|
||||
response, err := req.C().R().
|
||||
SetBody(map[string]string{"license": license, "machine_id": s.machineId}).
|
||||
SetSuccessResult(&res).Post(apiURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送激活请求失败: %v", err)
|
||||
}
|
||||
|
||||
if response.IsErrorState() {
|
||||
return fmt.Errorf("发送激活请求失败:%v", response.Status)
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
return fmt.Errorf("激活失败:%v", res.Message)
|
||||
}
|
||||
|
||||
if res.Data.ExpiredAt > 0 && res.Data.ExpiredAt < time.Now().Unix() {
|
||||
return fmt.Errorf("License 已过期")
|
||||
}
|
||||
|
||||
s.license = &types.License{
|
||||
Key: license,
|
||||
MachineId: s.machineId,
|
||||
Configs: res.Data.Configs,
|
||||
ExpiredAt: res.Data.ExpiredAt,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// 保存 License 到数据库
|
||||
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存 License 到数据库失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncLicense 定期同步 License
|
||||
func (s *LicenseService) SyncLicense() {
|
||||
go func() {
|
||||
retryCounter := 0
|
||||
for {
|
||||
license, err := s.fetchLicense()
|
||||
if err != nil {
|
||||
retryCounter++
|
||||
if retryCounter < 5 {
|
||||
logger.Debug(err)
|
||||
}
|
||||
s.license.IsActive = false
|
||||
} else {
|
||||
s.license = license
|
||||
// 保存 License 到数据库
|
||||
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
|
||||
if err != nil {
|
||||
logger.Errorf("保存 License 到数据库失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
urls, err := s.fetchUrlWhiteList()
|
||||
if err == nil {
|
||||
s.urlWhiteList = urls
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *LicenseService) fetchLicense() (*types.License, error) {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data License `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/check")
|
||||
response, err := req.C().R().
|
||||
SetBody(map[string]string{"license": s.license.Key, "machine_id": s.machineId}).
|
||||
SetSuccessResult(&res).Post(apiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("License 同步失败: %v", err)
|
||||
}
|
||||
if response.IsErrorState() {
|
||||
return nil, fmt.Errorf("License 同步失败:%v", response.Status)
|
||||
}
|
||||
if res.Code != types.Success {
|
||||
return nil, fmt.Errorf("License 同步失败:%v", res.Message)
|
||||
}
|
||||
|
||||
return &types.License{
|
||||
Key: res.Data.License,
|
||||
MachineId: res.Data.MachineId,
|
||||
Configs: res.Data.Configs,
|
||||
ExpiredAt: res.Data.ExpiredAt,
|
||||
IsActive: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LicenseService) fetchUrlWhiteList() ([]string, error) {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/urls")
|
||||
response, err := req.C().R().SetSuccessResult(&res).Get(apiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||
}
|
||||
if response.IsErrorState() {
|
||||
return nil, fmt.Errorf("发送请求失败:%v", response.Status)
|
||||
}
|
||||
if res.Code != types.Success {
|
||||
return nil, fmt.Errorf("获取白名单失败:%v", res.Message)
|
||||
}
|
||||
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
// GetLicense 获取许可信息
|
||||
func (s *LicenseService) GetLicense() *types.License {
|
||||
if s.license == nil {
|
||||
var config model.Config
|
||||
s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).First(&config)
|
||||
if config.Value != "" {
|
||||
utils.JsonDecode(config.Value, &s.license)
|
||||
}
|
||||
}
|
||||
return s.license
|
||||
}
|
||||
|
||||
func (s *LicenseService) SetLicense(licenseKey string) {
|
||||
s.license.Key = licenseKey
|
||||
|
||||
}
|
||||
|
||||
// IsValidApiURL 判断是否合法的中转 URL
|
||||
func (s *LicenseService) IsValidApiURL(uri string) error {
|
||||
// 获得许可授权的直接放行
|
||||
if s.license.IsActive {
|
||||
if s.license.MachineId != s.machineId {
|
||||
return errors.New("系统使用了盗版的许可证书")
|
||||
}
|
||||
|
||||
if time.Now().Unix() > s.license.ExpiredAt {
|
||||
return errors.New("系统许可证书已经过期")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(s.urlWhiteList) == 0 {
|
||||
urls, err := s.fetchUrlWhiteList()
|
||||
if err == nil {
|
||||
s.urlWhiteList = urls
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.urlWhiteList {
|
||||
if strings.HasPrefix(uri, v) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("当前 API 地址 %s 不在白名单列表当中。", uri)
|
||||
}
|
||||
+167
-109
@@ -8,16 +8,17 @@ package service
|
||||
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,61 +30,29 @@ const (
|
||||
|
||||
// MigrationService 配置迁移服务
|
||||
type MigrationService struct {
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
appConfig *types.AppConfig
|
||||
levelDB *store.LevelDB
|
||||
licenseService *LicenseService
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
appConfig *types.AppConfig
|
||||
levelDB *store.LevelDB
|
||||
}
|
||||
|
||||
func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig, levelDB *store.LevelDB, licenseService *LicenseService) *MigrationService {
|
||||
func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig, levelDB *store.LevelDB) *MigrationService {
|
||||
return &MigrationService{
|
||||
db: db,
|
||||
redisClient: redisClient,
|
||||
appConfig: appConfig,
|
||||
levelDB: levelDB,
|
||||
licenseService: licenseService,
|
||||
db: db,
|
||||
redisClient: redisClient,
|
||||
appConfig: appConfig,
|
||||
levelDB: levelDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MigrationService) StartMigrate() {
|
||||
// 表结构同步必须在对外服务前完成,避免缺列导致业务报错
|
||||
s.TableMigration()
|
||||
go func() {
|
||||
s.MigrateConfig(s.appConfig)
|
||||
s.TableMigration()
|
||||
s.MigrateLicense()
|
||||
_ = s.MigrateConfig(s.appConfig)
|
||||
}()
|
||||
}
|
||||
|
||||
// 迁移 License
|
||||
func (s *MigrationService) MigrateLicense() {
|
||||
key := "migrate:license"
|
||||
if s.redisClient.Get(context.Background(), key).Val() == "1" {
|
||||
logger.Info("License 已迁移,跳过迁移")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("开始迁移 License...")
|
||||
var license types.License
|
||||
err := s.levelDB.Get(types.LicenseKey, &license)
|
||||
if err != nil {
|
||||
license = types.License{
|
||||
Key: "",
|
||||
MachineId: "",
|
||||
Configs: types.LicenseConfig{UserNum: 0, DeCopy: false},
|
||||
ExpiredAt: 0,
|
||||
IsActive: false,
|
||||
}
|
||||
}
|
||||
logger.Infof("迁移 License: %+v", license)
|
||||
if err := s.saveConfig(types.ConfigKeyLicense, license); err != nil {
|
||||
logger.Errorf("迁移 License 失败: %v", err)
|
||||
return
|
||||
}
|
||||
s.licenseService.SetLicense(license.Key)
|
||||
logger.Info("迁移 License 完成")
|
||||
s.redisClient.Set(context.Background(), key, "1", 0)
|
||||
}
|
||||
|
||||
// 迁移配置内容
|
||||
func (s *MigrationService) MigrateConfigContent() error {
|
||||
// 用户协议
|
||||
@@ -157,74 +126,163 @@ func (s *MigrationService) MigrateConfigContent() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 数据表迁移
|
||||
func (s *MigrationService) TableMigration() {
|
||||
// 永不删除的保护列(大小写不敏感)
|
||||
var protectedColumns = map[string]struct{}{
|
||||
"id": {},
|
||||
"created_at": {},
|
||||
"updated_at": {},
|
||||
}
|
||||
|
||||
// v4.2.7 数据表迁移
|
||||
if s.db.Migrator().HasColumn(&model.JimengJob{}, "task_params") {
|
||||
s.db.Migrator().RenameColumn(&model.JimengJob{}, "task_params", "params")
|
||||
}
|
||||
|
||||
// 新数据表
|
||||
if !s.db.Migrator().HasTable(&model.Moderation{}) {
|
||||
s.db.AutoMigrate(&model.Moderation{})
|
||||
}
|
||||
|
||||
// 订单字段整理
|
||||
if s.db.Migrator().HasColumn(&model.Order{}, "pay_type") {
|
||||
s.db.Migrator().RenameColumn(&model.Order{}, "pay_type", "channel")
|
||||
}
|
||||
if !s.db.Migrator().HasColumn(&model.Order{}, "checked") {
|
||||
s.db.Migrator().AddColumn(&model.Order{}, "checked")
|
||||
}
|
||||
|
||||
// 重命名 config 表字段
|
||||
if s.db.Migrator().HasColumn(&model.Config{}, "config_json") {
|
||||
s.db.Migrator().RenameColumn(&model.Config{}, "config_json", "value")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Config{}, "marker") {
|
||||
s.db.Migrator().RenameColumn(&model.Config{}, "marker", "name")
|
||||
}
|
||||
if s.db.Migrator().HasIndex(&model.Config{}, "idx_chatgpt_configs_key") {
|
||||
s.db.Migrator().DropIndex(&model.Config{}, "idx_chatgpt_configs_key")
|
||||
}
|
||||
if s.db.Migrator().HasIndex(&model.Config{}, "marker") {
|
||||
s.db.Migrator().DropIndex(&model.Config{}, "marker")
|
||||
}
|
||||
|
||||
// 手动删除字段
|
||||
if s.db.Migrator().HasColumn(&model.Order{}, "deleted_at") {
|
||||
s.db.Migrator().DropColumn(&model.Order{}, "deleted_at")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.ChatItem{}, "deleted_at") {
|
||||
s.db.Migrator().DropColumn(&model.ChatItem{}, "deleted_at")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.ChatMessage{}, "deleted_at") {
|
||||
s.db.Migrator().DropColumn(&model.ChatMessage{}, "deleted_at")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.User{}, "chat_config") {
|
||||
s.db.Migrator().DropColumn(&model.User{}, "chat_config")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.ChatModel{}, "category") {
|
||||
s.db.Migrator().DropColumn(&model.ChatModel{}, "category")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.ChatModel{}, "description") {
|
||||
s.db.Migrator().DropColumn(&model.ChatModel{}, "description")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Product{}, "discount") {
|
||||
s.db.Migrator().DropColumn(&model.Product{}, "discount")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Product{}, "days") {
|
||||
s.db.Migrator().DropColumn(&model.Product{}, "days")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Product{}, "app_url") {
|
||||
s.db.Migrator().DropColumn(&model.Product{}, "app_url")
|
||||
}
|
||||
if s.db.Migrator().HasColumn(&model.Product{}, "url") {
|
||||
s.db.Migrator().DropColumn(&model.Product{}, "url")
|
||||
// allModels 全部需要同步的数据表 model
|
||||
func allModels() []any {
|
||||
return []any{
|
||||
&model.AdminUser{},
|
||||
&model.ApiKey{},
|
||||
&model.AppType{},
|
||||
&model.ChatApp{},
|
||||
&model.ChatItem{},
|
||||
&model.ChatMessage{},
|
||||
&model.ChatModel{},
|
||||
&model.Config{},
|
||||
&model.DallJob{},
|
||||
&model.File{},
|
||||
&model.Function{},
|
||||
&model.InviteCode{},
|
||||
&model.InviteLog{},
|
||||
&model.JimengJob{},
|
||||
&model.Menu{},
|
||||
&model.MidJourneyJob{},
|
||||
&model.Moderation{},
|
||||
&model.Order{},
|
||||
&model.PowerLog{},
|
||||
&model.Product{},
|
||||
&model.Redeem{},
|
||||
&model.SdJob{},
|
||||
&model.SunoJob{},
|
||||
&model.User{},
|
||||
&model.UserLoginLog{},
|
||||
&model.VideoJob{},
|
||||
}
|
||||
}
|
||||
|
||||
// 数据表迁移:先处理字段重命名(保数据),再全量同步 schema
|
||||
func (s *MigrationService) TableMigration() {
|
||||
logger.Info("开始数据表迁移...")
|
||||
s.renameColumns()
|
||||
if err := s.SyncAllModels(); err != nil {
|
||||
logger.Errorf("同步数据表字段失败: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Info("数据表迁移完成")
|
||||
}
|
||||
|
||||
// renameColumns 只处理「改名」场景:删旧加新会丢数据,必须先 Rename
|
||||
func (s *MigrationService) renameColumns() {
|
||||
m := s.db.Migrator()
|
||||
|
||||
if m.HasColumn(&model.JimengJob{}, "task_params") {
|
||||
_ = m.RenameColumn(&model.JimengJob{}, "task_params", "params")
|
||||
}
|
||||
if m.HasColumn(&model.Order{}, "pay_type") {
|
||||
_ = m.RenameColumn(&model.Order{}, "pay_type", "channel")
|
||||
}
|
||||
if m.HasColumn(&model.Config{}, "config_json") {
|
||||
_ = m.RenameColumn(&model.Config{}, "config_json", "value")
|
||||
}
|
||||
if m.HasColumn(&model.Config{}, "marker") {
|
||||
_ = m.RenameColumn(&model.Config{}, "marker", "name")
|
||||
}
|
||||
if m.HasIndex(&model.Config{}, "idx_chatgpt_configs_key") {
|
||||
_ = m.DropIndex(&model.Config{}, "idx_chatgpt_configs_key")
|
||||
}
|
||||
if m.HasIndex(&model.Config{}, "marker") {
|
||||
_ = m.DropIndex(&model.Config{}, "marker")
|
||||
}
|
||||
}
|
||||
|
||||
// SyncAllModels 按 model 定义同步所有数据表:缺列新建,多余列删除
|
||||
func (s *MigrationService) SyncAllModels() error {
|
||||
var firstErr error
|
||||
for _, m := range allModels() {
|
||||
if err := s.syncModel(m); err != nil {
|
||||
logger.Errorf("同步 model %T 失败: %v", m, err)
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (s *MigrationService) syncModel(dst any) error {
|
||||
tableName := s.tableName(dst)
|
||||
if err := s.db.AutoMigrate(dst); err != nil {
|
||||
return fmt.Errorf("AutoMigrate %s: %w", tableName, err)
|
||||
}
|
||||
if err := s.dropUnusedColumns(dst); err != nil {
|
||||
return fmt.Errorf("drop unused columns %s: %w", tableName, err)
|
||||
}
|
||||
logger.Infof("已同步数据表: %s", tableName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MigrationService) dropUnusedColumns(dst any) error {
|
||||
dbCols, err := s.db.Migrator().ColumnTypes(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
modelCols, err := s.modelColumnNames(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, col := range dbCols {
|
||||
name := col.Name()
|
||||
if s.isProtectedColumn(name) {
|
||||
continue
|
||||
}
|
||||
if _, ok := modelCols[strings.ToLower(name)]; ok {
|
||||
continue
|
||||
}
|
||||
logger.Infof("删除多余字段: %s.%s", s.tableName(dst), name)
|
||||
if err := s.db.Migrator().DropColumn(dst, name); err != nil {
|
||||
return fmt.Errorf("DropColumn %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MigrationService) modelColumnNames(dst any) (map[string]struct{}, error) {
|
||||
parsed, err := schema.Parse(dst, &schemaCache, s.db.Config.NamingStrategy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cols := make(map[string]struct{}, len(parsed.Fields))
|
||||
for _, field := range parsed.Fields {
|
||||
if field.DBName == "" || field.IgnoreMigration {
|
||||
continue
|
||||
}
|
||||
cols[strings.ToLower(field.DBName)] = struct{}{}
|
||||
}
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
func (s *MigrationService) isProtectedColumn(name string) bool {
|
||||
_, ok := protectedColumns[strings.ToLower(name)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *MigrationService) tableName(dst any) string {
|
||||
stmt := &gorm.Statement{DB: s.db}
|
||||
if err := stmt.Parse(dst); err != nil {
|
||||
return fmt.Sprintf("%T", dst)
|
||||
}
|
||||
return stmt.Schema.Table
|
||||
}
|
||||
|
||||
// schema.Parse 进程内复用的 schema cache
|
||||
var schemaCache sync.Map
|
||||
|
||||
// 迁移配置数据
|
||||
func (s *MigrationService) MigrateConfig(config *types.AppConfig) error {
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/imroc/req/v3"
|
||||
@@ -26,9 +25,8 @@ import (
|
||||
|
||||
// Client MidJourney client
|
||||
type Client struct {
|
||||
client *req.Client
|
||||
licenseService *service.LicenseService
|
||||
db *gorm.DB
|
||||
client *req.Client
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type ImageReq struct {
|
||||
@@ -77,11 +75,10 @@ type QueryRes struct {
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewClient(licenseService *service.LicenseService, db *gorm.DB) *Client {
|
||||
func NewClient(db *gorm.DB) *Client {
|
||||
return &Client{
|
||||
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
||||
licenseService: licenseService,
|
||||
db: db,
|
||||
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,10 +195,6 @@ func (c *Client) doRequest(body interface{}, apiPath string, channel string) (Im
|
||||
return ImageRes{}, fmt.Errorf("no available MidJourney api key: %v", err)
|
||||
}
|
||||
|
||||
if err = c.licenseService.IsValidApiURL(apiKey.ApiURL); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/%s", apiKey.ApiURL, apiPath)
|
||||
logger.Info("API URL: ", apiURL)
|
||||
r, err := req.C().R().
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package sora
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
logger2 "geekai/logger"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type SoraService struct {
|
||||
uploadManager *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewSoraService(uploadManager *oss.UploaderManager) *SoraService {
|
||||
return &SoraService{
|
||||
uploadManager: uploadManager,
|
||||
}
|
||||
}
|
||||
|
||||
// 下载视频地址
|
||||
func (s *SoraService) DownloadVideoURL(text string) (*vo.File, error) {
|
||||
videoDataURL, err := s.ExtractVideoURL(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取 JSON 数据
|
||||
resp, err := http.Get(videoDataURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析视频下载地址
|
||||
var videoData map[string]any
|
||||
err = json.Unmarshal(body, &videoData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := videoData["url"].(string); ok && v != "" {
|
||||
logger.Infof("try to download video: %s", v)
|
||||
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v, ".mp4", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
size, _ := utils.GetFileSize(videoURL)
|
||||
name := filepath.Base(videoURL)
|
||||
return &vo.File{
|
||||
Name: name,
|
||||
ObjKey: name,
|
||||
URL: videoURL,
|
||||
Ext: ".mp4",
|
||||
Size: size,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("no video URL found: " + string(body))
|
||||
|
||||
}
|
||||
|
||||
// 从文本中提取视频URL
|
||||
func (s *SoraService) ExtractVideoURL(text string) (string, error) {
|
||||
// 提取原始 JSON 数据地址
|
||||
//[原始数据](https://asyncdata.net/source/task_01k8pye324ef7t6heq6jyaxbfe) 类似这样的链接
|
||||
re := regexp.MustCompile(`\[原始数据\]\((https?://.*?)\)`)
|
||||
matches := re.FindAllStringSubmatch(text, 1)
|
||||
if len(matches) == 0 {
|
||||
return "", errors.New("no video URL found")
|
||||
}
|
||||
return matches[0][1], nil
|
||||
}
|
||||
@@ -70,6 +70,7 @@ func (s *UserService) DecreasePower(userId uint, power int, log model.PowerLog)
|
||||
return fmt.Errorf("扣减算力失败:%v", err)
|
||||
}
|
||||
|
||||
tx.Where("id", userId).First(&user)
|
||||
err = tx.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
|
||||
@@ -39,9 +39,9 @@ const (
|
||||
LoginStatusExpired = "expired" // 登录失效,需要重新登录
|
||||
)
|
||||
|
||||
func NewWxLoginService(config types.WxLoginConfig, redisClient *redis.Client) *WxLoginService {
|
||||
func NewWxLoginService(sysConfig *types.SystemConfig, redisClient *redis.Client) *WxLoginService {
|
||||
return &WxLoginService{
|
||||
config: config,
|
||||
config: sysConfig.WxLogin,
|
||||
client: req.C().SetTimeout(10 * time.Second),
|
||||
redisClient: redisClient,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type AdminUser struct {
|
||||
Salt string `gorm:"column:salt;type:char(12);not null;comment:密码盐" json:"salt"`
|
||||
Status bool `gorm:"column:status;type:tinyint(1);not null;comment:当前状态" json:"status"`
|
||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(32);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ type Function struct {
|
||||
Token string `gorm:"column:token;type:varchar(255);comment:API授权token" json:"token"`
|
||||
Action string `gorm:"column:action;type:varchar(255);comment:函数处理 API" json:"action"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;default:0;comment:是否启用" json:"enabled"`
|
||||
Power int `gorm:"column:power;type:int(11);not null;default:0;comment:消费算力点数" json:"power"`
|
||||
}
|
||||
|
||||
func (m *Function) TableName() string {
|
||||
|
||||
@@ -21,7 +21,7 @@ type User struct {
|
||||
ChatModels string `gorm:"column:chat_models_json;type:text;default:null;comment:AI模型 json" json:"chat_models"`
|
||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||
Vip bool `gorm:"column:vip;type:tinyint(1);not null;default:0;comment:是否会员" json:"vip"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(32);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
OpenId string `gorm:"column:openid;type:varchar(100);comment:第三方登录账号ID" json:"openid"`
|
||||
Platform string `gorm:"column:platform;type:varchar(30);comment:登录平台" json:"platform"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
|
||||
@@ -8,7 +8,7 @@ type UserLoginLog struct {
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户ID" json:"user_id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);not null;comment:用户名" json:"username"`
|
||||
LoginIp string `gorm:"column:login_ip;type:char(16);not null;comment:登录IP" json:"login_ip"`
|
||||
LoginIp string `gorm:"column:login_ip;type:char(32);not null;comment:登录IP" json:"login_ip"`
|
||||
LoginAddress string `gorm:"column:login_address;type:varchar(30);not null;comment:登录地址" json:"login_address"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
|
||||
@@ -20,4 +20,5 @@ type Function struct {
|
||||
Action string `json:"action"`
|
||||
Token string `json:"token"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Power int `json:"power"`
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
fmt.Println("test")
|
||||
}
|
||||
@@ -101,3 +101,16 @@ func GetImgExt(filename string) string {
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
func GetFileSize(url string) (int64, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(len(body)), nil
|
||||
}
|
||||
|
||||
+6
-6
@@ -14,15 +14,15 @@ npm run build
|
||||
cd ../build
|
||||
|
||||
# remove docker image if exists
|
||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-api:$version-$arch
|
||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:$version-$arch
|
||||
# build docker image for Geek-AI API
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-api:$version-$arch -f dockerfile-api-go ../
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:$version-$arch -f dockerfile-api-go ../
|
||||
|
||||
# build docker image for Geek-AI-web
|
||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-web:$version-$arch
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-web:$version-$arch -f dockerfile-web-$arch ../
|
||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:$version-$arch
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:$version-$arch -f dockerfile-web-$arch ../
|
||||
|
||||
if [ "$3" = "push" ];then
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-api:$version-$arch
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-web:$version-$arch
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:$version-$arch
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:$version-$arch
|
||||
fi
|
||||
|
||||
@@ -18,10 +18,10 @@ SET time_zone = "+00:00";
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- 数据库: `geekai_plus`
|
||||
-- 数据库: `geekai`
|
||||
--
|
||||
CREATE DATABASE IF NOT EXISTS `geekai_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
USE `geekai_plus`;
|
||||
CREATE DATABASE IF NOT EXISTS `geekai` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
USE `geekai`;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'websocket' upgrade;
|
||||
}
|
||||
server {
|
||||
# listen 443 ssl;
|
||||
listen 8080;
|
||||
# server_name www.chatgpt.com; #替换成你自己的域名
|
||||
|
||||
# ssl_certificate xxx.pem; # 替换成自己的 SSL 证书
|
||||
# ssl_certificate_key xxx.key;
|
||||
# ssl_session_timeout 5m;
|
||||
# ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
|
||||
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
|
||||
# 日志地址
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
index index.html;
|
||||
root /var/www/app/dist; # 这里改成前端静态页面的地址
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# 后端 API 的转发
|
||||
location /api/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 12s;
|
||||
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 Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://geekai-api:5678;
|
||||
}
|
||||
|
||||
# 静态资源转发
|
||||
location /static/ {
|
||||
proxy_pass http://geekai-api:5678;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
version: '3'
|
||||
services:
|
||||
# mysql
|
||||
geekai-mysql:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/mysql:8.0.33
|
||||
container_name: geekai-mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=mhSCk0NheGhmtsha
|
||||
ports:
|
||||
- '3307:3306'
|
||||
volumes:
|
||||
- ./conf/mysql/my.cnf:/etc/mysql/my.cnf
|
||||
- ./data/mysql/data:/var/lib/mysql
|
||||
- ./logs/mysql:/var/log/mysql
|
||||
- ./data/mysql/init.d:/docker-entrypoint-initdb.d/
|
||||
healthcheck:
|
||||
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
# redis
|
||||
geekai-redis:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/redis:6.0.6
|
||||
restart: always
|
||||
container_name: geekai-redis
|
||||
command: redis-server --requirepass mhSCk0NheGhmtsha
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
ports:
|
||||
- '6380:6379'
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 3s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
geekai-tika:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/tika:latest
|
||||
container_name: geekai-tika
|
||||
restart: always
|
||||
ports:
|
||||
- '9999:9998'
|
||||
|
||||
# 后端 API 程序
|
||||
geekai-api:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.2.8-amd64
|
||||
container_name: geekai-api
|
||||
restart: always
|
||||
depends_on:
|
||||
geekai-mysql:
|
||||
condition: service_healthy
|
||||
geekai-redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- DEBUG=false
|
||||
- LOG_LEVEL=info
|
||||
- CONFIG_FILE=config.toml
|
||||
ports:
|
||||
- '5678:5678'
|
||||
volumes:
|
||||
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
|
||||
- ./conf/config.toml:/var/www/app/config.toml
|
||||
- ./logs/app:/var/www/app/logs
|
||||
- ./static:/var/www/app/static
|
||||
- ./data/leveldb:/var/www/app/data
|
||||
|
||||
# 前端应用
|
||||
geekai-web:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.2.8-amd64
|
||||
container_name: geekai-web
|
||||
restart: always
|
||||
depends_on:
|
||||
- geekai-api
|
||||
ports:
|
||||
- '8080:8080'
|
||||
volumes:
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
- ./conf/nginx/conf.d:/etc/nginx/conf.d
|
||||
- ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./conf/nginx/ssl:/etc/nginx/ssl
|
||||
@@ -6,7 +6,7 @@ VITE_ADMIN_USER=admin
|
||||
VITE_ADMIN_PASS=admin123
|
||||
VITE_KEY_PREFIX=GeekAI_DEV_
|
||||
VITE_TITLE="Geek-AI 创作系统"
|
||||
VITE_VERSION=v4.2.7
|
||||
VITE_VERSION=v4.2.8
|
||||
VITE_DOCS_URL=https://docs.geekai.me
|
||||
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||
VITE_GITEE_URL=https://gitee.com/blackfox/geekai
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
VITE_API_HOST=
|
||||
VITE_WS_HOST=
|
||||
VITE_KEY_PREFIX=GeekAI_
|
||||
VITE_VERSION=v4.2.7
|
||||
VITE_VERSION=v4.2.8
|
||||
VITE_TITLE="Geek-AI 创作系统"
|
||||
VITE_DOCS_URL=https://docs.geekai.me
|
||||
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
.el-input__wrapper {
|
||||
background: var(--card-bg);
|
||||
//background: var(--card-bg);
|
||||
}
|
||||
.el-dialog__title {
|
||||
font-weight: bold;
|
||||
@@ -138,7 +138,8 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tag, .el-tag.el-tag--primary {
|
||||
.el-tag,
|
||||
.el-tag.el-tag--primary {
|
||||
--el-tag-bg-color: #f0ebff;
|
||||
}
|
||||
.box-card {
|
||||
@@ -148,4 +149,4 @@
|
||||
}
|
||||
.el-table th.el-table__cell {
|
||||
background-color: var(--chat-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,10 +189,11 @@
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #42b983;
|
||||
border-left: 5px solid #c1c1c1;
|
||||
padding: 10px 15px;
|
||||
color: #777;
|
||||
background-color: rgba(66, 185, 131, 0.1);
|
||||
background-color: rgba(193, 193, 193, 0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div v-if="list.length" class="space-y-3 mb-2">
|
||||
<div
|
||||
v-for="(f, idx) in list"
|
||||
:key="f.url || idx"
|
||||
class="flex !items-start flex-col justify-center gap-3 p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
>
|
||||
<!-- Image -->
|
||||
<div
|
||||
v-if="isImageFile(f)"
|
||||
class="flex-shrink-0 bg-gray-100 max-w-[500px] max-h-[500px] dark:bg-gray-700 rounded-lg overflow-hidden"
|
||||
>
|
||||
<el-image
|
||||
:src="f.url"
|
||||
fit="cover"
|
||||
:preview-src-list="imageUrls"
|
||||
:initial-index="imageIndexMap.get(f.url) || 0"
|
||||
hide-on-click-modal
|
||||
:z-index="3000"
|
||||
class="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Video -->
|
||||
<div
|
||||
v-else-if="isVideoFile(f)"
|
||||
class="flex-shrink-0 bg-gray-100 max-w-[500px] dark:bg-gray-700 rounded-lg overflow-hidden"
|
||||
>
|
||||
<video :src="f.url" controls preload="metadata" class="w-full h-full object-cover"></video>
|
||||
</div>
|
||||
|
||||
<!-- Audio -->
|
||||
<div
|
||||
v-else-if="isAudioFile(f)"
|
||||
class="flex-shrink-0 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<audio :src="f.url" controls preload="metadata"></audio>
|
||||
</div>
|
||||
|
||||
<!-- Other Files -->
|
||||
<div
|
||||
v-else
|
||||
class="flex-shrink-0 w-20 h-20 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<img :src="GetFileIcon(extOf(f))" class="w-20 h-20" alt="file" />
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-row min-w-0 justify-between items-center">
|
||||
<!-- File Info -->
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ extOf(f).replace('.', '').toUpperCase() || 'FILE' }} ·
|
||||
{{ FormatFileSize(f.size || 0) }}
|
||||
</div>
|
||||
|
||||
<!-- Download Button -->
|
||||
<div class="flex-shrink-0">
|
||||
<el-tooltip class="box-item" effect="dark" content="下载" placement="top">
|
||||
<i
|
||||
class="iconfont icon-download !text-sm cursor-pointer"
|
||||
v-if="!f.downloading"
|
||||
@click="downloadFile(f)"
|
||||
></i>
|
||||
<el-image src="/images/loading.gif" class="w-4 h-4" fit="cover" v-else />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { FormatFileSize, GetFileIcon } from '@/store/system'
|
||||
import { computed } from 'vue'
|
||||
import { httpDownload } from '@/utils/http'
|
||||
import { replaceImg } from '@/utils/libs'
|
||||
|
||||
const props = defineProps({
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const list = computed(() => props.files || [])
|
||||
|
||||
const normalizeExt = (ext) => (ext || '').toLowerCase()
|
||||
const urlExt = (url) => {
|
||||
if (!url) return ''
|
||||
try {
|
||||
const path = url.split('?')[0]
|
||||
const dot = path.lastIndexOf('.')
|
||||
return dot >= 0 ? path.substring(dot) : ''
|
||||
} catch (_) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const extOf = (f) => normalizeExt(f.ext || urlExt(f.url))
|
||||
|
||||
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'])
|
||||
const VIDEO_EXTS = new Set(['.mp4', '.webm', '.ogg', '.mov', '.m4v'])
|
||||
const AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg', '.aac', '.m4a'])
|
||||
|
||||
const isImageFile = (f) => IMAGE_EXTS.has(extOf(f))
|
||||
const isVideoFile = (f) => VIDEO_EXTS.has(extOf(f))
|
||||
const isAudioFile = (f) => AUDIO_EXTS.has(extOf(f))
|
||||
|
||||
// image preview urls and index mapping for el-image gallery
|
||||
const imageUrls = computed(() => list.value.filter((f) => isImageFile(f)).map((f) => f.url))
|
||||
const imageIndexMap = computed(() => {
|
||||
const map = new Map()
|
||||
imageUrls.value.forEach((u, i) => map.set(u, i))
|
||||
return map
|
||||
})
|
||||
|
||||
const downloadFile = async (item) => {
|
||||
const url = replaceImg(item.url)
|
||||
const downloadURL = `/api/download?url=${url}`
|
||||
const urlObj = new URL(url)
|
||||
const fileName = urlObj.pathname.split('/').pop()
|
||||
|
||||
item.downloading = true
|
||||
|
||||
try {
|
||||
const response = await httpDownload(downloadURL)
|
||||
const blob = new Blob([response.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
item.downloading = false
|
||||
} catch (error) {
|
||||
showMessageError('下载失败')
|
||||
item.downloading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@
|
||||
<div v-if="files && files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files" :key="file.url">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
<el-image :src="file.url" fit="cover" :preview-src-list="[file.url]" />
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<img :src="data.icon" alt="ChatGPT" />
|
||||
</div>
|
||||
<div class="chat-item">
|
||||
<div class="content-wrapper">
|
||||
<div class="content-wrapper flex-col">
|
||||
<div
|
||||
class="content"
|
||||
v-html="md.render(processContent(data.content.text))"
|
||||
@@ -15,6 +15,12 @@
|
||||
<div class="content flex justify-start items-center" v-else>
|
||||
<span class="mr-2">AI 思考中</span> <Thinking :duration="1.5" />
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<AttachmentList
|
||||
v-if="data.content && data.content.files && data.content.files.length"
|
||||
:files="data.content.files"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex text-gray-500 text-sm py-2 items-center space-x-2"
|
||||
@@ -79,6 +85,7 @@ import emoji from 'markdown-it-emoji'
|
||||
import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { nextTick, onMounted, reactive, ref, watchEffect } from 'vue'
|
||||
import Thinking from './Thinking.vue'
|
||||
import AttachmentList from './AttachmentList.vue'
|
||||
// eslint-disable-next-line no-undef,no-unused-vars
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -118,7 +125,7 @@ const md = new MarkdownIt({
|
||||
// 显示复制代码按钮和展开/收起按钮
|
||||
const copyBtn = `<div class="flex">
|
||||
<span class="text-[12px] mr-2 text-[#00e0e0] cursor-pointer expand-btn" data-code-id="${codeIndex}" onclick="window.toggleCodeBlock('${codeIndex}')">收起</span>
|
||||
<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
|
||||
<span class="copy-code-btn text-blue-500 text-sm cursor-pointer" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
|
||||
</div><textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
|
||||
/<\/textarea>/g,
|
||||
'</textarea>'
|
||||
@@ -127,7 +134,7 @@ const md = new MarkdownIt({
|
||||
let preCode = ''
|
||||
// 处理代码高亮
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
langHtml = `<span class="lang-name">${lang}</span>`
|
||||
langHtml = `<span class="lang-name text-white">${lang}</span>`
|
||||
preCode = hl.highlight(str, { language: lang }).value
|
||||
} else {
|
||||
preCode = md.utils.escapeHtml(str)
|
||||
@@ -137,7 +144,7 @@ const md = new MarkdownIt({
|
||||
return `<pre class="code-container flex flex-col code-expanded" data-code-id="${codeIndex}">
|
||||
<div class="flex justify-between bg-[#50505a] w-full rounded-tl-[10px] rounded-tr-[10px] px-3 py-1">${langHtml}${copyBtn}</div>
|
||||
<code class="language-${lang} hljs">${preCode}</code>
|
||||
<span class="copy-code-btn absolute right-3 bottom-3" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span></pre>`
|
||||
<span class="copy-code-btn absolute right-3 bottom-3 text-white text-sm cursor-pointer" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span></pre>`
|
||||
},
|
||||
})
|
||||
md.use(mathjaxPlugin)
|
||||
@@ -313,6 +320,16 @@ const setupCodeBlockEvents = () => {
|
||||
border-radius: 0 10px 10px 10px;
|
||||
width: 100%;
|
||||
|
||||
.code-expanded {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -320,6 +337,10 @@ const setupCodeBlockEvents = () => {
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
think {
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-container class="chat-file-list">
|
||||
<div v-for="file in fileList" :key="file.url">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
<el-image :src="file.url" fit="cover" :preview-src-list="[file.url]" />
|
||||
<div class="action">
|
||||
<el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon>
|
||||
</div>
|
||||
|
||||
@@ -11,22 +11,26 @@
|
||||
<span class="mr-2">{{ copyRight }}</span>
|
||||
</div>
|
||||
<div class="flex justify-center text-sm">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank">ICP备案:{{ icp }}</a>
|
||||
<span>|</span>
|
||||
<img :src="gaBeianImg" class="w-4 h-4 mx-1" alt="beian" />
|
||||
<a
|
||||
:href="`http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=${getCodeNum(
|
||||
gaBeian
|
||||
)}`"
|
||||
target="_blank"
|
||||
>{{ gaBeian }}</a
|
||||
>
|
||||
<template v-if="icp">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank">ICP备案:{{ icp }}</a>
|
||||
</template>
|
||||
<template v-if="gaBeian">
|
||||
<span>|</span>
|
||||
<img :src="gaBeianImg" class="w-4 h-4 mx-1" alt="beian" />
|
||||
<a
|
||||
:href="`http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=${getCodeNum(
|
||||
gaBeian
|
||||
)}`"
|
||||
target="_blank"
|
||||
>{{ gaBeian }}</a
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { getSystemInfo } from '@/store/cache'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -36,7 +40,6 @@ const gitURL = ref(import.meta.env.VITE_GITHUB_URL)
|
||||
const copyRight = ref('')
|
||||
const icp = ref('')
|
||||
const gaBeian = ref('')
|
||||
const license = ref({})
|
||||
const props = defineProps({
|
||||
textColor: {
|
||||
type: String,
|
||||
@@ -61,14 +64,6 @@ getSystemInfo()
|
||||
showMessageError('获取系统配置失败:' + e.message)
|
||||
})
|
||||
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError('获取 License 失败:' + e.message)
|
||||
})
|
||||
|
||||
// 获取公安备案号
|
||||
const getCodeNum = (code) => {
|
||||
// 提取数字
|
||||
@@ -86,6 +81,7 @@ const getCodeNum = (code) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.foot-container {
|
||||
// 仅在 PC 端 fixed,移动端正常流式布局
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
@@ -95,6 +91,12 @@ const getCodeNum = (code) => {
|
||||
// background: var(--theme-bg);
|
||||
margin-top: -4px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-top: 2rem !important;
|
||||
position: static;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
// max-width: 400px;
|
||||
text-align: center;
|
||||
|
||||
@@ -21,15 +21,19 @@
|
||||
</div>
|
||||
<div v-else class="upload-item single-image-item">
|
||||
<el-image :src="imageList[0]" fit="cover" class="upload-image" />
|
||||
<div class="upload-overlay">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
circle
|
||||
@click="removeImage(0)"
|
||||
class="remove-btn"
|
||||
/>
|
||||
<div class="upload-overlay flex items-center justify-center space-x-2">
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<i
|
||||
class="iconfont icon-remove text-base text-red-500 cursor-pointer"
|
||||
@click="removeImage(index)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="预览" placement="top">
|
||||
<i
|
||||
class="iconfont icon-eye-open text-lg text-white cursor-pointer"
|
||||
@click="previewImage(index)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,15 +44,19 @@
|
||||
<div class="upload-list" v-if="imageList.length > 0">
|
||||
<div v-for="(image, index) in imageList" :key="index" class="upload-item">
|
||||
<el-image :src="image" fit="cover" class="upload-image" />
|
||||
<div class="upload-overlay">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
circle
|
||||
@click="removeImage(index)"
|
||||
class="remove-btn"
|
||||
/>
|
||||
<div class="upload-overlay flex items-center justify-center space-x-2">
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<i
|
||||
class="iconfont icon-remove text-base text-red-500 cursor-pointer"
|
||||
@click="removeImage(index)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="预览" placement="top">
|
||||
<i
|
||||
class="iconfont icon-eye-open text-lg text-white cursor-pointer"
|
||||
@click="previewImage(index)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上传按钮 -->
|
||||
@@ -100,13 +108,20 @@
|
||||
:stroke-width="4"
|
||||
class="upload-progress"
|
||||
/>
|
||||
|
||||
<!-- 图片预览弹窗 -->
|
||||
<el-image-viewer
|
||||
v-if="previewVisible"
|
||||
:url-list="[previewImageSrc]"
|
||||
@close="previewVisible = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { replaceImg } from '@/utils/libs'
|
||||
import { Delete, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -138,6 +153,8 @@ const emit = defineEmits(['update:modelValue', 'upload-success'])
|
||||
// 上传状态
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const previewVisible = ref(false)
|
||||
const previewImageSrc = ref('')
|
||||
|
||||
// 图片列表
|
||||
const imageList = computed({
|
||||
@@ -225,6 +242,11 @@ const removeImage = (index) => {
|
||||
newList.splice(index, 1)
|
||||
imageList.value = newList
|
||||
}
|
||||
|
||||
const previewImage = (index) => {
|
||||
previewImageSrc.value = imageList.value[index]
|
||||
previewVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -224,11 +224,6 @@ const items = [
|
||||
index: '/admin/config/menu',
|
||||
title: '菜单配置',
|
||||
},
|
||||
{
|
||||
icon: 'license',
|
||||
index: '/admin/config/license',
|
||||
title: '授权激活',
|
||||
},
|
||||
{
|
||||
icon: 'recharge',
|
||||
index: '/admin/config/payment',
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<AttachmentList v-if="files && files.length" :files="files" />
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="action-buttons" v-if="showActions && orgContent">
|
||||
<van-button
|
||||
@@ -50,7 +54,8 @@ import emoji from 'markdown-it-emoji'
|
||||
import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { showImagePreview, showNotify } from 'vant'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import Thinking from '../Thinking.vue'
|
||||
import AttachmentList from '@/components/AttachmentList.vue'
|
||||
import Thinking from '@/components/Thinking.vue'
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
@@ -90,6 +95,8 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const files = computed(() => props.content.files || [])
|
||||
|
||||
const emits = defineEmits(['regenerate'])
|
||||
const md = new MarkdownIt({
|
||||
breaks: true,
|
||||
|
||||
@@ -8,8 +8,17 @@
|
||||
isImage(f.ext) ? 'p-0' : 'p-2',
|
||||
]"
|
||||
>
|
||||
<div v-if="isImage(f.ext)" class="relative w-[56px] h-[56px] overflow-hidden rounded-lg">
|
||||
<img :src="f.url" alt="img" class="w-full h-full object-cover" />
|
||||
<div
|
||||
v-if="isImage(f.ext)"
|
||||
class="relative max-w-[200px] max-h-[200px] overflow-hidden rounded-lg"
|
||||
>
|
||||
<el-image
|
||||
:src="f.url"
|
||||
alt="img"
|
||||
class="w-full h-full"
|
||||
:preview-src-list="[f.url]"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div v-else :class="['flex items-center', direction === 'col' ? 'w-full' : 'max-w-[240px]']">
|
||||
<img :src="GetFileIcon(f.ext)" class="w-10 h-10 mr-2" />
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center py-10 px-4 text-center">
|
||||
<div class="mb-4 flex items-center justify-center">
|
||||
<div class="w-14 h-14 rounded-full bg-gray-100 flex items-center justify-center shadow">
|
||||
<svg class="w-7 h-7 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 11c1.657 0 3-1.567 3-3.5S13.657 4 12 4 9 5.567 9 7.5 10.343 11 12 11z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 20v-1a5 5 0 015-5h2a5 5 0 015 5v1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-1">{{ title }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ statusText }}</p>
|
||||
<p v-if="description" class="text-xs text-gray-400 mt-2 max-w-sm leading-relaxed">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 允许使用插槽扩展,比如放置返回按钮或客服入口 -->
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '功能暂未开放',
|
||||
},
|
||||
statusText: {
|
||||
type: String,
|
||||
default: '该功能暂不可用,请稍后重试或联系管理员',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 保留最小样式,去除无关动画与进度条 */
|
||||
</style>
|
||||
+222
-210
@@ -6,6 +6,112 @@
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { getSystemInfo, getMenus } from '@/store/cache'
|
||||
|
||||
// PC 端主页面路由
|
||||
const homeRoutes = {
|
||||
name: 'home',
|
||||
path: '/home',
|
||||
redirect: '/chat',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'chat',
|
||||
path: '/chat',
|
||||
meta: { title: '创作中心' },
|
||||
component: () => import('@/views/ChatPlus.vue'),
|
||||
},
|
||||
{
|
||||
name: 'chat-id',
|
||||
path: '/chat/:id',
|
||||
meta: { title: '创作中心' },
|
||||
component: () => import('@/views/ChatPlus.vue'),
|
||||
},
|
||||
{
|
||||
name: 'image-mj',
|
||||
path: '/mj',
|
||||
meta: { title: 'MidJourney 绘画中心' },
|
||||
component: () => import('@/views/ImageMj.vue'),
|
||||
},
|
||||
{
|
||||
name: 'image-sd',
|
||||
path: '/sd',
|
||||
meta: { title: 'stable diffusion 绘画中心' },
|
||||
component: () => import('@/views/ImageSd.vue'),
|
||||
},
|
||||
{
|
||||
name: 'member',
|
||||
path: '/member',
|
||||
meta: { title: '会员充值中心' },
|
||||
component: () => import('@/views/Member.vue'),
|
||||
},
|
||||
{
|
||||
name: 'chat-app',
|
||||
path: '/apps',
|
||||
meta: { title: '应用中心' },
|
||||
component: () => import('@/views/ChatApps.vue'),
|
||||
},
|
||||
{
|
||||
name: 'images',
|
||||
path: '/images-wall',
|
||||
meta: { title: '作品展示' },
|
||||
component: () => import('@/views/ImagesWall.vue'),
|
||||
},
|
||||
{
|
||||
name: 'user-invitation',
|
||||
path: '/invite',
|
||||
meta: { title: '推广计划' },
|
||||
component: () => import('@/views/Invitation.vue'),
|
||||
},
|
||||
{
|
||||
name: 'powerLog',
|
||||
path: '/powerLog',
|
||||
meta: { title: '消费日志' },
|
||||
component: () => import('@/views/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
name: 'xmind',
|
||||
path: '/xmind',
|
||||
meta: { title: '思维导图' },
|
||||
component: () => import('@/views/MarkMap.vue'),
|
||||
},
|
||||
{
|
||||
name: 'dalle',
|
||||
path: '/dalle',
|
||||
meta: { title: 'DALLE-3' },
|
||||
component: () => import('@/views/Dalle.vue'),
|
||||
},
|
||||
{
|
||||
name: 'suno',
|
||||
path: '/suno',
|
||||
meta: { title: 'Suno音乐创作' },
|
||||
component: () => import('@/views/Suno.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ExternalLink',
|
||||
path: '/external',
|
||||
component: () => import('@/views/ExternalPage.vue'),
|
||||
},
|
||||
{
|
||||
name: 'song',
|
||||
path: '/song/:id',
|
||||
meta: { title: 'Suno音乐播放' },
|
||||
component: () => import('@/views/Song.vue'),
|
||||
},
|
||||
{
|
||||
name: 'video',
|
||||
path: '/video',
|
||||
meta: { title: '视频创作中心' },
|
||||
component: () => import('@/views/Video.vue'),
|
||||
},
|
||||
{
|
||||
name: 'jimeng',
|
||||
path: '/jimeng',
|
||||
meta: { title: '即梦AI' },
|
||||
component: () => import('@/views/Jimeng.vue'),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -14,109 +120,6 @@ const routes = [
|
||||
meta: { title: '首页' },
|
||||
component: () => import('@/views/Index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'home',
|
||||
path: '/home',
|
||||
redirect: '/chat',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'chat',
|
||||
path: '/chat',
|
||||
meta: { title: '创作中心' },
|
||||
component: () => import('@/views/ChatPlus.vue'),
|
||||
},
|
||||
{
|
||||
name: 'chat-id',
|
||||
path: '/chat/:id',
|
||||
meta: { title: '创作中心' },
|
||||
component: () => import('@/views/ChatPlus.vue'),
|
||||
},
|
||||
{
|
||||
name: 'image-mj',
|
||||
path: '/mj',
|
||||
meta: { title: 'MidJourney 绘画中心' },
|
||||
component: () => import('@/views/ImageMj.vue'),
|
||||
},
|
||||
{
|
||||
name: 'image-sd',
|
||||
path: '/sd',
|
||||
meta: { title: 'stable diffusion 绘画中心' },
|
||||
component: () => import('@/views/ImageSd.vue'),
|
||||
},
|
||||
{
|
||||
name: 'member',
|
||||
path: '/member',
|
||||
meta: { title: '会员充值中心' },
|
||||
component: () => import('@/views/Member.vue'),
|
||||
},
|
||||
{
|
||||
name: 'chat-app',
|
||||
path: '/apps',
|
||||
meta: { title: '应用中心' },
|
||||
component: () => import('@/views/ChatApps.vue'),
|
||||
},
|
||||
{
|
||||
name: 'images',
|
||||
path: '/images-wall',
|
||||
meta: { title: '作品展示' },
|
||||
component: () => import('@/views/ImagesWall.vue'),
|
||||
},
|
||||
{
|
||||
name: 'user-invitation',
|
||||
path: '/invite',
|
||||
meta: { title: '推广计划' },
|
||||
component: () => import('@/views/Invitation.vue'),
|
||||
},
|
||||
{
|
||||
name: 'powerLog',
|
||||
path: '/powerLog',
|
||||
meta: { title: '消费日志' },
|
||||
component: () => import('@/views/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
name: 'xmind',
|
||||
path: '/xmind',
|
||||
meta: { title: '思维导图' },
|
||||
component: () => import('@/views/MarkMap.vue'),
|
||||
},
|
||||
{
|
||||
name: 'dalle',
|
||||
path: '/dalle',
|
||||
meta: { title: 'DALLE-3' },
|
||||
component: () => import('@/views/Dalle.vue'),
|
||||
},
|
||||
{
|
||||
name: 'suno',
|
||||
path: '/suno',
|
||||
meta: { title: 'Suno音乐创作' },
|
||||
component: () => import('@/views/Suno.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ExternalLink',
|
||||
path: '/external',
|
||||
component: () => import('@/views/ExternalPage.vue'),
|
||||
},
|
||||
{
|
||||
name: 'song',
|
||||
path: '/song/:id',
|
||||
meta: { title: 'Suno音乐播放' },
|
||||
component: () => import('@/views/Song.vue'),
|
||||
},
|
||||
{
|
||||
name: 'video',
|
||||
path: '/video',
|
||||
meta: { title: '视频创作中心' },
|
||||
component: () => import('@/views/Video.vue'),
|
||||
},
|
||||
{
|
||||
name: 'jimeng',
|
||||
path: '/jimeng',
|
||||
meta: { title: '即梦AI' },
|
||||
component: () => import('@/views/Jimeng.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'chat-export',
|
||||
path: '/chat/export',
|
||||
@@ -240,12 +243,6 @@ const routes = [
|
||||
meta: { title: '菜单配置' },
|
||||
component: () => import('@/views/admin/settings/MenuConfig.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/config/license',
|
||||
name: 'admin-config-license',
|
||||
meta: { title: '授权激活' },
|
||||
component: () => import('@/views/admin/settings/LicenseConfig.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/user',
|
||||
name: 'admin-user',
|
||||
@@ -351,106 +348,6 @@ const routes = [
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'mobile',
|
||||
path: '/mobile',
|
||||
meta: { title: '首页' },
|
||||
component: () => import('@/views/mobile/Home.vue'),
|
||||
redirect: '/mobile/index',
|
||||
children: [
|
||||
{
|
||||
path: '/mobile/index',
|
||||
name: 'mobile-index',
|
||||
component: () => import('@/views/mobile/Index.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: 'AI对话' },
|
||||
path: '/mobile/chat',
|
||||
name: 'mobile-chat',
|
||||
component: () => import('@/views/mobile/ChatList.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '创作中心' },
|
||||
path: '/mobile/create',
|
||||
name: 'mobile-create',
|
||||
component: () => import('@/views/mobile/Create.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '发现' },
|
||||
path: '/mobile/discover',
|
||||
name: 'mobile-discover',
|
||||
component: () => import('@/views/mobile/Discover.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '个人中心' },
|
||||
path: '/mobile/profile',
|
||||
name: 'mobile-profile',
|
||||
component: () => import('@/views/mobile/Profile.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '会员充值' },
|
||||
path: '/mobile/member',
|
||||
name: 'mobile-member',
|
||||
component: () => import('@/views/mobile/Member.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '作品展示' },
|
||||
path: '/mobile/imgWall',
|
||||
name: 'mobile-img-wall',
|
||||
component: () => import('@/views/mobile/pages/ImgWall.vue'),
|
||||
},
|
||||
{
|
||||
path: '/mobile/chat/session',
|
||||
name: 'mobile-chat-session',
|
||||
component: () => import('@/views/mobile/ChatSession.vue'),
|
||||
},
|
||||
|
||||
{
|
||||
meta: { title: '应用中心' },
|
||||
path: '/mobile/apps',
|
||||
name: 'mobile-apps',
|
||||
component: () => import('@/views/mobile/Apps.vue'),
|
||||
},
|
||||
// 新增的功能页面路由
|
||||
{
|
||||
meta: { title: '消费日志' },
|
||||
path: '/mobile/power-log',
|
||||
name: 'mobile-power-log',
|
||||
component: () => import('@/views/mobile/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '推广计划' },
|
||||
path: '/mobile/invite',
|
||||
name: 'mobile-invite',
|
||||
component: () => import('@/views/mobile/Invite.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '设置' },
|
||||
path: '/mobile/settings',
|
||||
name: 'mobile-settings',
|
||||
component: () => import('@/views/mobile/Settings.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: 'Suno音乐创作' },
|
||||
path: '/mobile/suno',
|
||||
name: 'mobile-suno',
|
||||
component: () => import('@/views/mobile/SunoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '视频生成' },
|
||||
path: '/mobile/video',
|
||||
name: 'mobile-video',
|
||||
component: () => import('@/views/mobile/VideoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '即梦AI' },
|
||||
path: '/mobile/jimeng',
|
||||
name: 'mobile-jimeng',
|
||||
component: () => import('@/views/mobile/JimengCreate.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'test',
|
||||
path: '/test',
|
||||
@@ -466,6 +363,106 @@ const routes = [
|
||||
},
|
||||
]
|
||||
|
||||
const mobileRoutes = {
|
||||
name: 'mobile',
|
||||
path: '/mobile',
|
||||
meta: { title: '首页' },
|
||||
component: () => import('@/views/mobile/Home.vue'),
|
||||
redirect: '/mobile/index',
|
||||
children: [
|
||||
{
|
||||
path: '/mobile/index',
|
||||
name: 'mobile-index',
|
||||
component: () => import('@/views/mobile/Index.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: 'AI对话' },
|
||||
path: '/mobile/chat',
|
||||
name: 'mobile-chat',
|
||||
component: () => import('@/views/mobile/ChatList.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '创作中心' },
|
||||
path: '/mobile/create',
|
||||
name: 'mobile-create',
|
||||
component: () => import('@/views/mobile/Create.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '发现' },
|
||||
path: '/mobile/discover',
|
||||
name: 'mobile-discover',
|
||||
component: () => import('@/views/mobile/Discover.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '个人中心' },
|
||||
path: '/mobile/profile',
|
||||
name: 'mobile-profile',
|
||||
component: () => import('@/views/mobile/Profile.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '会员充值' },
|
||||
path: '/mobile/member',
|
||||
name: 'mobile-member',
|
||||
component: () => import('@/views/mobile/Member.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '作品展示' },
|
||||
path: '/mobile/imgWall',
|
||||
name: 'mobile-img-wall',
|
||||
component: () => import('@/views/mobile/pages/ImgWall.vue'),
|
||||
},
|
||||
{
|
||||
path: '/mobile/chat/session',
|
||||
name: 'mobile-chat-session',
|
||||
component: () => import('@/views/mobile/ChatSession.vue'),
|
||||
},
|
||||
|
||||
{
|
||||
meta: { title: '应用中心' },
|
||||
path: '/mobile/apps',
|
||||
name: 'mobile-apps',
|
||||
component: () => import('@/views/mobile/Apps.vue'),
|
||||
},
|
||||
// 新增的功能页面路由
|
||||
{
|
||||
meta: { title: '消费日志' },
|
||||
path: '/mobile/power-log',
|
||||
name: 'mobile-power-log',
|
||||
component: () => import('@/views/mobile/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '推广计划' },
|
||||
path: '/mobile/invite',
|
||||
name: 'mobile-invite',
|
||||
component: () => import('@/views/mobile/Invite.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '设置' },
|
||||
path: '/mobile/settings',
|
||||
name: 'mobile-settings',
|
||||
component: () => import('@/views/mobile/Settings.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: 'Suno音乐创作' },
|
||||
path: '/mobile/suno',
|
||||
name: 'mobile-suno',
|
||||
component: () => import('@/views/mobile/SunoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '视频生成' },
|
||||
path: '/mobile/video',
|
||||
name: 'mobile-video',
|
||||
component: () => import('@/views/mobile/VideoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '即梦AI' },
|
||||
path: '/mobile/jimeng',
|
||||
name: 'mobile-jimeng',
|
||||
component: () => import('@/views/mobile/JimengCreate.vue'),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// console.log(MY_VARIABLE)
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -480,4 +477,19 @@ router.beforeEach((to, from, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
export { prevRoute, router }
|
||||
// 检测是否启用了移动端(同步:顶层 await)
|
||||
const res = await getSystemInfo()
|
||||
const data = res.data
|
||||
if (data && data.enable_mobile_site) {
|
||||
router.addRoute(mobileRoutes)
|
||||
}
|
||||
|
||||
// 获取所有的菜单列表,然后把禁用的菜单从router中移除
|
||||
const menus = await getMenus()
|
||||
homeRoutes.children = homeRoutes.children.filter((route) => {
|
||||
return !menus[route.path] || menus[route.path]?.enabled
|
||||
})
|
||||
// 添加主页面路由
|
||||
router.addRoute(homeRoutes)
|
||||
|
||||
export { prevRoute, router, mobileRoutes }
|
||||
|
||||
+69
-65
@@ -1,99 +1,103 @@
|
||||
import { httpGet } from "@/utils/http";
|
||||
import Storage from "good-storage";
|
||||
import { randString } from "@/utils/libs";
|
||||
import { httpGet } from '@/utils/http'
|
||||
import Storage from 'good-storage'
|
||||
import { randString } from '@/utils/libs'
|
||||
|
||||
const userDataKey = "USER_INFO_CACHE_KEY";
|
||||
const adminDataKey = "ADMIN_INFO_CACHE_KEY";
|
||||
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY";
|
||||
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY";
|
||||
export function checkSession() {
|
||||
const item = Storage.get(userDataKey) ?? { expire: 0, data: null };
|
||||
const userDataKey = 'USER_INFO_CACHE_KEY'
|
||||
const adminDataKey = 'ADMIN_INFO_CACHE_KEY'
|
||||
const systemInfoKey = 'SYSTEM_INFO_CACHE_KEY'
|
||||
const menusKey = 'MENUS_CACHE_KEY'
|
||||
|
||||
export function getMenus() {
|
||||
const item = Storage.get(menusKey) ?? { expire: 0, data: null }
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/user/session")
|
||||
httpGet('/api/menu/list/all')
|
||||
.then((res) => {
|
||||
item.data = res.data;
|
||||
item.expire = Date.now() + 1000 * 3;
|
||||
Storage.set(userDataKey, item);
|
||||
resolve(item.data);
|
||||
const menus = {}
|
||||
for (const menu of res.data) {
|
||||
menus[menu.url] = menu
|
||||
}
|
||||
item.data = menus
|
||||
item.expire = Date.now() + 1000 * 3
|
||||
Storage.set(menusKey, item)
|
||||
resolve(item.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function checkSession() {
|
||||
const item = Storage.get(userDataKey) ?? { expire: 0, data: null }
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet('/api/user/session')
|
||||
.then((res) => {
|
||||
item.data = res.data
|
||||
item.expire = Date.now() + 1000 * 3
|
||||
Storage.set(userDataKey, item)
|
||||
resolve(item.data)
|
||||
})
|
||||
.catch((e) => {
|
||||
Storage.remove(userDataKey);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
Storage.remove(userDataKey)
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
export function checkAdminSession() {
|
||||
const item = Storage.get(adminDataKey) ?? { expire: 0, data: null };
|
||||
const item = Storage.get(adminDataKey) ?? { expire: 0, data: null }
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/admin/session")
|
||||
httpGet('/api/admin/session')
|
||||
.then((res) => {
|
||||
item.data = res.data;
|
||||
item.expire = Date.now() + 1000 * 30;
|
||||
Storage.set(adminDataKey, item);
|
||||
resolve(item.data);
|
||||
item.data = res.data
|
||||
item.expire = Date.now() + 1000 * 3
|
||||
Storage.set(adminDataKey, item)
|
||||
resolve(item.data)
|
||||
})
|
||||
.catch((e) => {
|
||||
Storage.remove(adminDataKey);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
Storage.remove(adminDataKey)
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function removeAdminInfo() {
|
||||
Storage.remove(adminDataKey);
|
||||
Storage.remove(adminDataKey)
|
||||
}
|
||||
|
||||
export function getSystemInfo() {
|
||||
const item = Storage.get(systemInfoKey) ?? { expire: 0, data: null };
|
||||
const item = Storage.get(systemInfoKey) ?? { expire: 0, data: null }
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/config/get?key=system")
|
||||
httpGet('/api/config/get?key=system')
|
||||
.then((res) => {
|
||||
item.data = res;
|
||||
item.expire = Date.now() + 1000 * 30;
|
||||
Storage.set(systemInfoKey, item);
|
||||
resolve(item.data);
|
||||
item.data = res
|
||||
item.expire = Date.now() + 1000 * 3
|
||||
Storage.set(systemInfoKey, item)
|
||||
resolve(item.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getLicenseInfo() {
|
||||
const item = Storage.get(licenseInfoKey) ?? { expire: 0, data: null };
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/config/license")
|
||||
.then((res) => {
|
||||
item.data = res;
|
||||
item.expire = Date.now() + 1000 * 30;
|
||||
Storage.set(licenseInfoKey, item);
|
||||
resolve(item.data);
|
||||
reject(err)
|
||||
})
|
||||
.catch((err) => {
|
||||
resolve(err);
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export function getClientId() {
|
||||
let clientId = Storage.get("client_id");
|
||||
let clientId = Storage.get('client_id')
|
||||
if (clientId) {
|
||||
return clientId;
|
||||
return clientId
|
||||
}
|
||||
clientId = randString(42);
|
||||
Storage.set("client_id", clientId);
|
||||
return clientId;
|
||||
clientId = randString(42)
|
||||
Storage.set('client_id', clientId)
|
||||
return clientId
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export const JimengParams = {
|
||||
type: 'image',
|
||||
required: false,
|
||||
placeholder: '请上传图片',
|
||||
maxSize: 10,
|
||||
maxSize: 15,
|
||||
multiple: true,
|
||||
maxCount: 10,
|
||||
accept: '.png,.jpg,.jpeg',
|
||||
|
||||
@@ -71,6 +71,7 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
// 获取任务状态文本
|
||||
const getTaskStatusText = (status) => {
|
||||
const statusMap = {
|
||||
submited: '任务已提交',
|
||||
in_queue: '任务排队中',
|
||||
generating: '任务执行中',
|
||||
success: '任务成功',
|
||||
|
||||
@@ -203,7 +203,7 @@ export function processContent(content) {
|
||||
if (content.includes('<think>')) {
|
||||
content = content.replace(/<think>(.*?)<\/think>/gs, (match, content) => {
|
||||
if (content.length > 10) {
|
||||
return `<blockquote>${content}</blockquote>`
|
||||
return `<blockquote>\n\n${content}</blockquote>`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
+74
-18
@@ -1,39 +1,95 @@
|
||||
<template>
|
||||
<div class="page-404" :style="{ height: winHeight + 'px' }">
|
||||
<div class="page-404">
|
||||
<div class="inner">
|
||||
<h1>404</h1>
|
||||
<h2>饶了地球一圈,还是没有找到您要的页面</h2>
|
||||
<div class="code">404</div>
|
||||
<p class="title">抱歉,页面走丢了</p>
|
||||
<p class="desc">您访问的页面不存在或已被移动</p>
|
||||
<div class="actions">
|
||||
<button class="btn" @click="goHome">返回首页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const winHeight = ref(window.innerHeight)
|
||||
const router = useRouter()
|
||||
function goHome() {
|
||||
router.replace({ path: '/' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-404 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
padding: calc(env(safe-area-inset-top, 0px) + 24px) 16px
|
||||
calc(env(safe-area-inset-bottom, 0px) + 32px);
|
||||
background: radial-gradient(1200px 600px at 50% -10%, #2f3542 0%, #1e222a 60%, #12161c 100%);
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
.inner {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #202020;
|
||||
font-size: 120px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.1em;
|
||||
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
|
||||
}
|
||||
.code {
|
||||
font-size: clamp(96px, 18vw, 160px);
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.04em;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #cbd5e1 60%, #94a3b8 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.title {
|
||||
margin-top: 16px;
|
||||
font-size: clamp(18px, 4.6vw, 28px);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 8px;
|
||||
font-size: clamp(14px, 3.6vw, 16px);
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
border-radius: 9999px;
|
||||
padding: 12px 20px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
background: #4f46e5;
|
||||
box-shadow: 0 10px 20px rgba(79, 70, 229, 0.35), inset 0 0 0 1px rgba(255, 255, 255, 0.15);
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #4338ca;
|
||||
box-shadow: 0 12px 24px rgba(79, 70, 229, 0.45), inset 0 0 0 1px rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -140,14 +140,9 @@
|
||||
plain
|
||||
>
|
||||
<div class="selected-model-display">
|
||||
<span class="model-name-text">{{ getSelectedModelName() }}</span>
|
||||
<el-tag
|
||||
v-if="getSelectedModel()"
|
||||
size="small"
|
||||
type="info"
|
||||
style="margin-left: 8px; flex-shrink: 0"
|
||||
>
|
||||
{{ getSelectedModel() && getSelectedModel().power }}算力
|
||||
<span class="model-name-text">{{ selectedModel.name }}</span>
|
||||
<el-tag size="small" type="info" style="margin-left: 8px; flex-shrink: 0">
|
||||
{{ selectedModel.power }}算力
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-button>
|
||||
@@ -546,10 +541,10 @@ watch(
|
||||
)
|
||||
|
||||
// 获取选中的模型名称
|
||||
const getSelectedModelName = () => {
|
||||
const selectedModel = computed(() => {
|
||||
const model = getSelectedModel()
|
||||
return model ? model.name : '选择模型'
|
||||
}
|
||||
return model ? model : { name: '选择模型', power: 0 }
|
||||
})
|
||||
|
||||
// 获取选中的模型
|
||||
const getSelectedModel = () => {
|
||||
@@ -585,10 +580,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
if (isMobile()) {
|
||||
router.push('/mobile/chat')
|
||||
}
|
||||
|
||||
// 初始化角色ID参数
|
||||
if (router.currentRoute.value.query.role_id) {
|
||||
roleId.value = parseInt(router.currentRoute.value.query.role_id)
|
||||
@@ -666,7 +657,7 @@ const initData = async () => {
|
||||
// 获取模型列表
|
||||
const modelRes = await httpGet('/api/model/list')
|
||||
models.value = modelRes.data
|
||||
if (models.value.length > 0) {
|
||||
if (models.value.length > 0 && !modelID.value) {
|
||||
modelID.value = models.value[0].id
|
||||
}
|
||||
|
||||
|
||||
+71
-69
@@ -3,7 +3,7 @@
|
||||
<div class="page-dall">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="sd-box">
|
||||
<h2>DALL-E 创作中心</h2>
|
||||
<h2 class="!text-[#252f76] py-3">AI图像生成</h2>
|
||||
|
||||
<div class="sd-params">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
@@ -25,55 +25,25 @@
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片质量">
|
||||
<el-form-item label="图片比例">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.quality" style="width: 150px">
|
||||
<el-option
|
||||
v-for="v in qualities"
|
||||
:label="v.name"
|
||||
:value="v.value"
|
||||
:key="v.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片尺寸">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.size" style="width: 150px">
|
||||
<el-option v-for="v in sizes" :label="v" :value="v" :key="v" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片样式">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.style" style="width: 150px">
|
||||
<el-option
|
||||
v-for="v in styles"
|
||||
:label="v.name"
|
||||
:value="v.value"
|
||||
:key="v.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
content="生动使模型倾向于生成超真实和戏剧性的图像"
|
||||
raw-content
|
||||
placement="right"
|
||||
<el-select
|
||||
v-model="params.aspect_ratio"
|
||||
style="width: 150px"
|
||||
value-key="value"
|
||||
@change="changeAspectRatio"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-option
|
||||
v-for="v in radioAspects"
|
||||
:label="v.label"
|
||||
:value="v.value"
|
||||
:key="v.value"
|
||||
>
|
||||
<i class="iconfont mr-1" :class="v.icon"></i>
|
||||
{{ v.label }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
@@ -105,7 +75,12 @@
|
||||
<div class="mt-2 mb-2">
|
||||
<label class="text-gray-700 font-semibold">参考图(可选)</label>
|
||||
<div class="py-2">
|
||||
<ImageUpload v-model="params.image" :max-count="5" :multiple="true" />
|
||||
<ImageUpload
|
||||
v-model="params.image"
|
||||
:max-count="5"
|
||||
:max-size="20"
|
||||
:multiple="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -291,18 +266,18 @@
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import BackTop from '@/components/BackTop.vue'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { Delete, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
|
||||
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
|
||||
const listBoxHeight = ref(0)
|
||||
// const paramBoxHeight = ref(0)
|
||||
@@ -321,21 +296,48 @@ resizeElement()
|
||||
window.onresize = () => {
|
||||
resizeElement()
|
||||
}
|
||||
const qualities = [
|
||||
{ name: '标准', value: 'standard' },
|
||||
{ name: '高清', value: 'hd' },
|
||||
]
|
||||
const dalleSizes = ['1024x1024', '1792x1024', '1024x1792']
|
||||
const fluxSizes = ['1024x1024', '1152x896', '896x1152', '1280x960', '1024x576']
|
||||
const sizes = ref(dalleSizes)
|
||||
const styles = [
|
||||
{ name: '生动', value: 'vivid' },
|
||||
{ name: '自然', value: 'natural' },
|
||||
]
|
||||
|
||||
// 为了兼容 size 和 aspect_ratio 参数,label 对应的是 aspect_ratio 的值,size 是预估值
|
||||
const radioAspects = ref({
|
||||
'1:1': { value: '1:1', size: '1024x1024', label: '1:1(正方形,头像)', icon: 'icon-aspect_1_1' },
|
||||
'2:3': {
|
||||
value: '2:3',
|
||||
size: '512x768',
|
||||
label: '2:3(社交媒体,自拍)',
|
||||
icon: 'icon-aspect_2_3',
|
||||
},
|
||||
'4:3': {
|
||||
value: '4:3',
|
||||
size: '1024x768',
|
||||
label: '4:3(文章配图,插画)',
|
||||
icon: 'icon-aspect_4_3',
|
||||
},
|
||||
'9:16': {
|
||||
value: '9:16',
|
||||
size: '768x1366',
|
||||
label: '9:16(手机壁纸,人像)',
|
||||
icon: 'icon-aspect_9_16',
|
||||
},
|
||||
'16:9': {
|
||||
value: '16:9',
|
||||
size: '1366x768',
|
||||
label: '16:9(桌面壁纸,风景)',
|
||||
icon: 'icon-aspect_16_9',
|
||||
},
|
||||
'3:2': { value: '3:2', size: '768x512', label: '3:2(1248x832)', icon: 'icon-aspect_3_2' },
|
||||
'3:4': { value: '3:4', size: '960x1280', label: '3:4(864x1152)', icon: 'icon-aspect_3_4' },
|
||||
'4:5': { value: '4:5', size: '960x1280', label: '4:5(896x1120)', icon: 'icon-aspect_4_5' },
|
||||
'5:4': {
|
||||
value: '5:4',
|
||||
size: '1280x960',
|
||||
label: '5:4(1120x896)',
|
||||
icon: 'icon-aspect_5_4',
|
||||
},
|
||||
'21:9': { value: '21:9', size: '1344x576', label: '21:9(1536x658)', icon: 'icon-aspect_16_9' },
|
||||
})
|
||||
const params = ref({
|
||||
quality: 'standard',
|
||||
aspect_ratio: '1:1',
|
||||
size: '1024x1024',
|
||||
style: 'vivid',
|
||||
prompt: '',
|
||||
})
|
||||
|
||||
@@ -385,6 +387,11 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 改变图片比例
|
||||
const changeAspectRatio = (value) => {
|
||||
params.value.size = radioAspects.value[value].size
|
||||
}
|
||||
|
||||
const initData = () => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
@@ -580,11 +587,6 @@ const generatePrompt = () => {
|
||||
|
||||
const changeModel = (model) => {
|
||||
dallPower.value = model.power
|
||||
if (model.name.startsWith('dall')) {
|
||||
sizes.value = dalleSizes
|
||||
} else {
|
||||
sizes.value = fluxSizes
|
||||
}
|
||||
params.value.model_id = selectedModel.value.id
|
||||
}
|
||||
</script>
|
||||
|
||||
+2
-12
@@ -82,7 +82,7 @@
|
||||
<span class="username title">账户信息</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!license.de_copy">
|
||||
<li>
|
||||
<a :href="githubURL" target="_blank" class="flex">
|
||||
<i class="iconfont icon-github"></i>
|
||||
<span class="title">项目源码</span>
|
||||
@@ -150,10 +150,9 @@
|
||||
import LoginDialog from '@/components/LoginDialog.vue'
|
||||
import ThemeChange from '@/components/ThemeChange.vue'
|
||||
import ConfigDialog from '@/components/UserInfoDialog.vue'
|
||||
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { removeUserToken } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -171,7 +170,6 @@ const store = useSharedStore()
|
||||
const loginUser = ref({})
|
||||
const routerViewKey = ref(0)
|
||||
const showConfigDialog = ref(false)
|
||||
const license = ref({ de_copy: true })
|
||||
const showLoginDialog = ref(false)
|
||||
const githubURL = ref(import.meta.env.VITE_GITHUB_URL)
|
||||
|
||||
@@ -261,14 +259,6 @@ onMounted(() => {
|
||||
ElMessage.error('获取系统菜单失败:' + e.message)
|
||||
})
|
||||
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
license.value = { de_copy: false }
|
||||
showMessageError('获取 License 配置:' + e.message)
|
||||
})
|
||||
curPath.value = '/' + getFirstPathSegment(window.location.href)
|
||||
init()
|
||||
})
|
||||
|
||||
+6
-18
@@ -8,7 +8,7 @@
|
||||
<img :src="logo" class="logo" alt="Geek-AI" />
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<span v-if="!license || !license.de_copy">
|
||||
<span>
|
||||
<el-tooltip class="box-item" content="部署文档" placement="bottom">
|
||||
<a :href="docsURL" class="link-button mr-3" target="_blank">
|
||||
<i class="iconfont icon-book"></i>
|
||||
@@ -88,7 +88,7 @@
|
||||
<script setup>
|
||||
import FooterBar from '@/components/FooterBar.vue'
|
||||
import ThemeChange from '@/components/ThemeChange.vue'
|
||||
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { removeUserToken } from '@/store/session'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { isMobile } from '@/utils/libs'
|
||||
@@ -102,7 +102,6 @@ const router = useRouter()
|
||||
|
||||
const title = ref('')
|
||||
const logo = ref('')
|
||||
const license = ref({ de_copy: true })
|
||||
|
||||
const isLogin = ref(false)
|
||||
const docsURL = ref(import.meta.env.VITE_DOCS_URL)
|
||||
@@ -123,17 +122,15 @@ const md = new MarkdownIt({
|
||||
typographer: true,
|
||||
}).use(emoji)
|
||||
|
||||
if (isMobile()) {
|
||||
router.push('/mobile/index')
|
||||
}
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
const data = res.data
|
||||
title.value = data.title
|
||||
logo.value = data.logo
|
||||
console.log(data)
|
||||
if (data.index_page) {
|
||||
|
||||
if (isMobile() && data.enable_mobile_site) {
|
||||
router.push('/mobile/index')
|
||||
} else if (data.index_page) {
|
||||
router.push(data.index_page)
|
||||
}
|
||||
})
|
||||
@@ -142,15 +139,6 @@ getSystemInfo()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
license.value = { de_copy: false }
|
||||
ElMessage.error('获取 License 配置失败:' + e.message)
|
||||
})
|
||||
|
||||
httpGet('/api/menu/list?index=1')
|
||||
.then((res) => {
|
||||
navs.value = res.data
|
||||
|
||||
@@ -229,7 +229,6 @@
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="task-item">
|
||||
<!-- 保持原有内容 -->
|
||||
<div class="task-left">
|
||||
<div class="task-preview">
|
||||
<el-image
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
<el-table-column prop="type" label="模型类型">
|
||||
<template #default="scope">
|
||||
<el-tag type="primary" v-if="scope.row.type === 'img'">绘图</el-tag>
|
||||
<el-tag type="success" v-else>聊天</el-tag>
|
||||
<el-tag type="warning" v-if="scope.row.type === 'tts'">语音</el-tag>
|
||||
<el-tag type="success" v-if="scope.row.type === 'chat'">聊天</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -230,7 +231,7 @@
|
||||
<script setup>
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { dateFormat, removeArrayItem, substr } from '@/utils/libs'
|
||||
import { DocumentCopy, InfoFilled, Plus, Search, Delete } from '@element-plus/icons-vue'
|
||||
import { Delete, DocumentCopy, InfoFilled, Plus, Search } from '@element-plus/icons-vue'
|
||||
import ClipboardJS from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Sortable } from 'sortablejs'
|
||||
@@ -239,7 +240,7 @@ import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
const query = ref({ name: '' })
|
||||
const item = ref({})
|
||||
const item = ref({ options: {} })
|
||||
const showDialog = ref(false)
|
||||
const title = ref('')
|
||||
const rules = reactive({
|
||||
@@ -353,6 +354,7 @@ const add = function () {
|
||||
max_tokens: 1024,
|
||||
max_context: 8192,
|
||||
temperature: 0.9,
|
||||
options: {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<el-switch v-model="scope.row.enabled" @change="functionSet('enabled', scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="消耗算力" prop="power" />
|
||||
|
||||
<el-table-column label="操作" width="150" align="right">
|
||||
<template #default="scope">
|
||||
@@ -128,6 +129,9 @@
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="item.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="消耗算力" prop="power">
|
||||
<el-input-number v-model.number="item.power" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
|
||||
@@ -138,12 +138,12 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="聊天角色" prop="chat_roles">
|
||||
<el-form-item label="应用权限" prop="chat_roles">
|
||||
<el-select
|
||||
v-model="user.chat_roles"
|
||||
multiple
|
||||
:filterable="true"
|
||||
placeholder="选择聊天角色,多选"
|
||||
placeholder="选择 AI 应用"
|
||||
>
|
||||
<el-option v-for="item in roles" :key="item.key" :label="item.name" :value="item.key" />
|
||||
</el-select>
|
||||
@@ -243,8 +243,6 @@ const rules = reactive({
|
||||
{ required: true, message: '请输入提问次数' },
|
||||
{ type: 'number', message: '请输入有效数字' },
|
||||
],
|
||||
chat_roles: [{ required: true, message: '请选择聊天角色', trigger: 'change' }],
|
||||
chat_models: [{ required: true, message: '请选择AI模型', trigger: 'change' }],
|
||||
})
|
||||
const loading = ref(true)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="版权信息" prop="copyright">
|
||||
<el-input v-model="system['copyright']" placeholder="更改此选项需要获取 License 授权" />
|
||||
<el-input v-model="system['copyright']" placeholder="请输入版权信息" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="ICP 备案号" prop="icp">
|
||||
@@ -251,7 +251,19 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
启用手机站
|
||||
<span class="text-xs text-gray-500"
|
||||
>(启用手机站点之后,用户可以在手机上访问网站)</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="system['enable_mobile_site']" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="py-2">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</el-form-item>
|
||||
@@ -323,6 +335,12 @@ const save = function () {
|
||||
if (valid) {
|
||||
httpPost('/api/admin/config/update/base', system.value)
|
||||
.then(() => {
|
||||
// 动态更新手机站路由
|
||||
if (system.value['enable_mobile_site']) {
|
||||
// addMobileRoutes()
|
||||
} else {
|
||||
// removeMobileRoutes()
|
||||
}
|
||||
ElMessage.success('操作成功!')
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
<template>
|
||||
<div class="license-config form p-5" v-loading="loading">
|
||||
<div class="container">
|
||||
<el-descriptions
|
||||
v-if="license.is_active"
|
||||
class="margin-top"
|
||||
title="已授权信息"
|
||||
:column="1"
|
||||
border
|
||||
>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">License Key</div>
|
||||
</template>
|
||||
{{ license.key }}
|
||||
<el-tooltip content="复制" placement="top">
|
||||
<i class="iconfont icon-copy ml-2 cursor-pointer" @click="copy(license.key)"></i>
|
||||
</el-tooltip>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">机器码</div>
|
||||
</template>
|
||||
{{ license.machine_id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">到期时间</div>
|
||||
</template>
|
||||
{{ dateFormat(license.expired_at) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">用户人数</div>
|
||||
</template>
|
||||
{{ license.configs?.user_num }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">去版权</div>
|
||||
</template>
|
||||
<el-icon class="selected" v-if="license.configs?.de_copy"><Select /></el-icon>
|
||||
<el-icon class="closed" v-else><CloseBold /></el-icon>
|
||||
<span class="text">去版权之后前端页面将不会显示版权信息和源码地址</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="mt-5 p-5 border border-gray-200 rounded-md bg-gray-50">
|
||||
<h3>激活后可获得以下权限:</h3>
|
||||
<div class="py-3 text-gray-500 leading-relaxed">
|
||||
<p>1、使用任意第三方中转 API KEY,而不用局限于 GeekAI 推荐的白名单列表</p>
|
||||
<p>2、可以在相关页面去除 GeekAI 的版权信息,或者修改为自己的版权信息</p>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="许可授权码" prop="license">
|
||||
<el-input v-model="licenseKey" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="active">立即激活</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { dateFormat } from '@/utils/libs'
|
||||
import { CloseBold, Select } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const license = ref({ is_active: false })
|
||||
const licenseKey = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
fetchLicense()
|
||||
})
|
||||
|
||||
const fetchLicense = () => {
|
||||
httpGet('/api/admin/config/license/get')
|
||||
.then((res) => {
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('获取 License 失败:' + e.message)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 激活授权
|
||||
const active = () => {
|
||||
if (licenseKey.value === '') {
|
||||
return ElMessage.error('请输入授权码')
|
||||
}
|
||||
httpPost('/api/admin/config/license/active', { license: licenseKey.value })
|
||||
.then((res) => {
|
||||
ElMessage.success('授权成功,机器编码为:' + res.data)
|
||||
fetchLicense()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const copy = (text) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
ElMessage.success('复制成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.license-config {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
padding: 10px 20px 40px 20px;
|
||||
}
|
||||
|
||||
.margin-top {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.cell-item {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.closed {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.active-info {
|
||||
margin: 20px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.active-info li {
|
||||
margin: 10px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -37,7 +37,7 @@
|
||||
<i class="iconfont icon-info"></i>
|
||||
</el-tooltip>
|
||||
</label>
|
||||
<el-input v-model="smtpConfig.port" type="number" placeholder="请输入端口号" />
|
||||
<el-input v-model.number="smtpConfig.port" type="number" placeholder="请输入端口号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否使用TLS"
|
||||
|
||||
@@ -38,14 +38,14 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="MinIO" name="minio">
|
||||
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/oss.html#%E6%90%AD%E5%BB%BA-minio-%E5%AD%98%E5%82%A8%E6%9C%8D%E5%8A%A1"
|
||||
target="_blank"
|
||||
>Minio 配置</a
|
||||
>。
|
||||
</div>
|
||||
</Alert>
|
||||
<el-form :model="minio" class="mt-4" label-position="top">
|
||||
<el-form-item label="Endpoint"><el-input v-model="minio.endpoint" /></el-form-item>
|
||||
<el-form-item label="AccessKey"><el-input v-model="minio.access_key" /></el-form-item>
|
||||
@@ -59,14 +59,14 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="七牛云" name="qiniu">
|
||||
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/oss.html#%E4%B8%83%E7%89%9B%E4%BA%91-oss-%E9%85%8D%E7%BD%AE"
|
||||
target="_blank"
|
||||
>七牛云配置</a
|
||||
>。
|
||||
</div>
|
||||
</Alert>
|
||||
<el-form :model="qiniu" class="mt-4" label-position="top">
|
||||
<el-form-item label="Zone">
|
||||
<template #label>
|
||||
@@ -96,6 +96,14 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="阿里云OSS" name="aliyun">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/oss.html#%E9%98%BF%E9%87%8C%E4%BA%91-oss-%E9%85%8D%E7%BD%AE"
|
||||
target="_blank"
|
||||
>阿里云OSS配置</a
|
||||
>。
|
||||
</Alert>
|
||||
<el-form :model="aliyun" class="mt-4" label-position="top">
|
||||
<el-form-item label="Endpoint"><el-input v-model="aliyun.endpoint" /></el-form-item>
|
||||
<el-form-item label="AccessKey"><el-input v-model="aliyun.access_key" /></el-form-item>
|
||||
@@ -128,11 +136,12 @@
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import Alert from '@/components/ui/Alert.vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const activeTab = ref('local')
|
||||
const active = ref('local')
|
||||
const local = ref({ base_path: '', base_url: '' })
|
||||
const local = ref({ base_path: './static/upload', base_url: '/static/upload' })
|
||||
const minio = ref({
|
||||
endpoint: '',
|
||||
access_key: '',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="app-background">
|
||||
<div class="app-background" v-if="menus['/chat']?.enabled">
|
||||
<div class="container mobile-chat-list">
|
||||
<van-nav-bar
|
||||
:title="title"
|
||||
@@ -72,15 +72,19 @@
|
||||
<van-field v-model="tmpChatTitle" label="" placeholder="请输入对话标题" class="field" />
|
||||
</van-dialog>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { checkSession, getMenus } from '@/store/cache'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { removeArrayItem, showLoginDialog } from '@/utils/libs'
|
||||
import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant'
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
|
||||
const title = ref('会话列表')
|
||||
const router = useRouter()
|
||||
@@ -99,50 +103,61 @@ const columns = ref([roles.value, models.value])
|
||||
const showEditChat = ref(false)
|
||||
const item = ref({})
|
||||
const tmpChatTitle = ref('')
|
||||
const menus = ref({})
|
||||
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
// 加载角色列表
|
||||
httpGet(`/api/app/list`)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// console.log(items[i])
|
||||
roles.value.push({
|
||||
text: items[i].name,
|
||||
value: items[i].id,
|
||||
icon: items[i].icon,
|
||||
helloMsg: items[i].hello_msg,
|
||||
model_id: items[i].model_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showFailToast('加载聊天角色失败')
|
||||
})
|
||||
onMounted(() => {
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
|
||||
// 加载模型
|
||||
httpGet('/api/model/list?enable=1')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
models.value.push({ text: items[i].name, value: items[i].id })
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
// 加载角色列表
|
||||
httpGet(`/api/app/list`)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// console.log(items[i])
|
||||
roles.value.push({
|
||||
text: items[i].name,
|
||||
value: items[i].id,
|
||||
icon: items[i].icon,
|
||||
helloMsg: items[i].hello_msg,
|
||||
model_id: items[i].model_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('加载模型失败: ' + e.message)
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
showFailToast('加载聊天角色失败')
|
||||
})
|
||||
|
||||
// 加载模型
|
||||
httpGet('/api/model/list?enable=1')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
models.value.push({ text: items[i].name, value: items[i].id })
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('加载模型失败: ' + e.message)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const onLoad = () => {
|
||||
checkSession()
|
||||
|
||||
@@ -50,12 +50,6 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const activeTab = ref(route.query.tab || 'mj')
|
||||
const menus = ref([])
|
||||
const activeMenu = ref({
|
||||
mj: false,
|
||||
sd: false,
|
||||
dall: false,
|
||||
})
|
||||
|
||||
// Tab切换处理
|
||||
const onTabChange = (name) => {
|
||||
|
||||
@@ -42,8 +42,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getMenus } from '@/store/cache'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -60,6 +62,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/create?tab=mj',
|
||||
path: '/mj',
|
||||
},
|
||||
{
|
||||
key: 'sd',
|
||||
@@ -71,6 +74,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/create?tab=sd',
|
||||
path: '/sd',
|
||||
},
|
||||
{
|
||||
key: 'dalle',
|
||||
@@ -82,6 +86,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/create?tab=dalle',
|
||||
path: '/dalle',
|
||||
},
|
||||
{
|
||||
key: 'suno',
|
||||
@@ -94,6 +99,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/suno',
|
||||
path: '/suno',
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
@@ -105,6 +111,7 @@ const aiTools = ref([
|
||||
status: 'beta',
|
||||
statusText: '测试版',
|
||||
url: '/mobile/video',
|
||||
path: '/video',
|
||||
},
|
||||
{
|
||||
key: 'jimeng',
|
||||
@@ -116,6 +123,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/jimeng',
|
||||
path: '/jimeng',
|
||||
},
|
||||
{
|
||||
key: 'imgWall',
|
||||
@@ -127,6 +135,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/imgWall',
|
||||
path: '/images-wall',
|
||||
},
|
||||
{
|
||||
key: 'apps',
|
||||
@@ -138,6 +147,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/apps',
|
||||
path: '/apps',
|
||||
},
|
||||
])
|
||||
|
||||
@@ -149,6 +159,12 @@ const navigateTo = (url) => {
|
||||
router.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getMenus().then((menus) => {
|
||||
aiTools.value = aiTools.value.filter((tool) => menus[tool.path]?.enabled)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -84,7 +84,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../assets/iconfont/iconfont.css' as *;
|
||||
@use '@/assets/iconfont/iconfont.css' as *;
|
||||
|
||||
.mobile-home {
|
||||
.page-content {
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -145,6 +145,7 @@ const features = ref([
|
||||
icon: 'icon-dalle',
|
||||
color: '#F59E0B',
|
||||
url: '/mobile/create?tab=dalle',
|
||||
path: '/dalle',
|
||||
},
|
||||
{
|
||||
key: 'suno',
|
||||
@@ -152,6 +153,7 @@ const features = ref([
|
||||
icon: 'icon-mp3',
|
||||
color: '#EF4444',
|
||||
url: '/mobile/suno',
|
||||
path: '/suno',
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
@@ -159,6 +161,7 @@ const features = ref([
|
||||
icon: 'icon-video',
|
||||
color: '#10B981',
|
||||
url: '/mobile/video',
|
||||
path: '/video',
|
||||
},
|
||||
{
|
||||
key: 'jimeng',
|
||||
@@ -166,6 +169,7 @@ const features = ref([
|
||||
icon: 'icon-jimeng',
|
||||
color: '#F97316',
|
||||
url: '/mobile/jimeng',
|
||||
path: '/jimeng',
|
||||
},
|
||||
{
|
||||
key: '3d',
|
||||
@@ -173,6 +177,7 @@ const features = ref([
|
||||
icon: 'icon-3d',
|
||||
color: '#8B5CF6',
|
||||
url: '/mobile/3d',
|
||||
path: '/3d',
|
||||
},
|
||||
{ key: 'agent', name: '智能体', icon: 'icon-app', color: '#3B82F6', url: '/mobile/apps' },
|
||||
{
|
||||
@@ -181,6 +186,7 @@ const features = ref([
|
||||
icon: 'icon-image-list',
|
||||
color: '#EC4899',
|
||||
url: '/mobile/imgWall',
|
||||
path: '/images-wall',
|
||||
},
|
||||
])
|
||||
|
||||
@@ -225,6 +231,14 @@ onMounted(() => {
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
getMenus()
|
||||
.then((menus) => {
|
||||
features.value = features.value.filter((feature) => menus[feature.path]?.enabled)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('获取菜单失败:' + e.message)
|
||||
})
|
||||
|
||||
fetchApps()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="member-page">
|
||||
<div class="member-page" v-if="menus['/member']?.enabled">
|
||||
<div class="member-content" v-loading="loading" :element-loading-text="loadingText">
|
||||
<!-- 产品套餐 -->
|
||||
<div class="products-section">
|
||||
@@ -103,14 +103,18 @@
|
||||
</div>
|
||||
</van-dialog>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RedeemVerify from '@/components/RedeemVerify.vue'
|
||||
import UserOrder from '@/components/UserOrder.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
import QRCode from 'qrcode'
|
||||
import { showFailToast, showLoadingToast, showSuccessToast } from 'vant'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
@@ -123,6 +127,7 @@ const loading = ref(true)
|
||||
const loadingText = ref('加载中...')
|
||||
const vipInfoText = ref('')
|
||||
const userOrderKey = ref(0)
|
||||
const menus = ref({})
|
||||
|
||||
// 弹窗控制
|
||||
const showRedeemVerifyDialog = ref(false)
|
||||
@@ -171,6 +176,14 @@ onMounted(() => {
|
||||
.catch((e) => {
|
||||
console.error('获取系统配置失败:', e.message)
|
||||
})
|
||||
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
})
|
||||
|
||||
// 支付处理
|
||||
|
||||
@@ -1,203 +1,193 @@
|
||||
<template>
|
||||
<div class="mobile-sd">
|
||||
<van-form>
|
||||
<van-cell-group class="px-3 pt-3 pb-4">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="selectedModel"
|
||||
is-link
|
||||
label="生图模型"
|
||||
placeholder="选择生图模型"
|
||||
@click="showModelPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showModelPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="models"
|
||||
@cancel="showModelPicker = false"
|
||||
@confirm="modelConfirm"
|
||||
<div v-if="menus['/dalle']?.enabled">
|
||||
<van-form>
|
||||
<van-cell-group class="px-3 pt-3 pb-4">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="selectedModel"
|
||||
is-link
|
||||
label="生图模型"
|
||||
placeholder="选择生图模型"
|
||||
@click="showModelPicker = true"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
<div>
|
||||
<van-field
|
||||
v-model="quality"
|
||||
is-link
|
||||
label="图片质量"
|
||||
placeholder="选择图片质量"
|
||||
@click="showQualityPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showQualityPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="qualities"
|
||||
@cancel="showQualityPicker = false"
|
||||
@confirm="qualityConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<van-field
|
||||
v-model="size"
|
||||
is-link
|
||||
label="图片尺寸"
|
||||
placeholder="选择图片尺寸"
|
||||
@click="showSizePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showSizePicker" position="bottom" teleport="#app">
|
||||
<van-picker :columns="sizes" @cancel="showSizePicker = false" @confirm="sizeConfirm" />
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<van-field
|
||||
v-model="style"
|
||||
is-link
|
||||
label="图片样式"
|
||||
placeholder="选择图片样式"
|
||||
@click="showStylePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showStylePicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="styles"
|
||||
@cancel="showStylePicker = false"
|
||||
@confirm="styleConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="2000"
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
|
||||
<div class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm">
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ dallPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
<van-popup v-model:show="showModelPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="models"
|
||||
@cancel="showModelPicker = false"
|
||||
@confirm="modelConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<van-field
|
||||
v-model="aspectRatio"
|
||||
is-link
|
||||
label="图片比例"
|
||||
placeholder="选择图片比例"
|
||||
@click="showSizePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showSizePicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="radioAspects"
|
||||
@cancel="showSizePicker = false"
|
||||
@confirm="sizeConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="2000"
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
|
||||
<div class="mt-3 mb-3 px-3">
|
||||
<label class="text-gray-700 font-semibold">参考图(可选)</label>
|
||||
<div class="py-2">
|
||||
<ImageUpload v-model="params.image" :max-count="5" :multiple="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
<div
|
||||
class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm"
|
||||
>
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ dallPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle />
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="removeImage($event, item)"
|
||||
circle
|
||||
/>
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-dall"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-dall"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-dall"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-dall"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
@@ -215,52 +205,64 @@ import {
|
||||
} from 'vant'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const isLogin = ref(false)
|
||||
const menus = ref({})
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
|
||||
const qualities = [
|
||||
{ text: '标准', value: 'standard' },
|
||||
{ text: '高清', value: 'hd' },
|
||||
]
|
||||
const fluxSizes = [
|
||||
{ text: '1024x1024', value: '1024x1024' },
|
||||
{ text: '1024x768', value: '1024x768' },
|
||||
{ text: '768x1024', value: '768x1024' },
|
||||
{ text: '1280x960', value: '1280x960' },
|
||||
{ text: '960x1280', value: '960x1280' },
|
||||
{ text: '1366x768', value: '1366x768' },
|
||||
{ text: '768x1366', value: '768x1366' },
|
||||
]
|
||||
const dalleSizes = [
|
||||
{ text: '1024x1024', value: '1024x1024' },
|
||||
{ text: '1792x1024', value: '1792x1024' },
|
||||
{ text: '1024x1792', value: '1024x1792' },
|
||||
]
|
||||
|
||||
let sizes = dalleSizes
|
||||
const styles = [
|
||||
{ text: '生动', value: 'vivid' },
|
||||
{ text: '自然', value: 'natural' },
|
||||
]
|
||||
// 为了兼容 size 和 aspect_ratio 参数,label 对应的是 aspect_ratio 的值,size 是预估值
|
||||
const radioAspects = ref([
|
||||
{ value: '1:1', size: '1024x1024', text: '1:1(正方形,头像)', icon: 'icon-aspect_1_1' },
|
||||
{
|
||||
value: '2:3',
|
||||
size: '512x768',
|
||||
text: '2:3(社交媒体,自拍)',
|
||||
icon: 'icon-aspect_2_3',
|
||||
},
|
||||
{
|
||||
value: '4:3',
|
||||
size: '1024x768',
|
||||
text: '4:3(文章配图,插画)',
|
||||
icon: 'icon-aspect_4_3',
|
||||
},
|
||||
{
|
||||
value: '9:16',
|
||||
size: '768x1366',
|
||||
text: '9:16(手机壁纸,人像)',
|
||||
icon: 'icon-aspect_9_16',
|
||||
},
|
||||
{
|
||||
value: '16:9',
|
||||
size: '1366x768',
|
||||
text: '16:9(桌面壁纸,风景)',
|
||||
icon: 'icon-aspect_16_9',
|
||||
},
|
||||
{ value: '3:2', size: '768x512', text: '3:2(1248x832)', icon: 'icon-aspect_3_2' },
|
||||
{ value: '3:4', size: '960x1280', text: '3:4(864x1152)', icon: 'icon-aspect_3_4' },
|
||||
{ value: '4:5', size: '960x1280', text: '4:5(896x1120)', icon: 'icon-aspect_4_5' },
|
||||
{
|
||||
value: '5:4',
|
||||
size: '1280x960',
|
||||
label: '5:4(1120x896)',
|
||||
icon: 'icon-aspect_5_4',
|
||||
},
|
||||
{ value: '21:9', size: '1344x576', text: '21:9(1536x658)', icon: 'icon-aspect_16_9' },
|
||||
])
|
||||
const params = ref({
|
||||
quality: qualities[0].value,
|
||||
size: sizes[0].value,
|
||||
style: styles[0].value,
|
||||
aspect_ratio: '1:1',
|
||||
size: '1024x1024',
|
||||
prompt: '',
|
||||
})
|
||||
const quality = ref(qualities[0].text)
|
||||
const size = ref(sizes[0].text)
|
||||
const style = ref(styles[0].text)
|
||||
const aspectRatio = ref(radioAspects.value[0].text)
|
||||
|
||||
const showQualityPicker = ref(false)
|
||||
const showStylePicker = ref(false)
|
||||
const showSizePicker = ref(false)
|
||||
const showModelPicker = ref(false)
|
||||
|
||||
@@ -313,6 +315,14 @@ onMounted(() => {
|
||||
.catch((e) => {
|
||||
showMessageError('获取模型列表失败:' + e.message)
|
||||
})
|
||||
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -408,10 +418,6 @@ const generate = () => {
|
||||
promptRef.value.focus()
|
||||
return showToast('请输入绘画提示词!')
|
||||
}
|
||||
|
||||
if (!params.value.seed) {
|
||||
params.value.seed = -1
|
||||
}
|
||||
params.value.session_id = getSessionId()
|
||||
httpPost('/api/dall/image', params.value)
|
||||
.then(() => {
|
||||
@@ -492,21 +498,10 @@ const imageView = (item) => {
|
||||
showImagePreview([item['img_url']])
|
||||
}
|
||||
|
||||
const qualityConfirm = (item) => {
|
||||
params.value.quality = item.selectedOptions[0].value
|
||||
quality.value = item.selectedOptions[0].text
|
||||
showQualityPicker.value = false
|
||||
}
|
||||
|
||||
const styleConfirm = (item) => {
|
||||
params.value.style = item.selectedOptions[0].value
|
||||
style.value = item.selectedOptions[0].text
|
||||
showStylePicker.value = false
|
||||
}
|
||||
|
||||
const sizeConfirm = (item) => {
|
||||
params.value.size = item.selectedOptions[0].value
|
||||
size.value = item.selectedOptions[0].text
|
||||
params.value.aspect_ratio = item.selectedOptions[0].value
|
||||
params.value.size = item.selectedOptions[0].size
|
||||
aspectRatio.value = item.selectedOptions[0].text
|
||||
showSizePicker.value = false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,338 +1,353 @@
|
||||
<template>
|
||||
<div class="mobile-mj">
|
||||
<van-form>
|
||||
<div class="text-line">图片比例</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="4" v-for="item in rates" :key="item.value">
|
||||
<div
|
||||
:class="item.value === params.rate ? 'rate active' : 'rate'"
|
||||
@click="changeRate(item)"
|
||||
>
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">模型选择</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="8" v-for="item in models" :key="item.value">
|
||||
<div
|
||||
:class="item.value === params.model ? 'model active' : 'model'"
|
||||
@click="changeModel(item)"
|
||||
>
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">
|
||||
<van-text-ellipsis :content="item.text" />
|
||||
</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="创意度">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.chaos"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.chaos)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="风格化">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.stylize"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.stylize)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="原始模式">
|
||||
<template #input>
|
||||
<van-switch v-model="params.raw" />
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-tabs v-model:active="activeName" @change="tabChange" animated>
|
||||
<van-tab title="文生图" name="txt2img">
|
||||
<div class="text-line">
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="图生图" name="img2img">
|
||||
<div class="text-line">
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="2000"
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="垫图权重">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.iw"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@update:model-value="showToast('当前值:' + params.iw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="tip-text">
|
||||
提示:只有于 niji6 和 v6 模型支持一致性功能,如果选择其他模型此功能将会生成失败。
|
||||
</div>
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.cref"
|
||||
center
|
||||
clearable
|
||||
label="角色一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
<div v-if="menus['/mj']?.enabled">
|
||||
<van-form>
|
||||
<div class="text-line">图片比例</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="4" v-for="item in rates" :key="item.value">
|
||||
<div
|
||||
:class="item.value === params.rate ? 'rate active' : 'rate'"
|
||||
@click="changeRate(item)"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('cref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.sref"
|
||||
center
|
||||
clearable
|
||||
label="风格一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">模型选择</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="8" v-for="item in models" :key="item.value">
|
||||
<div
|
||||
:class="item.value === params.model ? 'model active' : 'model'"
|
||||
@click="changeModel(item)"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('sref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="一致性权重">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.cw"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.cw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="融图" name="blend">
|
||||
<div class="tip-text">
|
||||
请上传两张以上的图片,最多不超过五张,超过五张图片请使用图生图功能。
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="换脸" name="swapFace">
|
||||
<div class="tip-text">请上传两张有脸部的图片,用左边图片的脸替换右边图片的脸。</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
</div>
|
||||
|
||||
<div class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm">
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ mjPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">
|
||||
<van-text-ellipsis :content="item.text" />
|
||||
</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="创意度">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.chaos"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.chaos)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="风格化">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.stylize"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.stylize)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<div class="text-line">
|
||||
<van-field label="原始模式">
|
||||
<template #input>
|
||||
<van-switch v-model="params.raw" />
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id" class="min-h-[270px]">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage(item)" size="small">删除</van-button>
|
||||
<div class="text-line">
|
||||
<van-tabs v-model:active="activeName" @change="tabChange" animated>
|
||||
<van-tab title="文生图" name="txt2img">
|
||||
<div class="text-line">
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['thumb_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
<template v-slot:error>
|
||||
<span style="margin-bottom: 20px">正在下载图片</span>
|
||||
<van-loading type="circular" color="#1989fa" size="40" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="opt" v-if="item['can_opt']">
|
||||
<van-grid :gutter="3" :column-num="4">
|
||||
<van-grid-item><a @click="upscale(1, item)" class="opt-btn">U1</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(2, item)" class="opt-btn">U2</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(3, item)" class="opt-btn">U3</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(4, item)" class="opt-btn">U4</a></van-grid-item>
|
||||
<van-grid-item
|
||||
><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item
|
||||
>
|
||||
</van-grid>
|
||||
</van-tab>
|
||||
<van-tab title="图生图" name="img2img">
|
||||
<div class="text-line">
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="2000"
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(item)" circle />
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage(item, false)"
|
||||
circle
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="垫图权重">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.iw"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@update:model-value="showToast('当前值:' + params.iw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="tip-text">
|
||||
提示:只有于 niji6 和 v6 模型支持一致性功能,如果选择其他模型此功能将会生成失败。
|
||||
</div>
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.cref"
|
||||
center
|
||||
clearable
|
||||
label="角色一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage(item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('cref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.sref"
|
||||
center
|
||||
clearable
|
||||
label="风格一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('sref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="一致性权重">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.cw"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.cw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="融图" name="blend">
|
||||
<div class="tip-text">
|
||||
请上传两张以上的图片,最多不超过五张,超过五张图片请使用图生图功能。
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="换脸" name="swapFace">
|
||||
<div class="tip-text">请上传两张有脸部的图片,用左边图片的脸替换右边图片的脸。</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
</div>
|
||||
|
||||
<div class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm">
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ mjPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">
|
||||
复制
|
||||
</button>
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id" class="min-h-[270px]">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage(item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['thumb_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
<template v-slot:error>
|
||||
<span style="margin-bottom: 20px">正在下载图片</span>
|
||||
<van-loading type="circular" color="#1989fa" size="40" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="opt" v-if="item['can_opt']">
|
||||
<van-grid :gutter="3" :column-num="4">
|
||||
<van-grid-item
|
||||
><a @click="upscale(1, item)" class="opt-btn">U1</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="upscale(2, item)" class="opt-btn">U2</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="upscale(3, item)" class="opt-btn">U3</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="upscale(4, item)" class="opt-btn">U4</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item
|
||||
>
|
||||
</van-grid>
|
||||
</div>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(item)" circle />
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage(item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage(item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
@@ -340,6 +355,7 @@ import { showLoginDialog } from '@/utils/libs'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import Compressor from 'compressorjs'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
import {
|
||||
showConfirmDialog,
|
||||
showDialog,
|
||||
@@ -351,6 +367,7 @@ import {
|
||||
} from 'vant'
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const menus = ref({})
|
||||
|
||||
const activeColspan = ref([''])
|
||||
|
||||
@@ -437,6 +454,14 @@ onMounted(() => {
|
||||
.catch(() => {
|
||||
// router.push('/login')
|
||||
})
|
||||
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -1,265 +1,278 @@
|
||||
<template>
|
||||
<div class="mobile-sd">
|
||||
<van-form>
|
||||
<van-cell-group class="px-3 pt-3 pb-4">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="params.sampler"
|
||||
is-link
|
||||
readonly
|
||||
label="采样方法"
|
||||
placeholder="选择采样方法"
|
||||
@click="showSamplerPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showSamplerPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="samplers"
|
||||
@cancel="showSamplerPicker = false"
|
||||
@confirm="samplerConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field label="图片尺寸">
|
||||
<template #input>
|
||||
<van-row gutter="20">
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.width" size="small" placeholder="宽" />
|
||||
</van-col>
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.height" size="small" placeholder="高" />
|
||||
</van-col>
|
||||
</van-row>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field v-model.number="params.steps" label="迭代步数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="showInfo('值越大则代表细节越多,同时也意味着出图速度越慢,一般推荐20-30')"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.cfg_scale" label="引导系数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.seed" label="随机因子" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field label="高清修复">
|
||||
<template #input>
|
||||
<van-switch v-model="params.hd_fix" />
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<div v-if="params.hd_fix">
|
||||
<div v-if="menus['/sd']?.enabled">
|
||||
<van-form>
|
||||
<van-cell-group class="px-3 pt-3 pb-4">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="params.hd_scale_alg"
|
||||
v-model="params.sampler"
|
||||
is-link
|
||||
readonly
|
||||
label="放大算法"
|
||||
placeholder="选择放大算法"
|
||||
@click="showUpscalePicker = true"
|
||||
label="采样方法"
|
||||
placeholder="选择采样方法"
|
||||
@click="showSamplerPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showUpscalePicker" position="bottom" teleport="#app">
|
||||
<van-popup v-model:show="showSamplerPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="upscaleAlgArr"
|
||||
@cancel="showUpscalePicker = false"
|
||||
@confirm="upscaleConfirm"
|
||||
:columns="samplers"
|
||||
@cancel="showSamplerPicker = false"
|
||||
@confirm="samplerConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field v-model.number="params.hd_scale" label="放大倍数" />
|
||||
<van-field v-model.number="params.hd_steps" label="迭代步数" />
|
||||
|
||||
<van-field label="重绘幅度">
|
||||
<van-field label="图片尺寸">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.hd_redraw_rate"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
@update:model-value="showToast('当前值:' + params.hd_redraw_rate)"
|
||||
/>
|
||||
<van-row gutter="20">
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.width" size="small" placeholder="宽" />
|
||||
</van-col>
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.height" size="small" placeholder="高" />
|
||||
</van-col>
|
||||
</van-row>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field v-model.number="params.steps" label="迭代步数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')"
|
||||
@click="showInfo('值越大则代表细节越多,同时也意味着出图速度越慢,一般推荐20-30')"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
|
||||
<div class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm">
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ sdPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
<van-field v-model.number="params.cfg_scale" label="引导系数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.seed" label="随机因子" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field label="高清修复">
|
||||
<template #input>
|
||||
<van-switch v-model="params.hd_fix" />
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<div v-if="params.hd_fix">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="params.hd_scale_alg"
|
||||
is-link
|
||||
readonly
|
||||
label="放大算法"
|
||||
placeholder="选择放大算法"
|
||||
@click="showUpscalePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showUpscalePicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="upscaleAlgArr"
|
||||
@cancel="showUpscalePicker = false"
|
||||
@confirm="upscaleConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field v-model.number="params.hd_scale" label="放大倍数" />
|
||||
<van-field v-model.number="params.hd_steps" label="迭代步数" />
|
||||
|
||||
<van-field label="重绘幅度">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.hd_redraw_rate"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
@update:model-value="showToast('当前值:' + params.hd_redraw_rate)"
|
||||
/>
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
|
||||
<div
|
||||
class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm"
|
||||
>
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ sdPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle />
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="removeImage($event, item)"
|
||||
circle
|
||||
/>
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-sd"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-sd"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-sd"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-sd"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { showLoginDialog } from '@/utils/libs'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
import {
|
||||
showConfirmDialog,
|
||||
showDialog,
|
||||
@@ -276,6 +289,7 @@ const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const isLogin = ref(false)
|
||||
const activeColspan = ref([''])
|
||||
const menus = ref({})
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
@@ -352,6 +366,14 @@ onMounted(() => {
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取系统配置失败:' + e.message })
|
||||
})
|
||||
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -25,6 +25,23 @@ export default defineConfig(({ mode }) => {
|
||||
base: env.VITE_BASE_URL,
|
||||
build: {
|
||||
outDir: 'dist', // 构建输出目录
|
||||
target: 'esnext', // 允许使用顶层 await 等最新特性
|
||||
},
|
||||
|
||||
// 开发环境与依赖预构建同样对齐到 esnext,避免 esbuild 报错
|
||||
esbuild: {
|
||||
target: 'esnext',
|
||||
supported: {
|
||||
'top-level-await': true,
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
target: 'esnext',
|
||||
supported: {
|
||||
'top-level-await': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user