Quellcode durchsuchen

前端代码更新提交

“陈博论” vor 10 Monaten
Ursprung
Commit
a7525b3735
74 geänderte Dateien mit 8363 neuen und 747 gelöschten Zeilen
  1. 1 1
      web/.env.development
  2. 1 0
      web/src/assets/icons/ad.svg
  3. 1 0
      web/src/assets/icons/administrator.svg
  4. 1 0
      web/src/assets/icons/agent.svg
  5. 1 0
      web/src/assets/icons/allocation.svg
  6. 0 0
      web/src/assets/icons/appAuth.svg
  7. 1 0
      web/src/assets/icons/console.svg
  8. 0 0
      web/src/assets/icons/consume_setting.svg
  9. 1 0
      web/src/assets/icons/consumption.svg
  10. 1 0
      web/src/assets/icons/creative-review.svg
  11. 0 0
      web/src/assets/icons/creative.svg
  12. 0 0
      web/src/assets/icons/customer _report.svg
  13. 1 0
      web/src/assets/icons/daily-newspaper.svg
  14. 0 0
      web/src/assets/icons/dingding-icon.svg
  15. 1 0
      web/src/assets/icons/dingding.svg
  16. 1 0
      web/src/assets/icons/fans.svg
  17. 0 0
      web/src/assets/icons/gear.svg
  18. 0 0
      web/src/assets/icons/ks-account.svg
  19. 1 0
      web/src/assets/icons/ks-upload.svg
  20. 0 0
      web/src/assets/icons/kuaishou.svg
  21. 1 0
      web/src/assets/icons/luodiye.svg
  22. 1 0
      web/src/assets/icons/material.svg
  23. 1 0
      web/src/assets/icons/monitor.svg
  24. 0 0
      web/src/assets/icons/ocean.svg
  25. 1 0
      web/src/assets/icons/oceanAdvertiser.svg
  26. 1 0
      web/src/assets/icons/oceanAuth.svg
  27. 1 0
      web/src/assets/icons/oceanElement.svg
  28. 0 0
      web/src/assets/icons/oceanReport.svg
  29. 1 0
      web/src/assets/icons/permission.svg
  30. 1 0
      web/src/assets/icons/pic.svg
  31. 1 0
      web/src/assets/icons/robot.svg
  32. 0 0
      web/src/assets/icons/role.svg
  33. 1 0
      web/src/assets/icons/rule.svg
  34. 1 0
      web/src/assets/icons/staff.svg
  35. 1 0
      web/src/assets/icons/subject.svg
  36. 1 0
      web/src/assets/icons/tc-cost.svg
  37. 1 0
      web/src/assets/icons/tc.svg
  38. 1 0
      web/src/assets/icons/tc_task.svg
  39. 1 0
      web/src/assets/icons/user-info.svg
  40. 1 0
      web/src/assets/icons/videosvg.svg
  41. 0 0
      web/src/assets/icons/winxin.svg
  42. BIN
      web/src/assets/images/login_bg.jpg
  43. BIN
      web/src/assets/images/logo.png
  44. 9 7
      web/src/components/table/fieldRender/buttons.vue
  45. 7 11
      web/src/components/table/header/index.vue
  46. 0 3
      web/src/components/table/index.vue
  47. 1 0
      web/src/lang/backend/en/module.ts
  48. 1 0
      web/src/lang/backend/zh-cn/module.ts
  49. 30 0
      web/src/router/frontend.ts
  50. 2 0
      web/src/router/static.ts
  51. 15 11
      web/src/utils/router.ts
  52. 212 0
      web/src/views/backend/application/combo/index.vue
  53. 128 0
      web/src/views/backend/application/combo/popupForm.vue
  54. 242 0
      web/src/views/backend/application/product/index.vue
  55. 152 0
      web/src/views/backend/application/product/popupForm.vue
  56. 4 4
      web/src/views/backend/dashboard.vue
  57. 28 5
      web/src/views/backend/module/components/goodsInfo.vue
  58. 290 0
      web/src/views/frontend/Me.vue
  59. 532 0
      web/src/views/frontend/Order.vue
  60. 342 0
      web/src/views/frontend/Register.vue
  61. 292 0
      web/src/views/frontend/Reset.vue
  62. 567 0
      web/src/views/frontend/components/AddMemberDialog.vue
  63. 176 0
      web/src/views/frontend/components/CreateEnterpriseDialog.vue
  64. 624 0
      web/src/views/frontend/components/Me/enterprise.vue
  65. 278 0
      web/src/views/frontend/components/Me/info.vue
  66. 465 0
      web/src/views/frontend/components/Me/purchaseHistory.vue
  67. 205 0
      web/src/views/frontend/components/Me/purchaseProduct.vue
  68. 366 0
      web/src/views/frontend/components/Me/subordinates.vue
  69. 1110 0
      web/src/views/frontend/components/ProductSeatDialog.vue
  70. 344 0
      web/src/views/frontend/components/goods.vue
  71. 840 0
      web/src/views/frontend/components/header.vue
  72. 377 0
      web/src/views/frontend/components/success.vue
  73. 331 130
      web/src/views/frontend/index.vue
  74. 364 575
      web/src/views/frontend/user/login.vue

+ 1 - 1
web/.env.development

@@ -5,4 +5,4 @@ ENV = 'development'
 VITE_BASE_PATH = './'
 
 # 本地环境接口地址 - 尾部无需带'/'
-VITE_AXIOS_BASE_URL = 'http://localhost:8000'
+VITE_AXIOS_BASE_URL = 'http://user.adjdds.com'

+ 1 - 0
web/src/assets/icons/ad.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1748400425482" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6082" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M358.016 140.544H230.144l-8.64 0.448a89.6 89.6 0 0 0-80.96 89.152v127.872L50.048 448.64l-6.144 6.784A89.6 89.6 0 0 0 50.048 575.36l90.496 90.496v128l0.448 8.64a89.6 89.6 0 0 0 89.152 80.96l127.872-0.128 90.624 90.624 6.784 6.144a89.6 89.6 0 0 0 119.936-6.144l90.496-90.624 128 0.128 8.576-0.448a89.6 89.6 0 0 0 81.024-89.152l-0.128-128 90.624-90.496 6.144-6.784a89.6 89.6 0 0 0-6.144-119.936l-90.624-90.624 0.128-127.872-0.448-8.576a89.6 89.6 0 0 0-89.152-81.024h-128L575.36 50.048a89.6 89.6 0 0 0-126.72 0L358.016 140.544z m163.008-36.16l112.96 112.96h159.872c7.04 0 12.8 5.76 12.8 12.8l-0.064 159.744 113.024 113.088a12.8 12.8 0 0 1 0 18.048l-113.024 112.96v159.872a12.8 12.8 0 0 1-12.8 12.8l-159.808-0.064-112.96 113.024a12.8 12.8 0 0 1-18.048 0l-113.088-113.024H230.144a12.8 12.8 0 0 1-12.8-12.8V634.048L104.384 521.024a12.8 12.8 0 0 1 0-18.048l112.96-113.088V230.144c0-7.04 5.76-12.8 12.8-12.8h159.744l113.024-112.96a12.8 12.8 0 0 1 18.112 0z" fill="#666666" p-id="6083"></path><path d="M637.312 387.712a40 40 0 0 1 61.184 51.008l-4.608 5.568-192 192a40 40 0 0 1-51.008 4.608l-5.568-4.608-115.2-115.2a40 40 0 0 1 51.008-61.184l5.568 4.608L473.6 551.488l163.712-163.776z" fill="#666666" p-id="6084"></path></svg>

+ 1 - 0
web/src/assets/icons/administrator.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747808568819" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29859" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M695.466667 793.6l119.466666-119.466667 29.866667 29.866667-149.333333 149.333333-4.266667-4.266666-89.6-89.6 29.866667-29.866667 64 64zM554.666667 810.666667v42.666666H213.333333v-136.533333L332.8 597.333333h311.466667l46.933333 46.933334-29.866667 29.866666-29.866666-34.133333H349.866667L256 733.866667V810.666667h298.666667z m128-448c0 106.666667-85.333333 192-192 192S298.666667 469.333333 298.666667 362.666667 384 170.666667 490.666667 170.666667 682.666667 256 682.666667 362.666667z m-42.666667 0C640 281.6 571.733333 213.333333 490.666667 213.333333S341.333333 281.6 341.333333 362.666667 409.6 512 490.666667 512 640 443.733333 640 362.666667z" fill="#444444" p-id="29860"></path></svg>

+ 1 - 0
web/src/assets/icons/agent.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747796813938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7959" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M870.071 216.042c18.748 0 34 15.252 34 34v523.916c0 18.748-15.252 34-34 34H153.929c-18.748 0-34-15.252-34-34V250.042c0-18.748 15.252-34 34-34h716.142m0-56H153.929c-49.706 0-90 40.294-90 90v523.916c0 49.706 40.294 90 90 90h716.143c49.706 0 90-40.294 90-90V250.042c-0.001-49.705-40.295-90-90.001-90z" fill="" p-id="7960"></path><path d="M478.953 522.602c20.776-21.575 33.556-50.905 33.556-83.223 0-66.274-53.726-120-120-120s-120 53.726-120 120c0 32.318 12.78 61.648 33.556 83.223-67.175 32.247-113.556 100.907-113.556 180.401h56c0-38.464 14.979-74.625 42.177-101.823 25.543-25.543 58.993-40.307 94.831-42.009 2.314 0.133 4.644 0.208 6.992 0.208s4.678-0.075 6.992-0.208c35.838 1.702 69.288 16.465 94.831 42.009 27.198 27.198 42.177 63.36 42.177 101.823h56c0-79.494-46.381-148.153-113.556-180.401z m-150.444-83.223c0-35.29 28.71-64 64-64s64 28.71 64 64c0 33.3-25.568 60.729-58.102 63.717-1.96-0.057-3.925-0.093-5.898-0.093s-3.938 0.036-5.898 0.093c-32.534-2.988-58.102-30.417-58.102-63.717zM832.47 348c0 15.4-12.6 28-28 28H620.509c-15.4 0-28-12.6-28-28s12.6-28 28-28H804.47c15.4 0 28 12.6 28 28zM832.47 479.982c0 15.4-12.6 28-28 28H679.727c-15.4 0-28-12.6-28-28s12.6-28 28-28H804.47c15.4 0 28 12.6 28 28z" fill="" p-id="7961"></path><path d="M832.47 611.965c0 15.4-12.6 28-28 28h-70.135c-15.4 0-28-12.6-28-28s12.6-28 28-28h70.135c15.4 0 28 12.6 28 28z" fill="" p-id="7962"></path></svg>

+ 1 - 0
web/src/assets/icons/allocation.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747808345332" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28804" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M884.224 755.917H659.866c-22.426 0-37.376 12.185-37.376 30.72s14.95 30.72 37.376 30.72h224.358c22.426 0 37.376-12.186 37.376-30.72s-14.95-30.72-37.376-30.72z m0 109.875H722.739c-22.425 0-37.376 12.186-37.376 30.72s14.95 30.72 37.376 30.72h161.485c22.426 0 37.376-12.186 37.376-30.72s-14.95-30.72-37.376-30.72zM748.646 675.84c0 18.227 14.95 30.72 37.376 30.72h98.202c22.426 0 37.376-12.083 37.376-30.72s-14.95-30.72-37.376-30.72h-98.202c-22.425 0.82-37.376 12.902-37.376 30.72z m-47.001-61.44c-15.36-8.806-32.973-16.486-49.46-24.166a267.366 267.366 0 0 0 119.809-223.232c0-148.48-124.212-269.415-277.095-269.415S217.907 218.42 217.907 366.9a267.366 267.366 0 0 0 120.935 222.106c-125.338 52.736-214.426 143.36-234.19 278.22a55.194 55.194 0 0 0-2.252 13.415v3.277a38.502 38.502 0 0 0 39.629 38.502 38.502 38.502 0 0 0 39.629-38.502c19.66-147.559 141.721-238.797 295.73-247.706a188.211 188.211 0 0 0 35.226 0 345.6 345.6 0 0 1 136.397 32.973 168.038 168.038 0 0 1 52.634-54.784z m-188.007-55.091a212.173 212.173 0 0 0-37.376 0A194.56 194.56 0 0 1 296.96 367.923c0-106.7 89.088-192.41 197.94-192.41s197.938 85.812 197.938 192.41c0 100.045-78.438 181.453-179.2 191.386z" p-id="28805"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/appAuth.svg


+ 1 - 0
web/src/assets/icons/console.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747297377773" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11916" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M944.38 70.19h-864c-44.19 0-80 35.81-80 80v571.43c0 44.19 35.81 80 80 80h864c44.18 0 80-35.81 80-80V150.19c0-44.19-35.82-80-80-80z m5.45 603.45c0 26.26-21.29 47.55-47.55 47.55H750.12V445.41c0-26.26-21.28-47.55-47.55-47.55-26.26 0-47.55 21.29-47.55 47.55v275.78h-95.1V350.31c0-26.26-21.28-47.55-47.55-47.55-26.26 0-47.55 21.29-47.55 47.55v370.88h-95.1v-199.7c0-26.26-21.28-47.55-47.55-47.55-26.26 0-47.55 21.28-47.55 47.55v199.7H122.49c-26.26 0-47.55-21.29-47.55-47.55V198.16c0-26.26 21.29-47.55 47.55-47.55h779.79c26.26 0 47.55 21.29 47.55 47.55v475.48zM722.67 874.76H302.09c-25.25 0-45.71 20.47-45.71 45.71 0 25.25 20.47 45.71 45.71 45.71h420.58c25.24 0 45.71-20.46 45.71-45.71 0-25.24-20.47-45.71-45.71-45.71z m0 0" fill="#333333" p-id="11917"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/consume_setting.svg


+ 1 - 0
web/src/assets/icons/consumption.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747297897596" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18920" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M937.487 903.914H132.272V30c0-16.568-13.431-30-30-30-16.569 0-30 13.431-30 30v873.914H37.001c-16.569 0-30 13.431-30 30 0 16.569 13.431 30 30 30h35.271V994c0 16.569 13.431 30 30 30 16.569 0 30-13.431 30-30v-30.086h805.215c16.569 0 30-13.431 30-30 0-16.569-13.432-30-30-30z" fill="#828282" p-id="18921"></path><path d="M186.007 777.468a32.856 32.856 0 0 0 20.717 7.331c9.626 0 19.169-4.192 25.69-12.264l223.42-276.564 28.417 134.333a33 33 0 0 0 54.734 17.358l378.491-351.271c13.359-12.398 14.138-33.278 1.74-46.636-12.398-13.359-33.276-14.137-46.637-1.74L536.771 559.673l-30.724-145.237a32.998 32.998 0 0 0-24.585-25.259 32.998 32.998 0 0 0-33.371 11.352L181.074 731.061c-11.453 14.176-9.244 34.954 4.933 46.407z" fill="#828282" p-id="18922"></path></svg>

+ 1 - 0
web/src/assets/icons/creative-review.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1748587087199" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10203" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M704 768a64 64 0 0 1-64 64H384a64 64 0 0 1-64-64v-51.392a384 384 0 1 1 384 0V768z m-32-106.752a320 320 0 1 0-319.936 0L384 679.68V768h256v-88.32l32-18.432zM384 896h256v64H384v-64z" fill="#595959" p-id="10204"></path><path d="M512 160A224 224 0 0 0 288 384h64A160 160 0 0 1 512 224v-64z" fill="#595959" p-id="10205"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/creative.svg


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/customer _report.svg


+ 1 - 0
web/src/assets/icons/daily-newspaper.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1751946607395" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11839" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M800 32h-576a128 128 0 0 0-128 128v704a128 128 0 0 0 128 128h576a128 128 0 0 0 128-128v-704a128 128 0 0 0-128-128z m64 832a64 64 0 0 1-64 64h-576a64 64 0 0 1-64-64v-704a64 64 0 0 1 64-64h576a64 64 0 0 1 64 64v704z" fill="" p-id="11840"></path><path d="M736 736h-448a32 32 0 0 0 0 64h448a32 32 0 0 0 0-64zM736 576h-448a32 32 0 0 0 0 64h448a32 32 0 0 0 0-64zM736 192a64 64 0 0 0-64 64c0 5.568 1.792 10.496 3.136 15.616L591.616 355.136C586.496 353.792 581.568 352 576 352c-4.608 0-8.704 1.664-12.992 2.624l-53.632-53.632C510.336 296.704 512 292.608 512 288a64 64 0 1 0-128 0c0 5.568 1.792 10.496 3.136 15.616L303.616 387.136C298.496 385.792 293.568 384 288 384a64 64 0 1 0 0 128 64 64 0 0 0 64-64c0-5.568-1.792-10.496-3.136-15.616l83.52-83.52c5.12 1.344 10.048 3.136 15.616 3.136 5.248 0 9.856-1.792 14.72-3.008l52.288 52.288C513.792 406.144 512 410.752 512 416a64 64 0 1 0 128 0c0-5.568-1.792-10.496-3.136-15.616l83.52-83.52c5.12 1.344 10.048 3.136 15.616 3.136a64 64 0 1 0 0-128z" fill="" p-id="11841"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/dingding-icon.svg


+ 1 - 0
web/src/assets/icons/dingding.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746525185703" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4947" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512.003 79C272.855 79 79 272.855 79 512.003 79 751.145 272.855 945 512.003 945 751.145 945 945 751.145 945 512.003 945 272.855 751.145 79 512.003 79z m200.075 375.014c-0.867 3.764-3.117 9.347-6.234 16.012h0.087l-0.347 0.648c-18.183 38.86-65.631 115.108-65.631 115.108l-0.215-0.52-13.856 24.147h66.8L565.063 779l29.002-115.368h-52.598l18.27-76.29c-14.76 3.55-32.253 8.436-52.945 15.1 0 0-27.967 16.36-80.607-31.5 0 0-35.501-31.29-14.891-39.078 8.744-3.33 42.466-7.573 69.004-11.122 35.93-4.845 57.965-7.441 57.965-7.441s-110.607 1.643-136.841-2.468c-26.237-4.11-59.525-47.905-66.626-86.377 0 0-10.953-21.117 23.595-11.122 34.547 10 177.535 38.95 177.535 38.95s-185.933-56.992-198.36-70.929c-12.381-13.846-36.406-75.902-33.289-113.981 0 0 1.343-9.521 11.127-6.926 0 0 137.49 62.75 231.475 97.152 94.028 34.403 175.76 51.885 165.2 96.414z" fill="#3AA2EB" p-id="4948"></path></svg>

+ 1 - 0
web/src/assets/icons/fans.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747298713323" class="icon" viewBox="0 0 1088 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="50857" width="51" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M572.8 160a160 160 0 1 0 0 320 160 160 0 0 0 0-320zM348.8 320a224 224 0 1 1 235.648 223.68v49.024a340.224 340.224 0 0 0-340.224 340.288H168.448A416 416 0 0 1 489.408 528 224.064 224.064 0 0 1 348.736 320z m284.672 486.72l46.464-121.472 45.568 211.392c8.448 39.232 63.808 40.832 74.56 2.176l46.144-166.08 50.304 79.616 9.408 14.912h62.912a32 32 0 1 0 0-64h-27.648l-75.52-119.68a35.2 35.2 0 0 0-63.68 9.408l-35.84 128.64-41.792-194.048c-8.064-37.312-59.776-41.28-73.408-5.632l-69.376 181.312H489.856a32 32 0 0 0 0 64h135.744l7.872-20.48z" fill="#000000" fill-opacity=".9" p-id="50858"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/gear.svg


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/ks-account.svg


+ 1 - 0
web/src/assets/icons/ks-upload.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1752029125851" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9859" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M921.6 128a25.6 25.6 0 0 1 25.1904 20.992L947.2 153.6v387.9936a25.6 25.6 0 0 1-50.7904 4.608l-0.4096-4.608V179.2h-768v665.6h422.7072a25.6 25.6 0 0 1 25.1904 20.992l0.4096 4.608a25.6 25.6 0 0 1-20.992 25.1904l-4.608 0.4096H102.4a25.6 25.6 0 0 1-25.1904-20.992L76.8 870.4V153.6a25.6 25.6 0 0 1 20.992-25.1904L102.4 128h819.2z" fill="#2c2c2c" p-id="9860"></path><path d="M415.9488 307.1488a25.6 25.6 0 0 1 42.7008-19.0464l192.1536 172.9536a25.6 25.6 0 0 1 1.8944 36.1472l-1.8944 1.8944-192.1536 172.9024a25.6 25.6 0 0 1-42.7008-18.9952V307.1488z m51.2 57.4976v230.912l128.256-115.5072-128.256-115.4048zM912.6912 699.392a25.6 25.6 0 0 1-32.6656 39.1168l-3.584-2.9184-126.6688-126.72-126.72 126.72a25.6 25.6 0 0 1-39.168-32.6656l2.9696-3.584 126.72-126.6688a51.2 51.2 0 0 1 67.9424-3.9936l4.4544 3.9936 126.72 126.6688z" fill="#2c2c2c" p-id="9861"></path><path d="M743.168 561.7664a25.6 25.6 0 0 1 25.1904 20.992l0.4096 4.608v307.2a25.6 25.6 0 0 1-50.7904 4.608l-0.4096-4.608v-307.2a25.6 25.6 0 0 1 25.6-25.6z" fill="#2c2c2c" p-id="9862"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/kuaishou.svg


+ 1 - 0
web/src/assets/icons/luodiye.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761116833819" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6159" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M848.457143 117.028571H146.285714c-32.182857 0-58.514286 26.331429-58.514285 58.514286v702.171429c0 32.182857 26.331429 58.514286 58.514285 58.514285h702.171429c32.182857 0 58.514286-26.331429 58.514286-58.514285V175.542857c0-32.182857-26.331429-58.514286-58.514286-58.514286zM146.285714 175.542857h702.171429v175.542857H146.285714V175.542857z m702.171429 702.171429H146.285714V409.6h702.171429v468.114286z" p-id="6160" fill="#2c2c2c"></path><path d="M263.314286 585.142857h468.114285c17.554286 0 29.257143-11.702857 29.257143-29.257143s-11.702857-29.257143-29.257143-29.257143H263.314286c-17.554286 0-29.257143 11.702857-29.257143 29.257143s11.702857 29.257143 29.257143 29.257143z m0 146.285714h468.114285c17.554286 0 29.257143-11.702857 29.257143-29.257142s-11.702857-29.257143-29.257143-29.257143H263.314286c-17.554286 0-29.257143 11.702857-29.257143 29.257143s11.702857 29.257143 29.257143 29.257142zM731.428571 234.057143h-58.514285c-17.554286 0-29.257143 11.702857-29.257143 29.257143s11.702857 29.257143 29.257143 29.257143h58.514285c17.554286 0 29.257143-11.702857 29.257143-29.257143s-11.702857-29.257143-29.257143-29.257143z" p-id="6161" fill="#2c2c2c"></path></svg>

+ 1 - 0
web/src/assets/icons/material.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747296774356" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7401" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M869.5296 296.2432H593.92v-30.0544a153.088 153.088 0 0 0-153.088-153.088H144.0768A41.6256 41.6256 0 0 0 102.4 154.7264v704.1536c0 28.7744 23.3472 52.0704 52.0704 52.0704h715.008c28.7744 0 52.0704-23.3472 52.0704-52.0704V348.3136a52.0192 52.0192 0 0 0-52.0192-52.0704zM154.4704 165.1712H440.832c55.7056 0 101.0176 45.312 101.0176 101.0176v30.0544H154.4704v-131.072z m715.0592 183.1424v510.5152H154.4704V348.9792H593.92v-0.6656h275.6096z" p-id="7402"></path></svg>

+ 1 - 0
web/src/assets/icons/monitor.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762588954379" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6398" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M932 848H120V148c0-15.5-12.5-28-28-28s-28 12.5-28 28v700c0 30.9 25.1 56 56 56h812c15.5 0 28-12.5 28-28s-12.5-28-28-28z" p-id="6399" fill="#515151"></path><path d="M204 652h66.7c21.3 0 40.5-11.8 50.1-31l26.6-53.4 54.9 82.3c10.5 15.8 27.9 25 46.5 25 2.1 0 4.3-0.2 6.4-0.4 21-2.4 38.7-16.2 46.1-36L625.6 307l133.3 311c8.9 20.6 29 33.9 51.5 33.9H876c15.5 0 28-12.5 28-28s-12.5-28-28-28h-65.5L677.2 285c-8.9-20.7-28.9-33.9-51.4-33.9h-1.4c-23 0.5-43 14.8-51.1 36.3L449 619l-55-82.4c-11.2-16.8-30-26-50-24.8-20.1 1.3-37.6 12.8-46.6 30.8L270.7 596H204c-15.5 0-28 12.5-28 28s12.5 28 28 28z" p-id="6400" fill="#515151"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/ocean.svg


+ 1 - 0
web/src/assets/icons/oceanAdvertiser.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1757124763570" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9541" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M810.666667 170.666667H234.666667a64 64 0 0 0-64 64v490.666666h704V234.666667a64 64 0 0 0-64-64z m-597.333334 64a21.333333 21.333333 0 0 1 21.333334-21.333334h576a21.333333 21.333333 0 0 1 21.333333 21.333334v448H213.333333V234.666667z" fill="#005BFF" p-id="9542"></path><path d="M512.512 597.056h-45.802667l-14.784-43.925333h-71.616l-14.506666 43.925333H320L392.192 405.333333h49.578667l70.741333 191.722667z m-70.442667-75.456l-22.336-66.432a70.954667 70.954667 0 0 1-3.2-16.618667h-1.152a78.229333 78.229333 0 0 1-3.477333 16.341334l-22.613333 66.986666h52.778666v-0.277333z m109.866667 75.456v-191.445333h68.16c70.144 0 105.237333 31.253333 105.237333 93.461333 0 29.568-9.856 53.504-29.568 71.253333-19.712 18.005333-44.949333 27.008-75.968 27.008h-67.84v-0.277333z m42.048-158.229333v124.714666h22.912c20.010667 0 35.669333-5.909333 46.976-17.450666 11.306667-11.52 17.109333-27.306667 17.109333-47.018667 0-19.136-6.101333-33.770667-17.984-44.48-11.882667-10.688-27.264-16.042667-46.4-16.042667h-22.613333v0.277334z" fill="#005BFF" p-id="9543"></path><path d="M309.333333 768h448a10.666667 10.666667 0 0 1 0 21.333333h-448a10.666667 10.666667 0 0 1 0-21.333333z" fill="#06F3FF" p-id="9544"></path><path d="M192 311.466667h650.666667v17.066666H192v-17.066666z" fill="#005BFF" p-id="9545"></path><path d="M416 832h234.666667a10.666667 10.666667 0 0 1 0 21.333333h-234.666667a10.666667 10.666667 0 0 1 0-21.333333z" fill="#005BFF" p-id="9546"></path></svg>

+ 1 - 0
web/src/assets/icons/oceanAuth.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1758350407022" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5631" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M897.1 908.4H144.3V83.7h752.8v824.7z m-718.6-34.2h684.4V117.9H178.5v756.3z" fill="" p-id="5632"></path><path d="M207.9 143.3h624.7v15.3H207.9z" fill="" p-id="5633"></path><path d="M446.4 561.4c-51.9-31.7-81.9-76.1-83.8-137.4-1.3-42.9 13.3-80.1 42.9-111.1 64.9-68 168.3-66.2 231.8 2.5 67.3 72.7 48.7 198.9-43.4 246.1 4.5 2 8.5 3.7 12.6 5.5C670.1 594.3 714 640.5 737 705.9c5.2 14.9 7.4 30.8 10.7 46.3 0.6 2.7 0.2 5.7-0.5 8.4-1.7 6.8-6.5 10.3-13 10.1-6.9-0.2-11.9-4.4-13.3-11.1-2.6-12.6-4.1-25.6-8.2-37.7-25.8-77-77.7-125.7-158.1-140.1-68.9-12.3-128.8 7.7-178.4 57.3C344 671.3 326 710.6 320 755.6c-1.1 8.5-4.5 13.3-10.8 14.9-9.5 2.3-17.6-4.8-16.6-14.9 3.4-36.1 15.1-69.3 34.7-99.7 26.6-41.1 62.7-70.9 108-89.5 2.9-1.2 5.8-2.5 8.6-3.8 0.5-0.2 1.1-0.5 2.5-1.2z m73.7-10.3c71.2 0 128.6-57.4 129.7-127.2 1.2-76-59.1-131.1-126.4-132.9-70.4-1.8-131.9 54.3-133.1 128-1.1 72.2 56.6 132 129.8 132.1z" fill="" p-id="5634"></path><path d="M626.3 421c0 53-43.1 95.9-96.4 95.9v-24c39.9 0 72.2-32.2 72.2-72s-32.3-72-72.2-72v-24c53.2 0.2 96.4 43.1 96.4 96.1z" fill="" p-id="5635"></path></svg>

+ 1 - 0
web/src/assets/icons/oceanElement.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1757124882175" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11558" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M425.228751 496.770134l-194.812941-95.762017 142.482085-274.90507 144.404877 0 218.205721 283.836487L425.228751 496.770134zM317.773414 372.264469 431.788144 428.309926l192.338586-54.715159L486.36004 190.434214l-74.345242 0L317.773414 372.264469z" fill="#272636" p-id="11559"></path><path d="M575.908542 785.697569 0.021489 785.697569l195.793268-329.556785 193.422268 97.240695L575.908542 785.697569zM113.072265 721.365378l328.617389 0L347.846336 604.577488l-127.352503-64.025199L113.072265 721.365378z" fill="#272636" p-id="11560"></path><path d="M1022.198981 785.383414 665.682171 785.383414l-180.697455-226.938616 279.339056-82.306564L1022.198981 785.383414zM696.692443 721.051223l183.498244 0L743.494375 549.948287 593.951477 592.017428 696.692443 721.051223z" fill="#272636" p-id="11561"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/oceanReport.svg


+ 1 - 0
web/src/assets/icons/permission.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747297037681" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10282" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M311.4 911c-16.9 0-32.6-9.1-40.9-23.8L70 536.8c-8.3-14.4-8.3-32.4 0-46.8l200.1-350.6c8.4-14.7 24.1-23.8 41-23.8l400.1-0.2c16.9 0 32.6 9.1 41 23.8l200.5 350.4c8.3 14.4 8.3 32.4 0 46.8L752.5 887c-8.4 14.7-24.1 23.8-41 23.8l-400.1 0.2z m399.8-755.1l-400.1 0.1c-2.4 0-4.7 1.3-5.9 3.4L105.1 510.1c-1.2 2.1-1.2 4.6 0 6.7l200.5 350.4c1.2 2.1 3.4 3.4 5.8 3.4l400.1-0.2c2.4 0 4.7-1.3 5.9-3.4l200.1-350.6c1.2-2.1 1.2-4.6 0-6.7L717.1 159.3c-1.2-2.1-3.5-3.4-5.9-3.4z" fill="#4F4F4F" p-id="10283"></path><path d="M511.3 521.8c-34.2 0-66.3-13.3-90.5-37.5-24.2-24.2-37.5-56.3-37.6-90.5 0-34.2 13.3-66.4 37.5-90.6 24.2-24.2 56.3-37.5 90.6-37.6h0.1c70.6 0 128 57.4 128.1 128 0 34.2-13.3 66.4-37.5 90.6-24.3 24.3-56.5 37.6-90.7 37.6z m0-215.7c-48.4 0-87.6 39.3-87.6 87.7 0 23.4 9.1 45.4 25.7 62 16.5 16.5 38.5 25.6 61.9 25.6s45.4-9.1 62-25.7c16.5-16.6 25.7-38.6 25.6-62 0-48.3-39.3-87.6-87.6-87.6zM514.3 628.9c-11.2 0-20.2-9-20.2-20.2s9-20.2 20.2-20.2h88.5c11.2 0 20.2 9 20.2 20.2s-9 20.2-20.2 20.2h-88.5z" fill="#4F4F4F" p-id="10284"></path><path d="M511.4 760.8c-11.2 0-20.2-9-20.2-20.2l-0.1-236.8c0-11.2 9-20.2 20.2-20.2s20.2 9 20.2 20.2l0.1 236.8c0 11.1-9 20.2-20.2 20.2z" fill="#4F4F4F" p-id="10285"></path></svg>

+ 1 - 0
web/src/assets/icons/pic.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747797780313" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10945" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M887.466667 136.533333H136.533333a68.266667 68.266667 0 0 0-68.266666 68.266667v614.4a68.266667 68.266667 0 0 0 68.266666 68.266667h750.933334a68.266667 68.266667 0 0 0 68.266666-68.266667V204.8a68.266667 68.266667 0 0 0-68.266666-68.266667zM136.533333 204.8h750.933334v614.4H136.533333V204.8z" fill="#444444" p-id="10946"></path><path d="M358.4 375.466667a51.2 51.2 0 1 1-102.4 0 51.2 51.2 0 0 1 102.4 0" fill="#444444" p-id="10947"></path><path d="M750.933333 409.6l48.264534 48.264533-204.8 204.8a34.133333 34.133333 0 0 1-48.264534 0l-112.401066-112.384-163.601067 163.584L221.866667 665.6l187.733333-187.733333a34.133333 34.133333 0 0 1 48.264533 0l112.401067 112.384L750.933333 409.6z" fill="#00B386" p-id="10948"></path></svg>

+ 1 - 0
web/src/assets/icons/robot.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747722416427" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7356" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M41.984 509.952h51.2v208.896h-51.2z m886.784 0h51.2v208.896h-51.2zM318.464 599.04a48.128 48.128 0 1 0 96.256 0 48.128 48.128 0 1 0-96.256 0zM601.088 599.04a48.128 48.128 0 1 0 96.256 0 48.128 48.128 0 1 0-96.256 0z" p-id="7357"></path><path d="M841.523 320.512H533.504V160.563c13.517-8.601 22.528-23.552 22.528-40.755 0-26.624-21.504-48.128-48.128-48.128s-48.128 21.504-48.128 48.128c0 17.203 9.011 32.154 22.528 40.755v159.949H180.429c-23.757 0-43.008 19.251-43.008 43.008v546.816c0 23.757 19.251 43.008 43.008 43.008h660.89c23.756 0 43.007-19.251 43.007-43.008V363.52c0.205-23.757-19.25-43.008-42.803-43.008z m-8.192 581.632h-644.71V371.712h644.505v530.432z" p-id="7358"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/role.svg


+ 1 - 0
web/src/assets/icons/rule.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747798998400" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25709" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M329.955556 56.888889L113.777778 273.066667V967.111111h580.266666l216.177778-216.177778V56.888889H329.955556zM853.333333 728.177778L671.288889 910.222222H170.666667V295.822222L352.711111 113.777778H853.333333V728.177778z" p-id="25710"></path><path d="M278.755556 312.888889h56.888888v56.888889h-56.888888zM278.755556 483.555556h56.888888v56.888888h-56.888888zM278.755556 654.222222h56.888888v56.888889h-56.888888zM398.222222 312.888889h341.333334v56.888889h-341.333334zM398.222222 483.555556h341.333334v56.888888h-341.333334zM398.222222 654.222222h341.333334v56.888889h-341.333334z" p-id="25711"></path></svg>

+ 1 - 0
web/src/assets/icons/staff.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747797927895" class="icon" viewBox="0 0 1050 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19708" width="32.8125" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M699.09839799 697.494468l39.86160901-49.764855a441.633001 441.633001 0 0 0-116.272762-67.103021A305.726491 305.726491 0 0 0 794.334504 305.710196 305.769427 305.769427 0 1 0 333.917489 569.577055c-181.889 55.034004-314.3117 224.004251-314.3117 423.911405h63.747022a378.992331 378.992331 0 0 1 378.991332-379.034269c87.289945 0 169.989729 29.577931 236.753256 83.040277zM246.456799 305.710196a242.06534199 242.06534199 0 1 1 484.13068299 0 242.06534199 242.06534199 0 0 1-484.13068299 0z" p-id="19709"></path><path d="M791.23212601 591.973685l-10.96366501 62.76848 138.28596501 24.096099-238.49466201 150.313045-76.32628-79.640342-285.795187 212.78496699 38.077268 51.12383001 240.66343-179.21099001 73.43558699 76.62283801 277.67828101-175.047193-29.493058 126.387692 62.087495 14.448473 56.437911-241.937532z" p-id="19710"></path></svg>

+ 1 - 0
web/src/assets/icons/subject.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747298822743" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="56994" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M832 796.16l56.32 115.2-64 5.12-40.96 51.2-56.32-115.2c-10.24 0-23.04 5.12-40.96 5.12-10.24 0-23.04 0-40.96-5.12L588.8 967.68 552.96 921.6l-64-5.12 56.32-115.2c-33.28-35.84-56.32-87.04-56.32-138.24 0-110.08 87.04-202.24 194.56-202.24 110.08 0 194.56 92.16 194.56 202.24 5.12 51.2-10.24 97.28-46.08 133.12zM793.6 593.92c-5.12-5.12-23.04-5.12-28.16 0l-102.4 104.96-51.2-51.2c-5.12-5.12-23.04-5.12-28.16 0-5.12 5.12-5.12 23.04 0 28.16l69.12 69.12c5.12 5.12 23.04 5.12 28.16 0l115.2-120.32c2.56-7.68 2.56-20.48-2.56-30.72z m-76.8-225.28h-23.04V104.96L350.72 220.16V184.32l378.88-138.24L844.8 143.36v253.44c-5.12 0-5.12-5.12-10.24-5.12-40.96-17.92-76.8-23.04-117.76-23.04z m-112.64-102.4v125.44c-33.28 17.92-64 35.84-92.16 64-23.04 23.04-40.96 46.08-51.2 74.24V271.36L174.08 363.52v535.04h-51.2v-563.2l384-133.12 97.28 64zM409.6 442.88l-184.32 51.2V409.6l176.64-56.32v92.16h7.68z m0 151.04l-179.2 33.28v-74.24l176.64-46.08v87.04z m0 156.16l-179.2 15.36V691.2l176.64-28.16v87.04z m0 148.48H225.28V819.2l176.64-12.8v92.16h7.68z" fill="#333333" p-id="56995"></path></svg>

+ 1 - 0
web/src/assets/icons/tc-cost.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762248470365" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7361" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M784.83 527.35a25 25 0 0 0-35.36 0l-78.34 78.34-78.34-78.34a25 25 0 0 0-35.36 35.36l53.39 53.39h-35.6a25 25 0 0 0 0 50h70.93v45.13h-70.92a25 25 0 0 0 0 50h70.93v71.54a25 25 0 0 0 50 0v-71.54h70.93a25 25 0 0 0 0-50h-70.93v-45.14h70.93a25 25 0 0 0 0-50h-35.65l53.39-53.39a25 25 0 0 0 0-35.35z" fill="#666666" p-id="7362"></path><path d="M863 65.16H161a64.19 64.19 0 0 0-64 64v767.68a64.19 64.19 0 0 0 64 64h702a64.19 64.19 0 0 0 64-64V129.16a64.19 64.19 0 0 0-64-64z m0 831.58l-0.11 0.11H161.12l-0.11-0.11V129.27l0.11-0.11h701.76l0.11 0.11z" fill="#666666" p-id="7363"></path><path d="M319.06 349.28h383.27a32 32 0 0 0 0-64H319.06a32 32 0 0 0 0 64zM319.06 477.11h383.27a32 32 0 0 0 0-64H319.06a32 32 0 0 0 0 64zM447.09 542.32h-128a32 32 0 0 0 0 64h128a32 32 0 1 0 0-64z" fill="#666666" p-id="7364"></path></svg>

+ 1 - 0
web/src/assets/icons/tc.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745726140044" class="icon" viewBox="0 0 1075 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1833" width="50.390625" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M684.3904 712.9088a72.3456 72.3456 0 0 1-57.088-117.0432l128.9728-164.9152A72.4992 72.4992 0 0 1 870.4 520.1408l-128.9216 164.9152a72.3456 72.3456 0 0 1-57.1392 27.8528zM555.4176 547.9424a72.3968 72.3968 0 0 1-57.0368-116.992l128.9216-164.9664a72.4992 72.4992 0 0 1 114.176 89.1904l-128.8704 164.9664a72.448 72.448 0 0 1-57.1904 27.8016z" fill="#8ED2FF" p-id="1834"></path><path d="M291.328 555.8272a72.3968 72.3968 0 0 1-57.0368-116.992l135.1168-172.8512a72.4992 72.4992 0 0 1 114.176 89.1904l-135.0144 172.8a72.3968 72.3968 0 0 1-57.1904 27.8528M369.4592 785.8176a72.448 72.448 0 0 1-57.0368-116.992l185.9584-237.8752a72.4992 72.4992 0 0 1 114.176 89.1904l-185.9584 237.8752a72.2944 72.2944 0 0 1-57.1392 27.8016zM627.3536 785.8176a72.448 72.448 0 0 1-57.0368-116.992l56.9856-72.96a72.4992 72.4992 0 0 1 114.176 89.1904l-56.9856 72.96a72.3456 72.3456 0 0 1-57.1392 27.8016z" fill="#296AEF" p-id="1835"></path><path d="M111.616 785.8176a72.3968 72.3968 0 0 1-57.088-116.992l56.9856-72.96A72.4992 72.4992 0 0 1 225.792 685.056l-57.0368 72.96a72.3456 72.3456 0 0 1-57.1392 27.8016z" fill="#FFA600" p-id="1836"></path><path d="M885.248 785.8176a72.3968 72.3968 0 0 1-57.0368-116.992l56.9856-72.96a72.4992 72.4992 0 0 1 114.176 89.1904l-56.9856 72.96a72.3968 72.3968 0 0 1-57.1392 27.8016z" fill="#8ED2FF" p-id="1837"></path></svg>

+ 1 - 0
web/src/assets/icons/tc_task.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762238669184" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4728" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M448 896H163.04C143.904 896 128 879.68 128 859.072V164.928C128 144.32 143.904 128 163.04 128h569.92C752.096 128 768 144.32 768 164.928v182.464a32 32 0 0 0 64 0V164.928C832 109.376 787.84 64 732.96 64H163.04C108.16 64 64 109.376 64 164.928v694.144C64 914.624 108.16 960 163.04 960H448a32 32 0 0 0 0-64z" p-id="4729"></path><path d="M736 448a256 256 0 1 0 0 512 256 256 0 0 0 0-512z m0 448a192 192 0 1 1 0-384 192 192 0 0 1 0 384z" p-id="4730"></path><path d="M831.456 688H752v-64a32 32 0 0 0-64 0v96a32 32 0 0 0 32 32h111.456a32 32 0 0 0 0-64zM640 320a32 32 0 0 0-32-32H288a32 32 0 0 0 0 64h320a32 32 0 0 0 32-32zM288 544a32 32 0 0 0 0 64h80.288a32 32 0 0 0 0-64H288z" p-id="4731"></path></svg>

+ 1 - 0
web/src/assets/icons/user-info.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747796726199" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7748" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M803.474286 641.389714a299.812571 299.812571 0 0 1 189.659428 253.513143 36.571429 36.571429 0 0 1-72.850285 6.436572C909.897143 784.822857 813.494857 694.857143 697.782857 694.857143c-115.785143 0-212.260571 89.965714-222.500571 206.555428a36.571429 36.571429 0 1 1-72.850286-6.436571 299.666286 299.666286 0 0 1 197.485714-256.512A156.818286 156.818286 0 0 1 548.571429 522.24C548.571429 435.858286 617.764571 365.714286 703.268571 365.714286c85.577143 0 154.770286 70.144 154.770286 156.452571 0 47.689143-21.211429 90.477714-54.564571 119.222857zM292.571429 914.285714a36.571429 36.571429 0 1 1 0 73.142857h-58.514286c-109.787429 0-197.485714-95.670857-197.485714-212.114285V248.685714C36.571429 132.242286 124.269714 36.571429 234.057143 36.571429h482.742857c109.787429 0 197.485714 95.670857 197.485714 212.114285a36.571429 36.571429 0 1 1-73.142857 0c0-77.458286-56.393143-138.971429-124.342857-138.971428H234.057143c-67.949714 0-124.342857 61.513143-124.342857 138.971428v526.628572c0 77.458286 56.393143 138.971429 124.342857 138.971428H292.571429zM182.857143 292.571429h292.571428a36.571429 36.571429 0 0 1 0 73.142857h-292.571428a36.571429 36.571429 0 0 1 0-73.142857z m0 219.428571h146.285714a36.571429 36.571429 0 0 1 0 73.142857h-146.285714a36.571429 36.571429 0 0 1 0-73.142857z m520.411428 93.403429c44.982857 0 81.627429-37.156571 81.627429-83.236572 0-46.08-36.644571-83.309714-81.627429-83.309714C658.358857 438.857143 621.714286 476.013714 621.714286 522.166857c0 46.08 36.644571 83.236571 81.554285 83.236572z" fill="#666666" p-id="7749"></path></svg>

+ 1 - 0
web/src/assets/icons/videosvg.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747797872972" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16724" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M861.44 246.8864a68.266667 68.266667 0 0 1 93.422933 58.965333l0.136534 4.3008 0.477866 232.5504a68.266667 68.266667 0 0 1-89.361066 65.0752l-4.061867-1.467733-157.832533-62.583467a34.133333 34.133333 0 0 1 22.647466-64.341333l2.5088 0.887467 157.832534 62.5664-0.477867-232.533334-157.2864 62.737067a34.133333 34.133333 0 0 1-27.733333-62.327467l2.440533-1.092266 157.2864-62.737067z" fill="#444444" p-id="16725"></path><path d="M682.666667 119.466667H136.533333a68.266667 68.266667 0 0 0-68.266666 68.266666v477.866667a68.266667 68.266667 0 0 0 68.266666 68.266667h546.133334a68.266667 68.266667 0 0 0 68.266666-68.266667V187.733333a68.266667 68.266667 0 0 0-68.266666-68.266666zM136.533333 187.733333h546.133334v477.866667H136.533333V187.733333z" fill="#444444" p-id="16726"></path><path d="M242.5344 701.5424a34.133333 34.133333 0 0 1 62.1568 28.091733l-1.092267 2.423467-85.333333 170.666667a34.133333 34.133333 0 0 1-62.1568-28.091734l1.092267-2.423466 85.333333-170.666667zM530.875733 686.267733a34.133333 34.133333 0 0 1 44.509867 12.919467l1.28 2.3552 85.333333 170.666667a34.133333 34.133333 0 0 1-59.784533 32.8704l-1.28-2.3552-85.333333-170.666667a34.133333 34.133333 0 0 1 15.274666-45.789867z" fill="#444444" p-id="16727"></path><path d="M512 529.066667v34.133333a34.133333 34.133333 0 0 1-34.133333 34.133333H238.933333a34.133333 34.133333 0 0 1-34.133333-34.133333v-34.133333h307.2z" fill="#00B386" p-id="16728"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web/src/assets/icons/winxin.svg


BIN
web/src/assets/images/login_bg.jpg


BIN
web/src/assets/images/logo.png


+ 9 - 7
web/src/components/table/fieldRender/buttons.vue

@@ -9,7 +9,7 @@
                     @click="onButtonClick(btn)"
                     :class="btn.class"
                     size="small"
-                    class="ba-table-render-buttons-item"
+                    class="ba-table-render-buttons-item buttons-ml-6"
                     :type="btn.type"
                     :loading="btn.loading && btn.loading(row, field)"
                     :disabled="btn.disabled && btn.disabled(row, field)"
@@ -32,7 +32,7 @@
                         @click="onButtonClick(btn)"
                         :class="btn.class"
                         size="small"
-                        class="ba-table-render-buttons-item"
+                        class="ba-table-render-buttons-item buttons-ml-6"
                         :type="btn.type"
                         :loading="btn.loading && btn.loading(row, field)"
                         :disabled="btn.disabled && btn.disabled(row, field)"
@@ -51,7 +51,7 @@
                     @confirm="onButtonClick(btn)"
                 >
                     <template #reference>
-                        <div class="ml-6">
+                        <div class="buttons-popconfirm-reference-box buttons-ml-6">
                             <el-tooltip
                                 :disabled="btn.title ? false : true"
                                 :content="getTranslation(btn.title)"
@@ -87,7 +87,7 @@
                     <el-button
                         :class="btn.class"
                         size="small"
-                        class="ba-table-render-buttons-item move-button"
+                        class="ba-table-render-buttons-item move-button buttons-ml-6"
                         :type="btn.type"
                         :loading="btn.loading && btn.loading(row, field)"
                         :disabled="btn.disabled && btn.disabled(row, field)"
@@ -137,6 +137,9 @@ const getTranslation = (key?: string) => {
 <style scoped lang="scss">
 .ba-table-render-buttons-item {
     .text {
+        font-size: 14px;
+    }
+    .icon + .text {
         padding-left: 5px;
     }
     &.el-button--small {
@@ -147,12 +150,11 @@ const getTranslation = (key?: string) => {
 .ba-table-render-buttons-move {
     cursor: move;
 }
-.ml-6 {
+.buttons-popconfirm-reference-box {
     display: inline-flex;
     vertical-align: middle;
-    margin-left: 6px;
 }
-.ml-6 + .el-button {
+.buttons-ml-6 + .buttons-ml-6 {
     margin-left: 6px;
 }
 </style>

+ 7 - 11
web/src/components/table/header/index.vue

@@ -1,7 +1,7 @@
 <template>
     <!-- 公共搜索 -->
     <transition name="el-zoom-in-bottom" mode="out-in">
-        <ComSearch v-show="props.buttons.includes('comSearch') && baTable.table.showComSearch">
+        <ComSearch v-if="props.buttons.includes('comSearch') && baTable.table.showComSearch">
             <template v-for="(slot, idx) in $slots" :key="idx" #[idx]>
                 <slot :name="idx"></slot>
             </template>
@@ -12,19 +12,19 @@
     <div v-bind="$attrs" class="table-header ba-scroll-style">
         <slot name="refreshPrepend"></slot>
         <el-tooltip v-if="props.buttons.includes('refresh')" :content="t('Refresh')" placement="top">
-            <el-button v-blur @click="onAction('refresh', { loading: true })" color="#40485b" class="table-header-operate" type="info">
+            <el-button v-blur @click="onAction('refresh', { loading: true })" color="#40485b" class="table-header-operate btns-ml-12" type="info">
                 <Icon name="fa fa-refresh" />
             </el-button>
         </el-tooltip>
         <slot name="refreshAppend"></slot>
         <el-tooltip v-if="props.buttons.includes('add') && baTable.auth('add')" :content="t('Add')" placement="top">
-            <el-button v-blur @click="onAction('add')" class="table-header-operate" type="primary">
+            <el-button v-blur @click="onAction('add')" class="table-header-operate btns-ml-12" type="primary">
                 <Icon name="fa fa-plus" />
                 <span class="table-header-operate-text">{{ t('Add') }}</span>
             </el-button>
         </el-tooltip>
         <el-tooltip v-if="props.buttons.includes('edit') && baTable.auth('edit')" :content="t('Edit selected row')" placement="top">
-            <el-button v-blur @click="onAction('edit')" :disabled="!enableBatchOpt" class="table-header-operate" type="primary">
+            <el-button v-blur @click="onAction('edit')" :disabled="!enableBatchOpt" class="table-header-operate btns-ml-12" type="primary">
                 <Icon name="fa fa-pencil" />
                 <span class="table-header-operate-text">{{ t('Edit') }}</span>
             </el-button>
@@ -39,7 +39,7 @@
             :disabled="!enableBatchOpt"
         >
             <template #reference>
-                <div class="mlr-12">
+                <div class="btns-ml-12">
                     <el-tooltip :content="t('Delete selected row')" placement="top">
                         <el-button v-blur :disabled="!enableBatchOpt" class="table-header-operate" type="danger">
                             <Icon name="fa fa-trash" />
@@ -57,7 +57,7 @@
             <el-button
                 v-blur
                 @click="baTable.onTableHeaderAction('unfold', { unfold: !baTable.table.expandAll })"
-                class="table-header-operate"
+                class="table-header-operate btns-ml-12"
                 :type="baTable.table.expandAll ? 'danger' : 'warning'"
             >
                 <span class="table-header-operate-text">{{ baTable.table.expandAll ? t('Shrink all') : t('Expand all') }}</span>
@@ -187,11 +187,7 @@ const onChangeShowColumn = (value: string | number | boolean, field: string) =>
         margin-left: 6px;
     }
 }
-
-.mlr-12 {
-    margin-left: 12px;
-}
-.mlr-12 + .el-button {
+.btns-ml-12 + .btns-ml-12 {
     margin-left: 12px;
 }
 .table-search {

+ 0 - 3
web/src/components/table/index.vue

@@ -230,9 +230,6 @@ defineExpose({
 </script>
 
 <style scoped lang="scss">
-.ba-data-table :deep(.el-button + .el-button) {
-    margin-left: 6px;
-}
 .ba-data-table :deep(.table-header-cell) .cell {
     color: var(--el-text-color-primary);
     overflow: hidden;

+ 1 - 0
web/src/lang/backend/en/module.ts

@@ -74,6 +74,7 @@ export default {
     'Published on': 'Published on:',
     'amount of downloads': 'amount of downloads',
     'Module classification': 'Module classification',
+    'Module documentation': 'Module documentation',
     'Developer Homepage': 'Developer Homepage',
     'Click to access': 'Click to access',
     'Module status': 'Module status',

+ 1 - 0
web/src/lang/backend/zh-cn/module.ts

@@ -71,6 +71,7 @@ export default {
     'Published on': '发布时间',
     'amount of downloads': '下载次数',
     'Module classification': '模块分类',
+    'Module documentation': '模块文档',
     'Developer Homepage': '开发者主页',
     'Click to access': '点击访问',
     'Module status': '模块状态',

+ 30 - 0
web/src/router/frontend.ts

@@ -0,0 +1,30 @@
+import { memberCenterBaseRoutePath } from '/@/router/static/memberCenterBase'
+
+export const frontendRoutes = [
+    //重置密码
+    {
+        path: memberCenterBaseRoutePath + '/reset',
+        name: 'userReset',
+        component: () => import('/@/views/frontend/Reset.vue'),
+    },
+    {
+        path: memberCenterBaseRoutePath + '/register',
+        name: 'userRegister',
+        component: () => import('/@/views/frontend/Register.vue'),
+    },
+    {
+        path: memberCenterBaseRoutePath + '/order/:goodsId',
+        name: 'userOrder',
+        component: () => import('/@/views/frontend/Order.vue'),
+    },
+    {
+        path: memberCenterBaseRoutePath + '/me',
+        name: 'userMe',
+        component: () => import('/@/views/frontend/Me.vue'),
+    },
+    {
+        path: memberCenterBaseRoutePath + '/success',
+        name: 'userSuccess',
+        component: () => import('/@/views/frontend/components/success.vue'),
+    },
+]

+ 2 - 0
web/src/router/static.ts

@@ -1,6 +1,7 @@
 import type { RouteRecordRaw } from 'vue-router'
 import { adminBaseRoutePath } from '/@/router/static/adminBase'
 import { memberCenterBaseRoutePath } from '/@/router/static/memberCenterBase'
+import { frontendRoutes } from '/@/router/frontend'
 
 const pageTitle = (name: string): string => {
     return `pagesTitle.${name}`
@@ -29,6 +30,7 @@ const staticRoutes: Array<RouteRecordRaw> = [
             title: pageTitle('adminLogin'),
         },
     },
+    ...frontendRoutes,
     {
         // 会员登录页
         path: memberCenterBaseRoutePath + '/login',

+ 15 - 11
web/src/utils/router.ts

@@ -101,20 +101,24 @@ export const handleFrontendRoute = (routes: any, menus: any) => {
     const memberCenter = useMemberCenter()
     const viewsComponent = import.meta.glob('/src/views/frontend/**/*.vue')
 
-    if (routes.length) {
-        addRouteAll(viewsComponent, routes, '', true)
-        memberCenter.mergeAuthNode(handleAuthNode(routes, '/'))
-        siteConfig.setHeadNav(handleMenuRule(routes, '/', ['nav']))
-        memberCenter.mergeNavUserMenus(handleMenuRule(routes, '/', ['nav_user_menu']))
+    // 后端返回可能为 undefined/null,统一转成空数组避免长度读取报错
+    const frontendRoutes = Array.isArray(routes) ? routes : []
+    const memberMenus = Array.isArray(menus) ? menus : []
+
+    if (frontendRoutes.length) {
+        addRouteAll(viewsComponent, frontendRoutes, '', true)
+        memberCenter.mergeAuthNode(handleAuthNode(frontendRoutes, '/'))
+        siteConfig.setHeadNav(handleMenuRule(frontendRoutes, '/', ['nav']))
+        memberCenter.mergeNavUserMenus(handleMenuRule(frontendRoutes, '/', ['nav_user_menu']))
     }
-    if (menus.length && isEmpty(memberCenter.state.viewRoutes)) {
-        addRouteAll(viewsComponent, menus, memberCenterBaseRoute.name as string)
+    if (memberMenus.length && isEmpty(memberCenter.state.viewRoutes)) {
+        addRouteAll(viewsComponent, memberMenus, memberCenterBaseRoute.name as string)
         const menuMemberCenterBaseRoute = (memberCenterBaseRoute.path as string) + '/'
-        memberCenter.mergeAuthNode(handleAuthNode(menus, menuMemberCenterBaseRoute))
+        memberCenter.mergeAuthNode(handleAuthNode(memberMenus, menuMemberCenterBaseRoute))
 
-        memberCenter.mergeNavUserMenus(handleMenuRule(menus, '/', ['nav_user_menu']))
-        memberCenter.setShowHeadline(menus.length > 1)
-        memberCenter.setViewRoutes(handleMenuRule(menus, menuMemberCenterBaseRoute))
+        memberCenter.mergeNavUserMenus(handleMenuRule(memberMenus, '/', ['nav_user_menu']))
+        memberCenter.setShowHeadline(memberMenus.length > 1)
+        memberCenter.setViewRoutes(handleMenuRule(memberMenus, menuMemberCenterBaseRoute))
     }
 }
 

+ 212 - 0
web/src/views/backend/application/combo/index.vue

@@ -0,0 +1,212 @@
+<template>
+    <!-- 外层容器,保持默认的表格布局框架 -->
+    <div class="default-main ba-table-box">
+        <!-- 备注提示,当有 remark 时展示 -->
+        <el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
+
+        <!-- 顶部筛选与新增区域,左侧输入与下拉,右侧添加按钮 -->
+         <!-- 表格顶部菜单 -->
+        <TableHeader :buttons="['refresh', 'columnDisplay']"
+            :quick-search-placeholder="t('Quick search placeholder', { fields: '产品名称' })">
+            <!-- 套餐名称输入框,支持清空 -->
+            <el-input
+                class="filter-item"
+                v-model="searchForm.name"
+                placeholder="套餐名称"
+                clearable
+                @input="handleFilter"
+                @clear="handleFilter"
+            />
+            <!-- 库存状态选择框,模拟原型的状态下拉 -->
+            <el-select
+                class="filter-item"
+                v-model="searchForm.inventory_status"
+                placeholder="库存状态"
+                clearable
+                @change="handleFilter"
+                @clear="handleFilter"
+            >
+                <el-option
+                    v-for="item in inventoryOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                />
+            </el-select>
+            <!-- 右侧添加按钮 -->
+            <el-button type="primary" @click="onAdd">添加</el-button>
+        </TableHeader>
+        <!-- 表格主体,使用通用 Table 组件并自定义操作列 -->
+        <Table>
+            <!-- 自定义操作列插槽,呈现编辑/删除/详情 -->
+            <template #operate>
+                <el-table-column prop="operate" align="center" label="操作" min-width="180">
+                    <template #default="scope">
+                        <el-button  type="primary" size="small" @click="onEdit(scope.row)">编辑</el-button>
+                        <el-button  type="danger" size="small" @click="del(scope.row)">删除</el-button>
+                        <el-button style="color: #fff;"  color="#9ca0fa" size="small" @click="viewDetail(scope.row)">套餐详情</el-button>
+                    </template>
+                </el-table-column>
+            </template>
+        </Table>
+    </div>
+</template>
+
+<script setup lang="ts">
+// 引入响应式工具与生命周期钩子
+import { onMounted, provide, reactive } from 'vue'
+// 引入表格封装类
+import baTableClass from '/@/utils/baTable'
+// 引入通用表格组件
+import Table from '/@/components/table/index.vue'
+import TableHeader from '/@/components/table/header/index.vue'
+// 引入接口封装
+import { baTableApi } from '/@/api/common'
+// 引入国际化
+import { useI18n } from 'vue-i18n'
+// 引入消息提示
+import { ElMessage } from 'element-plus'
+
+// 组件名称定义
+defineOptions({
+    name: 'application/combo',
+})
+
+// 国际化方法
+const { t } = useI18n()
+
+// 筛选表单数据,包含套餐名称与库存状态
+const searchForm = reactive({
+    name: '', // 套餐名称关键词
+    inventory_status: '', // 库存状态筛选值
+})
+
+// 库存状态选项,映射下拉显示
+const inventoryOptions = [
+    { label: '充足', value: '1' },
+    { label: '即将售罄', value: '2' },
+    { label: '售罄', value: '3' },
+]
+
+// 静态模拟数据,来源于原型图
+const baseData = [
+    {
+        id: 1,
+        name: '聚德神笔',
+        inventory_status: '1',
+        remaining_stock: 120,
+        sold_quantity: 12,
+        create_time: '2025-10-19',
+    },
+    {
+        id: 2,
+        name: '棱镜',
+        inventory_status: '2',
+        remaining_stock: 12,
+        sold_quantity: 88,
+        create_time: '2025-10-19',
+    },
+    {
+        id: 3,
+        name: '套装示例',
+        inventory_status: '3',
+        remaining_stock: 0,
+        sold_quantity: 120,
+        create_time: '2025-10-19',
+    },
+]
+
+// 初始化表格实例,配置列信息
+const baTable = new baTableClass(
+    new baTableApi('/admin/application.combo'),
+    {
+        column: [
+            { label: '编号', prop: 'id', align: 'center', width: 70 },
+            { label: '套餐名称', prop: 'name', align: 'center', minWidth: 140 },
+            {
+                label: '库存状态',
+                prop: 'inventory_status',
+                align: 'center',
+                render: 'tag',
+                custom: { '1': 'primary', '2': 'warning', '3': 'danger' },
+                replaceValue: { '1': '充足', '2': '即将售罄', '3': '售罄' },
+                effect: 'plain',
+                minWidth: 120,
+            },
+            { label: '剩余库存', prop: 'remaining_stock', align: 'center', minWidth: 100 },
+            { label: '已卖数量', prop: 'sold_quantity', align: 'center', minWidth: 100 },
+            { label: '创建时间', prop: 'create_time', align: 'center', minWidth: 160 },
+            { label: '操作', align: 'center', render: 'slot', slotName: 'operate', operator: false, minWidth: 180 },
+        ],
+        dblClickNotEditColumn: [undefined],
+    },
+    {
+        defaultItems: {
+            name: '',
+            inventory_status: '1',
+            remaining_stock: 0,
+            sold_quantity: 0,
+        },
+    }
+)
+
+// 覆写获取数据方法,基于筛选条件过滤静态数据
+baTable.getData = () => {
+    baTable.table.loading = true // 开启加载状态
+    return new Promise<void>((resolve) => {
+        setTimeout(() => {
+            // 根据输入与下拉值过滤数据
+            const filtered = baseData.filter((item) => {
+                const matchName = searchForm.name ? item.name.includes(searchForm.name.trim()) : true
+                const matchStatus = searchForm.inventory_status ? item.inventory_status === searchForm.inventory_status : true
+                return matchName && matchStatus
+            })
+            baTable.table.data = filtered // 写入表格数据
+            baTable.table.total = filtered.length // 写入总数
+            baTable.table.loading = false // 关闭加载状态
+            resolve() // 完成 Promise
+        }, 200)
+    })
+}
+
+// 删除动作,展示提示
+const del = (row:TableRow) => {
+    ElMessage.success(`已删除套餐:${row.name}`)
+}
+
+// 编辑动作,展示提示
+const onEdit = (row:TableRow ) => {
+    ElMessage.info(`编辑套餐:${row.name}`)
+}
+
+// 新增动作,展示提示
+const onAdd = () => {
+    ElMessage.info('点击添加新套餐')
+}
+
+// 套餐详情动作,展示提示
+const viewDetail = (row: Record<string, any>) => {
+    ElMessage.success(`查看套餐详情:${row.name}`)
+}
+
+// 输入或选择变化时触发过滤
+const handleFilter = () => {
+    baTable.getData()
+}
+
+// 注入表格实例供子组件使用
+provide('baTable', baTable)
+
+// 初始化挂载表格并拉取数据
+onMounted(() => {
+    baTable.mount()
+    baTable.getData()
+})
+</script>
+
+<style scoped lang="scss">
+.filter-item {
+    width: 180px;
+    margin: 0 6px;
+}
+</style>

+ 128 - 0
web/src/views/backend/application/combo/popupForm.vue

@@ -0,0 +1,128 @@
+<template>
+	<!-- 对话框表单 -->
+	<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :destroy-on-close="true" v-model="dialogVisible"
+		@close="handleClose" width="600px">
+		<template #header>
+			<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+				
+			</div>
+		</template>
+		<el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
+			<div class="ba-operate-form">
+				<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" class="form-container">
+					<el-form-item label="套餐名称" prop="name">
+						<el-input v-model="formData.name" placeholder="请输入套餐名称" />
+					</el-form-item>
+				</el-form>
+			</div>
+		</el-scrollbar>
+		<template #footer>
+			<div class="dialog-footer">
+				<el-button @click="handleClose" size="large" class="cancel-btn">{{ t('Cancel') }}</el-button>
+				<el-button @click="onSubmit()" type="primary" size="large" :loading="loading" class="submit-btn">
+					<el-icon>
+						<Check />
+					</el-icon>
+					确定
+				</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
+import { Check } from '@element-plus/icons-vue';
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+
+// 显示隐藏
+const dialogVisible = ref(false)
+// 类型:add、edit
+const dialogType = ref('add')
+
+const loading = ref(false)
+
+const formRef = ref<FormInstance>()
+
+const emits = defineEmits(['success'])
+
+
+const formData = reactive({
+	name: '',
+	specificationsList:[],
+})
+
+
+
+// 表单验证规则
+const rules: FormRules = {
+	name: [
+		{ required: true, message: '请输入套餐名称', trigger: 'change' }
+	],
+	specificationsList: [
+		{ required: true, message: '请选择规格', trigger: 'blur' }
+	]
+}
+
+// 打开对话框
+const openDialog = () => {
+	dialogVisible.value = true
+	dialogType.value = 'add'
+	resetForm()
+
+}
+
+// 编辑
+const editDialog = (row: TableRow) => {
+	dialogVisible.value = true
+	dialogType.value = 'edit'
+}
+
+// 重置表单
+const resetForm = () => {
+	formRef.value?.clearValidate()
+}
+
+// 提交表单
+const onSubmit = async () => {
+	if (!formRef.value) return
+
+	try {
+		await formRef.value.validate()
+		loading.value = true
+
+		// 构建提交数据
+		const submitData = {
+			name: formData.name,
+			specificationsList: formData.specificationsList
+		}
+
+
+		emits('success')
+		handleClose()
+	} catch (error) {
+		ElMessage({
+			type: 'error',
+			message: '账户添加失败,请重试!',
+		})
+	} finally {
+		loading.value = false
+	}
+}
+
+// 关闭弹框
+const handleClose = () => {
+	dialogVisible.value = false
+	resetForm()
+}
+
+// 导出方法
+defineExpose({ openDialog,editDialog });
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 242 - 0
web/src/views/backend/application/product/index.vue

@@ -0,0 +1,242 @@
+<template>
+    <div class="default-main ba-table-box">
+        <el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info"
+            show-icon />
+
+        <!-- 表格顶部菜单 -->
+        <TableHeader :buttons="['refresh', 'quickSearch', 'columnDisplay']"
+            :quick-search-placeholder="t('Quick search placeholder', { fields: '产品名称' })">
+            <el-button type="primary" @click="onAdd">添加</el-button>
+        </TableHeader>
+
+        <!-- 表格 -->
+        <!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+        <Table>
+            <template #status>
+                <el-table-column prop="status" align="center" label="状态" min-width="100">
+                    <template #default="scope">
+                        <el-switch v-model="scope.row.status" 
+                            active-value="1" inactive-value="0" />
+                    </template>
+                </el-table-column>
+            </template>
+            <template #operate>
+                <el-table-column prop="operate" align="center" label="操作" min-width="150">
+                    <template #default="scope">
+                        <el-button type="primary" size="small" @click="onEdit(scope.row)">编辑</el-button>
+                        <el-button type="danger" size="small" @click="del(scope.row)">删除</el-button>
+                    </template>
+                </el-table-column>
+            </template>
+        </Table>
+
+        <!-- 表单 -->
+        <PopupForm />
+    </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, provide } from 'vue'
+import { cloneDeep } from 'lodash-es'
+import baTableClass from '/@/utils/baTable'
+import Table from '/@/components/table/index.vue'
+import TableHeader from '/@/components/table/header/index.vue'
+import PopupForm from './popupForm.vue'
+import { baTableApi } from '/@/api/common'
+import { useI18n } from 'vue-i18n'
+
+type TableRow = Record<string, any>
+
+defineOptions({
+    name: 'application/product',
+})
+
+const { t } = useI18n()
+
+const baTable = new baTableClass(
+    new baTableApi('/admin/auth.Admin/'),
+    {
+        column: [
+            { label: '编号', prop: 'id', align: 'center', width: 70 },
+            {
+                label: '产品名称',
+                prop: 'name',
+                align: 'center',
+                minWidth: 150
+            },
+            {
+                label: '产品图片',
+                prop: 'image',
+                align: 'center',
+                render: 'image',
+                operator: false,
+                minWidth: 100
+            },
+            {
+                label: '关联套餐',
+                prop: 'package',
+                align: 'center',
+                minWidth: 150
+            },
+            {
+                label: '库存状态',
+                prop: 'inventory_status',
+                align: 'center',
+                render: 'tag',
+                custom: {
+                    '1': 'primary',
+                    '2': 'warning',
+                    '3': 'danger'
+                },
+                replaceValue: {
+                    '1': '充足',
+                    '2': '即将售罄',
+                    '3': '售罄'
+                },
+                effect: 'dark',
+                minWidth: 120
+            },
+            {
+                label: '产品状态',
+                prop: 'status',
+                align: 'center',
+                render: 'slot',
+                slotName: 'status',
+                minWidth: 100
+            },
+            {
+                label: '产品类型',
+                prop: 'type',
+                render: 'tag',
+                custom: {
+                    '1': 'primary',
+                    '2': 'success',
+                },
+                replaceValue: {
+                    '1': '网页',
+                    '2': '软件',
+                },
+                align: 'center',
+                minWidth: 130
+            },
+            {
+                label: '产品描述',
+                prop: 'description',
+                align: 'center',
+                showOverflowTooltip: true,
+                minWidth: 200
+            },
+            {
+                label: '创建时间',
+                prop: 'create_time',
+                align: 'center',
+                minWidth: 160
+            },
+            {
+                label: '操作',
+                align: 'center',
+                render: 'slot',
+                slotName: 'operate',
+                operator: false,
+            },
+        ],
+        dblClickNotEditColumn: [undefined,],
+    },
+    {
+        defaultItems: {
+            id: undefined,
+            name: '',
+            package: '',
+            description: '',
+            image: '',
+            inventory_status: '1',
+            status: '1',
+            type: '1',
+            link: '',
+        },
+    }
+)
+
+// 设置假数据
+baTable.getData = () => {
+    baTable.table.loading = true
+    // 模拟异步请求
+    return new Promise<void>((resolve) => {
+        setTimeout(() => {
+            baTable.table.data = [
+                {
+                    id: 1,
+                    name: '聚德神笔',
+                    image: 'https://picsum.photos/200/300',
+                    package: '聚德神笔套餐A',
+                    inventory_status: '1',
+                    status: '1',
+                    description: '这是聚德神笔',
+                    create_time: '2025-10-13 10:00:00',
+                    type: 1,
+                },
+                {
+                    id: 2,
+                    name: '棱镜',
+                    image: 'https://picsum.photos/300',
+                    package: '棱镜套餐B',
+                    inventory_status: '2',
+                    status: '0',
+                    description: '这是棱镜',
+                    create_time: '2025-10-13 11:00:00',
+                    type: 2,
+                },
+                {
+                    id: 3,
+                    name: '测试产品',
+                    image: 'https://picsum.photos/100',
+                    package: '测试套餐2',
+                    inventory_status: '3',
+                    status: '0',
+                    description: '我是描述内容',
+                    create_time: '2025-10-13 11:00:00',
+                    type: 1,
+                },
+                {
+                    id: 4,
+                    name: '测试产品',
+                    image: 'https://picsum.photos/400',
+                    package: '测试套餐',
+                    inventory_status: '1',
+                    status: '1',
+                    description: '这是一个测试产品',
+                    create_time: '2025-10-14 09:00:00',
+                    type: 2,
+                },
+            ]
+            baTable.table.total = 4
+            baTable.table.loading = false
+            resolve()
+        }, 300)
+    })
+}
+
+provide('baTable', baTable)
+
+const del = (row: TableRow) => {
+    baTable.onTableAction('delete', { row })
+}
+
+const openForm = (type: 'Add' | 'Edit', row?: TableRow) => {
+    if (type === 'Edit' && !row) return
+    baTable.form.items = row ? cloneDeep(row) : cloneDeep(baTable.form.defaultItems)
+    baTable.form.operate = type
+    baTable.form.operateIds = row && baTable.table.pk ? [row[baTable.table.pk]] : []
+}
+
+const onAdd = () => openForm('Add')
+const onEdit = (row: TableRow) => openForm('Edit', row)
+
+onMounted(() => {
+    baTable.mount()
+    baTable.getData()
+})
+</script>
+
+<style scoped lang="scss"></style>
+

+ 152 - 0
web/src/views/backend/application/product/popupForm.vue

@@ -0,0 +1,152 @@
+<template>
+    <!-- 对话框表单 -->
+    <el-dialog
+        class="ba-operate-dialog"
+        :close-on-click-modal="false"
+        :model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
+        @close="baTable.toggleForm"
+        :destroy-on-close="true"
+    >
+        <template #header>
+            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+                {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+            </div>
+        </template>
+        <el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
+            <div
+                class="ba-operate-form"
+                :class="'ba-' + baTable.form.operate + '-form'"
+                :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
+            >
+                <el-form
+                    ref="formRef"
+                    @keyup.enter="baTable.onSubmit(formRef)"
+                    :model="baTable.form.items"
+                    :label-position="config.layout.shrink ? 'top' : 'right'"
+                    :label-width="baTable.form.labelWidth + 'px'"
+                    :rules="rules"
+                    v-if="!baTable.form.loading"
+                >
+                    <FormItem
+                        label="产品名称"
+                        prop="name"
+                        v-model="baTable.form.items!.name"
+                        type="string"
+                        :placeholder="t('Please input field', { field: '产品名称' })"
+                    />
+                    <el-form-item label="关联套餐" prop="package">
+                        <el-select
+                            v-model="baTable.form.items!.package"
+                            placeholder="请选择关联套餐"
+                            style="width: 100%"
+                        >
+                            <el-option
+                                v-for="item in packageOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            />
+                        </el-select>
+                    </el-form-item>
+                    <FormItem
+                        label="产品图片"
+                        prop="image"
+                        type="image"
+                        v-model="baTable.form.items!.image"
+                    />
+                    <FormItem
+                        label="产品状态"
+                        prop="status"
+                        type="radio"
+                        v-model="baTable.form.items!.status"
+                        :input-attr="{ border: true, content: statusOptions }"
+                    />
+                    <FormItem
+                        label="产品类型"
+                        prop="type"
+                        type="radio"
+                        v-model="baTable.form.items!.type"
+                        :input-attr="{ border: true, content: typeOptions }"
+                    />
+                    <el-form-item label="产品链接" prop="link">
+                        <el-input
+                            v-model="baTable.form.items!.link"
+                            placeholder="请输入产品链接"
+                        />
+                    </el-form-item>
+                    
+                    <el-form-item label="产品描述" prop="description">
+                        <el-input
+                            v-model="baTable.form.items!.description"
+                            type="textarea"
+                            :rows="4"
+                            placeholder="请输入产品描述"
+                        />
+                    </el-form-item>
+                </el-form>
+            </div>
+        </el-scrollbar>
+        <template #footer>
+            <div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
+                <el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
+                <el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
+                    {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, inject,  useTemplateRef } from 'vue'
+import { useI18n } from 'vue-i18n'
+import type baTableClass from '/@/utils/baTable'
+import FormItem from '/@/components/formItem/index.vue'
+import type { FormItemRule } from 'element-plus'
+import { useConfig } from '/@/stores/config'
+import { buildValidatorData } from '/@/utils/validate'
+
+const config = useConfig()
+const formRef = useTemplateRef('formRef')
+const baTable = inject('baTable') as baTableClass
+
+const { t } = useI18n()
+
+const rules: Partial<Record<string, FormItemRule[]>> = reactive({
+    name: [buildValidatorData({ name: 'required', title: '产品名称' })],
+    package: [buildValidatorData({ name: 'required', trigger: 'change', title: '关联套餐' })],
+    description: [buildValidatorData({ name: 'required', title: '产品描述' })],
+    image: [buildValidatorData({ name: 'required', title: '产品图片' })],
+    type: [buildValidatorData({ name: 'required', title: '产品类型' })],
+    link: [buildValidatorData({ name: 'required', title: '产品链接' })],
+    inventory_status: [buildValidatorData({ name: 'required', title: '库存状态' })],
+    status: [buildValidatorData({ name: 'required', title: '产品状态' })],
+})
+
+const packageOptions = [
+    { label: '聚德神笔A套餐', value: '聚德神笔A套餐' },
+    { label: '聚德神笔B套餐', value: '聚德神笔B套餐' },
+    { label: '企业定制套餐', value: '企业定制套餐' },
+]
+
+const typeOptions = {
+    '1': '网页',
+    '2': '软件',
+}
+
+const inventoryOptions = {
+    '1': '充足',
+    '2': '即将售罄',
+    '3': '售罄',
+}
+
+const statusOptions = {
+    '1': '启用',
+    '0': '禁用',
+}
+
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 4 - 4
web/src/views/backend/dashboard.vue

@@ -153,7 +153,7 @@ import { useEventListener, useTemplateRefsList, useTransition } from '@vueuse/co
 import * as echarts from 'echarts'
 import { CSSProperties, nextTick, onActivated, onBeforeMount, onMounted, onUnmounted, reactive, toRefs, watch } from 'vue'
 import { useI18n } from 'vue-i18n'
-import { index } from '/@/api/backend/dashboard'
+// import { index } from '/@/api/backend/dashboard'
 import coffeeSvg from '/@/assets/dashboard/coffee.svg'
 import headerSvg from '/@/assets/dashboard/header-1.svg'
 import { useAdminInfo } from '/@/stores/adminInfo'
@@ -204,9 +204,9 @@ const statisticValueStyle: CSSProperties = {
     fontSize: '28px',
 }
 
-index().then((res) => {
-    state.remark = res.data.remark
-})
+// index().then((res) => {
+//     state.remark = res.data.remark
+// })
 
 const initCountUp = () => {
     // 虚拟数据

+ 28 - 5
web/src/views/backend/module/components/goodsInfo.vue

@@ -43,15 +43,32 @@
                             <div class="basic-item-title">{{ t('module.Module classification') }}</div>
                             <div class="basic-item-content">{{ state.goodsInfo.category ? state.goodsInfo.category.name : '-' }}</div>
                         </div>
+                        <div class="basic-item">
+                            <div class="basic-item-title">{{ t('module.Module documentation') }}</div>
+                            <div class="basic-item-content">
+                                <el-link
+                                    type="primary"
+                                    class="basic-item-link"
+                                    v-if="state.goodsInfo.docs"
+                                    target="_blank"
+                                    :href="`https://doc.buildadmin.com/md/${state.goodsInfo.docs.name ? state.goodsInfo.docs.name : state.goodsInfo.docs.id}`"
+                                    rel="noopener noreferrer"
+                                >
+                                    {{ t('module.Click to access') }}
+                                </el-link>
+                                <span v-else>-</span>
+                            </div>
+                        </div>
                         <div class="basic-item">
                             <div class="basic-item-title">{{ t('module.Developer Homepage') }}</div>
                             <div class="basic-item-content">
                                 <el-link
                                     type="primary"
-                                    class="developer-homepage"
+                                    class="basic-item-link"
                                     v-if="state.goodsInfo.author_url"
                                     target="_blank"
                                     :href="state.goodsInfo.author_url"
+                                    rel="noopener noreferrer"
                                 >
                                     {{ t('module.Click to access') }}
                                 </el-link>
@@ -398,6 +415,12 @@ const onUpdate = (uid: string, order: number) => {
             align-items: center;
             justify-content: center;
         }
+        :deep(.el-carousel__indicators) {
+            line-height: 10px;
+            .el-carousel__indicator {
+                padding: 0 var(--el-carousel-indicator-padding-horizontal);
+            }
+        }
     }
     .goods-basic {
         position: relative;
@@ -410,7 +433,7 @@ const onUpdate = (uid: string, order: number) => {
         .basic-item {
             display: flex;
             align-items: center;
-            padding: 5px 0;
+            padding: 4px 0;
             .basic-item-title {
                 font-size: var(--el-font-size-base);
                 color: var(--el-text-color-secondary);
@@ -430,8 +453,7 @@ const onUpdate = (uid: string, order: number) => {
         }
         .basic-buttons {
             position: absolute;
-            bottom: 26px;
-            padding-top: 3px;
+            bottom: 15px;
         }
         .basic-button-demo {
             margin-right: 10px;
@@ -513,7 +535,7 @@ const onUpdate = (uid: string, order: number) => {
     .el-carousel__item:nth-child(2n) {
         background-color: #99a9bf;
     }
-    .developer-homepage {
+    .basic-item-link {
         font-size: var(--el-font-size-small);
     }
 }
@@ -583,6 +605,7 @@ const onUpdate = (uid: string, order: number) => {
         }
         .goods-basic .basic-buttons {
             position: unset;
+            padding-top: 3px;
         }
     }
     .goods-detail {

+ 290 - 0
web/src/views/frontend/Me.vue

@@ -0,0 +1,290 @@
+<template>
+  <div class="client-me">
+    <Header />
+    <!-- 用户模块 -->
+    <div class="user-box">
+      <div class="me-container">
+        <!-- 左侧导航菜单 -->
+        <div class="nav-sidebar">
+          <el-radio-group
+            v-model="activeMenu"
+            class="nav-radio-group"
+            @change="handleMenuChange"
+          >
+          <el-radio-button
+            v-for="item in menuList"
+            :key="item.value"
+            :value="item.value"
+            class="nav-menu-item"
+          >
+            <el-icon class="menu-icon">
+              <component :is="item.icon" />
+            </el-icon>
+            <span class="menu-label">{{ item.label }}</span>
+          </el-radio-button>
+          </el-radio-group>
+        </div>
+
+        <!-- 右侧内容区域 -->
+        <div class="content-area">
+          <div class="content-wrapper">
+            <!-- 使用动态组件根据选中的菜单项显示不同的组件 -->
+            <component :is="currentComponent" />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, markRaw, reactive, ref, type Component } from 'vue'
+import { Goods, HomeFilled, OfficeBuilding, ShoppingCartFull, User } from '@element-plus/icons-vue'
+import Header from './components/header.vue'
+// 导入各个功能组件
+import Info from './components/Me/info.vue'
+import PurchaseHistory from './components/Me/purchaseHistory.vue'
+import PurchaseProduct from './components/Me/purchaseProduct.vue'
+import Subordinates from './components/Me/subordinates.vue'
+import Enterprise from './components/Me/enterprise.vue'
+
+defineOptions({ name: 'ClientMe' })
+
+type MenuItem = { label: string; value: string; component: Component; icon: Component }
+
+const activeMenu = ref('home')
+const menuList = reactive<MenuItem[]>([
+  { label: '我的主页', value: 'home', component: markRaw(Info), icon: markRaw(HomeFilled) },
+  { label: '购买记录', value: 'purchase-history', component: markRaw(PurchaseHistory), icon: markRaw(ShoppingCartFull) },
+  { label: '我的产品', value: 'purchase-product', component: markRaw(PurchaseProduct), icon: markRaw(Goods) },
+  { label: '我的下级', value: 'subordinates', component: markRaw(Subordinates), icon: markRaw(User) },
+  { label: '我的企业', value: 'enterprise', component: markRaw(Enterprise), icon: markRaw(OfficeBuilding) }
+])
+
+const currentMenuLabel = computed(() => {
+  const current = menuList.find(item => item.value === activeMenu.value)
+  return current ? current.label : ''
+})
+
+const currentComponent = computed(() => {
+  const current = menuList.find(item => item.value === activeMenu.value)
+  return current ? current.component : Info
+})
+
+const handleMenuChange = (value: any) => {
+}
+</script>
+
+<style scoped lang="scss">
+.client-me {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #f5f7fa 0%, #fafbfc 100%);
+}
+
+.user-box {
+  width: 100%;
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px;
+  box-sizing: border-box;
+}
+
+.me-container {
+  display: flex;
+  gap: 20px;
+  min-height: calc(100vh - 100px);
+
+  // 移动端改为垂直布局
+  @media (max-width: 768px) {
+    flex-direction: column;
+    gap: 15px;
+  }
+}
+
+// 左侧导航菜单
+.nav-sidebar {
+  width: 220px;
+  flex-shrink: 0;
+  background-color: #fff;
+  border-radius: 8px;
+  padding: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  height: fit-content;
+
+  // 移动端全宽显示,横向滚动
+  @media (max-width: 768px) {
+    width: 100%;
+    padding: 10px;
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
+  }
+}
+
+// 单选框组样式
+.nav-radio-group {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+
+  // 移动端改为横向滚动
+  @media (max-width: 768px) {
+    flex-direction: row;
+    overflow-x: auto;
+    overflow-y: hidden;
+    -webkit-overflow-scrolling: touch;
+    padding-bottom: 5px;
+
+    // 隐藏滚动条但保持滚动功能
+    &::-webkit-scrollbar {
+      height: 4px;
+    }
+
+    &::-webkit-scrollbar-track {
+      background: #f1f1f1;
+      border-radius: 2px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #c1c1c1;
+      border-radius: 2px;
+
+      &:hover {
+        background: #a8a8a8;
+      }
+    }
+  }
+}
+
+// 菜单项样式
+::v-deep .nav-menu-item {
+  width: 100%;
+  margin-bottom: 0;
+  margin-right: 0;
+  display: block;
+
+  // 移动端样式调整
+  @media (max-width: 768px) {
+    margin-bottom: 0;
+    margin-right: 8px;
+    flex-shrink: 0;
+    min-width: 100px;
+    width: auto;
+  }
+
+  // 单选框按钮样式
+  .el-radio-button__inner {
+    width: 100%;
+    padding: 14px 16px;
+    border: none;
+    border-radius: 8px;
+    background-color: #fff;
+    color: #333;
+    font-size: 14px;
+    text-align: left;
+    transition: all 0.3s ease;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+
+    // 移动端样式
+    @media (max-width: 768px) {
+      text-align: center;
+      justify-content: center;
+      margin-bottom: 0;
+      margin-right: 8px;
+      padding: 12px 16px;
+      white-space: nowrap;
+      flex-direction: column;
+    }
+
+    &:hover {
+      background-color: #f5f7fa;
+      color: #409EFF;
+      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+    }
+  }
+
+  // 选中状态样式 - 蓝色渐变背景,白色文字
+  &.is-active .el-radio-button__inner {
+    background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
+    color: #fff;
+    font-weight: 500;
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
+
+    .menu-icon {
+      color: #fff;
+    }
+
+    &:hover {
+      background: linear-gradient(135deg, #66b1ff 0%, #85c1ff 100%);
+      box-shadow: 0 4px 16px rgba(64, 158, 255, 0.5);
+    }
+  }
+
+  // 菜单图标样式
+  .menu-icon {
+    font-size: 18px;
+    margin-right: 10px;
+    color: #606266;
+    transition: all 0.3s ease;
+
+    @media (max-width: 768px) {
+      margin-right: 0;
+      margin-bottom: 4px;
+      font-size: 20px;
+    }
+  }
+
+  // 菜单文字样式
+  .menu-label {
+    flex: 1;
+
+    @media (max-width: 768px) {
+      font-size: 12px;
+    }
+  }
+
+  // 移除默认的边框和单选框样式
+  .el-radio-button__original-radio {
+    display: none;
+  }
+
+  // 移除Element UI默认的边框连接效果
+  &:first-child .el-radio-button__inner {
+    border-radius: 4px;
+  }
+
+  &:last-child .el-radio-button__inner {
+    border-radius: 4px;
+    margin-bottom: 0;
+
+    @media (max-width: 768px) {
+      margin-bottom: 0;
+    }
+  }
+}
+
+// 右侧内容区域
+.content-area {
+  flex: 1;
+  background-color: #fff;
+  border-radius: 4px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+
+  @media (max-width: 768px) {
+    width: 100%;
+  }
+}
+
+.content-wrapper {
+  padding: 24px;
+  min-height: 500px;
+
+  @media (max-width: 768px) {
+    padding: 16px;
+    min-height: 400px;
+  }
+}
+</style>

+ 532 - 0
web/src/views/frontend/Order.vue

@@ -0,0 +1,532 @@
+<template>
+  <div class="client-order">
+    <Header />
+
+    <div class="order-body">
+      <!-- 上半部分:商品信息 + 支付信息 -->
+      <el-card class="goods-panel" shadow="hover">
+        <div class="goods-layout">
+          <!-- 左侧:大图区域 -->
+          <div class="goods-cover">
+            <div class="cover-image">
+              <img :src="goodsInfo.cover" alt="商品封面">
+            </div>
+          </div>
+
+          <!-- 右侧:详情区域 -->
+          <div class="goods-detail">
+            <!-- 标题与标签 -->
+            <div class="goods-header">
+              <h2 class="goods-title">
+                {{ goodsInfo.name }}
+              </h2>
+              <div class="goods-tags">
+                <el-tag type="success" size="small">{{ goodsInfo.deliveryType }}</el-tag>
+                <el-tag size="small">已售 {{ goodsInfo.sold }}</el-tag>
+                <el-tag type="warning" size="small">库存 {{ goodsInfo.stockText }}</el-tag>
+                <el-tag type="info" size="small">{{ cardTypeText }}</el-tag>
+              </div>
+            </div>
+
+            <!-- 价格信息 -->
+            <div class="price-section">
+              <div class="price-row">
+                <span class="price-label">售价</span>
+                <div class="price-value">
+                  <span class="currency">¥</span>
+                  <span class="amount">{{ displayPrice }}</span>
+                </div>
+              </div>
+              <div class="price-desc">
+                <span v-if="isSeatBasedEnterprise">
+                  {{ goodsInfo.comboPeopleNum }} 人席位,总价将按 {{ goodsInfo.comboPrice }} 元/席位计算
+                </span>
+                <span v-else>
+                  已选择 {{ buyCount }} 件,自动根据数量合计
+                </span>
+              </div>
+            </div>
+
+            <!-- 购买数量 -->
+            <div
+              v-if="!isSeatBasedEnterprise"
+              class="field-row quantity-row"
+            >
+              <div class="field-label">
+                <span>购买数量</span>
+              </div>
+              <div class="field-content quantity-control">
+                <el-input-number
+                  v-model="buyCount"
+                  :min="1"
+                  :max="goodsInfo.stock"
+                  :step="1"
+                  size="small"
+                  controls-position="right"
+                  class="qty-input-number"
+                />
+                <span class="stock-hint">
+                  库存剩余:{{ goodsInfo.stock }} 件
+                </span>
+              </div>
+            </div>
+
+            <!-- 企业套餐:席位人数 -->
+            <div
+              v-else
+              class="field-row quantity-row"
+            >
+              <div class="field-label">
+                <span>购买席位</span>
+              </div>
+              <div class="field-content quantity-control">
+                <el-input-number
+                  v-model="goodsInfo.comboPeopleNum"
+                  :min="2"
+                  :step="1"
+                  size="small"
+                  controls-position="right"
+                  class="qty-input-number"
+                />
+                <span class="stock-hint">
+                  最少2个席位,当前默认 {{ goodsInfo.comboPeopleNum }} 个
+                </span>
+              </div>
+            </div>
+
+            <!-- 支付方式 -->
+            <div class="field-row pay-row">
+              <div class="field-label">
+                <span>付款</span>
+              </div>
+              <div class="field-content pay-methods">
+                <el-button
+                  size="small"
+                  :type="selectedPayMethod === 'alipay' ? 'primary' : 'default'"
+                  :plain="selectedPayMethod !== 'alipay'"
+                  class="pay-btn"
+                  :class="{ active: selectedPayMethod === 'alipay' }"
+                  @click="selectedPayMethod = 'alipay'"
+                >
+<el-icon class="pay-icon alipay">
+  <Coin />
+                  </el-icon>
+                  支付宝
+                </el-button>
+                <el-button
+                  size="small"
+                  :type="selectedPayMethod === 'wechat' ? 'success' : 'default'"
+                  :plain="selectedPayMethod !== 'wechat'"
+                  class="pay-btn"
+                  :class="{ active: selectedPayMethod === 'wechat' }"
+                  @click="selectedPayMethod = 'wechat'"
+                >
+                  <el-icon class="pay-icon wechat">
+                    <ChatDotRound />
+                  </el-icon>
+                  微信支付
+                </el-button>
+              </div>
+            </div>
+
+            <div class="submit-row">
+              <el-button
+                type="primary"
+                size="large"
+                class="submit-btn"
+                @click="handleSubmit"
+              >
+                立即下单
+              </el-button>
+            </div>
+          </div>
+        </div>
+      </el-card>
+
+      <!-- 创建企业弹框组件 -->
+      <CreateEnterpriseDialog
+        v-model:visible="createEnterpriseVisible"
+        @success="handleEnterpriseCreated"
+      />
+
+      <!-- 下半部分:宝贝详情 -->
+      <el-card class="detail-panel" shadow="never">
+        <template #header>
+          <div class="detail-header">
+            <span class="detail-title">宝贝详情</span>
+          </div>
+        </template>
+
+        <div class="detail-content">
+          <p v-for="(line, index) in goodsInfo.detailLines" :key="index">
+            {{ line }}
+          </p>
+        </div>
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { ChatDotRound, Coin } from '@element-plus/icons-vue'
+import Header from './components/header.vue'
+import CreateEnterpriseDialog from './components/CreateEnterpriseDialog.vue'
+
+defineOptions({ name: 'ClientOrder' })
+
+const route = useRoute()
+
+const goodsId = ref(null)
+const goodsInfo = reactive({
+  name: '惊鸿·日卡',
+  price: '25',
+  sold: 318,
+  stockText: '即将售罄',
+  stock: 5,
+  deliveryType: '自动激活',
+  cover: 'https://picsum.photos/900/400',
+  type: 2, // 1: 个人 2: 企业
+  cardType: 4, // 1: 日卡 2: 周卡 3: 月卡 4: 季卡 5: 年卡 , 6: 按次数 , 7: 按积分
+  comboPeopleNum: 5, // 默认席位
+  comboPrice: 5, // 每席位单价
+  detailLines: [
+    '该卡可续杯 2 次',
+    '该卡可绑定一台设备',
+    '未激活前永久有效',
+    '激活后自下单起 7 日内有效',
+    '下单激活后不予退款,请确认好需要再下单',
+    '在 Iron 工具上使用视为激活',
+    '使用教程:请参考商家提供的使用说明链接'
+  ]
+})
+
+const buyCount = ref(1)
+const hasEnterprise = ref(false)
+const createEnterpriseVisible = ref(false)
+const selectedPayMethod = ref('alipay')
+
+const isEnterprise = computed(() => Number(goodsInfo.type) === 2)
+const isSeatBasedEnterprise = computed(() => {
+  if (!isEnterprise.value) return false
+  const cardType = Number(goodsInfo.cardType)
+  return ![6, 7].includes(cardType)
+})
+
+const cardTypeText = computed(() => {
+  const map = {
+    1: '日卡',
+    2: '周卡',
+    3: '月卡',
+    4: '季卡',
+    5: '年卡',
+    6: '按次数',
+    7: '按积分'
+  }
+  const label = map[Number(goodsInfo.cardType)] || '未知类型'
+  return isEnterprise.value ? `企业·${label}` : `个人·${label}`
+})
+
+const displayPrice = computed(() => {
+  if (isSeatBasedEnterprise.value) {
+    return (Number(goodsInfo.comboPrice) * Number(goodsInfo.comboPeopleNum)).toFixed(2)
+  }
+  return (Number(goodsInfo.price) * buyCount.value).toFixed(2)
+})
+
+const getGoodsInfo = () => {
+  goodsId.value = route.params?.goodsId
+}
+
+const placePersonalOrder = () => {
+  ElMessage.success(`已下单 ${buyCount.value} 件个人套餐`)
+}
+
+const placeEnterpriseOrder = () => {
+  if (isSeatBasedEnterprise.value) {
+    ElMessage.success(`已为企业下单 ${goodsInfo.comboPeopleNum} 个席位`)
+  } else {
+    ElMessage.success(`已为企业下单 ${buyCount.value} 件套餐`)
+  }
+}
+
+const openCreateEnterpriseDialog = () => {
+  createEnterpriseVisible.value = true
+}
+
+const handleEnterpriseCreated = () => {
+  hasEnterprise.value = true
+}
+
+const handleSubmit = () => {
+  if (isEnterprise.value) {
+    if (!hasEnterprise.value) {
+      ElMessage.warning('请先创建企业后再下单')
+      openCreateEnterpriseDialog()
+      return
+    }
+    placeEnterpriseOrder()
+  } else {
+    placePersonalOrder()
+  }
+}
+
+onMounted(() => {
+  getGoodsInfo()
+})
+</script>
+
+<style scoped lang="scss">
+.client-order {
+  min-height: 100vh;
+  background: #f5f7fa;
+  display: flex;
+  flex-direction: column;
+}
+
+.order-body {
+  width: 60%;
+  margin: 24px auto 40px;
+}
+
+.goods-panel {
+  border-radius: 16px;
+  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
+  border: none;
+}
+
+.goods-layout {
+  display: flex;
+  gap: 32px;
+}
+
+.goods-cover {
+  flex: 5;
+
+  .cover-image {
+    width: 100%;
+    border-radius: 14px;
+    overflow: hidden;
+    background: #f2f6fc;
+
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      display: block;
+    }
+  }
+}
+
+.goods-detail {
+  flex: 4;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.goods-header {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.goods-title {
+  margin: 0;
+  font-size: 22px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.goods-tags {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+
+  .favorite-icon {
+    font-size: 18px;
+    color: #f56c6c;
+    cursor: pointer;
+  }
+}
+
+.price-section {
+  padding: 12px 16px;
+  border-radius: 12px;
+  background: linear-gradient(90deg, rgba(255, 255, 255, 0.9), #fff7f7);
+  border: 1px solid #fde2e2;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.price-row {
+  display: flex;
+  align-items: baseline;
+  gap: 12px;
+}
+
+.price-desc {
+  font-size: 12px;
+  color: #909399;
+}
+
+.price-label {
+  font-size: 14px;
+  color: #909399;
+}
+
+.price-value {
+  display: flex;
+  align-items: baseline;
+  color: #f56c6c;
+
+  .currency {
+    font-size: 18px;
+    margin-right: 2px;
+  }
+
+  .amount {
+    font-size: 28px;
+    font-weight: 600;
+  }
+}
+
+.field-row {
+  display: flex;
+  align-items: center;
+  margin-top: 4px;
+}
+
+.field-label {
+  width: 64px;
+  font-size: 13px;
+  color: #909399;
+  flex-shrink: 0;
+}
+
+.field-content {
+  flex: 1;
+}
+
+.quantity-row {
+  margin-top: 8px;
+}
+
+.quantity-control {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+
+  .qty-input-number {
+    width: 140px;
+  }
+
+  .stock-hint {
+    font-size: 12px;
+    color: #909399;
+  }
+}
+
+.pay-row {
+  margin-top: 8px;
+}
+
+.pay-methods {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+
+  .pay-icon {
+    margin-right: 4px;
+  }
+
+  .pay-btn {
+    transition: all 0.2s ease;
+
+    &.active .pay-icon {
+      color: #fff;
+    }
+  }
+
+  .wechat {
+    color: #37c460;
+  }
+
+  .alipay {
+    color: #1677ff;
+  }
+
+  .balance {
+    color: #e6a23c;
+  }
+}
+
+.submit-row {
+  margin-top: 12px;
+
+  .submit-btn {
+    width: 100%;
+    border-radius: 22px;
+  }
+}
+
+.detail-panel {
+  margin-top: 24px;
+  border-radius: 16px;
+  border: none;
+}
+
+.detail-header {
+  display: flex;
+  align-items: center;
+}
+
+.detail-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #303133;
+}
+
+.detail-content {
+  line-height: 1.8;
+  font-size: 14px;
+  color: #606266;
+
+  p {
+    margin: 0 0 4px;
+  }
+}
+
+@media (max-width: 992px) {
+  .order-body {
+    width: 94%;
+  }
+
+  .goods-layout {
+    flex-direction: column;
+  }
+}
+
+@media (max-width: 768px) {
+  .order-body {
+    width: 100%;
+    padding: 0 16px;
+  }
+
+  .goods-panel {
+    padding: 12px !important;
+  }
+
+  .goods-title {
+    font-size: 18px;
+  }
+
+  .price-value .amount {
+    font-size: 22px;
+  }
+}
+</style>

+ 342 - 0
web/src/views/frontend/Register.vue

@@ -0,0 +1,342 @@
+<template>
+  <div class="client-register">
+    <div class="client-register__panel">
+      <div class="client-register__header">
+        <div>
+          <h2>注册账户</h2>
+          <p>用邮箱完成注册即可开始使用</p>
+        </div>
+        <div class="client-register__header-link">
+          已有账号?<router-link to="/user/login">立即登录</router-link>
+        </div>
+      </div>
+      <el-form
+        ref="registerFormRef"
+        :model="registerForm"
+        :rules="registerRules"
+        label-position="top"
+        class="client-register__form"
+      >
+        <el-form-item label="联系人姓名" prop="name">
+          <el-input v-model.trim="registerForm.name" placeholder="请输入联系人姓名" clearable />
+        </el-form-item>
+
+        <el-form-item label="邮箱" prop="email">
+          <el-input v-model.trim="registerForm.email" placeholder="用于接收验证码及通知" clearable />
+        </el-form-item>
+
+        <el-form-item label="邮箱验证码" prop="emailCode">
+          <div class="client-register__code-row">
+            <el-input
+              class="client-register__code-input"
+              v-model.trim="registerForm.emailCode"
+              placeholder="请输入6位验证码"
+              maxlength="6"
+            />
+            <el-button
+              class="client-register__code-btn"
+              type="primary"
+              :loading="sendingCode"
+              :disabled="codeDisabled"
+              @click="handleSendEmailCode"
+            >
+              {{ codeButtonText }}
+            </el-button>
+          </div>
+        </el-form-item>
+
+        <el-form-item label="登录密码" prop="password">
+          <el-input v-model="registerForm.password" type="password" placeholder="至少设置6位密码,包含字母或数字" show-password />
+        </el-form-item>
+
+        <el-form-item label="确认密码" prop="confirmPassword">
+          <el-input v-model="registerForm.confirmPassword" type="password" placeholder="再次输入密码" show-password />
+        </el-form-item>
+
+        <el-form-item label="邀请码(选填)" prop="inviteCode">
+          <el-input v-model.trim="registerForm.inviteCode" placeholder="没有可留空" clearable />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" class="client-register__submit" :loading="submitting" @click="handleRegister">
+            注册并登录
+          </el-button>
+        </el-form-item>
+        <p class="client-register__agreement">
+          @2019-2025 成都聚德鼎盛文化传播有限公司 蜀ICP备19030658号-1
+        </p>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed, onBeforeUnmount, reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { checkIn } from '/@/api/frontend/user/index'
+import { sendEms } from '/@/api/common'
+import { uuid } from '/@/utils/random'
+import clickCaptcha from '/@/components/clickCaptcha'
+import { useUserInfo } from '/@/stores/userInfo'
+
+defineOptions({ name: 'ClientRegister' })
+
+const router = useRouter()
+const userInfo = useUserInfo()
+
+const registerFormRef = ref(null)
+const registerForm = reactive({
+  name: '',
+  email: '',
+  emailCode: '',
+  password: '',
+  confirmPassword: '',
+  inviteCode: '',
+  captchaId: uuid(),
+  captchaInfo: ''
+})
+
+const validateConfirmPassword = (rule, value, callback) => {
+  if (!value) {
+    callback(new Error('请再次输入密码'))
+  } else if (value !== registerForm.password) {
+    callback(new Error('两次输入的密码不一致'))
+  } else {
+    callback()
+  }
+}
+
+const registerRules = reactive({
+  name: [{ required: true, message: '请输入联系人姓名', trigger: 'blur' }],
+  email: [
+    { required: true, message: '请输入企业邮箱', trigger: 'blur' },
+    { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
+  ],
+  emailCode: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
+  ],
+  confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
+  inviteCode: []
+})
+
+const submitting = ref(false)
+const sendingCode = ref(false)
+const countdown = ref(0)
+const countdownTimer = ref(null)
+
+const codeDisabled = computed(() => sendingCode.value || countdown.value > 0)
+const codeButtonText = computed(() => {
+  if (sendingCode.value) return '发送中...'
+  return countdown.value > 0 ? `${countdown.value}s 后重试` : '发送验证码'
+})
+
+const clearCountdown = () => {
+  if (countdownTimer.value) {
+    clearInterval(countdownTimer.value)
+    countdownTimer.value = null
+  }
+}
+
+const startCountdown = seconds => {
+  countdown.value = seconds
+  clearCountdown()
+  countdownTimer.value = setInterval(() => {
+    if (countdown.value <= 1) {
+      clearCountdown()
+      countdown.value = 0
+    } else {
+      countdown.value -= 1
+    }
+  }, 1000)
+}
+
+const handleRegister = () => {
+  if (!registerFormRef.value) return
+  registerFormRef.value.validate(async valid => {
+    if (!valid) return
+    submitting.value = true
+    try {
+      const payload = {
+        tab: 'register',
+        registerType: 'email',
+        name: registerForm.name.trim(),
+        email: registerForm.email.trim(),
+        username: registerForm.email.trim(),
+        password: registerForm.password,
+        captcha: registerForm.emailCode.trim(),
+        captchaId: registerForm.captchaId,
+        captchaInfo: registerForm.captchaInfo,
+        inviteCode: registerForm.inviteCode.trim() || undefined
+      }
+
+      const res = await checkIn('post', payload)
+
+      // 注册成功后直接填充用户信息并跳转
+      if (res?.data?.userInfo) {
+        userInfo.dataFill(res.data.userInfo, false)
+      }
+
+      if (res?.data?.routePath) {
+        router.push({ path: res.data.routePath })
+      } else {
+        router.push({ path: '/user/home' })
+      }
+    } catch (error) {
+      ElMessage.error(error?.message || '注册失败,请稍后重试')
+      // 注册失败后重置验证码ID以防止重复验证
+      registerForm.captchaId = uuid()
+    } finally {
+      submitting.value = false
+    }
+  })
+}
+
+const handleSendEmailCode = () => {
+  if (codeDisabled.value || !registerFormRef.value) return
+  registerFormRef.value.validateField('email', async error => {
+    if (error) return
+    // 发送验证码前先进行点选验证码验证
+    clickCaptcha(registerForm.captchaId, async (captchaInfo) => {
+      sendingCode.value = true
+      registerForm.captchaInfo = captchaInfo
+      try {
+        await sendEms(registerForm.email.trim(), 'user_register', {
+          captchaInfo,
+          captchaId: registerForm.captchaId
+        })
+        ElMessage.success('验证码发送成功,请查收邮箱')
+        startCountdown(60)
+      } catch (err) {
+        ElMessage.error(err?.message || '验证码发送失败')
+        // 失败时刷新点击验证码ID
+        registerForm.captchaId = uuid()
+      } finally {
+        sendingCode.value = false
+      }
+    })
+  })
+}
+
+onBeforeUnmount(() => {
+  clearCountdown()
+})
+</script>
+
+<style scoped>
+.client-register {
+	min-height: 100vh;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 48px 16px;
+	background: url('/@/assets/images/login_bg.jpg') center center / cover no-repeat;
+}
+
+.client-register__panel {
+	width: 520px;
+	max-width: 100%;
+	padding: 28px 36px 24px;
+	background: #fff;
+	border-radius: 16px;
+	box-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
+}
+
+.client-register__header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 20px;
+}
+
+.client-register__header h2 {
+	margin: 0;
+	font-size: 22px;
+	color: #303133;
+}
+
+.client-register__header p {
+	margin: 6px 0 0;
+	color: #909399;
+}
+
+.client-register__header-link {
+	font-size: 14px;
+	color: #606266;
+}
+
+.client-register__header-link a {
+	color: #409eff;
+	margin-left: 4px;
+}
+
+.client-register__form ::v-deep .el-form-item__label {
+	font-weight: 500;
+	color: #303133;
+}
+
+.client-register__form :deep(.el-form-item) {
+	margin-bottom: 18px;
+}
+
+.client-register__form :deep(.el-input) {
+	width: 100%;
+}
+
+.client-register__code-row {
+	display: flex;
+	align-items: stretch;
+	width: 100%;
+}
+
+.client-register__code-input {
+	flex: 1;
+}
+
+.client-register__code-btn {
+	white-space: nowrap;
+	margin-left: -1px;
+	min-width: 128px;
+	border-radius: 0 8px 8px 0;
+}
+
+.client-register__code-input :deep(.el-input__wrapper) {
+	border-radius: 8px 0 0 8px;
+}
+
+.client-register__submit {
+	width: 100%;
+	height: 44px;
+	font-size: 15px;
+}
+
+.client-register__agreement {
+	margin: -8px 0 0;
+	font-size: 12px;
+	color: #909399;
+	text-align: center;
+}
+
+.client-register__agreement a {
+	color: #409eff;
+	text-decoration: none;
+}
+
+@media (max-width: 600px) {
+	.client-register {
+		padding: 24px 12px;
+	}
+
+	.client-register__panel {
+		padding: 24px 20px;
+	}
+
+	.client-register__header {
+		flex-direction: column;
+		align-items: flex-start;
+		gap: 6px;
+	}
+}
+</style>

+ 292 - 0
web/src/views/frontend/Reset.vue

@@ -0,0 +1,292 @@
+<template>
+  <div class="client-reset">
+    <div class="client-reset__panel">
+      <div class="client-reset__header">
+        <div>
+          <h2>重置密码</h2>
+          <p>通过邮箱验证码找回并重置登录密码</p>
+        </div>
+        <div class="client-reset__header-link">
+          想起密码了?<router-link to="/user/login">返回登录</router-link>
+        </div>
+      </div>
+
+      <el-form
+        ref="resetFormRef"
+        :model="resetForm"
+        :rules="resetRules"
+        label-position="top"
+        class="client-reset__form"
+      >
+        <el-form-item label="注册邮箱" prop="email">
+          <el-input v-model.trim="resetForm.email" placeholder="请输入注册邮箱" clearable />
+        </el-form-item>
+
+        <el-form-item label="邮箱验证码" prop="emailCode">
+          <div class="client-reset__code-row">
+            <el-input
+              class="client-reset__code-input"
+              v-model.trim="resetForm.emailCode"
+              placeholder="请输入6位验证码"
+              maxlength="6"
+            />
+            <el-button
+              class="client-reset__code-btn"
+              type="primary"
+              :loading="sendingCode"
+              :disabled="codeDisabled"
+              @click="handleSendEmailCode"
+            >
+              {{ codeButtonText }}
+            </el-button>
+          </div>
+        </el-form-item>
+
+        <el-form-item label="新密码" prop="password">
+          <el-input v-model="resetForm.password" type="password" placeholder="至少设置6位密码,包含字母或数字" show-password />
+        </el-form-item>
+
+        <el-form-item label="确认新密码" prop="confirmPassword">
+          <el-input v-model="resetForm.confirmPassword" type="password" placeholder="再次输入新密码" show-password />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" class="client-reset__submit" :loading="submitting" @click="handleReset">
+            确认重置
+          </el-button>
+        </el-form-item>
+        <p class="client-reset__tips">
+          @2019-2025 成都聚德鼎盛文化传播有限公司 蜀ICP备19030658号-1
+        </p>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed, onBeforeUnmount, reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+
+defineOptions({ name: 'ClientReset' })
+
+const router = useRouter()
+
+const resetFormRef = ref(null)
+const resetForm = reactive({
+  email: '',
+  emailCode: '',
+  password: '',
+  confirmPassword: ''
+})
+
+const validateConfirmPassword = (rule, value, callback) => {
+  if (!value) {
+    callback(new Error('请再次输入新密码'))
+  } else if (value !== resetForm.password) {
+    callback(new Error('两次输入的密码不一致'))
+  } else {
+    callback()
+  }
+}
+
+const resetRules = reactive({
+  email: [
+    { required: true, message: '请输入注册邮箱', trigger: 'blur' },
+    { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
+  ],
+  emailCode: [
+    { required: true, message: '请输入验证码', trigger: 'blur' },
+    { min: 6, max: 6, message: '请输入6位验证码', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
+  ],
+  confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }]
+})
+
+const submitting = ref(false)
+const sendingCode = ref(false)
+const countdown = ref(0)
+const countdownTimer = ref(null)
+
+const codeDisabled = computed(() => sendingCode.value || countdown.value > 0)
+const codeButtonText = computed(() => {
+  if (sendingCode.value) return '发送中...'
+  return countdown.value > 0 ? `${countdown.value}s 后重试` : '发送验证码'
+})
+
+const clearCountdown = () => {
+  if (countdownTimer.value) {
+    clearInterval(countdownTimer.value)
+    countdownTimer.value = null
+  }
+}
+
+const startCountdown = seconds => {
+  countdown.value = seconds
+  clearCountdown()
+  countdownTimer.value = setInterval(() => {
+    if (countdown.value <= 1) {
+      clearCountdown()
+      countdown.value = 0
+    } else {
+      countdown.value -= 1
+    }
+  }, 1000)
+}
+
+const handleReset = () => {
+  if (!resetFormRef.value) return
+  resetFormRef.value.validate(async valid => {
+    if (!valid) return
+    submitting.value = true
+    try {
+      // TODO: 接入后端重置密码接口
+      await new Promise(resolve => setTimeout(resolve, 600))
+      ElMessage.success('密码已重置,请使用新密码登录')
+      router.push({ path: '/user/login', query: { email: resetForm.email.trim() }})
+    } catch (error) {
+      ElMessage.error(error?.message || '重置失败,请稍后重试')
+    } finally {
+      submitting.value = false
+    }
+  })
+}
+
+const handleSendEmailCode = () => {
+  if (codeDisabled.value || !resetFormRef.value) return
+  resetFormRef.value.validateField('email', async error => {
+    if (error) return
+    sendingCode.value = true
+    try {
+      // TODO: 接入后端发送重置验证码接口
+      await new Promise(resolve => setTimeout(resolve, 500))
+      ElMessage.success('验证码发送成功,请查收邮箱')
+      startCountdown(60)
+    } catch (err) {
+      ElMessage.error(err?.message || '验证码发送失败')
+    } finally {
+      sendingCode.value = false
+    }
+  })
+}
+
+onBeforeUnmount(() => {
+  clearCountdown()
+})
+</script>
+
+<style scoped>
+.client-reset {
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 48px 16px;
+  background: url('/@/assets/images/login_bg.jpg') center center / cover no-repeat;
+}
+
+.client-reset__panel {
+  width: 520px;
+  max-width: 100%;
+  padding: 28px 36px 24px;
+  background: #fff;
+  border-radius: 16px;
+  box-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
+}
+
+.client-reset__header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.client-reset__header h2 {
+  margin: 0;
+  font-size: 22px;
+  color: #303133;
+}
+
+.client-reset__header p {
+  margin: 6px 0 0;
+  color: #909399;
+}
+
+.client-reset__header-link {
+  font-size: 14px;
+  color: #606266;
+}
+
+.client-reset__header-link a {
+  color: #409eff;
+  margin-left: 4px;
+}
+
+.client-reset__form ::v-deep .el-form-item__label {
+  font-weight: 500;
+  color: #303133;
+}
+
+.client-reset__form :deep(.el-form-item) {
+	margin-bottom: 18px;
+}
+
+.client-reset__form :deep(.el-input) {
+	width: 100%;
+}
+
+.client-reset__code-row {
+	display: flex;
+	align-items: stretch;
+	width: 100%;
+}
+
+.client-reset__code-input {
+	flex: 1;
+}
+
+.client-reset__code-btn {
+	white-space: nowrap;
+	margin-left: -1px;
+	min-width: 128px;
+	border-radius: 0 8px 8px 0;
+}
+
+.client-reset__code-input :deep(.el-input__wrapper) {
+	border-radius: 8px 0 0 8px;
+}
+
+.client-reset__submit {
+  width: 100%;
+  height: 44px;
+  font-size: 15px;
+}
+
+.client-reset__tips {
+  margin: -6px 0 0;
+  font-size: 12px;
+  color: #909399;
+  text-align: center;
+  line-height: 1.6;
+}
+
+@media (max-width: 600px) {
+  .client-reset {
+    padding: 24px 12px;
+  }
+
+  .client-reset__panel {
+    padding: 24px 20px;
+  }
+
+  .client-reset__header {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 6px;
+  }
+}
+</style>
+

+ 567 - 0
web/src/views/frontend/components/AddMemberDialog.vue

@@ -0,0 +1,567 @@
+<template>
+  <el-dialog
+    title="邮箱添加人员"
+    v-model:visible="dialogVisible"
+    width="480px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="true"
+    class="add-member-dialog"
+    @close="handleClose"
+  >
+    <div class="add-member-form">
+      <div class="form-item">
+        <div class="label">
+          <el-icon class="label-icon">
+            <Message />
+          </el-icon>
+          <span>邮箱地址</span>
+        </div>
+        <div class="input-with-button">
+          <el-input
+            v-model="form.email"
+            placeholder="请输入邮箱地址"
+            clearable
+            :disabled="searching"
+            @keyup.enter="handleSearch"
+            @clear="handleClear"
+          >
+            <template #prefix>
+              <el-icon class="input-prefix">
+                <Message />
+              </el-icon>
+            </template>
+          </el-input>
+          <el-button
+            type="primary"
+            :loading="searching"
+            :disabled="!form.email || !form.email.trim()"
+            class="search-btn"
+            @click="handleSearch"
+          >
+            <el-icon v-if="!searching" class="search-icon">
+              <Search />
+            </el-icon>
+            <span>{{ searching ? '搜索中...' : '搜索' }}</span>
+          </el-button>
+        </div>
+        <div v-if="form.email && !isValidEmail" class="error-tip">
+          <el-icon class="error-icon">
+            <WarningFilled />
+          </el-icon>
+          <span>请输入正确的邮箱格式</span>
+        </div>
+      </div>
+
+      <transition name="fade-slide">
+        <div v-if="searchedUser" class="form-item member-info">
+          <div class="label">
+            <el-icon class="label-icon">
+              <User />
+            </el-icon>
+            <span>人员信息</span>
+          </div>
+          <div class="member-card">
+            <div class="member-avatar">
+              <el-icon class="member-avatar-icon">
+                <UserFilled />
+              </el-icon>
+            </div>
+            <div class="member-details">
+              <div class="member-name">
+                <span class="name-label">姓名:</span>
+                <span class="name-value">{{ searchedUser.name }}</span>
+              </div>
+              <div class="member-email">
+                <span class="email-label">邮箱:</span>
+                <span class="email-value">{{ form.email }}</span>
+              </div>
+            </div>
+            <div class="member-status">
+              <el-icon class="status-icon">
+                <SuccessFilled />
+              </el-icon>
+            </div>
+          </div>
+        </div>
+      </transition>
+
+      <div v-if="!searchedUser && form.email && isValidEmail && !searching" class="empty-state">
+        <el-icon class="empty-icon">
+          <Search />
+        </el-icon>
+        <p>点击搜索按钮查找用户</p>
+      </div>
+    </div>
+
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button size="medium" @click="handleCancel">取消</el-button>
+        <el-button
+          type="primary"
+          size="medium"
+          :loading="submitting"
+          :disabled="!searchedUser"
+          @click="handleSubmit"
+        >
+        <el-icon v-if="!submitting" class="submit-icon">
+          <Check />
+        </el-icon>
+          <span>{{ submitting ? '添加中...' : '确认添加' }}</span>
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, getCurrentInstance, reactive, ref, watch } from 'vue'
+import { Check, Message, Search, SuccessFilled, User, UserFilled, WarningFilled } from '@element-plus/icons-vue'
+
+defineOptions({ name: 'AddMemberDialog' })
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  companyId: {
+    type: [Number, String],
+    default: null
+  }
+})
+
+const emit = defineEmits(['update:visible', 'success', 'close'])
+const { proxy } = getCurrentInstance()
+
+const form = reactive({
+  email: ''
+})
+const searchedUser = ref(null)
+const searching = ref(false)
+const submitting = ref(false)
+
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: val => emit('update:visible', val)
+})
+
+const isValidEmail = computed(() => {
+  if (!form.email || !form.email.trim()) {
+    return true
+  }
+  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+  return emailRegex.test(form.email.trim())
+})
+
+watch(
+  () => props.visible,
+  newVal => {
+    if (!newVal) {
+      resetForm()
+    }
+  }
+)
+
+const message = proxy?.$message
+
+const resetForm = () => {
+  form.email = ''
+  searchedUser.value = null
+  searching.value = false
+  submitting.value = false
+}
+
+const handleClear = () => {
+  searchedUser.value = null
+}
+
+const handleSearch = async () => {
+  if (!form.email || !form.email.trim()) {
+    message?.warning('请输入邮箱')
+    return
+  }
+
+  if (!isValidEmail.value) {
+    message?.error('邮箱格式不正确')
+    return
+  }
+
+  searching.value = true
+  try {
+    message?.info('搜索功能待实现,需要调用搜索用户接口')
+    searchedUser.value = null
+  } catch (error) {
+    console.error('搜索用户失败:', error)
+    message?.error('搜索用户失败')
+    searchedUser.value = null
+  } finally {
+    searching.value = false
+  }
+}
+
+const handleSubmit = async () => {
+  if (!searchedUser.value) {
+    message?.warning('请先搜索用户')
+    return
+  }
+
+  if (!props.companyId) {
+    message?.error('企业ID不能为空')
+    return
+  }
+
+  submitting.value = true
+  try {
+    message?.info('添加成员功能待实现,需要调用添加成员接口')
+    emit('success', searchedUser.value)
+    dialogVisible.value = false
+  } catch (error) {
+    console.error('添加成员失败:', error)
+    message?.error('添加成员失败')
+  } finally {
+    submitting.value = false
+  }
+}
+
+const handleCancel = () => {
+  dialogVisible.value = false
+}
+
+const handleClose = () => {
+  resetForm()
+  emit('close')
+}
+</script>
+
+<style scoped lang="scss">
+::v-deep .add-member-dialog {
+  .label-icon,
+  .input-prefix,
+  .search-icon,
+  .error-icon,
+  .member-avatar-icon,
+  .status-icon,
+  .empty-icon,
+  .submit-icon {
+    font-size: 16px;
+  }
+
+  .member-avatar-icon {
+    font-size: 20px;
+  }
+
+  .status-icon {
+    color: #67c23a;
+  }
+
+  .error-icon {
+    color: #f56c6c;
+  }
+
+  .empty-icon {
+    font-size: 22px;
+    color: #909399;
+  }
+
+  .el-dialog {
+    border-radius: 8px;
+    overflow: hidden;
+  }
+
+  .el-dialog__header {
+    padding: 20px 24px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: #fff;
+    border-bottom: none;
+
+    .el-dialog__title {
+      font-size: 18px;
+      font-weight: 600;
+      color: #fff;
+    }
+
+    .el-dialog__close {
+      color: #fff;
+      font-size: 20px;
+
+      &:hover {
+        color: rgba(255, 255, 255, 0.8);
+      }
+    }
+  }
+
+  .el-dialog__body {
+    padding: 24px;
+  }
+
+  .el-dialog__footer {
+    padding: 16px 24px;
+    border-top: 1px solid #ebeef5;
+    background: #fafafa;
+  }
+}
+
+.add-member-form {
+  .form-item {
+    margin-bottom: 24px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    &.member-info {
+      margin-top: 28px;
+      padding-top: 24px;
+      border-top: 1px solid #ebeef5;
+    }
+  }
+
+  .label {
+    display: flex;
+    align-items: center;
+    margin-bottom: 10px;
+    font-size: 14px;
+    color: #303133;
+    font-weight: 500;
+
+    i {
+      margin-right: 6px;
+      font-size: 16px;
+      color: #667eea;
+    }
+
+    span {
+      line-height: 1;
+    }
+  }
+
+  .input-with-button {
+    display: flex;
+    gap: 12px;
+    align-items: flex-start;
+
+    .el-input {
+      flex: 1;
+
+      ::v-deep .el-input__inner {
+        height: 40px;
+        line-height: 40px;
+        border-radius: 6px;
+        border: 1px solid #dcdfe6;
+        transition: all 0.3s ease;
+
+        &:focus {
+          border-color: #667eea;
+          box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
+        }
+      }
+
+      ::v-deep .el-input__prefix {
+        left: 12px;
+        color: #909399;
+      }
+
+      ::v-deep .el-input__inner {
+        padding-left: 36px;
+      }
+    }
+
+    .search-btn {
+      flex-shrink: 0;
+      min-width: 100px;
+      height: 40px;
+      border-radius: 6px;
+      font-weight: 500;
+      transition: all 0.3s ease;
+      box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
+
+      &:hover:not(:disabled) {
+        transform: translateY(-1px);
+        box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
+      }
+
+      &:active:not(:disabled) {
+        transform: translateY(0);
+      }
+
+      i {
+        margin-right: 4px;
+      }
+    }
+  }
+
+  .error-tip {
+    display: flex;
+    align-items: center;
+    margin-top: 8px;
+    font-size: 12px;
+    color: #f56c6c;
+
+    i {
+      margin-right: 4px;
+      font-size: 14px;
+    }
+  }
+
+  .member-card {
+    display: flex;
+    align-items: center;
+    padding: 16px;
+    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+    border-radius: 8px;
+    border: 1px solid #e4e7ed;
+    transition: all 0.3s ease;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+
+    &:hover {
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+      transform: translateY(-2px);
+    }
+
+    .member-avatar {
+      width: 48px;
+      height: 48px;
+      border-radius: 50%;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-right: 16px;
+      flex-shrink: 0;
+      box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+
+      i {
+        font-size: 24px;
+        color: #fff;
+      }
+    }
+
+    .member-details {
+      flex: 1;
+      min-width: 0;
+
+      .member-name,
+      .member-email {
+        display: flex;
+        align-items: center;
+        margin-bottom: 6px;
+        font-size: 14px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+
+        .name-label,
+        .email-label {
+          color: #606266;
+          margin-right: 4px;
+          font-weight: 400;
+        }
+
+        .name-value {
+          font-weight: 600;
+          color: #303133;
+          font-size: 15px;
+        }
+
+        .email-value {
+          color: #606266;
+          word-break: break-all;
+        }
+      }
+    }
+
+    .member-status {
+      margin-left: 12px;
+      flex-shrink: 0;
+
+      .status-icon {
+        font-size: 24px;
+        color: #67c23a;
+        animation: scaleIn 0.3s ease;
+      }
+    }
+  }
+
+  .empty-state {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 40px 20px;
+    text-align: center;
+    color: #909399;
+
+    i {
+      font-size: 48px;
+      margin-bottom: 12px;
+      color: #c0c4cc;
+    }
+
+    p {
+      font-size: 14px;
+      margin: 0;
+    }
+  }
+}
+
+// 过渡动画
+.fade-slide-enter-active,
+.fade-slide-leave-active {
+  transition: all 0.3s ease;
+}
+
+.fade-slide-enter {
+  opacity: 0;
+  transform: translateY(-10px);
+}
+
+.fade-slide-leave-to {
+  opacity: 0;
+  transform: translateY(-10px);
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+
+  .el-button {
+    min-width: 100px;
+    height: 40px;
+    border-radius: 6px;
+    font-weight: 500;
+    transition: all 0.3s ease;
+
+    &.el-button--primary {
+      box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
+
+      &:hover:not(:disabled) {
+        transform: translateY(-1px);
+        box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
+      }
+
+      &:active:not(:disabled) {
+        transform: translateY(0);
+      }
+    }
+
+    i {
+      margin-right: 4px;
+    }
+  }
+}
+
+// 动画
+@keyframes scaleIn {
+  from {
+    transform: scale(0);
+    opacity: 0;
+  }
+  to {
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+</style>
+

+ 176 - 0
web/src/views/frontend/components/CreateEnterpriseDialog.vue

@@ -0,0 +1,176 @@
+<template>
+  <el-dialog
+    title="创建企业"
+    v-model:visible="dialogVisible"
+    width="420px"
+    @close="handleClose"
+  >
+    <div class="enterprise-form">
+      <div class="form-item">
+        <div class="label">当前账号</div>
+        <el-input
+          v-model="form.account"
+          disabled
+        />
+        <p class="hint">注:一个账号只能归属一个企业</p>
+      </div>
+      <div class="form-item">
+        <div class="label required">企业名称</div>
+        <el-input
+          v-model="form.name"
+          placeholder="请输入企业名称"
+          @keyup.enter="handleSubmit"
+        />
+      </div>
+    </div>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleCancel">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleSubmit">确认</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, getCurrentInstance, reactive, ref, watch } from 'vue'
+
+defineOptions({ name: 'CreateEnterpriseDialog' })
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits(['update:visible', 'close'])
+const { proxy } = getCurrentInstance()
+// mock client user info to decouple from vuex
+const mockClientUser = reactive({
+  email: 'demo_client@example.com'
+})
+
+const form = reactive({
+  account: '',
+  name: ''
+})
+const submitting = ref(false)
+
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: val => emit('update:visible', val)
+})
+
+const clientEmail = computed(() => mockClientUser.email || '')
+
+watch(
+  () => props.visible,
+  newVal => {
+    if (newVal) {
+      initForm()
+      ensureClientEmail()
+    } else {
+      resetForm()
+    }
+  }
+)
+
+watch(
+  clientEmail,
+  newVal => {
+    if (newVal && !form.account) {
+      form.account = newVal
+    }
+  },
+  { immediate: true }
+)
+
+const message = proxy?.$message
+
+const initForm = () => {
+  form.name = ''
+  ensureClientEmail()
+}
+
+const resetForm = () => {
+  form.name = ''
+  submitting.value = false
+}
+
+const ensureClientEmail = () => {
+  // use mock email as placeholder until real API is wired
+  form.account = clientEmail.value || 'client@example.com'
+}
+
+const validateForm = () => {
+  if (!form.name || !form.name.trim()) {
+    message?.error('请输入企业名称')
+    return false
+  }
+
+  const trimmedName = form.name.trim()
+  if (trimmedName.length < 2) {
+    message?.error('企业名称至少需要2个字符')
+    return false
+  }
+
+  if (trimmedName.length > 50) {
+    message?.error('企业名称不能超过50个字符')
+    return false
+  }
+
+  return true
+}
+
+const handleSubmit = async () => {
+  if (!validateForm()) {
+    return
+  }
+  await createEnterprise()
+}
+
+const createEnterprise = async () => {
+  // TODO: 接入创建企业接口
+}
+
+const handleCancel = () => {
+  dialogVisible.value = false
+}
+
+const handleClose = () => {
+  resetForm()
+  emit('close')
+}
+</script>
+
+<style scoped lang="scss">
+.enterprise-form {
+  .form-item {
+    margin-bottom: 16px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .label {
+    margin-bottom: 6px;
+    font-size: 13px;
+    color: #606266;
+
+    &.required::after {
+      content: '*';
+      color: #f56c6c;
+      margin-left: 4px;
+    }
+  }
+
+  .hint {
+    margin: 4px 0 0;
+    font-size: 12px;
+    color: #909399;
+  }
+}
+</style>
+

+ 624 - 0
web/src/views/frontend/components/Me/enterprise.vue

@@ -0,0 +1,624 @@
+<template>
+  <div class="me-enterprise">
+    <div class="enterprise-header">
+      <h2 class="enterprise-title">我的企业</h2>
+    </div>
+    <div class="enterprise-content">
+      <!-- 加载状态 -->
+      <div v-if="loading" class="loading-state">
+        <el-icon class="loading-icon">
+          <Loading />
+        </el-icon>
+        <p>加载中...</p>
+      </div>
+
+      <!-- 如果没有企业,显示创建企业提示 -->
+      <div v-else-if="!hasEnterprise" class="empty-state">
+        <div class="empty-icon">
+          <el-icon class="empty-icon__icon">
+            <OfficeBuilding />
+          </el-icon>
+        </div>
+        <p class="empty-text">您还没有加入任何企业</p>
+
+        <el-button type="primary" @click="showCreateDialog = true">
+          创建企业
+        </el-button>
+      </div>
+
+      <!-- 如果有企业,显示企业员工列表 -->
+      <div v-else class="enterprise-list">
+        <!-- 企业信息展示 -->
+        <div class="list-header">
+          <div class="enterprise-info">
+            <div class="enterprise-name">
+              <el-icon class="enterprise-name-icon">
+                <OfficeBuilding />
+              </el-icon>
+              <span class="name-text">{{ currentEnterpriseName }}</span>
+            </div>
+            <p class="hint-text">注:一个账号只能归属一个企业</p>
+          </div>
+          <div class="header-actions">
+            <el-button type="primary" :icon="Plus" @click="showAddMemberDialog = true">
+              添加企业成员
+            </el-button>
+          </div>
+        </div>
+
+        <!-- 员工列表表格 -->
+        <div class="table-section">
+          <el-table
+            ref="employeeTable"
+            :data="employeeList"
+            style="width: 100%"
+            :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '500' }"
+            empty-text="暂无员工数据"
+            @selection-change="handleSelectionChange"
+          >
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column prop="id" label="编号" width="100" align="center" />
+            <el-table-column prop="name" label="姓名" min-width="150" align="center" />
+            <el-table-column prop="email" label="邮箱" min-width="200" align="center" />
+            <el-table-column prop="status" label="状态" width="120" align="center">
+              <template #default="{ row }">
+                <el-tag :type="getStatusType(row.status)" size="small">
+                  {{ getStatusText(row.status) }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="createdAt" label="注册时间" width="180" align="center">
+              <template #default="{ row }">
+                {{ formatDate(row.createdAt || row.created_time || row.createdTime) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="200" align="center" fixed="right">
+              <template #default="{ row }">
+                <el-button
+                  type="primary"
+                  size="small"
+                  @click="handleOpenProductSeatDialog([row.id])"
+                >
+                  产品分配
+                </el-button>
+                <el-button
+                  type="danger"
+                  size="small"
+                  :disabled="row.id === currentUserId"
+                  @click="handleDeleteMember(row)"
+                >
+                  删除
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+
+        <!-- 批量操作工具栏 -->
+        <div v-if="selectedEmployeeIds.length > 0" class="batch-actions">
+          <div class="batch-info">
+            <span>已选择 <strong>{{ selectedEmployeeIds.length }}</strong> 位员工</span>
+          </div>
+          <div class="batch-buttons">
+            <el-button
+              type="primary"
+              size="small"
+              :icon="Goods"
+              @click="handleOpenProductSeatDialog(selectedEmployeeIds)"
+            >
+              批量产品分配
+            </el-button>
+            <el-button
+              size="small"
+              :icon="Close"
+              @click="handleClearSelection"
+            >
+              取消选择
+            </el-button>
+          </div>
+        </div>
+
+        <!-- 分页控件 -->
+        <div v-if="employeeList.length > 0 || total > 0" class="pagination-section">
+          <el-pagination
+            :current-page="currentPage"
+            :page-size="pageSize"
+            :page-sizes="pageSizes"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            background
+            @size-change="handlePageSizeChange"
+            @current-change="handleCurrentPageChange"
+          />
+        </div>
+      </div>
+    </div>
+
+    <!-- 创建企业弹框 -->
+    <CreateEnterpriseDialog
+      v-model:visible="showCreateDialog"
+      @success="handleCreateSuccess"
+      @close="handleDialogClose"
+    />
+
+    <!-- 添加企业成员弹框 -->
+    <AddMemberDialog
+      v-model:visible="showAddMemberDialog"
+      :company-id="currentCompanyId"
+      @success="handleAddMemberSuccess"
+      @close="handleAddMemberDialogClose"
+    />
+
+    <!-- 产品分配席位弹框 -->
+    <ProductSeatDialog
+      v-model:visible="showProductSeatDialog"
+      :member-list="employeeList"
+      :selected-member-ids="selectedMemberIdsForDialog"
+      @success="handleProductSeatSuccess"
+      @close="handleProductSeatDialogClose"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, watch } from 'vue'
+import { storeToRefs } from 'pinia'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Close, Goods, Loading, OfficeBuilding, Plus } from '@element-plus/icons-vue'
+import { useUserInfo } from '/@/stores/userInfo'
+import CreateEnterpriseDialog from '../CreateEnterpriseDialog.vue'
+import AddMemberDialog from '../AddMemberDialog.vue'
+import ProductSeatDialog from '../ProductSeatDialog.vue'
+
+defineOptions({ name: 'MeEnterprise' })
+
+type Employee = {
+  id: number
+  name: string
+  email: string
+  status: number
+  createdAt: string
+  created_time?: string
+  createdTime?: string
+}
+
+type ProductSeatResult = {
+  mode?: 'assign' | 'revoke'
+  memberIds: number[]
+}
+
+const userStore = useUserInfo()
+const { id: userId } = storeToRefs(userStore)
+
+const loading = ref(false)
+const employeeList = ref<Employee[]>([
+  { id: 1, name: '张三', email: 'zhangsan@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
+  { id: 2, name: '李四', email: 'lisi@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
+  { id: 3, name: '王五', email: 'wangwu@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
+  { id: 4, name: '赵六', email: 'zhaoliu@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
+  { id: 5, name: '孙七', email: 'sunqi@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
+  { id: 6, name: '周八', email: 'zhouba@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
+  { id: 7, name: '吴九', email: 'wujiu@example.com', status: 1, createdAt: '2025-01-01 12:00:00' },
+  { id: 8, name: '郑十', email: 'zhengshi@example.com', status: 1, createdAt: '2025-01-01 12:00:00' }
+])
+const showCreateDialog = ref(false)
+const showAddMemberDialog = ref(false)
+const showProductSeatDialog = ref(false)
+const selectedEmployeeIds = ref<number[]>([])
+const selectedMemberIdsForDialog = ref<number[]>([])
+const employeeTable = ref()
+const currentPage = ref(1)
+const pageSize = ref(10)
+const pageSizes = [10, 20, 30, 50]
+const total = ref(1)
+
+// 企业数据占位,后续可通过接口填充
+const companyId = ref<number | string | null>(null)
+const companyName = ref('')
+
+const hasEnterprise = computed(() => {
+  const id = companyId.value
+  if (id === null || id === undefined) return false
+  if (id === '' || id === 0) return false
+  return true
+})
+
+const currentCompanyId = computed(() => companyId.value)
+
+const currentEnterpriseName = computed(() => {
+  if (!hasEnterprise.value) return '我的企业'
+  if (companyName.value) return companyName.value
+  return `企业 (ID: ${companyId.value})`
+})
+
+const currentUserId = computed(() => userId.value || null)
+
+const formatDate = (dateStr?: string) => {
+  if (!dateStr) return '-'
+  const date = new Date(dateStr)
+  if (Number.isNaN(date.getTime())) return dateStr
+
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
+  const hours = String(date.getHours()).padStart(2, '0')
+  const minutes = String(date.getMinutes()).padStart(2, '0')
+
+  return `${year}-${month}-${day} ${hours}:${minutes}`
+}
+
+type TagType = 'success' | 'warning' | 'info' | 'danger' | 'primary'
+
+const getStatusType = (status: number): TagType => {
+  const statusMap: Record<number, TagType> = {
+    1: 'success',
+    2: 'danger'
+  }
+  return statusMap[status] || 'info'
+}
+
+const getStatusText = (status: number) => {
+  const statusMap: Record<number, string> = {
+    1: '启用',
+    2: '禁用'
+  }
+  return statusMap[status] || '未知'
+}
+
+const loadEnterpriseList = async () => {
+  // TODO: 接入实际接口
+  loading.value = false
+}
+
+const initEnterpriseData = async () => {
+  // TODO: 拉取企业数据并填充 companyId/companyName
+  loadEnterpriseList()
+}
+
+const handlePageSizeChange = (val: number) => {
+  pageSize.value = val
+  currentPage.value = 1
+  loadEnterpriseList()
+}
+
+const handleCurrentPageChange = (val: number) => {
+  currentPage.value = val
+  loadEnterpriseList()
+}
+
+const handleCreateSuccess = async () => {
+  // TODO: 创建成功后刷新企业信息
+  currentPage.value = 1
+  loadEnterpriseList()
+  ElMessage.success('企业创建成功')
+}
+
+const handleDialogClose = () => {
+  showCreateDialog.value = false
+}
+
+const handleDeleteMember = async (member: Employee) => {
+  if (!member?.id) {
+    ElMessage.error('成员信息不完整')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(`确定要删除成员 "${member.name || member.email}" 吗?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    // TODO: 调用删除成员 API
+    ElMessage.success('删除成员成功')
+    loadEnterpriseList()
+  } catch {
+    // 用户取消
+  }
+}
+
+const handleAddMemberSuccess = () => {
+  loadEnterpriseList()
+}
+
+const handleAddMemberDialogClose = () => {
+  showAddMemberDialog.value = false
+}
+
+const handleSelectionChange = (selection: Employee[]) => {
+  selectedEmployeeIds.value = selection.map(item => item.id)
+}
+
+const handleClearSelection = () => {
+  employeeTable.value?.clearSelection()
+  selectedEmployeeIds.value = []
+}
+
+const handleOpenProductSeatDialog = (memberIds: number[]) => {
+  if (!memberIds || memberIds.length === 0) {
+    ElMessage.warning('请至少选择一个员工')
+    return
+  }
+  selectedMemberIdsForDialog.value = memberIds
+  showProductSeatDialog.value = true
+}
+
+const handleProductSeatSuccess = (data: ProductSeatResult) => {
+  loadEnterpriseList()
+  handleClearSelection()
+  const count = data?.memberIds?.length || 0
+  ElMessage.success(
+    data.mode === 'assign'
+      ? `成功为 ${count} 位员工分配产品席位`
+      : `成功取消 ${count} 位员工的产品席位`
+  )
+}
+
+const handleProductSeatDialogClose = () => {
+  showProductSeatDialog.value = false
+  selectedMemberIdsForDialog.value = []
+}
+
+watch(
+  companyId,
+  (newVal, oldVal) => {
+    if (newVal && !oldVal) {
+      loadEnterpriseList()
+    }
+  },
+  { immediate: true }
+)
+
+onMounted(() => {
+  initEnterpriseData()
+})
+</script>
+
+<style scoped lang="scss">
+.me-enterprise {
+  width: 100%;
+
+  .enterprise-header {
+    margin-bottom: 20px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid #e4e7ed;
+
+    .enterprise-title {
+      margin: 0;
+      font-size: 20px;
+      font-weight: 500;
+      color: #303133;
+    }
+  }
+
+  .enterprise-content {
+    padding: 20px 0;
+    color: #606266;
+  }
+
+  .empty-state {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+    text-align: center;
+
+    .empty-icon {
+      font-size: 64px;
+      color: #c0c4cc;
+      margin-bottom: 20px;
+
+      .empty-icon__icon {
+        font-size: 64px;
+      }
+    }
+
+    .empty-text {
+      margin: 0 0 30px 0;
+      font-size: 16px;
+      color: #909399;
+    }
+  }
+
+  .loading-state {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+    text-align: center;
+
+    .loading-icon {
+      font-size: 32px;
+      color: #409eff;
+      margin-bottom: 16px;
+      animation: rotating 2s linear infinite;
+    }
+
+    p {
+      margin: 0;
+      font-size: 14px;
+      color: #909399;
+    }
+  }
+
+  .enterprise-list {
+    .list-header {
+      margin-bottom: 20px;
+      padding: 20px 24px;
+      background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
+      border-radius: 12px;
+      border: 1px solid #e4e7ed;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-start;
+      gap: 20px;
+
+      .enterprise-info {
+        display: flex;
+        flex-direction: column;
+        gap: 12px;
+        flex: 1;
+
+        .enterprise-name {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+
+          .enterprise-name-icon {
+            font-size: 20px;
+            color: #409eff;
+          }
+
+          .name-text {
+            font-size: 18px;
+            font-weight: 600;
+            color: #303133;
+          }
+        }
+
+        .hint-text {
+          margin: 0;
+          font-size: 13px;
+          color: #909399;
+        }
+      }
+
+      .header-actions {
+        flex-shrink: 0;
+      }
+    }
+
+    .table-section {
+      margin-bottom: 20px;
+      background: #fff;
+      border-radius: 12px;
+      overflow: hidden;
+      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+      ::v-deep .el-table {
+        border: none;
+
+        &::before {
+          display: none;
+        }
+
+        .el-table__header-wrapper {
+          .el-table__header {
+            th {
+              border-bottom: 2px solid #f0f2f5;
+              border-right: none;
+            }
+          }
+        }
+
+        .el-table__body-wrapper {
+          .el-table__body {
+            tr {
+              td {
+                border-bottom: 1px solid #f5f7fa;
+                border-right: none;
+              }
+
+              &:hover {
+                background-color: #fafbfc;
+              }
+
+              &:last-child {
+                td {
+                  border-bottom: none;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
+    .batch-actions {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 16px 20px;
+      background: linear-gradient(135deg, #ecf5ff 0%, #e1f3ff 100%);
+      border-radius: 12px;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+      margin-bottom: 20px;
+      border: 1px solid #b3d8ff;
+
+      .batch-info {
+        font-size: 14px;
+        color: #606266;
+
+        strong {
+          color: #409eff;
+          font-weight: 600;
+        }
+      }
+
+      .batch-buttons {
+        display: flex;
+        gap: 12px;
+      }
+    }
+
+    .pagination-section {
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+      padding: 16px 20px;
+      background: #fff;
+      border-radius: 12px;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+      margin-top: 20px;
+
+      ::v-deep .el-pagination {
+        .el-pagination__total {
+          color: #606266;
+          font-size: 14px;
+          font-weight: 500;
+        }
+      }
+    }
+  }
+}
+
+@keyframes rotating {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+@media (max-width: 768px) {
+  .me-enterprise {
+    .enterprise-list {
+      .list-header {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 12px;
+
+        .hint-text {
+          width: 100%;
+        }
+
+        .header-actions {
+          width: 100%;
+
+          .el-button {
+            width: 100%;
+          }
+        }
+      }
+
+      .table-section {
+        overflow-x: auto;
+      }
+    }
+  }
+}
+</style>

+ 278 - 0
web/src/views/frontend/components/Me/info.vue

@@ -0,0 +1,278 @@
+<template>
+  <div class="me-info">
+    <!-- 标题 -->
+    <div class="info-header">
+      <h2 class="info-title">我的主页</h2>
+    </div>
+
+    <!-- 欢迎信息 -->
+    <div class="welcome-section">
+      <p class="welcome-text">欢迎回来, {{ userName }}</p>
+    </div>
+
+    <!-- 财务卡片 -->
+    <div class="finance-cards">
+      <div class="finance-card">
+        <div class="card-icon">
+          <el-icon class="card-icon__inner">
+            <Medal />
+          </el-icon>
+        </div>
+        <div class="card-content">
+          <div class="card-label">总收入</div>
+          <div class="card-value">¥{{ commission_total || '0' }}</div>
+        </div>
+      </div>
+      <div class="finance-card">
+        <div class="card-icon">
+          <el-icon class="card-icon__inner">
+            <Coin />
+          </el-icon>
+        </div>
+        <div class="card-content">
+          <div class="card-label">余额</div>
+          <div class="card-value">¥{{ commission_amount || '0' }}</div>
+        </div>
+      </div>
+      <div class="finance-card">
+        <div class="card-icon">
+          <el-icon class="card-icon__inner">
+            <Avatar />
+          </el-icon>
+        </div>
+        <div class="card-content">
+          <div class="card-label">推广人数</div>
+          <div class="card-value">{{ promoters || '0' }}</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 商户信息 -->
+    <div class="info-section">
+      <h3 class="section-title">商户信息</h3>
+      <div class="info-grid">
+        <div class="info-item">
+          <span class="info-label">商户ID:</span>
+          <span class="info-value merchant-id">{{ user_id || '-' }}</span>
+        </div>
+        <div class="info-item full-width">
+          <span class="info-label">推广链接:</span>
+          <div class="promotion-link-wrapper">
+            <span>{{ promotion_link || '-' }}</span>
+            <span class="promotion-desc">(通过该链接消费的人会给您丰厚的佣金)</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue'
+import { Avatar, Coin, Medal } from '@element-plus/icons-vue'
+import { storeToRefs } from 'pinia'
+import { useUserInfo } from '/@/stores/userInfo'
+
+defineOptions({ name: 'MeInfo' })
+
+const userStore = useUserInfo()
+const { username, email, id } = storeToRefs(userStore)
+
+const promoters = ref<string | number>(0)
+const promotion_link = ref('')
+const commission_total = ref<string | number>(0)
+const commission_amount = ref<string | number>(0)
+const user_id = computed(() => id.value || '-')
+
+const userName = computed(() => username.value || email.value)
+
+const getUserInfo = () => {
+  // TODO: 接入实际的用户信息获取逻辑
+}
+
+onMounted(() => {
+  getUserInfo()
+})
+</script>
+
+<style scoped lang="scss">
+.me-info {
+  width: 100%;
+
+  .info-header {
+    margin-bottom: 24px;
+
+    .info-title {
+      margin: 0;
+      font-size: 24px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+
+  .welcome-section {
+    margin-bottom: 30px;
+
+    .welcome-text {
+      margin: 0;
+      font-size: 16px;
+      color: #606266;
+      font-weight: 500;
+    }
+  }
+
+  .info-section {
+    margin-top: 30px;
+    padding: 20px;
+    background-color: #fafafa;
+    border-radius: 8px;
+
+    .section-title {
+      margin: 0 0 20px 0;
+      font-size: 18px;
+      font-weight: 600;
+      color: #303133;
+    }
+
+    .info-grid {
+      display: grid;
+      grid-template-columns: repeat(2, 1fr);
+      gap: 20px;
+
+      @media (max-width: 768px) {
+        grid-template-columns: 1fr;
+        gap: 15px;
+      }
+
+      .info-item {
+        display: flex;
+        align-items: center;
+        flex-wrap: wrap;
+
+        &.full-width {
+          grid-column: 1 / -1;
+        }
+
+        .info-label {
+          font-size: 14px;
+          color: #606266;
+          margin-right: 10px;
+          min-width: 100px;
+          flex-shrink: 0;
+        }
+
+        .info-value {
+          font-size: 14px;
+          color: #303133;
+          font-weight: 500;
+
+          &.merchant-id,
+          &.merchant-key {
+            display: inline-block;
+            padding: 4px 12px;
+            background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
+            color: #fff;
+            border-radius: 4px;
+            font-weight: 600;
+            margin-right: 10px;
+          }
+        }
+
+        .info-value-wrapper {
+          display: flex;
+          align-items: center;
+          flex-wrap: wrap;
+          gap: 10px;
+        }
+
+        .promotion-link-wrapper {
+          display: flex;
+          flex-direction: column;
+          gap: 8px;
+
+          .promotion-link {
+            color: #409EFF;
+            text-decoration: none;
+            font-size: 14px;
+            word-break: break-all;
+
+            &:hover {
+              text-decoration: underline;
+            }
+          }
+
+          .promotion-desc {
+            font-size: 12px;
+            color: #909399;
+          }
+        }
+      }
+    }
+  }
+
+  .finance-cards {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 20px;
+    margin-top: 30px;
+
+    @media (max-width: 1024px) {
+      grid-template-columns: repeat(2, 1fr);
+    }
+
+    @media (max-width: 768px) {
+      grid-template-columns: 1fr;
+    }
+
+    .finance-card {
+      display: flex;
+      align-items: center;
+      padding: 20px;
+      background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
+      border-radius: 8px;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+      transition: all 0.3s ease;
+
+      &:hover {
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+        transform: translateY(-2px);
+      }
+
+      .card-icon {
+        width: 48px;
+        height: 48px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
+        border-radius: 8px;
+        margin-right: 16px;
+        flex-shrink: 0;
+
+        .card-icon__inner {
+          font-size: 24px;
+          color: #fff;
+        }
+      }
+
+      .card-content {
+        flex: 1;
+
+        .card-label {
+          font-size: 14px;
+          color: #909399;
+          margin-bottom: 8px;
+        }
+
+        .card-value {
+          font-size: 20px;
+          font-weight: 600;
+          color: #303133;
+        }
+      }
+    }
+  }
+}
+</style>
+

+ 465 - 0
web/src/views/frontend/components/Me/purchaseHistory.vue

@@ -0,0 +1,465 @@
+<template>
+  <div class="me-purchase-history">
+    <div class="history-header">
+      <div class="header-text">
+        <h2 class="history-title">购买记录</h2>
+        <p class="history-subtitle">实时掌握每一笔订单的支付状态</p>
+      </div>
+    </div>
+
+    <div class="history-filters">
+      <el-input v-model="searchForm.orderId" placeholder="请输入订单号" clearable class="filter-item order-input">
+        <template #prepend>订单号</template>
+      </el-input>
+
+      <el-date-picker
+        v-model="searchForm.dateRange"
+        class="filter-item date-picker"
+        type="daterange"
+        unlink-panels
+        start-placeholder="下单开始时间"
+        end-placeholder="下单结束时间"
+        range-separator="~"
+        value-format="yyyy-MM-dd"
+      />
+
+      <el-button size="small" type="primary" :icon="Search" @click="handleSearch">查询</el-button>
+      <el-button size="small" @click="handleReset">重置</el-button>
+    </div>
+
+    <div class="history-tabs">
+      <el-tabs v-model="activeStatus" @tab-click="handleTabClick">
+        <el-tab-pane
+          v-for="tab in statusTabs"
+          :key="tab.value"
+          :label="`${tab.label} (${getOrderCount(tab.value)})`"
+          :name="tab.value"
+        />
+      </el-tabs>
+    </div>
+
+    <div v-if="filteredOrders.length" class="order-list">
+      <div v-for="order in filteredOrders" :key="order.orderId" class="order-card">
+        <div class="order-card__header">
+          <div class="header-left">
+            <span class="order-id">订单号:{{ order.orderId }}</span>
+          </div>
+          <div class="header-right">
+            <span>下单时间:{{ order.orderTime }}</span>
+          </div>
+        </div>
+
+        <div class="order-card__body">
+          <div class="order-item product-info">
+            <div class="item-label">商品</div>
+            <div class="item-content">
+              <div class="product-name">{{ order.productName }}</div>
+            </div>
+          </div>
+
+          <div v-if="order.type === 1" class="order-item">
+            <div class="item-label">数量</div>
+            <div class="item-value">x{{ order.quantity }}</div>
+          </div>
+          <div v-if="order.type === 2" class="order-item">
+            <div class="item-label">席位</div>
+            <div class="item-value">{{ order.quantity }}个</div>
+          </div>
+
+          <div class="order-item">
+            <div class="item-label">金额</div>
+            <div class="item-value amount">¥{{ order.amount }}</div>
+          </div>
+
+          <div class="order-item">
+            <div class="item-label">支付方式</div>
+            <div class="item-value">{{ order.payMethod }}</div>
+          </div>
+
+          <div class="order-item">
+            <div class="item-label">支付状态</div>
+            <el-tag size="small" :type="order.payStatus === '已支付' ? 'success' : 'warning'">
+              {{ order.payStatus }}
+            </el-tag>
+          </div>
+
+          <div class="order-item">
+            <div class="item-label">发货方式</div>
+            <div class="item-value">自动发货</div>
+          </div>
+
+          <div class="order-item">
+            <div class="item-label">套餐类型</div>
+            <div class="item-value">{{ order.type === 1 ? '个人套餐' : '企业套餐' }}</div>
+          </div>
+
+          <div class="order-item full-width">
+            <div class="item-label">商家留言</div>
+            <div class="item-value note">
+              {{ order.merchantNote }}
+            </div>
+          </div>
+
+          <div class="order-item full-width time-info">
+            <span>下单时间:{{ order.payTime || '-' }}</span>
+            <span>付款时间:{{ order.deliverTime || '-' }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <el-empty v-else class="history-empty" description="暂无符合条件的订单" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+
+defineOptions({ name: 'MePurchaseHistory' })
+
+type OrderRecordStatus = 'paid' | 'unpaid'
+type OrderTabStatus = 'all' | OrderRecordStatus
+type Order = {
+  orderId: string
+  productName: string
+  sku: string
+  quantity: number
+  type: number
+  amount: number
+  payMethod: string
+  payStatus: string
+  merchantNote: string
+  codes: string[]
+  orderTime: string
+  payTime: string
+  deliverTime: string
+  status: OrderRecordStatus
+}
+
+const activeStatus = ref<OrderTabStatus>('all')
+const searchForm = reactive<{ orderId: string; dateRange: string[] }>({
+  orderId: '',
+  dateRange: []
+})
+const appliedFilters = reactive<{ orderId: string; dateRange: string[] }>({
+  orderId: '',
+  dateRange: []
+})
+const statusTabs: { label: string; value: OrderTabStatus }[] = [
+  { label: '全部', value: 'all' },
+  { label: '已支付', value: 'paid' },
+  { label: '未支付', value: 'unpaid' }
+]
+const orders = ref<Order[]>([
+  {
+    orderId: '6952512020095823599',
+    productName: '望舒·月卡',
+    sku: '-',
+    quantity: 1,
+    type: 1,
+    amount: 52,
+    payMethod: '支付宝',
+    payStatus: '已支付',
+    merchantNote: '请前往相对应的网站进行使用工具,具体方法在文档里。激活前永久有效,激活后30天内有效。',
+    codes: ['XCCSYVIPJ7LM85R6IIOYXP7S6TO'],
+    orderTime: '2025-10-01 12:01:02',
+    payTime: '2025-10-01 12:01:02',
+    deliverTime: '2025-10-01 12:01:32',
+    status: 'paid'
+  },
+  {
+    orderId: '4502508151712325808',
+    productName: '清和·季卡',
+    sku: '-',
+    quantity: 1,
+    type: 2,
+    amount: 168,
+    payMethod: '微信',
+    payStatus: '待支付',
+    merchantNote: '如有疑问请联系在线客服,超过24小时自动关闭。',
+    codes: [],
+    orderTime: '2025-09-22 09:36:15',
+    payTime: '',
+    deliverTime: '',
+    status: 'unpaid'
+  },
+  {
+    orderId: '55665111144555',
+    productName: '聚德神笔日卡',
+    sku: 'JD-001-DAY',
+    quantity: 1,
+    type: 1,
+    amount: 6,
+    payMethod: '微信',
+    payStatus: '已支付',
+    merchantNote: '请前往相对应的网站进行使用工具,具体方法在文档里。激活前永久有效,激活后30天内有效。',
+    codes: ['JDSB20251001-0001'],
+    orderTime: '2025-10-01 12:01:02',
+    payTime: '2025-10-01 12:01:02',
+    deliverTime: '2025-10-01 12:01:05',
+    status: 'paid'
+  }
+])
+
+const filteredOrders = computed(() =>
+  orders.value.filter(order => {
+    const matchesStatus = activeStatus.value === 'all' ? true : order.status === activeStatus.value
+    const orderIdFilter = appliedFilters.orderId
+      ? order.orderId.includes(appliedFilters.orderId.trim())
+      : true
+    const dateFilter = isWithinRange(order.orderTime, appliedFilters.dateRange)
+    return matchesStatus && orderIdFilter && dateFilter
+  })
+)
+
+const handleSearch = () => {
+  appliedFilters.orderId = searchForm.orderId
+  appliedFilters.dateRange = [...searchForm.dateRange]
+}
+
+const handleReset = () => {
+  searchForm.orderId = ''
+  searchForm.dateRange = []
+  appliedFilters.orderId = ''
+  appliedFilters.dateRange = []
+}
+
+const handleTabClick = (tab: { paneName?: string | number; name?: string | number }) => {
+  activeStatus.value = (tab.paneName || tab.name || 'all') as OrderTabStatus
+}
+
+const getOrderCount = (status: OrderTabStatus) => {
+  if (status === 'all') return orders.value.length
+  return orders.value.filter(order => order.status === status).length
+}
+
+const isWithinRange = (orderTime: string, range: string[]) => {
+  if (!range || range.length !== 2) return true
+  const [start, end] = range
+  const startDate = new Date(`${start} 00:00:00`)
+  const endDate = new Date(`${end} 23:59:59`)
+  const targetDate = new Date(orderTime.replace(/-/g, '/'))
+  return targetDate >= startDate && targetDate <= endDate
+}
+</script>
+
+<style scoped lang="scss">
+.me-purchase-history {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.history-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 12px;
+
+  .history-title {
+    margin: 0;
+    font-size: 24px;
+    font-weight: 600;
+    color: #1f2f3d;
+  }
+
+  .history-subtitle {
+    margin: 6px 0 0;
+    font-size: 14px;
+    color: #909399;
+  }
+
+  .refresh-btn {
+    border-color: #409EFF;
+  }
+}
+
+.history-filters {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  padding: 16px;
+  background: linear-gradient(135deg, #ecf5ff 0%, #f5f9ff 100%);
+  border-radius: 8px;
+  border: 1px solid #c6e2ff;
+
+  .filter-item {
+    flex: 1;
+    min-width: 200px;
+  }
+
+  .order-input {
+    max-width: 320px;
+  }
+
+  .date-picker {
+    max-width: 360px;
+  }
+}
+
+.history-tabs {
+  background: #fff;
+  border-radius: 8px;
+  padding: 0 16px;
+  box-shadow: 0 4px 16px rgba(64, 158, 255, 0.12);
+
+  ::v-deep .el-tabs__item {
+    font-weight: 500;
+  }
+
+  ::v-deep .el-tabs__active-bar {
+    height: 3px;
+    background: linear-gradient(90deg, #409EFF, #66b1ff);
+  }
+
+  ::v-deep .el-tabs__item.is-active {
+    color: #409EFF;
+  }
+}
+
+.order-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.order-card {
+  background: #fff;
+  border-radius: 12px;
+  border: 1px solid #d9ecff;
+  box-shadow: 0 6px 18px rgba(64, 158, 255, 0.12);
+  overflow: hidden;
+
+  &__header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 10px;
+    padding: 16px 20px;
+    background: linear-gradient(135deg, #ecf5ff, #ffffff);
+  }
+
+  .order-id {
+    font-weight: 600;
+    margin-right: 12px;
+    color: #1f2d3d;
+  }
+
+  .order-card__body {
+    padding: 20px;
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(180px, max-content));
+    gap: 16px 24px;
+    justify-content: flex-start;
+  }
+}
+
+.order-item {
+  display: inline-flex;
+  flex-direction: column;
+  gap: 6px;
+  font-size: 14px;
+  width: max-content;
+  min-width: 0;
+
+  .item-label {
+    color: #909399;
+  }
+
+  .item-value {
+    color: #303133;
+    font-weight: 500;
+  }
+
+  &.product-info {
+    grid-column: span 2;
+    min-width: 220px;
+  }
+
+  &.full-width {
+    grid-column: 1 / -1;
+    width: 100%;
+  }
+
+  .amount {
+    font-size: 18px;
+    color: #409EFF;
+    font-weight: 600;
+  }
+
+  .note {
+    padding: 10px;
+    background: #f0f5ff;
+    border-radius: 6px;
+    color: #606266;
+    line-height: 1.6;
+  }
+
+  .code-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    align-items: center;
+  }
+}
+
+.code-badge {
+  padding: 6px 10px;
+  background: #ecf5ff;
+  border-radius: 6px;
+  font-family: 'Courier New', Courier, monospace;
+  font-size: 13px;
+  color: #2d8cf0;
+  border: 1px dashed #c6e2ff;
+}
+
+.time-info {
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 10px;
+  font-size: 13px;
+  color: #606266;
+  padding-top: 10px;
+  border-top: 1px dashed #d9e2f0;
+}
+
+.history-empty {
+  padding: 60px 0;
+  background: #fff;
+  border-radius: 12px;
+  border: 1px dashed #c6e2ff;
+}
+
+@media (max-width: 768px) {
+  .history-header {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+
+  .history-filters {
+    flex-direction: column;
+
+    .filter-item,
+    .order-input,
+    .date-picker {
+      width: 100%;
+      max-width: none;
+    }
+  }
+
+  .order-card__body {
+    grid-template-columns: 1fr;
+  }
+
+  .order-item.product-info {
+    grid-column: 1 / -1;
+    width: 100%;
+  }
+}
+</style>
+

+ 205 - 0
web/src/views/frontend/components/Me/purchaseProduct.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="me-purchase-product">
+    <div class="product-header">
+      <h2 class="product-title">我的产品</h2>
+    </div>
+    <div class="product-content">
+      <div class="product-grid">
+        <div
+          v-for="item in products"
+          :key="item.id"
+          class="product-card"
+          @click="handleProductClick(item)"
+        >
+          <div class="product-image-wrapper">
+            <img src="https://picsum.photos/200" alt="">
+          </div>
+          <div class="product-info">
+            <div class="product-name">{{ item.name }}</div>
+            <div class="product-desc">{{ item.description }}</div>
+            <div v-if="item.expireAt" class="product-expire">
+              <span class="expire-label">有效期到:</span>
+              <span class="expire-text">{{ item.expireAt }}</span>
+            </div>
+            <div class="product-meta">
+              <span class="product-type">{{ item.typeLabel }}</span>
+              <span v-if="item.tag" class="product-tag">{{ item.tag }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+
+defineOptions({ name: 'MePurchaseProduct' })
+
+type ProductType = 'web' | 'download'
+type Product = {
+  id: string
+  name: string
+  description: string
+  expireAt?: string
+  type: ProductType
+  typeLabel: string
+  link: string
+  tag?: string
+}
+
+const products = ref<Product[]>([
+  {
+    id: 'jude-pen',
+    name: '聚德神笔',
+    description: '聚德 AI 写作/办公套件,可在线使用多种智能工具。',
+    expireAt: '2025-12-30',
+    type: 'web',
+    typeLabel: '在线工具',
+    link: 'https://www.example-tools.com/jude-pen',
+    tag: '网页'
+  },
+  {
+    id: 'lens-tool',
+    name: '棱镜',
+    description: '图像/视频增强助手,需要下载安装客户端使用。',
+    expireAt: '2025-12-30',
+    type: 'download',
+    typeLabel: '软件下载',
+    link: 'https://www.example-tools.com/download/lens-setup.exe',
+    tag: 'Windows'
+  }
+])
+
+const handleProductClick = (item: Product) => {
+  if (!item?.link) return
+
+  if (item.type === 'download') {
+    const a = document.createElement('a')
+    a.href = item.link
+    a.target = '_blank'
+    a.rel = 'noopener noreferrer'
+    a.download = ''
+    document.body.appendChild(a)
+    a.click()
+    document.body.removeChild(a)
+    return
+  }
+
+  window.open(item.link, '_blank', 'noopener,noreferrer')
+}
+</script>
+
+<style scoped lang="scss">
+.me-purchase-product {
+  width: 100%;
+
+  .product-header {
+    margin-bottom: 20px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid #e4e7ed;
+
+    .product-title {
+      margin: 0;
+      font-size: 20px;
+      font-weight: bold;
+      color: #303133;
+    }
+  }
+
+  .product-content {
+    padding: 20px 0;
+    color: #606266;
+
+    .product-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(220px, 260px));
+      gap: 24px;
+    }
+
+    .product-card {
+      cursor: pointer;
+      background: #fff;
+      border-radius: 10px;
+      border: 1px solid #e4e7ed;
+      transition: all 0.2s ease;
+      overflow: hidden;
+
+      &:hover {
+        border-color: #409eff;
+        box-shadow: 0 6px 16px rgba(64, 158, 255, 0.15);
+        transform: translateY(-2px);
+      }
+    }
+
+    .product-image-wrapper {
+      padding: 14px 14px 0;
+
+      img {
+        display: block;
+        width: 100%;
+        height: 160px;
+        border-radius: 6px;
+        object-fit: cover;
+      }
+    }
+
+    .product-info {
+      padding: 12px 16px 16px;
+      display: flex;
+      flex-direction: column;
+      gap: 6px;
+    }
+
+    .product-name {
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+    }
+
+    .product-desc {
+      font-size: 13px;
+      color: #909399;
+      line-height: 1.5;
+      min-height: 38px;
+    }
+
+    .product-expire {
+      margin-top: 2px;
+      font-size: 12px;
+
+      .expire-label {
+        color: #909399;
+      }
+
+      .expire-text {
+        color: #606266;
+      }
+    }
+
+    .product-meta {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-top: 4px;
+      font-size: 12px;
+    }
+
+    .product-type {
+      padding: 3px 8px;
+      border-radius: 10px;
+      background: #ecf5ff;
+      color: #409eff;
+    }
+
+    .product-tag {
+      padding: 3px 8px;
+      border-radius: 10px;
+      background: #fef0f0;
+      color: #f56c6c;
+    }
+  }
+}
+</style>
+

+ 366 - 0
web/src/views/frontend/components/Me/subordinates.vue

@@ -0,0 +1,366 @@
+<template>
+  <div class="me-subordinates">
+    <div class="subordinates-header">
+      <h2 class="subordinates-title">我的下级</h2>
+    </div>
+
+    <div class="subordinates-content">
+      <!-- 过滤/搜索区域 + 汇总指标 -->
+      <div class="filter-section">
+        <div class="filter-left">
+          <el-date-picker
+            v-model="searchForm.dateRange"
+            class="filter-item date-picker"
+            type="daterange"
+            unlink-panels
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            range-separator="~"
+            value-format="yyyy-MM-dd"
+          />
+
+          <el-input
+            v-model="searchForm.name"
+            placeholder="请输入名称"
+            clearable
+            class="filter-item name-input"
+            @keyup.enter="handleSearch"
+          />
+
+          <el-button type="primary" :icon="Search" size="small" class="search-btn" @click="handleSearch">
+            搜索
+          </el-button>
+        </div>
+
+        <!-- 汇总指标 -->
+        <div class="summary-item">
+          <span class="summary-label">分拥余额:</span>
+          <span class="summary-value">
+            {{ totalCommission }}
+          </span>
+        </div>
+      </div>
+
+      <!-- 数据表格 -->
+      <div class="table-section">
+        <el-table
+          :data="tableData"
+          style="width: 100%"
+          :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '500' }"
+        >
+          <el-table-column prop="id" label="ID" width="80" align="center" />
+          <el-table-column prop="userName" label="用户名称" min-width="120" align="center" />
+          <el-table-column prop="email" label="邮箱" min-width="180" align="center" />
+          <el-table-column prop="commissionAmount" label="已得分佣金额" width="140" align="center" />
+          <el-table-column prop="registerTime" label="注册时间" width="180" align="center" />
+          <el-table-column prop="status" label="状态" width="100" align="center">
+            <template #default="{ row }">
+              <span class="status-text">{{ row.status }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="operation" label="操作" width="100" align="center">
+            <template #default>
+              <span class="status-text">详情</span>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <!-- 分页控件 -->
+      <div class="pagination-section">
+        <el-pagination
+          :current-page="currentPage"
+          :page-size="pageSize"
+          :page-sizes="pageSizes"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          background
+          @size-change="handlePageSizeChange"
+          @current-change="handleCurrentPageChange"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+
+defineOptions({ name: 'MeSubordinates' })
+
+type TableRow = {
+  id: number | string
+  userName: string
+  email: string
+  commissionAmount: number
+  registerTime: string
+  status: string
+}
+
+const searchForm = reactive<{ dateRange: string[]; name: string }>({
+  dateRange: ['2025-10-01', '2025-10-01'],
+  name: ''
+})
+const totalCommission = ref(30)
+const tableData = ref<TableRow[]>([
+  { id: '-', userName: '汇总', email: '-', commissionAmount: 30, registerTime: '-', status: '-' },
+  { id: 1, userName: '张三', email: 'XXX.qq.com', commissionAmount: 10, registerTime: '2025-10-01 14:32:11', status: '正常' },
+  { id: 10, userName: '李四', email: 'XXX.qq.com', commissionAmount: 20, registerTime: '2025-10-01 14:32:11', status: '正常' }
+])
+const currentPage = ref(1)
+const pageSize = ref(10)
+const pageSizes = [10, 20, 30, 50]
+const total = ref(1)
+
+const loadData = () => {
+  // TODO: 接入实际 API 获取数据
+  console.log('搜索条件:', { ...searchForm })
+}
+
+const handleSearch = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+const handlePageSizeChange = (val: number) => {
+  pageSize.value = val
+  currentPage.value = 1
+  loadData()
+}
+
+const handleCurrentPageChange = (val: number) => {
+  currentPage.value = val
+  loadData()
+}
+</script>
+
+<style scoped lang="scss">
+.me-subordinates {
+  width: 100%;
+
+  .subordinates-header {
+    margin-bottom: 20px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid #e4e7ed;
+
+    .subordinates-title {
+      margin: 0;
+      font-size: 20px;
+      font-weight: 500;
+      color: #303133;
+    }
+  }
+
+  .subordinates-content {
+    padding: 20px 0;
+    color: #606266;
+  }
+
+  .filter-section {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 16px;
+    margin-bottom: 20px;
+    padding: 20px 24px;
+    background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
+    border-radius: 12px;
+    border: 1px solid #e4e7ed;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+
+    .filter-left {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 12px;
+      flex: 1;
+      min-width: 300px;
+    }
+
+    .filter-item {
+      flex: 1;
+      min-width: 200px;
+    }
+
+    .date-picker {
+      max-width: 360px;
+    }
+
+    .name-input {
+      max-width: 320px;
+    }
+
+    .search-btn {
+      height: 32px;
+      padding: 0 20px;
+      font-weight: 500;
+      box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
+      transition: all 0.3s ease;
+
+      &:hover {
+        box-shadow: 0 4px 8px rgba(64, 158, 255, 0.3);
+        transform: translateY(-1px);
+      }
+
+      &:active {
+        transform: translateY(0);
+      }
+    }
+
+    .summary-item {
+      display: flex;
+      align-items: center;
+      padding: 8px 10px;
+      border-radius: 10px;
+      box-shadow: 0 2px 6px rgba(104, 151, 238, 0.15);
+      white-space: nowrap;
+      flex-shrink: 0;
+
+      .summary-label {
+        color: #8c6e1f;
+        font-size: 14px;
+        font-weight: 500;
+      }
+
+      .summary-value {
+        position: relative;
+        color: #d48806;
+        font-weight: 600;
+        font-size: 20px;
+        letter-spacing: 0.5px;
+
+        .summary-badge {
+          position: absolute;
+          top: -10px;
+          right: -20px;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+          min-width: 20px;
+          height: 20px;
+          padding: 0 5px;
+          background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
+          color: #fff;
+          border-radius: 10px;
+          font-size: 11px;
+          font-weight: 600;
+          line-height: 1;
+          box-shadow: 0 2px 4px rgba(255, 152, 0, 0.4);
+          border: 2px solid #fff;
+        }
+      }
+    }
+  }
+
+  .table-section {
+    margin-bottom: 20px;
+    background: #fff;
+    border-radius: 12px;
+    overflow: hidden;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+    ::v-deep .el-table {
+      border: none;
+
+      &::before {
+        display: none;
+      }
+
+      .el-table__header-wrapper {
+        .el-table__header {
+          th {
+            border-bottom: 2px solid #f0f2f5;
+            border-right: none;
+          }
+        }
+      }
+
+      .el-table__body-wrapper {
+        .el-table__body {
+          tr {
+            td {
+              border-bottom: 1px solid #f5f7fa;
+              border-right: none;
+            }
+
+            &:hover {
+              background-color: #fafbfc;
+            }
+
+            &:last-child {
+              td {
+                border-bottom: none;
+              }
+            }
+          }
+        }
+      }
+
+      .status-text {
+        color: #409eff;
+        font-weight: 500;
+      }
+    }
+  }
+
+  .pagination-section {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    padding: 16px 20px;
+    background: #fff;
+    border-radius: 12px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+
+    ::v-deep .el-pagination {
+      .el-pagination__total {
+        color: #606266;
+        font-size: 14px;
+        font-weight: 500;
+      }
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .me-subordinates {
+    .filter-section {
+      flex-direction: column;
+      align-items: stretch;
+
+      .filter-left {
+        width: 100%;
+      }
+
+      .filter-item,
+      .date-picker,
+      .name-input {
+        width: 100%;
+        max-width: none;
+      }
+
+      .search-btn {
+        width: 100%;
+        margin-top: 8px;
+      }
+
+      .summary-item {
+        width: 100%;
+        justify-content: center;
+      }
+    }
+
+    .pagination-section {
+      justify-content: center;
+
+      ::v-deep .el-pagination {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: center;
+        gap: 8px;
+      }
+    }
+  }
+}
+</style>

+ 1110 - 0
web/src/views/frontend/components/ProductSeatDialog.vue

@@ -0,0 +1,1110 @@
+<template>
+  <el-dialog
+    title="产品分配席位"
+    v-model:visible="dialogVisible"
+    width="900px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="true"
+    class="product-seat-dialog"
+    @close="handleClose"
+  >
+
+    <div class="dialog-content">
+      <!-- 操作模式选择 - 使用标签页样式 -->
+      <div class="mode-section">
+        <div class="mode-tabs">
+          <div
+            class="mode-tab"
+            :class="{ active: operationMode === 'assign' }"
+            @click="operationMode = 'assign'; handleModeChange()"
+          >
+            <el-icon>
+              <Plus />
+            </el-icon>
+            <span>分配席位</span>
+          </div>
+          <div
+            class="mode-tab"
+            :class="{ active: operationMode === 'cancel' }"
+            @click="operationMode = 'cancel'; handleModeChange()"
+          >
+            <el-icon>
+              <Minus />
+            </el-icon>
+            <span>取消分配</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 选择人员 - 卡片式布局 -->
+      <div class="member-section">
+        <div class="section-header">
+          <div class="section-title">
+            <el-icon>
+              <UserFilled />
+            </el-icon>
+            <span>选择人员</span>
+          </div>
+          <div class="section-actions">
+            <el-button type="text" size="small" :icon="Check" @click="handleSelectAll">
+              全选
+            </el-button>
+            <el-button type="text" size="small" :icon="Close" @click="handleSelectNone">
+              清空
+            </el-button>
+          </div>
+        </div>
+        <div class="member-list-wrapper">
+          <div v-if="selectedMembers.length > 0" class="selected-badge">
+            <el-icon>
+              <Check />
+            </el-icon>
+            <span>已选择 <strong>{{ selectedMembers.length }}</strong> 位成员</span>
+          </div>
+          <div class="member-list">
+            <el-checkbox-group v-model="selectedMembers" @change="handleMemberChange">
+              <div
+                v-for="member in memberList"
+                :key="member.id"
+                class="member-card"
+                :class="{ selected: selectedMembers.includes(member.id) }"
+              >
+                <el-checkbox :label="member.id" class="member-checkbox">
+                  <div class="member-avatar">
+                    <el-icon>
+                      <UserFilled />
+                    </el-icon>
+                  </div>
+                  <div class="member-details">
+                    <div class="member-name">{{ member.name }}</div>
+                    <div class="member-email">{{ member.email }}</div>
+                  </div>
+                  <div v-if="selectedMembers.includes(member.id)" class="member-check-icon">
+                    <el-icon>
+                      <Check />
+                    </el-icon>
+                  </div>
+                </el-checkbox>
+              </div>
+            </el-checkbox-group>
+            <div v-if="memberList.length === 0" class="empty-member">
+              <el-icon>
+                <User />
+              </el-icon>
+              <p>暂无人员数据</p>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 选择产品 - 卡片式展示 -->
+      <div v-if="operationMode === 'assign'" class="product-section">
+        <div class="section-header">
+          <div class="section-title">
+            <el-icon>
+              <Goods />
+            </el-icon>
+            <span>选择套餐产品</span>
+          </div>
+        </div>
+        <div class="product-grid">
+          <div
+            v-for="product in availableProducts"
+            :key="product.id"
+            class="product-card"
+            :class="{ active: selectedProduct === product.id }"
+            @click="selectedProduct = product.id"
+          >
+            <div class="product-image">
+              <img :src="getProductImage(product.id)" :alt="product.name">
+            </div>
+            <div class="product-content">
+              <div class="product-name">{{ product.name }}</div>
+              <div v-if="product.description" class="product-desc">{{ product.description }}</div>
+              <div v-if="product.seats !== undefined" class="product-meta">
+                <span class="seats-info">
+                  <el-icon>
+                    <User />
+                  </el-icon>
+                  <span>可用席位: {{ product.seats }}</span>
+                </span>
+              </div>
+            </div>
+            <div v-if="selectedProduct === product.id" class="product-check">
+              <el-icon>
+                <Check />
+              </el-icon>
+            </div>
+          </div>
+          <div v-if="availableProducts.length === 0" class="empty-product">
+            <el-icon>
+              <Goods />
+            </el-icon>
+            <p>暂无可用产品</p>
+          </div>
+        </div>
+      </div>
+
+      <!-- 已分配产品列表(取消分配模式) -->
+      <div v-if="operationMode === 'cancel'" class="assigned-section">
+        <div class="section-header">
+          <div class="section-title">
+            <el-icon>
+              <List />
+            </el-icon>
+            <span>已分配产品</span>
+          </div>
+        </div>
+        <div v-if="assignedProducts.length > 0" class="assigned-product-list">
+          <div
+            v-for="product in assignedProducts"
+            :key="product.id"
+            class="assigned-product-card"
+            :class="{ active: selectedProduct === product.id }"
+            @click="selectedProduct = product.id"
+          >
+            <div class="product-icon assigned">
+              <el-icon>
+                <Goods />
+              </el-icon>
+            </div>
+            <div class="product-content">
+              <div class="product-name">{{ product.name }}</div>
+              <div v-if="product.description" class="product-desc">{{ product.description }}</div>
+              <div class="product-meta">
+                <span class="assigned-count">
+                  <el-icon>
+                    <UserFilled />
+                  </el-icon>
+                  <span>已分配 {{ product.assignedCount || selectedMembers.length }} 人</span>
+                </span>
+              </div>
+            </div>
+            <div v-if="selectedProduct === product.id" class="product-check">
+              <el-icon>
+                <Check />
+              </el-icon>
+            </div>
+          </div>
+        </div>
+        <div v-else-if="selectedMembers.length > 0" class="empty-assigned">
+          <el-icon>
+            <InfoFilled />
+          </el-icon>
+          <p>选中人员暂无已分配的产品</p>
+        </div>
+        <div v-else class="empty-assigned">
+          <el-icon>
+            <InfoFilled />
+          </el-icon>
+          <p>请先选择要操作的人员</p>
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button size="medium" @click="handleCancel">
+          <el-icon>
+            <Close />
+          </el-icon>
+          <span>取消</span>
+        </el-button>
+        <el-button type="primary" size="medium" :loading="submitting" :disabled="!canSubmit" @click="handleSubmit">
+          <el-icon v-if="!submitting">
+            <component :is="operationMode === 'assign' ? Check : Remove" />
+          </el-icon>
+          <span>{{ submitting ? '处理中...' : operationMode === 'assign' ? '确认分配' : '确认取消' }}</span>
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, getCurrentInstance, ref, watch } from 'vue'
+import { Check, Close, Goods, InfoFilled, List, Minus, Plus, Remove, User, UserFilled } from '@element-plus/icons-vue'
+
+defineOptions({ name: 'ProductSeatDialog' })
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  memberList: {
+    type: Array,
+    default: () => []
+  },
+  selectedMemberIds: {
+    type: Array,
+    default: () => []
+  }
+})
+
+const emit = defineEmits(['update:visible', 'success', 'close'])
+const { proxy } = getCurrentInstance()
+
+const operationMode = ref('assign')
+const selectedMembers = ref([])
+const selectedProduct = ref(null)
+const submitting = ref(false)
+const availableProducts = ref([])
+const assignedProducts = ref([])
+
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: val => emit('update:visible', val)
+})
+
+const canSubmit = computed(() => {
+  if (selectedMembers.value.length === 0) return false
+  if (operationMode.value === 'assign') {
+    return !!selectedProduct.value
+  }
+  return assignedProducts.value.length > 0
+})
+
+const message = proxy?.$message
+
+watch(
+  () => props.visible,
+  async newVal => {
+    if (newVal) {
+      await initDialog()
+    } else {
+      resetForm()
+    }
+  }
+)
+
+watch(
+  () => props.selectedMemberIds,
+  newVal => {
+    if (newVal && newVal.length > 0) {
+      selectedMembers.value = [...newVal]
+    }
+  },
+  { immediate: true, deep: true }
+)
+
+watch(operationMode, newVal => {
+  if (newVal === 'cancel' && selectedMembers.value.length > 0) {
+    loadAssignedProducts()
+  } else {
+    assignedProducts.value = []
+  }
+  selectedProduct.value = null
+})
+
+watch(
+  selectedMembers,
+  newVal => {
+    if (newVal.length > 0 && operationMode.value === 'cancel') {
+      loadAssignedProducts()
+    } else if (newVal.length === 0) {
+      assignedProducts.value = []
+    }
+  },
+  { deep: true }
+)
+
+const initDialog = async () => {
+  selectedMembers.value = props.selectedMemberIds.length > 0 ? [...props.selectedMemberIds] : []
+  await loadAvailableProducts()
+  if (operationMode.value === 'cancel' && selectedMembers.value.length > 0) {
+    await loadAssignedProducts()
+  }
+}
+
+const loadAvailableProducts = async () => {
+  try {
+    availableProducts.value = [
+      {
+        id: 1,
+        name: '聚德神笔套餐',
+        description: '聚德 AI 写作/办公套件',
+        seats: 10
+      },
+      {
+        id: 2,
+        name: '棱镜套餐',
+        description: '图像/视频增强助手',
+        seats: 5
+      }
+    ]
+  } catch (error) {
+    console.error('加载产品列表失败:', error)
+    message?.error('加载产品列表失败')
+    availableProducts.value = []
+  }
+}
+
+const loadAssignedProducts = async () => {
+  if (selectedMembers.value.length === 0) {
+    assignedProducts.value = []
+    selectedProduct.value = null
+    return
+  }
+
+  try {
+    assignedProducts.value = [
+      {
+        id: 1,
+        name: '聚德神笔套餐',
+        description: '聚德 AI 写作/办公套件',
+        assignedCount: selectedMembers.value.length
+      }
+    ]
+
+    if (assignedProducts.value.length === 1 && !selectedProduct.value) {
+      selectedProduct.value = assignedProducts.value[0].id
+    }
+  } catch (error) {
+    console.error('加载已分配产品失败:', error)
+    message?.error('加载已分配产品失败')
+    assignedProducts.value = []
+    selectedProduct.value = null
+  }
+}
+
+const handleModeChange = () => {
+  selectedProduct.value = null
+  if (operationMode.value === 'cancel' && selectedMembers.value.length > 0) {
+    loadAssignedProducts()
+  } else {
+    assignedProducts.value = []
+  }
+}
+
+const handleMemberChange = () => {
+  if (operationMode.value === 'cancel' && selectedMembers.value.length > 0) {
+    loadAssignedProducts()
+  } else if (selectedMembers.value.length === 0) {
+    assignedProducts.value = []
+  }
+}
+
+const handleSelectAll = () => {
+  selectedMembers.value = props.memberList.map(m => m.id)
+}
+
+const handleSelectNone = () => {
+  selectedMembers.value = []
+}
+
+const getProductImage = productId => `https://picsum.photos/200/200?random=${productId}`
+
+const handleCancelProduct = product => {
+  selectedProduct.value = product.id
+  handleSubmit()
+}
+
+const handleSubmit = async () => {
+  if (selectedMembers.value.length === 0) {
+    message?.warning('请至少选择一个人员')
+    return
+  }
+
+  if (operationMode.value === 'assign') {
+    if (!selectedProduct.value) {
+      message?.warning('请选择要分配的套餐产品')
+      return
+    }
+  } else if (!selectedProduct.value && assignedProducts.value.length === 0) {
+    message?.warning('没有可取消分配的产品')
+    return
+  }
+
+  submitting.value = true
+  try {
+    const productId =
+      selectedProduct.value || (assignedProducts.value.length > 0 ? assignedProducts.value[0].id : null)
+
+    if (operationMode.value === 'assign') {
+      message?.success(`成功为 ${selectedMembers.value.length} 位人员分配产品席位`)
+    } else {
+      message?.success(`成功取消 ${selectedMembers.value.length} 位人员的产品席位`)
+    }
+
+    emit('success', {
+      mode: operationMode.value,
+      productId,
+      memberIds: selectedMembers.value
+    })
+    dialogVisible.value = false
+  } catch (error) {
+    console.error('操作失败:', error)
+    message?.error(operationMode.value === 'assign' ? '分配席位失败' : '取消分配失败')
+  } finally {
+    submitting.value = false
+  }
+}
+
+const handleCancel = () => {
+  dialogVisible.value = false
+}
+
+const handleClose = () => {
+  resetForm()
+  emit('close')
+}
+
+const resetForm = () => {
+  operationMode.value = 'assign'
+  selectedMembers.value = []
+  selectedProduct.value = null
+  assignedProducts.value = []
+  submitting.value = false
+}
+</script>
+
+<style scoped lang="scss">
+::v-deep .product-seat-dialog {
+  .el-dialog {
+    border-radius: 16px;
+    overflow: hidden;
+    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
+  }
+
+  .el-dialog__header {
+    padding: 0;
+    border-bottom: none;
+  }
+
+  .el-dialog__body {
+    padding: 0;
+    max-height: 75vh;
+    overflow-y: auto;
+  }
+
+  .el-dialog__footer {
+    padding: 20px 30px;
+    border-top: 1px solid #f0f2f5;
+    background: #fafbfc;
+  }
+
+  .el-dialog__close {
+    z-index: 10;
+    color: #fff;
+    font-size: 22px;
+    font-weight: 600;
+
+    &:hover {
+      color: rgba(255, 255, 255, 0.9);
+      transform: rotate(90deg);
+    }
+  }
+}
+
+@keyframes pulse {
+
+  0%,
+  100% {
+    transform: scale(1);
+    opacity: 0.5;
+  }
+
+  50% {
+    transform: scale(1.1);
+    opacity: 0.3;
+  }
+}
+
+.dialog-content {
+
+  .mode-section,
+  .member-section,
+  .product-section,
+  .assigned-section {
+    margin-bottom: 30px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  // 操作模式标签页
+  .mode-section {
+    .mode-tabs {
+      display: flex;
+      gap: 12px;
+      background: #f5f7fa;
+      padding: 6px;
+      border-radius: 12px;
+
+      .mode-tab {
+        flex: 1;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: 8px;
+        padding: 12px 20px;
+        border-radius: 8px;
+        cursor: pointer;
+        transition: all 0.3s ease;
+        font-size: 14px;
+        font-weight: 500;
+        color: #606266;
+        background: transparent;
+
+        i {
+          font-size: 16px;
+        }
+
+        &:hover {
+          background: rgba(64, 158, 255, 0.1);
+          color: #409eff;
+        }
+
+        &.active {
+          background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+          color: #fff;
+          box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+          transform: translateY(-1px);
+        }
+      }
+    }
+  }
+
+  // 区域标题
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+
+    .section-title {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+
+      i {
+        font-size: 18px;
+        color: #409eff;
+      }
+    }
+
+    .section-actions {
+      display: flex;
+      gap: 8px;
+    }
+  }
+
+  // 人员选择区域
+  .member-section {
+    .member-list-wrapper {
+      .selected-badge {
+        display: inline-flex;
+        align-items: center;
+        gap: 6px;
+        padding: 8px 16px;
+        background: linear-gradient(135deg, #ecf5ff 0%, #e1f3ff 100%);
+        border: 1px solid #b3d8ff;
+        border-radius: 20px;
+        margin-bottom: 16px;
+        font-size: 14px;
+        color: #409eff;
+
+        i {
+          font-size: 16px;
+        }
+
+        strong {
+          font-weight: 600;
+          font-size: 16px;
+        }
+      }
+
+      .member-list {
+        max-height: 300px;
+        overflow-y: auto;
+        padding: 4px;
+        border-radius: 12px;
+
+        &::-webkit-scrollbar {
+          width: 6px;
+        }
+
+        &::-webkit-scrollbar-track {
+          background: #f5f7fa;
+          border-radius: 3px;
+        }
+
+        &::-webkit-scrollbar-thumb {
+          background: #dcdfe6;
+          border-radius: 3px;
+
+          &:hover {
+            background: #c0c4cc;
+          }
+        }
+
+        .member-card {
+          position: relative;
+          margin-bottom: 12px;
+          border-radius: 12px;
+          border: 2px solid #f0f2f5;
+          background: #fff;
+          transition: all 0.3s ease;
+          overflow: hidden;
+
+          &:last-child {
+            margin-bottom: 0;
+          }
+
+          &:hover {
+            border-color: #b3d8ff;
+            box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
+            transform: translateY(-2px);
+          }
+
+          &.selected {
+            border-color: #409eff;
+            background: linear-gradient(135deg, #ecf5ff 0%, #e1f3ff 100%);
+            box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2);
+          }
+
+          ::v-deep .member-checkbox {
+            width: 100%;
+            margin: 0;
+            padding: 16px;
+            display: flex;
+            align-items: center;
+            cursor: pointer;
+
+            .el-checkbox__input {
+              display: none;
+            }
+
+            .el-checkbox__label {
+              width: 100%;
+              padding: 0;
+              display: flex;
+              align-items: center;
+            }
+          }
+
+          .member-avatar {
+            width: 48px;
+            height: 48px;
+            border-radius: 12px;
+            background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            margin-right: 16px;
+            flex-shrink: 0;
+            box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
+
+            i {
+              font-size: 24px;
+              color: #fff;
+            }
+          }
+
+          .member-details {
+            flex: 1;
+            min-width: 0;
+
+            .member-name {
+              font-size: 15px;
+              font-weight: 600;
+              color: #303133;
+              margin-bottom: 4px;
+            }
+
+            .member-email {
+              font-size: 13px;
+              color: #909399;
+              word-break: break-all;
+            }
+          }
+
+          .member-check-icon {
+            width: 32px;
+            height: 32px;
+            border-radius: 50%;
+            background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            margin-left: 12px;
+            flex-shrink: 0;
+            animation: scaleIn 0.3s ease;
+
+            i {
+              font-size: 18px;
+              color: #fff;
+            }
+          }
+        }
+
+        .empty-member {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          padding: 60px 20px;
+          color: #c0c4cc;
+
+          i {
+            font-size: 48px;
+            margin-bottom: 12px;
+          }
+
+          p {
+            margin: 0;
+            font-size: 14px;
+          }
+        }
+      }
+    }
+  }
+
+  // 产品选择区域
+  .product-section {
+    .product-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+      gap: 16px;
+
+      .product-card {
+        position: relative;
+        padding: 0;
+        border-radius: 12px;
+        border: 2px solid #f0f2f5;
+        background: #fff;
+        cursor: pointer;
+        transition: all 0.3s ease;
+        overflow: hidden;
+        display: flex;
+        flex-direction: column;
+
+        &::before {
+          content: '';
+          position: absolute;
+          top: 0;
+          left: 0;
+          right: 0;
+          height: 4px;
+          background: linear-gradient(90deg, #409eff 0%, #66b1ff 100%);
+          transform: scaleX(0);
+          transition: transform 0.3s ease;
+          z-index: 1;
+        }
+
+        &:hover {
+          border-color: #b3d8ff;
+          box-shadow: 0 8px 24px rgba(64, 158, 255, 0.15);
+          transform: translateY(-4px);
+
+          &::before {
+            transform: scaleX(1);
+          }
+
+          .product-image {
+            transform: scale(1.05);
+          }
+        }
+
+        &.active {
+          border-color: #409eff;
+          background: linear-gradient(135deg, #ecf5ff 0%, #e1f3ff 100%);
+          box-shadow: 0 8px 24px rgba(64, 158, 255, 0.25);
+
+          &::before {
+            transform: scaleX(1);
+          }
+        }
+
+        .product-image {
+          width: 100%;
+          height: 160px;
+          overflow: hidden;
+          background: #f5f7fa;
+          position: relative;
+          transition: transform 0.3s ease;
+
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+            display: block;
+          }
+
+          &::after {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.02) 100%);
+          }
+        }
+
+        .product-content {
+          padding: 16px 20px 20px;
+          flex: 1;
+          display: flex;
+          flex-direction: column;
+
+          .product-name {
+            font-size: 16px;
+            font-weight: 600;
+            color: #303133;
+            margin-bottom: 8px;
+          }
+
+          .product-desc {
+            font-size: 13px;
+            color: #909399;
+            line-height: 1.5;
+            margin-bottom: 12px;
+            min-height: 38px;
+            flex: 1;
+          }
+
+          .product-meta {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            font-size: 13px;
+
+            .seats-info {
+              display: flex;
+              align-items: center;
+              gap: 4px;
+              color: #409eff;
+              font-weight: 500;
+
+              i {
+                font-size: 14px;
+              }
+            }
+          }
+        }
+
+        .product-check {
+          position: absolute;
+          top: 12px;
+          right: 12px;
+          width: 28px;
+          height: 28px;
+          border-radius: 50%;
+          background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          box-shadow: 0 2px 8px rgba(103, 194, 58, 0.3);
+          animation: scaleIn 0.3s ease;
+
+          i {
+            font-size: 16px;
+            color: #fff;
+          }
+        }
+      }
+
+      .empty-product {
+        grid-column: 1 / -1;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        padding: 60px 20px;
+        color: #c0c4cc;
+
+        i {
+          font-size: 48px;
+          margin-bottom: 12px;
+        }
+
+        p {
+          margin: 0;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+
+  // 已分配产品区域
+  .assigned-section {
+    .assigned-product-list {
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+
+      .assigned-product-card {
+        position: relative;
+        display: flex;
+        align-items: center;
+        padding: 16px 20px;
+        border-radius: 12px;
+        border: 2px solid #f0f2f5;
+        background: #fff;
+        cursor: pointer;
+        transition: all 0.3s ease;
+
+        &:hover {
+          border-color: #f56c6c;
+          box-shadow: 0 4px 12px rgba(245, 108, 108, 0.1);
+          transform: translateX(4px);
+        }
+
+        &.active {
+          border-color: #f56c6c;
+          background: linear-gradient(135deg, #fef0f0 0%, #fde2e2 100%);
+          box-shadow: 0 4px 16px rgba(245, 108, 108, 0.2);
+        }
+
+        .product-icon {
+          width: 48px;
+          height: 48px;
+          border-radius: 10px;
+          background: linear-gradient(135deg, #f56c6c 0%, #f78989 100%);
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          margin-right: 16px;
+          flex-shrink: 0;
+          box-shadow: 0 2px 8px rgba(245, 108, 108, 0.3);
+
+          i {
+            font-size: 24px;
+            color: #fff;
+          }
+        }
+
+        .product-content {
+          flex: 1;
+          min-width: 0;
+
+          .product-name {
+            font-size: 15px;
+            font-weight: 600;
+            color: #303133;
+            margin-bottom: 4px;
+          }
+
+          .product-desc {
+            font-size: 13px;
+            color: #909399;
+            margin-bottom: 8px;
+          }
+
+          .product-meta {
+            .assigned-count {
+              display: flex;
+              align-items: center;
+              gap: 4px;
+              font-size: 13px;
+              color: #f56c6c;
+              font-weight: 500;
+
+              i {
+                font-size: 14px;
+              }
+            }
+          }
+        }
+
+        .product-check {
+          width: 28px;
+          height: 28px;
+          border-radius: 50%;
+          background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          flex-shrink: 0;
+          box-shadow: 0 2px 8px rgba(103, 194, 58, 0.3);
+          animation: scaleIn 0.3s ease;
+
+          i {
+            font-size: 16px;
+            color: #fff;
+          }
+        }
+      }
+    }
+
+    .empty-assigned {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      padding: 60px 20px;
+      color: #c0c4cc;
+      background: #fafafa;
+      border-radius: 12px;
+      border: 2px dashed #e4e7ed;
+
+      i {
+        font-size: 48px;
+        margin-bottom: 12px;
+      }
+
+      p {
+        margin: 0;
+        font-size: 14px;
+      }
+    }
+  }
+}
+
+@keyframes scaleIn {
+  from {
+    transform: scale(0);
+    opacity: 0;
+  }
+
+  to {
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+
+  .el-button {
+    min-width: 120px;
+    height: 44px;
+    border-radius: 8px;
+    font-weight: 500;
+    font-size: 14px;
+    transition: all 0.3s ease;
+
+    i {
+      margin-right: 6px;
+      font-size: 16px;
+    }
+
+    &.el-button--primary {
+      background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+      border: none;
+      box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+
+      &:hover:not(:disabled) {
+        transform: translateY(-2px);
+        box-shadow: 0 6px 20px rgba(64, 158, 255, 0.4);
+      }
+
+      &:active:not(:disabled) {
+        transform: translateY(0);
+      }
+
+      &:disabled {
+        opacity: 0.6;
+      }
+    }
+  }
+}
+</style>

+ 344 - 0
web/src/views/frontend/components/goods.vue

@@ -0,0 +1,344 @@
+<template>
+    <div class="client-goods">
+      <div class="goods-grid">
+        <div
+          v-for="item in goodsList"
+          :key="item.id"
+          class="goods-card"
+          @click="handleCardClick(item)"
+        >
+          <!-- 商品图片 -->
+          <div class="goods-image-wrapper">
+            <img :src="item.img" :alt="item.name" class="goods-image">
+            <!-- 自动发货标签 -->
+            <div class="auto-delivery-badge" :class="item.type === 1 ? 'badge-personal' : 'badge-enterprise'">
+              <span>{{ item.type === 1 ? '个人套餐' : '企业套餐' }}</span>
+            </div>
+          </div>
+  
+          <!-- 商品信息 -->
+          <div class="goods-info">
+            <!-- 商品标题 -->
+            <h3 class="goods-title">{{ item.name }}</h3>
+  
+            <!-- 价格 -->
+            <div class="goods-price">
+              <span class="price-symbol">¥</span>
+              <span class="price-value">{{ item.price.toFixed(2) }}</span>
+            </div>
+  
+            <!-- 库存和已售信息 -->
+            <div class="goods-meta">
+              <div class="inventory-status" :class="getInventoryClass(item.status)">
+                <span class="meta-label">库存:</span>
+                <span class="meta-value">{{ getInventoryText(item.status) }}</span>
+              </div>
+              <div class="sold-count">
+                <span class="meta-label">已售:</span>
+                <span class="meta-value">{{ item.sold }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </template>
+  
+<script setup lang="ts">
+type GoodsStatus = '充足' | '一般' | '即将售罄' | '缺货' | string
+
+type GoodsItem = {
+  id: string | number
+  name: string
+  img: string
+  price: number
+  sold: number
+  status: GoodsStatus
+  type: number
+}
+
+const props = withDefaults(
+  defineProps<{
+    goodsList: GoodsItem[]
+  }>(),
+  {
+    goodsList: () => []
+  }
+)
+
+const emit = defineEmits<{
+  (e: 'handleClick', item: GoodsItem): void
+}>()
+
+const getInventoryText = (status: GoodsStatus) => {
+  const statusMap: Record<string, string> = {
+    充足: '充足',
+    一般: '一般',
+    即将售罄: '即将售罄',
+    缺货: '缺货'
+  }
+  return statusMap[status] || status
+}
+
+const getInventoryClass = (status: GoodsStatus) => {
+  if (status === '即将售罄') {
+    return 'status-warning'
+  }
+  if (status === '缺货') {
+    return 'status-danger'
+  }
+  if (status === '一般') {
+    return 'status-normal'
+  }
+  return 'status-adequate'
+}
+
+const handleCardClick = (item: GoodsItem) => {
+  emit('handleClick', item)
+}
+</script>
+  
+  <style scoped lang="scss">
+  .client-goods {
+    width: 100%;
+  
+    .goods-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+      gap: 24px;
+      padding: 0;
+  
+      @media (max-width: 1024px) {
+        grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+        gap: 20px;
+      }
+  
+      @media (max-width: 768px) {
+        grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+        gap: 16px;
+      }
+  
+      @media (max-width: 480px) {
+        grid-template-columns: 1fr;
+        gap: 16px;
+      }
+    }
+  
+    .goods-card {
+      background: #ffffff;
+      border-radius: 12px;
+      overflow: hidden;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+      transition: all 0.3s ease;
+      cursor: pointer;
+      display: flex;
+      flex-direction: column;
+  
+      &:hover {
+        transform: translateY(-4px);
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+  
+        .goods-image {
+          transform: scale(1.05);
+        }
+      }
+  
+      @media (max-width: 768px) {
+        border-radius: 10px;
+  
+        &:hover {
+          transform: translateY(-2px);
+        }
+      }
+    }
+  
+    .goods-image-wrapper {
+      position: relative;
+      width: 100%;
+      padding-top: 75%; // 4:3 比例
+      overflow: hidden;
+      background: #f5f7fa;
+  
+      .goods-image {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+        transition: transform 0.3s ease;
+      }
+  
+      .auto-delivery-badge {
+        position: absolute;
+        top: 12px;
+        left: 12px;
+        border-radius: 16px;
+        padding: 4px 12px;
+        transition: all 0.3s ease;
+  
+        span {
+          color: #ffffff;
+          font-size: 12px;
+          font-weight: 500;
+          letter-spacing: 0.3px;
+        }
+  
+        // 个人套餐样式 - 蓝色系
+        &.badge-personal {
+          background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+          box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
+        }
+  
+        // 企业套餐样式 - 橙色系
+        &.badge-enterprise {
+          background: linear-gradient(135deg, #e6a23c 0%, #f0c675 100%);
+          box-shadow: 0 2px 4px rgba(230, 162, 60, 0.3);
+        }
+  
+        @media (max-width: 768px) {
+          top: 8px;
+          left: 8px;
+          padding: 3px 10px;
+          border-radius: 12px;
+  
+          span {
+            font-size: 11px;
+          }
+        }
+      }
+    }
+  
+    .goods-info {
+      padding: 16px;
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+      flex: 1;
+  
+      @media (max-width: 768px) {
+        padding: 12px;
+        gap: 10px;
+      }
+    }
+  
+    .goods-title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+      margin: 0;
+      line-height: 1.4;
+      display: -webkit-box;
+      -webkit-line-clamp: 2;
+      line-clamp: 2;
+      -webkit-box-orient: vertical;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      min-height: 44px;
+  
+      @media (max-width: 768px) {
+        font-size: 14px;
+        min-height: 40px;
+      }
+    }
+  
+    .goods-price {
+      display: flex;
+      align-items: baseline;
+      gap: 2px;
+      color: #f56c6c;
+      font-weight: 600;
+  
+      .price-symbol {
+        font-size: 14px;
+  
+        @media (max-width: 768px) {
+          font-size: 12px;
+        }
+      }
+  
+      .price-value {
+        font-size: 20px;
+  
+        @media (max-width: 768px) {
+          font-size: 18px;
+        }
+      }
+    }
+  
+    .goods-meta {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-size: 13px;
+      margin-top: auto;
+      padding-top: 8px;
+      border-top: 1px solid #f0f0f0;
+  
+      @media (max-width: 768px) {
+        font-size: 12px;
+        padding-top: 6px;
+      }
+  
+      .inventory-status,
+      .sold-count {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+  
+        .meta-label {
+          color: #909399;
+          font-weight: 400;
+        }
+  
+        .meta-value {
+          color: #606266;
+          font-weight: 500;
+        }
+  
+        &.status-warning .meta-value {
+          color: #e6a23c;
+        }
+  
+        &.status-danger .meta-value {
+          color: #f56c6c;
+        }
+  
+        &.status-normal .meta-value {
+          color: #909399;
+        }
+  
+        &.status-adequate .meta-value {
+          color: #67c23a;
+        }
+      }
+    }
+  }
+  
+  // 移动端优化
+  @media (max-width: 768px) {
+    .client-goods {
+      .goods-grid {
+        padding: 0 4px;
+      }
+  
+      .goods-card {
+        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
+      }
+    }
+  }
+  
+  // 超小屏幕
+  @media (max-width: 480px) {
+    .client-goods {
+      .goods-grid {
+        gap: 12px;
+      }
+  
+      .goods-image-wrapper {
+        padding-top: 70%;
+      }
+    }
+  }
+  </style>
+  

+ 840 - 0
web/src/views/frontend/components/header.vue

@@ -0,0 +1,840 @@
+<template>
+    <div class="client-header">
+      <div class="header-container">
+        <!-- 左侧:Logo和标题 -->
+        <div class="header-left">
+          <div class="logo-icon">
+            <img src="/@/assets/images/logo.png" alt="">
+          </div>
+          <div class="header-title">{{ siteConfig.siteName || '用户充值中心' }}</div>
+          <!-- 导航链接 -->
+          <div class="nav-links">
+            <div class="nav-item" @click="goToShopping">
+              <el-icon>
+                <ShoppingCartFull />
+              </el-icon>
+              <span>购物</span>
+            </div>
+            <div class="nav-item" @click="goToOrders">
+              <el-icon>
+                <FolderOpened />
+              </el-icon>
+              <span>订单查询</span>
+            </div>
+          </div>
+        </div>
+  
+        <!-- 中间:PC 显示搜索框,移动端显示“购物”入口 -->
+        <div class="header-center">
+          <div v-if="!isMobile" class="search-box">
+            <el-icon class="search-icon">
+              <Search />
+            </el-icon>
+            <input
+              v-model="searchKeyword"
+              type="text"
+              placeholder="搜索商品关键词.."
+              class="search-input"
+              @keyup.enter="handleSearch"
+            >
+          </div>
+          <div
+            v-else
+            class="mobile-shop-entry"
+            @click="goToShopping"
+          >
+            <el-icon>
+              <ShoppingCartFull />
+            </el-icon>
+            <span>购物</span>
+          </div>
+        </div>
+  
+        <!-- 右侧:用户信息 -->
+        <div class="header-right">
+          <!-- 移动端:未登录显示登录按钮;已登录显示菜单方块 -->
+          <div v-if="isMobile" class="mobile-menu-btn">
+            <el-button
+              v-if="!isLoggedIn"
+              type="primary"
+              size="small"
+              @click="goToLogin"
+            >
+              登录
+            </el-button>
+            <el-icon
+              v-else
+              class="menu-icon"
+              @click="toggleMobileMenu"
+            >
+              <Menu />
+            </el-icon>
+          </div>
+  
+          <!-- 用户信息区域(PC 端始终存在,移动端仅登录后用于下拉菜单) -->
+          <div
+            v-if="isLoggedIn || !isMobile"
+            class="user-info-wrapper"
+            :class="{ 'mobile-open': showMobileMenu }"
+          >
+            <div v-if="isLoggedIn && !isMobile" class="user-info" @click.stop="toggleUserMenu">
+              <div class="user-avatar">
+                <img :src="userAvatar" alt="avatar" />
+              </div>
+              <div class="user-details">
+                <div class="user-name">{{ displayName }}</div>
+                <div v-if="displayEmail" class="user-email">
+                  {{ displayEmail }}
+                </div>
+              </div>
+            </div>
+  
+            <!-- 未登录状态(仅 PC 显示) -->
+            <div v-if="!isLoggedIn && !isMobile" class="login-btn-wrapper">
+              <div class="login-actions">
+                <el-button type="primary" size="small" @click="goToLogin">登录</el-button>
+              </div>
+            </div>
+  
+            <!-- 用户下拉菜单 -->
+            <transition name="fade">
+              <div v-show="showUserMenu && isLoggedIn" v-clickoutside="closeUserMenu" class="user-dropdown">
+                <!-- 用户信息头部 -->
+                <div class="dropdown-header">
+                  <div class="dropdown-avatar">
+                    <img :src="userAvatar" alt="avatar" />
+                  </div>
+                  <div class="dropdown-user-info">
+                    <div class="dropdown-username">{{ displayName }}</div>
+                    <div v-if="displayEmail" class="dropdown-email">{{ displayEmail }}</div>
+                  </div>
+                  <el-icon class="dropdown-arrow">
+                    <ArrowDown />
+                  </el-icon>
+                </div>
+  
+                <!-- 菜单项 -->
+                <div class="dropdown-menu">
+                  <div class="menu-group">
+                    <div class="menu-item" @click="goToProfile">
+                      <el-icon class="menu-icon">
+                        <User />
+                      </el-icon>
+                      <span>个人中心</span>
+                    </div>
+                    <div class="menu-item" @click="goToOrders">
+                      <el-icon class="menu-icon">
+                        <Document />
+                      </el-icon>
+                      <span>我的订单</span>
+                    </div>
+                  </div>
+                  <div class="menu-divider" />
+                  <div class="menu-group">
+                    <div class="menu-item logout" @click="handleLogout">
+                      <el-icon class="menu-icon">
+                        <Right />
+                      </el-icon>
+                      <span>退出登录</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </transition>
+          </div>
+        </div>
+      </div>
+    </div>
+  </template>
+  
+<script setup lang="ts">
+import { computed, onBeforeUnmount, onMounted, ref, watch, watchEffect, type DirectiveBinding } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { ArrowDown, Document, FolderOpened, Menu, Right, Search, ShoppingCartFull, User } from '@element-plus/icons-vue'
+import { useUserInfo } from '/@/stores/userInfo'
+import { postLogout } from '/@/api/frontend/user/index'
+import { Local } from '/@/utils/storage'
+import { USER_INFO } from '/@/stores/constant/cacheKey'
+import { useSiteConfig } from '/@/stores/siteConfig'
+import { fullUrl } from '/@/utils/common'
+
+type ClickOutsideEl = HTMLElement & { __clickOutside__?: (e: MouseEvent) => void }
+
+const route = useRoute()
+const router = useRouter()
+const userInfo = useUserInfo()
+const siteConfig = useSiteConfig()
+const DEFAULT_AVATAR = '/static/images/avatar.png'
+
+const searchKeyword = ref('')
+const showUserMenu = ref(false)
+const showMobileMenu = ref(false)
+const isMobile = ref(false)
+
+const isLoggedIn = computed(() => userInfo.isLogin())
+const displayName = computed(() => {
+  if (!isLoggedIn.value) {
+    return '未登录'
+  }
+  return userInfo.nickname || userInfo.username || '未命名用户'
+})
+const displayEmail = computed(() => (isLoggedIn.value ? userInfo.email || '' : ''))
+const userAvatar = computed(() => {
+  const avatar = userInfo.avatar || DEFAULT_AVATAR
+  return fullUrl(avatar)
+})
+
+const checkMobile = () => {
+  isMobile.value = window.innerWidth < 768
+  if (!isMobile.value) {
+    showMobileMenu.value = false
+  }
+}
+
+const toggleUserMenu = () => {
+  // PC 端点击头像展开/收起;移动端主要通过菜单按钮控制
+  if (isMobile.value) return
+  showUserMenu.value = !showUserMenu.value
+}
+
+const closeUserMenu = () => {
+  // 移动端展开整块菜单时,不让 clickoutside 把它收起
+  if (isMobile.value && showMobileMenu.value) return
+  showUserMenu.value = false
+}
+
+const toggleMobileMenu = () => {
+  showMobileMenu.value = !showMobileMenu.value
+  if (showMobileMenu.value && isLoggedIn.value) {
+    // 移动端:点右上角方块时直接展开完整下拉
+    showUserMenu.value = true
+  } else if (!showMobileMenu.value) {
+    showUserMenu.value = false
+  }
+}
+
+const handleSearch = () => {
+  const keyword = searchKeyword.value.trim()
+  if (keyword) {
+    router.push({
+      path: '/user/home',
+      query: { keyword }
+    })
+  }
+}
+
+const closeAllMenus = () => {
+  showMobileMenu.value = false
+  showUserMenu.value = false
+}
+
+const goToShopping = () => {
+  router.push('/')
+  closeAllMenus()
+}
+
+const goToOrders = () => {
+  // 跳转到订单查询页面
+  router.push('/user/me')
+  closeAllMenus()
+}
+
+const goToLogin = () => {
+  router.push('/user/login')
+  closeAllMenus()
+}
+
+const goToProfile = () => {
+  // 跳转到个人中心
+  router.push('/user/me')
+  closeAllMenus()
+}
+
+const handleLogout = async () => {
+  let success = false
+  try {
+    await postLogout()
+    success = true
+  } catch (error) {
+    console.error('退出登录失败:', error)
+    ElMessage.error('退出登录失败')
+  } finally {
+    Local.remove(USER_INFO)
+    userInfo.$reset()
+    router.push('/user/login')
+  }
+  if (success) {
+    ElMessage.success('退出登录成功')
+  }
+  closeAllMenus()
+}
+
+watch(isLoggedIn, (loggedIn) => {
+  if (!loggedIn) {
+    showUserMenu.value = false
+  }
+})
+
+watchEffect(() => {
+  // 路由切换时自动收起菜单,保持状态一致
+  route.fullPath
+  closeAllMenus()
+})
+
+onMounted(() => {
+  checkMobile()
+  window.addEventListener('resize', checkMobile)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', checkMobile)
+})
+
+// 点击外部关闭菜单指令
+const vClickoutside = {
+  beforeMount(el: ClickOutsideEl, binding: DirectiveBinding) {
+    const handler = (e: MouseEvent) => {
+      if (el.contains(e.target as Node)) {
+        return
+      }
+      if (typeof binding.value === 'function') {
+        binding.value(e)
+      }
+    }
+    el.__clickOutside__ = handler
+    document.addEventListener('click', handler)
+  },
+  unmounted(el: ClickOutsideEl) {
+    if (el.__clickOutside__) {
+      document.removeEventListener('click', el.__clickOutside__)
+      delete el.__clickOutside__
+    }
+  }
+}
+</script>
+  
+  <style scoped lang="scss">
+  .client-header {
+    background: #ffffff;
+    border-bottom: 1px solid #e4e7ed;
+    position: sticky;
+    top: 0;
+    z-index: 1000;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+  
+    .header-container {
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 0 20px;
+      height: 64px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      position: relative;
+    }
+  
+    // 左侧区域
+    .header-left {
+      display: flex;
+      align-items: center;
+      flex: 0 0 auto;
+      min-width: 0;
+  
+      .logo-icon {
+        width: 35px;
+        height: 35px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: #000000;
+        margin-right: 12px;
+        flex-shrink: 0;
+  
+        img {
+          width: 100%;
+          height: 100%;
+          border-radius: 50%;
+        }
+      }
+  
+      .header-title {
+        font-size: 18px;
+        font-weight: 500;
+        color: #1391e3;
+        white-space: nowrap;
+        margin-right: 24px;
+        flex-shrink: 0;
+      }
+  
+      .nav-links {
+        display: flex;
+        align-items: center;
+        gap: 20px;
+  
+        .nav-item {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+          cursor: pointer;
+          color: #606266;
+          font-size: 14px;
+          transition: color 0.3s;
+          white-space: nowrap;
+  
+          i {
+            font-size: 16px;
+          }
+  
+          &:hover {
+            color: #1391e3;
+          }
+        }
+      }
+    }
+  
+    // 中间搜索框
+    .header-center {
+      flex: 1;
+      display: flex;
+      justify-content: center;
+      padding: 0 20px;
+      min-width: 0;
+  
+      .search-box {
+        width: 100%;
+        max-width: 400px;
+        height: 40px;
+        background: #f5f7fa;
+        border-radius: 20px;
+        display: flex;
+        align-items: center;
+        padding: 0 16px;
+        transition: all 0.3s;
+  
+        &:focus-within {
+          background: #ffffff;
+          box-shadow: 0 0 0 2px rgba(103, 194, 58, 0.2);
+        }
+  
+        .search-icon {
+          color: #909399;
+          font-size: 16px;
+          margin-right: 8px;
+          flex-shrink: 0;
+        }
+  
+        .search-input {
+          flex: 1;
+          border: none;
+          outline: none;
+          background: transparent;
+          font-size: 14px;
+          color: #303133;
+          min-width: 0;
+  
+          &::placeholder {
+            color: #c0c4cc;
+          }
+        }
+      }
+  
+      .mobile-shop-entry {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding: 0 12px;
+        height: 40px;
+        border-radius: 20px;
+        background: #f5f7fa;
+        color: #303133;
+        font-size: 14px;
+        cursor: pointer;
+        gap: 6px;
+  
+        i {
+          font-size: 18px;
+        }
+  
+        &:active {
+          background: #e4e7ed;
+        }
+      }
+    }
+  
+    // 右侧用户区域
+    .header-right {
+      display: flex;
+      align-items: center;
+      flex: 0 0 auto;
+      position: relative;
+  
+      .mobile-menu-btn {
+        display: none;
+        width: 40px;
+        height: 40px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        cursor: pointer;
+        color: #606266;
+        font-size: 20px;
+        margin-left: 12px;
+  
+        &:hover {
+          color: #1391e3;
+        }
+      }
+  
+      .user-info-wrapper {
+        display: flex;
+        align-items: center;
+        position: relative;
+  
+        &.mobile-open {
+          .user-info {
+            display: flex;
+          }
+        }
+  
+        .user-info {
+          display: flex;
+          align-items: center;
+          cursor: pointer;
+          padding: 8px 12px;
+          border-radius: 8px;
+          transition: background-color 0.3s;
+  
+          &:hover {
+            background-color: #f5f7fa;
+          }
+  
+          .user-avatar {
+            width: 40px;
+            height: 40px;
+            border-radius: 50%;
+            background: #000000;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            color: #ffffff;
+            margin-right: 12px;
+            flex-shrink: 0;
+
+            img {
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+              border-radius: 50%;
+            }
+          }
+  
+          .user-details {
+            display: flex;
+            flex-direction: column;
+            min-width: 0;
+  
+            .user-name {
+              font-size: 14px;
+              color: #303133;
+              font-weight: 500;
+              margin-bottom: 2px;
+              white-space: nowrap;
+              overflow: hidden;
+              text-overflow: ellipsis;
+            }
+  
+            .user-email {
+              font-size: 12px;
+              color: #606266;
+              white-space: nowrap;
+              overflow: hidden;
+              text-overflow: ellipsis;
+            }
+          }
+        }
+  
+        .login-btn-wrapper {
+          display: flex;
+          align-items: center;
+  
+          .login-actions {
+            display: flex;
+            flex-direction: column;
+            align-items: flex-end;
+            text-align: right;
+            gap: 6px;
+  
+            .login-tip {
+              font-size: 12px;
+              color: #909399;
+            }
+  
+            .btn-group {
+              display: flex;
+              gap: 8px;
+  
+              .el-button {
+                padding: 8px 20px;
+              }
+            }
+          }
+        }
+  
+        // 用户下拉菜单
+        .user-dropdown {
+          position: absolute;
+          top: calc(100% + 8px);
+          right: 0;
+          background: #ffffff;
+          border-radius: 8px;
+          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+          min-width: 240px;
+          overflow: hidden;
+          z-index: 1001;
+  
+          .dropdown-header {
+            padding: 16px;
+            display: flex;
+            align-items: center;
+            border-bottom: 1px solid #e4e7ed;
+  
+            .dropdown-avatar {
+              width: 48px;
+              height: 48px;
+              border-radius: 50%;
+              background: #000000;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              color: #ffffff;
+              margin-right: 12px;
+              flex-shrink: 0;
+
+              img {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+                border-radius: 50%;
+              }
+            }
+  
+          .dropdown-user-info {
+              flex: 1;
+              min-width: 0;
+  
+              .dropdown-username {
+                font-size: 14px;
+                color: #303133;
+                font-weight: 500;
+                margin-bottom: 2px;
+                white-space: nowrap;
+                overflow: hidden;
+                text-overflow: ellipsis;
+              }
+  
+              .dropdown-email {
+                font-size: 12px;
+                color: #606266;
+                white-space: nowrap;
+                overflow: hidden;
+                text-overflow: ellipsis;
+              }
+            }
+  
+            .dropdown-arrow {
+              color: #909399;
+              font-size: 12px;
+            }
+          }
+  
+          .dropdown-menu {
+            padding: 8px 0;
+  
+            .menu-group {
+              .menu-item {
+                display: flex;
+                align-items: center;
+                padding: 12px 16px;
+                cursor: pointer;
+                color: #303133;
+                font-size: 14px;
+                transition: background-color 0.3s;
+  
+                .menu-icon {
+                  font-size: 16px;
+                  margin-right: 12px;
+                  color: #606266;
+                  width: 20px;
+                  text-align: center;
+                }
+  
+                &:hover {
+                  background-color: #f5f7fa;
+                }
+  
+                &.logout {
+                  color: #f56c6c;
+  
+                  i {
+                    color: #f56c6c;
+                  }
+  
+                  &:hover {
+                    background-color: #fef0f0;
+                  }
+                }
+              }
+            }
+  
+            .menu-divider {
+              height: 1px;
+              background: #e4e7ed;
+              margin: 4px 0;
+            }
+          }
+        }
+      }
+    }
+  
+    // 移动端样式
+    @media (max-width: 768px) {
+      .header-container {
+        padding: 0 16px;
+        height: 56px;
+      }
+  
+      .header-left {
+        .header-title {
+          font-size: 16px;
+          margin-right: 12px;
+        }
+  
+        .nav-links {
+          display: none;
+        }
+      }
+  
+      .header-center {
+        padding: 0;
+  
+        .search-box {
+          max-width: none;
+          height: 36px;
+          padding: 0 12px;
+  
+          .search-input {
+            font-size: 13px;
+          }
+        }
+      }
+  
+      .header-right {
+        position: static;
+  
+        .mobile-menu-btn {
+          display: flex;
+        }
+  
+        .user-info-wrapper {
+          position: absolute;
+          top: 100%;
+          right: 0;
+          left: 0;
+          background: #ffffff;
+          border-top: 1px solid #e4e7ed;
+          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+          flex-direction: column;
+          align-items: stretch;
+          max-height: 0;
+          overflow: hidden;
+          transition: max-height 0.3s ease;
+  
+          &.mobile-open {
+            max-height: 500px;
+            padding: 16px;
+          }
+  
+          .user-info {
+            width: 100%;
+            padding: 12px;
+            border-radius: 8px;
+            margin-bottom: 12px;
+          }
+  
+          .login-btn-wrapper {
+            width: 100%;
+  
+            .login-actions {
+              align-items: flex-start;
+              text-align: left;
+              width: 100%;
+  
+              .btn-group {
+                width: 100%;
+                flex-direction: column;
+  
+                .el-button {
+                  width: 100%;
+                  text-align: center;
+                }
+              }
+            }
+          }
+  
+          .user-dropdown {
+            position: static;
+            box-shadow: none;
+            border-radius: 8px;
+            border: 1px solid #e4e7ed;
+            margin-top: 8px;
+          }
+        }
+      }
+    }
+  
+    // 超小屏幕
+    @media (max-width: 480px) {
+      .header-left {
+        .logo-icon {
+          width: 24px;
+          height: 24px;
+          margin-right: 8px;
+        }
+  
+        .header-title {
+          font-size: 14px;
+          margin-right: 8px;
+        }
+      }
+  
+      .header-center {
+        padding: 0;
+  
+        .search-box {
+          .search-icon {
+            font-size: 14px;
+            margin-right: 6px;
+          }
+  
+          .search-input {
+            font-size: 12px;
+          }
+        }
+      }
+    }
+  }
+  
+  // 下拉菜单动画
+  .fade-enter-active,
+  .fade-leave-active {
+    transition: opacity 0.3s, transform 0.3s;
+  }
+  
+  .fade-enter,
+  .fade-leave-to {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  </style>
+  

+ 377 - 0
web/src/views/frontend/components/success.vue

@@ -0,0 +1,377 @@
+<template>
+  <div class="client-success">
+    <!-- 头部 -->
+    <Header />
+
+    <!-- 内容主体 -->
+    <div class="success-body">
+      <el-card class="success-card" shadow="hover">
+        <!-- 顶部成功图标与标题 -->
+        <div class="success-header">
+          <div class="success-icon-wrapper">
+            <div class="success-icon">
+              <el-icon class="success-icon-inner">
+                <SuccessFilled />
+              </el-icon>
+            </div>
+          </div>
+          <div class="success-text">
+            <h2 class="success-title">下单成功</h2>
+            <p class="success-subtitle">
+              您的订单已经创建成功,请尽快完成支付或前往订单中心查看详情。
+            </p>
+          </div>
+        </div>
+
+        <!-- 订单关键信息 -->
+        <div class="success-info">
+          <div class="info-row main-amount">
+            <div class="info-label">实付金额</div>
+            <div class="info-value amount">
+              <span class="currency">¥</span>
+              <span class="number">{{ displayAmount }}</span>
+            </div>
+          </div>
+
+          <div class="info-row">
+            <div class="info-label">订单编号</div>
+            <div class="info-value">{{ orderInfo.orderId }}</div>
+          </div>
+
+          <div class="info-row">
+            <div class="info-label">商品名称</div>
+            <div class="info-value">{{ orderInfo.goodsName }}</div>
+          </div>
+
+          <div class="info-row">
+            <div class="info-label">支付方式</div>
+            <div class="info-value">{{ displayPayMethod }}</div>
+          </div>
+
+          <div class="info-row">
+            <div class="info-label">下单时间</div>
+            <div class="info-value">{{ orderInfo.createdAt }}</div>
+          </div>
+
+          <div
+            v-if="orderInfo.contact"
+            class="info-row"
+          >
+            <div class="info-label">接收邮箱</div>
+            <div class="info-value">{{ orderInfo.contact }}</div>
+          </div>
+        </div>
+
+        <!-- 操作按钮 -->
+        <div class="success-actions">
+          <el-button
+            type="primary"
+            size="medium"
+            class="action-btn"
+            @click="goToOrders"
+          >
+            查看订单详情
+          </el-button>
+          <el-button
+            size="medium"
+            class="action-btn plain"
+            @click="goHome"
+          >
+            返回首页继续逛逛
+          </el-button>
+        </div>
+
+        <!-- 温馨提示 -->
+        <div class="success-tips">
+          <p>温馨提示:</p>
+          <ul>
+            <li>若长时间未开通,请先前往「订单查询」查看订单状态;</li>
+            <li>如支付扣款但页面未正常跳转,可凭支付截图联系商家人工处理;</li>
+            <li>请妥善保管订单号,便于后续售后查询与处理。</li>
+          </ul>
+        </div>
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed, onMounted, reactive } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { SuccessFilled } from '@element-plus/icons-vue'
+import Header from './header.vue'
+
+defineOptions({ name: 'ClientOrderSuccess' })
+
+const route = useRoute()
+const router = useRouter()
+
+const orderInfo = reactive({
+  orderId: '',
+  goodsName: '',
+  amount: 0,
+  payMethod: '',
+  contact: '',
+  createdAt: ''
+})
+
+const displayAmount = computed(() => {
+  const num = Number(orderInfo.amount || 0)
+  if (Number.isNaN(num)) return '0.00'
+  return num.toFixed(2)
+})
+
+const displayPayMethod = computed(() => {
+  const map = {
+    alipay: '支付宝',
+    wechat: '微信支付'
+  }
+  return map[orderInfo.payMethod] || orderInfo.payMethod
+})
+
+const initFromRoute = () => {
+  const query = route?.query || {}
+  const now = new Date()
+  const defaultDate = `${now.getFullYear()}-${(now.getMonth() + 1)
+    .toString()
+    .padStart(2, '0')}-${now
+    .getDate()
+    .toString()
+    .padStart(2, '0')} ${now
+    .getHours()
+    .toString()
+    .padStart(2, '0')}:${now
+    .getMinutes()
+    .toString()
+    .padStart(2, '0')}`
+
+  orderInfo.orderId = query.orderId || query.out_trade_no || '示例订单号 202312030001'
+  orderInfo.goodsName = query.goodsName || query.subject || '惊鸿·日卡'
+  orderInfo.amount = query.amount || query.totalAmount || query.total_fee || 25
+  orderInfo.payMethod = query.payMethod || query.pay_method || 'alipay'
+  orderInfo.contact = query.email || query.contact || ''
+  orderInfo.createdAt = query.createdAt || defaultDate
+}
+
+const goToOrders = () => {
+  router.push('/user/orders')
+}
+
+const goHome = () => {
+  router.push('/user/home')
+}
+
+onMounted(() => {
+  initFromRoute()
+})
+</script>
+
+<style scoped lang="scss">
+.client-success {
+  min-height: 100vh;
+  background: #f5f7fa;
+  display: flex;
+  flex-direction: column;
+}
+
+.success-body {
+  width: 60%;
+  margin: 32px auto 40px;
+
+  @media (max-width: 992px) {
+    width: 90%;
+  }
+
+  @media (max-width: 768px) {
+    width: 100%;
+    padding: 0 16px;
+    margin-top: 20px;
+  }
+}
+
+.success-card {
+  border-radius: 16px;
+  border: none;
+  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
+}
+
+.success-header {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  margin-bottom: 24px;
+
+  @media (max-width: 768px) {
+    align-items: flex-start;
+    gap: 14px;
+  }
+}
+
+.success-icon-wrapper {
+  flex-shrink: 0;
+}
+
+.success-icon {
+  width: 64px;
+  height: 64px;
+  border-radius: 50%;
+  background: #f0f9eb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #67c23a;
+  box-shadow: 0 0 0 6px rgba(103, 194, 58, 0.12);
+
+  .success-icon-inner {
+    font-size: 32px;
+  }
+
+  @media (max-width: 768px) {
+    width: 52px;
+    height: 52px;
+
+    .success-icon-inner {
+      font-size: 26px;
+    }
+  }
+}
+
+.success-text {
+  flex: 1;
+  min-width: 0;
+}
+
+.success-title {
+  margin: 0 0 6px;
+  font-size: 22px;
+  font-weight: 600;
+  color: #303133;
+
+  @media (max-width: 768px) {
+    font-size: 18px;
+  }
+}
+
+.success-subtitle {
+  margin: 0;
+  font-size: 13px;
+  color: #909399;
+  line-height: 1.7;
+}
+
+.success-info {
+  margin-top: 8px;
+  border-radius: 12px;
+  background: linear-gradient(90deg, rgba(255, 255, 255, 0.95), #f0f9ff);
+  border: 1px solid #e4f3ff;
+  padding: 16px 18px;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+
+  @media (max-width: 768px) {
+    padding: 12px 12px;
+    gap: 8px;
+  }
+}
+
+.info-row {
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+
+  .info-label {
+    width: 72px;
+    color: #909399;
+    flex-shrink: 0;
+  }
+
+  .info-value {
+    flex: 1;
+    color: #303133;
+    word-break: break-all;
+  }
+}
+
+.main-amount {
+  padding-bottom: 8px;
+  margin-bottom: 6px;
+  border-bottom: 1px dashed #dcdfe6;
+}
+
+.info-value.amount {
+  display: flex;
+  align-items: baseline;
+  gap: 2px;
+  color: #f56c6c;
+  font-weight: 600;
+
+  .currency {
+    font-size: 18px;
+  }
+
+  .number {
+    font-size: 26px;
+  }
+
+  @media (max-width: 768px) {
+    .currency {
+      font-size: 16px;
+    }
+
+    .number {
+      font-size: 22px;
+    }
+  }
+}
+
+.success-actions {
+  margin-top: 20px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+
+  .action-btn {
+    min-width: 160px;
+    border-radius: 22px;
+  }
+
+  .plain {
+    border-color: #dcdfe6;
+  }
+
+  @media (max-width: 768px) {
+    flex-direction: column;
+
+    .action-btn {
+      width: 100%;
+    }
+  }
+}
+
+.success-tips {
+  margin-top: 18px;
+  padding: 12px 14px;
+  border-radius: 10px;
+  background: #fdf6ec;
+  border: 1px solid #faecd8;
+  font-size: 12px;
+  color: #a87b35;
+  line-height: 1.7;
+
+  p {
+    margin: 0 0 4px;
+    font-weight: 500;
+  }
+
+  ul {
+    margin: 0;
+    padding-left: 18px;
+  }
+
+  li + li {
+    margin-top: 2px;
+  }
+}
+</style>
+

+ 331 - 130
web/src/views/frontend/index.vue

@@ -1,146 +1,347 @@
 <template>
-    <div>
-        <Header />
-        <el-container class="container">
-            <el-main class="main">
-                <div class="main-container">
-                    <div class="main-left">
-                        <div class="main-title">{{ siteConfig.siteName }}</div>
-                        <div class="main-content">
-                            {{ $t('index.Steve Jobs') }}
-                        </div>
-                        <el-button
-                            v-if="memberCenter.state.open"
-                            @click="$router.push(memberCenterBaseRoutePath)"
-                            class="container-button"
-                            color="#ffffff"
-                            size="large"
-                        >
-                            {{ $t('Member Center') }}
-                        </el-button>
-                    </div>
-                    <div class="main-right">
-                        <img :src="indexCover" alt="" />
-                    </div>
+    <div class="client-home">
+      <!-- 头部 -->
+      <Header />
+      <!-- 购买商品层 -->
+      <div class="home-goods">
+        <el-card class="box-card">
+          <template #header>
+            <div class="clearfix">
+              <span>
+                <el-icon>
+                  <Goods />
+                </el-icon>
+                商品列表
+              </span>
+            </div>
+          </template>
+          <div class="goods-list">
+            <!-- 商品列表 -->
+            <div class="goods-list-option">
+              <div class="option-scroll-wrapper">
+                <div
+                  v-for="(category, index) in goodsCategories"
+                  :key="index"
+                  class="option-item"
+                  :class="{ active: selectedCategory === index }"
+                  @click="selectCategory(index)"
+                >
+                  <div class="option-icon">
+                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                      <path
+                        d="M12 2L2 7L12 12L22 7L12 2Z"
+                        stroke="currentColor"
+                        stroke-width="2"
+                        stroke-linecap="round"
+                        stroke-linejoin="round"
+                      />
+                      <path
+                        d="M2 17L12 22L22 17"
+                        stroke="currentColor"
+                        stroke-width="2"
+                        stroke-linecap="round"
+                        stroke-linejoin="round"
+                      />
+                      <path
+                        d="M2 12L12 17L22 12"
+                        stroke="currentColor"
+                        stroke-width="2"
+                        stroke-linecap="round"
+                        stroke-linejoin="round"
+                      />
+                    </svg>
+                  </div>
+                  <span class="option-text">{{ category.name }}</span>
                 </div>
-            </el-main>
-        </el-container>
-        <Footer />
+              </div>
+            </div>
+  
+            <!-- 商品下的套餐列表 -->
+            <GoodsList :goods-list="goodsList" @handleClick="gotoBuy" />
+          </div>
+        </el-card>
+      </div>
     </div>
-</template>
-
+  </template>
+  
 <script setup lang="ts">
-import indexCover from '/@/assets/index/index-cover.svg'
-import { useSiteConfig } from '/@/stores/siteConfig'
-import { useMemberCenter } from '/@/stores/memberCenter'
-import Header from '/@/layouts/frontend/components/header.vue'
-import Footer from '/@/layouts/frontend/components/footer.vue'
-import { memberCenterBaseRoutePath } from '/@/router/static/memberCenterBase'
+import { ref } from 'vue'
+import { Goods } from '@element-plus/icons-vue'
+  import { useRouter } from 'vue-router'
+  import Header from './components/header.vue'
+  import GoodsList from './components/goods.vue'
+
+  type GoodsCategory = {
+    name: string
+    id: number
+  }
 
-const siteConfig = useSiteConfig()
-const memberCenter = useMemberCenter()
-</script>
+  type GoodsItem = {
+    id: string | number
+    img: string
+    name: string
+    price: number
+    sold: number
+    status: string
+    type: number // 类型 1.个人  2.企业
+  }
 
-<style scoped lang="scss">
-.container-button {
-    margin: 0 15px 15px 0;
-}
-.container {
-    width: 100vw;
-    height: 100vh;
-    background: url(/@/assets/bg.jpg) repeat;
-    color: var(--el-color-white);
-    .main {
-        height: calc(100vh - 120px);
-        padding: 0;
-        .main-container {
+  const router = useRouter()
+
+  const selectedCategory = ref(0)
+  const goodsCategories = ref<GoodsCategory[]>([
+    { name: '棱镜', id: 1 },
+    { name: '聚德神笔', id: 2 }
+  ])
+  const goodsList = ref<GoodsItem[]>([
+    {
+      id: 1,
+      img: 'https://picsum.photos/200/300',
+      name: '惊鸿·日卡',
+      price: 5.00,
+      sold: 318,
+      status: '即将售罄',
+      type: 1 // 类型 1.个人  2.企业
+    },
+    {
+      id: 2,
+      img: 'https://picsum.photos/200',
+      name: '扶摇·周卡',
+      price: 23.00,
+      sold: 79,
+      status: '即将售罄',
+      type: 1 // 类型 1.个人  2.企业
+    },
+    {
+      id: 3,
+      img: 'https://picsum.photos/200/300',
+      name: '望舒·月卡',
+      price: 52.00,
+      sold: 265,
+      status: '一般',
+      type: 2 // 类型 1.个人  2.企业
+    },
+    {
+      id: 4,
+      img: 'https://picsum.photos/310',
+      name: '惊鸿·日卡',
+      price: 5.00,
+      sold: 318,
+      status: '即将售罄',
+      type: 1 // 类型 1.个人  2.企业
+    },
+    {
+      id: 5,
+      img: 'https://picsum.photos/200',
+      name: '扶摇·周卡',
+      price: 23.00,
+      sold: 79,
+      status: '即将售罄',
+      type: 2 // 类型 1.个人  2.企业
+    },
+    {
+      id: 6,
+      img: 'https://picsum.photos/300',
+      name: '望舒·月卡',
+      price: 52.00,
+      sold: 265,
+      status: '一般',
+      type: 1 // 类型 1.个人  2.企业
+    }
+  ])
+
+  // 选择产品
+  const selectCategory = (index: number) => {
+    selectedCategory.value = index
+    // 这里可以触发商品列表的更新
+    // loadGoods(goodsCategories.value[index].id)
+  }
+
+  // 去下单页面
+  const gotoBuy = (item: GoodsItem) => {
+    router.push({
+      name: 'userOrder',
+      params: {
+        goodsId: item.id
+      }
+    })
+  }
+  </script>
+  
+  <style scoped lang="scss">
+  .client-home {
+    min-height: 100vh;
+    background: #f5f7fa;
+    display: flex;
+    flex-direction: column;
+  
+    .home-goods {
+      width: 80%;
+      margin: 0 auto;
+      margin-top: 20px;
+  
+      .goods-list {
+        .goods-list-option {
+          margin-bottom: 24px;
+  
+          .option-scroll-wrapper {
             display: flex;
-            height: 100%;
-            width: 66%;
-            margin: 0 auto;
-            align-items: center;
-            justify-content: space-between;
-            .main-left {
-                padding-right: 50px;
-                .main-title {
-                    font-size: 45px;
+            gap: 12px;
+            overflow-x: auto;
+            overflow-y: hidden;
+            padding: 4px 0 12px 0;
+            scrollbar-width: thin;
+            scrollbar-color: rgba(64, 158, 255, 0.3) transparent;
+  
+            // 自定义滚动条样式(Webkit浏览器)
+            &::-webkit-scrollbar {
+              height: 6px;
+            }
+  
+            &::-webkit-scrollbar-track {
+              background: transparent;
+              border-radius: 3px;
+            }
+  
+            &::-webkit-scrollbar-thumb {
+              background: rgba(64, 158, 255, 0.3);
+              border-radius: 3px;
+  
+              &:hover {
+                background: rgba(64, 158, 255, 0.5);
+              }
+            }
+  
+            // 平滑滚动
+            scroll-behavior: smooth;
+  
+            .option-item {
+              display: flex;
+              align-items: center;
+              gap: 8px;
+              padding: 10px 20px;
+              background: #ffffff;
+              border: 1.5px solid #303133;
+              border-radius: 24px;
+              cursor: pointer;
+              transition: all 0.3s ease;
+              white-space: nowrap;
+              flex-shrink: 0;
+              user-select: none;
+  
+              .option-icon {
+                width: 28px;
+                height: 28px;
+                border-radius: 50%;
+                background: #303133;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                color: #ffffff;
+                flex-shrink: 0;
+                transition: all 0.3s ease;
+  
+                svg {
+                  width: 16px;
+                  height: 16px;
                 }
-                .main-content {
-                    padding-top: 20px;
-                    padding-bottom: 40px;
-                    font-size: var(--el-font-size-large);
+              }
+  
+              .option-text {
+                color: #303133;
+                font-size: 14px;
+                font-weight: 500;
+                letter-spacing: 0.3px;
+                transition: color 0.3s ease;
+              }
+  
+              &:hover {
+                transform: translateY(-2px);
+                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+                border-color: #606266;
+              }
+  
+              // 激活状态:蓝色底色,白字
+              &.active {
+                background: #409eff;
+                border-color: #409eff;
+                box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
+                transform: translateY(-2px);
+  
+                .option-icon {
+                  background: rgba(255, 255, 255, 0.2);
+                  color: #ffffff;
                 }
-            }
-            .main-right {
-                img {
-                    width: 380px;
+  
+                .option-text {
+                  color: #ffffff;
                 }
+  
+                &:hover {
+                  background: #66b1ff;
+                  border-color: #66b1ff;
+                  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
+                }
+              }
+  
+              &:active {
+                transform: translateY(0);
+              }
             }
+          }
         }
+      }
     }
-}
-.header {
-    background-color: transparent !important;
-    box-shadow: none !important;
-    position: fixed;
-    width: 100%;
-    :deep(.header-logo) {
-        span {
-            padding-left: 4px;
-            color: var(--el-color-white);
-        }
-    }
-    :deep(.frontend-header-menu) {
-        background: transparent;
-        .el-menu-item,
-        .el-sub-menu .el-sub-menu__title {
-            color: var(--el-color-white);
-            &.is-active {
-                color: var(--el-color-white) !important;
-            }
-            &:hover {
-                background-color: transparent !important;
-                color: var(--el-menu-hover-text-color);
+  
+    // 移动端样式
+    @media (max-width: 768px) {
+      .home-goods {
+        width: 100%;
+        padding: 0 16px;
+        margin-top: 16px;
+  
+        .goods-list {
+          .goods-list-option {
+            margin-bottom: 16px;
+  
+            .option-scroll-wrapper {
+              gap: 8px;
+              padding: 2px 0 8px 0;
+  
+              .option-item {
+                padding: 6px 14px;
+                border-radius: 18px;
+                border-width: 1px;
+                gap: 6px;
+  
+                .option-icon {
+                  width: 22px;
+                  height: 22px;
+  
+                  svg {
+                    width: 12px;
+                    height: 12px;
+                  }
+                }
+  
+                .option-text {
+                  font-size: 12px;
+                  letter-spacing: 0.2px;
+                }
+  
+                &:hover {
+                  transform: translateY(-1px);
+                }
+  
+                &.active {
+                  transform: translateY(-1px);
+                }
+              }
             }
+          }
         }
+      }
     }
-}
-.footer {
-    color: var(--el-text-color-secondary);
-    background-color: transparent !important;
-    position: fixed;
-    bottom: 0;
-}
-
-@media screen and (max-width: 1024px) {
-    .container {
-        .main {
-            height: unset;
-        }
-    }
-    .main-container {
-        width: 90% !important;
-        flex-wrap: wrap;
-        align-content: center;
-        justify-content: center !important;
-        .main-right {
-            padding-top: 50px;
-        }
-    }
-}
-@media screen and (max-width: 375px) {
-    .main-right img {
-        width: 300px !important;
-    }
-}
-@media screen and (max-height: 650px) {
-    .main-right img {
-        display: none;
-    }
-}
-@at-root html.dark {
-    .container {
-        background: url(/@/assets/bg-dark.jpg) repeat;
-    }
-}
-</style>
+  }
+  </style>
+  

+ 364 - 575
web/src/views/frontend/user/login.vue

@@ -1,590 +1,379 @@
 <template>
-    <div class="login">
-        <el-container class="is-vertical">
-            <Header />
-            <el-main class="frontend-footer-brother">
-                <el-row justify="center">
-                    <el-col :span="16" :xs="24">
-                        <div v-if="memberCenter.state.open" class="login-box">
-                            <div class="login-title">
-                                {{ t('user.login.' + state.form.tab) + t('user.login.reach') + siteConfig.siteName }}
-                            </div>
-                            <el-form ref="formRef" @keyup.enter="onSubmitPre" :rules="rules" :model="state.form">
-                                <!-- 注册验证方式 -->
-                                <el-form-item v-if="state.form.tab == 'register'">
-                                    <el-radio-group size="large" v-model="state.form.registerType">
-                                        <el-radio
-                                            class="register-verification-radio"
-                                            value="email"
-                                            :disabled="!state.accountVerificationType.includes('email')"
-                                            border
-                                        >
-                                            {{ t('user.login.Via email') + t('user.login.register') }}
-                                        </el-radio>
-                                        <el-radio
-                                            class="register-verification-radio"
-                                            value="mobile"
-                                            :disabled="!state.accountVerificationType.includes('mobile')"
-                                            border
-                                        >
-                                            {{ t('user.login.Via mobile number') + t('user.login.register') }}
-                                        </el-radio>
-                                    </el-radio-group>
-                                </el-form-item>
-
-                                <!-- 登录注册用户名 -->
-                                <el-form-item prop="username">
-                                    <el-input
-                                        v-model="state.form.username"
-                                        :placeholder="
-                                            state.form.tab == 'register'
-                                                ? t('Please input field', { field: t('user.login.User name') })
-                                                : t('Please input field', { field: t('user.login.account') })
-                                        "
-                                        :clearable="true"
-                                        size="large"
-                                    >
-                                        <template #prefix>
-                                            <Icon name="fa fa-user" size="16" color="var(--el-input-icon-color)" />
-                                        </template>
-                                    </el-input>
-                                </el-form-item>
-
-                                <!-- 登录注册密码 -->
-                                <el-form-item prop="password">
-                                    <el-input
-                                        v-model="state.form.password"
-                                        :placeholder="t('Please input field', { field: t('user.login.password') })"
-                                        type="password"
-                                        show-password
-                                        size="large"
-                                    >
-                                        <template #prefix>
-                                            <Icon name="fa fa-unlock-alt" size="16" color="var(--el-input-icon-color)" />
-                                        </template>
-                                    </el-input>
-                                </el-form-item>
-
-                                <!-- 注册手机号 -->
-                                <el-form-item v-if="state.form.tab == 'register' && state.form.registerType == 'mobile'" prop="mobile">
-                                    <el-input
-                                        v-model="state.form.mobile"
-                                        :placeholder="t('Please input field', { field: t('user.login.mobile') })"
-                                        :clearable="true"
-                                        size="large"
-                                    >
-                                        <template #prefix>
-                                            <Icon name="fa fa-tablet" size="16" color="var(--el-input-icon-color)" />
-                                        </template>
-                                    </el-input>
-                                </el-form-item>
-
-                                <!-- 注册邮箱 -->
-                                <el-form-item v-if="state.form.tab == 'register' && state.form.registerType == 'email'" prop="email">
-                                    <el-input
-                                        v-model="state.form.email"
-                                        :placeholder="t('Please input field', { field: t('user.login.email') })"
-                                        :clearable="true"
-                                        size="large"
-                                    >
-                                        <template #prefix>
-                                            <Icon name="fa fa-envelope" size="16" color="var(--el-input-icon-color)" />
-                                        </template>
-                                    </el-input>
-                                </el-form-item>
-
-                                <!-- 注册验证码 -->
-                                <el-form-item v-if="state.form.tab == 'register'" prop="captcha">
-                                    <el-row class="w100">
-                                        <el-col :span="16">
-                                            <el-input
-                                                size="large"
-                                                v-model="state.form.captcha"
-                                                :placeholder="t('Please input field', { field: t('user.login.Verification Code') })"
-                                                autocomplete="off"
-                                            >
-                                                <template #prefix>
-                                                    <Icon name="fa fa-ellipsis-h" size="16" color="var(--el-input-icon-color)" />
-                                                </template>
-                                            </el-input>
-                                        </el-col>
-                                        <el-col class="captcha-box" :span="8">
-                                            <el-button
-                                                size="large"
-                                                @click="sendRegisterCaptchaPre"
-                                                :loading="state.sendCaptchaLoading"
-                                                :disabled="state.codeSendCountdown <= 0 ? false : true"
-                                                type="primary"
-                                            >
-                                                {{
-                                                    state.codeSendCountdown <= 0
-                                                        ? t('user.login.send')
-                                                        : state.codeSendCountdown + t('user.login.seconds')
-                                                }}
-                                            </el-button>
-                                        </el-col>
-                                    </el-row>
-                                </el-form-item>
-
-                                <div v-if="state.form.tab != 'register'" class="form-footer">
-                                    <el-checkbox v-model="state.form.keep" :label="t('user.login.Remember me')" size="default"></el-checkbox>
-                                    <div
-                                        v-if="state.accountVerificationType.length > 0"
-                                        @click="state.showRetrievePasswordDialog = true"
-                                        class="forgot-password"
-                                    >
-                                        {{ t('user.login.Forgot your password?') }}
-                                    </div>
-                                </div>
-                                <el-form-item class="form-buttons">
-                                    <el-button class="login-btn" @click="onSubmitPre" :loading="state.formLoading" round type="primary" size="large">
-                                        {{ t('user.login.' + state.form.tab) }}
-                                    </el-button>
-                                    <el-button
-                                        v-if="state.form.tab == 'register'"
-                                        @click="switchTab(formRef, 'login')"
-                                        round
-                                        plain
-                                        type="info"
-                                        size="large"
-                                    >
-                                        {{ t('user.login.Back to login') }}
-                                    </el-button>
-                                    <el-button v-else @click="switchTab(formRef, 'register')" round plain type="info" size="large">
-                                        {{ t('user.login.No account yet? Click Register') }}
-                                    </el-button>
-                                </el-form-item>
-                                <LoginFooterMixin />
-                            </el-form>
-                        </div>
-                        <el-alert v-else :center="true" :title="$t('Member center disabled')" type="error" />
-                    </el-col>
-                </el-row>
-            </el-main>
-            <Footer />
-        </el-container>
-
-        <el-dialog
-            :close-on-click-modal="false"
-            :close-on-press-escape="false"
-            v-model="state.showRetrievePasswordDialog"
-            :title="t('user.login.Retrieve password')"
-            :width="state.dialogWidth + '%'"
-            :draggable="true"
-        >
-            <div class="retrieve-password-form">
-                <el-form
-                    ref="retrieveFormRef"
-                    @keyup.enter="onSubmitRetrieve()"
-                    :rules="retrieveRules"
-                    :model="state.retrievePasswordForm"
-                    :label-width="100"
-                >
-                    <el-form-item :label="t('user.login.Retrieval method')">
-                        <el-radio-group v-model="state.retrievePasswordForm.type">
-                            <el-radio value="email" :disabled="!state.accountVerificationType.includes('email')" border>
-                                {{ t('user.login.Via email') }}
-                            </el-radio>
-                            <el-radio value="mobile" :disabled="!state.accountVerificationType.includes('mobile')" border>
-                                {{ t('user.login.Via mobile number') }}
-                            </el-radio>
-                        </el-radio-group>
-                    </el-form-item>
-                    <el-form-item prop="account" :label="state.retrievePasswordForm.type == 'email' ? t('user.login.email') : t('user.login.mobile')">
-                        <el-input
-                            v-model="state.retrievePasswordForm.account"
-                            :placeholder="
-                                t('Please input field', {
-                                    field: state.retrievePasswordForm.type == 'email' ? t('user.login.email') : t('user.login.mobile'),
-                                })
-                            "
-                            :clearable="true"
-                        >
-                            <template #prefix>
-                                <Icon name="fa fa-user" size="16" color="var(--el-input-icon-color)" />
-                            </template>
-                        </el-input>
-                    </el-form-item>
-                    <el-form-item prop="captcha" :label="t('user.login.Verification Code')">
-                        <el-row class="w100">
-                            <el-col :span="16">
-                                <el-input
-                                    v-model="state.retrievePasswordForm.captcha"
-                                    :placeholder="t('Please input field', { field: t('user.login.Verification Code') })"
-                                    autocomplete="off"
-                                >
-                                    <template #prefix>
-                                        <Icon name="fa fa-ellipsis-h" size="16" color="var(--el-input-icon-color)" />
-                                    </template>
-                                </el-input>
-                            </el-col>
-                            <el-col class="captcha-box" :span="8">
-                                <el-button
-                                    @click="sendRetrieveCaptchaPre"
-                                    :loading="state.sendCaptchaLoading"
-                                    :disabled="state.codeSendCountdown <= 0 ? false : true"
-                                    type="primary"
-                                >
-                                    {{ state.codeSendCountdown <= 0 ? t('user.login.send') : state.codeSendCountdown + t('user.login.seconds') }}
-                                </el-button>
-                            </el-col>
-                        </el-row>
-                    </el-form-item>
-                    <el-form-item prop="password" :label="t('user.login.New password')">
-                        <el-input
-                            v-model="state.retrievePasswordForm.password"
-                            :placeholder="t('Please input field', { field: t('user.login.New password') })"
-                            show-password
-                        >
-                            <template #prefix>
-                                <Icon name="fa fa-unlock-alt" size="16" color="var(--el-input-icon-color)" />
-                            </template>
-                        </el-input>
-                    </el-form-item>
-                    <el-form-item>
-                        <el-button @click="state.showRetrievePasswordDialog = false">{{ t('Cancel') }}</el-button>
-                        <el-button :loading="state.submitRetrieveLoading" @click="onSubmitRetrieve()" type="primary">
-                            {{ t('user.login.second') }}
-                        </el-button>
-                    </el-form-item>
-                </el-form>
-            </div>
-        </el-dialog>
+    <div class="client-login">
+      <div class="client-login__panel">
+        <h1 class="client-login__title">用户登录</h1>
+        <p class="client-login__subtitle">探索并购买前沿AI工具,赋能您的效率与创意</p>
+        <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" class="client-login__form" @submit.prevent>
+          <el-form-item prop="username">
+            <el-input 
+              v-model="loginForm.username" 
+              placeholder="请输入账号" 
+              size="large"
+              clearable
+            >
+              <template #prefix>
+                <Icon name="fa fa-user" size="16" color="var(--el-input-icon-color)" />
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item prop="password">
+            <el-input 
+              v-model="loginForm.password" 
+              type="password" 
+              placeholder="请输入密码"
+              size="large"
+              show-password 
+              @keyup.enter="handleLogin"
+            >
+              <template #prefix>
+                <Icon name="fa fa-unlock-alt" size="16" color="var(--el-input-icon-color)" />
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button 
+              :loading="loading" 
+              type="primary" 
+              class="client-login__button" 
+              size="large"
+              round
+              @click="handleLogin"
+            >
+              登录
+            </el-button>
+          </el-form-item>
+        </el-form>
+        <div class="client-login__links">
+          <router-link to="/user/reset">
+            忘记密码?
+          </router-link>
+        </div>
+        <div class="client-login__actions">
+          <span>还没有账号?</span>
+          <router-link to="/user/register">立即注册</router-link>
+        </div>
+      </div>
     </div>
-</template>
-
-<script setup lang="ts">
-import { reactive, onMounted, onUnmounted, useTemplateRef } from 'vue'
-import Header from '/@/layouts/frontend/components/header.vue'
-import Footer from '/@/layouts/frontend/components/footer.vue'
-import { useSiteConfig } from '/@/stores/siteConfig'
-import { useMemberCenter } from '/@/stores/memberCenter'
-import { sendEms, sendSms } from '/@/api/common'
-import { uuid } from '/@/utils/random'
-import { useI18n } from 'vue-i18n'
-import { buildValidatorData, validatorAccount } from '/@/utils/validate'
-import { checkIn, retrievePassword } from '/@/api/frontend/user/index'
-import { useEventListener } from '@vueuse/core'
-import { onResetForm } from '/@/utils/common'
-import { useUserInfo } from '/@/stores/userInfo'
-import { useRouter } from 'vue-router'
-import { useRoute } from 'vue-router'
-import loginMounted from '/@/components/mixins/loginMounted'
-import LoginFooterMixin from '/@/components/mixins/loginFooter.vue'
-import type { FormItemRule, FormInstance } from 'element-plus'
-import clickCaptcha from '/@/components/clickCaptcha'
-let timer: number
-
-const { t } = useI18n()
-const route = useRoute()
-const router = useRouter()
-const userInfo = useUserInfo()
-const siteConfig = useSiteConfig()
-const memberCenter = useMemberCenter()
-const formRef = useTemplateRef('formRef')
-const retrieveFormRef = useTemplateRef('retrieveFormRef')
-
-interface State {
-    form: {
-        tab: 'login' | 'register'
-        email: string
-        mobile: string
-        username: string
-        password: string
-        captcha: string
-        keep: boolean
-        captchaId: string
-        captchaInfo: string
-        registerType: 'email' | 'mobile'
+  </template>
+  
+  <script setup>
+  import { ref, reactive, watch, onMounted } from 'vue'
+  import { useRouter, useRoute } from 'vue-router'
+  import { useUserInfo } from '/@/stores/userInfo'
+  import { checkIn } from '/@/api/frontend/user/index'
+  import { uuid } from '/@/utils/random'
+  import clickCaptcha from '/@/components/clickCaptcha'
+  import Icon from '/@/components/icon/index.vue'
+  
+  defineOptions({
+    name: 'ClientLogin'
+  })
+  
+  const router = useRouter()
+  const route = useRoute()
+  const userInfo = useUserInfo()
+  
+  const loginFormRef = ref(null)
+  const loading = ref(false)
+  const redirect = ref(undefined)
+  const otherQuery = ref({})
+  
+  // 验证码开关
+  const userLoginCaptchaSwitch = ref(true)
+  
+  // 登录成功后要跳转的URL
+  const to = ref(Array.isArray(route.query.to) ? route.query.to[0] : (route.query.to || ''))
+  
+  const loginForm = reactive({
+    username: '',
+    password: '',
+    keep: false,
+    captchaId: uuid(),
+    captchaInfo: ''
+  })
+  
+  const validateUsername = (rule, value, callback) => {
+    if (!value) {
+      callback(new Error('请输入账号'))
+    } else {
+      callback()
     }
-    formLoading: boolean
-    showRetrievePasswordDialog: boolean
-    retrievePasswordForm: {
-        type: 'email' | 'mobile'
-        account: string
-        captcha: string
-        password: string
+  }
+  
+  const validatePassword = (rule, value, callback) => {
+    if (!value) {
+      callback(new Error('请输入密码'))
+    } else if (value.length < 6) {
+      callback(new Error('密码长度不能小于6位'))
+    } else {
+      callback()
     }
-    dialogWidth: number
-    userLoginCaptchaSwitch: boolean
-    accountVerificationType: string[]
-    codeSendCountdown: number
-    submitRetrieveLoading: boolean
-    sendCaptchaLoading: boolean
-    to: string
-}
-
-const state: State = reactive({
-    form: {
-        tab: 'login',
-        email: '',
-        mobile: '',
-        username: '',
-        password: '',
-        captcha: '',
-        keep: false,
-        captchaId: uuid(),
-        captchaInfo: '',
-        registerType: 'email',
-    },
-    formLoading: false,
-    showRetrievePasswordDialog: false,
-    retrievePasswordForm: {
-        type: 'email',
-        account: '',
-        captcha: '',
-        password: '',
-    },
-    dialogWidth: 36,
-    userLoginCaptchaSwitch: true,
-    accountVerificationType: [],
-    codeSendCountdown: 0,
-    submitRetrieveLoading: false,
-    sendCaptchaLoading: false,
-    // 登录成功后要跳转的URL
-    to: route.query.to as string,
-})
-
-const rules: Partial<Record<string, FormItemRule[]>> = reactive({
-    email: [
-        buildValidatorData({ name: 'required', title: t('user.login.email') }),
-        buildValidatorData({ name: 'email', title: t('user.login.email') }),
-    ],
-    username: [
-        buildValidatorData({ name: 'required', title: t('user.login.User name') }),
-        {
-            validator: (rule: any, val: string, callback: Function) => {
-                if (state.form.tab == 'register') {
-                    return validatorAccount(rule, val, callback)
-                } else {
-                    callback()
-                }
-            },
-            trigger: 'blur',
-        },
-    ],
-    password: [buildValidatorData({ name: 'required', title: t('user.login.password') }), buildValidatorData({ name: 'password' })],
-    mobile: [buildValidatorData({ name: 'required', title: t('user.login.mobile') }), buildValidatorData({ name: 'mobile' })],
-    captcha: [buildValidatorData({ name: 'required', title: t('user.login.Verification Code') })],
-})
-
-const retrieveRules: Partial<Record<string, FormItemRule[]>> = reactive({
-    account: [buildValidatorData({ name: 'required', title: t('user.login.Account name') })],
-    captcha: [buildValidatorData({ name: 'required', title: t('user.login.Verification Code') })],
-    password: [buildValidatorData({ name: 'required', title: t('user.login.password') }), buildValidatorData({ name: 'password' })],
-})
-
-const resize = () => {
-    let clientWidth = document.documentElement.clientWidth
-    let width = 36
-    if (clientWidth <= 790) {
-        width = 92
-    } else if (clientWidth <= 910) {
-        width = 56
-    } else if (clientWidth <= 1260) {
-        width = 46
-    }
-    state.dialogWidth = width
-}
-
-const onSubmitPre = () => {
-    formRef.value?.validate((valid) => {
-        if (!valid) return
-        if (state.form.tab == 'login' && state.userLoginCaptchaSwitch) {
-            clickCaptcha(state.form.captchaId, (captchaInfo: string) => onSubmit(captchaInfo))
-        } else {
-            onSubmit()
-        }
-    })
-}
-const onSubmit = (captchaInfo = '') => {
-    state.formLoading = true
-    state.form.captchaInfo = captchaInfo
-    checkIn('post', state.form)
-        .then((res) => {
-            userInfo.dataFill(res.data.userInfo, false)
-            if (state.to) return (location.href = state.to)
-            router.push({ path: res.data.routePath })
+  }
+  
+  const loginRules = reactive({
+    username: [{ required: true, trigger: 'blur', validator: validateUsername }],
+    password: [{ required: true, trigger: 'blur', validator: validatePassword }]
+  })
+  
+  const getOtherQuery = (query) => {
+    return Object.keys(query).reduce((acc, cur) => {
+      if (cur !== 'redirect' && cur !== 'to') {
+        acc[cur] = query[cur]
+      }
+      return acc
+    }, {})
+  }
+  
+  // 提交登录前的预处理(验证码检查)
+  const handleLogin = () => {
+    if (!loginFormRef.value) return
+    
+    loginFormRef.value.validate((valid) => {
+      if (!valid) return
+      
+      // 如果启用了验证码,先进行验证码验证
+      if (userLoginCaptchaSwitch.value) {
+        clickCaptcha(loginForm.captchaId, (captchaInfo) => {
+          onSubmit(captchaInfo)
         })
-        .finally(() => {
-            state.formLoading = false
-        })
-}
-const onSubmitRetrieve = () => {
-    if (!retrieveFormRef.value) return
-    retrieveFormRef.value.validate((valid) => {
-        if (valid) {
-            state.submitRetrieveLoading = true
-            retrievePassword(state.retrievePasswordForm)
-                .then((res) => {
-                    state.submitRetrieveLoading = false
-                    if (res.code == 1) {
-                        state.showRetrievePasswordDialog = false
-                        endTiming()
-                        onResetForm(retrieveFormRef.value)
-                    }
-                })
-                .catch(() => {
-                    state.submitRetrieveLoading = false
-                })
-        }
-    })
-}
-
-const sendRegisterCaptchaPre = () => {
-    if (state.codeSendCountdown > 0) return
-    formRef.value!.validateField([state.form.registerType, 'username', 'password']).then((valid) => {
-        if (!valid) return
-        clickCaptcha(state.form.captchaId, (captchaInfo: string) => sendRegisterCaptcha(captchaInfo))
+      } else {
+        onSubmit()
+      }
     })
-}
-const sendRegisterCaptcha = (captchaInfo: string) => {
-    state.sendCaptchaLoading = true
-    const func = state.form.registerType == 'email' ? sendEms : sendSms
-    func(state.form[state.form.registerType], 'user_register', {
-        captchaInfo,
-        captchaId: state.form.captchaId,
+  }
+  
+  // 提交登录
+  const onSubmit = (captchaInfo = '') => {
+    loading.value = true
+    loginForm.captchaInfo = captchaInfo
+    
+    checkIn('post', {
+      tab: 'login',
+      username: loginForm.username,
+      password: loginForm.password,
+      keep: loginForm.keep,
+      captchaId: loginForm.captchaId,
+      captchaInfo: loginForm.captchaInfo
+    }).then((res) => {
+      // 登录成功,更新用户信息
+      if (res.data && res.data.userInfo) {
+        userInfo.dataFill(res.data.userInfo, false)
+      }
+      
+      // 如果有指定的跳转URL,则跳转
+      if (to.value) {
+        location.href = to.value
+        return
+      }
+      
+      // 使用返回的routePath或redirect指定的页面
+      router.push({
+        path: res.data?.routePath || redirect.value || '/user/home',
+        query: otherQuery.value
+      })
+    }).catch(() => {
+      // 登录失败,错误信息由 axios 拦截器处理
+      // 如果验证失败,重新生成验证码ID
+      if (userLoginCaptchaSwitch.value) {
+        loginForm.captchaId = uuid()
+      }
+    }).finally(() => {
+      loading.value = false
     })
-        .then((res) => {
-            if (res.code == 1) startTiming(60)
-        })
-        .finally(() => {
-            state.sendCaptchaLoading = false
-        })
-}
-
-const sendRetrieveCaptchaPre = () => {
-    if (state.codeSendCountdown > 0) return
-    retrieveFormRef.value!.validateField('account').then((valid) => {
-        if (!valid) return
-        clickCaptcha(state.form.captchaId, (captchaInfo: string) => sendRetrieveCaptcha(captchaInfo))
-    })
-}
-const sendRetrieveCaptcha = (captchaInfo: string) => {
-    state.sendCaptchaLoading = true
-    const func = state.retrievePasswordForm.type == 'email' ? sendEms : sendSms
-    func(state.retrievePasswordForm.account, 'user_retrieve_pwd', {
-        captchaInfo,
-        captchaId: state.form.captchaId,
-    })
-        .then((res) => {
-            if (res.code == 1) startTiming(60)
-        })
-        .finally(() => {
-            state.sendCaptchaLoading = false
-        })
-}
-
-const switchTab = (formRef: FormInstance | null | undefined = undefined, tab: 'login' | 'register') => {
-    state.form.tab = tab
-    if (tab == 'register') state.form.username = ''
-    if (formRef) formRef.clearValidate()
-}
-
-const startTiming = (seconds: number) => {
-    state.codeSendCountdown = seconds
-    timer = window.setInterval(() => {
-        state.codeSendCountdown--
-        if (state.codeSendCountdown <= 0) {
-            endTiming()
-        }
-    }, 1000)
-}
-const endTiming = () => {
-    state.codeSendCountdown = 0
-    clearInterval(timer)
-}
-
-onMounted(async () => {
-    if (await loginMounted()) return
-
-    resize()
-    useEventListener(window, 'resize', resize)
-
+  }
+  
+  // 监听路由变化
+  watch(
+    () => route.query,
+    (query) => {
+      if (query) {
+        redirect.value = query.redirect
+        to.value = Array.isArray(query.to) ? query.to[0] : (query.to || '')
+        otherQuery.value = getOtherQuery(query)
+      }
+    },
+    { immediate: true }
+  )
+  
+  // 组件挂载时获取登录配置
+  onMounted(() => {
     checkIn('get').then((res) => {
-        state.userLoginCaptchaSwitch = res.data.userLoginCaptchaSwitch
-        state.accountVerificationType = res.data.accountVerificationType
-        state.retrievePasswordForm.type = res.data.accountVerificationType.length > 0 ? res.data.accountVerificationType[0] : ''
+      if (res.data) {
+        userLoginCaptchaSwitch.value = res.data.userLoginCaptchaSwitch !== undefined 
+          ? res.data.userLoginCaptchaSwitch 
+          : true
+      }
     })
-
-    if (route.query.type == 'register') state.form.tab = 'register'
-})
-onUnmounted(() => {
-    state.codeSendCountdown = 0
-    endTiming()
-})
-</script>
-
-<style scoped lang="scss">
-.login-box {
-    width: 460px;
-    margin: 40px auto;
-    padding: 10px 60px 20px 60px;
-    background-color: var(--ba-bg-color-overlay);
-}
-.login-title {
-    text-align: center;
-    font-size: var(--el-font-size-large);
-    line-height: 100px;
-    user-select: none;
-}
-:deep(.el-input--large) .el-input__wrapper {
-    padding: 4px 15px;
-}
-.form-buttons {
-    padding-top: 20px;
-    .el-button {
-        width: 100%;
-        letter-spacing: 2px;
-        font-weight: 300;
-        margin-top: 20px;
-        margin-left: 0;
-    }
-}
-.register-verification-radio {
-    margin-top: 10px;
-}
-.captcha-box {
+  })
+  </script>
+  
+  <style scoped>
+  .client-login {
+    min-height: 100vh;
     display: flex;
+    justify-content: center;
     align-items: center;
-    justify-content: flex-end;
-    .el-button {
-        width: 90%;
-        height: 100%;
-    }
-}
-.form-footer {
+    background: url('/@/assets/images/login_bg.jpg') center center / cover no-repeat;
+    position: relative;
+  }
+  
+  .client-login::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(255, 255, 255, 0.1);
+    backdrop-filter: blur(2px);
+  }
+  
+  .client-login__panel {
+    width: 420px;
+    padding: 48px 40px 40px;
+    border-radius: 16px;
+    background: rgba(255, 255, 255, 0.98);
+    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+    backdrop-filter: blur(10px);
+    position: relative;
+    z-index: 1;
+    transition: transform 0.3s ease, box-shadow 0.3s ease;
+  }
+  
+  .client-login__panel:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
+  }
+  
+  .client-login__title {
+    margin: 0 0 8px 0;
+    font-size: 28px;
+    font-weight: 700;
+    text-align: center;
+    background: linear-gradient(135deg, #409eff 0%, #638fee 100%);
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+    background-clip: text;
+    letter-spacing: 0.5px;
+  }
+  
+  .client-login__subtitle {
+    margin: 0 0 32px 0;
+    font-size: 14px;
+    text-align: center;
+    color: #909399;
+    line-height: 1.5;
+  }
+  
+  .client-login__form {
+    margin-top: 8px;
+  }
+  
+  .client-login__form :deep(.el-form-item) {
+    margin-bottom: 24px;
+  }
+  
+  .client-login__form :deep(.el-input__wrapper) {
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+    transition: all 0.3s ease;
+  }
+  
+  .client-login__form :deep(.el-input__wrapper:hover) {
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
+  }
+  
+  .client-login__form :deep(.el-input.is-focus .el-input__wrapper) {
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.25);
+  }
+  
+  .client-login__form :deep(.el-input__prefix) {
+    padding-left: 12px;
+  }
+  
+  .client-login__actions {
+    margin-top: 20px;
+    font-size: 14px;
+    color: #606266;
+    text-align: center;
+  }
+  
+  .client-login__actions a {
+    color: #409eff;
+    margin-left: 6px;
+    text-decoration: none;
+    transition: color 0.2s;
+    font-weight: 500;
+  }
+  
+  .client-login__actions a:hover {
+    color: #66b1ff;
+  }
+  
+  .client-login__button {
+    width: 100%;
+    height: 44px;
+    font-size: 16px;
+    font-weight: 600;
+    letter-spacing: 1px;
+    background: #409eff;
+    border: none;
+    transition: all 0.3s ease;
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+  }
+  
+  .client-login__button:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
+  }
+  
+  .client-login__button:active {
+    transform: translateY(0);
+  }
+  
+  .client-login__links {
+    margin-top: 8px;
+    margin-bottom: 8px;
     display: flex;
+    justify-content: flex-end;
+    gap: 6px;
+    font-size: 13px;
+    color: #909399;
+  }
+  
+  .client-login__links a {
+    display: inline-flex;
     align-items: center;
-    .forgot-password {
-        color: var(--ba-color-primary-light);
-        margin-left: auto;
-        user-select: none;
-        cursor: pointer;
-    }
-}
-.retrieve-password-form {
-    display: flex;
-    justify-content: center;
-    margin-right: 50px;
-}
-@media screen and (max-width: 768px) {
-    .login-box {
-        width: 100%;
-        margin: 0 auto;
-    }
-    .retrieve-password-form {
-        margin-right: 0;
+    gap: 4px;
+    color: #409eff;
+    text-decoration: none;
+    transition: all 0.2s;
+    font-weight: 500;
+  }
+  
+  .client-login__links a:hover {
+    color: #66b1ff;
+    transform: translateX(2px);
+  }
+  
+  .client-login__tips {
+    margin-top: 12px;
+    font-size: 12px;
+    color: #909399;
+    line-height: 1.6;
+  }
+  
+  /* 响应式设计 */
+  @media (max-width: 768px) {
+    .client-login__panel {
+      width: 90%;
+      max-width: 420px;
+      padding: 36px 28px 32px;
     }
-}
-
-// 暗黑样式
-@at-root .dark {
-    .form-buttons {
-        .login-btn {
-            --el-button-bg-color: var(--el-color-primary-light-5);
-            --el-button-border-color: rgba(240, 252, 241, 0.1);
-        }
+    
+    .client-login__title {
+      font-size: 24px;
     }
-}
-</style>
+  }
+  </style>
+  

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.