项目重构: 整理目录结构, 更新前后端代码, 添加测试和数据库迁移
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
<svg width="194" height="41" viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="black"/>
|
||||
<g clip-path="url(#clip0_1233_5144)">
|
||||
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0_linear_1233_5144)"/>
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM93.1291 38.6957C91.2536 38.6957 89.6317 38.2973 88.2633 37.5004C86.903 36.6955 85.8526 35.5766 85.112 34.1439C84.3715 32.7031 84.0012 31.0328 84.0012 29.1332C84.0012 27.2175 84.3715 25.5432 85.112 24.1105C85.8526 22.6697 86.903 21.5508 88.2633 20.7539C89.6317 19.949 91.2536 19.5466 93.1291 19.5466C95.0046 19.5466 96.6225 19.949 97.9828 20.7539C99.3511 21.5508 100.406 22.6697 101.146 24.1105C101.887 25.5432 102.257 27.2175 102.257 29.1332C102.257 31.0328 101.887 32.7031 101.146 34.1439C100.406 35.5766 99.3511 36.6955 97.9828 37.5004C96.6225 38.2973 95.0046 38.6957 93.1291 38.6957ZM93.1532 34.7113C94.0065 34.7113 94.7188 34.4699 95.2903 33.9869C95.8618 33.4959 96.2924 32.8278 96.5822 31.9826C96.88 31.1375 97.0289 30.1756 97.0289 29.097C97.0289 28.0184 96.88 27.0565 96.5822 26.2113C96.2924 25.3662 95.8618 24.6981 95.2903 24.2071C94.7188 23.7161 94.0065 23.4706 93.1532 23.4706C92.292 23.4706 91.5675 23.7161 90.9799 24.2071C90.4004 24.6981 89.9617 25.3662 89.6639 26.2113C89.3741 27.0565 89.2292 28.0184 89.2292 29.097C89.2292 30.1756 89.3741 31.1375 89.6639 31.9826C89.9617 32.8278 90.4004 33.4959 90.9799 33.9869C91.5675 34.4699 92.292 34.7113 93.1532 34.7113ZM110.745 27.6119V38.3335H105.601V19.788H110.503V23.0601H110.721C111.131 21.9815 111.819 21.1282 112.785 20.5004C113.751 19.8645 114.922 19.5466 116.299 19.5466C117.587 19.5466 118.71 19.8283 119.667 20.3917C120.625 20.9552 121.37 21.7601 121.901 22.8065C122.432 23.8449 122.698 25.0844 122.698 26.5253V38.3335H117.555V27.4429C117.563 26.3079 117.273 25.4225 116.685 24.7866C116.098 24.1427 115.289 23.8207 114.258 23.8207C113.566 23.8207 112.954 23.9696 112.423 24.2674C111.9 24.5653 111.489 24.9999 111.192 25.5714C110.902 26.1349 110.753 26.815 110.745 27.6119ZM135.131 38.6957C133.256 38.6957 131.634 38.2973 130.265 37.5004C128.905 36.6955 127.855 35.5766 127.114 34.1439C126.373 32.7031 126.003 31.0328 126.003 29.1332C126.003 27.2175 126.373 25.5432 127.114 24.1105C127.855 22.6697 128.905 21.5508 130.265 20.7539C131.634 19.949 133.256 19.5466 135.131 19.5466C137.007 19.5466 138.624 19.949 139.985 20.7539C141.353 21.5508 142.408 22.6697 143.148 24.1105C143.889 25.5432 144.259 27.2175 144.259 29.1332C144.259 31.0328 143.889 32.7031 143.148 34.1439C142.408 35.5766 141.353 36.6955 139.985 37.5004C138.624 38.2973 137.007 38.6957 135.131 38.6957ZM135.155 34.7113C136.008 34.7113 136.721 34.4699 137.292 33.9869C137.864 33.4959 138.294 32.8278 138.584 31.9826C138.882 31.1375 139.031 30.1756 139.031 29.097C139.031 28.0184 138.882 27.0565 138.584 26.2113C138.294 25.3662 137.864 24.6981 137.292 24.2071C136.721 23.7161 136.008 23.4706 135.155 23.4706C134.294 23.4706 133.569 23.7161 132.982 24.2071C132.402 24.6981 131.964 25.3662 131.666 26.2113C131.376 27.0565 131.231 28.0184 131.231 29.097C131.231 30.1756 131.376 31.1375 131.666 31.9826C131.964 32.8278 132.402 33.4959 132.982 33.9869C133.569 34.4699 134.294 34.7113 135.155 34.7113ZM150.797 38.3335L145.75 19.788H150.954L153.827 32.2483H153.996L156.991 19.788H162.098L165.141 32.1758H165.298L168.123 19.788H173.315L168.28 38.3335H162.835L159.647 26.6701H159.418L156.23 38.3335H150.797Z" fill="#1D2129"/>
|
||||
<g clip-path="url(#clip0_445_10776)">
|
||||
<path d="M5.74048 1.6455C2.43981 1.6455 0.0100346 1.57286 0.000244138 1.6455C0.000244144 1.71889 2.11281 5.37372 4.7063 9.58593C7.28518 13.8128 12.3555 22.0903 15.9543 27.9609L20.8684 35.9883C21.8795 37.6395 23.6773 38.6455 25.6135 38.6455L26.4299 38.6455C26.4299 38.6455 31.0566 38.7202 31.9963 37.2227C32.2253 36.8286 32.3459 36.3806 32.3459 35.9248L32.3459 21.8994L31.1799 21.8994L31.1799 26.5967C31.1799 31.1021 31.1657 31.3081 30.8889 31.5869C30.7286 31.7483 30.4661 31.8799 30.3059 31.8799C29.9854 31.8798 29.7815 31.5857 27.1448 27.2568C26.256 25.8038 24.0844 22.237 22.2922 19.3311C20.5147 16.4251 17.3532 11.2594 15.2698 7.85449L11.467 1.64551L5.74048 1.6455ZM31.6614 2.99609C31.4428 4.17012 30.5249 6.06336 29.6653 7.10547C28.456 8.60245 26.4303 9.83514 24.5071 10.2314C23.4726 10.4516 23.1959 10.5986 23.8079 10.5986C24.0266 10.5987 24.58 10.701 25.0315 10.833C28.2661 11.7576 31.0493 14.7672 31.6467 17.9814L31.8362 18.9795L32.011 18.1133C32.5647 15.3247 34.4441 12.8149 36.9065 11.5674C38.0138 10.995 39.2814 10.5986 39.9807 10.5986C40.6216 10.5986 40.4029 10.4956 39.2083 10.2461C37.4599 9.87917 36.1048 9.11605 34.6624 7.66309C33.2637 6.26884 32.5495 5.02109 32.0833 3.17187L31.8215 2.11523L31.6614 2.99609Z" fill="url(#paint0_linear_445_10776)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1233_5144" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient id="paint0_linear_445_10776" x1="23.4864" y1="5.39407" x2="32.9094" y2="11.1241" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||
<stop offset="1" stop-color="#0F9CFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1233_5144">
|
||||
<clipPath id="clip0_445_10776">
|
||||
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -1,15 +1,15 @@
|
||||
<svg width="194" height="41" viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="white"/>
|
||||
<g clip-path="url(#clip0_1233_5144)">
|
||||
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0_linear_1233_5144)"/>
|
||||
<svg width="101" height="21" viewBox="0 0 101 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28.4343 7.58893L31.5459 17.3698H31.6653L34.7831 7.58893H37.8003L33.3625 20.4624H29.855L25.4108 7.58893H28.4343ZM39.2995 20.4624V10.8073H41.9773V20.4624H39.2995ZM40.6447 9.56269C40.2466 9.56269 39.905 9.43069 39.6201 9.16668C39.3393 8.89848 39.1989 8.5779 39.1989 8.20494C39.1989 7.83617 39.3393 7.51978 39.6201 7.25577C39.905 6.98758 40.2466 6.85348 40.6447 6.85348C41.0428 6.85348 41.3822 6.98758 41.663 7.25577C41.9479 7.51978 42.0904 7.83617 42.0904 8.20494C42.0904 8.5779 41.9479 8.89848 41.663 9.16668C41.3822 9.43069 41.0428 9.56269 40.6447 9.56269ZM48.4847 20.651C47.5083 20.651 46.6639 20.4435 45.9515 20.0287C45.2433 19.6096 44.6964 19.0271 44.3109 18.2812C43.9254 17.5311 43.7326 16.6615 43.7326 15.6726C43.7326 14.6752 43.9254 13.8036 44.3109 13.0576C44.6964 12.3075 45.2433 11.725 45.9515 11.3102C46.6639 10.8911 47.5083 10.6816 48.4847 10.6816C49.4611 10.6816 50.3034 10.8911 51.0116 11.3102C51.724 11.725 52.273 12.3075 52.6585 13.0576C53.0441 13.8036 53.2368 14.6752 53.2368 15.6726C53.2368 16.6615 53.0441 17.5311 52.6585 18.2812C52.273 19.0271 51.724 19.6096 51.0116 20.0287C50.3034 20.4435 49.4611 20.651 48.4847 20.651ZM48.4973 18.5766C48.9415 18.5766 49.3124 18.4509 49.6099 18.1995C49.9074 17.9439 50.1316 17.596 50.2825 17.156C50.4375 16.716 50.5151 16.2152 50.5151 15.6537C50.5151 15.0922 50.4375 14.5914 50.2825 14.1514C50.1316 13.7114 49.9074 13.3636 49.6099 13.1079C49.3124 12.8523 48.9415 12.7245 48.4973 12.7245C48.0489 12.7245 47.6717 12.8523 47.3658 13.1079C47.0641 13.3636 46.8357 13.7114 46.6807 14.1514C46.5298 14.5914 46.4544 15.0922 46.4544 15.6537C46.4544 16.2152 46.5298 16.716 46.6807 17.156C46.8357 17.596 47.0641 17.9439 47.3658 18.1995C47.6717 18.4509 48.0489 18.5766 48.4973 18.5766ZM57.6558 14.8805V20.4624H54.978V10.8073H57.5301V12.5108H57.6432C57.857 11.9492 58.2153 11.505 58.7181 11.1782C59.221 10.8471 59.8307 10.6816 60.5473 10.6816C61.2178 10.6816 61.8024 10.8282 62.3011 11.1216C62.7998 11.4149 63.1874 11.834 63.464 12.3788C63.7405 12.9193 63.8788 13.5647 63.8788 14.3148V20.4624H61.2011V14.7925C61.2052 14.2017 61.0544 13.7407 60.7485 13.4096C60.4426 13.0744 60.0214 12.9068 59.485 12.9068C59.1246 12.9068 58.8061 12.9843 58.5296 13.1394C58.2572 13.2944 58.0434 13.5207 57.8884 13.8182C57.7375 14.1116 57.66 14.4657 57.6558 14.8805ZM70.3517 20.651C69.3753 20.651 68.5309 20.4435 67.8185 20.0287C67.1103 19.6096 66.5634 19.0271 66.1779 18.2812C65.7924 17.5311 65.5996 16.6615 65.5996 15.6726C65.5996 14.6752 65.7924 13.8036 66.1779 13.0576C66.5634 12.3075 67.1103 11.725 67.8185 11.3102C68.5309 10.8911 69.3753 10.6816 70.3517 10.6816C71.3281 10.6816 72.1704 10.8911 72.8786 11.3102C73.591 11.725 74.14 12.3075 74.5255 13.0576C74.9111 13.8036 75.1038 14.6752 75.1038 15.6726C75.1038 16.6615 74.9111 17.5311 74.5255 18.2812C74.14 19.0271 73.591 19.6096 72.8786 20.0287C72.1704 20.4435 71.3281 20.651 70.3517 20.651ZM70.3643 18.5766C70.8085 18.5766 71.1794 18.4509 71.4769 18.1995C71.7744 17.9439 71.9986 17.596 72.1495 17.156C72.3045 16.716 72.3821 16.2152 72.3821 15.6537C72.3821 15.0922 72.3045 14.5914 72.1495 14.1514C71.9986 13.7114 71.7744 13.3636 71.4769 13.1079C71.1794 12.8523 70.8085 12.7245 70.3643 12.7245C69.9159 12.7245 69.5387 12.8523 69.2328 13.1079C68.9311 13.3636 68.7027 13.7114 68.5477 14.1514C68.3968 14.5914 68.3214 15.0922 68.3214 15.6537C68.3214 16.2152 68.3968 16.716 68.5477 17.156C68.7027 17.596 68.9311 17.9439 69.2328 18.1995C69.5387 18.4509 69.9159 18.5766 70.3643 18.5766ZM78.5076 20.4624L75.8801 10.8073H78.5894L80.0854 17.2943H80.1734L81.7323 10.8073H84.3912L85.9753 17.2566H86.057L87.5279 10.8073H90.2308L87.6096 20.4624H84.7747L83.1152 14.3902H82.9958L81.3363 20.4624H78.5076Z" fill="white"/>
|
||||
<g clip-path="url(#clip0_287_18768)">
|
||||
<path d="M2.98853 0.856444C1.27414 0.856441 0.0111275 0.81906 0.000244139 0.856443C0.000244136 0.894647 1.10026 2.79734 2.45044 4.99023C3.79308 7.19083 6.43228 11.5012 8.30591 14.5576L10.8645 18.7363C11.3909 19.5959 12.3272 20.1201 13.3352 20.1201L13.76 20.1201C13.76 20.1201 16.1679 20.1583 16.6575 19.3789C16.7767 19.1737 16.8401 18.9404 16.8401 18.7031L16.8401 11.4014L16.2327 11.4014L16.2327 13.8467C16.2327 16.1919 16.2254 16.2992 16.0813 16.4443C15.9979 16.5284 15.861 16.5977 15.7776 16.5977C15.6109 16.5975 15.5042 16.4431 14.1321 14.1904C13.6694 13.434 12.5387 11.5774 11.6057 10.0645C10.6803 8.55154 9.03419 5.86157 7.94946 4.08887L5.96997 0.856444L2.98853 0.856444ZM16.4836 1.55957C16.3699 2.17078 15.8921 3.15667 15.4446 3.69922C14.815 4.47857 13.7603 5.12082 12.759 5.32715C12.2205 5.44176 12.0762 5.51758 12.3948 5.51758C12.5085 5.51759 12.7965 5.57095 13.0315 5.63965C14.7155 6.12103 16.1648 7.68795 16.4758 9.36133L16.5745 9.88086L16.6653 9.42969C16.9536 7.97804 17.9323 6.67195 19.2141 6.02246C19.7906 5.72446 20.4506 5.51758 20.8147 5.51758C21.1484 5.51757 21.0343 5.46388 20.4124 5.33398C19.5023 5.14293 18.797 4.74562 18.0461 3.98926C17.318 3.26338 16.9461 2.61411 16.7034 1.65137L16.5667 1.10156L16.4836 1.55957Z" fill="url(#paint0_linear_287_18768)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1233_5144" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient id="paint0_linear_287_18768" x1="12.2274" y1="2.80849" x2="17.1333" y2="5.79162" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||
<stop offset="1" stop-color="#0F9CFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1233_5144">
|
||||
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||
<clipPath id="clip0_287_18768">
|
||||
<rect width="20.9983" height="20.9983" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -57,6 +57,40 @@ export const imageToVideoApi = {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 通过图片URL创建图生视频任务(用于"做同款"功能)
|
||||
* @param {Object} params - 任务参数
|
||||
* @param {string} params.imageUrl - 图片URL
|
||||
* @param {string} params.prompt - 描述文字
|
||||
* @param {string} params.aspectRatio - 视频比例
|
||||
* @param {number} params.duration - 视频时长
|
||||
* @param {boolean} params.hdMode - 是否高清模式
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
createTaskByUrl(params) {
|
||||
if (!params) {
|
||||
throw new Error('参数不能为空')
|
||||
}
|
||||
if (!params.imageUrl) {
|
||||
throw new Error('图片URL不能为空')
|
||||
}
|
||||
if (!params.prompt || params.prompt.trim() === '') {
|
||||
throw new Error('描述文字不能为空')
|
||||
}
|
||||
|
||||
return request({
|
||||
url: '/image-to-video/create-by-url',
|
||||
method: 'POST',
|
||||
data: {
|
||||
imageUrl: params.imageUrl,
|
||||
prompt: params.prompt.trim(),
|
||||
aspectRatio: params.aspectRatio || '16:9',
|
||||
duration: params.duration || 5,
|
||||
hdMode: params.hdMode || false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户任务列表
|
||||
* @param {number} page - 页码
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- Logo -->
|
||||
<div class="navbar-brand">
|
||||
<router-link to="/" class="brand-link">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" class="brand-logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" class="brand-logo" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -140,14 +140,39 @@ const qrCodeUrl = ref('') // 二维码URL
|
||||
const showQrCode = ref(false) // 是否显示二维码
|
||||
let paymentPollingTimer = null
|
||||
let isPaymentStarted = false // 防止重复调用
|
||||
let lastPlanType = '' // 记录上一次的套餐类型
|
||||
|
||||
// 从orderId中提取套餐类型(如 SUB_standard_xxx -> standard)
|
||||
const getPlanTypeFromOrderId = (orderId) => {
|
||||
if (!orderId) return ''
|
||||
const parts = orderId.split('_')
|
||||
return parts.length >= 2 ? parts[1] : ''
|
||||
}
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
// 当模态框打开时,自动开始支付流程(只调用一次)
|
||||
if (newVal && !isPaymentStarted) {
|
||||
isPaymentStarted = true
|
||||
handlePay()
|
||||
// 当模态框打开时,检查是否需要创建新订单
|
||||
if (newVal) {
|
||||
const currentPlanType = getPlanTypeFromOrderId(props.orderId)
|
||||
|
||||
// 如果套餐类型变化了,重置paymentId
|
||||
if (currentPlanType !== lastPlanType) {
|
||||
console.log('套餐变化,重置 paymentId,旧套餐:', lastPlanType, '新套餐:', currentPlanType)
|
||||
currentPaymentId.value = null
|
||||
lastPlanType = currentPlanType
|
||||
} else {
|
||||
console.log('同一套餐,复用 paymentId:', currentPaymentId.value)
|
||||
}
|
||||
|
||||
qrCodeUrl.value = ''
|
||||
showQrCode.value = false
|
||||
|
||||
// 只有选择支付宝时才自动生成二维码,PayPal需要用户点击按钮
|
||||
if (!isPaymentStarted && selectedMethod.value === 'alipay') {
|
||||
isPaymentStarted = true
|
||||
handlePay()
|
||||
}
|
||||
}
|
||||
// 关闭时重置标志
|
||||
if (!newVal) {
|
||||
@@ -158,7 +183,7 @@ watch(() => props.modelValue, (newVal) => {
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
// 如果模态框关闭,停止轮询并重置状态
|
||||
// 如果模态框关闭,停止轮询并重置二维码状态(但保留paymentId和lastPlanType用于同套餐复用)
|
||||
if (!newVal) {
|
||||
stopPaymentPolling()
|
||||
qrCodeUrl.value = ''
|
||||
@@ -167,13 +192,62 @@ watch(visible, (newVal) => {
|
||||
})
|
||||
|
||||
// 选择支付方式
|
||||
const selectMethod = (method) => {
|
||||
// 如果切换了支付方式,需要重置 paymentId,因为支付方式不同需要创建新的支付记录
|
||||
const selectMethod = async (method) => {
|
||||
if (selectedMethod.value !== method) {
|
||||
currentPaymentId.value = null
|
||||
console.log('切换支付方式,重置 paymentId')
|
||||
console.log('切换支付方式:', selectedMethod.value, '->', method, '复用 paymentId:', currentPaymentId.value)
|
||||
selectedMethod.value = method
|
||||
|
||||
// 重置二维码显示
|
||||
qrCodeUrl.value = ''
|
||||
showQrCode.value = false
|
||||
|
||||
// 如果已有支付记录,更新支付方式和描述(复用paymentId)
|
||||
if (currentPaymentId.value) {
|
||||
try {
|
||||
const newDescription = `${props.title} - ${method === 'paypal' ? 'PayPal' : '支付宝'}支付`
|
||||
const newMethod = method === 'paypal' ? 'PAYPAL' : 'ALIPAY'
|
||||
|
||||
console.log('=== 开始更新支付方式 ===')
|
||||
console.log('paymentId:', currentPaymentId.value)
|
||||
console.log('newMethod:', newMethod)
|
||||
console.log('newDescription:', newDescription)
|
||||
|
||||
const response = await fetch(`/api/payments/${currentPaymentId.value}/method`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: newMethod,
|
||||
description: newDescription
|
||||
})
|
||||
})
|
||||
|
||||
console.log('API响应状态:', response.status)
|
||||
const responseData = await response.json()
|
||||
console.log('API响应数据:', responseData)
|
||||
|
||||
if (response.ok && responseData.success) {
|
||||
console.log('✅ 支付方式更新成功,复用paymentId:', currentPaymentId.value)
|
||||
// 如果切换到支付宝,生成新二维码
|
||||
if (method === 'alipay') {
|
||||
await handleAlipayPayment()
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 支付方式更新失败:', responseData.message || response.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 更新支付方式异常:', error)
|
||||
}
|
||||
} else {
|
||||
// 没有支付记录,如果是支付宝则自动创建
|
||||
if (method === 'alipay') {
|
||||
isPaymentStarted = true
|
||||
await handlePay()
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedMethod.value = method
|
||||
}
|
||||
|
||||
// 处理支付
|
||||
@@ -183,10 +257,9 @@ const handlePay = async () => {
|
||||
|
||||
// 如果还没有创建支付记录,先创建
|
||||
if (!currentPaymentId.value) {
|
||||
// 生成唯一的订单ID,加上时间戳避免重复
|
||||
const uniqueOrderId = `${props.orderId}_${Date.now()}`
|
||||
// 直接使用传入的orderId,不再添加时间戳(Subscription.vue已经处理了)
|
||||
const paymentData = {
|
||||
orderId: uniqueOrderId,
|
||||
orderId: props.orderId,
|
||||
amount: props.amount.toString(),
|
||||
method: selectedMethod.value === 'paypal' ? 'PAYPAL' : 'ALIPAY',
|
||||
description: `${props.title} - ${selectedMethod.value === 'paypal' ? 'PayPal' : '支付宝'}支付`
|
||||
@@ -204,6 +277,27 @@ const handlePay = async () => {
|
||||
} else {
|
||||
throw new Error(createResponse.data?.message || '创建支付记录失败')
|
||||
}
|
||||
} else {
|
||||
// 已有支付记录,确保支付方式和描述与当前选择一致
|
||||
try {
|
||||
const newDescription = `${props.title} - ${selectedMethod.value === 'paypal' ? 'PayPal' : '支付宝'}支付`
|
||||
const newMethod = selectedMethod.value === 'paypal' ? 'PAYPAL' : 'ALIPAY'
|
||||
|
||||
await fetch(`/api/payments/${currentPaymentId.value}/method`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: newMethod,
|
||||
description: newDescription
|
||||
})
|
||||
})
|
||||
console.log('支付方式已同步:', newMethod)
|
||||
} catch (e) {
|
||||
console.warn('同步支付方式失败,继续支付:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据选择的支付方式处理
|
||||
@@ -332,6 +426,9 @@ const startPaymentPolling = (paymentId) => {
|
||||
stopPaymentPolling()
|
||||
ElMessage.success('支付成功!')
|
||||
emit('pay-success', payment)
|
||||
// 重置状态,下次购买生成新订单
|
||||
currentPaymentId.value = null
|
||||
lastPlanType = ''
|
||||
// 延迟关闭模态框
|
||||
setTimeout(() => {
|
||||
visible.value = false
|
||||
@@ -412,6 +509,9 @@ const manualCheckPayment = async () => {
|
||||
stopPaymentPolling()
|
||||
ElMessage.success('支付成功!')
|
||||
emit('pay-success', payment)
|
||||
// 重置状态,下次购买生成新订单
|
||||
currentPaymentId.value = null
|
||||
lastPlanType = ''
|
||||
setTimeout(() => {
|
||||
visible.value = false
|
||||
}, 1500)
|
||||
|
||||
@@ -507,6 +507,7 @@ export default {
|
||||
permanent: 'Permanent',
|
||||
remainingPoints: 'Remaining Points',
|
||||
plans: 'Plans',
|
||||
pointsPerYear: 'points/year',
|
||||
currentPackage: 'Current Plan',
|
||||
firstPurchaseDiscount: 'First Purchase Discount up to 15% off',
|
||||
bestValue: 'Best Value',
|
||||
@@ -518,6 +519,23 @@ export default {
|
||||
commercialUse: 'Commercial Use',
|
||||
noWatermark: 'No Watermark',
|
||||
earlyAccess: 'Early Access to New Features',
|
||||
|
||||
// Points and items
|
||||
points: 'points',
|
||||
items: 'items',
|
||||
pointsValidOneYear: 'Points valid for one year',
|
||||
textToVideo30Points: 'Text to Video: 30 points/item',
|
||||
imageToVideo30Points: 'Image to Video: 30 points/item',
|
||||
storyboardImage30Points: 'Storyboard Image: 30 points/time',
|
||||
storyboardVideo30Points: 'Storyboard Video: 30 points/item',
|
||||
maxTextToVideo: 'Max Text to Video {count} items',
|
||||
maxImageToVideo: 'Max Image to Video {count} items',
|
||||
maxStoryboardImage: 'Max Storyboard Image {count} times',
|
||||
maxStoryboardVideo: 'Max Storyboard Video {count} items',
|
||||
textToVideoItems: 'Text to Video {count} items',
|
||||
imageToVideoItems: 'or Image to Video {count} items',
|
||||
storyboardImageTimes: 'or Storyboard Image {count} times',
|
||||
storyboardVideoItems: 'or Storyboard Video {count} items',
|
||||
|
||||
// Points history related
|
||||
pointsUsageHistory: 'Points Usage History',
|
||||
@@ -591,7 +609,11 @@ export default {
|
||||
onlineUsers: 'Online Users',
|
||||
systemUptime: 'System Uptime',
|
||||
todayVisitors: 'Today Visitors',
|
||||
loading: 'Loading...'
|
||||
loading: 'Loading...',
|
||||
memberManagement: 'Member Management',
|
||||
orderManagement: 'Order Management',
|
||||
taskRecord: 'Task Records',
|
||||
errorStats: 'Error Statistics'
|
||||
},
|
||||
|
||||
admin: {
|
||||
@@ -668,7 +690,12 @@ export default {
|
||||
batchDeleteSuccess: 'Batch delete successful',
|
||||
batchDeleteFailed: 'Batch delete failed',
|
||||
loadOrdersFailed: 'Failed to load orders',
|
||||
apiDataFormatError: 'API data format error'
|
||||
apiDataFormatError: 'API data format error',
|
||||
productOrder: 'Product Order',
|
||||
serviceOrder: 'Service Order',
|
||||
subscriptionOrder: 'Subscription Order',
|
||||
digitalProduct: 'Digital Product',
|
||||
physicalProduct: 'Physical Product'
|
||||
},
|
||||
|
||||
tasks: {
|
||||
@@ -685,10 +712,27 @@ export default {
|
||||
processing: 'Processing',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
pending: 'Pending',
|
||||
textToVideo: 'Text to Video',
|
||||
imageToVideo: 'Image to Video',
|
||||
storyboardVideo: 'Storyboard Video',
|
||||
taskDetail: 'Task Detail'
|
||||
taskDetail: 'Task Detail',
|
||||
unknown: 'Unknown',
|
||||
pointsUnit: ' points',
|
||||
basicInfo: 'Basic Info',
|
||||
timeInfo: 'Time Info',
|
||||
progressInfo: 'Progress Info',
|
||||
progress: 'Progress',
|
||||
result: 'Result',
|
||||
resultLink: 'Result Link',
|
||||
viewResult: 'View Result',
|
||||
errorInfo: 'Error Info',
|
||||
close: 'Close',
|
||||
updateTime: 'Update Time',
|
||||
completeTime: 'Complete Time',
|
||||
taskType: 'Task Type',
|
||||
resourcesConsumed: 'Resources Consumed',
|
||||
defaultPoints: '0 points'
|
||||
},
|
||||
|
||||
members: {
|
||||
@@ -706,22 +750,73 @@ export default {
|
||||
usernamePlaceholder: 'Enter username',
|
||||
levelPlaceholder: 'Select level',
|
||||
pointsPlaceholder: 'Enter points',
|
||||
expiryPlaceholder: 'Select expiry date'
|
||||
expiryPlaceholder: 'Select expiry date',
|
||||
memberLevel: 'Membership Level',
|
||||
freeMember: 'Free Member',
|
||||
standardMember: 'Standard Member',
|
||||
professionalMember: 'Professional Member',
|
||||
userStatus: 'User Status',
|
||||
activeUsers: 'Active Users',
|
||||
bannedUsers: 'Banned Users',
|
||||
allUsers: 'All Users',
|
||||
role: 'Role',
|
||||
status: 'Status',
|
||||
setAdmin: 'Set as Admin',
|
||||
revokeAdmin: 'Revoke Admin',
|
||||
ban: 'Ban',
|
||||
unban: 'Unban',
|
||||
active: 'Active',
|
||||
banned: 'Banned',
|
||||
superAdmin: 'Super Admin',
|
||||
admin: 'Admin',
|
||||
normalUser: 'User',
|
||||
userRole: 'User Role',
|
||||
selectRole: 'Select role',
|
||||
confirmRoleChange: 'Are you sure to {action} user {username}?',
|
||||
confirmBanAction: 'Are you sure to {action} user {username}?',
|
||||
confirmAction: 'Confirm {action}',
|
||||
actionSuccess: '{action} successful',
|
||||
actionFailed: '{action} failed'
|
||||
},
|
||||
|
||||
apiManagement: {
|
||||
title: 'API Management',
|
||||
apiKey: 'API Key',
|
||||
apiKeyPlaceholder: 'Enter API key',
|
||||
apiBaseUrl: 'API Base URL',
|
||||
apiBaseUrlPlaceholder: 'Enter API base URL, e.g. https://ai.comfly.chat',
|
||||
apiBaseUrlHint: 'Currently using',
|
||||
tokenExpiration: 'Token Expiration',
|
||||
tokenPlaceholder: 'Enter hours (1-720)',
|
||||
hours: 'hours',
|
||||
days: 'days',
|
||||
rangeHint: 'Range: 1-720 hours (1 hour - 30 days)',
|
||||
atLeastOneRequired: 'Please enter at least one configuration',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveFailed: 'Save failed'
|
||||
},
|
||||
|
||||
errorStats: {
|
||||
title: 'Error Statistics',
|
||||
userAvatar: 'User Avatar',
|
||||
totalErrors: 'Total Errors',
|
||||
todayErrors: 'Today Errors',
|
||||
weekErrors: 'Week Errors',
|
||||
errorTypeDistribution: 'Error Type Distribution',
|
||||
last7Days: 'Last 7 Days',
|
||||
last30Days: 'Last 30 Days',
|
||||
last90Days: 'Last 90 Days',
|
||||
times: 'times',
|
||||
noErrorData: 'No error data',
|
||||
recentErrors: 'Recent Errors',
|
||||
refresh: 'Refresh',
|
||||
time: 'Time',
|
||||
errorType: 'Error Type',
|
||||
user: 'User',
|
||||
taskId: 'Task ID',
|
||||
errorMessage: 'Error Message'
|
||||
},
|
||||
|
||||
systemSettings: {
|
||||
title: 'System Settings',
|
||||
membership: 'Membership Pricing',
|
||||
@@ -801,8 +896,6 @@ export default {
|
||||
unknown: 'Unknown',
|
||||
aiModel: 'AI Model Settings',
|
||||
promptOptimization: 'Prompt Optimization Settings',
|
||||
promptOptimizationApiUrl: 'API Endpoint',
|
||||
promptOptimizationApiUrlTip: 'Enter the API endpoint for prompt optimization, e.g., https://api.openai.com',
|
||||
promptOptimizationModel: 'Model Name',
|
||||
promptOptimizationModelTip: 'Enter the model name for prompt optimization, e.g., gpt-4o, gemini-pro',
|
||||
storyboardSystemPrompt: 'Storyboard System Prompt',
|
||||
|
||||
@@ -506,6 +506,7 @@ export default {
|
||||
unlimited: '无限',
|
||||
limited: '有限',
|
||||
pointsPerMonth: '积分/年',
|
||||
pointsPerYear: '积分/年',
|
||||
videoQuality: '视频质量',
|
||||
support: '客服支持',
|
||||
priorityQueue: '优先队列',
|
||||
@@ -532,6 +533,23 @@ export default {
|
||||
commercialUse: '支持商用',
|
||||
noWatermark: '下载去水印',
|
||||
earlyAccess: '新功能优先体验',
|
||||
|
||||
// 积分和条数
|
||||
points: '积分',
|
||||
items: '条',
|
||||
pointsValidOneYear: '积分一年有效',
|
||||
textToVideo30Points: '文生视频:30积分/条',
|
||||
imageToVideo30Points: '图生视频:30积分/条',
|
||||
storyboardImage30Points: '分镜图生成:30积分/次',
|
||||
storyboardVideo30Points: '分镜视频生成:30积分/条',
|
||||
maxTextToVideo: '最多文生视频 {count}条',
|
||||
maxImageToVideo: '最多图生视频 {count}条',
|
||||
maxStoryboardImage: '最多分镜图 {count}次',
|
||||
maxStoryboardVideo: '最多分镜视频 {count}条',
|
||||
textToVideoItems: '文生视频 {count}条',
|
||||
imageToVideoItems: '或图生视频 {count}条',
|
||||
storyboardImageTimes: '或分镜图 {count}次',
|
||||
storyboardVideoItems: '或分镜视频 {count}条',
|
||||
|
||||
// 积分历史相关
|
||||
pointsUsageHistory: '积分使用情况',
|
||||
@@ -604,7 +622,11 @@ export default {
|
||||
systemSettings: '系统设置',
|
||||
onlineUsers: '当前在线用户',
|
||||
systemUptime: '系统运行时间',
|
||||
todayVisitors: '今日访客'
|
||||
todayVisitors: '今日访客',
|
||||
memberManagement: '会员管理',
|
||||
orderManagement: '订单管理',
|
||||
taskRecord: '生成任务记录',
|
||||
errorStats: '错误统计'
|
||||
},
|
||||
|
||||
admin: {
|
||||
@@ -669,7 +691,12 @@ export default {
|
||||
orderDetail: '订单详情',
|
||||
basicInfo: '基本信息',
|
||||
orderType: '订单类型',
|
||||
paymentInfo: '支付信息'
|
||||
paymentInfo: '支付信息',
|
||||
productOrder: '商品订单',
|
||||
serviceOrder: '服务订单',
|
||||
subscriptionOrder: '订阅订单',
|
||||
digitalProduct: '数字商品',
|
||||
physicalProduct: '实体商品'
|
||||
},
|
||||
|
||||
tasks: {
|
||||
@@ -686,10 +713,27 @@ export default {
|
||||
processing: '处理中',
|
||||
failed: '失败',
|
||||
cancelled: '已取消',
|
||||
pending: '待处理',
|
||||
textToVideo: '文生视频',
|
||||
imageToVideo: '图生视频',
|
||||
storyboardVideo: '分镜视频',
|
||||
taskDetail: '任务详情'
|
||||
taskDetail: '任务详情',
|
||||
unknown: '未知',
|
||||
pointsUnit: '积分',
|
||||
basicInfo: '基本信息',
|
||||
timeInfo: '时间信息',
|
||||
progressInfo: '进度信息',
|
||||
progress: '进度',
|
||||
result: '结果',
|
||||
resultLink: '结果链接',
|
||||
viewResult: '查看结果',
|
||||
errorInfo: '错误信息',
|
||||
close: '关闭',
|
||||
updateTime: '更新时间',
|
||||
completeTime: '完成时间',
|
||||
taskType: '任务类型',
|
||||
resourcesConsumed: '消耗资源',
|
||||
defaultPoints: '0积分'
|
||||
},
|
||||
|
||||
members: {
|
||||
@@ -707,22 +751,73 @@ export default {
|
||||
usernamePlaceholder: '请输入用户名',
|
||||
levelPlaceholder: '请选择会员等级',
|
||||
pointsPlaceholder: '请输入资源点',
|
||||
expiryPlaceholder: '请选择到期时间'
|
||||
expiryPlaceholder: '请选择到期时间',
|
||||
memberLevel: '会员等级',
|
||||
freeMember: '免费会员',
|
||||
standardMember: '标准会员',
|
||||
professionalMember: '专业会员',
|
||||
userStatus: '用户状态',
|
||||
activeUsers: '活跃用户',
|
||||
bannedUsers: '封禁用户',
|
||||
allUsers: '全部用户',
|
||||
role: '角色',
|
||||
status: '状态',
|
||||
setAdmin: '设为管理员',
|
||||
revokeAdmin: '取消管理员',
|
||||
ban: '封禁',
|
||||
unban: '解封',
|
||||
active: '活跃',
|
||||
banned: '封禁',
|
||||
superAdmin: '超级管理员',
|
||||
admin: '管理员',
|
||||
normalUser: '普通用户',
|
||||
userRole: '用户角色',
|
||||
selectRole: '请选择用户角色',
|
||||
confirmRoleChange: '确定要将用户 {username} {action}吗?',
|
||||
confirmBanAction: '确定要{action}用户 {username} 吗?',
|
||||
confirmAction: '确认{action}',
|
||||
actionSuccess: '{action}成功',
|
||||
actionFailed: '{action}失败'
|
||||
},
|
||||
|
||||
apiManagement: {
|
||||
title: 'API管理',
|
||||
apiKey: 'API密钥',
|
||||
apiKeyPlaceholder: '请输入API密钥',
|
||||
apiBaseUrl: 'API基础URL',
|
||||
apiBaseUrlPlaceholder: '请输入API基础URL,如 https://ai.comfly.chat',
|
||||
apiBaseUrlHint: '当前使用',
|
||||
tokenExpiration: 'Token过期时间',
|
||||
tokenPlaceholder: '请输入小时数(1-720)',
|
||||
hours: '小时',
|
||||
days: '天',
|
||||
rangeHint: '范围:1-720小时(1小时-30天)',
|
||||
atLeastOneRequired: '请至少输入一个配置项',
|
||||
saveSuccess: '保存成功',
|
||||
saveFailed: '保存失败'
|
||||
},
|
||||
|
||||
errorStats: {
|
||||
title: '错误类型统计',
|
||||
userAvatar: '用户头像',
|
||||
totalErrors: '总错误数',
|
||||
todayErrors: '今日错误',
|
||||
weekErrors: '本周错误',
|
||||
errorTypeDistribution: '错误类型分布',
|
||||
last7Days: '最近7天',
|
||||
last30Days: '最近30天',
|
||||
last90Days: '最近90天',
|
||||
times: '次',
|
||||
noErrorData: '暂无错误数据',
|
||||
recentErrors: '最近错误',
|
||||
refresh: '刷新',
|
||||
time: '时间',
|
||||
errorType: '错误类型',
|
||||
user: '用户',
|
||||
taskId: '任务ID',
|
||||
errorMessage: '错误信息'
|
||||
},
|
||||
|
||||
systemSettings: {
|
||||
title: '系统设置',
|
||||
membership: '会员收费标准',
|
||||
@@ -779,8 +874,6 @@ export default {
|
||||
confirmCleanup: '确认清理',
|
||||
aiModel: 'AI模型设置',
|
||||
promptOptimization: '提示词优化设置',
|
||||
promptOptimizationApiUrl: 'API端点',
|
||||
promptOptimizationApiUrlTip: '输入用于优化提示词的API端点地址,如 https://api.openai.com',
|
||||
promptOptimizationModel: '模型名称',
|
||||
promptOptimizationModelTip: '输入用于优化提示词的模型名称,如 gpt-4o、gemini-pro 等',
|
||||
storyboardSystemPrompt: '分镜图系统引导词',
|
||||
|
||||
@@ -49,11 +49,10 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<!-- 顶部操作栏 -->
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input">
|
||||
<div class="page-title">
|
||||
<h2>{{ $t('nav.dashboard') }}</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<LanguageSwitcher />
|
||||
@@ -665,36 +664,11 @@ onUnmounted(() => {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: #9ca3af;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
padding: 10px 12px 10px 40px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #9ca3af;
|
||||
.page-title h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
|
||||
@@ -56,12 +56,11 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<!-- 顶部操作栏 -->
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" v-model="searchText" />
|
||||
</div>
|
||||
<div class="page-title">
|
||||
<h2>{{ $t('nav.orderManagement') }}</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<LanguageSwitcher />
|
||||
<el-dropdown v-if="isAdminMode" @command="handleUserCommand">
|
||||
@@ -229,11 +228,11 @@
|
||||
style="width: 120px;"
|
||||
@change="handleStatusChange"
|
||||
>
|
||||
<el-option label="待支付" value="PENDING" />
|
||||
<el-option label="已支付" value="PAID" />
|
||||
<el-option label="已完成" value="COMPLETED" />
|
||||
<el-option label="已取消" value="CANCELLED" />
|
||||
<el-option label="已退款" value="REFUNDED" />
|
||||
<el-option :label="$t('orders.pending')" value="PENDING" />
|
||||
<el-option :label="$t('orders.paid')" value="PAID" />
|
||||
<el-option :label="$t('orders.completed')" value="COMPLETED" />
|
||||
<el-option :label="$t('orders.cancelled')" value="CANCELLED" />
|
||||
<el-option :label="$t('orders.refunded')" value="REFUNDED" />
|
||||
</el-select>
|
||||
<span v-else class="status-tag" :class="getStatusClass(currentOrderDetail.status)">
|
||||
{{ getStatusText(currentOrderDetail.status) }}
|
||||
@@ -410,15 +409,15 @@ const getStatusClass = (status) => {
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'PENDING': '待支付',
|
||||
'CONFIRMED': '已确认',
|
||||
'PAID': '已支付',
|
||||
'PROCESSING': '处理中',
|
||||
'SHIPPED': '已发货',
|
||||
'DELIVERED': '已送达',
|
||||
'COMPLETED': '已完成',
|
||||
'CANCELLED': '已取消',
|
||||
'REFUNDED': '已退款'
|
||||
'PENDING': t('orders.pending'),
|
||||
'CONFIRMED': t('orders.confirmed'),
|
||||
'PAID': t('orders.paid'),
|
||||
'PROCESSING': t('orders.processing'),
|
||||
'SHIPPED': t('orders.shipped'),
|
||||
'DELIVERED': t('orders.delivered'),
|
||||
'COMPLETED': t('orders.completed'),
|
||||
'CANCELLED': t('orders.cancelled'),
|
||||
'REFUNDED': t('orders.refunded')
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
@@ -426,11 +425,11 @@ const getStatusText = (status) => {
|
||||
// 获取订单类型文本
|
||||
const getOrderTypeText = (orderType) => {
|
||||
const typeMap = {
|
||||
'PRODUCT': '商品订单',
|
||||
'SERVICE': '服务订单',
|
||||
'SUBSCRIPTION': '订阅订单',
|
||||
'DIGITAL': '数字商品',
|
||||
'PHYSICAL': '实体商品'
|
||||
'PRODUCT': t('orders.productOrder'),
|
||||
'SERVICE': t('orders.serviceOrder'),
|
||||
'SUBSCRIPTION': t('orders.subscriptionOrder'),
|
||||
'DIGITAL': t('orders.digitalProduct'),
|
||||
'PHYSICAL': t('orders.physicalProduct')
|
||||
}
|
||||
return typeMap[orderType] || orderType
|
||||
}
|
||||
@@ -840,38 +839,11 @@ const fetchSystemStats = async () => {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: #9ca3af;
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
padding: 10px 12px 10px 40px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #9ca3af;
|
||||
.page-title h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
|
||||
@@ -47,11 +47,10 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<!-- 顶部操作栏 -->
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" />
|
||||
<div class="page-title">
|
||||
<h2>{{ $t('nav.apiManagement') }}</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<LanguageSwitcher />
|
||||
@@ -88,10 +87,20 @@
|
||||
style="width: 100%; max-width: 600px;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('apiManagement.apiBaseUrl')">
|
||||
<el-input
|
||||
v-model="apiForm.apiBaseUrl"
|
||||
:placeholder="$t('apiManagement.apiBaseUrlPlaceholder')"
|
||||
style="width: 100%; max-width: 600px;"
|
||||
/>
|
||||
<div style="margin-top: 8px; color: #6b7280; font-size: 12px;">
|
||||
{{ $t('apiManagement.apiBaseUrlHint') }}: {{ currentApiBaseUrl || $t('common.notConfigured') || '未配置' }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('apiManagement.tokenExpiration')">
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%; max-width: 600px;">
|
||||
<el-input
|
||||
v-model.number="apiForm.jwtExpirationHours"
|
||||
v-model.number="apiForm.tokenExpireHours"
|
||||
type="number"
|
||||
:placeholder="$t('apiManagement.tokenPlaceholder')"
|
||||
style="flex: 1;"
|
||||
@@ -99,9 +108,6 @@
|
||||
:max="720"
|
||||
/>
|
||||
<span style="color: #6b7280; font-size: 14px;">{{ $t('apiManagement.hours') }}</span>
|
||||
<span style="color: #9ca3af; font-size: 12px;" v-if="apiForm.jwtExpirationHours">
|
||||
({{ formatJwtExpiration(apiForm.jwtExpirationHours) }})
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #6b7280; font-size: 12px;">
|
||||
{{ $t('apiManagement.rangeHint') }}
|
||||
@@ -147,8 +153,10 @@ const onlineUsers = ref('0/500')
|
||||
const systemUptime = ref(t('common.loading'))
|
||||
const apiForm = reactive({
|
||||
apiKey: '',
|
||||
jwtExpirationHours: 24 // 默认24小时
|
||||
apiBaseUrl: '',
|
||||
tokenExpireHours: null // 从数据库加载
|
||||
})
|
||||
const currentApiBaseUrl = ref('')
|
||||
|
||||
// 导航功能
|
||||
const goToDashboard = () => {
|
||||
@@ -200,20 +208,21 @@ const formatJwtExpiration = (hours) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载当前API密钥和JWT配置(仅显示部分)
|
||||
// 加载当前API配置
|
||||
const loadApiKey = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.get('/api-key')
|
||||
if (response.data?.maskedKey) {
|
||||
// 不显示掩码后的密钥,只用于验证
|
||||
console.log('当前API密钥已配置')
|
||||
}
|
||||
// 加载JWT过期时间(转换为小时)
|
||||
if (response.data?.jwtExpiration) {
|
||||
apiForm.jwtExpirationHours = Math.round(response.data.jwtExpiration / 3600000)
|
||||
} else if (response.data?.jwtExpirationHours) {
|
||||
apiForm.jwtExpirationHours = Math.round(response.data.jwtExpirationHours)
|
||||
// 加载当前API基础URL
|
||||
if (response.data?.apiBaseUrl) {
|
||||
currentApiBaseUrl.value = response.data.apiBaseUrl
|
||||
}
|
||||
// 加载当前Token过期时间
|
||||
if (response.data?.tokenExpireHours) {
|
||||
apiForm.tokenExpireHours = response.data.tokenExpireHours
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
@@ -222,21 +231,16 @@ const loadApiKey = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存API密钥和JWT配置
|
||||
// 保存API配置到数据库
|
||||
const saveApiKey = async () => {
|
||||
// 检查是否有任何输入
|
||||
const hasApiKey = apiForm.apiKey && apiForm.apiKey.trim() !== ''
|
||||
const hasJwtExpiration = apiForm.jwtExpirationHours != null && apiForm.jwtExpirationHours > 0
|
||||
const hasApiBaseUrl = apiForm.apiBaseUrl && apiForm.apiBaseUrl.trim() !== ''
|
||||
const hasTokenExpire = apiForm.tokenExpireHours && apiForm.tokenExpireHours >= 1 && apiForm.tokenExpireHours <= 720
|
||||
|
||||
// 验证输入:至少需要提供一个配置项
|
||||
if (!hasApiKey && !hasJwtExpiration) {
|
||||
ElMessage.warning(t('apiManagement.atLeastOneRequired') || '请至少输入API密钥或设置Token过期时间')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证JWT过期时间范围
|
||||
if (hasJwtExpiration && (apiForm.jwtExpirationHours < 1 || apiForm.jwtExpirationHours > 720)) {
|
||||
ElMessage.warning(t('apiManagement.tokenRangeError') || 'Token过期时间必须在1-720小时之间')
|
||||
if (!hasApiKey && !hasApiBaseUrl && !hasTokenExpire) {
|
||||
ElMessage.warning(t('apiManagement.atLeastOneRequired') || '请至少输入一个配置项')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -249,17 +253,25 @@ const saveApiKey = async () => {
|
||||
requestData.apiKey = apiForm.apiKey.trim()
|
||||
}
|
||||
|
||||
// 如果提供了JWT过期时间,转换为毫秒并添加到请求中
|
||||
if (hasJwtExpiration) {
|
||||
requestData.jwtExpiration = apiForm.jwtExpirationHours * 3600000 // 转换为毫秒
|
||||
// 如果提供了API基础URL,添加到请求中
|
||||
if (hasApiBaseUrl) {
|
||||
requestData.apiBaseUrl = apiForm.apiBaseUrl.trim()
|
||||
}
|
||||
|
||||
// 如果提供了Token过期时间,添加到请求中
|
||||
if (hasTokenExpire) {
|
||||
requestData.tokenExpireHours = apiForm.tokenExpireHours
|
||||
}
|
||||
|
||||
const response = await api.put('/api-key', requestData)
|
||||
|
||||
if (response.data?.success) {
|
||||
ElMessage.success(response.data.message || '配置保存成功,请重启应用以使配置生效')
|
||||
// 清空API密钥输入框(保留JWT过期时间)
|
||||
ElMessage.success(response.data.message || '配置已保存到数据库,立即生效')
|
||||
// 清空输入框
|
||||
apiForm.apiKey = ''
|
||||
apiForm.apiBaseUrl = ''
|
||||
// 重新加载当前配置
|
||||
loadApiKey()
|
||||
} else {
|
||||
ElMessage.error(response.data?.error || '保存失败')
|
||||
}
|
||||
@@ -274,7 +286,7 @@ const saveApiKey = async () => {
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
apiForm.apiKey = ''
|
||||
// 重新加载JWT过期时间
|
||||
apiForm.apiBaseUrl = ''
|
||||
loadApiKey()
|
||||
}
|
||||
|
||||
@@ -419,38 +431,11 @@ const fetchSystemStats = async () => {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: #9ca3af;
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
padding: 10px 12px 10px 40px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #9ca3af;
|
||||
.page-title h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="login-page">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 修改密码卡片 -->
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
<div class="nav-item active">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>错误统计</span>
|
||||
<span>{{ $t('nav.errorStats') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
@@ -52,13 +52,13 @@
|
||||
<!-- 顶部搜索栏 -->
|
||||
<header class="top-header">
|
||||
<div class="page-title">
|
||||
<h2>错误类型统计</h2>
|
||||
<h2>{{ $t('errorStats.title') }}</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<LanguageSwitcher />
|
||||
<el-dropdown @command="handleUserCommand">
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" :alt="$t('errorStats.userAvatar')" />
|
||||
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
@@ -81,7 +81,7 @@
|
||||
<el-icon><Warning /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">总错误数</div>
|
||||
<div class="stat-title">{{ $t('errorStats.totalErrors') }}</div>
|
||||
<div class="stat-number">{{ statistics.totalErrors || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@
|
||||
<el-icon><Clock /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">今日错误</div>
|
||||
<div class="stat-title">{{ $t('errorStats.todayErrors') }}</div>
|
||||
<div class="stat-number">{{ statistics.todayErrors || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,7 +101,7 @@
|
||||
<el-icon><Calendar /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">本周错误</div>
|
||||
<div class="stat-title">{{ $t('errorStats.weekErrors') }}</div>
|
||||
<div class="stat-number">{{ statistics.weekErrors || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,11 +112,11 @@
|
||||
<div class="charts-section">
|
||||
<div class="chart-card full-width">
|
||||
<div class="chart-header">
|
||||
<h3>错误类型分布</h3>
|
||||
<h3>{{ $t('errorStats.errorTypeDistribution') }}</h3>
|
||||
<el-select v-model="selectedDays" @change="loadStatistics" class="days-select">
|
||||
<el-option label="最近7天" :value="7"></el-option>
|
||||
<el-option label="最近30天" :value="30"></el-option>
|
||||
<el-option label="最近90天" :value="90"></el-option>
|
||||
<el-option :label="$t('errorStats.last7Days')" :value="7"></el-option>
|
||||
<el-option :label="$t('errorStats.last30Days')" :value="30"></el-option>
|
||||
<el-option :label="$t('errorStats.last90Days')" :value="90"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="error-type-list">
|
||||
@@ -127,7 +127,7 @@
|
||||
>
|
||||
<div class="type-info">
|
||||
<span class="type-name">{{ item.description || item.type }}</span>
|
||||
<span class="type-count">{{ item.count }} 次</span>
|
||||
<span class="type-count">{{ item.count }} {{ $t('errorStats.times') }}</span>
|
||||
</div>
|
||||
<div class="type-bar">
|
||||
<div
|
||||
@@ -137,7 +137,7 @@
|
||||
</div>
|
||||
<div class="type-percentage">{{ getPercentage(item.count) }}%</div>
|
||||
</div>
|
||||
<el-empty v-if="errorTypeStats.length === 0" description="暂无错误数据" />
|
||||
<el-empty v-if="errorTypeStats.length === 0" :description="$t('errorStats.noErrorData')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,23 +145,23 @@
|
||||
<!-- 最近错误列表 -->
|
||||
<div class="recent-errors-section">
|
||||
<div class="section-header">
|
||||
<h3>最近错误</h3>
|
||||
<el-button type="primary" size="small" @click="loadRecentErrors">刷新</el-button>
|
||||
<h3>{{ $t('errorStats.recentErrors') }}</h3>
|
||||
<el-button type="primary" size="small" @click="loadRecentErrors">{{ $t('errorStats.refresh') }}</el-button>
|
||||
</div>
|
||||
<el-table :data="recentErrors" v-loading="tableLoading" stripe>
|
||||
<el-table-column prop="createdAt" label="时间" width="180">
|
||||
<el-table-column prop="createdAt" :label="$t('errorStats.time')" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="errorType" label="错误类型" width="150">
|
||||
<el-table-column prop="errorType" :label="$t('errorStats.errorType')" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTagType(row.errorType)">{{ row.errorType }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户" width="120" />
|
||||
<el-table-column prop="taskId" label="任务ID" width="200" />
|
||||
<el-table-column prop="errorMessage" label="错误信息" show-overflow-tooltip />
|
||||
<el-table-column prop="username" :label="$t('errorStats.user')" width="120" />
|
||||
<el-table-column prop="taskId" :label="$t('errorStats.taskId')" width="200" />
|
||||
<el-table-column prop="errorMessage" :label="$t('errorStats.errorMessage')" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<div class="nav-item" @click="goToErrorStats">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>错误统计</span>
|
||||
<span>{{ $t('nav.errorStats') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
@@ -47,11 +47,10 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<!-- 顶部操作栏 -->
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" v-model="searchText" />
|
||||
<div class="page-title">
|
||||
<h2>{{ $t('nav.taskRecord') }}</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<LanguageSwitcher />
|
||||
@@ -62,7 +61,15 @@
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="exitAdmin">
|
||||
<el-dropdown-item command="apiManagement">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ $t('nav.apiManagement') }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="taskRecord">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ $t('nav.tasks') }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="exitAdmin">
|
||||
{{ $t('admin.exitAdmin') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -123,9 +130,9 @@
|
||||
@change="toggleTaskSelection(task)" />
|
||||
</td>
|
||||
<td>{{ task.taskId || task.id }}</td>
|
||||
<td>{{ task.username || '未知' }}</td>
|
||||
<td>{{ task.type || '未知' }}</td>
|
||||
<td>{{ task.resources || '0积分' }}</td>
|
||||
<td>{{ task.username || $t('tasks.unknown') }}</td>
|
||||
<td>{{ getTaskTypeText(task.type) }}</td>
|
||||
<td>{{ task.resources || '0' + $t('tasks.pointsUnit') }}</td>
|
||||
<td>
|
||||
<span class="status-tag" :class="getStatusClass(task.status)">
|
||||
{{ getStatusText(task.status) }}
|
||||
@@ -177,25 +184,25 @@
|
||||
>
|
||||
<div class="task-detail-content" v-if="currentTask">
|
||||
<div class="detail-section">
|
||||
<h4>基本信息</h4>
|
||||
<h4>{{ $t('tasks.basicInfo') }}</h4>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">任务ID:</span>
|
||||
<span class="detail-label">{{ $t('tasks.taskId') }}:</span>
|
||||
<span class="detail-value">{{ currentTask.taskId || currentTask.id }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">用户名:</span>
|
||||
<span class="detail-value">{{ currentTask.username || '未知' }}</span>
|
||||
<span class="detail-label">{{ $t('tasks.username') }}:</span>
|
||||
<span class="detail-value">{{ currentTask.username || $t('tasks.unknown') }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">任务类型:</span>
|
||||
<span class="detail-value">{{ currentTask.type || currentTask.taskType || '未知' }}</span>
|
||||
<span class="detail-label">{{ $t('tasks.taskType') }}:</span>
|
||||
<span class="detail-value">{{ getTaskTypeText(currentTask.type || currentTask.taskType) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">消耗资源:</span>
|
||||
<span class="detail-value">{{ currentTask.resources || '0积分' }}</span>
|
||||
<span class="detail-label">{{ $t('tasks.resourcesConsumed') }}:</span>
|
||||
<span class="detail-value">{{ currentTask.resources || $t('tasks.defaultPoints') }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">状态:</span>
|
||||
<span class="detail-label">{{ $t('tasks.status') }}:</span>
|
||||
<span class="status-tag" :class="getStatusClass(currentTask.status)">
|
||||
{{ getStatusText(currentTask.status) }}
|
||||
</span>
|
||||
@@ -203,34 +210,34 @@
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>时间信息</h4>
|
||||
<h4>{{ $t('tasks.timeInfo') }}</h4>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">创建时间:</span>
|
||||
<span class="detail-label">{{ $t('tasks.createTime') }}:</span>
|
||||
<span class="detail-value">{{ formatDate(currentTask.createdAt || currentTask.createTime) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="currentTask.updatedAt">
|
||||
<span class="detail-label">更新时间:</span>
|
||||
<span class="detail-label">{{ $t('tasks.updateTime') }}:</span>
|
||||
<span class="detail-value">{{ formatDate(currentTask.updatedAt) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="currentTask.completedAt">
|
||||
<span class="detail-label">完成时间:</span>
|
||||
<span class="detail-label">{{ $t('tasks.completeTime') }}:</span>
|
||||
<span class="detail-value">{{ formatDate(currentTask.completedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentTask.progress !== undefined">
|
||||
<h4>进度信息</h4>
|
||||
<h4>{{ $t('tasks.progressInfo') }}</h4>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">进度:</span>
|
||||
<span class="detail-label">{{ $t('tasks.progress') }}:</span>
|
||||
<el-progress :percentage="currentTask.progress || 0" :status="getProgressStatus(currentTask.status)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentTask.resultUrl">
|
||||
<h4>结果</h4>
|
||||
<h4>{{ $t('tasks.result') }}</h4>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">结果链接:</span>
|
||||
<a :href="currentTask.resultUrl" target="_blank" class="result-link">查看结果</a>
|
||||
<span class="detail-label">{{ $t('tasks.resultLink') }}:</span>
|
||||
<a :href="currentTask.resultUrl" target="_blank" class="result-link">{{ $t('tasks.viewResult') }}</a>
|
||||
</div>
|
||||
<div class="result-preview" v-if="isVideoUrl(currentTask.resultUrl)">
|
||||
<video :src="currentTask.resultUrl" controls class="preview-video"></video>
|
||||
@@ -241,19 +248,19 @@
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentTask.errorMessage">
|
||||
<h4>错误信息</h4>
|
||||
<h4>{{ $t('tasks.errorInfo') }}</h4>
|
||||
<div class="error-message">{{ currentTask.errorMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||
<el-button @click="detailDialogVisible = false">{{ $t('tasks.close') }}</el-button>
|
||||
<el-button
|
||||
v-if="currentTask?.resultUrl"
|
||||
type="primary"
|
||||
@click="openResult(currentTask.resultUrl)"
|
||||
>
|
||||
查看结果
|
||||
{{ $t('tasks.viewResult') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -263,6 +270,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Grid,
|
||||
@@ -281,6 +289,7 @@ import { taskStatusApi } from '@/api/taskStatus'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 响应式数据
|
||||
const statusFilter = ref('all')
|
||||
@@ -329,7 +338,11 @@ const goToSettings = () => {
|
||||
|
||||
// 处理用户头像下拉菜单
|
||||
const handleUserCommand = (command) => {
|
||||
if (command === 'exitAdmin') {
|
||||
if (command === 'apiManagement') {
|
||||
router.push('/api-management')
|
||||
} else if (command === 'taskRecord') {
|
||||
router.push('/generate-task-record')
|
||||
} else if (command === 'exitAdmin') {
|
||||
// 退出后台,返回个人首页
|
||||
router.push('/profile')
|
||||
}
|
||||
@@ -442,13 +455,27 @@ const getStatusClass = (status) => {
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'COMPLETED': '已完成',
|
||||
'PROCESSING': '处理中',
|
||||
'CANCELLED': '已取消',
|
||||
'FAILED': '失败',
|
||||
'PENDING': '待处理'
|
||||
'COMPLETED': t('tasks.completed'),
|
||||
'PROCESSING': t('tasks.processing'),
|
||||
'CANCELLED': t('tasks.cancelled'),
|
||||
'FAILED': t('tasks.failed'),
|
||||
'PENDING': t('tasks.pending')
|
||||
}
|
||||
return statusMap[status] || status || '未知'
|
||||
return statusMap[status] || status || t('tasks.unknown')
|
||||
}
|
||||
|
||||
// 获取任务类型文本
|
||||
const getTaskTypeText = (type) => {
|
||||
if (!type) return t('tasks.unknown')
|
||||
const typeMap = {
|
||||
'TEXT_TO_VIDEO': t('tasks.textToVideo'),
|
||||
'IMAGE_TO_VIDEO': t('tasks.imageToVideo'),
|
||||
'STORYBOARD': t('tasks.storyboardVideo'),
|
||||
'文生视频': t('tasks.textToVideo'),
|
||||
'图生视频': t('tasks.imageToVideo'),
|
||||
'分镜视频': t('tasks.storyboardVideo')
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
@@ -459,7 +486,7 @@ const handleStatusFilter = () => {
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '未知'
|
||||
if (!dateString) return t('tasks.unknown')
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
if (isNaN(date.getTime())) return '未知'
|
||||
@@ -898,38 +925,11 @@ const fetchSystemStats = async () => {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: #9ca3af;
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
padding: 10px 12px 10px 40px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #9ca3af;
|
||||
.page-title h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToProfile">
|
||||
|
||||
@@ -326,6 +326,14 @@
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>错误统计</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToApiManagement">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToTaskRecord">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.tasks') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 修改密码(所有登录用户可见) -->
|
||||
<div class="menu-item" @click.stop="goToChangePassword" style="cursor: pointer;">
|
||||
@@ -364,7 +372,7 @@ const isAuthenticated = computed(() => userStore.isAuthenticated)
|
||||
|
||||
// 表单数据
|
||||
const inputText = ref('')
|
||||
const aspectRatio = ref('16:9')
|
||||
const aspectRatio = ref('9:16')
|
||||
const duration = ref('15')
|
||||
const hdMode = ref(false)
|
||||
const inProgress = ref(false)
|
||||
@@ -481,6 +489,16 @@ const goToErrorStats = () => {
|
||||
router.push('/admin/error-statistics')
|
||||
}
|
||||
|
||||
const goToApiManagement = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/api-management')
|
||||
}
|
||||
|
||||
const goToTaskRecord = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/generate-task-record')
|
||||
}
|
||||
|
||||
const goToChangePassword = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/change-password')
|
||||
@@ -567,6 +585,34 @@ const removeLastFrame = () => {
|
||||
lastFrameFile.value = null
|
||||
}
|
||||
|
||||
// 处理创建任务错误的辅助函数
|
||||
const handleCreateError = (errorMsg) => {
|
||||
const msg = errorMsg || t('video.imageToVideo.createTaskFailed')
|
||||
|
||||
// 检测积分不足
|
||||
if (msg.includes('积分不足') || msg.includes('insufficient') || msg.includes('points')) {
|
||||
ElMessageBox.confirm(
|
||||
'您的积分不足,无法创建任务。是否前往充值?',
|
||||
'积分不足',
|
||||
{
|
||||
confirmButtonText: '去充值',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
router.push('/subscription')
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
|
||||
inProgress.value = false
|
||||
isCreatingTask.value = false
|
||||
currentTask.value = null
|
||||
taskStatus.value = ''
|
||||
taskProgress.value = 0
|
||||
}
|
||||
|
||||
// 开始生成视频
|
||||
const startGenerate = async () => {
|
||||
console.log('[StartGenerate] 开始,firstFrameFile:', firstFrameFile.value ? `File(${firstFrameFile.value.name})` : 'null',
|
||||
@@ -603,15 +649,60 @@ const startGenerate = async () => {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 外部 URL(如 COS),清空图片显示,让用户重新上传
|
||||
console.warn('[StartGenerate] 图片是外部URL,清空显示让用户重新上传')
|
||||
firstFrameImage.value = ''
|
||||
ElMessage.error('请重新选择图片文件')
|
||||
return
|
||||
// 外部 URL(如 COS),直接使用 URL 创建任务,无需下载
|
||||
console.log('[StartGenerate] 图片是外部URL,直接使用URL创建任务:', imageUrl.substring(0, 100))
|
||||
|
||||
// 验证描述文字
|
||||
if (!inputText.value.trim()) {
|
||||
ElMessage.error(t('video.imageToVideo.enterDescriptionRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
// 标记正在创建任务
|
||||
isCreatingTask.value = true
|
||||
|
||||
// 显示加载状态
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: t('video.imageToVideo.creatingTask'),
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
try {
|
||||
// 直接调用通过URL创建任务的API
|
||||
const response = await imageToVideoApi.createTaskByUrl({
|
||||
imageUrl: imageUrl,
|
||||
prompt: inputText.value.trim(),
|
||||
aspectRatio: aspectRatio.value,
|
||||
duration: parseInt(duration.value),
|
||||
hdMode: hdMode.value
|
||||
})
|
||||
|
||||
loading.close()
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
currentTask.value = response.data.data
|
||||
inProgress.value = true
|
||||
taskProgress.value = 0
|
||||
taskStatus.value = 'PENDING'
|
||||
ElMessage.success(t('video.imageToVideo.taskCreatedSuccess'))
|
||||
setTimeout(() => userStore.fetchCurrentUser(), 0)
|
||||
startPollingTask()
|
||||
setTimeout(() => { isCreatingTask.value = false }, 2000)
|
||||
} else {
|
||||
handleCreateError(response.data?.message)
|
||||
}
|
||||
return
|
||||
} catch (error) {
|
||||
loading.close()
|
||||
console.error('[StartGenerate] 通过URL创建任务失败:', error)
|
||||
handleCreateError(error.response?.data?.message || error.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证表单
|
||||
// 验证表单(有 File 对象的情况)
|
||||
if (!firstFrameFile.value) {
|
||||
ElMessage.error(t('video.imageToVideo.uploadFirstFrameRequired'))
|
||||
return
|
||||
@@ -633,7 +724,7 @@ const startGenerate = async () => {
|
||||
})
|
||||
|
||||
try {
|
||||
// 准备请求参数
|
||||
// 准备请求参数(使用 File 对象)
|
||||
const params = {
|
||||
firstFrame: firstFrameFile.value,
|
||||
prompt: inputText.value.trim(),
|
||||
@@ -1387,6 +1478,9 @@ const checkLastTaskStatus = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 强制刷新用户信息,确保获取管理员修改后的最新数据
|
||||
await userStore.fetchCurrentUser()
|
||||
|
||||
// 处理"做同款"传递的路由参数
|
||||
if (route.query.prompt || route.query.referenceImage) {
|
||||
console.log('[做同款] 接收参数:', route.query)
|
||||
@@ -2362,10 +2456,11 @@ onUnmounted(() => {
|
||||
|
||||
.video-player {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
/* height 由 aspect-ratio 动态计算 */
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
width: 80%;
|
||||
max-width: 1000px;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2376,7 +2471,6 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -2593,21 +2687,6 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.history-preview.vertical {
|
||||
aspect-ratio: auto;
|
||||
width: 80%;
|
||||
max-width: 1000px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.history-preview.vertical .history-video-thumbnail,
|
||||
.history-preview.vertical .history-placeholder {
|
||||
aspect-ratio: 9/16;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.history-placeholder {
|
||||
width: 100%;
|
||||
@@ -2671,7 +2750,7 @@ onUnmounted(() => {
|
||||
.history-video-thumbnail video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="login-page">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
|
||||
@@ -47,11 +47,10 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<!-- 顶部操作栏 -->
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" />
|
||||
<div class="page-title">
|
||||
<h2>{{ $t('nav.memberManagement') }}</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<LanguageSwitcher />
|
||||
@@ -82,16 +81,16 @@
|
||||
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-select v-model="selectedLevel" placeholder="会员等级" size="small" @change="handleFilterChange">
|
||||
<el-option label="全部等级" value="all" />
|
||||
<el-option label="免费会员" value="free" />
|
||||
<el-option label="标准会员" value="standard" />
|
||||
<el-option label="专业会员" value="professional" />
|
||||
<el-select v-model="selectedLevel" :placeholder="$t('members.memberLevel')" size="small" @change="handleFilterChange">
|
||||
<el-option :label="$t('members.allLevels')" value="all" />
|
||||
<el-option :label="$t('members.freeMember')" value="free" />
|
||||
<el-option :label="$t('members.standardMember')" value="standard" />
|
||||
<el-option :label="$t('members.professionalMember')" value="professional" />
|
||||
</el-select>
|
||||
<el-select v-model="selectedStatus" placeholder="用户状态" size="small" @change="handleFilterChange" style="margin-left: 10px;">
|
||||
<el-option label="活跃用户" value="active" />
|
||||
<el-option label="封禁用户" value="banned" />
|
||||
<el-option label="全部用户" value="all" />
|
||||
<el-select v-model="selectedStatus" :placeholder="$t('members.userStatus')" size="small" @change="handleFilterChange" style="margin-left: 10px;">
|
||||
<el-option :label="$t('members.activeUsers')" value="active" />
|
||||
<el-option :label="$t('members.bannedUsers')" value="banned" />
|
||||
<el-option :label="$t('members.allUsers')" value="all" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
@@ -111,9 +110,9 @@
|
||||
</th>
|
||||
<th>{{ $t('members.userId') }}</th>
|
||||
<th>{{ $t('members.username') }}</th>
|
||||
<th>角色</th>
|
||||
<th>{{ $t('members.role') }}</th>
|
||||
<th>{{ $t('members.level') }}</th>
|
||||
<th>状态</th>
|
||||
<th>{{ $t('members.status') }}</th>
|
||||
<th>{{ $t('members.points') }}</th>
|
||||
<th>{{ $t('members.expiryDate') }}</th>
|
||||
<th>{{ $t('members.operation') }}</th>
|
||||
@@ -135,13 +134,13 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="level-tag" :class="member.level === '专业会员' ? 'professional' : 'standard'">
|
||||
<span class="level-tag" :class="member.level === $t('members.professionalMember') ? 'professional' : 'standard'">
|
||||
{{ member.level }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-tag" :class="member.isActive ? 'active' : 'banned'">
|
||||
{{ member.isActive ? '活跃' : '封禁' }}
|
||||
{{ member.isActive ? $t('members.active') : $t('members.banned') }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ member.points.toLocaleString() }}</td>
|
||||
@@ -153,13 +152,13 @@
|
||||
:type="member.role === 'ROLE_ADMIN' ? 'info' : 'primary'"
|
||||
class="action-link"
|
||||
@click="toggleRole(member)">
|
||||
{{ member.role === 'ROLE_ADMIN' ? '取消管理员' : '设为管理员' }}
|
||||
{{ member.role === 'ROLE_ADMIN' ? $t('members.revokeAdmin') : $t('members.setAdmin') }}
|
||||
</el-link>
|
||||
<el-link
|
||||
:type="member.isActive ? 'warning' : 'success'"
|
||||
class="action-link"
|
||||
@click="toggleBan(member)">
|
||||
{{ member.isActive ? '封禁' : '解封' }}
|
||||
{{ member.isActive ? $t('members.ban') : $t('members.unban') }}
|
||||
</el-link>
|
||||
<el-link type="danger" class="action-link" @click="deleteMember(member)">{{ $t('common.delete') }}</el-link>
|
||||
</td>
|
||||
@@ -209,17 +208,17 @@
|
||||
<el-form-item label="用户ID" prop="id">
|
||||
<el-input v-model="editForm.id" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="editForm.username" placeholder="请输入用户名" />
|
||||
<el-form-item :label="$t('members.username')" prop="username">
|
||||
<el-input v-model="editForm.username" :placeholder="$t('members.usernamePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isSuperAdmin && editForm.role !== 'ROLE_SUPER_ADMIN'" label="用户角色" prop="role">
|
||||
<el-select v-model="editForm.role" placeholder="请选择用户角色">
|
||||
<el-option label="普通用户" value="ROLE_USER" />
|
||||
<el-option label="管理员" value="ROLE_ADMIN" />
|
||||
<el-form-item v-if="isSuperAdmin && editForm.role !== 'ROLE_SUPER_ADMIN'" :label="$t('members.userRole')" prop="role">
|
||||
<el-select v-model="editForm.role" :placeholder="$t('members.selectRole')">
|
||||
<el-option :label="$t('members.normalUser')" value="ROLE_USER" />
|
||||
<el-option :label="$t('members.admin')" value="ROLE_ADMIN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="会员等级" prop="level">
|
||||
<el-select v-model="editForm.level" placeholder="请选择会员等级">
|
||||
<el-form-item :label="$t('members.level')" prop="level">
|
||||
<el-select v-model="editForm.level" :placeholder="$t('members.levelPlaceholder')">
|
||||
<el-option
|
||||
v-for="level in membershipLevels"
|
||||
:key="level.id"
|
||||
@@ -228,7 +227,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="剩余资源点" prop="points">
|
||||
<el-form-item :label="$t('members.points')" prop="points">
|
||||
<el-input-number
|
||||
v-model="editForm.points"
|
||||
:min="0"
|
||||
@@ -257,6 +256,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Grid,
|
||||
@@ -275,6 +275,7 @@ import * as memberAPI from '@/api/members'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 数据状态
|
||||
const selectedMembers = ref([])
|
||||
@@ -581,14 +582,14 @@ const handleFilterChange = () => {
|
||||
|
||||
// 封禁/解封会员
|
||||
const toggleBan = async (member) => {
|
||||
const action = member.isActive ? '封禁' : '解封'
|
||||
const action = member.isActive ? t('members.ban') : t('members.unban')
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要${action}用户 ${member.username} 吗?`,
|
||||
`确认${action}`,
|
||||
t('members.confirmBanAction', { action: action, username: member.username }),
|
||||
t('members.confirmAction', { action: action }),
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
@@ -596,15 +597,15 @@ const toggleBan = async (member) => {
|
||||
const response = await memberAPI.toggleBanMember(member.id, !member.isActive)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
ElMessage.success(response.data.message || `${action}成功`)
|
||||
ElMessage.success(response.data.message || t('members.actionSuccess', { action: action }))
|
||||
await loadMembers()
|
||||
} else {
|
||||
ElMessage.error(response.data?.message || `${action}失败`)
|
||||
ElMessage.error(response.data?.message || t('members.actionFailed', { action: action }))
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(`${action}失败:`, error)
|
||||
const errorMsg = error.response?.data?.message || `${action}失败`
|
||||
const errorMsg = error.response?.data?.message || t('members.actionFailed', { action: action })
|
||||
ElMessage.error(errorMsg)
|
||||
}
|
||||
}
|
||||
@@ -648,10 +649,38 @@ const loadMembers = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:获取会员等级显示名称
|
||||
// 辅助函数:获取会员等级显示名称(支持国际化)
|
||||
const getMembershipLevel = (membership) => {
|
||||
if (!membership) return '标准会员'
|
||||
return membership.display_name || '标准会员'
|
||||
if (!membership) return t('members.standardMember')
|
||||
|
||||
// 根据会员等级name进行翻译
|
||||
const levelName = membership.name || membership.level_name
|
||||
if (levelName) {
|
||||
const levelMap = {
|
||||
'free': t('members.freeMember'),
|
||||
'standard': t('members.standardMember'),
|
||||
'professional': t('members.professionalMember')
|
||||
}
|
||||
if (levelMap[levelName]) {
|
||||
return levelMap[levelName]
|
||||
}
|
||||
}
|
||||
|
||||
// 如果display_name包含中文,尝试翻译
|
||||
const displayName = membership.display_name
|
||||
if (displayName) {
|
||||
if (displayName.includes('免费') || displayName.toLowerCase().includes('free')) {
|
||||
return t('members.freeMember')
|
||||
}
|
||||
if (displayName.includes('标准') || displayName.toLowerCase().includes('standard')) {
|
||||
return t('members.standardMember')
|
||||
}
|
||||
if (displayName.includes('专业') || displayName.toLowerCase().includes('professional')) {
|
||||
return t('members.professionalMember')
|
||||
}
|
||||
}
|
||||
|
||||
return membership.display_name || t('members.standardMember')
|
||||
}
|
||||
|
||||
// 辅助函数:获取会员到期时间
|
||||
@@ -663,11 +692,11 @@ const getMembershipExpiry = (membership) => {
|
||||
// 辅助函数:获取角色显示名称
|
||||
const getRoleLabel = (role) => {
|
||||
const roleMap = {
|
||||
'ROLE_SUPER_ADMIN': '超级管理员',
|
||||
'ROLE_ADMIN': '管理员',
|
||||
'ROLE_USER': '普通用户'
|
||||
'ROLE_SUPER_ADMIN': t('members.superAdmin'),
|
||||
'ROLE_ADMIN': t('members.admin'),
|
||||
'ROLE_USER': t('members.normalUser')
|
||||
}
|
||||
return roleMap[role] || '普通用户'
|
||||
return roleMap[role] || t('members.normalUser')
|
||||
}
|
||||
|
||||
// 辅助函数:获取角色样式类
|
||||
@@ -683,15 +712,15 @@ const getRoleClass = (role) => {
|
||||
// 设置/取消管理员
|
||||
const toggleRole = async (member) => {
|
||||
const newRole = member.role === 'ROLE_ADMIN' ? 'ROLE_USER' : 'ROLE_ADMIN'
|
||||
const action = newRole === 'ROLE_ADMIN' ? '设为管理员' : '取消管理员权限'
|
||||
const action = newRole === 'ROLE_ADMIN' ? t('members.setAdmin') : t('members.revokeAdmin')
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要将用户 ${member.username} ${action}吗?`,
|
||||
`确认${action}`,
|
||||
t('members.confirmRoleChange', { username: member.username, action: action }),
|
||||
t('members.confirmAction', { action: action }),
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
@@ -699,15 +728,15 @@ const toggleRole = async (member) => {
|
||||
const response = await memberAPI.setUserRole(member.id, newRole)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
ElMessage.success(response.data.message || `${action}成功`)
|
||||
ElMessage.success(response.data.message || t('members.actionSuccess', { action: action }))
|
||||
await loadMembers()
|
||||
} else {
|
||||
ElMessage.error(response.data?.message || `${action}失败`)
|
||||
ElMessage.error(response.data?.message || t('members.actionFailed', { action: action }))
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(`${action}失败:`, error)
|
||||
const errorMsg = error.response?.data?.message || `${action}失败`
|
||||
const errorMsg = error.response?.data?.message || t('members.actionFailed', { action: action })
|
||||
ElMessage.error(errorMsg)
|
||||
}
|
||||
}
|
||||
@@ -886,38 +915,11 @@ const fetchSystemStats = async () => {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: #9ca3af;
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
padding: 10px 12px 10px 40px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #9ca3af;
|
||||
.page-title h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<aside class="sidebar">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
@@ -412,6 +412,14 @@
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>错误统计</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="goToApiManagement">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="goToTaskRecord">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.tasks') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 修改密码 -->
|
||||
<div class="menu-item" @click.stop="goToChangePassword" style="cursor: pointer;">
|
||||
@@ -608,6 +616,24 @@ const goToSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const goToApiManagement = () => {
|
||||
showUserMenu.value = false
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/api-management')
|
||||
} else {
|
||||
ElMessage.warning(t('profile.insufficientPermission'))
|
||||
}
|
||||
}
|
||||
|
||||
const goToTaskRecord = () => {
|
||||
showUserMenu.value = false
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/generate-task-record')
|
||||
} else {
|
||||
ElMessage.warning(t('profile.insufficientPermission'))
|
||||
}
|
||||
}
|
||||
|
||||
const goToChangePassword = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/change-password')
|
||||
@@ -1524,13 +1550,11 @@ const loadUserInfo = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 强制刷新用户信息,确保获取管理员修改后的最新数据
|
||||
await userStore.fetchCurrentUser()
|
||||
loadUserInfo()
|
||||
loadList()
|
||||
})
|
||||
|
||||
// 注册/注销全局点击监听用于关闭菜单
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<aside class="sidebar">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
@@ -258,6 +258,14 @@
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>错误统计</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToApiManagement">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToTaskRecord">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.tasks') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 修改密码(所有登录用户可见) -->
|
||||
<div class="menu-item" @click.stop="goToChangePassword" style="cursor: pointer; pointer-events: auto;">
|
||||
@@ -420,6 +428,28 @@ const goToSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到API管理
|
||||
const goToApiManagement = () => {
|
||||
showUserMenu.value = false
|
||||
// 检查用户权限,只有管理员才能访问API管理
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/api-management')
|
||||
} else {
|
||||
ElMessage.warning(t('profile.insufficientPermission'))
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到生成任务管理
|
||||
const goToTaskRecord = () => {
|
||||
showUserMenu.value = false
|
||||
// 检查用户权限,只有管理员才能访问生成任务管理
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/generate-task-record')
|
||||
} else {
|
||||
ElMessage.warning(t('profile.insufficientPermission'))
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const logout = async () => {
|
||||
try {
|
||||
@@ -703,8 +733,10 @@ const onImageError = (event) => {
|
||||
event.target.style.display = 'none'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
// 强制刷新用户信息,确保获取管理员修改后的最新数据
|
||||
await userStore.fetchCurrentUser()
|
||||
loadUserInfo()
|
||||
loadVideos()
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="login-page">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 设置密码卡片 -->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToProfile">
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 分镜视频创作逻辑改了3版,有很多逻辑用不上但是我保留了,谁知道什么时候就又用上了 -->
|
||||
|
||||
<!-- 用户菜单下拉 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showUserMenu" class="user-menu-teleport" :style="menuStyle">
|
||||
@@ -46,6 +48,14 @@
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>错误统计</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToApiManagement">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToTaskRecord">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.tasks') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 修改密码(所有登录用户可见) -->
|
||||
<div class="menu-item" @click.stop="goToChangePassword" style="cursor: pointer;">
|
||||
@@ -567,7 +577,7 @@ const isAuthenticated = computed(() => userStore.isAuthenticated)
|
||||
// 表单数据
|
||||
const inputText = ref('')
|
||||
const videoPrompt = ref('') // 视频生成提示词
|
||||
const aspectRatio = ref('16:9')
|
||||
const aspectRatio = ref('9:16')
|
||||
const duration = ref('15')
|
||||
const hdMode = ref(false)
|
||||
const imageModel = ref('nano-banana2')
|
||||
@@ -704,6 +714,16 @@ const goToErrorStats = () => {
|
||||
router.push('/admin/error-statistics')
|
||||
}
|
||||
|
||||
const goToApiManagement = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/api-management')
|
||||
}
|
||||
|
||||
const goToTaskRecord = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/generate-task-record')
|
||||
}
|
||||
|
||||
const goToChangePassword = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/change-password')
|
||||
@@ -2733,6 +2753,9 @@ const checkLastTaskStatus = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 强制刷新用户信息,确保获取管理员修改后的最新数据
|
||||
await userStore.fetchCurrentUser()
|
||||
|
||||
// 处理"做同款"传递的路由参数
|
||||
if (route.query.prompt || route.query.referenceImage) {
|
||||
console.log('[做同款] 接收参数:', route.query)
|
||||
@@ -4439,12 +4462,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
.video-player {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
width: 80%;
|
||||
max-width: 1000px;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -4453,11 +4475,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
.result-video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -4742,6 +4761,7 @@ onBeforeUnmount(() => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.history-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -4804,7 +4824,7 @@ onBeforeUnmount(() => {
|
||||
.history-video-thumbnail video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<aside class="sidebar">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
@@ -127,7 +127,7 @@
|
||||
<div class="package-header">
|
||||
<h4 class="package-title">{{ $t('subscription.free') }}</h4>
|
||||
</div>
|
||||
<div class="package-price">¥{{ membershipPrices.free }}/年</div>
|
||||
<div class="package-price">¥{{ membershipPrices.free }}/{{ $t('subscription.perMonth') }}</div>
|
||||
<div class="points-box points-box-placeholder"> </div>
|
||||
<button class="package-button current">{{ $t('subscription.currentPackage') }}</button>
|
||||
<div class="package-features">
|
||||
@@ -137,19 +137,19 @@
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>文生视频:30积分/条</span>
|
||||
<span>{{ $t('subscription.textToVideo30Points') }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>图生视频:30积分/条</span>
|
||||
<span>{{ $t('subscription.imageToVideo30Points') }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>分镜图生成:30积分/次</span>
|
||||
<span>{{ $t('subscription.storyboardImage30Points') }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>分镜视频生成:30积分/条</span>
|
||||
<span>{{ $t('subscription.storyboardVideo30Points') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,8 +160,9 @@
|
||||
<h4 class="package-title">{{ $t('subscription.standard') }}</h4>
|
||||
<div class="discount-tag">{{ $t('subscription.firstPurchaseDiscount') }}</div>
|
||||
</div>
|
||||
<div class="package-price">¥{{ membershipPrices.standard }}/年</div>
|
||||
<div class="points-box">{{ $t('subscription.standardPoints') }}</div>
|
||||
<div class="package-price">¥{{ membershipPrices.standard }}/{{ Math.floor((membershipPoints.standard || 0) / 30) }}{{ $t('subscription.items') }}</div>
|
||||
<div class="points-box" v-if="membershipPoints.standard !== null">{{ membershipPoints.standard }}{{ $t('subscription.points') }}</div>
|
||||
<div class="points-box" v-else>{{ $t('subscription.loading') }}</div>
|
||||
<button class="package-button subscribe" @click.stop="handleSubscribe('standard')">{{ $t('subscription.subscribe') }}</button>
|
||||
<div class="package-features">
|
||||
<div class="feature-item">
|
||||
@@ -172,6 +173,26 @@
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.commercialUse') }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.pointsValidOneYear') }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.maxTextToVideo', { count: Math.floor((membershipPoints.standard || 0) / 30) }) }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.maxImageToVideo', { count: Math.floor((membershipPoints.standard || 0) / 30) }) }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.maxStoryboardImage', { count: Math.floor((membershipPoints.standard || 0) / 30) }) }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.maxStoryboardVideo', { count: Math.floor((membershipPoints.standard || 0) / 30) }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -181,8 +202,9 @@
|
||||
<h4 class="package-title">{{ $t('subscription.professional') }}</h4>
|
||||
<div class="value-tag">{{ $t('subscription.bestValue') }}</div>
|
||||
</div>
|
||||
<div class="package-price">¥{{ membershipPrices.premium }}/年</div>
|
||||
<div class="points-box">{{ $t('subscription.premiumPoints') }}</div>
|
||||
<div class="package-price">¥{{ membershipPrices.premium }}/{{ Math.floor((membershipPoints.premium || 0) / 30) }}{{ $t('subscription.items') }}</div>
|
||||
<div class="points-box" v-if="membershipPoints.premium !== null">{{ membershipPoints.premium }}{{ $t('subscription.points') }}</div>
|
||||
<div class="points-box" v-else>{{ $t('subscription.loading') }}</div>
|
||||
<button class="package-button premium" @click.stop="handleSubscribe('premium')">{{ $t('subscription.subscribe') }}</button>
|
||||
<div class="package-features">
|
||||
<div class="feature-item">
|
||||
@@ -197,6 +219,26 @@
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.earlyAccess') }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.pointsValidOneYear') }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.textToVideoItems', { count: Math.floor((membershipPoints.premium || 0) / 30) }) }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.imageToVideoItems', { count: Math.floor((membershipPoints.premium || 0) / 30) }) }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.storyboardImageTimes', { count: Math.floor((membershipPoints.premium || 0) / 30) }) }}</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<el-icon class="check-icon"><Check /></el-icon>
|
||||
<span>{{ $t('subscription.storyboardVideoItems', { count: Math.floor((membershipPoints.premium || 0) / 30) }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,6 +270,14 @@
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>错误统计</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="goToApiManagement">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="goToTaskRecord">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.tasks') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 修改密码 -->
|
||||
<div class="menu-item" @click.stop="goToChangePassword" style="cursor: pointer;">
|
||||
@@ -422,6 +472,9 @@ const loadUserSubscriptionInfo = async () => {
|
||||
await userStore.init()
|
||||
}
|
||||
|
||||
// 强制刷新用户信息,确保获取管理员修改后的最新数据
|
||||
await userStore.fetchCurrentUser()
|
||||
|
||||
// 检查用户是否已认证
|
||||
if (!userStore.isAuthenticated) {
|
||||
console.warn('用户未认证,跳转到登录页')
|
||||
@@ -766,6 +819,24 @@ const goToSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const goToApiManagement = () => {
|
||||
showUserMenu.value = false
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/api-management')
|
||||
} else {
|
||||
ElMessage.warning(t('profile.insufficientPermission'))
|
||||
}
|
||||
}
|
||||
|
||||
const goToTaskRecord = () => {
|
||||
showUserMenu.value = false
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/generate-task-record')
|
||||
} else {
|
||||
ElMessage.warning(t('profile.insufficientPermission'))
|
||||
}
|
||||
}
|
||||
|
||||
const goToChangePassword = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/change-password')
|
||||
@@ -793,6 +864,9 @@ const selectPlan = (plan) => {
|
||||
}
|
||||
|
||||
// 处理订阅
|
||||
// 记录当前选择的套餐类型,用于判断是否需要生成新订单
|
||||
let lastSelectedPlanType = ''
|
||||
|
||||
const handleSubscribe = async (planType) => {
|
||||
console.log('handleSubscribe 被调用,planType:', planType)
|
||||
try {
|
||||
@@ -800,13 +874,23 @@ const handleSubscribe = async (planType) => {
|
||||
const planInfo = getPlanInfo(planType)
|
||||
console.log('获取到的套餐信息:', planInfo)
|
||||
|
||||
// 设置支付数据
|
||||
currentPaymentData.value = {
|
||||
title: `${planInfo.name}会员`,
|
||||
amount: planInfo.price,
|
||||
orderId: `SUB_${planType}_${Date.now()}`,
|
||||
planType: planType,
|
||||
planInfo: planInfo
|
||||
// 只有套餐类型变化时才生成新的orderId
|
||||
if (planType !== lastSelectedPlanType || !currentPaymentData.value.orderId) {
|
||||
currentPaymentData.value = {
|
||||
title: `${planInfo.name}会员`,
|
||||
amount: planInfo.price,
|
||||
orderId: `SUB_${planType}_${Date.now()}`,
|
||||
planType: planType,
|
||||
planInfo: planInfo
|
||||
}
|
||||
lastSelectedPlanType = planType
|
||||
console.log('套餐变化,生成新的orderId:', currentPaymentData.value.orderId)
|
||||
} else {
|
||||
// 同一套餐,只更新必要信息,保留orderId
|
||||
currentPaymentData.value.title = `${planInfo.name}会员`
|
||||
currentPaymentData.value.amount = planInfo.price
|
||||
currentPaymentData.value.planInfo = planInfo
|
||||
console.log('同一套餐,复用orderId:', currentPaymentData.value.orderId)
|
||||
}
|
||||
|
||||
console.log('支付数据设置完成:', currentPaymentData.value)
|
||||
@@ -930,6 +1014,10 @@ const handlePaymentSuccess = async (paymentData) => {
|
||||
|
||||
// 关闭支付模态框
|
||||
paymentModalVisible.value = false
|
||||
|
||||
// 重置支付数据,下次购买生成新订单
|
||||
lastSelectedPlanType = ''
|
||||
currentPaymentData.value = {}
|
||||
|
||||
// 重新加载用户订阅信息
|
||||
await loadUserSubscriptionInfo()
|
||||
@@ -1334,6 +1422,12 @@ const createSubscriptionOrder = async (planType, planInfo) => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subscription-packages .points-box .points-validity {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.subscription-packages .points-box-placeholder {
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
|
||||
@@ -47,11 +47,10 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<!-- 顶部操作栏 -->
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><User /></el-icon>
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input">
|
||||
<div class="page-title">
|
||||
<h2>{{ $t('nav.systemSettings') }}</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<LanguageSwitcher />
|
||||
@@ -111,8 +110,8 @@
|
||||
<h3>{{ level.name }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="price">¥{{ level.price || 0 }}/年</p>
|
||||
<p class="description">包含{{ level.resourcePoints || level.pointsBonus || 0 }}积分/年</p>
|
||||
<p class="price">¥{{ level.price || 0 }}/{{ Math.floor((level.resourcePoints || level.pointsBonus || 0) / 30) }}{{ $t('subscription.items') }}</p>
|
||||
<p class="description">{{ level.resourcePoints || level.pointsBonus || 0 }}{{ $t('subscription.points') }}</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<el-button type="primary" @click="editLevel(level)">{{ $t('common.edit') }}</el-button>
|
||||
@@ -254,10 +253,6 @@
|
||||
</template>
|
||||
<div class="ai-model-content">
|
||||
<el-form label-width="180px">
|
||||
<el-form-item :label="$t('systemSettings.promptOptimizationApiUrl')">
|
||||
<el-input v-model="promptOptimizationApiUrl" style="width: 400px;" placeholder="https://ai.comfly.chat"></el-input>
|
||||
<div class="model-tip">{{ $t('systemSettings.promptOptimizationApiUrlTip') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('systemSettings.promptOptimizationModel')">
|
||||
<el-input v-model="promptOptimizationModel" style="width: 400px;" placeholder="gpt-5.1-thinking"></el-input>
|
||||
<div class="model-tip">{{ $t('systemSettings.promptOptimizationModelTip') }}</div>
|
||||
@@ -431,8 +426,8 @@ import {
|
||||
ShoppingCart,
|
||||
Document,
|
||||
Setting,
|
||||
User as Search,
|
||||
User as ArrowDown,
|
||||
Search,
|
||||
ArrowDown,
|
||||
Delete,
|
||||
Refresh,
|
||||
Check,
|
||||
@@ -503,7 +498,6 @@ const cleanupConfig = reactive({
|
||||
|
||||
// AI模型设置相关
|
||||
const promptOptimizationModel = ref('gpt-5.1-thinking')
|
||||
const promptOptimizationApiUrl = ref('https://ai.comfly.chat')
|
||||
const storyboardSystemPrompt = ref('')
|
||||
const savingAiModel = ref(false)
|
||||
|
||||
@@ -576,8 +570,14 @@ const saveEdit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const pointsInt = parseInt(editForm.resourcePoints)
|
||||
if (Number.isNaN(pointsInt) || pointsInt < 0) {
|
||||
ElMessage.error(t('systemSettings.enterValidNumber'))
|
||||
return
|
||||
}
|
||||
|
||||
// 直接更新membership_levels表
|
||||
const updateData = { price: priceInt }
|
||||
const updateData = { price: priceInt, pointsBonus: pointsInt }
|
||||
console.log('准备更新会员等级:', editForm.id, updateData)
|
||||
const response = await api.put(`/members/levels/${editForm.id}`, updateData)
|
||||
console.log('会员等级更新响应:', response.data)
|
||||
@@ -761,9 +761,6 @@ const loadAiModelSettings = async () => {
|
||||
if (data.promptOptimizationModel) {
|
||||
promptOptimizationModel.value = data.promptOptimizationModel
|
||||
}
|
||||
if (data.promptOptimizationApiUrl) {
|
||||
promptOptimizationApiUrl.value = data.promptOptimizationApiUrl
|
||||
}
|
||||
if (data.storyboardSystemPrompt !== undefined) {
|
||||
storyboardSystemPrompt.value = data.storyboardSystemPrompt
|
||||
}
|
||||
@@ -785,7 +782,6 @@ const saveAiModelSettings = async () => {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
promptOptimizationModel: promptOptimizationModel.value,
|
||||
promptOptimizationApiUrl: promptOptimizationApiUrl.value,
|
||||
storyboardSystemPrompt: storyboardSystemPrompt.value
|
||||
})
|
||||
})
|
||||
@@ -943,27 +939,11 @@ const fetchSystemStats = async () => {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 20px;
|
||||
padding: 8px 15px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: #909399;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
.page-title h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
@@ -1511,11 +1491,6 @@ const fetchSystemStats = async () => {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 200px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToProfile">
|
||||
|
||||
@@ -300,6 +300,14 @@
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>错误统计</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToApiManagement">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="goToTaskRecord">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('nav.tasks') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 修改密码(所有登录用户可见) -->
|
||||
<div class="menu-item" @click.stop="goToChangePassword" style="cursor: pointer;">
|
||||
@@ -338,7 +346,7 @@ const isAuthenticated = computed(() => userStore.isAuthenticated)
|
||||
|
||||
// 响应式数据
|
||||
const inputText = ref('')
|
||||
const aspectRatio = ref('16:9')
|
||||
const aspectRatio = ref('9:16')
|
||||
const duration = ref(15)
|
||||
const hdMode = ref(false)
|
||||
const inProgress = ref(false)
|
||||
@@ -447,6 +455,16 @@ const goToErrorStats = () => {
|
||||
router.push('/admin/error-statistics')
|
||||
}
|
||||
|
||||
const goToApiManagement = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/api-management')
|
||||
}
|
||||
|
||||
const goToTaskRecord = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/generate-task-record')
|
||||
}
|
||||
|
||||
const goToChangePassword = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/change-password')
|
||||
@@ -1152,6 +1170,9 @@ const checkLastTaskStatus = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 强制刷新用户信息,确保获取管理员修改后的最新数据
|
||||
await userStore.fetchCurrentUser()
|
||||
|
||||
// 处理"做同款"传递的路由参数
|
||||
if (route.query.prompt) {
|
||||
inputText.value = route.query.prompt
|
||||
@@ -2033,12 +2054,11 @@ onUnmounted(() => {
|
||||
|
||||
.video-player {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
width: 80%;
|
||||
max-width: 1000px;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2047,11 +2067,8 @@ onUnmounted(() => {
|
||||
|
||||
.result-video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -2268,21 +2285,6 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.history-preview.vertical {
|
||||
aspect-ratio: auto;
|
||||
width: 80%;
|
||||
max-width: 1000px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.history-preview.vertical .history-video-thumbnail,
|
||||
.history-preview.vertical .history-placeholder {
|
||||
aspect-ratio: 9/16;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.history-placeholder {
|
||||
width: 100%;
|
||||
@@ -2346,7 +2348,7 @@ onUnmounted(() => {
|
||||
.history-video-thumbnail video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<aside class="sidebar">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<header class="navbar">
|
||||
<div class="navbar-content">
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||
</div>
|
||||
<nav class="nav-links">
|
||||
<a href="#" class="nav-link" @click.prevent="goToTextToVideo">{{ $t('welcome.textToVideo') }}</a>
|
||||
|
||||
@@ -58,6 +58,24 @@ export default defineConfig({
|
||||
// 代码分割优化
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 为所有资源文件(包括SVG)生成内容哈希
|
||||
assetFileNames: (assetInfo) => {
|
||||
// 获取文件扩展名
|
||||
const extType = assetInfo.name.split('.').pop();
|
||||
// 图片类型(包括SVG)
|
||||
if (/png|jpe?g|gif|svg|webp|ico/i.test(extType)) {
|
||||
return `static/images/[name]-[hash][extname]`;
|
||||
}
|
||||
// 字体类型
|
||||
if (/woff2?|eot|ttf|otf/i.test(extType)) {
|
||||
return `static/fonts/[name]-[hash][extname]`;
|
||||
}
|
||||
// 其他资源
|
||||
return `static/[name]-[hash][extname]`;
|
||||
},
|
||||
// JS文件哈希
|
||||
chunkFileNames: 'static/js/[name]-[hash].js',
|
||||
entryFileNames: 'static/js/[name]-[hash].js',
|
||||
manualChunks: {
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
'element-plus': ['element-plus', '@element-plus/icons-vue'],
|
||||
|
||||
Reference in New Issue
Block a user