jerry 4 ماه پیش
کامیت
0fa8099fa3
76فایلهای تغییر یافته به همراه12543 افزوده شده و 0 حذف شده
  1. 4 0
      .env.local
  2. 2 0
      .gitignore
  3. 5 0
      next-env.d.ts
  4. 13 0
      next.config.js
  5. 2261 0
      package-lock.json
  6. 27 0
      package.json
  7. 6 0
      postcss.config.js
  8. 80 0
      src/app/admin/layout.tsx
  9. 255 0
      src/app/admin/orders/new/page.tsx
  10. 228 0
      src/app/admin/orders/page.tsx
  11. 206 0
      src/app/admin/page.tsx
  12. 132 0
      src/app/admin/payments/page.tsx
  13. 234 0
      src/app/admin/products/page.tsx
  14. 204 0
      src/app/admin/slots/page.tsx
  15. 211 0
      src/app/admin/tasks/page.tsx
  16. 132 0
      src/app/admin/tickets/page.tsx
  17. 181 0
      src/app/admin/users/page.tsx
  18. 41 0
      src/app/create-order/[id]/page.tsx
  19. 183 0
      src/app/dashboard/page.tsx
  20. 79 0
      src/app/dashboard/settings/page.tsx
  21. 8 0
      src/app/globals.css
  22. 155 0
      src/app/knowledge/page.tsx
  23. 20 0
      src/app/layout.tsx
  24. 12 0
      src/app/login/page.tsx
  25. 106 0
      src/app/page.tsx
  26. 10 0
      src/app/payment/[id]/page.tsx
  27. 93 0
      src/app/refund-policy/page.tsx
  28. 12 0
      src/app/services/page.js
  29. 257 0
      src/app/slots/page.tsx
  30. 111 0
      src/components/AuthForm.tsx
  31. 49 0
      src/components/AutoRegistrar.tsx
  32. 218 0
      src/components/BindEmailModal.tsx
  33. 363 0
      src/components/CreateOrderForm.tsx
  34. 51 0
      src/components/Footer.tsx
  35. 200 0
      src/components/ForgotPasswordModal.tsx
  36. 93 0
      src/components/Navbar.tsx
  37. 294 0
      src/components/PaymentProcessor.tsx
  38. 287 0
      src/components/ServiceList.tsx
  39. 158 0
      src/components/admin/AdminSidebar.tsx
  40. 47 0
      src/components/admin/DataStats.tsx
  41. 91 0
      src/components/admin/TicketActionModal.tsx
  42. 105 0
      src/components/admin/dashboard/OverviewCharts.tsx
  43. 321 0
      src/components/admin/orders/OrderDetailModal.tsx
  44. 142 0
      src/components/admin/orders/OrderEditModal.tsx
  45. 161 0
      src/components/admin/orders/OrderTable.tsx
  46. 107 0
      src/components/admin/payments/ProviderList.tsx
  47. 119 0
      src/components/admin/payments/ProviderModal.tsx
  48. 409 0
      src/components/admin/payments/QrManager.tsx
  49. 94 0
      src/components/admin/products/ProductList.tsx
  50. 230 0
      src/components/admin/products/ProductModal.tsx
  51. 234 0
      src/components/admin/products/RoutingManager.tsx
  52. 170 0
      src/components/admin/products/SchemaManager.tsx
  53. 143 0
      src/components/admin/tasks/TaskDetailModal.tsx
  54. 147 0
      src/components/admin/tasks/TaskEditModal.tsx
  55. 295 0
      src/components/admin/tasks/TaskTable.tsx
  56. 259 0
      src/components/admin/tickets/TicketDetailModal.tsx
  57. 119 0
      src/components/admin/tickets/TicketTable.tsx
  58. 131 0
      src/components/admin/users/UserModal.tsx
  59. 116 0
      src/components/admin/users/UserTable.tsx
  60. 78 0
      src/components/common/JsonEditor.tsx
  61. 94 0
      src/components/common/Pagination.tsx
  62. 212 0
      src/components/dashboard/ChangePasswordModal.tsx
  63. 255 0
      src/components/dashboard/OrderList.tsx
  64. 313 0
      src/components/dashboard/ProfileSettings.tsx
  65. 67 0
      src/components/dashboard/Sidebar.tsx
  66. 226 0
      src/components/dashboard/TicketList.tsx
  67. 142 0
      src/components/dashboard/TicketModal.tsx
  68. 175 0
      src/components/dashboard/UserOrderDetailModal.tsx
  69. 270 0
      src/components/dashboard/UserTicketDetailModal.tsx
  70. 102 0
      src/components/knowledge/KnowledgeCard.tsx
  71. 37 0
      src/hooks/useAutoRegister.ts
  72. 36 0
      src/lib/api.js
  73. 39 0
      src/lib/auth.ts
  74. 26 0
      src/types/products.ts
  75. 12 0
      tailwind.config.js
  76. 38 0
      tsconfig.json

+ 4 - 0
.env.local

@@ -0,0 +1,4 @@
+# .env.local
+# 如果你的后端在本地运行,通常是 http://127.0.0.1:8000
+# 如果已经部署到服务器,填写服务器地址,如 https://api.visafly.com
+NEXT_PUBLIC_API_URL=http://45.137.220.138:8888

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+node_modules
+.next

+ 5 - 0
next-env.d.ts

@@ -0,0 +1,5 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.

+ 13 - 0
next.config.js

@@ -0,0 +1,13 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+    async rewrites() {
+      return [
+        {
+          source: '/api/:path*',
+          destination: `${process.env.NEXT_PUBLIC_API_URL}/api/:path*`,
+        },
+      ];
+    },
+  };
+  
+  module.exports = nextConfig;

+ 2261 - 0
package-lock.json

@@ -0,0 +1,2261 @@
+{
+    "name": "visafly-frontend",
+    "version": "0.1.0",
+    "lockfileVersion": 3,
+    "requires": true,
+    "packages": {
+        "": {
+            "name": "visafly-frontend",
+            "version": "0.1.0",
+            "dependencies": {
+                "axios": "^1.6.7",
+                "lucide-react": "^0.330.0",
+                "next": "14.1.0",
+                "react": "^18",
+                "react-dom": "^18",
+                "recharts": "^3.6.0"
+            },
+            "devDependencies": {
+                "@types/node": "25.0.3",
+                "@types/react": "19.2.7",
+                "autoprefixer": "^10.0.1",
+                "postcss": "^8",
+                "tailwindcss": "^3.3.0",
+                "typescript": "5.9.3"
+            }
+        },
+        "node_modules/@alloc/quick-lru": {
+            "version": "5.2.0",
+            "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+            "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/@jridgewell/gen-mapping": {
+            "version": "0.3.13",
+            "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+            "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jridgewell/sourcemap-codec": "^1.5.0",
+                "@jridgewell/trace-mapping": "^0.3.24"
+            }
+        },
+        "node_modules/@jridgewell/resolve-uri": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+            "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/@jridgewell/sourcemap-codec": {
+            "version": "1.5.5",
+            "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+            "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/@jridgewell/trace-mapping": {
+            "version": "0.3.31",
+            "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+            "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jridgewell/resolve-uri": "^3.1.0",
+                "@jridgewell/sourcemap-codec": "^1.4.14"
+            }
+        },
+        "node_modules/@next/env": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/env/-/env-14.1.0.tgz",
+            "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==",
+            "license": "MIT"
+        },
+        "node_modules/@next/swc-darwin-arm64": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz",
+            "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==",
+            "cpu": [
+                "arm64"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-darwin-x64": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz",
+            "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==",
+            "cpu": [
+                "x64"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-linux-arm64-gnu": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz",
+            "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==",
+            "cpu": [
+                "arm64"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-linux-arm64-musl": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz",
+            "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==",
+            "cpu": [
+                "arm64"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-linux-x64-gnu": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz",
+            "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==",
+            "cpu": [
+                "x64"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-linux-x64-musl": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz",
+            "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==",
+            "cpu": [
+                "x64"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-win32-arm64-msvc": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz",
+            "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==",
+            "cpu": [
+                "arm64"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "win32"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-win32-ia32-msvc": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz",
+            "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==",
+            "cpu": [
+                "ia32"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "win32"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@next/swc-win32-x64-msvc": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz",
+            "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==",
+            "cpu": [
+                "x64"
+            ],
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "win32"
+            ],
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@nodelib/fs.scandir": {
+            "version": "2.1.5",
+            "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@nodelib/fs.stat": "2.0.5",
+                "run-parallel": "^1.1.9"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.stat": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.walk": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@nodelib/fs.scandir": "2.1.5",
+                "fastq": "^1.6.0"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@reduxjs/toolkit": {
+            "version": "2.11.2",
+            "resolved": "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+            "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@standard-schema/spec": "^1.0.0",
+                "@standard-schema/utils": "^0.3.0",
+                "immer": "^11.0.0",
+                "redux": "^5.0.1",
+                "redux-thunk": "^3.1.0",
+                "reselect": "^5.1.0"
+            },
+            "peerDependencies": {
+                "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+                "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+            },
+            "peerDependenciesMeta": {
+                "react": {
+                    "optional": true
+                },
+                "react-redux": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@reduxjs/toolkit/node_modules/immer": {
+            "version": "11.1.0",
+            "resolved": "https://registry.npmmirror.com/immer/-/immer-11.1.0.tgz",
+            "integrity": "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==",
+            "license": "MIT",
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/immer"
+            }
+        },
+        "node_modules/@standard-schema/spec": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
+            "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+            "license": "MIT"
+        },
+        "node_modules/@standard-schema/utils": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz",
+            "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+            "license": "MIT"
+        },
+        "node_modules/@swc/helpers": {
+            "version": "0.5.2",
+            "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.2.tgz",
+            "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "tslib": "^2.4.0"
+            }
+        },
+        "node_modules/@types/d3-array": {
+            "version": "3.2.2",
+            "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz",
+            "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+            "license": "MIT"
+        },
+        "node_modules/@types/d3-color": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
+            "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+            "license": "MIT"
+        },
+        "node_modules/@types/d3-ease": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+            "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+            "license": "MIT"
+        },
+        "node_modules/@types/d3-interpolate": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+            "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/d3-color": "*"
+            }
+        },
+        "node_modules/@types/d3-path": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz",
+            "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+            "license": "MIT"
+        },
+        "node_modules/@types/d3-scale": {
+            "version": "4.0.9",
+            "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+            "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/d3-time": "*"
+            }
+        },
+        "node_modules/@types/d3-shape": {
+            "version": "3.1.7",
+            "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+            "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/d3-path": "*"
+            }
+        },
+        "node_modules/@types/d3-time": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz",
+            "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+            "license": "MIT"
+        },
+        "node_modules/@types/d3-timer": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+            "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+            "license": "MIT"
+        },
+        "node_modules/@types/node": {
+            "version": "25.0.3",
+            "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.0.3.tgz",
+            "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "undici-types": "~7.16.0"
+            }
+        },
+        "node_modules/@types/react": {
+            "version": "19.2.7",
+            "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz",
+            "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
+            "devOptional": true,
+            "license": "MIT",
+            "dependencies": {
+                "csstype": "^3.2.2"
+            }
+        },
+        "node_modules/@types/use-sync-external-store": {
+            "version": "0.0.6",
+            "resolved": "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+            "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+            "license": "MIT"
+        },
+        "node_modules/any-promise": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
+            "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/anymatch": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
+            "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "normalize-path": "^3.0.0",
+                "picomatch": "^2.0.4"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/arg": {
+            "version": "5.0.2",
+            "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz",
+            "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/asynckit": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+            "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+            "license": "MIT"
+        },
+        "node_modules/autoprefixer": {
+            "version": "10.4.23",
+            "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.23.tgz",
+            "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/postcss/"
+                },
+                {
+                    "type": "tidelift",
+                    "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "browserslist": "^4.28.1",
+                "caniuse-lite": "^1.0.30001760",
+                "fraction.js": "^5.3.4",
+                "picocolors": "^1.1.1",
+                "postcss-value-parser": "^4.2.0"
+            },
+            "bin": {
+                "autoprefixer": "bin/autoprefixer"
+            },
+            "engines": {
+                "node": "^10 || ^12 || >=14"
+            },
+            "peerDependencies": {
+                "postcss": "^8.1.0"
+            }
+        },
+        "node_modules/axios": {
+            "version": "1.13.2",
+            "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz",
+            "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+            "license": "MIT",
+            "dependencies": {
+                "follow-redirects": "^1.15.6",
+                "form-data": "^4.0.4",
+                "proxy-from-env": "^1.1.0"
+            }
+        },
+        "node_modules/baseline-browser-mapping": {
+            "version": "2.9.11",
+            "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
+            "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "bin": {
+                "baseline-browser-mapping": "dist/cli.js"
+            }
+        },
+        "node_modules/binary-extensions": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
+            "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/braces": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
+            "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "fill-range": "^7.1.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/browserslist": {
+            "version": "4.28.1",
+            "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz",
+            "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/browserslist"
+                },
+                {
+                    "type": "tidelift",
+                    "url": "https://tidelift.com/funding/github/npm/browserslist"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "baseline-browser-mapping": "^2.9.0",
+                "caniuse-lite": "^1.0.30001759",
+                "electron-to-chromium": "^1.5.263",
+                "node-releases": "^2.0.27",
+                "update-browserslist-db": "^1.2.0"
+            },
+            "bin": {
+                "browserslist": "cli.js"
+            },
+            "engines": {
+                "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+            }
+        },
+        "node_modules/busboy": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz",
+            "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+            "dependencies": {
+                "streamsearch": "^1.1.0"
+            },
+            "engines": {
+                "node": ">=10.16.0"
+            }
+        },
+        "node_modules/call-bind-apply-helpers": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+            "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+            "license": "MIT",
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "function-bind": "^1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/camelcase-css": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz",
+            "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/caniuse-lite": {
+            "version": "1.0.30001761",
+            "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
+            "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/browserslist"
+                },
+                {
+                    "type": "tidelift",
+                    "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "CC-BY-4.0"
+        },
+        "node_modules/chokidar": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+            "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "anymatch": "~3.1.2",
+                "braces": "~3.0.2",
+                "glob-parent": "~5.1.2",
+                "is-binary-path": "~2.1.0",
+                "is-glob": "~4.0.1",
+                "normalize-path": "~3.0.0",
+                "readdirp": "~3.6.0"
+            },
+            "engines": {
+                "node": ">= 8.10.0"
+            },
+            "funding": {
+                "url": "https://paulmillr.com/funding/"
+            },
+            "optionalDependencies": {
+                "fsevents": "~2.3.2"
+            }
+        },
+        "node_modules/chokidar/node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/client-only": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz",
+            "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+            "license": "MIT"
+        },
+        "node_modules/clsx": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
+            "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/combined-stream": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+            "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+            "license": "MIT",
+            "dependencies": {
+                "delayed-stream": "~1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/commander": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
+            "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/cssesc": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
+            "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+            "dev": true,
+            "license": "MIT",
+            "bin": {
+                "cssesc": "bin/cssesc"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/csstype": {
+            "version": "3.2.3",
+            "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+            "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+            "devOptional": true,
+            "license": "MIT"
+        },
+        "node_modules/d3-array": {
+            "version": "3.2.4",
+            "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
+            "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+            "license": "ISC",
+            "dependencies": {
+                "internmap": "1 - 2"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-color": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
+            "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+            "license": "ISC",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-ease": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
+            "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+            "license": "BSD-3-Clause",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-format": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz",
+            "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+            "license": "ISC",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-interpolate": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+            "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+            "license": "ISC",
+            "dependencies": {
+                "d3-color": "1 - 3"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-path": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
+            "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+            "license": "ISC",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-scale": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
+            "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+            "license": "ISC",
+            "dependencies": {
+                "d3-array": "2.10.0 - 3",
+                "d3-format": "1 - 3",
+                "d3-interpolate": "1.2.0 - 3",
+                "d3-time": "2.1.1 - 3",
+                "d3-time-format": "2 - 4"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-shape": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
+            "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+            "license": "ISC",
+            "dependencies": {
+                "d3-path": "^3.1.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-time": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
+            "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+            "license": "ISC",
+            "dependencies": {
+                "d3-array": "2 - 3"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-time-format": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
+            "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+            "license": "ISC",
+            "dependencies": {
+                "d3-time": "1 - 3"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/d3-timer": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
+            "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+            "license": "ISC",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/decimal.js-light": {
+            "version": "2.5.1",
+            "resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+            "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+            "license": "MIT"
+        },
+        "node_modules/delayed-stream": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+            "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/didyoumean": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz",
+            "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+            "dev": true,
+            "license": "Apache-2.0"
+        },
+        "node_modules/dlv": {
+            "version": "1.1.3",
+            "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz",
+            "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/dunder-proto": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+            "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+            "license": "MIT",
+            "dependencies": {
+                "call-bind-apply-helpers": "^1.0.1",
+                "es-errors": "^1.3.0",
+                "gopd": "^1.2.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/electron-to-chromium": {
+            "version": "1.5.267",
+            "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+            "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+            "dev": true,
+            "license": "ISC"
+        },
+        "node_modules/es-define-property": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+            "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/es-errors": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+            "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/es-object-atoms": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+            "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+            "license": "MIT",
+            "dependencies": {
+                "es-errors": "^1.3.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/es-set-tostringtag": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+            "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+            "license": "MIT",
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "get-intrinsic": "^1.2.6",
+                "has-tostringtag": "^1.0.2",
+                "hasown": "^2.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/es-toolkit": {
+            "version": "1.43.0",
+            "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.43.0.tgz",
+            "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==",
+            "license": "MIT",
+            "workspaces": [
+                "docs",
+                "benchmarks"
+            ]
+        },
+        "node_modules/escalade": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
+            "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/eventemitter3": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz",
+            "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+            "license": "MIT"
+        },
+        "node_modules/fast-glob": {
+            "version": "3.3.3",
+            "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
+            "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@nodelib/fs.stat": "^2.0.2",
+                "@nodelib/fs.walk": "^1.2.3",
+                "glob-parent": "^5.1.2",
+                "merge2": "^1.3.0",
+                "micromatch": "^4.0.8"
+            },
+            "engines": {
+                "node": ">=8.6.0"
+            }
+        },
+        "node_modules/fast-glob/node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/fastq": {
+            "version": "1.19.1",
+            "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz",
+            "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "reusify": "^1.0.4"
+            }
+        },
+        "node_modules/fill-range": {
+            "version": "7.1.1",
+            "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
+            "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "to-regex-range": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/follow-redirects": {
+            "version": "1.15.11",
+            "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
+            "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+            "funding": [
+                {
+                    "type": "individual",
+                    "url": "https://github.com/sponsors/RubenVerborgh"
+                }
+            ],
+            "license": "MIT",
+            "engines": {
+                "node": ">=4.0"
+            },
+            "peerDependenciesMeta": {
+                "debug": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/form-data": {
+            "version": "4.0.5",
+            "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+            "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+            "license": "MIT",
+            "dependencies": {
+                "asynckit": "^0.4.0",
+                "combined-stream": "^1.0.8",
+                "es-set-tostringtag": "^2.1.0",
+                "hasown": "^2.0.2",
+                "mime-types": "^2.1.12"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/fraction.js": {
+            "version": "5.3.4",
+            "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz",
+            "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": "*"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/rawify"
+            }
+        },
+        "node_modules/fsevents": {
+            "version": "2.3.3",
+            "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+            "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+            "dev": true,
+            "hasInstallScript": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+            }
+        },
+        "node_modules/function-bind": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+            "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/get-intrinsic": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+            "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+            "license": "MIT",
+            "dependencies": {
+                "call-bind-apply-helpers": "^1.0.2",
+                "es-define-property": "^1.0.1",
+                "es-errors": "^1.3.0",
+                "es-object-atoms": "^1.1.1",
+                "function-bind": "^1.1.2",
+                "get-proto": "^1.0.1",
+                "gopd": "^1.2.0",
+                "has-symbols": "^1.1.0",
+                "hasown": "^2.0.2",
+                "math-intrinsics": "^1.1.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/get-proto": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+            "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+            "license": "MIT",
+            "dependencies": {
+                "dunder-proto": "^1.0.1",
+                "es-object-atoms": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/glob-parent": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+            "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "is-glob": "^4.0.3"
+            },
+            "engines": {
+                "node": ">=10.13.0"
+            }
+        },
+        "node_modules/gopd": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+            "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/graceful-fs": {
+            "version": "4.2.11",
+            "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+            "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+            "license": "ISC"
+        },
+        "node_modules/has-symbols": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+            "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-tostringtag": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+            "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+            "license": "MIT",
+            "dependencies": {
+                "has-symbols": "^1.0.3"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/hasown": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+            "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+            "license": "MIT",
+            "dependencies": {
+                "function-bind": "^1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/immer": {
+            "version": "10.2.0",
+            "resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz",
+            "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+            "license": "MIT",
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/immer"
+            }
+        },
+        "node_modules/internmap": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
+            "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+            "license": "ISC",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/is-binary-path": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
+            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "binary-extensions": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-core-module": {
+            "version": "2.16.1",
+            "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
+            "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "hasown": "^2.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-extglob": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+            "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-glob": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "is-extglob": "^2.1.1"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.12.0"
+            }
+        },
+        "node_modules/jiti": {
+            "version": "1.21.7",
+            "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
+            "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+            "dev": true,
+            "license": "MIT",
+            "bin": {
+                "jiti": "bin/jiti.js"
+            }
+        },
+        "node_modules/js-tokens": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
+            "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+            "license": "MIT"
+        },
+        "node_modules/lilconfig": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
+            "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/antonk52"
+            }
+        },
+        "node_modules/lines-and-columns": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+            "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/loose-envify": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
+            "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+            "license": "MIT",
+            "dependencies": {
+                "js-tokens": "^3.0.0 || ^4.0.0"
+            },
+            "bin": {
+                "loose-envify": "cli.js"
+            }
+        },
+        "node_modules/lucide-react": {
+            "version": "0.330.0",
+            "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.330.0.tgz",
+            "integrity": "sha512-CQwY+Fpbt2kxCoVhuN0RCZDCYlbYnqB870Bl/vIQf3ER/cnDDQ6moLmEkguRyruAUGd4j3Lc4mtnJosXnqHheA==",
+            "license": "ISC",
+            "peerDependencies": {
+                "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+            }
+        },
+        "node_modules/math-intrinsics": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+            "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/merge2": {
+            "version": "1.4.1",
+            "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
+            "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/micromatch": {
+            "version": "4.0.8",
+            "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
+            "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "braces": "^3.0.3",
+                "picomatch": "^2.3.1"
+            },
+            "engines": {
+                "node": ">=8.6"
+            }
+        },
+        "node_modules/mime-db": {
+            "version": "1.52.0",
+            "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+            "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/mime-types": {
+            "version": "2.1.35",
+            "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+            "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+            "license": "MIT",
+            "dependencies": {
+                "mime-db": "1.52.0"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/mz": {
+            "version": "2.7.0",
+            "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz",
+            "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "any-promise": "^1.0.0",
+                "object-assign": "^4.0.1",
+                "thenify-all": "^1.0.0"
+            }
+        },
+        "node_modules/nanoid": {
+            "version": "3.3.11",
+            "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+            "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "MIT",
+            "bin": {
+                "nanoid": "bin/nanoid.cjs"
+            },
+            "engines": {
+                "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+            }
+        },
+        "node_modules/next": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmmirror.com/next/-/next-14.1.0.tgz",
+            "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==",
+            "license": "MIT",
+            "dependencies": {
+                "@next/env": "14.1.0",
+                "@swc/helpers": "0.5.2",
+                "busboy": "1.6.0",
+                "caniuse-lite": "^1.0.30001579",
+                "graceful-fs": "^4.2.11",
+                "postcss": "8.4.31",
+                "styled-jsx": "5.1.1"
+            },
+            "bin": {
+                "next": "dist/bin/next"
+            },
+            "engines": {
+                "node": ">=18.17.0"
+            },
+            "optionalDependencies": {
+                "@next/swc-darwin-arm64": "14.1.0",
+                "@next/swc-darwin-x64": "14.1.0",
+                "@next/swc-linux-arm64-gnu": "14.1.0",
+                "@next/swc-linux-arm64-musl": "14.1.0",
+                "@next/swc-linux-x64-gnu": "14.1.0",
+                "@next/swc-linux-x64-musl": "14.1.0",
+                "@next/swc-win32-arm64-msvc": "14.1.0",
+                "@next/swc-win32-ia32-msvc": "14.1.0",
+                "@next/swc-win32-x64-msvc": "14.1.0"
+            },
+            "peerDependencies": {
+                "@opentelemetry/api": "^1.1.0",
+                "react": "^18.2.0",
+                "react-dom": "^18.2.0",
+                "sass": "^1.3.0"
+            },
+            "peerDependenciesMeta": {
+                "@opentelemetry/api": {
+                    "optional": true
+                },
+                "sass": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/next/node_modules/postcss": {
+            "version": "8.4.31",
+            "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz",
+            "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/postcss/"
+                },
+                {
+                    "type": "tidelift",
+                    "url": "https://tidelift.com/funding/github/npm/postcss"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "nanoid": "^3.3.6",
+                "picocolors": "^1.0.0",
+                "source-map-js": "^1.0.2"
+            },
+            "engines": {
+                "node": "^10 || ^12 || >=14"
+            }
+        },
+        "node_modules/node-releases": {
+            "version": "2.0.27",
+            "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz",
+            "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/normalize-path": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
+            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/object-assign": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
+            "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/object-hash": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz",
+            "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/path-parse": {
+            "version": "1.0.7",
+            "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/picocolors": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+            "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+            "license": "ISC"
+        },
+        "node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=8.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/pify": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz",
+            "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/pirates": {
+            "version": "4.0.7",
+            "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz",
+            "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/postcss": {
+            "version": "8.5.6",
+            "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
+            "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/postcss/"
+                },
+                {
+                    "type": "tidelift",
+                    "url": "https://tidelift.com/funding/github/npm/postcss"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "nanoid": "^3.3.11",
+                "picocolors": "^1.1.1",
+                "source-map-js": "^1.2.1"
+            },
+            "engines": {
+                "node": "^10 || ^12 || >=14"
+            }
+        },
+        "node_modules/postcss-import": {
+            "version": "15.1.0",
+            "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz",
+            "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "postcss-value-parser": "^4.0.0",
+                "read-cache": "^1.0.0",
+                "resolve": "^1.1.7"
+            },
+            "engines": {
+                "node": ">=14.0.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.0.0"
+            }
+        },
+        "node_modules/postcss-js": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz",
+            "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/postcss/"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "camelcase-css": "^2.0.1"
+            },
+            "engines": {
+                "node": "^12 || ^14 || >= 16"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.21"
+            }
+        },
+        "node_modules/postcss-load-config": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+            "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/postcss/"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "lilconfig": "^3.1.1"
+            },
+            "engines": {
+                "node": ">= 18"
+            },
+            "peerDependencies": {
+                "jiti": ">=1.21.0",
+                "postcss": ">=8.0.9",
+                "tsx": "^4.8.1",
+                "yaml": "^2.4.2"
+            },
+            "peerDependenciesMeta": {
+                "jiti": {
+                    "optional": true
+                },
+                "postcss": {
+                    "optional": true
+                },
+                "tsx": {
+                    "optional": true
+                },
+                "yaml": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/postcss-nested": {
+            "version": "6.2.0",
+            "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz",
+            "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/postcss/"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "postcss-selector-parser": "^6.1.1"
+            },
+            "engines": {
+                "node": ">=12.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.2.14"
+            }
+        },
+        "node_modules/postcss-selector-parser": {
+            "version": "6.1.2",
+            "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+            "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "cssesc": "^3.0.0",
+                "util-deprecate": "^1.0.2"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/postcss-value-parser": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+            "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/proxy-from-env": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+            "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+            "license": "MIT"
+        },
+        "node_modules/queue-microtask": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
+            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "license": "MIT"
+        },
+        "node_modules/react": {
+            "version": "18.3.1",
+            "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
+            "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+            "license": "MIT",
+            "dependencies": {
+                "loose-envify": "^1.1.0"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/react-dom": {
+            "version": "18.3.1",
+            "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
+            "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+            "license": "MIT",
+            "dependencies": {
+                "loose-envify": "^1.1.0",
+                "scheduler": "^0.23.2"
+            },
+            "peerDependencies": {
+                "react": "^18.3.1"
+            }
+        },
+        "node_modules/react-is": {
+            "version": "19.2.3",
+            "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.3.tgz",
+            "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==",
+            "license": "MIT",
+            "peer": true
+        },
+        "node_modules/react-redux": {
+            "version": "9.2.0",
+            "resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz",
+            "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/use-sync-external-store": "^0.0.6",
+                "use-sync-external-store": "^1.4.0"
+            },
+            "peerDependencies": {
+                "@types/react": "^18.2.25 || ^19",
+                "react": "^18.0 || ^19",
+                "redux": "^5.0.0"
+            },
+            "peerDependenciesMeta": {
+                "@types/react": {
+                    "optional": true
+                },
+                "redux": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/read-cache": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz",
+            "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "pify": "^2.3.0"
+            }
+        },
+        "node_modules/readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "picomatch": "^2.2.1"
+            },
+            "engines": {
+                "node": ">=8.10.0"
+            }
+        },
+        "node_modules/recharts": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmmirror.com/recharts/-/recharts-3.6.0.tgz",
+            "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==",
+            "license": "MIT",
+            "workspaces": [
+                "www"
+            ],
+            "dependencies": {
+                "@reduxjs/toolkit": "1.x.x || 2.x.x",
+                "clsx": "^2.1.1",
+                "decimal.js-light": "^2.5.1",
+                "es-toolkit": "^1.39.3",
+                "eventemitter3": "^5.0.1",
+                "immer": "^10.1.1",
+                "react-redux": "8.x.x || 9.x.x",
+                "reselect": "5.1.1",
+                "tiny-invariant": "^1.3.3",
+                "use-sync-external-store": "^1.2.2",
+                "victory-vendor": "^37.0.2"
+            },
+            "engines": {
+                "node": ">=18"
+            },
+            "peerDependencies": {
+                "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+                "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+                "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+            }
+        },
+        "node_modules/redux": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz",
+            "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+            "license": "MIT"
+        },
+        "node_modules/redux-thunk": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz",
+            "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+            "license": "MIT",
+            "peerDependencies": {
+                "redux": "^5.0.0"
+            }
+        },
+        "node_modules/reselect": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz",
+            "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+            "license": "MIT"
+        },
+        "node_modules/resolve": {
+            "version": "1.22.11",
+            "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
+            "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "is-core-module": "^2.16.1",
+                "path-parse": "^1.0.7",
+                "supports-preserve-symlinks-flag": "^1.0.0"
+            },
+            "bin": {
+                "resolve": "bin/resolve"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/reusify": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
+            "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "iojs": ">=1.0.0",
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/run-parallel": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
+            "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "queue-microtask": "^1.2.2"
+            }
+        },
+        "node_modules/scheduler": {
+            "version": "0.23.2",
+            "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
+            "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+            "license": "MIT",
+            "dependencies": {
+                "loose-envify": "^1.1.0"
+            }
+        },
+        "node_modules/source-map-js": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+            "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+            "license": "BSD-3-Clause",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/streamsearch": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz",
+            "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+            "engines": {
+                "node": ">=10.0.0"
+            }
+        },
+        "node_modules/styled-jsx": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.1.tgz",
+            "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
+            "license": "MIT",
+            "dependencies": {
+                "client-only": "0.0.1"
+            },
+            "engines": {
+                "node": ">= 12.0.0"
+            },
+            "peerDependencies": {
+                "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
+            },
+            "peerDependenciesMeta": {
+                "@babel/core": {
+                    "optional": true
+                },
+                "babel-plugin-macros": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/sucrase": {
+            "version": "3.35.1",
+            "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz",
+            "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jridgewell/gen-mapping": "^0.3.2",
+                "commander": "^4.0.0",
+                "lines-and-columns": "^1.1.6",
+                "mz": "^2.7.0",
+                "pirates": "^4.0.1",
+                "tinyglobby": "^0.2.11",
+                "ts-interface-checker": "^0.1.9"
+            },
+            "bin": {
+                "sucrase": "bin/sucrase",
+                "sucrase-node": "bin/sucrase-node"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
+            }
+        },
+        "node_modules/supports-preserve-symlinks-flag": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+            "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/tailwindcss": {
+            "version": "3.4.19",
+            "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz",
+            "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@alloc/quick-lru": "^5.2.0",
+                "arg": "^5.0.2",
+                "chokidar": "^3.6.0",
+                "didyoumean": "^1.2.2",
+                "dlv": "^1.1.3",
+                "fast-glob": "^3.3.2",
+                "glob-parent": "^6.0.2",
+                "is-glob": "^4.0.3",
+                "jiti": "^1.21.7",
+                "lilconfig": "^3.1.3",
+                "micromatch": "^4.0.8",
+                "normalize-path": "^3.0.0",
+                "object-hash": "^3.0.0",
+                "picocolors": "^1.1.1",
+                "postcss": "^8.4.47",
+                "postcss-import": "^15.1.0",
+                "postcss-js": "^4.0.1",
+                "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+                "postcss-nested": "^6.2.0",
+                "postcss-selector-parser": "^6.1.2",
+                "resolve": "^1.22.8",
+                "sucrase": "^3.35.0"
+            },
+            "bin": {
+                "tailwind": "lib/cli.js",
+                "tailwindcss": "lib/cli.js"
+            },
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/thenify": {
+            "version": "3.3.1",
+            "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
+            "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "any-promise": "^1.0.0"
+            }
+        },
+        "node_modules/thenify-all": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz",
+            "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "thenify": ">= 3.1.0 < 4"
+            },
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
+        "node_modules/tiny-invariant": {
+            "version": "1.3.3",
+            "resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+            "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+            "license": "MIT"
+        },
+        "node_modules/tinyglobby": {
+            "version": "0.2.15",
+            "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
+            "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "fdir": "^6.5.0",
+                "picomatch": "^4.0.3"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/SuperchupuDev"
+            }
+        },
+        "node_modules/tinyglobby/node_modules/fdir": {
+            "version": "6.5.0",
+            "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
+            "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=12.0.0"
+            },
+            "peerDependencies": {
+                "picomatch": "^3 || ^4"
+            },
+            "peerDependenciesMeta": {
+                "picomatch": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/tinyglobby/node_modules/picomatch": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
+            "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "is-number": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=8.0"
+            }
+        },
+        "node_modules/ts-interface-checker": {
+            "version": "0.1.13",
+            "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+            "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+            "dev": true,
+            "license": "Apache-2.0"
+        },
+        "node_modules/tslib": {
+            "version": "2.8.1",
+            "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+            "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+            "license": "0BSD"
+        },
+        "node_modules/typescript": {
+            "version": "5.9.3",
+            "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+            "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
+            },
+            "engines": {
+                "node": ">=14.17"
+            }
+        },
+        "node_modules/undici-types": {
+            "version": "7.16.0",
+            "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
+            "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/update-browserslist-db": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+            "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/browserslist"
+                },
+                {
+                    "type": "tidelift",
+                    "url": "https://tidelift.com/funding/github/npm/browserslist"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "escalade": "^3.2.0",
+                "picocolors": "^1.1.1"
+            },
+            "bin": {
+                "update-browserslist-db": "cli.js"
+            },
+            "peerDependencies": {
+                "browserslist": ">= 4.21.0"
+            }
+        },
+        "node_modules/use-sync-external-store": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+            "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+            "license": "MIT",
+            "peerDependencies": {
+                "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+            }
+        },
+        "node_modules/util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/victory-vendor": {
+            "version": "37.3.6",
+            "resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz",
+            "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+            "license": "MIT AND ISC",
+            "dependencies": {
+                "@types/d3-array": "^3.0.3",
+                "@types/d3-ease": "^3.0.0",
+                "@types/d3-interpolate": "^3.0.1",
+                "@types/d3-scale": "^4.0.2",
+                "@types/d3-shape": "^3.1.0",
+                "@types/d3-time": "^3.0.0",
+                "@types/d3-timer": "^3.0.0",
+                "d3-array": "^3.1.6",
+                "d3-ease": "^3.0.1",
+                "d3-interpolate": "^3.0.1",
+                "d3-scale": "^4.0.2",
+                "d3-shape": "^3.1.0",
+                "d3-time": "^3.0.0",
+                "d3-timer": "^3.0.1"
+            }
+        }
+    }
+}

+ 27 - 0
package.json

@@ -0,0 +1,27 @@
+{
+    "name": "visafly-frontend",
+    "version": "0.1.0",
+    "private": true,
+    "scripts": {
+        "dev": "next dev",
+        "build": "next build",
+        "start": "next start",
+        "lint": "next lint"
+    },
+    "dependencies": {
+        "axios": "^1.6.7",
+        "lucide-react": "^0.330.0",
+        "next": "14.1.0",
+        "react": "^18",
+        "react-dom": "^18",
+        "recharts": "^3.6.0"
+    },
+    "devDependencies": {
+        "@types/node": "25.0.3",
+        "@types/react": "19.2.7",
+        "autoprefixer": "^10.0.1",
+        "postcss": "^8",
+        "tailwindcss": "^3.3.0",
+        "typescript": "5.9.3"
+    }
+}

+ 6 - 0
postcss.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+    plugins: {
+      tailwindcss: {},
+      autoprefixer: {},
+    },
+  };

+ 80 - 0
src/app/admin/layout.tsx

@@ -0,0 +1,80 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import AdminSidebar from '@/components/admin/AdminSidebar';
+import { isAdmin, getCurrentUser } from '@/lib/auth';
+import { Menu } from 'lucide-react'; // 引入汉堡图标
+
+export default function AdminLayout({ children }: { children: React.ReactNode }) {
+  const router = useRouter();
+  const [isAuthorized, setIsAuthorized] = useState(false);
+  const [user, setUser] = useState<any>(null);
+  
+  // 控制移动端侧边栏开关
+  const [isSidebarOpen, setSidebarOpen] = useState(false);
+
+  useEffect(() => {
+    // 权限检查逻辑 (保持不变)
+    const token = localStorage.getItem('rsid');
+    if (!token) {
+      router.push('/login');
+      return;
+    }
+    if (!isAdmin()) {
+      alert("您没有权限访问管理后台");
+      router.push('/dashboard');
+      return;
+    }
+    setIsAuthorized(true);
+    setUser(getCurrentUser());
+  }, [router]);
+
+  if (!isAuthorized) return null;
+
+  return (
+    <div className="min-h-screen bg-slate-100 flex font-sans text-slate-900">
+      
+      {/* 侧边栏 (传入状态控制) */}
+      <AdminSidebar 
+        isOpen={isSidebarOpen} 
+        onClose={() => setSidebarOpen(false)} 
+      />
+
+      {/* 主内容区域 */}
+      <main className="flex-1 overflow-y-auto h-screen flex flex-col w-full">
+        
+        {/* 顶部 Header */}
+        <header className="bg-white shadow-sm h-16 flex items-center justify-between px-4 md:px-8 flex-shrink-0 z-10 sticky top-0">
+          
+          <div className="flex items-center gap-3">
+            {/* === 新增:汉堡菜单按钮 (仅移动端显示) === */}
+            <button 
+              onClick={() => setSidebarOpen(true)}
+              className="p-2 -ml-2 mr-2 text-slate-600 hover:bg-slate-100 rounded-lg md:hidden"
+            >
+              <Menu size={24} />
+            </button>
+            {/* ======================================= */}
+
+            <h2 className="font-bold text-lg text-slate-800 truncate">管理控制台</h2>
+          </div>
+
+          <div className="flex items-center gap-4">
+            <div className="text-sm text-slate-500 hidden sm:block">
+              {user?.nickname || user?.email} ({user?.role})
+            </div>
+            <div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-bold text-xs">
+              AD
+            </div>
+          </div>
+        </header>
+
+        {/* 页面内容 */}
+        <div className="p-4 md:p-8 flex-1 w-full overflow-x-hidden">
+          {children}
+        </div>
+      </main>
+    </div>
+  );
+}

+ 255 - 0
src/app/admin/orders/new/page.tsx

@@ -0,0 +1,255 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { useRouter } from 'next/navigation';
+import { Loader2, ArrowLeft, Search, CheckCircle, User } from 'lucide-react';
+
+export default function AdminCreateOrderPage() {
+  const router = useRouter();
+  
+  // 步骤控制
+  const [loading, setLoading] = useState(false);
+  const [products, setProducts] = useState<any[]>([]);
+  
+  // 表单状态
+  const [targetEmail, setTargetEmail] = useState('');
+  const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
+  const [formSchema, setFormSchema] = useState<any>(null);
+  const [formValues, setFormValues] = useState<Record<string, any>>({});
+  const [skipPayment, setSkipPayment] = useState(true); // 默认跳过支付
+
+  // 1. 加载商品列表
+  useEffect(() => {
+    async function loadProducts() {
+      try {
+        const res = await api.get('/api/vas/product/list');
+        setProducts(Array.isArray(res.data.data) ? res.data.data : []);
+      } catch (e) {
+        console.error(e);
+      }
+    }
+    loadProducts();
+  }, []);
+
+  // 2. 当选择了商品,加载 Schema
+  useEffect(() => {
+    async function loadSchema() {
+      if (!selectedProductId) {
+        setFormSchema(null);
+        return;
+      }
+      
+      const product = products.find(p => p.id === Number(selectedProductId));
+      if (!product?.schema_id) return;
+
+      try {
+        // API: 获取 Schema 定义
+        const res = await api.get(`/api/vas/schema/${product.schema_id}`);
+        const data = res.data.data || res.data;
+        // 解析 JSON
+        const schemaJson = typeof data.schema_json === 'string' 
+          ? JSON.parse(data.schema_json) 
+          : data.schema_json;
+        
+        setFormSchema(schemaJson);
+        setFormValues({}); // 重置表单
+      } catch (e) {
+        alert("加载表单定义失败");
+      }
+    }
+    loadSchema();
+  }, [selectedProductId, products]);
+
+  // 处理动态表单输入
+  const handleInputChange = (key: string, value: any) => {
+    setFormValues(prev => ({ ...prev, [key]: value }));
+  };
+
+  // 3. 提交订单
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!targetEmail) return alert("请输入目标用户邮箱");
+    if (!selectedProductId) return alert("请选择商品");
+
+    setLoading(true);
+    try {
+      // Step A: 创建订单
+      // 注意:这里假设后端 create 接口支持通过 extra 参数指定 user_email (管理员权限)
+      // 如果后端不支持直接指定 user,可能需要先根据 email 查 user_id,然后传 user_id
+      const payload = {
+        product_id: Number(selectedProductId),
+        user_inputs: formValues,
+        // 传递目标用户标识 (需要后端支持处理,或者后端从 user_inputs 里提取 email)
+        target_user_email: targetEmail, 
+        // 标记为后台创建
+        is_admin_created: true 
+      };
+
+      const res = await api.post('/api/vas/order/create', payload);
+      const orderData = res.data.data || res.data;
+      const orderId = orderData.id;
+
+      // Step B: 如果勾选了“跳过支付”,直接将订单更新为已支付
+      if (skipPayment && orderId) {
+        // 调用更新接口将状态改为 paid
+        // API: PUT /api/vas/order/{id}
+        await api.put(`/api/vas/order/${orderId}`, {
+          status: 'paid', // 强制标记为已支付
+          admin_note: '管理员后台代下单,免支付'
+        });
+      }
+
+      alert("订单创建成功!");
+      router.push('/admin/orders');
+
+    } catch (error: any) {
+      console.error(error);
+      alert("创建失败: " + (error.response?.data?.message || error.message));
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 动态字段渲染器 (复用之前逻辑)
+  const renderField = (key: string, fieldSchema: any) => {
+    const commonClass = "w-full border rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none";
+    
+    if (fieldSchema.enum) {
+      return (
+        <select className={commonClass} onChange={e => handleInputChange(key, e.target.value)}>
+          <option value="">请选择</option>
+          {fieldSchema.enum.map((v: string) => <option key={v} value={v}>{v}</option>)}
+        </select>
+      );
+    }
+    return (
+      <input 
+        type={fieldSchema.type === 'integer' ? 'number' : 'text'} 
+        className={commonClass}
+        onChange={e => handleInputChange(key, e.target.value)}
+      />
+    );
+  };
+
+  return (
+    <div className="max-w-3xl mx-auto">
+      <button onClick={() => router.back()} className="flex items-center text-slate-500 hover:text-slate-800 mb-6 text-sm">
+        <ArrowLeft size={16} className="mr-1" /> 返回订单列表
+      </button>
+
+      <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
+        <div className="px-6 py-4 border-b bg-slate-50">
+          <h1 className="text-lg font-bold text-slate-800">管理员代下单 (后台创建)</h1>
+          <p className="text-xs text-slate-500 mt-1">为用户创建订单并可选择直接跳过支付流程</p>
+        </div>
+
+        <form onSubmit={handleSubmit} className="p-6 space-y-8">
+          
+          {/* 1. 用户与商品选择 */}
+          <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+            <div>
+              <label className="block text-sm font-bold text-slate-700 mb-2">
+                目标用户邮箱 <span className="text-red-500">*</span>
+              </label>
+              <div className="relative">
+                <input 
+                  type="email" required
+                  className="w-full border border-slate-300 rounded-lg pl-9 p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                  placeholder="user@example.com"
+                  value={targetEmail}
+                  onChange={e => setTargetEmail(e.target.value)}
+                />
+                <User size={16} className="absolute left-3 top-3 text-slate-400" />
+              </div>
+              <p className="text-xs text-slate-400 mt-1">如果用户不存在,系统将尝试自动创建或报错。</p>
+            </div>
+
+            <div>
+              <label className="block text-sm font-bold text-slate-700 mb-2">
+                选择商品 <span className="text-red-500">*</span>
+              </label>
+              <select 
+                required
+                className="w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none bg-white"
+                value={selectedProductId || ''}
+                onChange={e => setSelectedProductId(Number(e.target.value))}
+              >
+                <option value="">-- 请选择服务 --</option>
+                {products.map(p => (
+                  <option key={p.id} value={p.id}>{p.title} ({p.price_amount/100} {p.price_currency})</option>
+                ))}
+              </select>
+            </div>
+          </div>
+
+          <div className="border-t border-slate-100"></div>
+
+          {/* 2. 动态表单区域 */}
+          <div>
+            <h3 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
+              填写申请资料
+              {formSchema && <span className="text-xs font-normal text-slate-500 bg-slate-100 px-2 py-0.5 rounded">Schema Loaded</span>}
+            </h3>
+            
+            {!selectedProductId ? (
+              <div className="text-center py-8 bg-slate-50 rounded-lg text-slate-400 text-sm">
+                请先选择左侧的商品,以加载对应的表单字段
+              </div>
+            ) : !formSchema ? (
+              <div className="text-center py-8 text-slate-400 text-sm">正在加载表单定义...</div>
+            ) : (
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                {formSchema.properties && Object.keys(formSchema.properties).map(key => (
+                  <div key={key}>
+                    <label className="block text-sm font-medium text-slate-700 mb-1">
+                      {formSchema.properties[key].title || key}
+                    </label>
+                    {renderField(key, formSchema.properties[key])}
+                  </div>
+                ))}
+              </div>
+            )}
+          </div>
+
+          <div className="border-t border-slate-100"></div>
+
+          {/* 3. 支付设置 */}
+          <div className="bg-green-50 border border-green-100 rounded-lg p-4 flex items-start gap-3">
+            <div className="mt-0.5">
+              <input 
+                type="checkbox" 
+                id="skipPayment"
+                className="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
+                checked={skipPayment}
+                onChange={e => setSkipPayment(e.target.checked)}
+              />
+            </div>
+            <div>
+              <label htmlFor="skipPayment" className="block text-sm font-bold text-green-800 cursor-pointer">
+                免支付 (直接标记为已支付)
+              </label>
+              <p className="text-xs text-green-700 mt-1">
+                选中后,订单创建后将自动把状态更为 <strong>PAID</strong>,并触发后续业务流程(如机器人任务)。
+                <br/>如果不选中,订单将保持 Pending 状态,需要用户自行登录付款。
+              </p>
+            </div>
+          </div>
+
+          {/* 提交按钮 */}
+          <div className="flex justify-end pt-4">
+            <button 
+              type="submit" 
+              disabled={loading}
+              className="px-8 py-3 bg-slate-900 text-white rounded-lg font-bold hover:bg-slate-800 transition flex items-center gap-2 disabled:opacity-50 shadow-lg"
+            >
+              {loading ? <Loader2 className="animate-spin" /> : <CheckCircle size={18} />}
+              {skipPayment ? '创建并标记成功' : '创建待支付订单'}
+            </button>
+          </div>
+
+        </form>
+      </div>
+    </div>
+  );
+}

+ 228 - 0
src/app/admin/orders/page.tsx

@@ -0,0 +1,228 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import api from '@/lib/api';
+import { RefreshCw, Search, Plus } from 'lucide-react';
+import OrderTable from '@/components/admin/orders/OrderTable';
+import OrderDetailModal, { OrderDetail } from '@/components/admin/orders/OrderDetailModal';
+import OrderEditModal from '@/components/admin/orders/OrderEditModal';
+import Pagination from '@/components/common/Pagination'; // 1. 引入分页组件
+
+export default function AdminOrdersPage() {
+  const router = useRouter();
+  
+  // 数据状态
+  const [orders, setOrders] = useState<OrderDetail[]>([]);
+  const [loading, setLoading] = useState<boolean>(true);
+  
+  // 分页与搜索状态
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(10); // 每页 10 条
+  const [total, setTotal] = useState(0);
+  const [keyword, setKeyword] = useState('');
+  
+  // 弹窗状态
+  const [isDetailOpen, setIsDetailOpen] = useState<boolean>(false);
+  const [selectedOrder, setSelectedOrder] = useState<OrderDetail | null>(null);
+  const [isEditOpen, setIsEditOpen] = useState<boolean>(false);
+  const [editingOrder, setEditingOrder] = useState<OrderDetail | null>(null);
+
+  // 1. 获取订单列表
+  const fetchOrders = async (targetPage: number = page) => {
+    setLoading(true);
+    try {
+      // API: GET /api/vas/order/list?page=1&size=10&keyword=...
+      const res = await api.get('/api/vas/order/list_all', {
+        params: { 
+          keyword,
+          page: targetPage,
+          size: pageSize
+        }
+      });
+      
+      // 适配后端响应结构 (兼容纯数组和分页对象)
+      const data = res.data.data || {};
+      
+      if (Array.isArray(data)) {
+        // 旧接口 (无分页)
+        setOrders(data);
+        setTotal(data.length);
+      } else {
+        // 标准分页接口 { items: [], total: 100 }
+        setOrders(data.items || []);
+        setTotal(data.total || 0);
+      }
+      
+      setPage(targetPage);
+
+    } catch (e) {
+      console.warn("API Error, using mock data for orders");
+      
+      // === Mock Data & 前端模拟分页/搜索 ===
+      const mockData: OrderDetail[] = [
+        { 
+          id: 'ORD-2025-001', user_id: 'u_1001', base_amount: 30000, base_currency: 'CNY', status: 'paid', created_at: '2025-01-01T12:00:00',
+          product_title: '日本单次旅游签证 (VIP)', user_email: 'customer@vip.com', applicant_name: '张三',
+          user_inputs: { "name": "张三", "passport": "E12345678" }
+        },
+        { 
+          id: 'ORD-2025-002', user_id: 'u_1002', base_amount: 8000, base_currency: 'EUR', status: 'pending', created_at: '2025-01-02T09:30:00',
+          product_title: '法国申根签证', user_email: 'li_si@example.com', applicant_name: '李四',
+          user_inputs: { "name": "李四", "passport": "G87654321" }
+        },
+        { 
+          id: 'ORD-2025-003', user_id: 'u_1003', base_amount: 45000, base_currency: 'CNY', status: 'cancelled', created_at: '2025-01-03T14:20:00',
+          product_title: '泰国电子落地签', user_email: 'wang5@test.com', applicant_name: '王五',
+          user_inputs: { "name": "王五" }
+        },
+        // ... 可以复制更多数据测试分页
+      ];
+
+      // 模拟过滤
+      let filtered = mockData;
+      if (keyword) {
+        const lowerKey = keyword.toLowerCase();
+        filtered = mockData.filter(o => 
+          o.id.toLowerCase().includes(lowerKey) || 
+          (o.product_title && o.product_title.toLowerCase().includes(lowerKey)) ||
+          (o.user_email && o.user_email.toLowerCase().includes(lowerKey)) ||
+          (o.applicant_name && o.applicant_name.toLowerCase().includes(lowerKey))
+        );
+      }
+
+      // 模拟切片
+      const start = (targetPage - 1) * pageSize;
+      const end = start + pageSize;
+      setOrders(filtered.slice(start, end));
+      setTotal(filtered.length);
+      setPage(targetPage);
+
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    fetchOrders(1);
+  }, []);
+
+  // 搜索处理
+  const handleSearch = () => {
+    fetchOrders(1); // 搜索时重置到第一页
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') handleSearch();
+  };
+
+  // 2. 取消订单
+  const handleCancelOrder = async (orderId: string) => {
+    if(!confirm(`确定要取消订单 ${orderId} 吗?此操作不可逆。`)) return;
+    try {
+      await api.post('/api/vas/order/cancel', null, { params: { order_id: orderId } });
+      alert("订单已取消");
+      fetchOrders(page); // 刷新当前页
+    } catch (e: any) {
+      alert("取消失败: " + (e.response?.data?.message || "未知错误"));
+    }
+  };
+
+  // 3. 打开详情
+  const handleViewDetail = (order: OrderDetail) => {
+    setSelectedOrder(order);
+    setIsDetailOpen(true);
+  };
+
+  // 4. 打开编辑
+  const handleEditOrder = (order: OrderDetail) => {
+    setEditingOrder(order);
+    setIsEditOpen(true);
+  };
+
+  // 5. 提交编辑
+  const handleSubmitEdit = async (orderId: string, data: any) => {
+    try {
+      await api.post(`/api/vas/order/patch_user_inputs`, data, { params: { order_id: orderId } });
+      alert("订单信息更新成功。");
+      setIsEditOpen(false);
+      fetchOrders(page); // 刷新当前页
+    } catch (error: any) {
+      alert("更新失败: " + (error.response?.data?.message || error.message));
+    }
+  };
+
+  return (
+    <div>
+      <div className="flex justify-between items-center mb-6">
+        <div>
+          <h1 className="text-2xl font-bold text-slate-800">订单管理</h1>
+          <p className="text-sm text-slate-500 mt-1">查看及管理所有用户提交的签证申请</p>
+        </div>
+        <div className="flex gap-3">
+          
+          {/* === 搜索框 === */}
+          <div className="relative hidden md:block">
+            <input 
+              type="text" 
+              placeholder="搜索订单号/邮箱/商品..." 
+              className="pl-10 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64 transition"
+              value={keyword}
+              onChange={e => setKeyword(e.target.value)}
+              onKeyDown={handleKeyDown}
+            />
+            <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+          </div>
+
+          {/* 搜索按钮 (移动端也可以显示) */}
+          <button 
+            onClick={handleSearch} 
+            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-700 font-medium shadow-sm transition"
+          >
+            <RefreshCw size={16} /> 刷新
+          </button>
+
+          {/* 代客下单 */}
+          <button 
+            onClick={() => router.push('/admin/orders/new')} 
+            className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm transition"
+          >
+            <Plus size={16} /> 代客下单
+          </button>
+        </div>
+      </div>
+      
+      {/* 订单列表表格 */}
+      <OrderTable 
+        orders={orders} 
+        loading={loading} 
+        onCancel={handleCancelOrder} 
+        onViewDetail={handleViewDetail} 
+        onEdit={handleEditOrder} 
+      />
+
+      {/* === 分页组件 === */}
+      <Pagination 
+        currentPage={page}
+        total={total}
+        pageSize={pageSize}
+        onPageChange={(p) => fetchOrders(p)}
+      />
+
+      {/* 详情弹窗 */}
+      <OrderDetailModal 
+        isOpen={isDetailOpen}
+        onClose={() => setIsDetailOpen(false)}
+        order={selectedOrder}
+      />
+
+      {/* 编辑弹窗 */}
+      <OrderEditModal 
+        isOpen={isEditOpen}
+        onClose={() => setIsEditOpen(false)}
+        order={editingOrder}
+        onSubmit={handleSubmitEdit}
+      />
+    </div>
+  );
+}

+ 206 - 0
src/app/admin/page.tsx

@@ -0,0 +1,206 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import DataStats from '@/components/admin/DataStats'; // 复用之前的组件
+import OverviewCharts from '@/components/admin/dashboard/OverviewCharts'; // 新组件
+import { ShoppingBag, Users, AlertCircle, CheckCircle, Wallet, ArrowRight } from 'lucide-react';
+import Link from 'next/link';
+
+export default function AdminDashboard() {
+  const [loading, setLoading] = useState(true);
+  
+  // 状态数据
+  const [stats, setStats] = useState({
+    totalOrders: 0,
+    totalRevenue: 0,
+    activeUsers: 0,
+    pendingTickets: 0,
+    successRate: '0%'
+  });
+
+  // 图表数据
+  const [chartData, setChartData] = useState<{
+    revenue: any[],
+    products: any[]
+  }>({ revenue: [], products: [] });
+
+  const [recentActivities, setRecentActivities] = useState<any[]>([]);
+
+  useEffect(() => {
+    fetchDashboardData();
+  }, []);
+
+  const fetchDashboardData = async () => {
+    setLoading(true);
+    try {
+      // -------------------------------------------------------------
+      // TODO: 后端 API: GET /api/vas/statistics/overview
+      // -------------------------------------------------------------
+      const res = await api.get('/api/vas/statistics/overview');
+      const data = res.data.data;
+
+      setStats(data.stats);
+
+      // 注意这里后端返回的是 revenue_trend 和 product_dist
+      setChartData({ 
+        revenue: data.revenue_trend, 
+        products: data.product_dist 
+      });
+
+      setRecentActivities(data.recent_activities);
+      
+      // === 暂用 Mock Data 模拟真实业务场景 ===
+      
+      // // 1. 顶部卡片数据
+      // setStats({
+      //   totalOrders: 128,
+      //   totalRevenue: 384000, // 单位分
+      //   activeUsers: 45,
+      //   pendingTickets: 3,
+      //   successRate: '94.5%'
+      // });
+
+      // // 2. 模拟近 7 天营收
+      // const revenueMock = [];
+      // const now = new Date();
+      // for (let i = 6; i >= 0; i--) {
+      //   const d = new Date();
+      //   d.setDate(now.getDate() - i);
+      //   revenueMock.push({
+      //     date: d.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }),
+      //     amount: Math.floor(Math.random() * 5000) + 2000, // 2000~7000 元
+      //     orders: Math.floor(Math.random() * 10) + 1
+      //   });
+      // }
+
+      // // 3. 模拟商品销量分布
+      // const productMock = [
+      //   { name: '日本旅游签证', value: 45 },
+      //   { name: '法国申根签', value: 32 },
+      //   { name: '泰国电子签', value: 24 },
+      //   { name: '美国 B1/B2', value: 15 },
+      //   { name: '其他', value: 12 },
+      // ];
+
+      // setChartData({
+      //   revenue: revenueMock,
+      //   products: productMock
+      // });
+
+      // // 4. 模拟最新动态
+      // setRecentActivities([
+      //   { id: 1, text: '用户 Zhang San 提交了新的日本签证申请', time: '10分钟前', type: 'order' },
+      //   { id: 2, text: '系统自动抓取了 "fr_visabot" 的预约号', time: '35分钟前', type: 'system' },
+      //   { id: 3, text: '收到一笔新的 Stripe 支付 ¥800.00', time: '1小时前', type: 'money' },
+      //   { id: 4, text: '客服处理了 "退款申请" 工单 #102', time: '2小时前', type: 'ticket' },
+      // ]);
+
+    } catch (error) {
+      console.error("Failed to load dashboard data", error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div>
+      <h1 className="text-2xl font-bold mb-6 text-slate-800">系统概览</h1>
+      
+      {/* 1. 核心指标卡片 */}
+      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
+        <DataStats 
+          title="总营收 (本月)" 
+          value={`¥${(stats.totalRevenue / 100).toLocaleString()}`} 
+          icon={Wallet} 
+          trend="+12.5%" 
+          trendUp={true} 
+          color="blue"
+        />
+        <DataStats 
+          title="活跃订单数" 
+          value={stats.totalOrders} 
+          icon={ShoppingBag} 
+          trend="+8%" 
+          trendUp={true} 
+          color="purple"
+        />
+        <DataStats 
+          title="待处理工单" 
+          value={stats.pendingTickets} 
+          icon={AlertCircle} 
+          trend={stats.pendingTickets > 0 ? "需关注" : "良好"} 
+          trendUp={stats.pendingTickets === 0}
+          color={stats.pendingTickets > 0 ? "red" : "green"}
+        />
+        <DataStats 
+          title="机器人成功率" 
+          value={stats.successRate} 
+          icon={CheckCircle} 
+          color="green"
+        />
+      </div>
+
+      {/* 2. 图表区域 (核心新增) */}
+      <OverviewCharts 
+        revenueData={chartData.revenue} 
+        productData={chartData.products} 
+      />
+
+      {/* 3. 底部:最新动态与快捷入口 */}
+      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
+        
+        {/* 最新动态列表 */}
+        <div className="lg:col-span-2 bg-white p-6 rounded-xl shadow-sm border border-slate-100">
+          <div className="flex justify-between items-center mb-4">
+            <h3 className="font-bold text-slate-800">最新系统动态</h3>
+            <button className="text-xs text-blue-600 hover:underline">查看全部日志</button>
+          </div>
+          <div className="space-y-4">
+            {recentActivities.map((item) => (
+              <div key={item.id} className="flex items-start gap-3 pb-3 border-b border-slate-50 last:border-0 last:pb-0">
+                <div className={`mt-1 w-2 h-2 rounded-full flex-shrink-0 
+                  ${item.type === 'order' ? 'bg-blue-500' : 
+                    item.type === 'money' ? 'bg-green-500' : 
+                    item.type === 'ticket' ? 'bg-orange-500' : 'bg-gray-400'}`} 
+                />
+                <div className="flex-1">
+                  <p className="text-sm text-slate-700">{item.text}</p>
+                  <p className="text-xs text-slate-400 mt-0.5">{item.time}</p>
+                </div>
+              </div>
+            ))}
+          </div>
+        </div>
+
+        {/* 快捷操作区 */}
+        <div className="bg-slate-900 text-white p-6 rounded-xl shadow-lg flex flex-col justify-between">
+          <div>
+            <h3 className="font-bold text-lg mb-2">管理快捷入口</h3>
+            <p className="text-slate-400 text-sm mb-6">常用功能快速导航</p>
+            
+            <div className="space-y-3">
+              <Link href="/admin/tickets" className="block bg-white/10 hover:bg-white/20 px-4 py-3 rounded-lg transition flex justify-between items-center">
+                <span>处理售后工单</span>
+                <ArrowRight size={16} />
+              </Link>
+              <Link href="/admin/products" className="block bg-white/10 hover:bg-white/20 px-4 py-3 rounded-lg transition flex justify-between items-center">
+                <span>发布新商品</span>
+                <ArrowRight size={16} />
+              </Link>
+              <Link href="/admin/tasks" className="block bg-white/10 hover:bg-white/20 px-4 py-3 rounded-lg transition flex justify-between items-center">
+                <span>监控任务状态</span>
+                <ArrowRight size={16} />
+              </Link>
+            </div>
+          </div>
+          
+          <div className="mt-6 pt-6 border-t border-white/10">
+            <p className="text-xs text-slate-500">System Status: <span className="text-green-400">Normal</span></p>
+          </div>
+        </div>
+
+      </div>
+    </div>
+  );
+}

+ 132 - 0
src/app/admin/payments/page.tsx

@@ -0,0 +1,132 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { Plus, RefreshCw } from 'lucide-react';
+import ProviderList from '@/components/admin/payments/ProviderList';
+import ProviderModal from '@/components/admin/payments/ProviderModal';
+import QrManager from '@/components/admin/payments/QrManager';
+
+export default function AdminPaymentsPage() {
+  const [providers, setProviders] = useState<any[]>([]);
+  const [loading, setLoading] = useState(true);
+
+  // Modal States
+  const [isProviderModalOpen, setProviderModalOpen] = useState(false);
+  const [isQrManagerOpen, setQrManagerOpen] = useState(false);
+  const [selectedProvider, setSelectedProvider] = useState<any>(null);
+
+  const fetchProviders = async () => {
+    setLoading(true);
+    try {
+      // API: GET /api/vas/payment_provider/list_all
+      const res = await api.get('/api/vas/payment_provider/list_all');
+      const list = Array.isArray(res.data) ? res.data : (res.data.data || []);
+      setProviders(list);
+    } catch (e) {
+      console.warn("Using mock providers");
+      setProviders([
+        { id: 1, name: 'wechat', title: '微信支付', icon: '', channel: 'qr_static', currency: 'CNY', enabled: 1 },
+        { id: 2, name: 'stripe', title: 'Credit Card', icon: '', channel: 'online_link', currency: 'USD', enabled: 1 },
+      ]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => { fetchProviders(); }, []);
+
+  const handleSaveProvider = async (data: any) => {
+    try {
+      if (data.id) {
+        // Update: POST /api/vas/payment_provider/update?id=xxx
+        await api.post('/api/vas/payment_provider/update', data, {params : {"id": data.id}});
+      } else {
+        // Create: POST /api/vas/payment_provider/create
+        await api.post('/api/vas/payment_provider/create', data);
+      }
+      alert("保存成功");
+      fetchProviders();
+    } catch (e: any) {
+      alert("保存失败: " + (e.response?.data?.message || "未知错误"));
+      throw e;
+    }
+  };
+
+  // 新增:删除服务商逻辑
+  const handleDeleteProvider = async (provider: any) => {
+    if (!confirm(`确定要删除服务商 "${provider.title || provider.name}" 吗?\n\n警告:删除后该渠道将无法使用,且关联的配置可能会丢失。`)) {
+      return;
+    }
+
+    try {
+      // Delete: delete /api/vas/payment_provider/delete?id=xxx (根据 update 接口风格推测)
+      // 如果后端支持标准的 RESTful DELETE,请改为: await api.delete(`/api/vas/payment_provider/${provider.id}`);
+      await api.delete('/api/vas/payment_provider/delete', {
+        params: { id: provider.id }
+      });
+      
+      alert("删除成功");
+      fetchProviders(); // 刷新列表
+    } catch (e: any) {
+      console.error(e);
+      alert("删除失败: " + (e.response?.data?.message || "未知错误"));
+    }
+  };
+
+  const openEdit = (p: any) => {
+    setSelectedProvider(p);
+    setProviderModalOpen(true);
+  };
+
+  const openCreate = () => {
+    setSelectedProvider(null);
+    setProviderModalOpen(true);
+  };
+
+  const openQrManager = (p: any) => {
+    setSelectedProvider(p);
+    setQrManagerOpen(true);
+  };
+
+  return (
+    <div>
+      <div className="flex justify-between items-center mb-6">
+        <div>
+          <h1 className="text-2xl font-bold text-slate-800">支付服务商配置</h1>
+          <p className="text-sm text-slate-500 mt-1">管理支付渠道 (Stripe, WeChat, Alipay) 及其参数</p>
+        </div>
+        <div className="flex gap-3">
+          <button onClick={fetchProviders} className="flex items-center gap-2 px-4 py-2 bg-white border rounded-lg hover:bg-slate-50 text-slate-700 font-medium">
+            <RefreshCw size={16} /> 刷新
+          </button>
+          <button onClick={openCreate} className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm">
+            <Plus size={16} /> 添加服务商
+          </button>
+        </div>
+      </div>
+
+      <ProviderList 
+        providers={providers} 
+        loading={loading} 
+        onEdit={openEdit}
+        onManageQr={openQrManager}
+        onDelete={handleDeleteProvider} // 传递删除回调
+      />
+
+      <ProviderModal 
+        isOpen={isProviderModalOpen}
+        onClose={() => setProviderModalOpen(false)}
+        provider={selectedProvider}
+        onSubmit={handleSaveProvider}
+      />
+
+      <QrManager 
+        isOpen={isQrManagerOpen}
+        onClose={() => setQrManagerOpen(false)}
+        providerId={selectedProvider?.id}
+        providerName={selectedProvider?.name}
+      />
+    </div>
+  );
+}

+ 234 - 0
src/app/admin/products/page.tsx

@@ -0,0 +1,234 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { Plus, RefreshCw, Search } from 'lucide-react'; // 引入 Search
+import ProductList from '@/components/admin/products/ProductList';
+import ProductModal from '@/components/admin/products/ProductModal';
+import RoutingManager from '@/components/admin/products/RoutingManager';
+import SchemaManager from '@/components/admin/products/SchemaManager';
+import Pagination from '@/components/common/Pagination'; // 1. 引入分页组件
+
+export default function AdminProductsPage() {
+  // 数据状态
+  const [products, setProducts] = useState<any[]>([]);
+  const [loading, setLoading] = useState(true);
+
+  // 分页与搜索状态
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(10); // 每页 10 条
+  const [total, setTotal] = useState(0);
+  const [keyword, setKeyword] = useState('');
+
+  // Modals 状态控制
+  const [isProductModalOpen, setProductModalOpen] = useState(false);
+  const [isRoutingModalOpen, setRoutingModalOpen] = useState(false);
+  const [isSchemaModalOpen, setSchemaModalOpen] = useState(false);
+  
+  // 当前操作对象
+  const [selectedProduct, setSelectedProduct] = useState<any>(null);
+
+  // 1. 获取商品列表
+  const fetchProducts = async (targetPage: number = page) => {
+    setLoading(true);
+    try {
+      // API: GET /api/vas/product/list?page=1&size=10&keyword=...
+      const res = await api.get('/api/vas/product/list', {
+        params: {
+          page: targetPage,
+          size: pageSize,
+          keyword: keyword
+        }
+      });
+
+      // 适配后端返回结构 (兼容纯数组和分页对象)
+      const data = res.data.data || {};
+      
+      if (Array.isArray(data)) {
+        // 旧接口 (无分页)
+        setProducts(data);
+        setTotal(data.length);
+      } else {
+        // 标准分页接口 { items: [], total: 100 }
+        setProducts(data.items || []);
+        setTotal(data.total || 0);
+      }
+      
+      setPage(targetPage);
+
+    } catch (e) {
+      console.warn("Using mock products due to API failure");
+      
+      // === Mock Data & 前端模拟分页/搜索 ===
+      const mockData = [
+        { id: 1, title: 'France Visa (Tourist)', country: 'France', price_amount: 8000, price_currency: 'EUR', provider: 'TROOV', enabled: 1 },
+        { id: 2, title: 'Japan E-Visa', country: 'Japan', price_amount: 3000, price_currency: 'CNY', provider: 'Official', enabled: 1 },
+        { id: 3, title: 'US B1/B2 Interview', country: 'USA', price_amount: 120000, price_currency: 'CNY', provider: 'AIS', enabled: 1 },
+        { id: 4, title: 'Thailand Visa on Arrival', country: 'Thailand', price_amount: 45000, price_currency: 'CNY', provider: 'E-VOA', enabled: 0 },
+        { id: 5, title: 'Schengen Business (Germany)', country: 'Germany', price_amount: 9000, price_currency: 'EUR', provider: 'TLSContact', enabled: 1 },
+        { id: 6, title: 'UK Standard Visitor', country: 'UK', price_amount: 11500, price_currency: 'GBP', provider: 'TLSContact', enabled: 1 },
+        { id: 7, title: 'Singapore E-Visa', country: 'Singapore', price_amount: 30000, price_currency: 'CNY', provider: 'ICA', enabled: 1 },
+        // ... 更多数据用于测试分页 ...
+      ];
+
+      // 模拟过滤
+      let filtered = mockData;
+      if (keyword) {
+        const lowerKey = keyword.toLowerCase();
+        filtered = mockData.filter(p => 
+          p.title.toLowerCase().includes(lowerKey) || 
+          p.country.toLowerCase().includes(lowerKey) ||
+          p.provider.toLowerCase().includes(lowerKey)
+        );
+      }
+
+      // 模拟切片
+      const start = (targetPage - 1) * pageSize;
+      const end = start + pageSize;
+      
+      setProducts(filtered.slice(start, end));
+      setTotal(filtered.length);
+      setPage(targetPage);
+
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    fetchProducts(1);
+  }, []);
+
+  // 搜索处理
+  const handleSearch = () => {
+    fetchProducts(1); // 搜索重置到第一页
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') handleSearch();
+  };
+
+  // 保存商品 (Create / Update)
+  const handleSaveProduct = async (data: any) => {
+    try {
+      if (data.id) {
+        await api.post('/api/vas/product/update', data, {params: {"id": data.id}});
+      } else {
+        await api.post('/api/vas/product/create', data);
+      }
+      alert('操作成功');
+      fetchProducts(page); // 刷新当前页
+    } catch (error: any) {
+      alert('操作失败: ' + (error.response?.data?.detail || error.message));
+      throw error; 
+    }
+  };
+
+  // --- 事件处理 ---
+
+  const openCreateModal = () => {
+    setSelectedProduct(null);
+    setProductModalOpen(true);
+  };
+
+  const openEditModal = (product: any) => {
+    setSelectedProduct(product);
+    setProductModalOpen(true);
+  };
+
+  const openRoutingModal = (product: any) => {
+    setSelectedProduct(product);
+    setRoutingModalOpen(true);
+  };
+
+  const openSchemaManager = () => {
+    setSchemaModalOpen(true);
+  };
+
+  return (
+    <div>
+      {/* Page Header */}
+      <div className="flex justify-between items-center mb-6">
+        <div>
+          <h1 className="text-2xl font-bold text-slate-800">商品配置中心</h1>
+          <p className="text-sm text-slate-500 mt-1">管理上架商品、关联表单及后台路由策略</p>
+        </div>
+        <div className="flex gap-3">
+          
+          {/* === 搜索框 === */}
+          <div className="relative">
+            <input 
+              type="text" 
+              placeholder="搜索商品名/国家/Provider..." 
+              className="pl-10 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64 transition"
+              value={keyword}
+              onChange={e => setKeyword(e.target.value)}
+              onKeyDown={handleKeyDown}
+            />
+            <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+          </div>
+
+          <button 
+            onClick={handleSearch} 
+            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-700 font-medium transition shadow-sm"
+          >
+            <RefreshCw size={16} /> 刷新
+          </button>
+          
+          <button 
+            onClick={openSchemaManager} 
+            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-purple-700 font-medium transition shadow-sm"
+          >
+            表单 Schema
+          </button>
+          
+          <button 
+            onClick={openCreateModal} 
+            className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm transition"
+          >
+            <Plus size={16} /> 发布商品
+          </button>
+        </div>
+      </div>
+
+      {/* 商品列表组件 */}
+      <ProductList 
+        products={products} 
+        loading={loading} 
+        onEdit={openEditModal}
+        onManageRouting={openRoutingModal}
+      />
+
+      {/* === 分页组件 === */}
+      <Pagination 
+        currentPage={page}
+        total={total}
+        pageSize={pageSize}
+        onPageChange={(p) => fetchProducts(p)}
+      />
+
+      {/* 弹窗:创建/编辑商品 */}
+      <ProductModal 
+        isOpen={isProductModalOpen}
+        onClose={() => setProductModalOpen(false)}
+        product={selectedProduct}
+        onSubmit={handleSaveProduct}
+        onManageSchemas={openSchemaManager} 
+      />
+
+      {/* 弹窗:配置路由 */}
+      <RoutingManager 
+        isOpen={isRoutingModalOpen}
+        onClose={() => setRoutingModalOpen(false)}
+        productId={selectedProduct?.id}
+        productTitle={selectedProduct?.title}
+      />
+
+      {/* 弹窗:Schema 管理 */}
+      <SchemaManager 
+        isOpen={isSchemaModalOpen}
+        onClose={() => setSchemaModalOpen(false)}
+      />
+    </div>
+  );
+}

+ 204 - 0
src/app/admin/slots/page.tsx

@@ -0,0 +1,204 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { Search, Loader2, Users, Clock, Percent, AlertTriangle, CheckCircle } from 'lucide-react';
+
+interface SlotItem {
+  time: string;
+  rate: string | number; // 使用率 (e.g. 50, 100, 120)
+  capacity: number;
+}
+
+export default function AdminSlotsPage() {
+  const [loading, setLoading] = useState(false);
+  const [slots, setSlots] = useState<SlotItem[]>([]);
+  
+  const today = new Date().toISOString().split('T')[0];
+  const [searchDate, setSearchDate] = useState(today);
+
+  useEffect(() => {
+    fetchSlots();
+  }, []);
+
+  const fetchSlots = async () => {
+    if (!searchDate) return alert("请选择日期");
+    setLoading(true);
+    try {
+      // API: GET /api/troov/rate?date=YYYY-MM-DD
+      const res = await api.get('/api/troov/rate', {
+        params: { date: searchDate }
+      });
+      
+      const list = Array.isArray(res.data.data) ? res.data.data : (res.data.data || []);
+      // 按时间排序
+      list.sort((a: SlotItem, b: SlotItem) => a.time.localeCompare(b.time));
+      setSlots(list);
+    } catch (e) {
+      console.warn("API Error, using mock data");
+      // Mock Data: 包含正常和高风险数据
+      setSlots([
+        { time: '09:00', rate: 50, capacity: 5 },   // 正常
+        { time: '10:00', rate: 95, capacity: 2 },   // 拥挤
+        { time: '11:00', rate: 120, capacity: 10 }, // >100 高风险 (虽然有 capacity,但可能会被取消)
+        { time: '13:30', rate: 10, capacity: 0 },   // 无号
+        { time: '14:45', rate: 150, capacity: 1 },  // 极高风险
+        { time: '16:00', rate: 80, capacity: 3 },   // 正常
+      ]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 统计信息
+  const totalCapacity = slots.reduce((acc, curr) => acc + curr.capacity, 0);
+  const validSlots = slots.filter(s => s.capacity > 0 && Number(s.rate) <= 100).length;
+  const riskSlots = slots.filter(s => Number(s.rate) > 100).length;
+
+  return (
+    <div>
+      {/* 头部 & 搜索 */}
+      <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
+        <div>
+          <h1 className="text-2xl font-bold text-slate-800">Slot 监控与风险预警</h1>
+          <p className="text-sm text-slate-500 mt-1">查询预约名额容量(Capacity)及使用率(Rate)</p>
+        </div>
+
+        <div className="flex items-center gap-3 bg-white p-1.5 rounded-lg border border-slate-200 shadow-sm">
+          <div className="relative">
+            <input 
+              type="date" 
+              className="pl-9 pr-3 py-2 text-sm border-none outline-none bg-transparent text-slate-700 font-medium"
+              value={searchDate}
+              onChange={(e) => setSearchDate(e.target.value)}
+            />
+            <Clock size={16} className="absolute left-3 top-2.5 text-slate-400" />
+          </div>
+          <button 
+            onClick={fetchSlots}
+            disabled={loading}
+            className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-md text-sm font-bold hover:bg-slate-800 transition disabled:opacity-70"
+          >
+            {loading ? <Loader2 size={16} className="animate-spin" /> : <Search size={16} />}
+            查询
+          </button>
+        </div>
+      </div>
+
+      {/* 统计看板 */}
+      <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
+        <div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
+          <div>
+            <p className="text-xs text-slate-400 font-bold uppercase mb-1">总释放名额</p>
+            <p className="text-2xl font-bold text-slate-800">{totalCapacity}</p>
+          </div>
+          <div className="p-3 bg-blue-50 text-blue-600 rounded-full"><Users size={20} /></div>
+        </div>
+        
+        <div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
+          <div>
+            <p className="text-xs text-slate-400 font-bold uppercase mb-1">有效时间点 (Safe)</p>
+            <p className="text-2xl font-bold text-green-600">{validSlots}</p>
+          </div>
+          <div className="p-3 bg-green-50 text-green-600 rounded-full"><CheckCircle size={20} /></div>
+        </div>
+
+        <div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
+          <div>
+            <p className="text-xs text-slate-400 font-bold uppercase mb-1">高风险 (Rate &gt; 100)</p>
+            <p className="text-2xl font-bold text-red-600">{riskSlots}</p>
+          </div>
+          <div className="p-3 bg-red-50 text-red-600 rounded-full"><AlertTriangle size={20} /></div>
+        </div>
+      </div>
+
+      {/* Slot 列表 */}
+      {loading ? (
+        <div className="py-20 text-center text-slate-400 flex flex-col items-center">
+          <Loader2 size={32} className="animate-spin mb-3" />
+          正在同步 TROOV 数据...
+        </div>
+      ) : slots.length === 0 ? (
+        <div className="py-20 text-center border-2 border-dashed border-slate-200 rounded-xl bg-slate-50 text-slate-400">
+          该日期暂无记录
+        </div>
+      ) : (
+        <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
+          {slots.map((slot, index) => {
+            const rateVal = Number(slot.rate);
+            const isRisk = rateVal > 100;
+            const hasCapacity = slot.capacity > 0;
+
+            // 样式逻辑
+            let cardStyle = "bg-white border-slate-200 opacity-60"; // 默认无号
+            let textStyle = "text-slate-400";
+            
+            if (isRisk) {
+              // 高风险 (红色警告)
+              cardStyle = "bg-red-50 border-red-300 shadow-sm ring-1 ring-red-200";
+              textStyle = "text-red-700";
+            } else if (hasCapacity) {
+              // 正常有号 (绿色)
+              cardStyle = "bg-emerald-50 border-emerald-300 shadow-sm hover:shadow-md transition-all";
+              textStyle = "text-emerald-800";
+            }
+
+            return (
+              <div 
+                key={index}
+                className={`relative p-4 rounded-xl border flex flex-col justify-between h-36 ${cardStyle}`}
+              >
+                {/* 顶部时间 */}
+                <div className="flex justify-between items-start">
+                  <span className={`text-xl font-mono font-bold ${textStyle}`}>
+                    {slot.time}
+                  </span>
+                  {/* 高风险图标 */}
+                  {isRisk && (
+                    <AlertTriangle size={18} className="text-red-500 animate-pulse" />
+                  )}
+                </div>
+
+                <div className="space-y-2">
+                  {/* Rate 使用率 */}
+                  <div className="flex items-center justify-between text-xs">
+                    <div className="flex items-center gap-1 opacity-80">
+                      <Percent size={12} />
+                      <span>使用率</span>
+                    </div>
+                    <span className={`font-bold ${isRisk ? 'text-red-600' : 'text-slate-600'}`}>
+                      {rateVal}%
+                    </span>
+                  </div>
+                  
+                  {/* 进度条可视化 Rate */}
+                  <div className="w-full bg-black/5 rounded-full h-1.5 overflow-hidden">
+                    <div 
+                      className={`h-full rounded-full ${isRisk ? 'bg-red-500' : 'bg-emerald-500'}`} 
+                      style={{ width: `${Math.min(rateVal, 100)}%` }}
+                    ></div>
+                  </div>
+
+                  {/* Capacity 余号 */}
+                  <div className={`flex items-center justify-between text-sm font-bold pt-1 border-t border-black/5 ${textStyle}`}>
+                    <span className="text-xs opacity-80 font-normal">余号</span>
+                    <span className="flex items-center gap-1">
+                      <Users size={14} /> {slot.capacity}
+                    </span>
+                  </div>
+                </div>
+
+                {/* 高风险提示文案 */}
+                {isRisk && (
+                  <div className="absolute -top-2 -right-2 bg-red-600 text-white text-[10px] px-2 py-0.5 rounded-full shadow-sm font-bold">
+                    易取消
+                  </div>
+                )}
+              </div>
+            );
+          })}
+        </div>
+      )}
+    </div>
+  );
+}

+ 211 - 0
src/app/admin/tasks/page.tsx

@@ -0,0 +1,211 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { RefreshCw, Search } from 'lucide-react';
+import TaskTable, { VasTask } from '@/components/admin/tasks/TaskTable';
+import TaskDetailModal from '@/components/admin/tasks/TaskDetailModal';
+import TaskEditModal from '@/components/admin/tasks/TaskEditModal'; // 引入编辑弹窗
+import Pagination from '@/components/common/Pagination';
+
+export default function AdminTasksPage() {
+  const [tasks, setTasks] = useState<VasTask[]>([]);
+  const [loading, setLoading] = useState(true);
+  
+  // 分页、搜索、筛选状态
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(10);
+  const [total, setTotal] = useState(0);
+  const [keyword, setKeyword] = useState('');
+  
+  // 新增:状态筛选
+  const [statusFilter, setStatusFilter] = useState('all');
+
+  // 弹窗状态
+  const [selectedTask, setSelectedTask] = useState<VasTask | null>(null);
+  const [isDetailOpen, setIsDetailOpen] = useState(false);
+  const [isEditOpen, setIsEditOpen] = useState(false); // 编辑弹窗
+
+  // 1. 获取任务列表
+  const fetchTasks = async (targetPage: number = page) => {
+    setLoading(true);
+    try {
+      // 构造 API 参数
+      const params: any = {
+        keyword,
+        page: targetPage,
+        size: pageSize,
+      };
+      
+      // 如果不是 'all',才传 status 参数
+      if (statusFilter !== 'all') {
+        params.status = statusFilter;
+      }
+
+      // API: GET /api/vas/task/pending (注意:很多后端设计 list 接口会支持 filter)
+      // 如果后端还不支持 status 参数,你需要联系后端增加
+      const res = await api.get('/api/vas/task/list', { params });
+      
+      const data = res.data.data || {};
+      
+      if (Array.isArray(data)) {
+        setTasks(data);
+        setTotal(data.length);
+      } else {
+        setTasks(data.items || []);
+        setTotal(data.total || 0);
+      }
+      
+      setPage(targetPage);
+
+    } catch (e) {
+      console.warn("API Error, using mock data");
+      // ... Mock Logic (略,保持原有逻辑或简单过滤) ...
+      setTasks([]);
+      setTotal(0);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 监听筛选变化自动刷新
+  useEffect(() => {
+    fetchTasks(1);
+  }, [statusFilter]); // 当状态改变时,重置到第一页
+
+  const handleSearch = () => {
+    fetchTasks(1);
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') handleSearch();
+  };
+
+  // --- 操作逻辑 ---
+
+  const handleRetry = async (taskId: number) => {
+    if(!confirm("确定要重置该任务回队列吗?")) return;
+    try {
+      await api.post('/api/vas/task/return_to_queue', null, { params: { task_id: taskId } });
+      alert("操作成功");
+      fetchTasks(page);
+    } catch (e) {
+      alert("操作失败");
+    }
+  };
+
+  const handleConfirm = async (taskId: number) => {
+    if(!confirm("确定要强制标记完成吗?")) return;
+    try {
+      await api.post('/api/vas/task/manual_confirm', null, { params: { task_id: taskId } });
+      alert("操作成功");
+      fetchTasks(page);
+    } catch (e) {
+      alert("操作失败");
+    }
+  };
+
+  const handleViewDetail = (task: VasTask) => {
+    setSelectedTask(task);
+    setIsDetailOpen(true);
+  };
+
+  // 打开编辑
+  const handleEdit = (task: VasTask) => {
+    setSelectedTask(task);
+    setIsEditOpen(true);
+  };
+
+  // 提交编辑
+  const handleSubmitEdit = async (taskId: number, data: any) => {
+    try {
+      // TODO: 请确保后端有 Update 接口
+      // 假设接口:POST /api/vas/task/update
+      await api.post('/api/vas/task/update', data, {params: {"id": taskId}});
+      alert("任务更新成功");
+      setIsEditOpen(false);
+      fetchTasks(page);
+    } catch (e: any) {
+      alert("更新失败: " + (e.response?.data?.message || "未知错误"));
+    }
+  };
+
+  return (
+    <div>
+      <div className="flex flex-col md:flex-row justify-between items-center mb-6 gap-4">
+        <div>
+          <h1 className="text-2xl font-bold text-slate-800">系统任务队列</h1>
+          <p className="text-sm text-slate-500 mt-1">监控机器人执行状态及调试日志</p>
+        </div>
+        
+        <div className="flex gap-3 w-full md:w-auto">
+          
+          {/* === 新增:状态筛选下拉框 === */}
+          <select
+            className="border border-slate-300 rounded-lg text-sm px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500 bg-white"
+            value={statusFilter}
+            onChange={(e) => setStatusFilter(e.target.value)}
+          >
+            <option value="all">所有状态 (All Status)</option>
+            <option value="pending">等待中 (Pending)</option>
+            <option value="running">运行中 (Running)</option>
+            <option value="grabbed">待确认 (Grabbed)</option>
+            <option value="completed">已完成 (Completed)</option>
+            <option value="cancelled">已取消 (Cancelled)</option>
+          </select>
+
+          {/* 搜索框 */}
+          <div className="relative flex-1 md:w-64">
+            <input 
+              type="text" 
+              placeholder="TaskID / Route / OrderID..." 
+              className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+              value={keyword}
+              onChange={e => setKeyword(e.target.value)}
+              onKeyDown={handleKeyDown}
+            />
+            <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+          </div>
+
+          <button 
+            onClick={handleSearch} 
+            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-700 font-medium shadow-sm transition"
+          >
+            <RefreshCw size={16} />
+          </button>
+        </div>
+      </div>
+
+      <TaskTable 
+        tasks={tasks} 
+        loading={loading} 
+        onRetry={handleRetry} 
+        onManualConfirm={handleConfirm} 
+        onViewDetail={handleViewDetail} 
+        onEdit={handleEdit} // 传递编辑回调
+      />
+
+      <Pagination 
+        currentPage={page}
+        total={total}
+        pageSize={pageSize}
+        onPageChange={(p) => fetchTasks(p)}
+      />
+
+      {/* 详情弹窗 */}
+      <TaskDetailModal 
+        isOpen={isDetailOpen}
+        onClose={() => setIsDetailOpen(false)}
+        task={selectedTask}
+      />
+
+      {/* 编辑弹窗 */}
+      <TaskEditModal 
+        isOpen={isEditOpen}
+        onClose={() => setIsEditOpen(false)}
+        task={selectedTask}
+        onSubmit={handleSubmitEdit}
+      />
+    </div>
+  );
+}

+ 132 - 0
src/app/admin/tickets/page.tsx

@@ -0,0 +1,132 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import TicketTable, { AdminTicket } from '@/components/admin/tickets/TicketTable';
+import TicketDetailModal from '@/components/admin/tickets/TicketDetailModal';
+import Pagination from '@/components/common/Pagination';
+import { RefreshCw, Search } from 'lucide-react';
+
+export default function AdminTicketsPage() {
+  const [loading, setLoading] = useState<boolean>(true);
+  const [tickets, setTickets] = useState<AdminTicket[]>([]);
+  
+  // 分页与搜索状态
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(10);
+  const [total, setTotal] = useState(0);
+  const [keyword, setKeyword] = useState('');
+
+  // 弹窗状态
+  const [isDetailModalOpen, setIsDetailModalOpen] = useState<boolean>(false);
+  const [detailTicket, setDetailTicket] = useState<AdminTicket | null>(null);
+
+  // 获取数据
+  const fetchTickets = async (targetPage: number = page) => {
+    setLoading(true);
+    try {
+      // API: /api/vas/ticket/list_all
+      const res = await api.get('/api/vas/ticket/list_all', {
+        params: {
+          page: targetPage,
+          size: pageSize,
+          keyword: keyword
+        }
+      });
+
+      const data = res.data.data;
+      if (data && Array.isArray(data.items)) {
+        setTickets(data.items);
+        setTotal(data.total || 0);
+      } else {
+        setTickets([]);
+        setTotal(0);
+      }
+      setPage(targetPage);
+
+    } catch (error) {
+      console.error("Fetch tickets failed", error);
+      setTickets([]); // 实际环境可以留空,或者使用 Mock
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    fetchTickets(1);
+  }, []);
+
+  const handleSearch = () => fetchTickets(1);
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') handleSearch();
+  };
+
+  const handleViewDetail = (ticket: AdminTicket) => {
+    setDetailTicket(ticket);
+    setIsDetailModalOpen(true);
+  };
+
+  const handleModalClose = () => {
+    setIsDetailModalOpen(false);
+    setDetailTicket(null);
+  };
+
+  return (
+    <div className="p-6 max-w-7xl mx-auto">
+      {/* 顶部标题栏 */}
+      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
+        <div>
+          <h1 className="text-2xl font-bold text-gray-900">工单管理中心</h1>
+          <p className="text-sm text-gray-500 mt-1">处理用户的退款、投诉及服务请求</p>
+        </div>
+        
+        <div className="flex gap-2">
+          <div className="relative">
+            <input 
+              type="text" 
+              placeholder="工单号 / 订单号 / 用户" 
+              className="pl-9 pr-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 outline-none w-64 transition shadow-sm"
+              value={keyword}
+              onChange={e => setKeyword(e.target.value)}
+              onKeyDown={handleKeyDown}
+            />
+            <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+          </div>
+
+          <button 
+            onClick={handleSearch} 
+            className="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-700 text-sm font-medium transition shadow-sm flex items-center gap-2"
+          >
+            <RefreshCw size={16} /> 刷新
+          </button>
+        </div>
+      </div>
+
+      {/* 列表 */}
+      <TicketTable 
+        tickets={tickets} 
+        loading={loading} 
+        onViewDetail={handleViewDetail} 
+      />
+
+      {/* 分页 */}
+      <div className="mt-4">
+        <Pagination 
+          currentPage={page}
+          total={total}
+          pageSize={pageSize}
+          onPageChange={fetchTickets}
+        />
+      </div>
+
+      {/* 详情弹窗 (集成处理功能) */}
+      <TicketDetailModal 
+        isOpen={isDetailModalOpen}
+        onClose={handleModalClose}
+        ticket={detailTicket}
+        onUpdate={() => fetchTickets(page)} // 处理后刷新当前页
+      />
+    </div>
+  );
+}

+ 181 - 0
src/app/admin/users/page.tsx

@@ -0,0 +1,181 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { RefreshCw, Search } from 'lucide-react';
+import UserTable, { AdminUser } from '@/components/admin/users/UserTable';
+import UserModal from '@/components/admin/users/UserModal';
+import Pagination from '@/components/common/Pagination'; // 1. 引入分页组件
+
+export default function AdminUsersPage() {
+  // 数据状态
+  const [users, setUsers] = useState<AdminUser[]>([]);
+  const [loading, setLoading] = useState(true);
+  
+  // 分页与搜索状态
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(10); // 每页 10 条
+  const [total, setTotal] = useState(0);
+  const [keyword, setKeyword] = useState('');
+  
+  // 编辑弹窗
+  const [isEditOpen, setIsEditOpen] = useState(false);
+  const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null);
+
+  // 1. 获取用户列表
+  const fetchUsers = async (targetPage: number = page) => {
+    setLoading(true);
+    try {
+      // API: GET /api/user/list_all?keyword=...&page=1&size=10
+      const res = await api.get('/api/user/list_all', { 
+        params: { 
+          keyword,
+          page: targetPage,
+          size: pageSize
+        } 
+      });
+      
+      // 适配后端返回结构 (兼容纯数组和分页对象)
+      const data = res.data.data || {};
+      
+      if (Array.isArray(data)) {
+        // 兼容旧接口 (无分页)
+        setUsers(data);
+        setTotal(data.length);
+      } else {
+        // 标准分页接口 { items: [], total: 100 }
+        setUsers(data.items || []);
+        setTotal(data.total || 0);
+      }
+      
+      setPage(targetPage);
+
+    } catch (e) {
+      console.warn("API Error, using mock users with pagination logic");
+      
+      // === Mock Data & 前端模拟分页/搜索 ===
+      const mockUsers: AdminUser[] = [
+        { id: '1', email: 'admin@visafly.com', nickname: 'Super Admin', role: 'admin', email_verified: 1, created_at: '2024-01-01' },
+        { id: '2', email: 'user@example.com', nickname: 'John Doe', role: 'user', email_verified: 0, created_at: '2025-01-10' },
+        { id: '3', email: 'alice@test.com', nickname: 'Alice', role: 'user', email_verified: 1, created_at: '2025-02-15' },
+        { id: '4', email: 'bob@test.com', nickname: 'Bob Smith', role: 'user', email_verified: 1, created_at: '2025-02-16' },
+        { id: '5', email: 'charlie@test.com', nickname: 'Charlie', role: 'user', email_verified: 0, created_at: '2025-02-17' },
+        { id: '6', email: 'david@test.com', nickname: 'David', role: 'user', email_verified: 1, created_at: '2025-02-18' },
+        { id: '7', email: 'eve@test.com', nickname: 'Eve', role: 'user', email_verified: 1, created_at: '2025-02-19' },
+        { id: '8', email: 'frank@test.com', nickname: 'Frank', role: 'admin', email_verified: 1, created_at: '2025-02-20' },
+        { id: '9', email: 'grace@test.com', nickname: 'Grace', role: 'user', email_verified: 0, created_at: '2025-02-21' },
+        { id: '10', email: 'heidi@test.com', nickname: 'Heidi', role: 'user', email_verified: 1, created_at: '2025-02-22' },
+        { id: '11', email: 'ivan@test.com', nickname: 'Ivan', role: 'user', email_verified: 1, created_at: '2025-02-23' },
+      ];
+
+      // 模拟过滤
+      let filtered = mockUsers;
+      if (keyword) {
+        const lowerKey = keyword.toLowerCase();
+        filtered = mockUsers.filter(u => 
+          u.email.toLowerCase().includes(lowerKey) || 
+          (u.nickname && u.nickname.toLowerCase().includes(lowerKey)) ||
+          u.id.includes(lowerKey)
+        );
+      }
+
+      // 模拟切片
+      const start = (targetPage - 1) * pageSize;
+      const end = start + pageSize;
+      setUsers(filtered.slice(start, end));
+      setTotal(filtered.length);
+      setPage(targetPage);
+
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    fetchUsers(1);
+  }, []);
+
+  // 搜索处理
+  const handleSearch = () => {
+    fetchUsers(1); // 重置到第一页
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') handleSearch();
+  };
+
+  const handleEditUser = (user: AdminUser) => {
+    setSelectedUser(user);
+    setIsEditOpen(true);
+  };
+
+  const handleSubmitUpdate = async (userId: string, data: any) => {
+    try {
+      // API: POST /api/user/update?uid=xxx
+      await api.post('/api/user/update', data, {
+        params: { uid: userId }
+      });
+      
+      alert("用户信息更新成功");
+      setIsEditOpen(false);
+      fetchUsers(page); // 刷新当前页
+    } catch (e: any) {
+      console.error(e);
+      alert("更新失败: " + (e.response?.data?.message || e.message || "未知错误"));
+    }
+  };
+
+  return (
+    <div>
+      <div className="flex justify-between items-center mb-6">
+        <div>
+          <h1 className="text-2xl font-bold text-slate-800">用户管理</h1>
+          <p className="text-sm text-slate-500 mt-1">查看注册用户、管理权限及状态</p>
+        </div>
+        <div className="flex gap-3">
+          
+          {/* === 搜索框 === */}
+          <div className="relative">
+            <input 
+              type="text" 
+              placeholder="搜索邮箱/昵称..." 
+              className="pl-10 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64 transition"
+              value={keyword}
+              onChange={e => setKeyword(e.target.value)}
+              onKeyDown={handleKeyDown}
+            />
+            <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+          </div>
+
+          <button 
+            onClick={handleSearch} 
+            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-700 font-medium shadow-sm active:scale-95 transition"
+          >
+            <RefreshCw size={16} /> 刷新
+          </button>
+        </div>
+      </div>
+
+      <UserTable 
+        users={users} 
+        loading={loading} 
+        onEdit={handleEditUser} 
+      />
+
+      {/* === 分页组件 === */}
+      <Pagination 
+        currentPage={page}
+        total={total}
+        pageSize={pageSize}
+        onPageChange={(p) => fetchUsers(p)}
+      />
+
+      <UserModal 
+        isOpen={isEditOpen} 
+        onClose={() => setIsEditOpen(false)} 
+        user={selectedUser}
+        onSubmit={handleSubmitUpdate}
+      />
+    </div>
+  );
+}

+ 41 - 0
src/app/create-order/[id]/page.tsx

@@ -0,0 +1,41 @@
+'use client';
+
+import CreateOrderForm from '@/components/CreateOrderForm'; // 如果报错,尝试 ../../../components/CreateOrderForm
+import { useEffect, useState } from 'react';
+
+// 这里暂时硬编码商品名称映射,或者你可以稍后改为从 API 获取详情
+const PRODUCT_MAP: Record<string, string> = {
+  '1': '日本单次旅游签证',
+  '2': '泰国电子签',
+  '3': '美国B1/B2咨询',
+  // 如果你的真实ID是其他数字,请在这里添加
+};
+
+export default function CreateOrderPage({ params }: { params: { id: string } }) {
+  const [productName, setProductName] = useState('加载中...');
+
+  useEffect(() => {
+    // 如果有获取商品详情的 API,应该在这里调用
+    // 目前先用 ID 查找简单的名字,或者直接显示 ID
+    const name = PRODUCT_MAP[params.id] || `未知商品 (ID: ${params.id})`;
+    setProductName(name);
+  }, [params.id]);
+
+  return (
+    <div className="min-h-screen bg-slate-50 py-12 px-4">
+      <div className="max-w-3xl mx-auto">
+        <button 
+          onClick={() => window.history.back()} 
+          className="mb-6 text-gray-500 hover:text-gray-900 flex items-center text-sm"
+        >
+          ← 返回服务列表
+        </button>
+        
+        <CreateOrderForm 
+          productId={params.id} 
+          productName={productName} 
+        />
+      </div>
+    </div>
+  );
+}

+ 183 - 0
src/app/dashboard/page.tsx

@@ -0,0 +1,183 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { LogOut, Plus } from 'lucide-react';
+
+// 引入各个业务组件
+import Sidebar from '@/components/dashboard/Sidebar';
+import OrderList from '@/components/dashboard/OrderList';
+import TicketList from '@/components/dashboard/TicketList';
+import ProfileSettings from '@/components/dashboard/ProfileSettings';
+
+// 引入弹窗组件
+import TicketModal from '@/components/dashboard/TicketModal';
+import UserOrderDetailModal, { UserOrder } from '@/components/dashboard/UserOrderDetailModal';
+import UserTicketDetailModal, { UserTicket } from '@/components/dashboard/UserTicketDetailModal';
+
+export default function DashboardPage() {
+  const router = useRouter();
+  
+  // 当前激活的标签页: 'orders' | 'tickets' | 'settings'
+  const [activeTab, setActiveTab] = useState<string>('orders');
+  
+  // --- 状态管理 ---
+
+  // 1. 新建工单弹窗
+  const [isTicketModalOpen, setIsTicketModalOpen] = useState<boolean>(false);
+  const [ticketDefaultOrderId, setTicketDefaultOrderId] = useState<string>('');
+
+  // 2. 订单详情弹窗
+  const [isOrderDetailOpen, setIsOrderDetailOpen] = useState<boolean>(false);
+  const [selectedOrder, setSelectedOrder] = useState<UserOrder | null>(null);
+
+  // 3. 工单详情/回复弹窗
+  const [isTicketDetailOpen, setIsTicketDetailOpen] = useState<boolean>(false);
+  const [selectedTicket, setSelectedTicket] = useState<UserTicket | null>(null);
+  
+  // 4. 刷新触发器 (用于子组件重新加载数据)
+  const [refreshTickets, setRefreshTickets] = useState<number>(0);
+
+  // --- 事件处理 ---
+
+  const handleLogout = () => {
+    if (confirm('确定要退出登录吗?')) {
+      localStorage.removeItem('rsid');
+      localStorage.removeItem('user_info');
+      // 触发 storage 事件,通知 Navbar 更新状态
+      window.dispatchEvent(new Event('storage'));
+      router.push('/login');
+    }
+  };
+
+  // 打开新建工单弹窗 (可选带入 OrderID)
+  const openCreateTicketModal = (orderId: string = '') => {
+    setTicketDefaultOrderId(orderId);
+    setIsTicketModalOpen(true);
+    // 如果是在订单页点击售后,为了体验顺畅,可以考虑自动切到工单列表,或者保持不动
+    // setActiveTab('tickets'); 
+  };
+
+  // 打开订单详情
+  const openOrderDetail = (order: UserOrder) => {
+    setSelectedOrder(order);
+    setIsOrderDetailOpen(true);
+  };
+
+  // 打开工单详情
+  const openTicketDetail = (ticket: UserTicket) => {
+    setSelectedTicket(ticket);
+    setIsTicketDetailOpen(true);
+  };
+
+  // 工单更新回调 (比如用户回复了信息,刷新列表)
+  const handleTicketUpdate = () => {
+    setRefreshTickets(prev => prev + 1);
+  };
+
+  return (
+    <div className="min-h-screen bg-slate-50">
+      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
+        
+        {/* === 顶部 Header === */}
+        <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
+          <div>
+            <h1 className="text-2xl font-bold text-gray-900">用户控制台</h1>
+            <p className="text-sm text-gray-500 mt-1">管理您的签证申请进度和售后服务</p>
+          </div>
+          <div className="flex gap-3">
+             <button 
+               onClick={() => router.push('/services')} 
+               className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition shadow-sm"
+             >
+               <Plus size={16} /> 新建申请
+             </button>
+             <button 
+               onClick={handleLogout} 
+               className="flex items-center gap-2 bg-white border border-gray-300 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 text-gray-700 transition"
+             >
+               <LogOut size={16} /> 退出
+             </button>
+          </div>
+        </div>
+
+        {/* === 主布局 === */}
+        <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
+          
+          {/* 左侧侧边栏 */}
+          <div className="lg:col-span-1">
+            <Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
+          </div>
+
+          {/* 右侧内容区 */}
+          <div className="lg:col-span-3">
+            
+            {/* Tab 1: 订单列表 */}
+            {activeTab === 'orders' && (
+              <div className="animate-in fade-in slide-in-from-right-4 duration-300">
+                <OrderList 
+                  onRequestTicket={openCreateTicketModal} 
+                  onViewDetail={openOrderDetail} 
+                />
+              </div>
+            )}
+
+            {/* Tab 2: 售后工单 */}
+            {activeTab === 'tickets' && (
+              <div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300">
+                {/* 顶部操作栏 */}
+                <div className="flex justify-end">
+                  <button 
+                    onClick={() => openCreateTicketModal()}
+                    className="flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 transition shadow-sm"
+                  >
+                    <Plus size={16} /> 提交新工单
+                  </button>
+                </div>
+                
+                {/* 工单列表组件 */}
+                <TicketList 
+                  onViewDetail={openTicketDetail} 
+                  refreshTrigger={refreshTickets}
+                />
+              </div>
+            )}
+
+            {/* Tab 3: 账户设置 (新增) */}
+            {activeTab === 'settings' && (
+              <div className="animate-in fade-in slide-in-from-right-4 duration-300">
+                {/* ProfileSettings 组件内部处理了 查看/编辑 模式切换 */}
+                <ProfileSettings />
+              </div>
+            )}
+
+          </div>
+        </div>
+      </div>
+
+      {/* === 全局弹窗组件 === */}
+
+      {/* 1. 新建工单弹窗 */}
+      <TicketModal 
+        isOpen={isTicketModalOpen} 
+        onClose={() => setIsTicketModalOpen(false)}
+        defaultOrderId={ticketDefaultOrderId}
+      />
+
+      {/* 2. 订单详情弹窗 */}
+      <UserOrderDetailModal 
+        isOpen={isOrderDetailOpen} 
+        onClose={() => setIsOrderDetailOpen(false)}
+        order={selectedOrder}
+      />
+
+      {/* 3. 工单详情/回复弹窗 */}
+      <UserTicketDetailModal 
+        isOpen={isTicketDetailOpen}
+        onClose={() => setIsTicketDetailOpen(false)}
+        ticket={selectedTicket}
+        onUpdate={handleTicketUpdate}
+      />
+    </div>
+  );
+}

+ 79 - 0
src/app/dashboard/settings/page.tsx

@@ -0,0 +1,79 @@
+'use client';
+
+import ProfileSettings from '@/components/dashboard/ProfileSettings';
+import Sidebar from '@/components/dashboard/Sidebar';
+import { useRouter } from 'next/navigation';
+import { LogOut, Plus } from 'lucide-react';
+
+export default function SettingsPage() {
+  const router = useRouter();
+
+  const handleLogout = () => {
+    localStorage.removeItem('rsid');
+    localStorage.removeItem('user_info');
+    router.push('/login');
+  };
+
+  return (
+    <div className="min-h-screen bg-slate-50">
+      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
+        
+        {/* Header (与 Dashboard 保持一致) */}
+        <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
+          <div>
+            <h1 className="text-2xl font-bold text-gray-900">账户设置</h1>
+            <p className="text-sm text-gray-500 mt-1">管理您的个人资料和安全选项</p>
+          </div>
+          <div className="flex gap-3">
+             <button 
+               onClick={() => router.push('/services')} 
+               className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition shadow-sm"
+             >
+               <Plus size={16} /> 新建申请
+             </button>
+             <button 
+               onClick={handleLogout} 
+               className="flex items-center gap-2 bg-white border border-gray-300 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 text-gray-700 transition"
+             >
+               <LogOut size={16} /> 退出
+             </button>
+          </div>
+        </div>
+
+        <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
+          
+          {/* 左侧侧边栏 (复用 Sidebar,它会自动检测路径变为设置模式) */}
+          <div className="lg:col-span-1">
+            <Sidebar />
+          </div>
+
+          {/* 右侧内容区 */}
+          <div className="lg:col-span-3">
+            
+            <div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
+              {/* 主要设置区域 */}
+              <div className="xl:col-span-2">
+                <ProfileSettings />
+              </div>
+              
+              {/* 右侧提示信息 (在大屏幕显示) */}
+              <div className="hidden xl:block space-y-6">
+                <div className="bg-blue-50 p-6 rounded-xl border border-blue-100">
+                  <h3 className="font-bold text-blue-800 mb-2 text-sm">账户安全</h3>
+                  <p className="text-xs text-blue-600 leading-relaxed mb-4">
+                    为了保障您的账户安全,建议您定期更新个人信息。如果您发现账户有异常登录情况,请立即联系客服。
+                  </p>
+                  <div className="h-px bg-blue-200 my-4"></div>
+                  <p className="text-xs text-blue-500">
+                    当前账号状态:<span className="font-bold text-green-600">正常</span>
+                  </p>
+                </div>
+              </div>
+            </div>
+
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 8 - 0
src/app/globals.css

@@ -0,0 +1,8 @@
+/* src/app/globals.css */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+  background-color: #f8fafc; /* bg-slate-50 */
+}

+ 155 - 0
src/app/knowledge/page.tsx

@@ -0,0 +1,155 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { Search, BookOpen, Loader2 } from 'lucide-react';
+import KnowledgeCard from '@/components/knowledge/KnowledgeCard';
+import Pagination from '@/components/common/Pagination';
+
+export default function KnowledgePage() {
+  const [loading, setLoading] = useState(true);
+  const [cards, setCards] = useState<any[]>([]);
+  
+  // 搜索与分页
+  const [keyword, setKeyword] = useState('');
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(9);
+  const [total, setTotal] = useState(0);
+
+  // 初始化
+  useEffect(() => {
+    fetchCards(1);
+  }, []);
+
+  const fetchCards = async (targetPage: number) => {
+    setLoading(true);
+    try {
+      // API: GET /api/cards/view2
+      const res = await api.get('/api/cards/view2', {
+        params: {
+          keywords: keyword,
+          page: targetPage,
+          size: pageSize,
+          culture: 'english'
+        }
+      });
+
+      const data = res.data.data || {};
+      
+      if (Array.isArray(data)) {
+        setCards(data);
+        setTotal(data.length);
+      } else {
+        setCards(data.items || []);
+        setTotal(data.total || 0);
+      }
+      setPage(targetPage);
+
+    } catch (error) {
+      console.warn("API Error, using mock data");
+      setCards([
+        {
+          id: 53,
+          image: null,
+          title: "French Visa Stamps and Envelope Purchase",
+          content: "<p>You can purchase stamps and envelopes at An Post. Just tell the staff that they are for France Visa use, and they will provide them to you. The stamp costs €9.5, and you can buy a large yellow envelope.</p>",
+          label: "France Visa",
+          country: "France",
+          created_at: "2024-11-26T11:22:55"
+        },
+        {
+          id: 54,
+          image: "3,3f53a1b2ec", 
+          title: "Photo Requirements for Schengen Visa",
+          content: "<p>Photos must be 35x45mm, white background, no glasses. <a href='#'>Check details here</a>.</p><ul><li>Size: 35x45mm</li><li>Background: White</li></ul>",
+          label: "General",
+          country: "Schengen",
+          created_at: "2024-12-01T09:00:00"
+        }
+      ]);
+      setTotal(2);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSearch = () => {
+    fetchCards(1);
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') handleSearch();
+  };
+
+  return (
+    <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
+      <div className="max-w-7xl mx-auto">
+        
+        {/* Header Section */}
+        <div className="text-center mb-12">
+          <h1 className="text-3xl font-bold text-slate-900 flex items-center justify-center gap-3">
+            <BookOpen className="text-blue-600" /> 办理指南 & 常见问题
+          </h1>
+          <p className="text-slate-500 mt-3 max-w-2xl mx-auto">
+            这里汇集了签证办理过程中的常见问题解答、材料准备指南以及最新政策解读。
+          </p>
+        </div>
+
+        {/* Search Bar */}
+        <div className="max-w-2xl mx-auto mb-12 relative">
+          <div className="relative">
+            <input 
+              type="text" 
+              placeholder="搜索关键词,例如:照片、邮票、指纹..." 
+              className="w-full pl-12 pr-28 py-4 rounded-xl border border-slate-200 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-lg transition"
+              value={keyword}
+              onChange={(e) => setKeyword(e.target.value)}
+              onKeyDown={handleKeyDown}
+            />
+            {/* 修复:垂直居中 */}
+            <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" size={24} />
+            <button 
+              onClick={handleSearch}
+              className="absolute right-2 top-1/2 -translate-y-1/2 bg-slate-900 text-white px-6 py-2.5 rounded-lg font-bold hover:bg-slate-800 transition"
+            >
+              搜索
+            </button>
+          </div>
+        </div>
+
+        {/* Content Grid */}
+        {loading ? (
+          <div className="flex justify-center py-20">
+            <Loader2 className="animate-spin text-blue-600 w-10 h-10" />
+          </div>
+        ) : cards.length === 0 ? (
+          <div className="text-center py-20 text-slate-400">
+            <BookOpen size={48} className="mx-auto mb-4 opacity-20" />
+            <p>未找到相关指南,请尝试更换关键词</p>
+          </div>
+        ) : (
+          /* 
+             核心修复:添加 items-start 
+             这会让每一行的卡片顶部对齐,但高度各算各的,不会互相拉伸。
+          */
+          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-start">
+            {cards.map((card) => (
+              <KnowledgeCard key={card.id} data={card} />
+            ))}
+          </div>
+        )}
+
+        {/* Pagination */}
+        <div className="mt-8">
+          <Pagination 
+            currentPage={page}
+            total={total}
+            pageSize={pageSize}
+            onPageChange={(p) => fetchCards(p)}
+          />
+        </div>
+
+      </div>
+    </div>
+  );
+}

+ 20 - 0
src/app/layout.tsx

@@ -0,0 +1,20 @@
+import './globals.css';
+import Navbar from '@/components/Navbar';
+import Footer from '@/components/Footer'; // 引入 Footer
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+  return (
+    <html lang="zh-CN">
+      <body className="bg-slate-50 min-h-screen flex flex-col"> {/* flex flex-col 确保 footer 沉底 */}
+        <Navbar />
+        
+        {/* main 自动撑开高度 */}
+        <main className="flex-grow">
+          {children}
+        </main>
+        
+        <Footer /> {/* 放在这里 */}
+      </body>
+    </html>
+  );
+}

+ 12 - 0
src/app/login/page.tsx

@@ -0,0 +1,12 @@
+'use client'; // 这一行依然需要,因为 AuthForm 是客户端组件
+
+import AuthForm from '@/components/AuthForm';
+
+export default function LoginPage() {
+  return (
+    // 这里只负责全屏居中布局
+    <div className="min-h-screen w-full flex items-center justify-center bg-slate-50 px-4">
+      <AuthForm />
+    </div>
+  );
+}

+ 106 - 0
src/app/page.tsx

@@ -0,0 +1,106 @@
+'use client';
+
+import Link from "next/link";
+import { ArrowRight, Globe, CheckCircle, Zap, FileText, UserCheck, Bot, CreditCard } from "lucide-react";
+
+export default function Home() {
+  const steps = [
+    {
+      icon: Globe,
+      title: "1. 选择服务",
+      desc: "在服务列表中找到您需要办理的国家和签证类型(如日本旅游签、法国申根签)。"
+    },
+    {
+      icon: FileText,
+      title: "2. 提交资料",
+      desc: "填写简单的申请表单(支持 JSON 动态表单),无需繁琐的手写文件。"
+    },
+    {
+      icon: Bot,
+      title: "3. 智能托管",
+      desc: "我们的 24/7 机器人系统会自动为您监控名额并锁定预约,无需人工守候。"
+    },
+    {
+      icon: CheckCircle,
+      title: "4. 成功出签",
+      desc: "预约成功后支付费用(支持随机立减),获取确认函,准备递签。"
+    }
+  ];
+
+  return (
+    <div className="flex flex-col items-center">
+      {/* Hero Section */}
+      <section className="w-full bg-white py-24 px-4 text-center border-b border-slate-100">
+        <h1 className="text-5xl font-bold tracking-tight text-slate-900 mb-6">
+          签证申请,<span className="text-blue-600">从未如此简单</span>
+        </h1>
+        <p className="text-xl text-slate-500 max-w-2xl mx-auto mb-10">
+          Visafly 为您提供全球签证自动化处理服务。实时追踪状态,专家级审核,让您的出行无后顾之忧。
+        </p>
+        <div className="flex gap-4 justify-center">
+          <Link href="/services" className="px-8 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 flex items-center gap-2 transition shadow-lg shadow-blue-200">
+            开始申请 <ArrowRight size={20} />
+          </Link>
+          <Link href="/slots" className="px-8 py-3 bg-white border border-slate-300 text-slate-700 rounded-lg font-medium hover:bg-slate-50 transition">
+            查询名额
+          </Link>
+        </div>
+      </section>
+
+      {/* Work Process (新增模块) */}
+      <section className="w-full bg-slate-50 py-20 px-4">
+        <div className="max-w-7xl mx-auto">
+          <div className="text-center mb-16">
+            <h2 className="text-3xl font-bold text-slate-900">工作流程</h2>
+            <p className="text-slate-500 mt-2">只需 4 步,全自动化处理您的签证需求</p>
+          </div>
+
+          <div className="grid grid-cols-1 md:grid-cols-4 gap-8 relative">
+            {/* 连接线 (仅在大屏幕显示) */}
+            <div className="hidden md:block absolute top-12 left-0 w-full h-0.5 bg-slate-200 -z-10 transform scale-x-75" />
+
+            {steps.map((step, index) => {
+              const Icon = step.icon;
+              return (
+                <div key={index} className="flex flex-col items-center text-center bg-white p-6 rounded-xl shadow-sm border border-slate-100 relative z-10 h-full hover:-translate-y-1 transition duration-300">
+                  <div className="w-24 h-24 bg-blue-50 rounded-full flex items-center justify-center mb-6 border-4 border-white shadow-sm text-blue-600">
+                    <Icon size={40} />
+                  </div>
+                  <h3 className="text-xl font-bold text-slate-800 mb-3">{step.title}</h3>
+                  <p className="text-sm text-slate-500 leading-relaxed">
+                    {step.desc}
+                  </p>
+                </div>
+              );
+            })}
+          </div>
+        </div>
+      </section>
+      
+      {/* Features */}
+      <section className="max-w-7xl mx-auto py-20 px-4 grid md:grid-cols-3 gap-10">
+        <div className="p-8 bg-white rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition">
+          <div className="bg-blue-100 w-12 h-12 rounded-lg flex items-center justify-center mb-4">
+            <Globe className="text-blue-600" />
+          </div>
+          <h3 className="text-xl font-bold mb-2">覆盖全球</h3>
+          <p className="text-slate-600">支持美国、日本、申根区等超过 50 个国家和地区的签证办理。</p>
+        </div>
+        <div className="p-8 bg-white rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition">
+          <div className="bg-green-100 w-12 h-12 rounded-lg flex items-center justify-center mb-4">
+            <CheckCircle className="text-green-600" />
+          </div>
+          <h3 className="text-xl font-bold mb-2">高成功率</h3>
+          <p className="text-slate-600">智能系统预审加上人工专家复核,确保资料准确无误。</p>
+        </div>
+        <div className="p-8 bg-white rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition">
+          <div className="bg-purple-100 w-12 h-12 rounded-lg flex items-center justify-center mb-4">
+            <Zap className="text-purple-600" />
+          </div>
+          <h3 className="text-xl font-bold mb-2">极速处理</h3>
+          <p className="text-slate-600">对接官方 API,自动化流程将申请时间缩短至传统的 1/3。</p>
+        </div>
+      </section>
+    </div>
+  );
+}

+ 10 - 0
src/app/payment/[id]/page.tsx

@@ -0,0 +1,10 @@
+'use client';
+import PaymentProcessor from '@/components/PaymentProcessor';
+
+export default function PaymentPage({ params }) {
+  return (
+    <div className="min-h-screen bg-slate-50 py-12 px-4">
+      <PaymentProcessor orderId={params.id} />
+    </div>
+  );
+}

+ 93 - 0
src/app/refund-policy/page.tsx

@@ -0,0 +1,93 @@
+'use client';
+
+import { ShieldCheck, HelpCircle, AlertTriangle } from 'lucide-react';
+
+export default function RefundPolicyPage() {
+  return (
+    <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
+      <div className="max-w-4xl mx-auto bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
+        
+        {/* Header */}
+        <div className="bg-slate-900 text-white p-8 sm:p-12 text-center">
+          <div className="flex justify-center mb-4">
+            <ShieldCheck size={48} className="text-blue-400" />
+          </div>
+          <h1 className="text-3xl font-bold mb-2">退款政策 & 服务条款</h1>
+          <p className="text-slate-400">为了保障您的权益,请仔细阅读以下条款</p>
+        </div>
+
+        {/* Content */}
+        <div className="p-8 sm:p-12 space-y-10 text-slate-700 leading-relaxed">
+          
+          {/* Section 1 */}
+          <section>
+            <h2 className="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
+              <span className="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-sm font-bold">1</span>
+              全额退款情形
+            </h2>
+            <p className="mb-4">在以下情况下,您可以申请全额退款:</p>
+            <ul className="list-disc pl-6 space-y-2 text-sm marker:text-blue-500">
+              <li>您已支付订单,但我们的系统尚未开始执行任何预约操作(Status 为 Pending)。</li>
+              <li>由于 Visafly 系统故障导致重复扣款。</li>
+              <li>我们在承诺的时限内(通常为 30 天,具体视服务而定)未能为您成功预约到名额。</li>
+            </ul>
+          </section>
+
+          {/* Section 2 */}
+          <section>
+            <h2 className="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
+              <span className="w-8 h-8 rounded-full bg-orange-100 text-orange-600 flex items-center justify-center text-sm font-bold">2</span>
+              无法退款情形
+            </h2>
+            <div className="bg-orange-50 border-l-4 border-orange-400 p-4 mb-4 text-sm text-orange-800">
+              <p className="font-bold flex items-center gap-2 mb-1">
+                <AlertTriangle size={16} /> 重要提示
+              </p>
+              <p>一旦服务进入执行阶段或产生第三方费用,退款将受到限制。</p>
+            </div>
+            <ul className="list-disc pl-6 space-y-2 text-sm marker:text-orange-500">
+              <li>我们的系统已经为您成功预约到了名额(以截图或确认函为准)。</li>
+              <li>您的签证申请已经递交至大使馆或签证中心。</li>
+              <li>因您提供的个人信息(如护照号)错误导致预约失败或无效。</li>
+              <li>您因个人原因(如改变行程、生病)决定放弃申请,但此时我们已完成了预约工作。</li>
+            </ul>
+          </section>
+
+          {/* Section 3 */}
+          <section>
+            <h2 className="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
+              <span className="w-8 h-8 rounded-full bg-slate-100 text-slate-600 flex items-center justify-center text-sm font-bold">3</span>
+              退款流程
+            </h2>
+            <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-center text-sm">
+              <div className="p-4 border rounded-lg bg-slate-50">
+                <div className="font-bold text-slate-900 mb-1">1. 提交工单</div>
+                <p className="text-slate-500">在控制台选择订单,点击“售后/帮助”提交申请。</p>
+              </div>
+              <div className="p-4 border rounded-lg bg-slate-50">
+                <div className="font-bold text-slate-900 mb-1">2. 客服审核</div>
+                <p className="text-slate-500">我们将在 1-3 个工作日内核实订单状态。</p>
+              </div>
+              <div className="p-4 border rounded-lg bg-slate-50">
+                <div className="font-bold text-slate-900 mb-1">3. 原路退回</div>
+                <p className="text-slate-500">批准后,资金将在 5-10 个工作日原路退回。</p>
+              </div>
+            </div>
+          </section>
+
+          {/* Section 4 */}
+          <section className="border-t pt-8">
+            <h2 className="text-lg font-bold text-slate-900 mb-2 flex items-center gap-2">
+              <HelpCircle size={20} className="text-slate-400" /> 还有疑问?
+            </h2>
+            <p className="text-sm">
+              如果您对退款政策有任何疑问,请联系我们的客服团队:
+              <a href="mailto:support@visafly.com" className="text-blue-600 hover:underline ml-1">support@visafly.com</a>
+            </p>
+          </section>
+
+        </div>
+      </div>
+    </div>
+  );
+}

+ 12 - 0
src/app/services/page.js

@@ -0,0 +1,12 @@
+'use client';
+
+import ServiceList from '@/components/ServiceList';
+
+export default function ServicesPage() {
+  return (
+    <div className="max-w-7xl mx-auto py-12 px-4">
+      <h1 className="text-3xl font-bold mb-8 text-gray-900">热门签证服务</h1>
+      <ServiceList />
+    </div>
+  );
+}

+ 257 - 0
src/app/slots/page.tsx

@@ -0,0 +1,257 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { Search, MapPin, Calendar, Clock, RefreshCw, AlertCircle, CheckCircle } from 'lucide-react';
+
+// === 类型定义 ===
+
+interface TimeSlot {
+  time: string;
+  label?: string; // e.g. "Prime Time", "VIP"
+}
+
+interface DayAvailability {
+  date: string;
+  times: TimeSlot[];
+}
+
+interface SlotSnapshot {
+  id: number;
+  country: string;
+  city: string;
+  visa_type: string;
+  availability_status: 'None' | 'Available' | 'Waitlist';
+  earliest_date: string | null;
+  snapshot_at: string;
+  slots_data: DayAvailability[]; // 对应数据库的 availability JSON
+}
+
+export default function SlotQueryPage() {
+  const [loading, setLoading] = useState(false);
+  const [snapshot, setSnapshot] = useState<SlotSnapshot | null>(null);
+  
+  // 筛选状态
+  const [country, setCountry] = useState('France');
+  const [city, setCity] = useState('Dublin');
+  const [visaType, setVisaType] = useState('Short Stay');
+
+  // 模拟配置 (实际项目中这些选项也应该从 API 获取)
+  const options = {
+    countries: [
+      'Austria',
+      'Croatia',
+      'Denmark',
+      'Finland',
+      'France',
+      'Germany',
+      'Greece',
+      'Hungary',
+      'Iceland',
+      'Italy',
+      'Netherlands',
+      'Poland',
+      'Spain'
+    ],
+    cities: [
+      'Dublin',
+      'Edinburgh',
+      'London',
+      'Manchester',
+      'Melbourne',
+      'Montreal',
+      'Singapore',
+      'Sydney',
+      'Toronto'
+    ],
+    types: [
+      'Tourist',
+      'Business',
+      'Family',
+      'Student',
+      'Work',
+      'Transit',
+      'e-Visa'
+    ]
+  };
+
+  const fetchSlots = async () => {
+    setLoading(true);
+    try {
+      // API: GET /api/public/slots/latest
+      const res = await api.get('/api/slots/latest', {
+        params: { country, city, visa_type: visaType }
+      });
+      
+      const data = res.data.data;
+      if (data) {
+        setSnapshot(data);
+      } else {
+        setSnapshot(null); // 无数据
+      }
+    } catch (e) {
+      console.warn("API Error, using mock data");
+      // --- Mock Data 演示 ---
+      setSnapshot({
+        id: 1,
+        country, city, visa_type: visaType,
+        availability_status: 'Available',
+        earliest_date: '2025-02-14',
+        snapshot_at: new Date().toISOString(),
+        slots_data: [
+          {
+            date: '2025-02-14',
+            times: [
+              { time: '09:00', label: 'Premium' },
+              { time: '09:15', label: 'Premium' },
+              { time: '10:30', label: 'Normal' }
+            ]
+          },
+          {
+            date: '2025-02-18',
+            times: [
+              { time: '14:00', label: 'Normal' },
+              { time: '15:00', label: 'Normal' }
+            ]
+          }
+        ]
+      });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 辅助函数:格式化日期
+  const formatDate = (dateStr: string) => {
+    const date = new Date(dateStr);
+    return date.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' });
+  };
+
+  return (
+    <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
+      <div className="max-w-5xl mx-auto">
+        
+        {/* 标题区 */}
+        <div className="text-center mb-10">
+          <h1 className="text-3xl font-bold text-slate-900">签证名额查询</h1>
+          <p className="text-slate-500 mt-2">实时查询各使馆最新可预约名额</p>
+        </div>
+
+        {/* 筛选区 */}
+        <div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 mb-8">
+          <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+            <div>
+              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">申请国家</label>
+              <select 
+                className="w-full border rounded-lg p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500 transition"
+                value={country} onChange={e => setCountry(e.target.value)}
+              >
+                {options.countries.map(c => <option key={c} value={c}>{c}</option>)}
+              </select>
+            </div>
+            <div>
+              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">递签城市</label>
+              <select 
+                className="w-full border rounded-lg p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500 transition"
+                value={city} onChange={e => setCity(e.target.value)}
+              >
+                {options.cities.map(c => <option key={c} value={c}>{c}</option>)}
+              </select>
+            </div>
+            <div>
+              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">签证类型</label>
+              <select 
+                className="w-full border rounded-lg p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500 transition"
+                value={visaType} onChange={e => setVisaType(e.target.value)}
+              >
+                {options.types.map(c => <option key={c} value={c}>{c}</option>)}
+              </select>
+            </div>
+          </div>
+          <div className="mt-6 flex justify-end">
+            <button 
+              onClick={fetchSlots}
+              disabled={loading}
+              className="flex items-center gap-2 bg-slate-900 text-white px-8 py-2.5 rounded-lg font-bold hover:bg-slate-800 transition disabled:opacity-70 shadow-lg shadow-slate-200"
+            >
+              {loading ? <RefreshCw className="animate-spin" size={18} /> : <Search size={18} />}
+              查询名额
+            </button>
+          </div>
+        </div>
+
+        {/* 结果展示区 */}
+        {snapshot ? (
+          <div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
+            
+            {/* 概览 Banner */}
+            <div className={`p-6 rounded-xl border flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4
+              ${snapshot.availability_status === 'Available' ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200'}
+            `}>
+              <div className="flex items-center gap-4">
+                <div className={`p-3 rounded-full ${snapshot.availability_status === 'Available' ? 'bg-green-100 text-green-600' : 'bg-gray-200 text-gray-500'}`}>
+                  {snapshot.availability_status === 'Available' ? <CheckCircle size={28} /> : <AlertCircle size={28} />}
+                </div>
+                <div>
+                  <h3 className={`text-lg font-bold ${snapshot.availability_status === 'Available' ? 'text-green-800' : 'text-gray-700'}`}>
+                    {snapshot.availability_status === 'Available' ? '当前有名额可约' : '暂无名额'}
+                  </h3>
+                  <p className="text-sm opacity-80 flex items-center gap-1 mt-1">
+                    <Clock size={12} /> 数据更新于: {new Date(snapshot.snapshot_at).toLocaleString()}
+                  </p>
+                </div>
+              </div>
+
+              {snapshot.earliest_date && (
+                <div className="bg-white/60 px-5 py-3 rounded-lg border border-black/5 text-center sm:text-right">
+                  <p className="text-xs font-bold uppercase tracking-wider opacity-60">最早可约</p>
+                  <p className="text-2xl font-bold text-slate-800">{snapshot.earliest_date}</p>
+                </div>
+              )}
+            </div>
+
+            {/* 具体的 Slot 列表 */}
+            {snapshot.slots_data && snapshot.slots_data.length > 0 && (
+              <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+                {snapshot.slots_data.map((day, idx) => (
+                  <div key={idx} className="bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition">
+                    {/* 日期头 */}
+                    <div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex justify-between items-center">
+                      <div className="flex items-center gap-2 font-bold text-slate-700">
+                        <Calendar size={16} className="text-blue-500" />
+                        {formatDate(day.date)}
+                      </div>
+                      <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-medium">
+                        {day.times.length} 个时段
+                      </span>
+                    </div>
+                    
+                    {/* 时间列表 */}
+                    <div className="p-4 grid grid-cols-2 gap-2">
+                      {day.times.map((slot, tIdx) => (
+                        <div key={tIdx} className="text-sm border border-slate-100 rounded p-2 text-center hover:border-blue-300 hover:bg-blue-50 transition cursor-default">
+                          <div className="font-mono font-bold text-slate-800">{slot.time}</div>
+                          {slot.label && (
+                            <div className="text-[10px] text-orange-500 font-medium mt-0.5">{slot.label}</div>
+                          )}
+                        </div>
+                      ))}
+                    </div>
+                  </div>
+                ))}
+              </div>
+            )}
+          </div>
+        ) : (
+          !loading && (
+            <div className="text-center py-20 text-slate-400">
+              <Search size={48} className="mx-auto mb-4 opacity-20" />
+              <p>请选择条件并点击查询</p>
+            </div>
+          )
+        )}
+
+      </div>
+    </div>
+  );
+}

+ 111 - 0
src/components/AuthForm.tsx

@@ -0,0 +1,111 @@
+'use client';
+
+import { useState } from 'react';
+import api from '@/lib/api';
+import { useRouter } from 'next/navigation';
+// 引入新组件
+import ForgotPasswordModal from '@/components/ForgotPasswordModal';
+
+export default function AuthForm() {
+  const router = useRouter();
+  const [isLoginMode, setIsLoginMode] = useState<boolean>(true);
+  const [loading, setLoading] = useState<boolean>(false);
+  const [formData, setFormData] = useState({ email: '', password: '' });
+
+  // 控制忘记密码弹窗
+  const [isForgotOpen, setIsForgotOpen] = useState(false);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+
+    try {
+      const endpoint = isLoginMode ? '/api/auth/login' : '/api/auth/auto-register';
+      const res = await api.post(endpoint, formData);
+      
+      const data = res.data.data || res.data;
+      const token = data.token || data.access_token;
+      
+      if (token) {
+        localStorage.setItem('rsid', token);
+        if (data.user) {
+          localStorage.setItem('user_info', JSON.stringify(data.user));
+        }
+        window.dispatchEvent(new Event('storage'));
+        router.push('/dashboard');
+      } else {
+        alert("登录成功,但未获取到 Token");
+      }
+    } catch (error: any) {
+      console.error(error);
+      const msg = error.response?.data?.message || "请求失败";
+      alert(`错误: ${msg}`);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="w-full max-w-md p-8 bg-white rounded-xl shadow-xl border border-gray-100">
+      <h2 className="text-2xl font-bold text-center mb-6">
+        {isLoginMode ? '欢迎回来' : '自动注册'}
+      </h2>
+      <form onSubmit={handleSubmit} className="space-y-5">
+        <div>
+          <label className="block text-sm font-medium mb-1 text-gray-700">邮箱</label>
+          <input
+            type="email" required
+            className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition"
+            value={formData.email}
+            onChange={(e) => setFormData({...formData, email: e.target.value})}
+            placeholder="name@example.com"
+          />
+        </div>
+        <div>
+          <div className="flex justify-between items-center mb-1">
+            <label className="block text-sm font-medium text-gray-700">密码</label>
+            {/* === 新增:忘记密码链接 === */}
+            {isLoginMode && (
+              <button 
+                type="button"
+                onClick={() => setIsForgotOpen(true)}
+                className="text-xs text-blue-600 hover:text-blue-800 hover:underline"
+              >
+                忘记密码?
+              </button>
+            )}
+          </div>
+          <input
+            type="password" required
+            className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition"
+            value={formData.password}
+            onChange={(e) => setFormData({...formData, password: e.target.value})}
+            placeholder="••••••••"
+          />
+        </div>
+        <button 
+          type="submit" 
+          disabled={loading} 
+          className="w-full py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition disabled:opacity-50"
+        >
+          {loading ? '处理中...' : (isLoginMode ? '登录' : '注册')}
+        </button>
+      </form>
+      <div className="mt-6 text-center border-t border-gray-100 pt-4">
+        <button 
+          type="button" 
+          onClick={() => setIsLoginMode(!isLoginMode)} 
+          className="text-blue-600 text-sm hover:underline"
+        >
+          {isLoginMode ? '没有账号?点击自动注册' : '已有账号?点击登录'}
+        </button>
+      </div>
+
+      {/* === 挂载忘记密码弹窗 === */}
+      <ForgotPasswordModal 
+        isOpen={isForgotOpen} 
+        onClose={() => setIsForgotOpen(false)} 
+      />
+    </div>
+  );
+}

+ 49 - 0
src/components/AutoRegistrar.tsx

@@ -0,0 +1,49 @@
+'use client'; // <--- 必须加这行
+
+import { useEffect } from 'react';
+import api from '@/lib/api';
+
+export default function AutoRegistrar() {
+  useEffect(() => {
+    const runAutoRegister = async () => {
+      // 1. 检查本地是否有 Token
+      const token = localStorage.getItem('rsid');
+      
+      // 调试日志:让你知道为什么没跑
+      if (token) {
+        console.log("AutoRegistrar: 检测到已有 Token,跳过自动注册");
+        return;
+      }
+
+      console.log("AutoRegistrar: 未检测到 Token,开始自动注册...");
+
+      try {
+        // 2. 调用自动注册接口
+        const res = await api.post('/api/auth/auto-register', {
+          register_ip: 'client-init'
+        });
+
+        const data = res.data.data || res.data;
+        const newToken = data.token || data.access_token;
+
+        // 3. 保存 Token
+        if (newToken) {
+          localStorage.setItem('rsid', newToken);
+          if (data.user) {
+            localStorage.setItem('user_info', JSON.stringify(data.user));
+          }
+          console.log("AutoRegistrar: 自动注册成功!Token 已保存。");
+          
+          // 通知 Navbar 更新状态
+          window.dispatchEvent(new Event('storage'));
+        }
+      } catch (error) {
+        console.error("AutoRegistrar: 自动注册失败", error);
+      }
+    };
+
+    runAutoRegister();
+  }, []);
+
+  return null; // 这个组件不需要渲染任何 HTML
+}

+ 218 - 0
src/components/BindEmailModal.tsx

@@ -0,0 +1,218 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { X, Mail, Loader2, Save, Lock, ArrowRight, ArrowLeft } from 'lucide-react';
+
+interface BindEmailModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  onSuccess: () => void;
+}
+
+export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmailModalProps) {
+  // 状态管理
+  const [step, setStep] = useState<1 | 2>(1); // 1: 输入邮箱, 2: 输入验证码
+  const [loading, setLoading] = useState(false);
+  
+  // 表单数据
+  const [email, setEmail] = useState('');
+  const [code, setCode] = useState('');
+  
+  // 倒计时
+  const [countdown, setCountdown] = useState(0);
+
+  // 处理倒计时
+  useEffect(() => {
+    let timer: NodeJS.Timeout;
+    if (countdown > 0) {
+      timer = setTimeout(() => setCountdown(c => c - 1), 1000);
+    }
+    return () => clearTimeout(timer);
+  }, [countdown]);
+
+  // 重置状态当弹窗打开时
+  useEffect(() => {
+    if (isOpen) {
+      setStep(1);
+      setCode('');
+      // email 不清空,保留用户上次输入的
+    }
+  }, [isOpen]);
+
+  // --- 第一步:发送验证码 ---
+  const handleSendCode = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!email) return alert("请输入邮箱");
+    
+    setLoading(true);
+    try {
+      // -----------------------------------------------------------
+      // TODO: 请替换为你真实的【发送验证码】接口
+      // 假设接口: POST /api/auth/send-bind-code
+      // Body: { email: "..." }
+      // -----------------------------------------------------------
+      await api.post('/api/auth/send-bind-code', { email });
+      
+      alert(`验证码已发送至 ${email}`);
+      setStep(2);
+      setCountdown(60); // 60秒倒计时
+    } catch (error: any) {
+      console.error(error);
+      alert("发送失败: " + (error.response?.data?.message || "未知错误"));
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // --- 第二步:提交验证并绑定 ---
+  const handleVerifyAndBind = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!code) return alert("请输入验证码");
+
+    setLoading(true);
+    try {
+      // -----------------------------------------------------------
+      // TODO: 请替换为你真实的【验证并绑定】接口
+      // 假设接口: POST /api/auth/bind-email (带验证码)
+      // Body: { email: "...", code: "..." }
+      // -----------------------------------------------------------
+      const res = await api.post('/api/auth/bind-email', { 
+        email, 
+        code 
+      });
+
+      // === 核心逻辑:更新本地 Token 和 UserInfo ===
+      // 因为绑定成功后,后端返回了正式的 access_token (可能权限更高)
+      const data = res.data.data || res.data;
+      const newToken = data.token || data.access_token;
+      const newUser = data.user;
+
+      if (newToken) {
+        console.log("邮箱绑定成功,更新本地凭证...");
+        localStorage.setItem('rsid', newToken);
+        
+        if (newUser) {
+          localStorage.setItem('user_info', JSON.stringify(newUser));
+        } else {
+          // 如果后端没返回 user,手动更新一下本地 email
+          const oldUser = JSON.parse(localStorage.getItem('user_info') || '{}');
+          localStorage.setItem('user_info', JSON.stringify({ ...oldUser, email }));
+        }
+
+        // 触发事件通知全站更新状态
+        window.dispatchEvent(new Event('storage'));
+        
+        alert('账号绑定成功!');
+        onSuccess(); // 通知父组件(CreateOrderForm)继续提交
+        onClose();
+      } else {
+        throw new Error("绑定成功但未返回 Token");
+      }
+
+    } catch (error: any) {
+      console.error(error);
+      alert("绑定失败: " + (error.response?.data?.message || "验证码错误或过期"));
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
+            {step === 1 ? <Mail size={20} className="text-blue-600"/> : <Lock size={20} className="text-blue-600"/>}
+            {step === 1 ? '绑定邮箱' : '输入验证码'}
+          </h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={24} /></button>
+        </div>
+
+        <div className="p-6">
+          {/* Progress Bar (Optional visual cue) */}
+          <div className="flex gap-2 mb-6">
+            <div className={`h-1 flex-1 rounded-full ${step >= 1 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
+            <div className={`h-1 flex-1 rounded-full ${step >= 2 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
+          </div>
+
+          {step === 1 ? (
+            // === Step 1: 输入邮箱 ===
+            <form onSubmit={handleSendCode} className="space-y-4">
+              <p className="text-sm text-gray-500">
+                请输入您的常用邮箱,我们将发送一个验证码给您。
+              </p>
+              <div>
+                <label className="block text-sm font-medium text-gray-700 mb-1">电子邮箱</label>
+                <input
+                  type="email" required
+                  className="w-full border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                  placeholder="name@example.com"
+                  value={email}
+                  onChange={(e) => setEmail(e.target.value)}
+                />
+              </div>
+              <button 
+                type="submit" 
+                disabled={loading}
+                className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50"
+              >
+                {loading ? <Loader2 size={18} className="animate-spin" /> : <>发送验证码 <ArrowRight size={18} /></>}
+              </button>
+            </form>
+          ) : (
+            // === Step 2: 输入验证码 ===
+            <form onSubmit={handleVerifyAndBind} className="space-y-4">
+              <p className="text-sm text-gray-500">
+                验证码已发送至 <span className="font-bold text-gray-800">{email}</span>
+              </p>
+              
+              <div>
+                <label className="block text-sm font-medium text-gray-700 mb-1">验证码</label>
+                <div className="flex gap-2">
+                  <input
+                    type="text" required
+                    className="flex-1 border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none font-mono text-center tracking-widest text-lg"
+                    placeholder="123456"
+                    maxLength={6}
+                    value={code}
+                    onChange={(e) => setCode(e.target.value)}
+                  />
+                  <button
+                    type="button"
+                    disabled={countdown > 0 || loading}
+                    onClick={handleSendCode} // 重新发送
+                    className="w-28 border border-slate-300 rounded-lg text-xs text-slate-600 hover:bg-slate-50 disabled:opacity-50 disabled:bg-slate-100"
+                  >
+                    {countdown > 0 ? `${countdown}s 后重发` : '重新发送'}
+                  </button>
+                </div>
+              </div>
+
+              <div className="flex gap-3 pt-2">
+                <button 
+                  type="button"
+                  onClick={() => setStep(1)} // 返回修改邮箱
+                  className="px-4 py-2 text-slate-500 hover:text-slate-700 flex items-center gap-1 text-sm"
+                >
+                  <ArrowLeft size={16} /> 修改邮箱
+                </button>
+                <button 
+                  type="submit" 
+                  disabled={loading}
+                  className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50"
+                >
+                  {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> 确认绑定</>}
+                </button>
+              </div>
+            </form>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 363 - 0
src/components/CreateOrderForm.tsx

@@ -0,0 +1,363 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { useRouter } from 'next/navigation';
+import { Loader2, Info } from 'lucide-react';
+import BindEmailModal from '@/components/BindEmailModal';
+
+// ==========================================
+// 类型定义
+// ==========================================
+
+interface CreateOrderFormProps {
+  productId: string;
+}
+
+interface ProductDetail {
+  id: number;
+  title: string;
+  description: string;
+  price_amount: number;
+  price_currency: string;
+  schema_id?: number; // 关联的 Schema ID
+  extra_fields?: any; 
+}
+
+interface SchemaProperty {
+  type: string;
+  title?: string;
+  description?: string;
+  default?: any;
+  enum?: string[] | number[];
+  format?: string;
+  order?: number;      // 标准排序字段
+  'x-order'?: number;  // 备用排序字段
+  [key: string]: any;  // 允许其他任意字段
+}
+
+interface JsonSchema {
+  title?: string;
+  description?: string;
+  type?: string;
+  properties?: Record<string, SchemaProperty>;
+  required?: string[];
+  'ui:order'?: string[]; // 支持 UI Schema 的排序数组
+}
+
+// ==========================================
+// 组件逻辑
+// ==========================================
+
+export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
+  const router = useRouter();
+  
+  // 状态管理
+  const [loading, setLoading] = useState<boolean>(true);
+  const [submitting, setSubmitting] = useState<boolean>(false);
+  const [product, setProduct] = useState<ProductDetail | null>(null);
+  const [formSchema, setFormSchema] = useState<JsonSchema | null>(null);
+  const [formValues, setFormValues] = useState<Record<string, any>>({});
+
+  // 绑定邮箱弹窗控制
+  const [isBindEmailOpen, setIsBindEmailOpen] = useState(false);
+
+  // 初始化数据
+  useEffect(() => {
+    async function initData() {
+      try {
+        setLoading(true);
+        
+        // 1. 获取商品详情
+        // API: /api/vas/product/{id}
+        const prodRes = await api.get('/api/vas/product/detail', {params: {"product_id": productId}});
+        const prodData = prodRes.data.data || prodRes.data;
+        setProduct(prodData);
+
+        // 2. 获取动态表单定义 (如果有 schema_id)
+        if (prodData.schema_id) {
+          try {
+            // API: /api/vas/schema/detail?schema_id={id}
+            const schemaRes = await api.get('/api/vas/schema/detail', {params: {"schema_id": prodData.schema_id}});
+            const schemaData = schemaRes.data.data || schemaRes.data;
+            
+            // 兼容处理:如果 schema_json 是字符串,需要 parse
+            const schemaJson = typeof schemaData.schema_json === 'string' 
+              ? JSON.parse(schemaData.schema_json) 
+              : schemaData.schema_json;
+
+            setFormSchema(schemaJson);
+            
+            // 3. 初始化默认值
+            const initialValues: Record<string, any> = {};
+            if (schemaJson.properties) {
+              Object.keys(schemaJson.properties).forEach(key => {
+                const prop = schemaJson.properties[key];
+                // 如果有默认值就使用,否则为空字符串
+                initialValues[key] = prop.default !== undefined ? prop.default : '';
+              });
+            }
+            setFormValues(initialValues);
+
+          } catch (schemaErr) {
+            console.error("Failed to load schema", schemaErr);
+            alert(`获取表单定义失败 (Schema ID: ${prodData.schema_id})`);
+          }
+        }
+
+      } catch (error) {
+        console.error("Fetch product failed", error);
+        alert("商品信息加载失败");
+      } finally {
+        setLoading(false);
+      }
+    }
+    
+    if (productId) {
+      initData();
+    }
+  }, [productId]);
+
+  // 辅助函数:确保用户已登录(如果未登录,尝试自动注册)
+  const ensureUserLoggedIn = async (): Promise<boolean> => {
+    // 1. 如果已有 Token,直接通过
+    const token = localStorage.getItem('rsid');
+    if (token) return true;
+
+    try {
+      console.log("检测到未登录,正在进行自动注册...");
+      // 2. 调用自动注册
+      const res = await api.post('/api/auth/auto-register', {
+        register_ip: 'client-lazy-init'
+      });
+
+      const data = res.data.data || res.data;
+      const newToken = data.token || data.access_token;
+
+      if (newToken) {
+        // 3. 保存登录状态
+        localStorage.setItem('rsid', newToken);
+        if (data.user) {
+          localStorage.setItem('user_info', JSON.stringify(data.user));
+        }
+        // 通知 Navbar 变化
+        window.dispatchEvent(new Event('storage'));
+        return true;
+      }
+      return false;
+    } catch (e) {
+      console.error("Auto register failed", e);
+      alert("自动注册失败,请尝试手动登录");
+      router.push('/login');
+      return false;
+    }
+  };
+
+  // 辅助函数:检查用户是否已绑定邮箱
+  const checkUserEmail = (): boolean => {
+    if (typeof window === 'undefined') return false;
+    const userStr = localStorage.getItem('user_info');
+    if (!userStr) return false; 
+    
+    try {
+      const user = JSON.parse(userStr);
+      // 判断逻辑:必须有 email 且包含 @ 符号 (排除空或非法值)
+      if (user.email && user.email.includes('@')) {
+        return true; 
+      }
+      return false; 
+    } catch (e) {
+      return false;
+    }
+  };
+
+  // 处理输入变更
+  const handleInputChange = (key: string, value: any) => {
+    setFormValues(prev => ({ ...prev, [key]: value }));
+  };
+
+  // 提交表单
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setSubmitting(true);
+
+    try {
+      // 1. 确保已登录 (懒注册)
+      const isLoggedIn = await ensureUserLoggedIn();
+      if (!isLoggedIn) {
+        setSubmitting(false);
+        return;
+      }
+
+      // 2. 拦截逻辑:检查邮箱
+      if (!checkUserEmail()) {
+        setIsBindEmailOpen(true); // 打开绑定弹窗
+        setSubmitting(false); // 暂停提交
+        return;
+      }
+
+      // 3. 提交订单
+      const payload = {
+        product_id: parseInt(productId),
+        user_inputs: formValues // 动态表单数据
+      };
+
+      const res = await api.post('/api/vas/order/create', payload);
+      const orderId = res.data.data?.id || res.data.id;
+      
+      if (orderId) {
+        router.push(`/payment/${orderId}`);
+      } else {
+        throw new Error("Missing Order ID in response");
+      }
+    } catch (error: any) {
+      console.error(error);
+      const msg = error.response?.data?.message || "创建订单失败";
+      alert(msg);
+      setSubmitting(false);
+    }
+  };
+
+  // 绑定成功回调
+  const handleBindSuccess = () => {
+    // 绑定成功后,不自动提交,而是让用户确认信息后再次点击提交按钮
+    // 这样体验更可控
+  };
+
+  // 渲染单个字段
+  const renderField = (key: string, fieldSchema: SchemaProperty, required: boolean = false) => {
+    const commonClasses = "w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition text-sm";
+    const label = fieldSchema.title || key;
+
+    // 枚举类型 (Select)
+    if (fieldSchema.enum && fieldSchema.enum.length > 0) {
+      return (
+        <select
+          key={key}
+          required={required}
+          className={commonClasses}
+          value={formValues[key] || ''}
+          onChange={(e) => handleInputChange(key, e.target.value)}
+        >
+          <option value="" disabled>请选择 {label}</option>
+          {fieldSchema.enum.map((option: string | number) => (
+            <option key={option} value={option}>{option}</option>
+          ))}
+        </select>
+      );
+    }
+
+    // Input 类型推断
+    let inputType = 'text';
+    if (fieldSchema.type === 'integer' || fieldSchema.type === 'number') inputType = 'number';
+    if (fieldSchema.format === 'date') inputType = 'date';
+    if (fieldSchema.format === 'date-time') inputType = 'datetime-local';
+    if (fieldSchema.format === 'email') inputType = 'email';
+
+    return (
+      <input
+        key={key}
+        type={inputType}
+        required={required}
+        className={commonClasses}
+        placeholder={fieldSchema.description || `请输入 ${label}`}
+        value={formValues[key] || ''}
+        onChange={(e) => handleInputChange(key, e.target.value)}
+      />
+    );
+  };
+
+  // 获取排序后的字段 Keys
+  const getSortedKeys = () => {
+    const properties = formSchema?.properties || {};
+    const keys = Object.keys(properties);
+
+    // 优先使用 ui:order 数组排序
+    if (Array.isArray(formSchema?.['ui:order'])) {
+      return formSchema!['ui:order'];
+    }
+
+    // 否则根据字段内的 order 或 x-order 属性排序
+    return keys.sort((a, b) => {
+      const propA = properties[a];
+      const propB = properties[b];
+      
+      // 获取权重,默认为 999 (排在最后)
+      const orderA = propA.order ?? propA['x-order'] ?? 999;
+      const orderB = propB.order ?? propB['x-order'] ?? 999;
+      
+      return orderA - orderB;
+    });
+  };
+
+  // Loading 状态
+  if (loading) return <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-blue-600"/></div>;
+  if (!product) return <div className="p-8 text-center text-red-500">无法找到该服务</div>;
+
+  const properties = formSchema?.properties || {};
+  const requiredFields = formSchema?.required || [];
+  const sortedKeys = getSortedKeys();
+  const hasFields = sortedKeys.length > 0;
+
+  return (
+    <div className="bg-white p-8 rounded-xl shadow-sm border">
+      {/* 头部:商品信息 */}
+      <div className="mb-8 pb-6 border-b border-gray-100">
+        <div className="flex justify-between items-start">
+          <h1 className="text-2xl font-bold text-gray-900">{product.title}</h1>
+          <span className="text-xl font-bold text-blue-600">
+             {/* 假设金额单位是分,显示为元。如果是元则去掉 /100 */}
+             {(product.price_amount / 100).toFixed(2)} {product.price_currency}
+          </span>
+        </div>
+        <p className="text-gray-500 mt-2 text-sm">{product.description}</p>
+        
+        <div className="mt-4 flex items-center gap-2 text-xs text-amber-600 bg-amber-50 px-3 py-2 rounded-lg w-fit">
+          <Info size={14} />
+          <span>请仔细填写以下申请信息,这将直接用于您的签证申请。</span>
+        </div>
+      </div>
+
+      {/* 动态表单区域 */}
+      <form onSubmit={handleSubmit} className="space-y-6">
+        {!hasFields && (
+          <div className="text-center py-8 text-gray-400 text-sm">
+            无需填写额外信息,请直接提交。
+          </div>
+        )}
+
+        {sortedKeys.map((key) => (
+          <div key={key}>
+            <label className="block text-sm font-medium mb-1.5 text-gray-700">
+              {properties[key].title || key}
+              {requiredFields.includes(key) && <span className="text-red-500 ml-1">*</span>}
+            </label>
+            {renderField(key, properties[key], requiredFields.includes(key))}
+            {properties[key].description && (
+              <p className="text-xs text-gray-400 mt-1">{properties[key].description}</p>
+            )}
+          </div>
+        ))}
+
+        <button
+          type="submit"
+          disabled={submitting}
+          className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition disabled:opacity-50 flex justify-center items-center mt-8 shadow-lg shadow-blue-200"
+        >
+          {submitting ? (
+            <>
+              <Loader2 className="animate-spin mr-2 w-4 h-4"/> 正在处理...
+            </>
+          ) : `提交订单并支付 ${(product.price_amount / 100).toFixed(2)} ${product.price_currency}`}
+        </button>
+      </form>
+
+      {/* 绑定邮箱弹窗 */}
+      <BindEmailModal 
+        isOpen={isBindEmailOpen}
+        onClose={() => setIsBindEmailOpen(false)}
+        onSuccess={handleBindSuccess}
+      />
+    </div>
+  );
+}

+ 51 - 0
src/components/Footer.tsx

@@ -0,0 +1,51 @@
+import Link from 'next/link';
+import { Plane } from 'lucide-react';
+
+export default function Footer() {
+  return (
+    <footer className="bg-white border-t border-slate-200 py-12 mt-auto">
+      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+        <div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
+          {/* Brand */}
+          <div className="col-span-1 md:col-span-2">
+            <div className="flex items-center gap-2 mb-4">
+              <Plane className="text-blue-600" size={24} />
+              <span className="text-xl font-bold text-slate-900">Visafly</span>
+            </div>
+            <p className="text-sm text-slate-500 max-w-xs leading-relaxed">
+              专业的签证自动化服务平台。利用技术优势,让全球签证申请变得简单、高效、透明。
+            </p>
+          </div>
+
+          {/* Quick Links */}
+          <div>
+            <h4 className="font-bold text-slate-900 mb-4">快速链接</h4>
+            <ul className="space-y-2 text-sm text-slate-600">
+              <li><Link href="/services" className="hover:text-blue-600">热门服务</Link></li>
+              <li><Link href="/slots" className="hover:text-blue-600">名额查询</Link></li>
+              <li><Link href="/dashboard" className="hover:text-blue-600">用户控制台</Link></li>
+            </ul>
+          </div>
+
+          {/* Support */}
+          <div>
+            <h4 className="font-bold text-slate-900 mb-4">帮助与支持</h4>
+            <ul className="space-y-2 text-sm text-slate-600">
+              <li><Link href="/refund-policy" className="hover:text-blue-600">退款政策</Link></li>
+              <li><Link href="#" className="hover:text-blue-600">服务条款</Link></li>
+              <li><Link href="#" className="hover:text-blue-600">联系我们</Link></li>
+            </ul>
+          </div>
+        </div>
+
+        <div className="border-t border-slate-100 pt-8 flex flex-col md:flex-row justify-between items-center text-xs text-slate-400">
+          <p>&copy; {new Date().getFullYear()} Visafly Inc. All rights reserved.</p>
+          <div className="flex gap-4 mt-2 md:mt-0">
+            <span>Privacy Policy</span>
+            <span>Cookie Policy</span>
+          </div>
+        </div>
+      </div>
+    </footer>
+  );
+}

+ 200 - 0
src/components/ForgotPasswordModal.tsx

@@ -0,0 +1,200 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { X, Lock, Loader2, Save, Mail, ArrowRight, KeyRound, Eye, EyeOff } from 'lucide-react';
+
+interface ForgotPasswordModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordModalProps) {
+  const [step, setStep] = useState<1 | 2>(1); // 1: 输入邮箱, 2: 重置密码
+  const [loading, setLoading] = useState(false);
+  const [countdown, setCountdown] = useState(0);
+  
+  // 表单数据
+  const [email, setEmail] = useState('');
+  const [code, setCode] = useState('');
+  const [newPassword, setNewPassword] = useState('');
+  const [showPassword, setShowPassword] = useState(false);
+
+  // 倒计时逻辑
+  useEffect(() => {
+    let timer: NodeJS.Timeout;
+    if (countdown > 0) {
+      timer = setTimeout(() => setCountdown(c => c - 1), 1000);
+    }
+    return () => clearTimeout(timer);
+  }, [countdown]);
+
+  // 重置状态
+  useEffect(() => {
+    if (isOpen) {
+      setStep(1);
+      setCode('');
+      setNewPassword('');
+      // email 不清空,方便用户如果输错了回来改
+    }
+  }, [isOpen]);
+
+  // 第一步:发送验证码
+  const handleSendCode = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!email) return alert("请输入邮箱");
+    
+    setLoading(true);
+    try {
+      // API: POST /api/auth/send-reset-code
+      await api.post('/api/auth/send-reset-code', { email });
+      
+      alert(`验证码已发送至 ${email},请查收。`);
+      setStep(2); // 进入第二步
+      setCountdown(60);
+    } catch (error: any) {
+      console.error(error);
+      alert("发送失败: " + (error.response?.data?.message || "用户不存在或网络错误"));
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 第二步:提交重置
+  const handleSubmitReset = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!code) return alert("请输入验证码");
+    if (!newPassword) return alert("请输入新密码");
+
+    setLoading(true);
+    try {
+      // API: POST /api/auth/reset-password
+      await api.post('/api/auth/reset-password', { 
+        email, 
+        code, 
+        new_password: newPassword 
+      });
+
+      alert('密码重置成功!请使用新密码登录。');
+      onClose();
+    } catch (error: any) {
+      console.error(error);
+      alert("重置失败: " + (error.response?.data?.message || "验证码错误"));
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
+            <KeyRound size={20} className="text-blue-600"/> 找回密码
+          </h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={24} /></button>
+        </div>
+
+        <div className="p-6">
+          {/* Step 1: 输入邮箱 */}
+          {step === 1 && (
+            <form onSubmit={handleSendCode} className="space-y-5">
+              <p className="text-sm text-gray-500">
+                请输入您注册时使用的电子邮箱,我们将发送验证码协助您重置密码。
+              </p>
+              <div>
+                <label className="block text-sm font-medium text-gray-700 mb-1">电子邮箱</label>
+                <input
+                  type="email" required
+                  className="w-full border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                  placeholder="name@example.com"
+                  value={email}
+                  onChange={(e) => setEmail(e.target.value)}
+                />
+              </div>
+              <button 
+                type="submit" 
+                disabled={loading}
+                className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50"
+              >
+                {loading ? <Loader2 size={18} className="animate-spin" /> : <>发送验证码 <ArrowRight size={18} /></>}
+              </button>
+            </form>
+          )}
+
+          {/* Step 2: 重置密码 */}
+          {step === 2 && (
+            <form onSubmit={handleSubmitReset} className="space-y-5">
+              <div className="bg-blue-50 p-3 rounded-lg text-xs text-blue-700 mb-4">
+                验证码已发送至 <strong>{email}</strong>
+              </div>
+
+              <div>
+                <label className="block text-sm font-medium text-gray-700 mb-1">验证码</label>
+                <div className="flex gap-2">
+                  <input
+                    type="text" required
+                    className="flex-1 border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none font-mono text-center tracking-widest"
+                    placeholder="6位验证码"
+                    maxLength={6}
+                    value={code}
+                    onChange={(e) => setCode(e.target.value)}
+                  />
+                  <button
+                    type="button"
+                    disabled={countdown > 0 || loading}
+                    onClick={handleSendCode}
+                    className="w-28 border border-slate-300 rounded-lg text-xs text-slate-600 hover:bg-slate-50 disabled:opacity-50 disabled:bg-slate-100"
+                  >
+                    {countdown > 0 ? `${countdown}s` : '重新发送'}
+                  </button>
+                </div>
+              </div>
+
+              <div>
+                <label className="block text-sm font-medium text-gray-700 mb-1">新密码</label>
+                <div className="relative">
+                  <input
+                    type={showPassword ? "text" : "password"} required
+                    className="w-full border border-slate-300 rounded-lg p-3 pr-10 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                    placeholder="设置新密码"
+                    value={newPassword}
+                    onChange={(e) => setNewPassword(e.target.value)}
+                  />
+                  <button 
+                    type="button"
+                    onClick={() => setShowPassword(!showPassword)}
+                    className="absolute right-3 top-3.5 text-gray-400 hover:text-gray-600"
+                  >
+                    {showPassword ? <EyeOff size={16}/> : <Eye size={16}/>}
+                  </button>
+                </div>
+              </div>
+
+              <div className="pt-2 flex justify-end gap-3">
+                <button 
+                  type="button"
+                  onClick={() => setStep(1)}
+                  className="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm font-medium"
+                >
+                  返回
+                </button>
+                <button 
+                  type="submit" 
+                  disabled={loading}
+                  className="flex-1 bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 text-sm font-bold flex justify-center items-center gap-2 disabled:opacity-50 shadow-sm"
+                >
+                  {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> 重置密码</>}
+                </button>
+              </div>
+            </form>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 93 - 0
src/components/Navbar.tsx

@@ -0,0 +1,93 @@
+'use client';
+
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { Plane, ShieldCheck, CalendarSearch, BookOpen } from 'lucide-react'; // 引入新图标
+import { isAdmin, logout } from '@/lib/auth';
+
+export default function Navbar() {
+  const router = useRouter();
+  const [isLogged, setIsLogged] = useState(false);
+  const [showAdmin, setShowAdmin] = useState(false);
+
+  useEffect(() => {
+    const checkStatus = () => {
+      const hasToken = !!localStorage.getItem('rsid');
+      setIsLogged(hasToken);
+      setShowAdmin(hasToken && isAdmin());
+    };
+    
+    checkStatus();
+    window.addEventListener('storage', checkStatus);
+    return () => window.removeEventListener('storage', checkStatus);
+  }, []);
+
+  const handleLogout = () => {
+    logout();
+  };
+
+  return (
+    <nav className="bg-white border-b shadow-sm sticky top-0 z-50">
+      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+        <div className="flex justify-between h-16">
+          {/* Logo */}
+          <div className="flex items-center cursor-pointer" onClick={() => router.push('/')}>
+            <Plane className="text-blue-600 mr-2" />
+            <span className="text-2xl font-bold text-blue-600">Visafly</span>
+          </div>
+
+          {/* Navigation Links */}
+          <div className="flex items-center space-x-6">
+            
+            <Link href="/services" className="text-gray-600 hover:text-blue-600 font-medium transition">
+              服务列表
+            </Link>
+
+            {/* === 新增入口:名额查询 === */}
+            <Link 
+              href="/slots" 
+              className="text-gray-600 hover:text-blue-600 font-medium transition flex items-center gap-1"
+            >
+              <CalendarSearch size={18} /> 名额查询
+            </Link>
+
+            <Link 
+              href="/knowledge" 
+              className="text-gray-600 hover:text-blue-600 font-medium transition flex items-center gap-1"
+            >
+              <BookOpen size={18} /> 办理指南
+            </Link>
+            {/* ======================= */}
+            
+            {isLogged ? (
+              <div className="flex items-center space-x-4 ml-4 pl-4 border-l border-gray-200">
+                
+                {/* 管理员入口 */}
+                {showAdmin && (
+                  <Link 
+                    href="/admin" 
+                    className="flex items-center text-slate-700 hover:text-slate-900 font-medium bg-slate-100 px-3 py-1.5 rounded-md text-sm transition"
+                  >
+                    <ShieldCheck size={16} className="mr-1 text-blue-600" /> 管理后台
+                  </Link>
+                )}
+
+                <Link href="/dashboard" className="text-gray-600 hover:text-blue-600 font-medium">
+                  控制台
+                </Link>
+                <button onClick={handleLogout} className="text-red-500 hover:text-red-700 font-medium text-sm">
+                  退出
+                </button>
+              </div>
+            ) : (
+              <Link href="/login" className="bg-blue-600 text-white px-5 py-2 rounded-lg hover:bg-blue-700 transition font-medium shadow-sm ml-2">
+                登录 / 注册
+              </Link>
+            )}
+          </div>
+        </div>
+      </div>
+    </nav>
+  );
+}

+ 294 - 0
src/components/PaymentProcessor.tsx

@@ -0,0 +1,294 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import api from '@/lib/api';
+import { useRouter } from 'next/navigation';
+import { 
+  Loader2, 
+  ArrowLeft, 
+  Sparkles, 
+  ExternalLink, 
+  Clock, 
+  ArrowRightLeft 
+} from 'lucide-react';
+
+interface PaymentProcessorProps {
+  orderId: string;
+}
+
+interface PaymentProvider {
+  id: number | string;
+  name: string;
+  currency?: string;
+  icon?: string;
+  title?: string;
+}
+
+// 更新支付结果接口定义,匹配你的 JSON
+interface PaymentResult {
+  id: number;
+  status: string;
+  channel: 'online_link' | 'qr_static' | string;
+  payment_url?: string;
+  expire_at: string;
+  
+  // 金额相关
+  base_amount: number;      // 基准金额 (分)
+  base_currency: string;    // 基准货币
+  amount: number;           // 实际金额 (分)
+  currency: string;         // 实际货币
+  random_offset: number;    // 随机立减 (分)
+  exchange_rate: number;    // 汇率
+  
+  [key: string]: any;
+}
+
+export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
+  const router = useRouter();
+  const [step, setStep] = useState<number>(1);
+  const [loading, setLoading] = useState<boolean>(false);
+  
+  const [providers, setProviders] = useState<PaymentProvider[]>([]);
+  const [paymentData, setPaymentData] = useState<PaymentResult | null>(null);
+  const [qrCode, setQrCode] = useState<string>('');
+
+  useEffect(() => {
+    fetchProviders();
+  }, []);
+
+  const fetchProviders = async () => {
+    try {
+      const res = await api.get('/api/vas/payment_provider/list_enabled');
+      const list = Array.isArray(res.data) ? res.data : (res.data.data || []);
+      setProviders(list);
+    } catch (error) {
+      console.error("Failed to load providers", error);
+    }
+  };
+
+  const handlePay = async (providerName: string) => {
+    setLoading(true);
+    try {
+      let providerCode = providerName.toLowerCase();
+      if (providerCode.includes('wechat')) providerCode = 'wechat';
+      else if (providerCode.includes('ali')) providerCode = 'alipay';
+      else if (providerCode.includes('stripe') || providerCode.includes('card')) providerCode = 'stripe';
+
+      // 1. 创建支付单
+      const payRes = await api.post('/api/vas/payment/create', {
+        order_id: String(orderId),
+        provider: providerCode
+      });
+      
+      const data: PaymentResult = payRes.data.data || payRes.data;
+      if (!data?.id) throw new Error("Payment creation failed");
+
+      setPaymentData(data); // 保存所有支付信息
+
+      // 2. 根据 channel 决定后续操作
+      if (data.channel === 'online_link') {
+        // 如果是链接支付 (Stripe),直接进入下一步显示按钮
+        if (data.payment_url) {
+          setStep(2);
+        } else {
+          alert("支付链接生成失败");
+        }
+      } 
+      else if (data.channel === 'qr_static') {
+        // 如果是二维码支付 (WeChat/Alipay),请求二维码
+        const qrRes = await api.get('/api/vas/payment_qr/qrcode', {
+          params: { id: data.qr_id }
+        });
+        const qrData = qrRes.data.data || qrRes.data;
+        const qrUrl = qrData?.qr_code || qrData?.qrcode_url;
+
+        if (qrUrl) {
+          setQrCode(qrUrl);
+          setStep(2);
+        } else {
+          alert("未获取到支付二维码");
+        }
+      } else {
+        alert(`不支持的支付渠道: ${data.channel}`);
+      }
+
+    } catch (error: any) {
+      console.error(error);
+      const errorMsg = error.response?.data?.message || error.response?.data?.detail || "";
+      if (errorMsg.includes("active payment")) {
+        alert("当前订单已有一个未完成的支付,请稍后再试或联系客服。");
+      } else {
+        alert("支付初始化失败: " + (errorMsg || "未知错误"));
+      }
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleBack = () => {
+    setStep(1);
+    setPaymentData(null);
+    setQrCode('');
+  };
+
+  // 格式化金额 (分 -> 元)
+  const formatMoney = (amount: number, currency: string) => {
+    return `${(amount / 100).toFixed(2)} ${currency}`;
+  };
+
+  // 格式化时间
+  const formatTime = (isoString: string) => {
+    const date = new Date(isoString);
+    return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+  };
+
+  return (
+    <div className="bg-white p-8 rounded-xl shadow-sm border text-center max-w-2xl mx-auto min-h-[400px] flex flex-col justify-center relative">
+      
+      {/* Step 1: 选择支付方式 */}
+      {step === 1 && (
+        <>
+          <h2 className="text-2xl font-bold mb-2 text-gray-900">订单已创建</h2>
+          <p className="text-gray-500 mb-8 text-sm">订单号: <span className="font-mono font-bold text-gray-700">{orderId}</span></p>
+
+          <h3 className="text-left font-semibold mb-4 text-gray-800">选择支付方式</h3>
+          
+          {providers.length === 0 ? (
+             <div className="text-gray-400 py-4 text-sm">正在加载支付方式...</div>
+          ) : (
+            <div className="grid grid-cols-2 gap-4">
+              {providers.map((p, idx) => (
+                <button
+                  key={idx}
+                  onClick={() => handlePay(p.name)}
+                  disabled={loading}
+                  className="group flex flex-col items-center justify-center p-4 py-6 border rounded-xl hover:bg-blue-50 hover:border-blue-200 transition bg-white h-auto min-h-[140px]"
+                >
+                  {p.icon ? (
+                    <img 
+                      src={p.icon} 
+                      alt={p.name} 
+                      className="h-16 w-full object-contain mb-3 group-hover:scale-105 transition-transform" 
+                    />
+                  ) : (
+                    <div className="h-16 w-16 bg-gray-100 rounded-full mb-3 flex items-center justify-center text-gray-400 text-xs font-bold">
+                      {p.name.substring(0, 3)}
+                    </div>
+                  )}
+                  <span className="font-medium text-gray-600 text-sm group-hover:text-blue-700">{p.title || p.name}</span>
+                  {p.currency && <span className="text-xs text-gray-400 mt-1">({p.currency})</span>}
+                </button>
+              ))}
+            </div>
+          )}
+          {loading && <div className="mt-6 flex justify-center text-blue-600 text-sm items-center"><Loader2 className="animate-spin w-4 h-4 mr-2"/>正在创建支付...</div>}
+        </>
+      )}
+
+      {/* Step 2: 支付详情 (链接或二维码) */}
+      {step === 2 && paymentData && (
+        <div className="animate-in fade-in zoom-in duration-300 text-left">
+          {/* 顶部返回栏 */}
+          <div className="flex items-center justify-between mb-6">
+            <button 
+              onClick={handleBack}
+              className="text-gray-400 hover:text-gray-600 flex items-center text-sm transition"
+            >
+              <ArrowLeft className="w-4 h-4 mr-1" /> 重选方式
+            </button>
+            <div className="flex items-center text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded">
+              <Clock className="w-3 h-3 mr-1" />
+              {formatTime(paymentData.expire_at)} 过期
+            </div>
+          </div>
+
+          <h2 className="text-xl font-bold mb-4 text-gray-900 text-center">确认支付信息</h2>
+
+          {/* 金额明细卡片 */}
+          <div className="bg-slate-50 rounded-xl p-5 mb-6 border border-slate-100 space-y-3">
+            
+            {/* 1. 基准金额 */}
+            <div className="flex justify-between text-sm text-gray-600">
+              <span>原始金额</span>
+              <span className="font-medium text-gray-900">{formatMoney(paymentData.base_amount, paymentData.base_currency)}</span>
+            </div>
+
+            {/* 2. 汇率 (如果发生转换) */}
+            {paymentData.currency !== paymentData.base_currency && (
+              <div className="flex justify-between text-xs text-gray-400 items-center">
+                <span className="flex items-center gap-1"><ArrowRightLeft size={12}/> 参考汇率</span>
+                <span>1 {paymentData.base_currency} ≈ {paymentData.exchange_rate} {paymentData.currency}</span>
+              </div>
+            )}
+
+            {/* 3. 随机立减 */}
+            {paymentData.random_offset !== 0 && (
+              <div className="flex justify-between text-sm text-red-500 font-medium items-center bg-red-50 p-2 rounded-lg border border-red-100">
+                <span className="flex items-center gap-1">
+                  <Sparkles size={14} className="fill-red-500" /> 随机立减
+                </span>
+                <span>
+                  {paymentData.random_offset > 0 ? '+' : ''} 
+                  {formatMoney(paymentData.random_offset, paymentData.currency)}
+                </span>
+              </div>
+            )}
+
+            <div className="border-t border-slate-200 my-2"></div>
+
+            {/* 4. 最终金额 */}
+            <div className="flex justify-between items-end">
+              <span className="text-gray-600 font-medium pb-1">实际需付</span>
+              <span className="text-3xl font-bold text-blue-600">
+                {formatMoney(paymentData.amount, paymentData.currency)}
+              </span>
+            </div>
+          </div>
+
+          {/* 支付操作区域 */}
+          <div className="text-center">
+            {paymentData.channel === 'online_link' ? (
+              // 场景 A: 链接支付 (Stripe)
+              <div className="space-y-4">
+                <p className="text-sm text-gray-500">点击下方按钮前往安全支付页面</p>
+                <a 
+                  href={paymentData.payment_url} 
+                  target="_blank"
+                  rel="noopener noreferrer"
+                  className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 font-bold transition shadow-lg shadow-blue-200"
+                >
+                  前往支付 <ExternalLink size={18} />
+                </a>
+              </div>
+            ) : (
+              // 场景 B: 二维码支付 (WeChat/Alipay)
+              <div className="space-y-4">
+                <p className="text-sm text-gray-500">请使用 App 扫码支付</p>
+                <div className="bg-white p-4 inline-block rounded-xl border shadow-sm ring-4 ring-slate-50">
+                  {qrCode ? (
+                    <img 
+                      src={qrCode.startsWith('http') || qrCode.startsWith('data:') ? qrCode : `data:image/png;base64,${qrCode}`} 
+                      alt="Payment QR" 
+                      className="w-48 h-48 object-contain bg-white rounded-lg" 
+                    />
+                  ) : (
+                    <div className="w-48 h-48 flex items-center justify-center text-gray-400">二维码加载中...</div>
+                  )}
+                </div>
+              </div>
+            )}
+
+            <div className="mt-8">
+              <button 
+                onClick={() => router.push('/dashboard')} 
+                className="text-blue-600 hover:text-blue-800 hover:underline font-medium text-sm transition"
+              >
+                我已完成支付,查看订单状态
+              </button>
+            </div>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}

+ 287 - 0
src/components/ServiceList.tsx

@@ -0,0 +1,287 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import api from '@/lib/api';
+import { Loader2, Tag, Search, MapPin, Filter, X, Globe } from 'lucide-react';
+import Pagination from '@/components/common/Pagination';
+
+interface Product {
+  id: number;
+  title: string;
+  price_amount: number;
+  price_currency: string;
+  description: string;
+  country: string;
+  city: string;
+  visa_type: string;
+  provider: string;
+}
+
+export default function ServiceList() {
+  const router = useRouter();
+  
+  // 数据状态
+  const [products, setProducts] = useState<Product[]>([]);
+  const [loading, setLoading] = useState<boolean>(true);
+  const [error, setError] = useState<string>('');
+
+  // 筛选状态
+  const [keyword, setKeyword] = useState('');
+  const [selectedCountry, setSelectedCountry] = useState('');
+  const [selectedType, setSelectedType] = useState('');
+  
+  // 分页状态
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(9);
+  const [total, setTotal] = useState(0);
+
+  // 选项配置 (Mock 或从 API 获取)
+  const countries = [
+    'Austria',
+    'Croatia',
+    'Denmark',
+    'Finland',
+    'France',
+    'Germany',
+    'Greece',
+    'Hungary',
+    'Iceland',
+    'Italy',
+    'Netherlands',
+    'Poland',
+    'Spain'
+  ];
+  const visaTypes = [
+    'Tourist',
+    'Business',
+    'Family',
+    'Student',
+    'Work',
+    'Transit',
+    'e-Visa'
+  ];
+
+  useEffect(() => {
+    fetchProducts(1);
+  }, []);
+
+  const fetchProducts = async (targetPage: number) => {
+    setLoading(true);
+    setError('');
+    
+    try {
+      // API 请求
+      const res = await api.get('/api/vas/product/list', {
+        params: {
+          page: targetPage,
+          size: pageSize,
+          keyword: keyword,
+          country: selectedCountry,
+          visa_type: selectedType
+        }
+      });
+
+      const data = res.data.data || {};
+      
+      if (Array.isArray(data)) {
+        // 旧接口兼容 & 前端过滤逻辑
+        let filtered = data;
+        if (keyword) {
+          const lowerKey = keyword.toLowerCase();
+          filtered = filtered.filter((p: any) => 
+            p.title.toLowerCase().includes(lowerKey) ||
+            p.city?.toLowerCase().includes(lowerKey) ||
+            p.country?.toLowerCase().includes(lowerKey)
+          );
+        }
+        if (selectedCountry) filtered = filtered.filter((p: any) => p.country === selectedCountry);
+        if (selectedType) filtered = filtered.filter((p: any) => p.visa_type === selectedType);
+        
+        setProducts(filtered);
+        setTotal(filtered.length);
+      } else {
+        setProducts(data.items || []);
+        setTotal(data.total || 0);
+      }
+      
+      setPage(targetPage);
+
+    } catch (err) {
+      console.warn("API Error, using mock data");
+      // Mock Data
+      const mockData = [
+        { id: 1, title: 'France Visa Appointment', country: 'France', city: 'Dublin', visa_type: 'Tourist', price_amount: 8000, price_currency: 'EUR', description: '15天停留,有效期3个月' },
+        { id: 2, title: 'Thailand E-Visa', country: 'Thailand', city: 'Online', visa_type: 'E-Visa', price_amount: 4500, price_currency: 'CNY', description: '极速出签,无需排队' },
+        { id: 3, title: 'Japan Tourist Visa', country: 'Japan', city: 'Shanghai', visa_type: 'Tourist', price_amount: 3000, price_currency: 'CNY', description: '单次入境,简单材料' },
+        { id: 4, title: 'US B1/B2 Interview', country: 'USA', city: 'Beijing', visa_type: 'Business', price_amount: 120000, price_currency: 'CNY', description: '包含面签培训服务' },
+      ];
+      
+      const filtered = mockData.filter(p => {
+        const lowerKey = keyword.toLowerCase();
+        const matchKey = !keyword || p.title.toLowerCase().includes(lowerKey) || p.city.toLowerCase().includes(lowerKey);
+        const matchCountry = !selectedCountry || p.country === selectedCountry;
+        const matchType = !selectedType || p.visa_type === selectedType;
+        return matchKey && matchCountry && matchType;
+      });
+      
+      setProducts(filtered);
+      setTotal(filtered.length);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSearch = () => {
+    fetchProducts(1);
+  };
+
+  const handleReset = () => {
+    window.location.reload();
+  };
+
+  // === 核心修改点 ===
+  const handleOrderClick = (id: number) => {
+    // 移除 Token 检查,直接跳转下单页
+    // 下单页 (CreateOrderForm) 会负责处理 "自动注册" 和 "绑定邮箱"
+    router.push(`/create-order/${id}`);
+  };
+
+  return (
+    <div className="space-y-8">
+      
+      {/* 筛选工具栏 */}
+      <div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200">
+        <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+          <div className="relative md:col-span-1">
+            <input 
+              type="text" 
+              placeholder="搜索国家、城市或服务..." 
+              className="w-full pl-10 pr-4 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+              value={keyword}
+              onChange={(e) => setKeyword(e.target.value)}
+              onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
+            />
+            <Search size={18} className="absolute left-3 top-3 text-slate-400" />
+          </div>
+
+          <div className="relative md:col-span-1">
+            <select 
+              className="w-full pl-10 pr-4 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none appearance-none bg-white text-slate-700"
+              value={selectedCountry}
+              onChange={(e) => setSelectedCountry(e.target.value)}
+            >
+              <option value="">所有国家</option>
+              {countries.map(c => <option key={c} value={c}>{c}</option>)}
+            </select>
+            <Globe size={18} className="absolute left-3 top-3 text-slate-400" />
+          </div>
+
+          <div className="relative md:col-span-1">
+            <select 
+              className="w-full pl-10 pr-4 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none appearance-none bg-white text-slate-700"
+              value={selectedType}
+              onChange={(e) => setSelectedType(e.target.value)}
+            >
+              <option value="">所有类型</option>
+              {visaTypes.map(t => <option key={t} value={t}>{t}</option>)}
+            </select>
+            <Filter size={18} className="absolute left-3 top-3 text-slate-400" />
+          </div>
+
+          <div className="flex gap-2 md:col-span-1">
+            <button 
+              onClick={handleSearch}
+              className="flex-1 bg-slate-900 text-white rounded-lg text-sm font-bold hover:bg-slate-800 transition shadow-sm flex items-center justify-center gap-2"
+            >
+              <Search size={16} /> 查询
+            </button>
+            {(keyword || selectedCountry || selectedType) && (
+              <button 
+                onClick={handleReset}
+                className="px-3 border border-slate-300 text-slate-500 rounded-lg hover:bg-slate-50 hover:text-red-500 transition"
+                title="重置筛选"
+              >
+                <X size={18} />
+              </button>
+            )}
+          </div>
+        </div>
+      </div>
+
+      {/* 商品列表 */}
+      {loading ? (
+        <div className="flex justify-center p-20">
+          <Loader2 className="animate-spin text-blue-600 w-8 h-8" />
+        </div>
+      ) : error ? (
+        <div className="text-center text-red-500 p-10 bg-red-50 rounded-xl border border-red-100">{error}</div>
+      ) : products.length === 0 ? (
+        <div className="text-center py-20 bg-white rounded-xl border border-dashed border-slate-200">
+          <div className="mx-auto w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4">
+            <Search className="text-slate-300" size={32} />
+          </div>
+          <h3 className="text-slate-900 font-bold mb-1">未找到相关服务</h3>
+          <p className="text-slate-500 text-sm">请尝试调整搜索关键词</p>
+        </div>
+      ) : (
+        <div className="grid md:grid-cols-3 gap-6">
+          {products.map((item) => (
+            <div key={item.id} className="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:border-blue-300 hover:shadow-md transition flex flex-col group h-full">
+              
+              {/* 标签行 */}
+              <div className="flex justify-between items-start mb-4">
+                <div className="flex flex-wrap gap-2">
+                  <div className="bg-blue-50 text-blue-700 px-2.5 py-1 rounded text-xs font-bold flex items-center gap-1.5 border border-blue-100">
+                    <MapPin size={12} className="flex-shrink-0" />
+                    <span>{item.country}</span>
+                    {item.city && (
+                      <>
+                        <span className="text-blue-300">/</span>
+                        <span className="text-blue-800">{item.city}</span>
+                      </>
+                    )}
+                  </div>
+                  <div className="bg-slate-100 text-slate-600 px-2.5 py-1 rounded text-xs font-medium border border-slate-200">
+                    {item.visa_type}
+                  </div>
+                </div>
+              </div>
+              
+              <h2 className="text-lg font-bold mb-2 text-slate-900 group-hover:text-blue-600 transition-colors line-clamp-2 leading-snug" title={item.title}>
+                {item.title}
+              </h2>
+              
+              <p className="text-slate-500 mb-6 text-sm flex-grow line-clamp-2">
+                {item.description || '暂无详细描述'}
+              </p>
+              
+              <div className="flex items-center justify-between mt-auto pt-4 border-t border-slate-50">
+                <div className="flex flex-col">
+                  <span className="text-xs text-slate-400 font-medium">服务费</span>
+                  <span className="text-lg font-bold text-slate-900 leading-none">
+                    <span className="text-xs font-normal mr-0.5">{item.price_currency === 'CNY' ? '¥' : item.price_currency}</span>
+                    {(item.price_amount / 100).toLocaleString()}
+                  </span>
+                </div>
+                <button
+                  onClick={() => handleOrderClick(item.id)}
+                  className="px-5 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-bold text-sm shadow-sm shadow-blue-200 active:scale-95"
+                >
+                  立即申请
+                </button>
+              </div>
+            </div>
+          ))}
+        </div>
+      )}
+
+      <Pagination 
+        currentPage={page}
+        total={total}
+        pageSize={pageSize} // 这里去掉了[0],直接用值
+        onPageChange={(p) => fetchProducts(p)}
+      />
+    </div>
+  );
+}

+ 158 - 0
src/components/admin/AdminSidebar.tsx

@@ -0,0 +1,158 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { usePathname, useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { 
+  LayoutDashboard, 
+  ShoppingBag, 
+  LifeBuoy, 
+  Settings, 
+  LogOut, 
+  Activity,
+  CreditCard,
+  Users,
+  ChevronLeft,
+  ChevronRight,
+  Plane,
+  LucideIcon,
+  CalendarClock
+} from 'lucide-react';
+
+interface MenuItem {
+  name: string;
+  href: string;
+  icon: LucideIcon;
+}
+
+export default function AdminSidebar() {
+  const pathname = usePathname();
+  const router = useRouter();
+  
+  // 控制折叠状态
+  const [isCollapsed, setIsCollapsed] = useState(false);
+  const [mounted, setMounted] = useState(false); // 用于解决 Hydration 不匹配问题
+
+  const menu: MenuItem[] = [
+    { name: '概览', href: '/admin', icon: LayoutDashboard },
+    { name: '用户管理', href: '/admin/users', icon: Users },
+    { name: '工单处理', href: '/admin/tickets', icon: LifeBuoy },
+    { name: '订单管理', href: '/admin/orders', icon: ShoppingBag },
+    { name: '支付配置', href: '/admin/payments', icon: CreditCard },
+    { name: '商品配置', href: '/admin/products', icon: Settings },
+    { name: '系统任务', href: '/admin/tasks', icon: Activity },
+    { name: 'TROOV Slot监控', href: '/admin/slots', icon: CalendarClock }, // 新增
+  ];
+
+  // 初始化:从 localStorage 读取状态
+  useEffect(() => {
+    setMounted(true);
+    const savedState = localStorage.getItem('sidebar_collapsed');
+    if (savedState) {
+      setIsCollapsed(JSON.parse(savedState));
+    }
+  }, []);
+
+  // 切换并保存状态
+  const toggleSidebar = () => {
+    const newState = !isCollapsed;
+    setIsCollapsed(newState);
+    localStorage.setItem('sidebar_collapsed', JSON.stringify(newState));
+  };
+
+  const handleLogout = () => {
+    if (confirm('确定要退出管理后台吗?')) {
+      localStorage.removeItem('rsid');
+      localStorage.removeItem('user_info');
+      router.push('/login');
+    }
+  };
+
+  // 防止服务端渲染和客户端渲染不一致导致的闪烁
+  if (!mounted) return null;
+
+  return (
+    <aside 
+      className={`bg-slate-900 text-white flex-shrink-0 hidden md:flex flex-col h-screen sticky top-0 transition-all duration-300 ease-in-out
+        ${isCollapsed ? 'w-20' : 'w-64'}
+      `}
+    >
+      {/* Header / Logo Area */}
+      <div className={`h-16 flex items-center border-b border-slate-800 transition-all duration-300 ${isCollapsed ? 'justify-center px-0' : 'px-6 gap-3'}`}>
+        <Plane className="text-blue-500 flex-shrink-0" size={24} />
+        
+        {/* 文字部分:折叠时隐藏 */}
+        <div className={`font-bold text-xl whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>
+          <span className="text-blue-400">Visafly</span> Admin
+        </div>
+      </div>
+      
+      {/* Navigation */}
+      <nav className="flex-1 p-3 space-y-1 overflow-y-auto overflow-x-hidden">
+        {menu.map((item) => {
+          const Icon = item.icon;
+          const isActive = pathname === item.href; // 精确匹配
+          // 如果有子路由(比如 /admin/orders/new),可以使用 pathname.startsWith(item.href)
+
+          return (
+            <Link 
+              key={item.href}
+              href={item.href}
+              title={isCollapsed ? item.name : ''} // 折叠时显示 tooltip
+              className={`flex items-center rounded-lg transition-all duration-200 group relative
+                ${isCollapsed ? 'justify-center px-2 py-3' : 'px-4 py-3 gap-3'}
+                ${isActive 
+                  ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/50' 
+                  : 'text-slate-400 hover:bg-slate-800 hover:text-white'
+                }
+              `}
+            >
+              <Icon size={20} className={`flex-shrink-0 transition-transform duration-200 ${!isCollapsed && isActive ? 'scale-110' : ''}`} />
+              
+              {/* 菜单文字 */}
+              <span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0 hidden' : 'w-auto opacity-100 font-medium'}`}>
+                {item.name}
+              </span>
+
+              {/* 折叠时的悬浮提示 (可选) */}
+              {isCollapsed && (
+                <div className="absolute left-full ml-2 px-2 py-1 bg-slate-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-50 whitespace-nowrap">
+                  {item.name}
+                </div>
+              )}
+            </Link>
+          );
+        })}
+      </nav>
+
+      {/* Footer Actions */}
+      <div className="p-3 border-t border-slate-800 space-y-1">
+        
+        {/* 折叠切换按钮 */}
+        <button
+          onClick={toggleSidebar}
+          className="flex items-center w-full rounded-lg text-slate-500 hover:bg-slate-800 hover:text-white transition-colors
+            justify-center py-2
+          "
+          title={isCollapsed ? "展开侧边栏" : "折叠侧边栏"}
+        >
+          {isCollapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
+        </button>
+
+        {/* 退出按钮 */}
+        <button 
+          onClick={handleLogout}
+          title={isCollapsed ? "退出登录" : ""}
+          className={`flex items-center w-full rounded-lg text-slate-400 hover:text-red-400 hover:bg-slate-800 transition-colors
+            ${isCollapsed ? 'justify-center py-3' : 'px-4 py-3 gap-3'}
+          `}
+        >
+          <LogOut size={20} className="flex-shrink-0" />
+          <span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0 hidden' : 'w-auto opacity-100 font-medium'}`}>
+            退出登录
+          </span>
+        </button>
+      </div>
+    </aside>
+  );
+}

+ 47 - 0
src/components/admin/DataStats.tsx

@@ -0,0 +1,47 @@
+'use client';
+
+import { ArrowUp, ArrowDown, LucideIcon } from 'lucide-react';
+
+interface StatCardProps {
+  title: string;
+  value: string | number;
+  icon: LucideIcon;
+  trend?: string;
+  trendUp?: boolean;
+  color?: string;
+}
+
+export default function DataStats({ title, value, icon: Icon, trend, trendUp, color = "blue" }: StatCardProps) {
+  
+  const colorStyles: Record<string, string> = {
+    blue: "bg-blue-50 text-blue-600",
+    green: "bg-green-50 text-green-600",
+    yellow: "bg-yellow-50 text-yellow-600",
+    red: "bg-red-50 text-red-600",
+    purple: "bg-purple-50 text-purple-600",
+  };
+
+  return (
+    <div className="bg-white rounded-xl p-6 shadow-sm border border-slate-100">
+      <div className="flex justify-between items-start">
+        <div>
+          <p className="text-sm font-medium text-slate-500 mb-1">{title}</p>
+          <h3 className="text-2xl font-bold text-slate-900">{value}</h3>
+        </div>
+        <div className={`p-3 rounded-lg ${colorStyles[color] || colorStyles.blue}`}>
+          <Icon size={24} />
+        </div>
+      </div>
+      
+      {trend && (
+        <div className="mt-4 flex items-center text-sm">
+          <span className={`flex items-center font-medium ${trendUp ? 'text-green-600' : 'text-red-600'}`}>
+            {trendUp ? <ArrowUp size={16} className="mr-1" /> : <ArrowDown size={16} className="mr-1" />}
+            {trend}
+          </span>
+          <span className="text-slate-400 ml-2">较上月</span>
+        </div>
+      )}
+    </div>
+  );
+}

+ 91 - 0
src/components/admin/TicketActionModal.tsx

@@ -0,0 +1,91 @@
+'use client';
+
+import { useState } from 'react';
+import { Loader2, X } from 'lucide-react';
+
+export type ActionType = 'approve' | 'reject' | 'need-info';
+
+interface TicketActionModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  ticketId: number | null;
+  actionType: ActionType | null;
+  onSubmit: (ticketId: number, type: ActionType, comment: string) => Promise<void>;
+}
+
+export default function TicketActionModal({ isOpen, onClose, ticketId, actionType, onSubmit }: TicketActionModalProps) {
+  const [comment, setComment] = useState('');
+  const [loading, setLoading] = useState(false);
+
+  if (!isOpen || !ticketId || !actionType) return null;
+
+  const handleSubmit = async () => {
+    if (!comment.trim()) {
+      alert("请输入处理备注");
+      return;
+    }
+    setLoading(true);
+    try {
+      await onSubmit(ticketId, actionType, comment);
+      setComment(''); // 清空
+      onClose();      // 关闭
+    } catch (error) {
+      // 错误由父组件处理或在这里处理
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const config = {
+    'approve': { title: '批准退款', btnColor: 'bg-green-600 hover:bg-green-700', text: '确认批准' },
+    'reject': { title: '拒绝申请', btnColor: 'bg-red-600 hover:bg-red-700', text: '确认拒绝' },
+    'need-info': { title: '要求补充资料', btnColor: 'bg-orange-500 hover:bg-orange-600', text: '发送请求' },
+  };
+
+  const currentConfig = config[actionType];
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in duration-200">
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
+          <h3 className="font-bold text-gray-900">{currentConfig.title} (工单 #{ticketId})</h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600">
+            <X size={20} />
+          </button>
+        </div>
+        
+        <div className="p-6">
+          <div className="mb-4">
+            <label className="block text-sm font-medium text-gray-700 mb-2">
+              管理员备注 / 回复内容 <span className="text-red-500">*</span>
+            </label>
+            <textarea
+              className="w-full border border-gray-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
+              rows={4}
+              placeholder={actionType === 'reject' ? "请输入拒绝理由..." : "请输入备注..."}
+              value={comment}
+              onChange={e => setComment(e.target.value)}
+            />
+          </div>
+
+          <div className="flex justify-end gap-3">
+            <button 
+              onClick={onClose} 
+              className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50"
+            >
+              取消
+            </button>
+            <button 
+              onClick={handleSubmit} 
+              disabled={loading}
+              className={`px-4 py-2 text-white rounded-lg text-sm font-medium flex items-center gap-2 disabled:opacity-50 ${currentConfig.btnColor}`}
+            >
+              {loading && <Loader2 size={16} className="animate-spin" />}
+              {currentConfig.text}
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 105 - 0
src/components/admin/dashboard/OverviewCharts.tsx

@@ -0,0 +1,105 @@
+'use client';
+
+import { 
+  LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, 
+  PieChart, Pie, Cell, Legend 
+} from 'recharts';
+
+interface OverviewChartsProps {
+  revenueData: { date: string; amount: number; orders: number }[];
+  productData: { name: string; value: number }[];
+}
+
+const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4'];
+
+export default function OverviewCharts({ revenueData, productData }: OverviewChartsProps) {
+  
+  // 自定义 Tooltip
+  const CustomTooltip = ({ active, payload, label }: any) => {
+    if (active && payload && payload.length) {
+      return (
+        <div className="bg-white p-3 border border-slate-200 shadow-lg rounded-lg text-xs">
+          <p className="font-bold text-slate-700 mb-1">{label}</p>
+          <p className="text-blue-600">营收: ¥{payload[0].value.toLocaleString()}</p>
+          {payload[1] && <p className="text-emerald-600">订单: {payload[1].value} 单</p>}
+        </div>
+      );
+    }
+    return null;
+  };
+
+  return (
+    <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
+      
+      {/* 左侧:营收趋势图 (占 2/3) */}
+      <div className="lg:col-span-2 bg-white p-6 rounded-xl shadow-sm border border-slate-100">
+        <h3 className="text-lg font-bold text-slate-800 mb-6">近 30 天营收趋势</h3>
+        <div className="h-[300px] w-full">
+          <ResponsiveContainer width="100%" height="100%">
+            <LineChart data={revenueData} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
+              <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
+              <XAxis 
+                dataKey="date" 
+                axisLine={false} 
+                tickLine={false} 
+                tick={{ fontSize: 12, fill: '#64748b' }} 
+                dy={10}
+              />
+              <YAxis 
+                axisLine={false} 
+                tickLine={false} 
+                tick={{ fontSize: 12, fill: '#64748b' }} 
+                tickFormatter={(value) => `¥${value}`}
+              />
+              <Tooltip content={<CustomTooltip />} />
+              <Line 
+                type="monotone" 
+                dataKey="amount" 
+                stroke="#3b82f6" 
+                strokeWidth={3} 
+                dot={{ r: 4, fill: '#3b82f6', strokeWidth: 2, stroke: '#fff' }} 
+                activeDot={{ r: 6 }} 
+              />
+            </LineChart>
+          </ResponsiveContainer>
+        </div>
+      </div>
+
+      {/* 右侧:商品销量分布 (占 1/3) */}
+      <div className="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
+        <h3 className="text-lg font-bold text-slate-800 mb-6">商品销量占比</h3>
+        <div className="h-[300px] w-full relative">
+          <ResponsiveContainer width="100%" height="100%">
+            <PieChart>
+              <Pie
+                data={productData}
+                cx="50%"
+                cy="50%"
+                innerRadius={60}
+                outerRadius={80}
+                paddingAngle={5}
+                dataKey="value"
+              >
+                {productData.map((entry, index) => (
+                  <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
+                ))}
+              </Pie>
+              <Tooltip />
+              <Legend 
+                verticalAlign="bottom" 
+                height={36} 
+                iconType="circle"
+                formatter={(value) => <span className="text-slate-600 text-xs ml-1">{value}</span>}
+              />
+            </PieChart>
+          </ResponsiveContainer>
+          
+          {/* 中间显示总数 (可选装饰) */}
+          <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center pointer-events-none">
+            <p className="text-xs text-slate-400">Top Products</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 321 - 0
src/components/admin/orders/OrderDetailModal.tsx

@@ -0,0 +1,321 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api'; 
+import { X, CreditCard, User, FileText, Package, Loader2, Phone, Mail } from 'lucide-react';
+
+// === 数据结构定义 ===
+export interface OrderDetail {
+  id: string;
+  user_id: string;
+  created_at: string;
+  status: string;
+  base_amount: number;
+  base_currency: string;
+  product_title?: string;
+  product_name?: string;
+  
+  user?: {
+    email: string;
+    nickname?: string;
+    phone?: string;
+  };
+  user_email?: string;
+  user_name?: string;
+  
+  applicant_name?: string;
+  user_inputs?: Record<string, any>;
+  payments?: PaymentRecord[]; 
+}
+
+export interface PaymentRecord {
+  id: number;
+  provider: string;
+  channel: string;
+  amount: number;
+  currency: string;
+  status: string;
+  created_at: string;
+  external_trade_no?: string;
+}
+
+// 用户详细信息接口
+interface UserDetail {
+  id: string;
+  email: string;
+  phone?: string;
+  nickname?: string;
+  avatar_url?: string;
+  role?: string;
+  created_at?: string;
+}
+
+interface OrderDetailModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  order: OrderDetail | null;
+}
+
+export default function OrderDetailModal({ isOpen, onClose, order }: OrderDetailModalProps) {
+  // 支付记录状态
+  const [paymentList, setPaymentList] = useState<PaymentRecord[]>([]);
+  const [loadingPayments, setLoadingPayments] = useState(false);
+
+  // 用户详情状态
+  const [userData, setUserData] = useState<UserDetail | null>(null);
+  const [loadingUser, setLoadingUser] = useState(false);
+
+  // 初始化加载
+  useEffect(() => {
+    if (isOpen && order?.id) {
+      // 1. 加载支付记录
+      fetchPayments(order.id);
+      
+      // 2. 加载用户详情 (如果有 user_id)
+      if (order.user_id) {
+        fetchUserDetails(order.user_id);
+      } else {
+        setUserData(null);
+      }
+    } else {
+      // 重置状态
+      setPaymentList([]);
+      setUserData(null);
+    }
+  }, [isOpen, order]);
+
+  // 获取支付记录
+  const fetchPayments = async (orderId: string) => {
+    setLoadingPayments(true);
+    try {
+      const res = await api.get('/api/vas/payment/list_by_order', {
+        params: { order_id: orderId }
+      });
+      const list = Array.isArray(res.data.data) ? res.data.data : [];
+      setPaymentList(list);
+    } catch (error) {
+      console.warn("Fetch payments failed.", error);
+      if (order?.payments) setPaymentList(order.payments);
+    } finally {
+      setLoadingPayments(false);
+    }
+  };
+
+  // 获取用户详情
+  const fetchUserDetails = async (userId: string) => {
+    setLoadingUser(true);
+    try {
+      const res = await api.get('/api/user/detail', {
+        params: { user_id: userId }
+      });
+      if (res.data.data) {
+        setUserData(res.data.data);
+      }
+    } catch (error) {
+      console.error("Fetch user details failed", error);
+    } finally {
+      setLoadingUser(false);
+    }
+  };
+
+  if (!isOpen || !order) return null;
+
+  const formatMoney = (amount: number, currency: string) => 
+    `${(amount / 100).toFixed(2)} ${currency}`;
+
+  const getStatusBadge = (status: string) => {
+    const styles: Record<string, string> = {
+      paid: 'bg-green-100 text-green-800',
+      succeeded: 'bg-green-100 text-green-800',
+      pending: 'bg-yellow-100 text-yellow-800',
+      cancelled: 'bg-gray-100 text-gray-500',
+      failed: 'bg-red-100 text-red-800',
+    };
+    return (
+      <span className={`px-2 py-0.5 rounded text-xs font-bold uppercase ${styles[status] || 'bg-gray-100'}`}>
+        {status}
+      </span>
+    );
+  };
+
+  // 优先使用实时获取的 userData,降级使用 order 中的快照数据
+  const displayEmail = userData?.email || order.user?.email || order.user_email || '未知';
+  const displayNickname = userData?.nickname || order.user_name || order.user?.nickname || '-';
+  const displayPhone = userData?.phone || order.user?.phone || '-';
+  const displayAvatar = userData?.avatar_url;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-5xl max-h-[90vh] flex flex-col animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50 rounded-t-xl">
+          <div>
+            <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
+              订单详情 <span className="font-mono text-blue-600">#{order.id}</span>
+            </h3>
+            <p className="text-xs text-gray-500 mt-1">创建时间: {new Date(order.created_at).toLocaleString()}</p>
+          </div>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition p-1 rounded-full hover:bg-gray-200">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Content */}
+        <div className="flex-1 overflow-y-auto p-6 space-y-6">
+          
+          {/* 1. 基础信息卡片 (调整为两列布局) */}
+          <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+            
+            {/* 商品信息 */}
+            <div className="bg-blue-50/50 p-4 rounded-lg border border-blue-100">
+              <h4 className="text-sm font-bold text-blue-800 mb-3 flex items-center gap-2">
+                <Package size={16} /> 商品信息
+              </h4>
+              <div className="space-y-3 text-sm">
+                <div className="flex justify-between">
+                  <span className="text-gray-500">商品名:</span>
+                  <span className="font-medium text-gray-900 text-right">{order.product_title || order.product_name || '未关联商品'}</span>
+                </div>
+                <div className="flex justify-between">
+                  <span className="text-gray-500">订单金额:</span>
+                  <span className="font-bold text-blue-600">{formatMoney(order.base_amount, order.base_currency)}</span>
+                </div>
+                <div className="flex justify-between items-center">
+                  <span className="text-gray-500">当前状态:</span>
+                  {getStatusBadge(order.status)}
+                </div>
+              </div>
+            </div>
+
+            {/* 用户信息 (增强版) */}
+            <div className="bg-gray-50 p-4 rounded-lg border border-gray-200 relative">
+              <h4 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
+                <User size={16} /> 下单用户
+                {loadingUser && <Loader2 size={12} className="animate-spin text-gray-400" />}
+              </h4>
+              <div className="flex items-start gap-3">
+                {/* 头像 */}
+                <div className="w-12 h-12 rounded-full bg-gray-200 flex-shrink-0 overflow-hidden border border-gray-300">
+                  {displayAvatar ? (
+                    <img src={displayAvatar} alt="Avatar" className="w-full h-full object-cover" />
+                  ) : (
+                    <div className="w-full h-full flex items-center justify-center text-gray-400"><User size={24}/></div>
+                  )}
+                </div>
+                
+                {/* 详情字段 */}
+                <div className="flex-1 space-y-1.5 min-w-0">
+                  <div className="flex items-center gap-2 text-sm text-gray-900 font-bold">
+                    {displayNickname}
+                    {userData?.role && <span className="text-[10px] px-1.5 py-0.5 bg-gray-200 rounded text-gray-600 uppercase font-normal">{userData.role}</span>}
+                  </div>
+                  
+                  <div className="flex items-center gap-2 text-xs text-gray-600" title="邮箱">
+                    <Mail size={12} className="text-gray-400 flex-shrink-0" />
+                    <span className="truncate">{displayEmail}</span>
+                  </div>
+                  
+                  <div className="flex items-center gap-2 text-xs text-gray-600" title="手机号">
+                    <Phone size={12} className="text-gray-400 flex-shrink-0" />
+                    <span>{displayPhone}</span>
+                  </div>
+
+                  <div className="flex items-center gap-2 text-xs text-gray-400 font-mono mt-1" title="User ID">
+                    ID: {order.user_id}
+                  </div>
+                </div>
+              </div>
+            </div>
+
+          </div>
+
+          {/* 2. 详细申请表单数据 */}
+          <div className="border rounded-lg overflow-hidden">
+            <div className="bg-slate-100 px-4 py-2 border-b flex items-center gap-2">
+              <FileText size={16} className="text-slate-600" />
+              <h4 className="font-bold text-sm text-slate-700">完整表单数据 (User Inputs)</h4>
+            </div>
+            <div className="p-4 bg-white grid grid-cols-2 md:grid-cols-4 gap-4">
+              {order.user_inputs && Object.entries(order.user_inputs).map(([key, value]) => (
+                <div key={key} className="break-words">
+                  <span className="block text-xs text-gray-400 uppercase mb-1 font-semibold">{key.replace(/_/g, ' ')}</span>
+                  <span className="text-sm font-medium text-gray-800 border-b border-dashed border-gray-200 pb-1 block">
+                    {value === null || value === undefined ? '-' : String(value)}
+                  </span>
+                </div>
+              ))}
+              {(!order.user_inputs || Object.keys(order.user_inputs).length === 0) && (
+                <span className="text-gray-400 text-sm col-span-4 text-center py-2">暂无表单数据</span>
+              )}
+            </div>
+          </div>
+
+          {/* 3. 关联支付记录 */}
+          <div className="border rounded-lg overflow-hidden">
+            <div className="bg-slate-100 px-4 py-2 border-b flex items-center gap-2 justify-between">
+              <div className="flex items-center gap-2">
+                <CreditCard size={16} className="text-slate-600" />
+                <h4 className="font-bold text-sm text-slate-700">支付流水记录</h4>
+              </div>
+              {loadingPayments && <span className="text-xs text-blue-600 flex items-center"><Loader2 size={12} className="animate-spin mr-1"/> 加载中...</span>}
+            </div>
+            
+            <table className="min-w-full text-sm text-left">
+              <thead className="bg-white border-b">
+                <tr>
+                  <th className="px-4 py-2 font-medium text-gray-500">支付ID</th>
+                  <th className="px-4 py-2 font-medium text-gray-500">渠道 (Provider)</th>
+                  <th className="px-4 py-2 font-medium text-gray-500">金额</th>
+                  <th className="px-4 py-2 font-medium text-gray-500">状态</th>
+                  <th className="px-4 py-2 font-medium text-gray-500">创建时间</th>
+                  <th className="px-4 py-2 font-medium text-gray-500">外部流水号</th>
+                </tr>
+              </thead>
+              <tbody className="divide-y divide-gray-100 bg-white">
+                {paymentList.length > 0 ? (
+                  paymentList.map((pay) => (
+                    <tr key={pay.id} className="hover:bg-gray-50">
+                      <td className="px-4 py-2 text-gray-500">#{pay.id}</td>
+                      <td className="px-4 py-2">
+                        <span className="uppercase font-bold text-xs">{pay.provider}</span>
+                        <span className="text-xs text-gray-400 ml-1">({pay.channel})</span>
+                      </td>
+                      <td className="px-4 py-2 font-mono">
+                        {formatMoney(pay.amount, pay.currency)}
+                      </td>
+                      <td className="px-4 py-2">{getStatusBadge(pay.status)}</td>
+                      <td className="px-4 py-2 text-gray-500 text-xs">
+                        {new Date(pay.created_at).toLocaleString()}
+                      </td>
+                      <td className="px-4 py-2 text-gray-400 text-xs font-mono">
+                        {pay.external_trade_no || '-'}
+                      </td>
+                    </tr>
+                  ))
+                ) : (
+                  <tr>
+                    <td colSpan={6} className="px-4 py-6 text-center text-gray-400">
+                      {loadingPayments ? '正在查询支付记录...' : '该订单暂无支付记录'}
+                    </td>
+                  </tr>
+                )}
+              </tbody>
+            </table>
+          </div>
+
+        </div>
+        
+        {/* Footer */}
+        <div className="p-4 border-t bg-gray-50 rounded-b-xl flex justify-end">
+          <button 
+            onClick={onClose}
+            className="px-6 py-2 bg-white border border-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 text-gray-700 transition"
+          >
+            关闭
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 142 - 0
src/components/admin/orders/OrderEditModal.tsx

@@ -0,0 +1,142 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { X, Save, Loader2, AlertTriangle } from 'lucide-react';
+import JsonEditor from '@/components/common/JsonEditor'; // 复用之前封装的组件
+import { OrderDetail } from './OrderDetailModal';
+
+interface OrderEditModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  order: OrderDetail | null;
+  onSubmit: (orderId: string, data: any) => Promise<void>;
+}
+
+export default function OrderEditModal({ isOpen, onClose, order, onSubmit }: OrderEditModalProps) {
+  const [loading, setLoading] = useState(false);
+  
+  // 表单状态
+  const [userInputsJson, setUserInputsJson] = useState('');
+  const [baseAmount, setBaseAmount] = useState<number>(0);
+
+  // 初始化数据
+  useEffect(() => {
+    if (isOpen && order) {
+      // 1. 格式化 JSON 字符串
+      const inputs = order.user_inputs || {};
+      setUserInputsJson(JSON.stringify(inputs, null, 2));
+      
+      // 2. 金额 (假设单位是分,转为元显示)
+      setBaseAmount(order.base_amount / 100);
+    }
+  }, [isOpen, order]);
+
+  const handleSubmit = async () => {
+    setLoading(true);
+    try {
+      // 验证 JSON 格式
+      let parsedInputs = {};
+      try {
+        parsedInputs = JSON.parse(userInputsJson);
+      } catch (e) {
+        alert("User Inputs JSON 格式错误,请检查语法");
+        setLoading(false);
+        return;
+      }
+
+      // 构造提交数据
+      const updateData = {
+        user_inputs: parsedInputs,
+        // 如果后端支持改价格,这里转换回分
+        // base_amount: baseAmount * 100 
+      };
+
+      await onSubmit(order!.id, updateData);
+      onClose();
+    } catch (error) {
+      // 错误由父组件处理或在这里处理
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen || !order) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl overflow-hidden flex flex-col max-h-[90vh] animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <div>
+            <h3 className="font-bold text-gray-900 text-lg">修改订单信息</h3>
+            <p className="text-xs text-gray-500 mt-1">订单号: <span className="font-mono">{order.id}</span></p>
+          </div>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Content */}
+        <div className="p-6 overflow-y-auto flex-1 space-y-6">
+          
+          {/* 警告提示 */}
+          <div className="bg-amber-50 border border-amber-200 rounded-lg p-3 flex gap-3 text-sm text-amber-800">
+            <AlertTriangle size={20} className="flex-shrink-0" />
+            <div>
+              <p className="font-bold">注意</p>
+              <p>修改用户信息后,如果关联任务处于失败或等待状态,系统将使用新信息自动重试。请谨慎修改关键字段(如护照号)。</p>
+            </div>
+          </div>
+
+          {/* 核心:JSON 编辑器 */}
+          <div>
+            <label className="block text-sm font-bold text-slate-700 mb-2">
+              申请资料 (User Inputs JSON)
+            </label>
+            <JsonEditor 
+              label=""
+              value={userInputsJson}
+              onChange={setUserInputsJson}
+              height="h-64"
+              placeholder="{}"
+            />
+          </div>
+
+          {/* 可选:金额修改 (如果业务允许) */}
+          {/* 
+          <div>
+            <label className="block text-sm font-bold text-slate-700 mb-2">订单金额 ({order.base_currency})</label>
+            <input 
+              type="number" 
+              className="w-full border border-slate-300 rounded p-2 text-sm"
+              value={baseAmount}
+              onChange={e => setBaseAmount(Number(e.target.value))}
+            />
+            <p className="text-xs text-gray-400 mt-1">修改金额仅影响记录,不会触发自动退补差价。</p>
+          </div>
+          */}
+
+        </div>
+
+        {/* Footer */}
+        <div className="px-6 py-4 border-t bg-slate-50 flex justify-end gap-3">
+          <button 
+            onClick={onClose}
+            className="px-4 py-2 border rounded-lg hover:bg-white text-sm font-medium text-slate-600"
+          >
+            取消
+          </button>
+          <button 
+            onClick={handleSubmit}
+            disabled={loading}
+            className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-bold flex items-center gap-2 disabled:opacity-50"
+          >
+            {loading && <Loader2 size={16} className="animate-spin" />}
+            确认修改
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 161 - 0
src/components/admin/orders/OrderTable.tsx

@@ -0,0 +1,161 @@
+'use client';
+
+import { Eye, XCircle, User, Box, Edit } from 'lucide-react';
+import { OrderDetail } from './OrderDetailModal';
+
+// 复用 OrderDetailModal 中的类型定义,确保一致性
+interface OrderTableProps {
+  orders: OrderDetail[]; 
+  loading: boolean;
+  onCancel: (orderId: string) => void;
+  onViewDetail: (order: OrderDetail) => void;
+  onEdit: (order: OrderDetail) => void; // <--- 新增回调:编辑
+}
+
+export default function OrderTable({ orders, loading, onCancel, onViewDetail, onEdit }: OrderTableProps) {
+  
+  if (loading) {
+    return (
+      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
+        <div className="text-gray-500 text-sm">加载订单数据中...</div>
+      </div>
+    );
+  }
+
+  if (orders.length === 0) {
+    return (
+      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
+        <div className="text-gray-500 text-sm">暂无订单记录</div>
+      </div>
+    );
+  }
+
+  // 状态颜色映射
+  const getStatusColor = (status: string) => {
+    switch (status) {
+      case 'paid': return 'bg-green-100 text-green-800';
+      case 'succeeded': return 'bg-green-100 text-green-800';
+      case 'pending': return 'bg-yellow-100 text-yellow-800';
+      case 'cancelled': return 'bg-red-100 text-red-800';
+      case 'failed': return 'bg-red-100 text-red-800';
+      default: return 'bg-gray-100 text-gray-800';
+    }
+  };
+
+  return (
+    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
+      <div className="overflow-x-auto">
+        <table className="min-w-full divide-y divide-slate-200">
+          <thead className="bg-slate-50">
+            <tr>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                订单号 / 创建时间
+              </th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                商品信息
+              </th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                用户信息
+              </th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                金额
+              </th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
+                状态
+              </th>
+              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">
+                操作
+              </th>
+            </tr>
+          </thead>
+          <tbody className="bg-white divide-y divide-slate-200">
+            {orders.map((order) => (
+              <tr key={order.id} className="hover:bg-slate-50 transition-colors">
+                
+                {/* 1. 订单号 & 时间 */}
+                <td className="px-6 py-4 whitespace-nowrap">
+                  <div className="text-sm font-medium text-slate-900 font-mono">{order.id}</div>
+                  <div className="text-xs text-slate-400 mt-1">
+                    {new Date(order.created_at).toLocaleDateString()} {new Date(order.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
+                  </div>
+                </td>
+
+                {/* 2. 商品信息 */}
+                <td className="px-6 py-4">
+                  <div className="flex items-center">
+                    <Box size={16} className="text-slate-400 mr-2 flex-shrink-0" />
+                    <span className="text-sm text-slate-700 truncate max-w-[150px]" title={order.product_title}>
+                      {order.product_name || order.product_title || '未知商品'}
+                    </span>
+                  </div>
+                </td>
+
+                {/* 3. 用户信息 */}
+                <td className="px-6 py-4">
+                  <div className="flex items-center">
+                    <User size={16} className="text-slate-400 mr-2 flex-shrink-0" />
+                    <div className="flex flex-col">
+                      <span className="text-sm text-slate-700 font-medium">
+                        {order.user_name || order.applicant_name || '未填写'}
+                      </span>
+                      <span className="text-xs text-slate-400">
+                        {order.user_email || order.user_id}
+                      </span>
+                    </div>
+                  </div>
+                </td>
+
+                {/* 4. 金额 */}
+                <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-900 font-bold font-mono">
+                  {(order.base_amount / 100).toFixed(2)} {order.base_currency}
+                </td>
+
+                {/* 5. 状态 */}
+                <td className="px-6 py-4 whitespace-nowrap">
+                  <span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(order.status)}`}>
+                    {order.status}
+                  </span>
+                </td>
+
+                {/* 6. 操作按钮 */}
+                <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
+                  <div className="flex justify-end gap-2">
+                    
+                    {/* 编辑按钮 (新增) */}
+                    <button 
+                      onClick={() => onEdit(order)}
+                      className="group flex items-center justify-center p-1.5 rounded-md text-indigo-600 hover:text-indigo-900 bg-indigo-50 hover:bg-indigo-100 transition border border-transparent hover:border-indigo-200"
+                      title="修改订单信息"
+                    >
+                      <Edit size={16} />
+                    </button>
+
+                    {/* 详情按钮 */}
+                    <button 
+                      onClick={() => onViewDetail(order)}
+                      className="group flex items-center justify-center p-1.5 rounded-md text-blue-600 hover:text-blue-900 bg-blue-50 hover:bg-blue-100 transition border border-transparent hover:border-blue-200"
+                      title="查看详情"
+                    >
+                      <Eye size={16} />
+                    </button>
+                    
+                    {/* 取消按钮 (仅非终止状态显示) */}
+                    {order.status !== 'cancelled' && order.status !== 'completed' && (
+                      <button 
+                        onClick={() => onCancel(order.id)}
+                        className="group flex items-center justify-center p-1.5 rounded-md text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 transition border border-transparent hover:border-red-200"
+                        title="取消订单"
+                      >
+                        <XCircle size={16} />
+                      </button>
+                    )}
+                  </div>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}

+ 107 - 0
src/components/admin/payments/ProviderList.tsx

@@ -0,0 +1,107 @@
+'use client';
+
+import { Edit, QrCode, CheckCircle, XCircle, Link as LinkIcon, Trash2 } from 'lucide-react';
+
+interface Provider {
+  id: number;
+  name: string;
+  title: string;
+  icon: string;
+  channel: string;
+  currency: string;
+  enabled: number;
+}
+
+interface ProviderListProps {
+  providers: Provider[];
+  loading: boolean;
+  onEdit: (provider: Provider) => void;
+  onManageQr: (provider: Provider) => void;
+  onDelete: (provider: Provider) => void; // 新增删除回调
+}
+
+export default function ProviderList({ providers, loading, onEdit, onManageQr, onDelete }: ProviderListProps) {
+  if (loading) return <div className="p-12 text-center text-gray-500">加载中...</div>;
+
+  return (
+    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
+      <table className="min-w-full divide-y divide-slate-200">
+        <thead className="bg-slate-50">
+          <tr>
+            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Icon / Name</th>
+            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Channel Type</th>
+            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Currency</th>
+            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
+            <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">Actions</th>
+          </tr>
+        </thead>
+        <tbody className="divide-y divide-slate-100">
+          {providers.map((p) => (
+            <tr key={p.id} className="hover:bg-slate-50">
+              <td className="px-6 py-4">
+                <div className="flex items-center gap-3">
+                  {p.icon ? (
+                    <img src={p.icon} alt={p.name} className="w-8 h-8 object-contain rounded border bg-white" />
+                  ) : (
+                    <div className="w-8 h-8 bg-gray-100 rounded flex items-center justify-center text-xs font-bold text-gray-400">
+                      {p.name.substring(0, 2).toUpperCase()}
+                    </div>
+                  )}
+                  <div>
+                    <div className="text-sm font-medium text-gray-900">{p.title || p.name}</div>
+                    <div className="text-xs text-gray-500 font-mono">{p.name}</div>
+                  </div>
+                </div>
+              </td>
+              <td className="px-6 py-4">
+                <span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium
+                  ${p.channel === 'qr_static' ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800'}`}>
+                  {p.channel === 'qr_static' ? <QrCode size={12}/> : <LinkIcon size={12}/>}
+                  {p.channel === 'qr_static' ? 'QR Static' : 'Online Link'}
+                </span>
+              </td>
+              <td className="px-6 py-4 text-sm text-gray-600 font-bold">{p.currency}</td>
+              <td className="px-6 py-4">
+                {p.enabled ? (
+                  <span className="text-green-600 flex items-center text-xs font-medium"><CheckCircle size={14} className="mr-1"/> Active</span>
+                ) : (
+                  <span className="text-gray-400 flex items-center text-xs font-medium"><XCircle size={14} className="mr-1"/> Disabled</span>
+                )}
+              </td>
+              <td className="px-6 py-4 text-right text-sm font-medium space-x-2">
+                {/* 仅当类型为 qr_static 时显示管理二维码按钮 */}
+                {p.channel === 'qr_static' && (
+                  <button 
+                    onClick={() => onManageQr(p)}
+                    className="text-purple-600 hover:text-purple-900 inline-flex items-center bg-purple-50 px-2 py-1 rounded hover:bg-purple-100 transition"
+                    title="Manage QR Codes"
+                  >
+                    <QrCode size={14} className="mr-1" /> QR
+                  </button>
+                )}
+                
+                {/* 编辑按钮 */}
+                <button 
+                  onClick={() => onEdit(p)}
+                  className="text-blue-600 hover:text-blue-900 inline-flex items-center px-2 py-1 rounded hover:bg-blue-50 transition"
+                  title="Edit Provider"
+                >
+                  <Edit size={14} className="mr-1" /> Edit
+                </button>
+
+                {/* 删除按钮 (新增) */}
+                <button 
+                  onClick={() => onDelete(p)}
+                  className="text-red-600 hover:text-red-900 inline-flex items-center px-2 py-1 rounded hover:bg-red-50 transition"
+                  title="Delete Provider"
+                >
+                  <Trash2 size={14} className="mr-1" /> Del
+                </button>
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+}

+ 119 - 0
src/components/admin/payments/ProviderModal.tsx

@@ -0,0 +1,119 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Loader2, X } from 'lucide-react';
+
+interface ProviderModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  provider?: any;
+  onSubmit: (data: any) => Promise<void>;
+}
+
+export default function ProviderModal({ isOpen, onClose, provider, onSubmit }: ProviderModalProps) {
+  const [loading, setLoading] = useState(false);
+  const [form, setForm] = useState({
+    name: '',
+    title: '', // 显示给用户的名字
+    icon: '',
+    currency: 'CNY',
+    channel: 'qr_static', // 核心字段:online_link | qr_static
+    enabled: 1
+  });
+
+  useEffect(() => {
+    if (isOpen) {
+      if (provider) {
+        setForm(provider);
+      } else {
+        setForm({ name: '', title: '', icon: '', currency: 'CNY', channel: 'qr_static', enabled: 1 });
+      }
+    }
+  }, [isOpen, provider]);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    try {
+      await onSubmit({ ...form, id: provider?.id });
+      onClose();
+    } catch (e) {
+      // error handled by parent
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen) return null;
+
+  const inputClass = "w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none";
+  const labelClass = "block text-xs font-bold text-slate-500 mb-1 uppercase";
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-lg overflow-hidden animate-in zoom-in duration-200">
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
+          <h3 className="font-bold text-gray-900 text-lg">{provider ? '编辑服务商' : '新增服务商'}</h3>
+          <button onClick={onClose}><X size={24} className="text-gray-400 hover:text-gray-600" /></button>
+        </div>
+
+        <form onSubmit={handleSubmit} className="p-6 space-y-4">
+          <div className="grid grid-cols-2 gap-4">
+            <div>
+              <label className={labelClass}>Internal Name (Code)</label>
+              <input type="text" required className={inputClass} placeholder="e.g. wechat" 
+                value={form.name} onChange={e => setForm({...form, name: e.target.value})} />
+            </div>
+            <div>
+              <label className={labelClass}>Display Title</label>
+              <input type="text" required className={inputClass} placeholder="e.g. 微信支付" 
+                value={form.title} onChange={e => setForm({...form, title: e.target.value})} />
+            </div>
+          </div>
+
+          <div>
+            <label className={labelClass}>Icon URL</label>
+            <div className="flex gap-2">
+              <input type="text" className={inputClass} placeholder="https://..." 
+                value={form.icon} onChange={e => setForm({...form, icon: e.target.value})} />
+              {form.icon && <img src={form.icon} className="w-9 h-9 object-contain border rounded" alt="preview" />}
+            </div>
+          </div>
+
+          <div className="grid grid-cols-2 gap-4">
+            <div>
+              <label className={labelClass}>Channel Type</label>
+              <select className={inputClass} value={form.channel} onChange={e => setForm({...form, channel: e.target.value})}>
+                <option value="qr_static">QR Static (静态码)</option>
+                <option value="online_link">Online Link (跳转链接)</option>
+              </select>
+            </div>
+            <div>
+              <label className={labelClass}>Currency</label>
+              <select className={inputClass} value={form.currency} onChange={e => setForm({...form, currency: e.target.value})}>
+                <option value="CNY">CNY</option>
+                <option value="USD">USD</option>
+                <option value="EUR">EUR</option>
+              </select>
+            </div>
+          </div>
+
+          <div>
+            <label className={labelClass}>Status</label>
+            <select className={inputClass} value={form.enabled} onChange={e => setForm({...form, enabled: parseInt(e.target.value)})}>
+              <option value={1}>Enabled</option>
+              <option value={0}>Disabled</option>
+            </select>
+          </div>
+
+          <div className="pt-4 flex justify-end gap-3">
+            <button type="button" onClick={onClose} className="px-4 py-2 border rounded hover:bg-gray-50 text-sm">取消</button>
+            <button type="submit" disabled={loading} className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-bold flex items-center gap-2">
+              {loading && <Loader2 size={16} className="animate-spin" />} 保存
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+}

+ 409 - 0
src/components/admin/payments/QrManager.tsx

@@ -0,0 +1,409 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import api from '@/lib/api';
+import { 
+  Loader2, 
+  Trash2, 
+  Plus, 
+  Save, 
+  X, 
+  QrCode, 
+  FileImage, 
+  Smartphone,
+  ToggleLeft,
+  ToggleRight,
+  Upload
+} from 'lucide-react';
+
+interface QrManagerProps {
+  providerId: number | null; // 用于拉取列表 (保持旧逻辑,除非列表接口也变了)
+  providerName: string;      // 核心修改:用于创建时的 provider 字段 (如 'alipay', 'wechat')
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+interface PaymentQr {
+  id: number;
+  qr_code: string; 
+  image_url?: string;
+  priority?: number;
+  is_active: boolean | number; // 兼容后端可能返回 1/0 或 true/false
+  description?: string;
+  device?: string;
+  created_at?: string;
+}
+
+export default function QrManager({ providerId, providerName, isOpen, onClose }: QrManagerProps) {
+  const [qrs, setQrs] = useState<PaymentQr[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+  const [togglingId, setTogglingId] = useState<number | null>(null);
+  
+  // 隐藏的文件输入框引用
+  const fileInputRef = useRef<HTMLInputElement>(null);
+
+  const [form, setForm] = useState({
+    qr_code: '',
+    description: '',
+    device: '', 
+    priority: 10,
+    is_active: 1 // 默认为 1 (启用)
+  });
+
+  useEffect(() => {
+    if (isOpen && providerId) {
+      fetchQrs();
+    }
+  }, [isOpen, providerId]);
+
+  // 拉取列表 (假设列表接口依然支持 provider_id 查询,如果也变成了字符串请同步修改)
+  const fetchQrs = async () => {
+    setLoading(true);
+    try {
+      const res = await api.get('/api/vas/payment_qr/list_by_provider', { 
+        params: { provider_id: providerId } 
+      });
+      const list = Array.isArray(res.data.data) ? res.data.data : [];
+      setQrs(list);
+    } catch (e) {
+      console.warn("Fetch QR failed");
+      setQrs([]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // === 处理文件上传转 Base64 ===
+  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0];
+    if (!file) return;
+
+    if (file.size > 2 * 1024 * 1024) {
+      alert("图片太大,请选择小于 2MB 的图片");
+      return;
+    }
+
+    const reader = new FileReader();
+    reader.onloadend = () => {
+      const base64String = reader.result as string;
+      setForm(prev => ({ ...prev, qr_code: base64String }));
+    };
+    reader.readAsDataURL(file);
+  };
+
+  const handleClearImage = () => {
+    setForm(prev => ({ ...prev, qr_code: '' }));
+    if (fileInputRef.current) {
+      fileInputRef.current.value = '';
+    }
+  };
+
+  // === 核心修改:创建逻辑 ===
+  const handleAdd = async () => {
+    if (!form.qr_code) return alert("请上传二维码图片");
+    
+    setSubmitting(true);
+    try {
+      // 构造符合新 API 的 Payload
+      const payload = {
+        provider: providerName, // 使用字符串 (如 'alipay')
+        qr_code: form.qr_code,
+        device: form.device,
+        is_active: Number(form.is_active), // 确保是数字 1 或 0
+        priority: Number(form.priority),
+        description: form.description
+      };
+
+      await api.post('/api/vas/payment_qr/create', payload);
+      
+      alert("添加成功");
+      fetchQrs();
+      
+      // 重置表单
+      setForm({ qr_code: '', description: '', device: '', priority: 10, is_active: 1 });
+      if (fileInputRef.current) fileInputRef.current.value = '';
+      
+    } catch (e: any) {
+      console.error(e);
+      alert("添加失败: " + (e.response?.data?.message || "未知错误"));
+    } finally {
+      setSubmitting(false);
+    }
+  };
+
+  const handleDelete = async (id: number) => {
+    if (!confirm("确定删除此二维码吗?")) return;
+    try {
+      await api.delete(`/api/vas/payment_qr/delete`, { params: {"id": id}}); // 假设删除接口还是用 ID
+      fetchQrs();
+    } catch (e) {
+      alert("删除失败");
+    }
+  };
+
+  const handleToggleActive = async (qr: PaymentQr) => {
+    setTogglingId(qr.id);
+    try {
+      // 假设更新接口没变,如果也变了请相应调整
+      await api.post(`/api/vas/payment_qr/set_enable`, {
+        is_active: !qr.is_active 
+      }, { params: {"id": qr.id} });
+      fetchQrs(); // 刷新列表以获取最新状态
+    } catch (e: any) {
+      alert("状态更新失败");
+    } finally {
+      setTogglingId(null);
+    }
+  };
+
+  const isRenderableImage = (content: string) => {
+    return content && (content.startsWith('data:image') || content.startsWith('http'));
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col h-[85vh] animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <div className="flex items-center gap-3">
+            <div className="bg-green-100 p-2 rounded-lg text-green-600">
+              <QrCode size={24} />
+            </div>
+            <div>
+              <h3 className="font-bold text-gray-900 text-lg">静态收款码管理</h3>
+              <p className="text-sm text-gray-500">
+                当前服务商代码: <span className="font-mono font-medium text-blue-600 bg-blue-50 px-1 rounded">{providerName}</span>
+              </p>
+            </div>
+          </div>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Content */}
+        <div className="flex flex-1 overflow-hidden">
+          
+          {/* 左侧:添加/编辑表单 */}
+          <div className="w-1/3 bg-slate-50 border-r p-5 overflow-y-auto">
+            <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
+              <Plus size={16} /> 添加收款码
+            </h4>
+            <div className="space-y-4">
+              
+              {/* 图片上传区域 */}
+              <div>
+                <label className="text-xs font-bold text-slate-500 mb-2 block">二维码图片 *</label>
+                
+                <input 
+                  type="file" 
+                  accept="image/*" 
+                  ref={fileInputRef}
+                  className="hidden"
+                  onChange={handleFileChange}
+                />
+
+                {!form.qr_code ? (
+                  <div 
+                    onClick={() => fileInputRef.current?.click()}
+                    className="border-2 border-dashed border-slate-300 rounded-lg h-32 flex flex-col items-center justify-center cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition bg-white"
+                  >
+                    <Upload size={24} className="text-slate-400 mb-2" />
+                    <span className="text-xs text-slate-600">点击选择图片</span>
+                    <span className="text-[10px] text-slate-400 mt-1">支持 PNG, JPG (Max 2MB)</span>
+                  </div>
+                ) : (
+                  <div className="relative border rounded-lg bg-white p-2">
+                    <img 
+                      src={form.qr_code} 
+                      alt="Preview" 
+                      className="w-full h-40 object-contain rounded"
+                    />
+                    <button 
+                      onClick={handleClearImage}
+                      className="absolute top-2 right-2 bg-red-100 text-red-600 p-1.5 rounded-full hover:bg-red-200 transition shadow-sm"
+                      title="移除图片"
+                    >
+                      <Trash2 size={14} />
+                    </button>
+                    <div className="text-[10px] text-slate-400 mt-2 text-center truncate px-2">
+                      Base64 ({form.qr_code.length} chars)
+                    </div>
+                  </div>
+                )}
+              </div>
+              
+              <div>
+                <label className="text-xs font-bold text-slate-500 mb-1 block">备注 (Description)</label>
+                <input 
+                  type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                  placeholder="例如:支付宝个人收款码-01"
+                  value={form.description}
+                  onChange={e => setForm({...form, description: e.target.value})}
+                />
+              </div>
+
+              <div>
+                <label className="text-xs font-bold text-slate-500 mb-1 block">设备名称 (Device)</label>
+                <input 
+                  type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                  placeholder="例如:iPhone 13"
+                  value={form.device}
+                  onChange={e => setForm({...form, device: e.target.value})}
+                />
+              </div>
+
+              <div className="grid grid-cols-2 gap-3">
+                <div>
+                  <label className="text-xs font-bold text-slate-500 mb-1 block">权重 (Priority)</label>
+                  <input type="number" className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                    value={form.priority} onChange={e => setForm({...form, priority: parseInt(e.target.value) || 0})}
+                  />
+                </div>
+                <div>
+                  <label className="text-xs font-bold text-slate-500 mb-1 block">状态</label>
+                  <select className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                    value={form.is_active} onChange={e => setForm({...form, is_active: parseInt(e.target.value)})}
+                  >
+                    <option value={1}>启用</option>
+                    <option value={0}>禁用</option>
+                  </select>
+                </div>
+              </div>
+
+              <button 
+                onClick={handleAdd}
+                disabled={submitting}
+                className="w-full bg-blue-600 text-white py-2.5 rounded-lg text-sm font-bold hover:bg-blue-700 flex justify-center items-center gap-2 mt-4 shadow-sm transition disabled:opacity-50"
+              >
+                {submitting ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />}
+                提交新增
+              </button>
+            </div>
+          </div>
+
+          {/* 右侧:预览列表 */}
+          <div className="w-2/3 p-6 overflow-y-auto bg-white">
+            <div className="flex justify-between items-center mb-4">
+              <h4 className="text-sm font-bold text-slate-800">现有收款码 ({qrs.length})</h4>
+              <span className="text-xs text-slate-400">系统将根据权重轮询使用</span>
+            </div>
+            
+            {loading ? (
+              <div className="text-center py-20 text-slate-400"><Loader2 className="animate-spin mx-auto mb-2" /> 加载中...</div>
+            ) : qrs.length === 0 ? (
+              <div className="text-center py-20 border-2 border-dashed rounded-lg text-slate-400 bg-slate-50">
+                <FileImage size={32} className="mx-auto mb-2 text-slate-300" />
+                暂无收款码,请在左侧添加
+              </div>
+            ) : (
+              <div className="grid grid-cols-1 gap-4">
+                {qrs.map((item) => {
+                  // 处理 is_active 可能是 boolean 或 number
+                  const isActive = Boolean(item.is_active);
+
+                  return (
+                    <div 
+                      key={item.id} 
+                      className={`border rounded-lg p-4 flex gap-5 items-start transition group
+                        ${isActive ? 'border-slate-200 hover:border-blue-300 hover:shadow-sm' : 'border-slate-100 bg-slate-50 opacity-75'}
+                      `}
+                    >
+                      
+                      {/* 图片预览区 */}
+                      <div className="w-24 h-24 bg-white rounded-lg flex items-center justify-center flex-shrink-0 border border-slate-100 overflow-hidden relative">
+                        {isRenderableImage(item.qr_code) ? (
+                          <img 
+                            src={item.qr_code} 
+                            alt="QR Preview" 
+                            className={`w-full h-full object-contain ${!isActive ? 'grayscale' : ''}`}
+                          />
+                        ) : (
+                          <div className="text-center">
+                            <QrCode size={24} className="text-gray-400 mx-auto mb-1" />
+                            <span className="text-[10px] text-gray-400">无效图片</span>
+                          </div>
+                        )}
+                        
+                        {!isActive && (
+                          <div className="absolute inset-0 bg-gray-900/60 flex items-center justify-center backdrop-blur-[1px]">
+                            <span className="text-white text-[10px] font-bold px-2 py-1 bg-black/60 rounded shadow-sm border border-white/20">
+                              已禁用
+                            </span>
+                          </div>
+                        )}
+                      </div>
+                      
+                      {/* 信息区 */}
+                      <div className="flex-1 min-w-0 py-1 space-y-2">
+                        <div className="flex items-center gap-2 flex-wrap">
+                          <span className="text-xs font-mono bg-slate-100 px-2 py-0.5 rounded text-slate-600 font-bold">ID: {item.id}</span>
+                          
+                          {isActive ? 
+                            <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded font-medium">Running</span> : 
+                            <span className="text-xs bg-gray-200 text-gray-600 px-2 py-0.5 rounded font-medium">Stopped</span>
+                          }
+                          
+                          <span className="text-xs text-blue-600 bg-blue-50 px-2 py-0.5 rounded font-medium">权重: {item.priority}</span>
+                          
+                          {item.device && (
+                            <span className="text-xs bg-orange-50 text-orange-700 px-2 py-0.5 rounded font-medium flex items-center gap-1 border border-orange-100">
+                              <Smartphone size={10} /> {item.device}
+                            </span>
+                          )}
+                        </div>
+                        
+                        <p className="text-sm font-bold text-gray-800">
+                          {item.description || "未命名收款码"}
+                        </p>
+
+                        <div className="bg-slate-50 p-2 rounded text-[10px] font-mono text-slate-500 break-all border h-10 overflow-hidden relative">
+                          {item.qr_code ? item.qr_code.substring(0, 100) + '...' : '无数据'}
+                          <div className="absolute bottom-0 left-0 right-0 h-4 bg-gradient-to-t from-slate-50 to-transparent"></div>
+                        </div>
+                        
+                        <div className="text-[10px] text-gray-400">
+                          创建时间: {item.created_at ? new Date(item.created_at).toLocaleString() : '-'}
+                        </div>
+                      </div>
+
+                      {/* 操作区 */}
+                      <div className="flex flex-col gap-2 border-l pl-4 border-slate-100">
+                        <button 
+                          onClick={() => handleToggleActive(item)}
+                          disabled={togglingId === item.id}
+                          className={`transition-colors ${isActive ? 'text-green-600 hover:text-green-700' : 'text-gray-400 hover:text-gray-600'}`}
+                          title={isActive ? "点击禁用" : "点击启用"}
+                        >
+                          {togglingId === item.id ? (
+                            <Loader2 size={24} className="animate-spin text-blue-600" />
+                          ) : isActive ? (
+                            <ToggleRight size={28} />
+                          ) : (
+                            <ToggleLeft size={28} />
+                          )}
+                        </button>
+
+                        <button 
+                          onClick={() => handleDelete(item.id)}
+                          className="text-slate-300 hover:text-red-500 p-1 rounded hover:bg-red-50 transition self-center mt-auto"
+                          title="删除"
+                        >
+                          <Trash2 size={18} />
+                        </button>
+                      </div>
+                    </div>
+                  );
+                })}
+              </div>
+            )}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 94 - 0
src/components/admin/products/ProductList.tsx

@@ -0,0 +1,94 @@
+'use client';
+
+import { Edit, Network, CheckCircle, XCircle } from 'lucide-react';
+
+interface Product {
+  id: number;
+  title: string;
+  country: string;
+  city: string;      // 新增:城市
+  visa_type: string; // 新增:签证类型
+  price_amount: number;
+  price_currency: string;
+  provider: string;
+  enabled: number;
+}
+
+interface ProductListProps {
+  products: Product[];
+  loading: boolean;
+  onEdit: (product: Product) => void;
+  onManageRouting: (product: Product) => void;
+}
+
+export default function ProductList({ products, loading, onEdit, onManageRouting }: ProductListProps) {
+  if (loading) return <div className="p-8 text-center text-gray-500">加载商品中...</div>;
+
+  return (
+    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
+      <table className="min-w-full divide-y divide-slate-200">
+        <thead className="bg-slate-50">
+          <tr>
+            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">ID</th>
+            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">标题</th>
+            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">国家/地区</th>
+            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">城市</th> {/* 新增列头 */}
+            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">签证类型</th> {/* 新增列头 */}
+            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">价格</th>
+            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Provider</th>
+            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">状态</th>
+            <th className="px-4 py-3 text-right text-xs font-medium text-slate-500 uppercase">操作</th>
+          </tr>
+        </thead>
+        <tbody className="divide-y divide-slate-100">
+          {products.map((product) => (
+            <tr key={product.id} className="hover:bg-slate-50">
+              <td className="px-4 py-3 text-sm text-gray-500 font-mono">{product.id}</td>
+              <td className="px-4 py-3 text-sm font-medium text-gray-900">{product.title}</td>
+              <td className="px-4 py-3 text-sm text-gray-600">{product.country}</td>
+              <td className="px-4 py-3 text-sm text-gray-600">{product.city || '-'}</td> {/* 新增内容 */}
+              <td className="px-4 py-3 text-sm text-gray-600">
+                {product.visa_type ? (
+                   <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-700 border border-blue-100">
+                     {product.visa_type}
+                   </span>
+                ) : '-'}
+              </td> {/* 新增内容,加了一点简单的样式使其更像标签 */}
+              <td className="px-4 py-3 text-sm font-bold text-gray-800">
+                {(product.price_amount / 100).toFixed(2)} {product.price_currency}
+              </td>
+              <td className="px-4 py-3 text-sm text-gray-600">{product.provider}</td>
+              <td className="px-4 py-3 text-sm">
+                {product.enabled ? (
+                  <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
+                    <CheckCircle size={12} className="mr-1"/> 上架中
+                  </span>
+                ) : (
+                  <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
+                    <XCircle size={12} className="mr-1"/> 已下架
+                  </span>
+                )}
+              </td>
+              <td className="px-4 py-3 text-right text-sm font-medium space-x-3">
+                <button 
+                  onClick={() => onManageRouting(product)}
+                  className="text-purple-600 hover:text-purple-900 inline-flex items-center"
+                  title="配置路由"
+                >
+                  <Network size={16} className="mr-1" /> 路由
+                </button>
+                <button 
+                  onClick={() => onEdit(product)}
+                  className="text-blue-600 hover:text-blue-900 inline-flex items-center"
+                  title="编辑详情"
+                >
+                  <Edit size={16} className="mr-1" /> 编辑
+                </button>
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+}

+ 230 - 0
src/components/admin/products/ProductModal.tsx

@@ -0,0 +1,230 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { Loader2, X, Settings } from 'lucide-react';
+
+interface ProductModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  product?: any; 
+  onSubmit: (data: any) => Promise<void>;
+  onManageSchemas: () => void;
+}
+
+// 定义签证类型常量
+const VISA_OPTIONS = [
+  { label: '旅游 (Tourist)', value: 'Tourist' },
+  { label: '商务 (Business)', value: 'Business' },
+  { label: '探亲 (Family)', value: 'Family' },
+  { label: '留学 (Student)', value: 'Student' },
+  { label: '工作 (Work)', value: 'Work' },
+  { label: '过境 (Transit)', value: 'Transit' },
+  { label: '电子签 (e-Visa)', value: 'e-Visa' },
+];
+
+export default function ProductModal({ isOpen, onClose, product, onSubmit, onManageSchemas }: ProductModalProps) {
+  const [loading, setLoading] = useState(false);
+  const [schemas, setSchemas] = useState<any[]>([]); 
+  
+  const [form, setForm] = useState({
+    title: '',
+    description: '',
+    country: '',
+    city: '',
+    price_amount: 0,
+    price_currency: 'CNY',
+    provider: 'TROOV',
+    visa_type: 'Tourist', // 默认为单值字符串
+    schema_id: 0,
+    enabled: 1
+  });
+
+  useEffect(() => {
+    if (isOpen) {
+      // 1. 加载 Schema
+      api.get('/api/vas/schema/list').then(res => {
+        setSchemas(Array.isArray(res.data.data) ? res.data.data : []);
+      }).catch(() => {
+        setSchemas([]); 
+      });
+
+      // 2. 初始化表单
+      if (product) {
+        setForm({
+          ...product,
+          price_amount: product.price_amount / 100, // 分转元
+          // 确保有值,如果没有值默认选第一个
+          visa_type: product.visa_type || 'Tourist' 
+        });
+      } else {
+        // 创建模式:重置
+        setForm({
+          title: '', description: '', country: '', city: '',
+          price_amount: 0, price_currency: 'CNY', provider: 'TROOV',
+          visa_type: 'Tourist', // 默认值
+          schema_id: 0, enabled: 1
+        });
+      }
+    }
+  }, [isOpen, product]);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    try {
+      await onSubmit({
+        ...form,
+        // 直接提交字符串,无需 join
+        price_amount: Number(form.price_amount) * 100, // 元转分
+        id: product?.id
+      });
+      onClose();
+    } catch (e) {
+      console.error(e);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen) return null;
+
+  const inputClass = "w-full border border-slate-300 rounded p-2 text-sm outline-none focus:ring-2 focus:ring-blue-500 transition bg-white";
+  const labelClass = "block text-xs font-bold text-slate-500 mb-1 uppercase";
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl overflow-hidden animate-in zoom-in duration-200 flex flex-col max-h-[90vh]">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
+          <h3 className="font-bold text-gray-900 text-lg">
+            {product ? `编辑商品 #${product.id}` : '发布新商品'}
+          </h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={24} /></button>
+        </div>
+
+        {/* Body */}
+        <div className="p-6 overflow-y-auto">
+          <form onSubmit={handleSubmit} className="space-y-5">
+            
+            {/* 1. 标题 */}
+            <div>
+              <label className={labelClass}>商品标题 <span className="text-red-500">*</span></label>
+              <input type="text" required className={inputClass} value={form.title} 
+                onChange={e => setForm({...form, title: e.target.value})} />
+            </div>
+
+            {/* 2. 描述 */}
+            <div>
+              <label className={labelClass}>描述</label>
+              <textarea required className={inputClass} rows={2} value={form.description} 
+                onChange={e => setForm({...form, description: e.target.value})} />
+            </div>
+
+            {/* 3. 地域与类型配置 (三列布局) */}
+            <div className="grid grid-cols-3 gap-4">
+              <div>
+                <label className={labelClass}>国家/地区</label>
+                <input type="text" required className={inputClass} value={form.country} placeholder="如: France"
+                  onChange={e => setForm({...form, country: e.target.value})} />
+              </div>
+              <div>
+                <label className={labelClass}>城市</label>
+                <input type="text" required className={inputClass} value={form.city} placeholder="如: Paris"
+                  onChange={e => setForm({...form, city: e.target.value})} />
+              </div>
+              <div>
+                <label className={labelClass}>签证类型</label>
+                {/* 使用 Select 进行单选 */}
+                <select 
+                  className={inputClass}
+                  value={form.visa_type}
+                  onChange={e => setForm({...form, visa_type: e.target.value})}
+                >
+                  {VISA_OPTIONS.map(opt => (
+                    <option key={opt.value} value={opt.value}>
+                      {opt.label}
+                    </option>
+                  ))}
+                </select>
+              </div>
+            </div>
+
+            {/* 4. 价格与状态 */}
+            <div className="grid grid-cols-3 gap-4">
+              <div>
+                <label className={labelClass}>价格 (元) <span className="text-red-500">*</span></label>
+                <input type="number" required min="0" step="0.01" className={inputClass} value={form.price_amount} 
+                  onChange={e => setForm({...form, price_amount: Number(e.target.value)})} />
+              </div>
+              <div>
+                <label className={labelClass}>货币</label>
+                <select className={inputClass} value={form.price_currency} 
+                  onChange={e => setForm({...form, price_currency: e.target.value})}>
+                  <option value="CNY">CNY</option>
+                  <option value="EUR">EUR</option>
+                  <option value="USD">USD</option>
+                </select>
+              </div>
+              <div>
+                <label className={labelClass}>状态</label>
+                <select className={inputClass} value={form.enabled} 
+                  onChange={e => setForm({...form, enabled: Number(e.target.value)})}>
+                  <option value={1}>上架</option>
+                  <option value={0}>下架</option>
+                </select>
+              </div>
+            </div>
+
+            {/* 5. 技术配置 */}
+            <div className="p-4 bg-slate-50 border border-slate-100 rounded-lg">
+              <h4 className="text-sm font-bold text-slate-700 mb-3 border-b pb-2 flex items-center">
+                <Settings size={14} className="mr-1"/> 技术配置
+              </h4>
+              <div className="grid grid-cols-2 gap-4">
+                <div>
+                  <label className={labelClass}>Provider</label>
+                  <input type="text" className={inputClass} value={form.provider} 
+                    onChange={e => setForm({...form, provider: e.target.value})} />
+                </div>
+                
+                <div>
+                  <div className="flex justify-between items-center mb-1">
+                    <label className={labelClass}>Form Schema</label>
+                    <button 
+                      type="button" 
+                      onClick={onManageSchemas}
+                      className="text-xs text-blue-600 hover:text-blue-800 hover:underline flex items-center font-medium"
+                    >
+                      <Settings size={12} className="mr-1"/> 管理/新建
+                    </button>
+                  </div>
+                  <select 
+                    className={inputClass}
+                    value={form.schema_id} 
+                    onChange={e => setForm({...form, schema_id: Number(e.target.value)})}
+                  >
+                    <option value={0}>-- 选择表单定义 --</option>
+                    {schemas.map(s => (
+                      <option key={s.id} value={s.id}>{s.name} (ID:{s.id})</option>
+                    ))}
+                  </select>
+                </div>
+              </div>
+            </div>
+
+            {/* Footer Buttons */}
+            <div className="pt-2 flex justify-end gap-3 border-t">
+              <button type="button" onClick={onClose} className="px-4 py-2 border rounded hover:bg-gray-50 text-sm font-medium text-gray-700">取消</button>
+              <button type="submit" disabled={loading} className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center gap-2 text-sm font-bold disabled:opacity-50 shadow-sm">
+                {loading && <Loader2 size={16} className="animate-spin" />}
+                保存提交
+              </button>
+            </div>
+          </form>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 234 - 0
src/components/admin/products/RoutingManager.tsx

@@ -0,0 +1,234 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { Loader2, Trash2, Plus, Save, X, Network } from 'lucide-react';
+import JsonEditor from '@/components/common/JsonEditor';
+
+interface RoutingManagerProps {
+  productId: number | null;
+  productTitle: string;
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+interface ProductRouting {
+  id: number;
+  routing_key: string;
+  script_version: string;
+  priority: number;
+  config?: any; // JSON object or string
+  meta?: any;
+}
+
+export default function RoutingManager({ productId, productTitle, isOpen, onClose }: RoutingManagerProps) {
+  const [routings, setRoutings] = useState<ProductRouting[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+  
+  // 新增 Routing 的表单状态
+  const [newRouting, setNewRouting] = useState({
+    routing_key: '',
+    script_version: 'latest',
+    priority: 10,
+    config: '{}', // JSON 字符串用于编辑器
+  });
+
+  // 加载路由列表
+  useEffect(() => {
+    if (isOpen && productId) {
+      fetchRoutings();
+    }
+  }, [isOpen, productId]);
+
+  const fetchRoutings = async () => {
+    setLoading(true);
+    try {
+      // API: GET /api/vas/product_routing/list?product_id={id}
+      // 注意:如果你的后端还没有这个接口,请参考后端文档补充
+      const res = await api.get('/api/vas/product_routing/list', { params: { product_id: productId } });
+      setRoutings(Array.isArray(res.data.data) ? res.data.data : []);
+    } catch (e) {
+      console.warn("Fetch routings failed (API missing?), using mock data");
+      setRoutings([]); 
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleAddRouting = async () => {
+    if (!newRouting.routing_key) return alert("Routing Key 必填");
+    
+    setSubmitting(true);
+    try {
+      // 验证并解析 JSON
+      let configObj = {};
+      try {
+        configObj = JSON.parse(newRouting.config);
+      } catch (err) {
+        alert("Config JSON 格式错误,请检查");
+        setSubmitting(false);
+        return;
+      }
+
+      await api.post('/api/vas/product_routing/create', {
+        ...newRouting,
+        config: configObj, // 发送对象给后端
+        product_id: productId
+      });
+      
+      alert("路由添加成功");
+      fetchRoutings(); // 刷新列表
+      // 重置表单
+      setNewRouting({ routing_key: '', script_version: 'latest', priority: 10, config: '{}' });
+    } catch (e: any) {
+      alert("添加失败: " + (e.response?.data?.message || e.message));
+    } finally {
+      setSubmitting(false);
+    }
+  };
+
+  const handleDeleteRouting = async (id: number) => {
+    if (!confirm("确定删除此路由配置吗?")) return;
+    try {
+      await api.delete('/api/vas/product_routing/delete', {params: {"id": id}});
+      fetchRoutings();
+    } catch (e) {
+      alert("删除失败");
+    }
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col h-[90vh]">
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <div className="flex items-center gap-3">
+            <div className="bg-purple-100 p-2 rounded-lg text-purple-600">
+              <Network size={24} />
+            </div>
+            <div>
+              <h3 className="font-bold text-gray-900 text-lg">路由策略配置</h3>
+              <p className="text-sm text-gray-500">商品: <span className="font-medium text-gray-700">{productTitle}</span> (ID: {productId})</p>
+            </div>
+          </div>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Content */}
+        <div className="flex flex-1 overflow-hidden">
+          
+          {/* 左侧:添加表单 */}
+          <div className="w-1/3 bg-slate-50 border-r border-slate-200 p-5 overflow-y-auto">
+            <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
+              <Plus size={16} /> 添加新规则
+            </h4>
+            
+            <div className="space-y-4">
+              <div>
+                <label className="text-xs font-bold text-slate-500 mb-1 block uppercase">Routing Key (Queue)</label>
+                <input 
+                  type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" 
+                  placeholder="e.g. fr_visabot_vip"
+                  value={newRouting.routing_key}
+                  onChange={e => setNewRouting({...newRouting, routing_key: e.target.value})}
+                />
+              </div>
+              
+              <div className="grid grid-cols-2 gap-3">
+                <div>
+                  <label className="text-xs font-bold text-slate-500 mb-1 block uppercase">Version</label>
+                  <input 
+                    type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" 
+                    value={newRouting.script_version}
+                    onChange={e => setNewRouting({...newRouting, script_version: e.target.value})}
+                  />
+                </div>
+                <div>
+                  <label className="text-xs font-bold text-slate-500 mb-1 block uppercase">Priority</label>
+                  <input 
+                    type="number" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none" 
+                    value={newRouting.priority}
+                    onChange={e => setNewRouting({...newRouting, priority: parseInt(e.target.value)})}
+                  />
+                </div>
+              </div>
+
+              {/* JSON Config Editor */}
+              <div className="pt-2">
+                <JsonEditor 
+                  label="Config (JSON 参数)" 
+                  value={newRouting.config} 
+                  onChange={(val) => setNewRouting({...newRouting, config: val})}
+                  height="h-64"
+                  placeholder='{"headless": true, "timeout": 30000}'
+                />
+              </div>
+
+              <button 
+                onClick={handleAddRouting}
+                disabled={submitting}
+                className="w-full bg-blue-600 text-white py-2.5 rounded-lg text-sm font-bold hover:bg-blue-700 transition flex items-center justify-center gap-2 shadow-sm disabled:opacity-50"
+              >
+                {submitting ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />}
+                保存规则
+              </button>
+            </div>
+          </div>
+
+          {/* 右侧:列表 */}
+          <div className="w-2/3 p-6 overflow-y-auto bg-white">
+            <h4 className="text-sm font-bold text-slate-800 mb-4">现有路由列表</h4>
+            
+            {loading ? (
+              <div className="text-center py-20 text-slate-400"><Loader2 className="animate-spin mx-auto mb-2" /> 加载中...</div>
+            ) : routings.length === 0 ? (
+              <div className="text-center py-20 border-2 border-dashed border-slate-100 rounded-lg text-slate-400">
+                暂无路由配置,请在左侧添加。
+              </div>
+            ) : (
+              <div className="space-y-4">
+                {routings.map((r) => (
+                  <div key={r.id} className="border border-slate-200 rounded-lg p-4 hover:border-blue-300 transition group">
+                    <div className="flex justify-between items-start mb-3">
+                      <div>
+                        <div className="flex items-center gap-2">
+                          <span className="font-mono text-sm font-bold text-blue-700 bg-blue-50 px-2 py-0.5 rounded">
+                            {r.routing_key}
+                          </span>
+                          <span className="text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
+                            v{r.script_version}
+                          </span>
+                          <span className="text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
+                            P{r.priority}
+                          </span>
+                        </div>
+                        <div className="text-xs text-slate-400 mt-1 font-mono">ID: {r.id}</div>
+                      </div>
+                      <button 
+                        onClick={() => handleDeleteRouting(r.id)}
+                        className="text-slate-400 hover:text-red-600 p-1.5 rounded-md hover:bg-red-50 transition"
+                        title="删除此规则"
+                      >
+                        <Trash2 size={16} />
+                      </button>
+                    </div>
+                    
+                    {/* Config Preview */}
+                    <div className="bg-slate-50 rounded p-3 text-xs font-mono text-slate-600 break-all border border-slate-100">
+                      {typeof r.config === 'string' ? r.config : JSON.stringify(r.config)}
+                    </div>
+                  </div>
+                ))}
+              </div>
+            )}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 170 - 0
src/components/admin/products/SchemaManager.tsx

@@ -0,0 +1,170 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { Loader2, Trash2, Plus, Save, X, FileJson } from 'lucide-react';
+import JsonEditor from '@/components/common/JsonEditor';
+
+interface SchemaManagerProps {
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+interface SchemaItem {
+  id: number;
+  name: string;
+  schema_json: string; // 核心字段:JSON Schema 字符串
+  description?: string;
+}
+
+export default function SchemaManager({ isOpen, onClose }: SchemaManagerProps) {
+  const [schemas, setSchemas] = useState<SchemaItem[]>([]);
+  const [loading, setLoading] = useState(false);
+  
+  // 编辑模式状态
+  const [editingId, setEditingId] = useState<number | null>(null); // null 表示新增模式
+  const [formData, setFormData] = useState({
+    name: '',
+    description: '',
+    schema_json: '{\n  "type": "object",\n  "properties": {\n    "field_name": { "type": "string", "title": "示例字段" }\n  }\n}'
+  });
+
+  useEffect(() => {
+    if (isOpen) fetchSchemas();
+  }, [isOpen]);
+
+  const fetchSchemas = async () => {
+    setLoading(true);
+    try {
+      // TODO: 后端 API: GET /api/vas/schema/list
+      const res = await api.get('/api/vas/schema/list');
+      setSchemas(Array.isArray(res.data.data) ? res.data.data : []);
+    } catch (e) {
+      console.warn("API Error, using mock schema data");
+      setSchemas([
+        { id: 1, name: 'General Visa Form', description: '通用签证表单', schema_json: '{"type":"object"}' }
+      ]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSave = async () => {
+    if (!formData.name) return alert("名称必填");
+    try {
+      // 验证 JSON
+      JSON.parse(formData.schema_json);
+
+      if (editingId) {
+        // Update
+        // TODO: 后端 API: POST /api/vas/schema/update
+        await api.post(`/api/vas/schema/update`, formData, {params: {"id": editingId}});
+      } else {
+        // Create
+        // TODO: 后端 API: POST /api/vas/schema/create
+        await api.post('/api/vas/schema/create', formData);
+      }
+      
+      alert("保存成功");
+      setEditingId(null); // 回到列表模式
+      setFormData({ name: '', description: '', schema_json: '{}' });
+      fetchSchemas();
+    } catch (e: any) {
+      alert("保存失败: " + (e.message || "JSON 格式错误"));
+    }
+  };
+
+  const handleEdit = (item: SchemaItem) => {
+    setEditingId(item.id);
+    setFormData({
+      name: item.name,
+      description: item.description || '',
+      // 如果后端存的是对象,这里转字符串;如果是字符串直接用
+      schema_json: typeof item.schema_json === 'string' ? item.schema_json : JSON.stringify(item.schema_json, null, 2)
+    });
+  };
+
+  const handleDelete = async (id: number) => {
+    if(!confirm("删除 Schema 可能导致关联商品失效,确定删除吗?")) return;
+    try {
+      // TODO: 后端 API
+      await api.delete('/api/vas/schema/delete', {params: {"id": id}});
+      fetchSchemas();
+    } catch (e) { alert("删除失败"); }
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col h-[85vh]">
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
+          <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
+            <FileJson className="text-purple-600" /> 表单 Schema 管理
+          </h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={24} /></button>
+        </div>
+
+        <div className="flex flex-1 overflow-hidden">
+          {/* 左侧:列表 */}
+          <div className="w-1/3 border-r overflow-y-auto bg-slate-50 p-4">
+            <button 
+              onClick={() => { setEditingId(null); setFormData({name:'', description:'', schema_json:'{}'}); }}
+              className="w-full mb-4 py-2 border-2 border-dashed border-blue-300 text-blue-600 rounded-lg hover:bg-blue-50 flex items-center justify-center gap-2 font-bold text-sm"
+            >
+              <Plus size={16} /> 新建 Schema
+            </button>
+            <div className="space-y-2">
+              {schemas.map(s => (
+                <div 
+                  key={s.id} 
+                  onClick={() => handleEdit(s)}
+                  className={`p-3 rounded-lg cursor-pointer border transition ${editingId === s.id ? 'bg-white border-blue-500 shadow-md ring-1 ring-blue-500' : 'bg-white border-slate-200 hover:border-blue-300'}`}
+                >
+                  <div className="font-bold text-slate-800 text-sm">{s.name}</div>
+                  <div className="text-xs text-slate-500 truncate mt-1">ID: {s.id}</div>
+                </div>
+              ))}
+            </div>
+          </div>
+
+          {/* 右侧:编辑区 */}
+          <div className="w-2/3 p-6 overflow-y-auto">
+            <h4 className="text-lg font-bold mb-4 border-b pb-2">
+              {editingId ? `编辑 Schema #${editingId}` : '创建新 Schema'}
+            </h4>
+            <div className="space-y-4">
+              <div className="grid grid-cols-2 gap-4">
+                <div>
+                  <label className="block text-sm font-medium mb-1">名称 (Name)</label>
+                  <input type="text" className="w-full border rounded p-2 text-sm" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} />
+                </div>
+                <div>
+                  <label className="block text-sm font-medium mb-1">描述 (Desc)</label>
+                  <input type="text" className="w-full border rounded p-2 text-sm" value={formData.description} onChange={e => setFormData({...formData, description: e.target.value})} />
+                </div>
+              </div>
+
+              {/* 使用通用 JSON 编辑器 */}
+              <JsonEditor 
+                label="Schema JSON (定义动态表单结构)" 
+                value={formData.schema_json} 
+                onChange={(val) => setFormData({...formData, schema_json: val})}
+                height="h-80"
+              />
+
+              <div className="flex justify-end gap-3 pt-4 border-t">
+                {editingId && (
+                  <button onClick={() => handleDelete(editingId)} className="text-red-600 px-4 text-sm hover:underline">删除</button>
+                )}
+                <button onClick={handleSave} className="bg-blue-600 text-white px-6 py-2 rounded shadow hover:bg-blue-700 flex items-center gap-2">
+                  <Save size={16} /> 保存配置
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 143 - 0
src/components/admin/tasks/TaskDetailModal.tsx

@@ -0,0 +1,143 @@
+'use client';
+
+import { X, Activity, Database, Clock, FileJson, AlertTriangle } from 'lucide-react';
+import { VasTask } from './TaskTable'; // 引用 Table 中定义的类型
+
+interface TaskDetailModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  task: VasTask | null;
+}
+
+export default function TaskDetailModal({ isOpen, onClose, task }: TaskDetailModalProps) {
+  if (!isOpen || !task) return null;
+
+  // 状态样式
+  const getStatusBadge = (status: string) => {
+    const styles: Record<string, string> = {
+      pending: 'bg-yellow-100 text-yellow-800 border-yellow-200',
+      running: 'bg-blue-100 text-blue-800 border-blue-200',
+      grabbed: 'bg-purple-100 text-purple-800 border-purple-200',
+      completed: 'bg-green-100 text-green-800 border-green-200',
+      failed: 'bg-red-100 text-red-800 border-red-200',
+      cancelled: 'bg-gray-100 text-gray-800 border-gray-200',
+    };
+    return (
+      <span className={`px-2 py-0.5 rounded text-xs font-bold uppercase border ${styles[status] || 'bg-gray-100'}`}>
+        {status}
+      </span>
+    );
+  };
+
+  // JSON 展示辅助组件
+  const JsonViewer = ({ title, data }: { title: string, data: any }) => {
+    const isEmpty = !data || (typeof data === 'object' && Object.keys(data).length === 0);
+    
+    return (
+      <div className="border rounded-lg overflow-hidden border-slate-200">
+        <div className="bg-slate-50 px-3 py-2 border-b border-slate-200 flex justify-between items-center">
+          <span className="text-xs font-bold text-slate-700 uppercase flex items-center gap-2">
+            <FileJson size={14} /> {title}
+          </span>
+          {isEmpty && <span className="text-xs text-slate-400">Empty</span>}
+        </div>
+        {!isEmpty && (
+          <div className="bg-slate-900 p-3 overflow-x-auto">
+            <pre className="text-xs font-mono text-green-400 leading-relaxed">
+              {JSON.stringify(data, null, 2)}
+            </pre>
+          </div>
+        )}
+      </div>
+    );
+  };
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-5xl max-h-[90vh] flex flex-col animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50 rounded-t-xl">
+          <div className="flex items-center gap-3">
+            <div className="bg-indigo-100 p-2 rounded-lg text-indigo-600">
+              <Activity size={20} />
+            </div>
+            <div>
+              <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
+                Task Detail <span className="font-mono text-slate-500">#{task.id}</span>
+              </h3>
+              <div className="flex items-center gap-2 text-xs text-gray-500 mt-1">
+                <span className="font-mono bg-slate-200 px-1 rounded">{task.routing_key}</span>
+                <span>•</span>
+                <span>{new Date(task.updated_at).toLocaleString()}</span>
+              </div>
+            </div>
+          </div>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition p-1 hover:bg-slate-200 rounded-full">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Content */}
+        <div className="flex-1 overflow-y-auto p-6 bg-white">
+          
+          {/* 1. 概览信息 */}
+          <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
+            <div className="p-3 bg-slate-50 rounded-lg border border-slate-100">
+              <div className="text-xs text-slate-500 font-bold uppercase mb-1">Current Status</div>
+              <div>{getStatusBadge(task.status)}</div>
+            </div>
+            <div className="p-3 bg-slate-50 rounded-lg border border-slate-100">
+              <div className="text-xs text-slate-500 font-bold uppercase mb-1">Order ID</div>
+              <div className="text-sm font-mono font-medium text-blue-600">{task.order_id}</div>
+            </div>
+            <div className="p-3 bg-slate-50 rounded-lg border border-slate-100">
+              <div className="text-xs text-slate-500 font-bold uppercase mb-1">Priority / Attempts</div>
+              <div className="text-sm font-mono text-slate-700">
+                P{task.priority} <span className="text-slate-300">|</span> {task.attempt_count} Retries
+              </div>
+            </div>
+            <div className="p-3 bg-slate-50 rounded-lg border border-slate-100">
+              <div className="text-xs text-slate-500 font-bold uppercase mb-1">Version</div>
+              <div className="text-sm font-mono text-slate-700">{task.script_version || 'latest'}</div>
+            </div>
+          </div>
+
+          {/* 2. 详细 JSON 数据区 */}
+          <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
+            <div className="space-y-6">
+              {/* 配置参数 */}
+              <JsonViewer title="Task Config (Route Config)" data={task.config} />
+              
+              {/* 用户输入 */}
+              <JsonViewer title="User Inputs (Form Data)" data={task.user_inputs} />
+            </div>
+
+            <div className="space-y-6">
+              {/* 执行历史/日志 (最重要用于排错) */}
+              <div className="flex flex-col h-full">
+                 <JsonViewer title="Grabbed History / Logs" data={task.grabbed_history} />
+                 
+                 {/* Meta 信息 */}
+                 <div className="mt-6">
+                    <JsonViewer title="Meta Data" data={task.meta} />
+                 </div>
+              </div>
+            </div>
+          </div>
+
+        </div>
+        
+        {/* Footer */}
+        <div className="p-4 border-t bg-slate-50 rounded-b-xl flex justify-end">
+          <button 
+            onClick={onClose}
+            className="px-6 py-2 bg-white border border-slate-300 rounded-lg text-sm font-medium hover:bg-slate-100 text-slate-700 transition"
+          >
+            关闭
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 147 - 0
src/components/admin/tasks/TaskEditModal.tsx

@@ -0,0 +1,147 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Loader2, X, Save, AlertTriangle } from 'lucide-react';
+import JsonEditor from '@/components/common/JsonEditor';
+import { VasTask } from './TaskTable';
+
+interface TaskEditModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  task: VasTask | null;
+  onSubmit: (taskId: number, data: any) => Promise<void>;
+}
+
+export default function TaskEditModal({ isOpen, onClose, task, onSubmit }: TaskEditModalProps) {
+  const [loading, setLoading] = useState(false);
+  
+  // 表单状态
+  const [priority, setPriority] = useState(10);
+  const [configJson, setConfigJson] = useState('{}');
+  const [userInputsJson, setUserInputsJson] = useState('{}');
+
+  useEffect(() => {
+    if (isOpen && task) {
+      setPriority(task.priority || 10);
+      setConfigJson(JSON.stringify(task.config || {}, null, 2));
+      setUserInputsJson(JSON.stringify(task.user_inputs || {}, null, 2));
+    }
+  }, [isOpen, task]);
+
+  const handleSubmit = async () => {
+    setLoading(true);
+    try {
+      // 1. 验证并解析 JSON
+      let parsedConfig = {};
+      let parsedInputs = {};
+      
+      try {
+        parsedConfig = JSON.parse(configJson);
+      } catch (e) {
+        alert("Task Config JSON 格式错误");
+        setLoading(false);
+        return;
+      }
+
+      try {
+        parsedInputs = JSON.parse(userInputsJson);
+      } catch (e) {
+        alert("User Inputs JSON 格式错误");
+        setLoading(false);
+        return;
+      }
+
+      // 2. 构造提交数据
+      const updateData = {
+        priority: Number(priority),
+        config: parsedConfig,
+        user_inputs: parsedInputs,
+      };
+
+      await onSubmit(task!.id, updateData);
+      onClose();
+    } catch (error) {
+      // 错误由父组件处理
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen || !task) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[90vh] animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <div>
+            <h3 className="font-bold text-gray-900 text-lg">编辑任务 #{task.id}</h3>
+            <p className="text-xs text-gray-500 mt-1 font-mono">Routing: {task.routing_key}</p>
+          </div>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Content */}
+        <div className="p-6 overflow-y-auto flex-1 space-y-6">
+          
+          <div className="bg-amber-50 border border-amber-200 rounded-lg p-3 flex gap-3 text-sm text-amber-800">
+            <AlertTriangle size={20} className="flex-shrink-0" />
+            <div>
+              <p className="font-bold">警告</p>
+              <p>修改正在运行的任务可能会导致不可预知的错误。建议在修改前先停止或重置任务。</p>
+            </div>
+          </div>
+
+          <div>
+            <label className="block text-sm font-bold text-slate-700 mb-2">优先级 (Priority)</label>
+            <input 
+              type="number" 
+              className="w-full border border-slate-300 rounded p-2 text-sm max-w-xs"
+              value={priority}
+              onChange={e => setPriority(Number(e.target.value))}
+            />
+            <p className="text-xs text-gray-400 mt-1">数字越大优先级越高</p>
+          </div>
+
+          <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
+            <div>
+              <JsonEditor 
+                label="Task Config (运行配置)"
+                value={configJson}
+                onChange={setConfigJson}
+                height="h-64"
+              />
+            </div>
+            <div>
+              <JsonEditor 
+                label="User Inputs (表单数据)"
+                value={userInputsJson}
+                onChange={setUserInputsJson}
+                height="h-64"
+              />
+            </div>
+          </div>
+
+        </div>
+
+        {/* Footer */}
+        <div className="px-6 py-4 border-t bg-slate-50 flex justify-end gap-3">
+          <button onClick={onClose} className="px-4 py-2 border rounded-lg hover:bg-white text-sm text-slate-600">
+            取消
+          </button>
+          <button 
+            onClick={handleSubmit}
+            disabled={loading}
+            className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-bold flex items-center gap-2 disabled:opacity-50"
+          >
+            {loading && <Loader2 size={16} className="animate-spin" />}
+            保存修改
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 295 - 0
src/components/admin/tasks/TaskTable.tsx

@@ -0,0 +1,295 @@
+'use client';
+
+import { useState } from 'react';
+import { 
+  RotateCcw, 
+  CheckCircle, 
+  ChevronDown, 
+  ChevronUp, 
+  Terminal, 
+  FileJson, 
+  User, 
+  History, 
+  Edit,
+  AlertCircle 
+} from 'lucide-react';
+
+// 定义任务数据结构 (导出供其他组件使用)
+export interface VasTask {
+  id: number;
+  order_id: string;
+  routing_key: string;
+  status: string;       // pending, grabbed, running, cancelled, completed, failed
+  priority: number;
+  script_version?: string;
+  attempt_count: number;
+  notify_count: number;
+  
+  // JSON 字段
+  config?: any;
+  user_inputs?: any;
+  
+  // 数组字段 (日志)
+  grabbed_history?: string[];
+  meta?: string[];
+  
+  created_at: string;
+  updated_at: string;
+  expire_at: string;
+}
+
+interface TaskTableProps {
+  tasks: VasTask[];
+  loading: boolean;
+  onRetry: (taskId: number) => void;
+  onManualConfirm: (taskId: number) => void;
+  onEdit: (task: VasTask) => void;       // 编辑回调
+  onViewDetail: (task: VasTask) => void; // 详情回调 (可选,保留以备不时之需)
+}
+
+export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, onEdit }: TaskTableProps) {
+  // 控制展开行的 ID 集合
+  const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
+
+  const toggleRow = (id: number) => {
+    const newSet = new Set(expandedRows);
+    if (newSet.has(id)) {
+      newSet.delete(id);
+    } else {
+      newSet.add(id);
+    }
+    setExpandedRows(newSet);
+  };
+
+  // 辅助函数:获取数组最后一条
+  const getLastItem = (arr: any) => {
+    if (!Array.isArray(arr) || arr.length === 0) return null;
+    const last = arr[arr.length - 1];
+    return typeof last === 'string' ? last : JSON.stringify(last);
+  };
+
+  // 辅助函数:从 UserInputs 提取摘要
+  const getUserSummary = (inputs: any) => {
+    if (!inputs) return '-';
+    const name = inputs.name || inputs.applicant_name || inputs.first_name || inputs.full_name;
+    const passport = inputs.passport || inputs.passport_no || inputs.passport_number;
+    
+    if (name && passport) return `${name}`;
+    if (name) return name;
+    return 'User Data';
+  };
+
+  if (loading) {
+    return (
+      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
+        <div className="text-gray-500 text-sm">加载系统任务中...</div>
+      </div>
+    );
+  }
+
+  if (tasks.length === 0) {
+    return (
+      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
+        <div className="text-gray-500 text-sm">暂无待处理任务</div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
+      <div className="overflow-x-auto">
+        {/* 使用 table-fixed 强制列宽,防止内容挤压 */}
+        <table className="min-w-full text-sm text-left table-fixed">
+          <thead className="bg-slate-50 border-b border-slate-200">
+            <tr>
+              <th className="w-12 px-4 py-3"></th>
+              <th className="w-[160px] px-4 py-3 font-medium text-slate-500">ID / Route</th>
+              <th className="w-[180px] px-4 py-3 font-medium text-slate-500">Order / User</th>
+              <th className="px-4 py-3 font-medium text-slate-500">业务进度 (History)</th>
+              <th className="px-4 py-3 font-medium text-slate-500">系统调试 (Meta)</th>
+              <th className="w-[100px] px-4 py-3 font-medium text-slate-500">状态</th>
+              <th className="w-[140px] px-4 py-3 font-medium text-slate-500 text-right">操作</th>
+            </tr>
+          </thead>
+
+          {/* 
+            使用多个 tbody 是合法的 HTML 结构,
+            可以完美解决“展开行”导致的样式错位问题。
+          */}
+          {tasks.map((task) => {
+            const isExpanded = expandedRows.has(task.id);
+            const lastHistory = getLastItem(task.grabbed_history);
+            const lastMeta = getLastItem(task.meta);
+            
+            return (
+              <tbody key={task.id} className="group hover:bg-slate-50 transition-colors border-b border-slate-100 last:border-0">
+                
+                {/* === 主行 === */}
+                <tr className="cursor-pointer" onClick={() => toggleRow(task.id)}>
+                  <td className="px-4 py-4 text-slate-400 align-top">
+                    {isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
+                  </td>
+                  
+                  {/* 1. ID & Route */}
+                  <td className="px-4 py-4 align-top">
+                    <div className="font-mono text-slate-500 text-xs mb-1">#{task.id}</div>
+                    <span className="font-mono text-[10px] text-blue-700 bg-blue-50 px-2 py-1 rounded border border-blue-100 inline-block break-all">
+                      {task.routing_key}
+                    </span>
+                  </td>
+
+                  {/* 2. Order & User */}
+                  <td className="px-4 py-4 align-top">
+                    <div className="text-slate-900 font-bold text-xs mb-1 break-all font-mono">{task.order_id}</div>
+                    <div className="text-xs text-slate-500 flex items-center gap-1 truncate" title={JSON.stringify(task.user_inputs)}>
+                      <User size={12} className="text-slate-400 flex-shrink-0" /> 
+                      {getUserSummary(task.user_inputs)}
+                    </div>
+                  </td>
+
+                  {/* 3. 业务进度 (只显示最后一条) */}
+                  <td className="px-4 py-4 align-top">
+                    {lastHistory ? (
+                      <div className="text-xs text-slate-700 font-medium break-words line-clamp-3" title={lastHistory}>
+                        <span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500 mr-2 mb-0.5"></span>
+                        {lastHistory}
+                      </div>
+                    ) : (
+                      <span className="text-xs text-gray-300 italic">No history</span>
+                    )}
+                  </td>
+
+                  {/* 4. 系统调试 (只显示最后一条) */}
+                  <td className="px-4 py-4 align-top">
+                    {lastMeta ? (
+                      <div className="text-[10px] text-slate-600 font-mono break-all line-clamp-3 bg-slate-100 p-1.5 rounded border border-slate-200" title={lastMeta}>
+                        {lastMeta}
+                      </div>
+                    ) : (
+                      <span className="text-xs text-gray-300 italic">No logs</span>
+                    )}
+                    <div className="text-[10px] text-slate-300 mt-1 text-right">
+                      {new Date(task.updated_at).toLocaleTimeString()}
+                    </div>
+                  </td>
+
+                  {/* 5. 状态 */}
+                  <td className="px-4 py-4 align-top">
+                    <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase whitespace-nowrap
+                      ${task.status === 'failed' ? 'bg-red-100 text-red-700' : 
+                        task.status === 'running' ? 'bg-blue-100 text-blue-700 animate-pulse' : 
+                        task.status === 'completed' ? 'bg-green-100 text-green-700' :
+                        'bg-gray-100 text-gray-600'}`}>
+                      {task.status}
+                    </span>
+                    {task.attempt_count > 0 && (
+                      <div className="text-[10px] text-slate-400 mt-1 pl-1">
+                        Retry: {task.attempt_count}
+                      </div>
+                    )}
+                  </td>
+
+                  {/* 6. 操作按钮 */}
+                  <td className="px-4 py-4 text-right align-top" onClick={(e) => e.stopPropagation()}>
+                    <div className="flex justify-end gap-1 flex-wrap">
+                      <button 
+                        onClick={() => onEdit(task)} 
+                        className="p-1.5 rounded text-indigo-600 hover:bg-indigo-50 transition border border-transparent hover:border-indigo-100" 
+                        title="编辑任务配置"
+                      >
+                        <Edit size={16} />
+                      </button>
+                      <button 
+                        onClick={() => onRetry(task.id)} 
+                        className="p-1.5 rounded text-blue-600 hover:bg-blue-50 transition border border-transparent hover:border-blue-100" 
+                        title="重置回队列"
+                      >
+                        <RotateCcw size={16} />
+                      </button>
+                      <button 
+                        onClick={() => onManualConfirm(task.id)} 
+                        className="p-1.5 rounded text-green-600 hover:bg-green-50 transition border border-transparent hover:border-green-100" 
+                        title="强制完成"
+                      >
+                        <CheckCircle size={16} />
+                      </button>
+                    </div>
+                  </td>
+                </tr>
+
+                {/* === 展开详情行 (跨越所有列) === */}
+                {isExpanded && (
+                  <tr className="bg-slate-50 shadow-inner border-t border-slate-100">
+                    <td colSpan={7} className="px-4 py-4">
+                      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 text-xs">
+                        
+                        {/* 左列:配置 & 表单 */}
+                        <div className="space-y-4">
+                          <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
+                            <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-slate-600 flex items-center gap-2">
+                              <FileJson size={14} /> Config (配置)
+                            </div>
+                            <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px]">
+                              {JSON.stringify(task.config, null, 2)}
+                            </pre>
+                          </div>
+                          <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
+                            <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-blue-600 flex items-center gap-2">
+                              <User size={14} /> User Inputs (用户输入)
+                            </div>
+                            <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px]">
+                              {JSON.stringify(task.user_inputs, null, 2)}
+                            </pre>
+                          </div>
+                        </div>
+
+                        {/* 中列:业务历史 (History) */}
+                        <div className="bg-white border border-slate-200 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px]">
+                          <div className="px-3 py-2 bg-green-50 border-b border-green-100 font-bold text-green-800 flex items-center gap-2">
+                            <History size={14} /> Grabbed History
+                          </div>
+                          <div className="p-3 overflow-y-auto flex-1 font-mono text-slate-700 space-y-2">
+                            {Array.isArray(task.grabbed_history) && task.grabbed_history.length > 0 ? (
+                              task.grabbed_history.map((line, i) => (
+                                <div key={i} className="border-b border-slate-50 last:border-0 pb-1 flex gap-2">
+                                  <span className="text-slate-300 select-none w-6 text-right">{(i+1).toString()}</span>
+                                  <span className="break-all">{line}</span>
+                                </div>
+                              ))
+                            ) : (
+                              <span className="text-slate-400 italic">暂无业务日志</span>
+                            )}
+                          </div>
+                        </div>
+
+                        {/* 右列:调试日志 (Meta) */}
+                        <div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px] text-slate-300">
+                          <div className="px-3 py-2 bg-slate-950 border-b border-slate-800 font-bold text-slate-100 flex items-center gap-2">
+                            <Terminal size={14} /> Meta / Debug Logs
+                          </div>
+                          <div className="p-3 overflow-y-auto flex-1 font-mono text-[11px] space-y-1">
+                            {Array.isArray(task.meta) && task.meta.length > 0 ? (
+                              task.meta.map((line, i) => (
+                                <div key={i} className="break-all border-b border-slate-800/30 pb-1 flex gap-2">
+                                  <span className="text-slate-600 mr-1">$</span>
+                                  <span>{line}</span>
+                                </div>
+                              ))
+                            ) : (
+                              <span className="text-slate-600 italic">暂无调试信息</span>
+                            )}
+                          </div>
+                        </div>
+
+                      </div>
+                    </td>
+                  </tr>
+                )}
+              </tbody>
+            );
+          })}
+        </table>
+      </div>
+    </div>
+  );
+}

+ 259 - 0
src/components/admin/tickets/TicketDetailModal.tsx

@@ -0,0 +1,259 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import api from '@/lib/api';
+import { X, Send, User, Shield, Check, Ban, RefreshCw, Loader2, MessageSquare } from 'lucide-react';
+import { AdminTicket } from './TicketTable';
+
+interface TicketDetailModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  ticket: AdminTicket | null;
+  onUpdate?: () => void; // 状态更新后刷新列表
+}
+
+interface TicketMessage {
+  id: number;
+  ticket_id: number;
+  sender_type: 'user' | 'admin' | 'system';
+  content: string;
+  created_at: string;
+}
+
+export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }: TicketDetailModalProps) {
+  const [messages, setMessages] = useState<TicketMessage[]>([]);
+  const [reply, setReply] = useState('');
+  const [loadingMsg, setLoadingMsg] = useState(false);
+  const [sending, setSending] = useState(false);
+  const [statusUpdating, setStatusUpdating] = useState(false);
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+
+  // 加载消息
+  const fetchMessages = async () => {
+    if (!ticket) return;
+    setLoadingMsg(true);
+    try {
+      const res = await api.get('/api/vas/tickets/fetch_message', {
+        params: { ticket_id: ticket.id, page: 1, size: 50 }
+      });
+      if (res.data.data?.items) {
+        // 按时间正序
+        const sorted = res.data.data.items.sort((a: any, b: any) => 
+          new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
+        );
+        setMessages(sorted);
+        setTimeout(() => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100);
+      }
+    } catch (e) {
+      console.error(e);
+    } finally {
+      setLoadingMsg(false);
+    }
+  };
+
+  useEffect(() => {
+    if (isOpen && ticket) {
+      fetchMessages();
+    }
+  }, [isOpen, ticket]);
+
+  // 发送回复
+  const handleSend = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!reply.trim() || !ticket) return;
+
+    setSending(true);
+    try {
+      await api.post(`/api/vas/tickets/send_message`, {
+        content: reply,
+        attachments: null
+      }, {
+        params: { ticket_id: ticket.id }
+      });
+      setReply('');
+      fetchMessages(); 
+    } catch (e) {
+      alert("发送失败");
+    } finally {
+      setSending(false);
+    }
+  };
+
+  // 修改状态
+  const handleStatusChange = async (status: 'resolved' | 'info_required' | 'rejected') => {
+    // 根据 API 要求,status 变更需要 comment
+    const comment = prompt("请输入处理备注/回复给用户的内容 (必填):", "");
+    if (comment === null) return; // 取消
+    if (!comment.trim()) {
+      alert("备注不能为空");
+      return;
+    }
+
+    setStatusUpdating(true);
+    try {
+      // API: POST /api/vas/tickets/status?ticket_id=...
+      await api.post(`/api/vas/tickets/status`, {
+        status,
+        comment
+      }, {
+        params: { ticket_id: ticket?.id }
+      });
+      
+      alert("状态更新成功");
+      if (onUpdate) onUpdate();
+      onClose();
+    } catch (e: any) {
+      console.error(e);
+      alert("操作失败: " + (e.response?.data?.message || '未知错误'));
+    } finally {
+      setStatusUpdating(false);
+    }
+  };
+
+  if (!isOpen || !ticket) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-5xl h-[85vh] flex flex-col overflow-hidden animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
+          <div>
+            <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
+              工单 #{ticket.id} 
+              <span className="text-sm font-normal text-gray-500 bg-white border px-2 py-0.5 rounded-md">{ticket.type}</span>
+            </h3>
+            <p className="text-xs text-gray-500 mt-1">
+              用户: {ticket.user_id} | 订单: {ticket.order_id} | 创建: {new Date(ticket.created_at).toLocaleString()}
+            </p>
+          </div>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1 hover:bg-gray-200 rounded-full transition">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Body Container */}
+        <div className="flex-1 flex overflow-hidden">
+          
+          {/* 左侧:详情与操作 (30%) */}
+          <div className="w-80 border-r border-slate-200 bg-slate-50 flex flex-col overflow-y-auto">
+            <div className="p-5 space-y-6">
+              
+              {/* 状态卡片 */}
+              <div className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
+                <label className="text-xs font-bold text-slate-400 uppercase block mb-2">当前状态</label>
+                <div className="font-bold text-slate-800 text-lg mb-1 capitalize">
+                  {ticket.status.replace('_', ' ')}
+                </div>
+              </div>
+
+              {/* 描述 */}
+              <div>
+                <label className="text-xs font-bold text-slate-400 uppercase block mb-2">用户申请理由</label>
+                <div className="bg-white p-3 rounded-lg border border-slate-200 text-sm text-slate-700 whitespace-pre-wrap">
+                  {ticket.reason}
+                </div>
+              </div>
+
+              {/* 操作区 */}
+              <div className="pt-4 border-t border-slate-200">
+                <label className="text-xs font-bold text-slate-400 uppercase block mb-3">管理操作</label>
+                <div className="space-y-2">
+                  <button 
+                    onClick={() => handleStatusChange('resolved')}
+                    disabled={statusUpdating || ticket.status === 'resolved'}
+                    className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-300 text-white rounded-lg text-sm font-medium transition shadow-sm"
+                  >
+                    <Check size={16} /> 批准 / 已解决
+                  </button>
+                  <button 
+                     onClick={() => handleStatusChange('info_required')}
+                     disabled={statusUpdating || ticket.status === 'info_required'}
+                     className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-300 text-white rounded-lg text-sm font-medium transition shadow-sm"
+                  >
+                    <RefreshCw size={16} /> 要求补充资料
+                  </button>
+                  <button 
+                     onClick={() => handleStatusChange('rejected')}
+                     disabled={statusUpdating || ticket.status === 'rejected'}
+                     className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-red-300 text-white rounded-lg text-sm font-medium transition shadow-sm"
+                  >
+                    <Ban size={16} /> 拒绝 / 驳回
+                  </button>
+                </div>
+                <p className="text-[10px] text-gray-400 mt-2 text-center">点击操作需填写备注</p>
+              </div>
+            </div>
+          </div>
+
+          {/* 右侧:聊天记录 (70%) */}
+          <div className="flex-1 flex flex-col bg-white">
+            <div className="flex-1 overflow-y-auto p-6 space-y-4 bg-slate-50/30">
+              {loadingMsg ? (
+                <div className="h-full flex flex-col items-center justify-center text-gray-400">
+                  <Loader2 className="animate-spin w-8 h-8 mb-2" />
+                  <p>加载对话中...</p>
+                </div>
+              ) : messages.length === 0 ? (
+                <div className="h-full flex flex-col items-center justify-center text-gray-300">
+                  <MessageSquare className="w-12 h-12 mb-2 opacity-50" />
+                  <p>暂无对话记录</p>
+                </div>
+              ) : (
+                messages.map((msg) => {
+                  const isAdmin = msg.sender_type === 'admin' || msg.sender_type === 'system';
+                  return (
+                    <div key={msg.id} className={`flex ${isAdmin ? 'justify-end' : 'justify-start'}`}>
+                      <div className={`flex max-w-[80%] ${isAdmin ? 'flex-row-reverse' : 'flex-row'} items-end gap-2`}>
+                        {/* Avatar */}
+                        <div className={`flex-shrink-0 h-8 w-8 rounded-full flex items-center justify-center ${isAdmin ? 'bg-purple-100 text-purple-600' : 'bg-gray-200 text-gray-600'}`}>
+                          {isAdmin ? <Shield size={14} /> : <User size={14} />}
+                        </div>
+                        
+                        {/* Bubble */}
+                        <div>
+                          <div className={`px-4 py-2 rounded-2xl text-sm shadow-sm whitespace-pre-wrap ${
+                            isAdmin 
+                              ? 'bg-purple-600 text-white rounded-tr-none' 
+                              : 'bg-white text-gray-800 border border-gray-200 rounded-tl-none'
+                          }`}>
+                            {msg.content}
+                          </div>
+                          <div className={`text-[10px] text-gray-400 mt-1 px-1 ${isAdmin ? 'text-right' : 'text-left'}`}>
+                             {msg.sender_type === 'system' ? '系统' : (isAdmin ? '客服' : '用户')} • {new Date(msg.created_at).toLocaleString([], {month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit'})}
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  );
+                })
+              )}
+              <div ref={messagesEndRef} />
+            </div>
+
+            {/* Input */}
+            <div className="p-4 border-t bg-white">
+              <form onSubmit={handleSend} className="flex gap-2">
+                <input
+                  type="text"
+                  value={reply}
+                  onChange={(e) => setReply(e.target.value)}
+                  placeholder="输入回复内容..."
+                  className="flex-1 border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-purple-500 outline-none transition"
+                />
+                <button 
+                  type="submit" 
+                  disabled={sending || !reply.trim()}
+                  className="bg-purple-600 text-white px-6 py-2 rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium shadow-sm transition flex items-center gap-2"
+                >
+                  {sending ? <Loader2 className="animate-spin w-4 h-4" /> : <Send size={16} />} 发送
+                </button>
+              </form>
+            </div>
+          </div>
+        </div>
+
+      </div>
+    </div>
+  );
+}

+ 119 - 0
src/components/admin/tickets/TicketTable.tsx

@@ -0,0 +1,119 @@
+'use client';
+
+import { Eye, Clock, CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
+
+// 定义工单数据结构 (对应 API: VasTicketOut)
+export interface AdminTicket {
+  id: number;
+  order_id: string;
+  user_id: string;
+  type: string;     // refund, dispute, change_request
+  status: string;   // pending, info_required, resolved, rejected
+  reason: string;
+  admin_comment?: string;
+  created_at: string;
+  updated_at: string;
+}
+
+interface TicketTableProps {
+  tickets: AdminTicket[];
+  loading: boolean;
+  onViewDetail: (ticket: AdminTicket) => void;
+}
+
+export default function TicketTable({ tickets, loading, onViewDetail }: TicketTableProps) {
+  
+  // Loading
+  if (loading) {
+    return (
+      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
+        <div className="text-gray-500 text-sm">加载工单数据中...</div>
+      </div>
+    );
+  }
+
+  // Empty
+  if (tickets.length === 0) {
+    return (
+      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
+        <div className="text-gray-500 text-sm">暂无工单记录</div>
+      </div>
+    );
+  }
+
+  // 状态徽章
+  const getStatusBadge = (status: string) => {
+    switch (status) {
+      case 'pending':
+        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"><Clock size={12}/> 待处理</span>;
+      case 'info_required':
+        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"><HelpCircle size={12}/> 需补充资料</span>;
+      case 'resolved':
+        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><CheckCircle size={12}/> 已解决</span>;
+      case 'rejected':
+        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"><XCircle size={12}/> 已拒绝</span>;
+      default:
+        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">{status}</span>;
+    }
+  };
+
+  const getTypeText = (type: string) => {
+    const map: Record<string, string> = {
+      refund: '退款申请',
+      dispute: '交易纠纷',
+      change_request: '变更请求'
+    };
+    return map[type] || type;
+  };
+
+  return (
+    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
+      <div className="overflow-x-auto">
+        <table className="min-w-full divide-y divide-slate-200">
+          <thead className="bg-slate-50">
+            <tr>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">ID</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">类型</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">关联订单 / 用户</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">描述摘要</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">状态</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">创建时间</th>
+              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">操作</th>
+            </tr>
+          </thead>
+          <tbody className="bg-white divide-y divide-slate-200">
+            {tickets.map((ticket) => (
+              <tr key={ticket.id} className="hover:bg-slate-50 transition-colors">
+                <td className="px-6 py-4 text-sm font-mono text-gray-500">#{ticket.id}</td>
+                <td className="px-6 py-4 text-sm font-bold text-gray-800">{getTypeText(ticket.type)}</td>
+                <td className="px-6 py-4">
+                  <div className="text-sm font-medium text-blue-600 font-mono">{ticket.order_id}</div>
+                  <div className="text-xs text-gray-500 mt-0.5">{ticket.user_id}</div>
+                </td>
+                <td className="px-6 py-4">
+                  <div className="text-sm text-gray-600 max-w-[200px] truncate" title={ticket.reason}>
+                    {ticket.reason}
+                  </div>
+                </td>
+                <td className="px-6 py-4 whitespace-nowrap">
+                  {getStatusBadge(ticket.status)}
+                </td>
+                <td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
+                  {new Date(ticket.created_at).toLocaleString()}
+                </td>
+                <td className="px-6 py-4 text-right">
+                  <button 
+                    onClick={() => onViewDetail(ticket)}
+                    className="text-blue-600 hover:text-blue-900 inline-flex items-center text-sm font-medium"
+                  >
+                    <Eye size={16} className="mr-1" /> 处理/详情
+                  </button>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}

+ 131 - 0
src/components/admin/users/UserModal.tsx

@@ -0,0 +1,131 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Loader2, X, Save, UserCog } from 'lucide-react';
+import { AdminUser } from './UserTable';
+
+interface UserModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  user: AdminUser | null;
+  onSubmit: (id: string, data: any) => Promise<void>;
+}
+
+export default function UserModal({ isOpen, onClose, user, onSubmit }: UserModalProps) {
+  const [loading, setLoading] = useState(false);
+  const [form, setForm] = useState({
+    nickname: '',
+    role: 'user',
+    email_verified: 0,
+    phone: ''
+  });
+
+  useEffect(() => {
+    if (isOpen && user) {
+      setForm({
+        nickname: user.nickname || '',
+        role: user.role || 'user',
+        // 兼容 boolean 和 number (0/1)
+        email_verified: user.email_verified ? 1 : 0,
+        phone: user.phone || ''
+      });
+    }
+  }, [isOpen, user]);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!user) return;
+    setLoading(true);
+    try {
+      await onSubmit(user.id, {
+        ...form,
+        // 如果后端需要 boolean,这里转一下;如果是 int,保持 0/1
+        email_verified: Number(form.email_verified) 
+      });
+      onClose();
+    } catch (e) {
+      // parent handles alert
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen || !user) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-lg overflow-hidden animate-in zoom-in duration-200">
+        
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
+            <UserCog size={20} className="text-blue-600"/> 编辑用户
+          </h3>
+          <button onClick={onClose}><X size={24} className="text-gray-400 hover:text-gray-600" /></button>
+        </div>
+
+        <form onSubmit={handleSubmit} className="p-6 space-y-5">
+          {/* 只读字段 */}
+          <div className="bg-gray-50 p-3 rounded border border-gray-200 text-sm">
+            <div className="flex justify-between mb-1">
+              <span className="text-gray-500">User ID:</span>
+              <span className="font-mono text-gray-700">{user.id}</span>
+            </div>
+            <div className="flex justify-between">
+              <span className="text-gray-500">Email:</span>
+              <span className="font-bold text-gray-800">{user.email}</span>
+            </div>
+          </div>
+
+          <div className="grid grid-cols-2 gap-4">
+            <div>
+              <label className="block text-xs font-bold text-slate-500 mb-1 uppercase">昵称</label>
+              <input 
+                type="text" className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                value={form.nickname} onChange={e => setForm({...form, nickname: e.target.value})}
+              />
+            </div>
+            <div>
+              <label className="block text-xs font-bold text-slate-500 mb-1 uppercase">手机号</label>
+              <input 
+                type="text" className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                value={form.phone} onChange={e => setForm({...form, phone: e.target.value})}
+              />
+            </div>
+          </div>
+
+          <div className="grid grid-cols-2 gap-4">
+            <div>
+              <label className="block text-xs font-bold text-slate-500 mb-1 uppercase">系统角色</label>
+              <select 
+                className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none bg-white"
+                value={form.role} onChange={e => setForm({...form, role: e.target.value})}
+              >
+                <option value="user">普通用户 (User)</option>
+                <option value="admin">管理员 (Admin)</option>
+              </select>
+              <p className="text-[10px] text-gray-400 mt-1">管理员拥有所有后台权限,请谨慎操作。</p>
+            </div>
+            <div>
+              <label className="block text-xs font-bold text-slate-500 mb-1 uppercase">邮箱验证</label>
+              <select 
+                className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none bg-white"
+                value={form.email_verified} onChange={e => setForm({...form, email_verified: Number(e.target.value)})}
+              >
+                <option value={1}>已验证 (True)</option>
+                <option value={0}>未验证 (False)</option>
+              </select>
+            </div>
+          </div>
+
+          <div className="pt-4 flex justify-end gap-3 border-t">
+            <button type="button" onClick={onClose} className="px-4 py-2 border rounded hover:bg-gray-50 text-sm">取消</button>
+            <button type="submit" disabled={loading} className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center gap-2 text-sm font-bold">
+              {loading && <Loader2 size={16} className="animate-spin" />}
+              保存修改
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+}

+ 116 - 0
src/components/admin/users/UserTable.tsx

@@ -0,0 +1,116 @@
+'use client';
+
+import { Edit, Shield, CheckCircle, XCircle, User } from 'lucide-react';
+
+// 定义用户类型 (参考 VasUserOut)
+export interface AdminUser {
+  id: string;
+  email: string;
+  phone?: string;
+  nickname?: string;
+  role?: string; // 'admin' | 'user'
+  email_verified?: number | boolean; // 1 or 0, true or false
+  created_at: string;
+  avatar_url?: string;
+}
+
+interface UserTableProps {
+  users: AdminUser[];
+  loading: boolean;
+  onEdit: (user: AdminUser) => void;
+}
+
+export default function UserTable({ users, loading, onEdit }: UserTableProps) {
+  if (loading) {
+    return <div className="p-12 text-center text-gray-500">加载用户数据中...</div>;
+  }
+
+  return (
+    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
+      <div className="overflow-x-auto">
+        <table className="min-w-full divide-y divide-slate-200">
+          <thead className="bg-slate-50">
+            <tr>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">用户</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">角色 (Role)</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">联系方式</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">状态</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">注册时间</th>
+              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">操作</th>
+            </tr>
+          </thead>
+          <tbody className="bg-white divide-y divide-slate-200">
+            {users.map((user) => (
+              <tr key={user.id} className="hover:bg-slate-50 transition">
+                
+                {/* 用户基本信息 */}
+                <td className="px-6 py-4 whitespace-nowrap">
+                  <div className="flex items-center">
+                    <div className="h-10 w-10 flex-shrink-0 bg-slate-100 rounded-full flex items-center justify-center text-slate-400 overflow-hidden">
+                      {user.avatar_url ? (
+                        <img src={user.avatar_url} alt="" className="h-full w-full object-cover" />
+                      ) : (
+                        <User size={20} />
+                      )}
+                    </div>
+                    <div className="ml-4">
+                      <div className="text-sm font-medium text-slate-900">{user.nickname || '未设置昵称'}</div>
+                      <div className="text-xs text-slate-500 font-mono">ID: {user.id}</div>
+                    </div>
+                  </div>
+                </td>
+
+                {/* 角色 */}
+                <td className="px-6 py-4 whitespace-nowrap">
+                  {user.role === 'admin' ? (
+                    <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
+                      <Shield size={12} className="mr-1" /> 管理员
+                    </span>
+                  ) : (
+                    <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-600">
+                      普通用户
+                    </span>
+                  )}
+                </td>
+
+                {/* 联系方式 */}
+                <td className="px-6 py-4 whitespace-nowrap">
+                  <div className="text-sm text-slate-900">{user.email}</div>
+                  <div className="text-xs text-slate-500">{user.phone || '-'}</div>
+                </td>
+
+                {/* 状态 (Email Verified) */}
+                <td className="px-6 py-4 whitespace-nowrap">
+                  {user.email_verified ? (
+                    <span className="text-green-600 flex items-center text-xs font-medium">
+                      <CheckCircle size={14} className="mr-1" /> 已验证
+                    </span>
+                  ) : (
+                    <span className="text-orange-500 flex items-center text-xs font-medium">
+                      <XCircle size={14} className="mr-1" /> 未验证
+                    </span>
+                  )}
+                </td>
+
+                {/* 时间 */}
+                <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
+                  {new Date(user.created_at).toLocaleDateString()}
+                </td>
+
+                {/* 操作 */}
+                <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
+                  <button 
+                    onClick={() => onEdit(user)}
+                    className="text-blue-600 hover:text-blue-900 inline-flex items-center bg-blue-50 px-3 py-1.5 rounded hover:bg-blue-100 transition"
+                  >
+                    <Edit size={14} className="mr-1" /> 编辑
+                  </button>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}

+ 78 - 0
src/components/common/JsonEditor.tsx

@@ -0,0 +1,78 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Braces, Check, AlertCircle } from 'lucide-react';
+
+interface JsonEditorProps {
+  label: string;
+  value: string; // JSON string
+  onChange: (val: string) => void;
+  height?: string;
+  placeholder?: string;
+}
+
+export default function JsonEditor({ label, value, onChange, height = "h-40", placeholder }: JsonEditorProps) {
+  const [text, setText] = useState(value);
+  const [error, setError] = useState<string | null>(null);
+
+  useEffect(() => {
+    setText(value);
+  }, [value]);
+
+  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+    const val = e.target.value;
+    setText(val);
+    onChange(val);
+    
+    try {
+      if (val.trim()) {
+        JSON.parse(val);
+      }
+      setError(null);
+    } catch (err) {
+      setError("无效的 JSON 格式");
+    }
+  };
+
+  const formatJson = (e: React.MouseEvent) => {
+    e.preventDefault();
+    try {
+      const obj = JSON.parse(text);
+      const formatted = JSON.stringify(obj, null, 2);
+      setText(formatted);
+      onChange(formatted);
+      setError(null);
+    } catch (err) {
+      alert("无法格式化:JSON 语法错误");
+    }
+  };
+
+  return (
+    <div>
+      <div className="flex justify-between items-center mb-1">
+        <label className="block text-sm font-medium text-slate-700">{label}</label>
+        <button 
+          onClick={formatJson}
+          className="text-xs flex items-center text-blue-600 hover:text-blue-800"
+          type="button"
+        >
+          <Braces size={12} className="mr-1" /> 格式化
+        </button>
+      </div>
+      <textarea
+        className={`w-full border rounded-lg p-2 font-mono text-xs leading-relaxed outline-none transition
+          ${error ? 'border-red-300 focus:ring-red-200' : 'border-slate-300 focus:ring-blue-200'}
+          ${height}
+        `}
+        value={text}
+        onChange={handleChange}
+        placeholder={placeholder || '{\n  "key": "value"\n}'}
+      />
+      {error && (
+        <p className="text-xs text-red-500 mt-1 flex items-center">
+          <AlertCircle size={12} className="mr-1" /> {error}
+        </p>
+      )}
+    </div>
+  );
+}

+ 94 - 0
src/components/common/Pagination.tsx

@@ -0,0 +1,94 @@
+'use client';
+
+import { ChevronLeft, ChevronRight } from 'lucide-react';
+
+interface PaginationProps {
+  currentPage: number;
+  total: number;
+  pageSize: number;
+  onPageChange: (page: number) => void;
+}
+
+export default function Pagination({ currentPage, total, pageSize, onPageChange }: PaginationProps) {
+  const totalPages = Math.ceil(total / pageSize);
+
+  if (totalPages <= 1) return null; // 只有一页时不显示
+
+  // 生成页码数组 (简单的逻辑,比如显示 [1, 2, 3, 4, 5])
+  // 如果页数很多,这里可以优化为显示 [1, ... 5, 6, 7 ... 10]
+  const renderPageNumbers = () => {
+    const pages = [];
+    // 简单策略:最多显示前后2页
+    const start = Math.max(1, currentPage - 2);
+    const end = Math.min(totalPages, currentPage + 2);
+
+    for (let i = start; i <= end; i++) {
+      pages.push(i);
+    }
+    return pages;
+  };
+
+  return (
+    <div className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 mt-4 rounded-b-lg">
+      <div className="flex flex-1 justify-between sm:hidden">
+        <button
+          onClick={() => onPageChange(Math.max(1, currentPage - 1))}
+          disabled={currentPage === 1}
+          className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
+        >
+          上一页
+        </button>
+        <button
+          onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
+          disabled={currentPage === totalPages}
+          className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
+        >
+          下一页
+        </button>
+      </div>
+      
+      <div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
+        <div>
+          <p className="text-sm text-gray-700">
+            显示第 <span className="font-medium">{(currentPage - 1) * pageSize + 1}</span> 到 <span className="font-medium">{Math.min(currentPage * pageSize, total)}</span> 条,
+            共 <span className="font-medium">{total}</span> 条
+          </p>
+        </div>
+        <div>
+          <nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
+            <button
+              onClick={() => onPageChange(currentPage - 1)}
+              disabled={currentPage === 1}
+              className="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50 disabled:cursor-not-allowed"
+            >
+              <span className="sr-only">Previous</span>
+              <ChevronLeft className="h-5 w-5" aria-hidden="true" />
+            </button>
+            
+            {renderPageNumbers().map((p) => (
+              <button
+                key={p}
+                onClick={() => onPageChange(p)}
+                className={`relative inline-flex items-center px-4 py-2 text-sm font-semibold 
+                  ${p === currentPage 
+                    ? 'z-10 bg-blue-600 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600' 
+                    : 'text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0'}`}
+              >
+                {p}
+              </button>
+            ))}
+
+            <button
+              onClick={() => onPageChange(currentPage + 1)}
+              disabled={currentPage === totalPages}
+              className="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50 disabled:cursor-not-allowed"
+            >
+              <span className="sr-only">Next</span>
+              <ChevronRight className="h-5 w-5" aria-hidden="true" />
+            </button>
+          </nav>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 212 - 0
src/components/dashboard/ChangePasswordModal.tsx

@@ -0,0 +1,212 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { X, Lock, Loader2, Save, Mail, KeyRound, Eye, EyeOff } from 'lucide-react';
+
+interface ChangePasswordModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) {
+  const [loading, setLoading] = useState(false);
+  const [countdown, setCountdown] = useState(0);
+  
+  // 表单数据
+  const [email, setEmail] = useState('');
+  const [code, setCode] = useState('');
+  const [newPassword, setNewPassword] = useState('');
+  const [confirmPassword, setConfirmPassword] = useState('');
+  
+  // 密码显示状态
+  const [showPassword, setShowPassword] = useState(false);
+
+  // 初始化:获取当前用户邮箱
+  useEffect(() => {
+    if (isOpen) {
+      const userStr = localStorage.getItem('user_info');
+      if (userStr) {
+        try {
+          const user = JSON.parse(userStr);
+          setEmail(user.email || '');
+        } catch (e) {}
+      }
+      // 重置表单
+      setCode('');
+      setNewPassword('');
+      setConfirmPassword('');
+    }
+  }, [isOpen]);
+
+  // 倒计时逻辑
+  useEffect(() => {
+    let timer: NodeJS.Timeout;
+    if (countdown > 0) {
+      timer = setTimeout(() => setCountdown(c => c - 1), 1000);
+    }
+    return () => clearTimeout(timer);
+  }, [countdown]);
+
+  // 发送验证码
+  const handleSendCode = async () => {
+    if (!email) return alert("未找到用户邮箱");
+    
+    try {
+      // -----------------------------------------------------------
+      // TODO: 请替换为你真实的【发送重置密码验证码】接口
+      // API: POST /api/auth/send-reset-code (Body: { email })
+      // -----------------------------------------------------------
+      await api.post('/api/auth/send-reset-code', { email });
+      
+      alert(`验证码已发送至 ${email}`);
+      setCountdown(60);
+    } catch (error: any) {
+      console.error(error);
+      alert("发送失败: " + (error.response?.data?.message || "未知错误"));
+    }
+  };
+
+  // 提交修改
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!code) return alert("请输入验证码");
+    if (!newPassword) return alert("请输入新密码");
+    if (newPassword !== confirmPassword) return alert("两次输入的密码不一致");
+
+    setLoading(true);
+    try {
+      // -----------------------------------------------------------
+      // TODO: 请替换为你真实的【重置密码】接口
+      // API: POST /api/auth/reset-password
+      // Body: { email, code, new_password }
+      // -----------------------------------------------------------
+      await api.post('/api/auth/reset-password', { 
+        email, 
+        code, 
+        new_password: newPassword 
+      });
+
+      alert('密码修改成功!请使用新密码重新登录。');
+      
+      // 可选:强制退出登录
+      localStorage.removeItem('rsid');
+      localStorage.removeItem('user_info');
+      window.location.href = '/login';
+      
+      onClose();
+    } catch (error: any) {
+      console.error(error);
+      alert("修改失败: " + (error.response?.data?.message || error.message));
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
+            <KeyRound size={20} className="text-blue-600"/> 修改密码
+          </h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={24} /></button>
+        </div>
+
+        <form onSubmit={handleSubmit} className="p-6 space-y-5">
+          
+          {/* 邮箱 & 验证码 */}
+          <div>
+            <label className="block text-sm font-medium text-gray-700 mb-1">邮箱验证</label>
+            <div className="relative mb-3">
+              <input 
+                type="text" disabled value={email}
+                className="w-full border border-slate-300 rounded-lg p-2.5 pl-9 text-sm bg-gray-50 text-gray-500"
+              />
+              <Mail size={16} className="absolute left-3 top-3 text-gray-400" />
+            </div>
+            
+            <div className="flex gap-2">
+              <input
+                type="text" required
+                className="flex-1 border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none font-mono tracking-widest text-center"
+                placeholder="6位验证码"
+                maxLength={6}
+                value={code}
+                onChange={(e) => setCode(e.target.value)}
+              />
+              <button
+                type="button"
+                disabled={countdown > 0}
+                onClick={handleSendCode}
+                className="w-32 border border-blue-200 bg-blue-50 text-blue-600 rounded-lg text-xs font-medium hover:bg-blue-100 disabled:opacity-50 disabled:bg-slate-100 disabled:text-slate-400 disabled:border-slate-200 transition"
+              >
+                {countdown > 0 ? `${countdown}s 后重发` : '获取验证码'}
+              </button>
+            </div>
+          </div>
+
+          <div className="border-t border-slate-100 my-2"></div>
+
+          {/* 新密码 */}
+          <div>
+            <label className="block text-sm font-medium text-gray-700 mb-1">设置新密码</label>
+            <div className="relative">
+              <input
+                type={showPassword ? "text" : "password"} required
+                className="w-full border border-slate-300 rounded-lg p-2.5 pl-9 pr-10 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                placeholder="新密码"
+                value={newPassword}
+                onChange={(e) => setNewPassword(e.target.value)}
+              />
+              <Lock size={16} className="absolute left-3 top-3 text-gray-400" />
+              <button 
+                type="button"
+                onClick={() => setShowPassword(!showPassword)}
+                className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
+              >
+                {showPassword ? <EyeOff size={16}/> : <Eye size={16}/>}
+              </button>
+            </div>
+          </div>
+
+          {/* 确认密码 */}
+          <div>
+            <input
+              type={showPassword ? "text" : "password"} required
+              className={`w-full border rounded-lg p-2.5 pl-9 text-sm focus:ring-2 outline-none transition
+                ${confirmPassword && newPassword !== confirmPassword ? 'border-red-300 focus:ring-red-200' : 'border-slate-300 focus:ring-blue-500'}`}
+              placeholder="确认新密码"
+              value={confirmPassword}
+              onChange={(e) => setConfirmPassword(e.target.value)}
+            />
+            {confirmPassword && newPassword !== confirmPassword && (
+              <p className="text-xs text-red-500 mt-1">两次输入的密码不一致</p>
+            )}
+          </div>
+
+          <div className="pt-2 flex justify-end gap-3">
+            <button 
+              type="button"
+              onClick={onClose}
+              className="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm font-medium"
+            >
+              取消
+            </button>
+            <button 
+              type="submit" 
+              disabled={loading}
+              className="flex-1 bg-blue-600 text-white py-2.5 rounded-lg hover:bg-blue-700 text-sm font-bold flex justify-center items-center gap-2 disabled:opacity-50 shadow-sm"
+            >
+              {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> 确认修改</>}
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+}

+ 255 - 0
src/components/dashboard/OrderList.tsx

@@ -0,0 +1,255 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import api from '@/lib/api';
+import { CreditCard, Loader2, AlertCircle, Package, Clock, Hash, Eye, Search } from 'lucide-react';
+import Pagination from '@/components/common/Pagination'; // 引入通用分页组件
+
+// 订单类型定义
+export interface Order {
+  id: string;
+  created_at: string;
+  status: string;
+  base_amount: number;
+  base_currency: string;
+  amount?: number;
+  currency?: string;
+  product_title?: string;
+  product_name?: string;
+  user_inputs?: Record<string, any>;
+}
+
+interface OrderListProps {
+  onRequestTicket: (orderId: string) => void;
+  onViewDetail: (order: Order) => void;
+}
+
+export default function OrderList({ onRequestTicket, onViewDetail }: OrderListProps) {
+  const router = useRouter();
+  
+  // 数据状态
+  const [loading, setLoading] = useState<boolean>(true);
+  const [orders, setOrders] = useState<Order[]>([]);
+  
+  // 分页与搜索状态
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(5); // 用户端每页显示 5 条,保持界面清爽
+  const [total, setTotal] = useState(0);
+  const [keyword, setKeyword] = useState('');
+
+  // 初始化加载
+  useEffect(() => {
+    fetchOrders(1);
+  }, []);
+
+  // 获取订单列表
+  const fetchOrders = async (targetPage: number) => {
+    setLoading(true);
+    try {
+      // API: GET /api/vas/order/list?page=1&size=5&keyword=...
+      const res = await api.get('/api/vas/order/list_by_user', {
+        params: {
+          page: targetPage,
+          size: pageSize,
+          keyword: keyword
+        }
+      });
+
+      // 适配后端返回结构
+      // 1. 如果后端返回的是分页对象: { items: [], total: 100 }
+      // 2. 如果后端返回的是纯数组 (旧接口): []
+      const data = res.data.data || {};
+      
+      if (Array.isArray(data)) {
+        // 兼容旧接口 (无分页)
+        setOrders(data);
+        setTotal(data.length);
+      } else {
+        // 标准分页接口
+        setOrders(data.items || []);
+        setTotal(data.total || 0);
+      }
+      
+      setPage(targetPage);
+
+    } catch (error) {
+      console.warn("API Error (Using Mock Data):", error);
+      // Mock Data 演示分页
+      const mockOrders = [
+        { id: 'ORD-2025-001', product_title: '日本单次旅游签证 (VIP)', base_amount: 30000, base_currency: 'CNY', status: 'pending', created_at: '2025-01-01T10:00:00' },
+        { id: 'ORD-2024-888', product_title: '泰国电子落地签', base_amount: 45000, base_currency: 'CNY', status: 'paid', created_at: '2024-12-20T14:30:00' },
+        { id: 'ORD-2024-777', product_title: '韩国五年多次', base_amount: 80000, base_currency: 'CNY', status: 'completed', created_at: '2024-11-15T09:00:00' },
+      ];
+      // 简单的 Mock 过滤
+      const filtered = keyword 
+        ? mockOrders.filter(o => o.id.includes(keyword) || o.product_title.includes(keyword))
+        : mockOrders;
+        
+      setOrders(filtered);
+      setTotal(filtered.length);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 处理搜索 (回车或点击按钮触发)
+  const handleSearch = () => {
+    fetchOrders(1); // 搜索时重置到第1页
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') {
+      handleSearch();
+    }
+  };
+
+  // 格式化金额
+  const formatMoney = (amount: number, currency: string) => {
+    if (isNaN(amount)) return '0.00';
+    return `${(amount / 100).toFixed(2)} ${currency}`;
+  };
+
+  // 状态样式
+  const renderStatusBadge = (status: string) => {
+    const styles: Record<string, string> = {
+      pending: 'bg-yellow-50 text-yellow-700 ring-yellow-600/20',
+      paid: 'bg-green-50 text-green-700 ring-green-600/20',
+      succeeded: 'bg-green-50 text-green-700 ring-green-600/20',
+      completed: 'bg-blue-50 text-blue-700 ring-blue-600/20',
+      failed: 'bg-red-50 text-red-700 ring-red-600/20',
+      cancelled: 'bg-gray-50 text-gray-600 ring-gray-500/20',
+    };
+    const labels: Record<string, string> = {
+      pending: '待支付', paid: '已支付', succeeded: '支付成功',
+      completed: '已完成', failed: '失败', cancelled: '已取消',
+    };
+    return (
+      <span className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${styles[status] || 'bg-gray-50 text-gray-600 ring-gray-500/10'}`}>
+        {labels[status] || status.toUpperCase()}
+      </span>
+    );
+  };
+
+  return (
+    <div className="space-y-4">
+      {/* === 顶部工具栏:搜索 === */}
+      <div className="flex justify-between items-center bg-white p-3 rounded-xl shadow-sm border border-slate-200">
+        <div className="relative w-full max-w-sm">
+          <input 
+            type="text" 
+            placeholder="搜索订单号或商品名称..." 
+            className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+            value={keyword}
+            onChange={(e) => setKeyword(e.target.value)}
+            onKeyDown={handleKeyDown}
+          />
+          <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+        </div>
+        <button 
+          onClick={handleSearch}
+          className="ml-3 px-4 py-2 bg-slate-100 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-200 transition"
+        >
+          搜索
+        </button>
+      </div>
+
+      {/* === 订单列表 === */}
+      <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
+        <div className="px-6 py-4 border-b bg-gray-50/50 flex justify-between items-center">
+          <h2 className="text-base font-semibold leading-7 text-gray-900">申请记录</h2>
+          <span className="text-xs text-slate-500">共 {total} 条</span>
+        </div>
+        
+        {loading ? (
+          <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-gray-400" /></div>
+        ) : orders.length === 0 ? (
+          <div className="p-12 text-center">
+            <div className="mx-auto h-12 w-12 text-gray-300 mb-4 bg-gray-50 rounded-full flex items-center justify-center">
+              <AlertCircle className="w-6 h-6" />
+            </div>
+            <h3 className="mt-2 text-sm font-semibold text-gray-900">暂无订单</h3>
+            <p className="mt-1 text-sm text-gray-500">没有找到匹配的订单记录。</p>
+          </div>
+        ) : (
+          <ul className="divide-y divide-gray-100">
+            {orders.map((order) => {
+              const title = order.product_title || order.product_name || '未命名服务';
+              const price = order.base_amount ?? order.amount ?? 0;
+              const currency = order.base_currency || order.currency || 'CNY';
+
+              return (
+                <li key={order.id} className="flex flex-col sm:flex-row items-start sm:items-center justify-between p-6 hover:bg-slate-50 transition gap-4">
+                  
+                  {/* 左侧信息 */}
+                  <div className="min-w-0 flex-1 space-y-2">
+                    <div className="flex items-start justify-between sm:justify-start sm:gap-4">
+                      <div className="flex items-center gap-2">
+                        <Package size={18} className="text-blue-600 flex-shrink-0" />
+                        <p className="text-base font-bold text-gray-900 leading-snug">{title}</p>
+                      </div>
+                      <div className="sm:hidden">{renderStatusBadge(order.status)}</div>
+                    </div>
+                    <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-500 pl-6 sm:pl-7">
+                      <div className="flex items-center gap-1 font-mono bg-slate-100 px-1.5 py-0.5 rounded">
+                        <Hash size={10} /> {order.id}
+                      </div>
+                      <div className="flex items-center gap-1">
+                        <Clock size={12} /> 
+                        <time dateTime={order.created_at}>{new Date(order.created_at).toLocaleString()}</time>
+                      </div>
+                    </div>
+                  </div>
+                  
+                  {/* 右侧金额与操作 */}
+                  <div className="flex w-full sm:w-auto items-center justify-between sm:justify-end gap-6 sm:pl-4 sm:border-l sm:border-slate-100">
+                    <div className="text-right">
+                      <p className="text-lg font-bold text-slate-900">{formatMoney(price, currency)}</p>
+                      <div className="hidden sm:block mt-1">{renderStatusBadge(order.status)}</div>
+                    </div>
+                    
+                    <div className="flex items-center gap-2">
+                      <button
+                        onClick={() => onViewDetail(order)}
+                        className="inline-flex items-center justify-center p-2 rounded-lg text-slate-500 hover:text-blue-600 hover:bg-blue-50 transition border border-transparent hover:border-blue-100"
+                        title="查看详情"
+                      >
+                        <Eye size={20} />
+                      </button>
+
+                      {order.status === 'pending' && (
+                        <button
+                          onClick={() => router.push(`/payment/${order.id}`)}
+                          className="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 transition"
+                        >
+                          支付
+                        </button>
+                      )}
+
+                      {(order.status === 'paid' || order.status === 'succeeded' || order.status === 'completed') && (
+                        <button
+                          onClick={() => onRequestTicket(order.id)}
+                          className="inline-flex items-center justify-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition"
+                        >
+                          售后
+                        </button>
+                      )}
+                    </div>
+                  </div>
+                </li>
+              );
+            })}
+          </ul>
+        )}
+        
+        {/* === 分页组件 === */}
+        <Pagination 
+          currentPage={page}
+          total={total}
+          pageSize={pageSize}
+          onPageChange={(p) => fetchOrders(p)}
+        />
+      </div>
+    </div>
+  );
+}

+ 313 - 0
src/components/dashboard/ProfileSettings.tsx

@@ -0,0 +1,313 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import api from '@/lib/api';
+import { getCurrentUser } from '@/lib/auth';
+import { Loader2, Save, Camera, User, Mail, Shield, Lock, ChevronRight, Edit2 } from 'lucide-react';
+import ChangePasswordModal from '@/components/dashboard/ChangePasswordModal';
+
+export default function ProfileSettings() {
+  const [loading, setLoading] = useState(false);
+  const [isDataLoaded, setIsDataLoaded] = useState(false);
+  const [user, setUser] = useState<any>(null);
+  
+  // 模式控制
+  const [isEditing, setIsEditing] = useState(false);
+  const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
+
+  // 表单状态
+  const [nickname, setNickname] = useState('');
+  const [phone, setPhone] = useState(''); // 新增 phone 状态
+  const [avatarPreview, setAvatarPreview] = useState(''); // 用于 UI 预览
+  const [avatarFile, setAvatarFile] = useState<File | null>(null); // 用于实际上传
+  
+  const fileInputRef = useRef<HTMLInputElement>(null);
+
+  useEffect(() => {
+    const currentUser = getCurrentUser();
+    if (currentUser) {
+      setUser(currentUser);
+      setNickname(currentUser.nickname || '');
+      setPhone(currentUser.phone || '');
+      setAvatarPreview(currentUser.avatar_url || '');
+    }
+    setIsDataLoaded(true);
+  }, []);
+
+  // 处理文件选择(仅预览,不立即上传)
+  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0];
+    if (!file) return;
+    
+    // 限制大小 2MB
+    if (file.size > 2 * 1024 * 1024) {
+      alert("图片大小不能超过 2MB");
+      return;
+    }
+
+    // 保存文件对象用于后续上传
+    setAvatarFile(file);
+
+    // 生成本地预览 URL
+    const previewUrl = URL.createObjectURL(file);
+    setAvatarPreview(previewUrl);
+  };
+
+  // 上传图片到服务器
+  const uploadImage = async (file: File): Promise<string> => {
+    const formData = new FormData();
+    // 注意:根据 swagger 文档,字段名为 'pdf',如果是通用接口可能兼容 'file'
+    // 这里依据文档 Body_resource_upload_file_api_resource_upload_file_post 使用 'pdf'
+    formData.append('file', file); 
+
+    const res = await api.post('/api/resource/upload_file', formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data',
+      },
+    });
+
+    // 假设返回结构为 { code: 0, data: { url: "http..." } } 或直接返回 url 字符串
+    // 根据实际 API 返回调整。通常 resource 接口会返回完整的 URL 对象
+    if (res.data.data && res.data.data.url) {
+      return res.data.data.url;
+    } else if (typeof res.data.data === 'string') {
+      return res.data.data;
+    }
+    throw new Error("图片上传响应格式错误");
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!user) return;
+    setLoading(true);
+
+    try {
+      let finalAvatarUrl = user.avatar_url || '';
+
+      // 1. 如果有新选的文件,先上传文件
+      if (avatarFile) {
+        try {
+          finalAvatarUrl = await uploadImage(avatarFile);
+        } catch (uploadError) {
+          console.error("Image upload failed", uploadError);
+          alert("头像上传失败,请重试");
+          setLoading(false);
+          return;
+        }
+      }
+
+      // 2. 准备更新资料的 Payload
+      const payload = {
+        phone: phone || user.phone || '', // 确保 phone 不为空
+        nickname: nickname,
+        avatar_url: finalAvatarUrl
+      };
+
+      // 3. 调用 set_profiles 接口
+      await api.post('/api/user/set_profiles', payload);
+
+      // 4. 更新本地存储和状态
+      const newUserInfo = { 
+        ...user, 
+        nickname: payload.nickname, 
+        phone: payload.phone, 
+        avatar_url: payload.avatar_url 
+      };
+      
+      localStorage.setItem('user_info', JSON.stringify(newUserInfo));
+      // 触发 storage 事件以便 Header 等组件更新
+      window.dispatchEvent(new Event('storage'));
+
+      alert("个人资料更新成功!");
+      setUser(newUserInfo);
+      setAvatarFile(null); // 清空待上传文件
+      setIsEditing(false);
+      
+    } catch (error: any) {
+      console.error(error);
+      alert("更新失败: " + (error.response?.data?.message || "未知错误"));
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleCancel = () => {
+    if (user) {
+      setNickname(user.nickname || '');
+      setPhone(user.phone || '');
+      setAvatarPreview(user.avatar_url || '');
+      setAvatarFile(null);
+    }
+    setIsEditing(false);
+  };
+
+  if (!isDataLoaded) return <div className="p-12 text-center text-gray-400">加载中...</div>;
+  if (!user) return <div className="p-8 text-center text-red-500">无法获取用户信息</div>;
+
+  return (
+    <div className="space-y-6">
+      
+      {/* 卡片 1: 基础资料 */}
+      <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden max-w-3xl">
+        <div className="px-6 py-4 border-b border-slate-100 bg-gray-50/50 flex justify-between items-center">
+          <div>
+            <h2 className="text-base font-bold text-gray-900">个人资料</h2>
+            <p className="text-xs text-gray-500 mt-0.5">管理您的基本信息展示</p>
+          </div>
+          {!isEditing && (
+            <button 
+              onClick={() => setIsEditing(true)}
+              className="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-slate-300 rounded-lg text-sm text-slate-700 hover:bg-slate-50 hover:text-blue-600 transition shadow-sm"
+            >
+              <Edit2 size={14} /> 编辑资料
+            </button>
+          )}
+        </div>
+
+        <div className="p-8">
+          {!isEditing ? (
+            // === 查看模式 ===
+            <div className="flex flex-col sm:flex-row gap-8 items-center sm:items-start">
+              <div className="w-28 h-28 rounded-full overflow-hidden border-4 border-slate-50 shadow-sm ring-1 ring-slate-200 bg-slate-100 flex items-center justify-center flex-shrink-0">
+                {user.avatar_url ? (
+                  <img src={user.avatar_url} alt="Avatar" className="w-full h-full object-cover" />
+                ) : (
+                  <User size={48} className="text-slate-300" />
+                )}
+              </div>
+              <div className="flex-1 space-y-4 w-full text-center sm:text-left">
+                <div>
+                  <h3 className="text-2xl font-bold text-gray-900">{user.nickname || '未设置昵称'}</h3>
+                  <p className="text-sm text-gray-500 mt-1">User ID: <span className="font-mono">{user.id}</span></p>
+                </div>
+                <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2">
+                  <div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
+                    <div className="p-2 bg-white rounded-full text-blue-500 shadow-sm"><Mail size={16} /></div>
+                    <div className="text-left">
+                      <p className="text-xs text-gray-400 font-bold uppercase">Email</p>
+                      <p className="text-sm text-gray-700 font-medium truncate max-w-[180px]" title={user.email}>{user.email}</p>
+                    </div>
+                  </div>
+                  <div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
+                    <div className="p-2 bg-white rounded-full text-purple-500 shadow-sm"><Shield size={16} /></div>
+                    <div className="text-left">
+                      <p className="text-xs text-gray-400 font-bold uppercase">Role</p>
+                      <p className="text-sm text-gray-700 font-medium capitalize">{user.role || 'User'}</p>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          ) : (
+            // === 编辑模式 ===
+            <form onSubmit={handleSubmit} className="animate-in fade-in zoom-in duration-200">
+              <div className="flex flex-col sm:flex-row gap-8">
+                {/* 头像上传区 */}
+                <div className="flex flex-col items-center space-y-3">
+                  <label htmlFor="avatar-upload" className="relative group cursor-pointer w-28 h-28 rounded-full overflow-hidden border-4 border-slate-50 shadow-sm ring-2 ring-blue-100 hover:ring-blue-400 transition-all">
+                    {avatarPreview ? (
+                      <img src={avatarPreview} alt="Avatar Preview" className="w-full h-full object-cover" />
+                    ) : (
+                      <div className="w-full h-full bg-slate-100 flex items-center justify-center text-slate-400">
+                        <User size={48} />
+                      </div>
+                    )}
+                    <div className="absolute inset-0 bg-black/30 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
+                      <Camera className="text-white drop-shadow-md" size={28} />
+                    </div>
+                  </label>
+                  <p className="text-xs text-blue-600 font-medium">点击更换头像</p>
+                  <input 
+                    id="avatar-upload" 
+                    type="file" 
+                    className="hidden" 
+                    accept="image/*" 
+                    onChange={handleFileChange} 
+                    ref={fileInputRef}
+                  />
+                </div>
+
+                {/* 表单字段 */}
+                <div className="flex-1 space-y-5 max-w-lg">
+                  <div>
+                    <label className="block text-sm font-semibold text-slate-700 mb-1.5">账号邮箱 (不可改)</label>
+                    <div className="w-full px-3 py-2.5 bg-slate-100 border border-slate-200 rounded-lg text-slate-500 text-sm">
+                      {user.email}
+                    </div>
+                  </div>
+                  
+                  <div>
+                    <label className="block text-sm font-semibold text-slate-700 mb-1.5">昵称</label>
+                    <input 
+                      type="text" 
+                      className="w-full border border-slate-300 rounded-lg px-3 py-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+                      value={nickname}
+                      onChange={(e) => setNickname(e.target.value)}
+                      placeholder="请输入昵称"
+                    />
+                  </div>
+
+                  <div>
+                    <label className="block text-sm font-semibold text-slate-700 mb-1.5">手机号码</label>
+                    <input 
+                      type="text" 
+                      className="w-full border border-slate-300 rounded-lg px-3 py-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+                      value={phone}
+                      onChange={(e) => setPhone(e.target.value)}
+                      placeholder="请输入手机号码"
+                    />
+                  </div>
+
+                  <div className="pt-4 flex gap-3">
+                    <button type="button" onClick={handleCancel} className="px-4 py-2 border border-slate-300 rounded-lg text-slate-700 hover:bg-slate-50 text-sm font-medium transition">
+                      取消
+                    </button>
+                    <button type="submit" disabled={loading} className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium flex items-center gap-2 disabled:opacity-50 transition">
+                      {loading ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />} 
+                      {loading ? '上传保存中...' : '保存更改'}
+                    </button>
+                  </div>
+                </div>
+              </div>
+            </form>
+          )}
+        </div>
+      </div>
+
+      {/* 卡片 2: 账户安全 */}
+      {!isEditing && (
+        <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden max-w-3xl">
+          <div className="px-6 py-4 border-b border-slate-100 bg-gray-50/50">
+            <h2 className="text-base font-bold text-gray-900">账户安全</h2>
+          </div>
+          <div className="p-6">
+            <div className="flex items-center justify-between p-4 border border-slate-200 rounded-lg hover:border-slate-300 transition">
+              <div className="flex items-center gap-4">
+                <div className="p-2 bg-slate-100 rounded-full text-slate-600">
+                  <Lock size={20} />
+                </div>
+                <div>
+                  <h4 className="font-bold text-gray-800 text-sm">登录密码</h4>
+                  <p className="text-xs text-gray-500 mt-0.5">建议定期更换密码以保护账户安全</p>
+                </div>
+              </div>
+              <button 
+                onClick={() => setIsPasswordModalOpen(true)}
+                className="text-sm font-medium text-slate-600 hover:text-blue-600 flex items-center gap-1 px-3 py-1.5 rounded-lg hover:bg-slate-50 transition"
+              >
+                修改 <ChevronRight size={16} />
+              </button>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* 密码修改弹窗 */}
+      <ChangePasswordModal 
+        isOpen={isPasswordModalOpen} 
+        onClose={() => setIsPasswordModalOpen(false)} 
+        email={user.email} 
+      />
+    </div>
+  );
+}

+ 67 - 0
src/components/dashboard/Sidebar.tsx

@@ -0,0 +1,67 @@
+'use client';
+
+import { FileText, LifeBuoy, MessageSquare, Settings, LucideIcon } from 'lucide-react';
+
+interface SidebarProps {
+  activeTab: string; 
+  setActiveTab: (tab: string) => void;
+}
+
+interface MenuItem {
+  id: string;
+  label: string;
+  icon: LucideIcon;
+}
+
+export default function Sidebar({ activeTab, setActiveTab }: SidebarProps) {
+  
+  const menuItems: MenuItem[] = [
+    { id: 'orders', label: '我的订单', icon: FileText },
+    { id: 'tickets', label: '售后工单', icon: LifeBuoy },
+    // 恢复为普通 Tab
+    { id: 'settings', label: '账户设置', icon: Settings },
+  ];
+
+  return (
+    <div className="space-y-2">
+      {/* 导航菜单 */}
+      {menuItems.map((item) => {
+        const Icon = item.icon;
+        const isActive = activeTab === item.id;
+        return (
+          <button
+            key={item.id}
+            onClick={() => setActiveTab(item.id)}
+            className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition text-sm font-medium
+              ${isActive 
+                ? 'bg-white shadow-sm text-blue-600 ring-1 ring-black/5' 
+                : 'text-gray-600 hover:bg-white/50 hover:text-gray-900'
+              }`}
+          >
+            <Icon size={20} /> {item.label}
+          </button>
+        );
+      })}
+
+      {/* 客服卡片 */}
+      <div className="mt-8 bg-blue-50 p-5 rounded-xl border border-blue-100">
+        <h3 className="font-bold text-blue-800 flex items-center gap-2 mb-3 text-sm">
+          <MessageSquare size={16} /> 联系客服
+        </h3>
+        <p className="text-xs text-blue-600 mb-4 leading-relaxed">
+          遇到问题?您可以直接提交工单,或通过以下方式联系:
+        </p>
+        <ul className="text-xs text-slate-600 space-y-2 font-mono">
+          <li className="flex items-center gap-2">
+            <span className="w-1.5 h-1.5 bg-blue-400 rounded-full"></span>
+            support@visafly.com
+          </li>
+          <li className="flex items-center gap-2">
+            <span className="w-1.5 h-1.5 bg-blue-400 rounded-full"></span>
+            @visafly_support
+          </li>
+        </ul>
+      </div>
+    </div>
+  );
+}

+ 226 - 0
src/components/dashboard/TicketList.tsx

@@ -0,0 +1,226 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import api from '@/lib/api';
+import { Loader2, MessageSquare, AlertCircle, Clock, CheckCircle, XCircle, ArrowRight, Eye, Search, FileText } from 'lucide-react';
+import Pagination from '@/components/common/Pagination'; 
+// 假设 UserTicketDetailModal 导出了 UserTicket 类型,如果没有,可以使用下方的本地定义
+import { UserTicket } from './UserTicketDetailModal';
+
+interface TicketListProps {
+  onViewDetail: (ticket: UserTicket) => void;
+  refreshTrigger?: number;
+}
+
+// 对应 API: VasTicketOut
+interface TicketData extends UserTicket {
+  // 确保接口字段与 API 响应一致
+  id: number;
+  order_id: string;
+  type: 'refund' | 'dispute' | 'change_request';
+  reason: string;
+  status: 'pending' | 'info_required' | 'resolved' | 'rejected';
+  admin_comment?: string;
+  created_at: string;
+}
+
+export default function TicketList({ onViewDetail, refreshTrigger }: TicketListProps) {
+  // 数据状态
+  const [loading, setLoading] = useState<boolean>(true);
+  const [tickets, setTickets] = useState<TicketData[]>([]);
+  
+  // 分页与搜索状态
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(5); 
+  const [total, setTotal] = useState(0);
+  const [keyword, setKeyword] = useState('');
+
+  // 监听外部刷新触发器
+  useEffect(() => {
+    fetchTickets(1); 
+  }, [refreshTrigger]);
+
+  const fetchTickets = async (targetPage: number) => {
+    try {
+      setLoading(true);
+      
+      // API: GET /api/vas/ticket/list_by_user
+      const res = await api.get('/api/vas/ticket/list_by_user', {
+        params: {
+          page: targetPage,
+          size: pageSize,
+          keyword: keyword
+        }
+      });
+
+      const data = res.data.data;
+      
+      if (data && Array.isArray(data.items)) {
+        setTickets(data.items);
+        setTotal(data.total || 0);
+      } else {
+        setTickets([]);
+        setTotal(0);
+      }
+      
+      setPage(targetPage);
+
+    } catch (error) {
+      console.error("Failed to fetch tickets", error);
+      // 如果 API 失败,清空列表,不再显示 Mock 数据以免混淆
+      setTickets([]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 搜索处理
+  const handleSearch = () => {
+    fetchTickets(1);
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') handleSearch();
+  };
+
+  const getStatusConfig = (status: string) => {
+    switch (status) {
+      case 'pending': return { text: '待处理', color: 'text-yellow-700 bg-yellow-50 border-yellow-200', icon: Clock };
+      case 'info_required': return { text: '需补充资料', color: 'text-blue-700 bg-blue-50 border-blue-200', icon: AlertCircle };
+      case 'resolved': return { text: '已解决', color: 'text-green-700 bg-green-50 border-green-200', icon: CheckCircle };
+      case 'rejected': return { text: '已拒绝', color: 'text-red-700 bg-red-50 border-red-200', icon: XCircle };
+      default: return { text: status, color: 'text-gray-600 bg-gray-50 border-gray-200', icon: MessageSquare };
+    }
+  };
+
+  const getTypeText = (type: string) => {
+    const map: Record<string, string> = {
+      refund: '退款申请',
+      dispute: '交易纠纷',
+      change_request: '变更请求'
+    };
+    return map[type] || type;
+  };
+
+  return (
+    <div className="space-y-4">
+      
+      {/* === 顶部工具栏 === */}
+      <div className="flex gap-2 items-center bg-white p-3 rounded-xl shadow-sm border border-slate-200">
+        <div className="relative flex-1">
+          <input 
+            type="text" 
+            placeholder="搜索工单号、订单号..." 
+            className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+            value={keyword}
+            onChange={(e) => setKeyword(e.target.value)}
+            onKeyDown={handleKeyDown}
+          />
+          <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+        </div>
+        <button 
+          onClick={handleSearch}
+          className="px-5 py-2 bg-slate-800 text-white rounded-lg text-sm font-medium hover:bg-slate-700 transition shadow-sm"
+        >
+          搜索
+        </button>
+      </div>
+
+      {/* === 工单列表 === */}
+      <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
+        <div className="px-6 py-4 border-b border-slate-100 bg-gray-50/50 flex justify-between items-center">
+          <h2 className="text-sm font-bold uppercase text-slate-500 tracking-wide">我的工单</h2>
+          <span className="text-xs font-medium px-2 py-1 bg-white border rounded text-slate-500">Total: {total}</span>
+        </div>
+
+        {loading ? (
+          <div className="p-16 flex justify-center"><Loader2 className="animate-spin text-blue-600 w-8 h-8" /></div>
+        ) : tickets.length === 0 ? (
+          <div className="p-16 text-center text-gray-500 flex flex-col items-center">
+            <div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
+               <FileText className="h-8 w-8 text-gray-400" />
+            </div>
+            <p className="text-lg font-medium text-gray-900">暂无工单记录</p>
+            <p className="text-sm text-gray-500 mt-1">如果您遇到问题,请点击右上角提交新工单。</p>
+          </div>
+        ) : (
+          <div className="divide-y divide-slate-100">
+            {tickets.map((ticket) => {
+              const status = getStatusConfig(ticket.status);
+              const StatusIcon = status.icon;
+              const isActionRequired = ticket.status === 'info_required';
+              
+              return (
+                <div key={ticket.id} className="p-5 hover:bg-slate-50 transition group">
+                  <div className="flex flex-col sm:flex-row gap-4">
+                    {/* 左侧主要信息 */}
+                    <div className="flex-1">
+                      <div className="flex items-center flex-wrap gap-2 mb-2">
+                        <span className="font-bold text-gray-900 text-base">{getTypeText(ticket.type)}</span>
+                        <span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium border ${status.color}`}>
+                          <StatusIcon size={12} /> {status.text}
+                        </span>
+                        <span className="text-xs text-gray-400 font-mono">#{ticket.id}</span>
+                      </div>
+                      
+                      <div className="text-sm text-gray-600 mb-3 line-clamp-2">
+                        {ticket.reason}
+                      </div>
+
+                      <div className="flex items-center gap-4 text-xs text-gray-400">
+                        <span className="flex items-center gap-1">
+                          <FileText size={12} /> 订单: {ticket.order_id}
+                        </span>
+                        <span className="flex items-center gap-1">
+                          <Clock size={12} /> {new Date(ticket.created_at).toLocaleString()}
+                        </span>
+                      </div>
+                    </div>
+
+                    {/* 右侧操作栏 */}
+                    <div className="flex flex-col justify-center items-end gap-2 min-w-[120px]">
+                      {isActionRequired ? (
+                        <button 
+                          onClick={() => onViewDetail(ticket)}
+                          className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-bold shadow-sm shadow-blue-200 transition"
+                        >
+                          回复消息 <ArrowRight size={16} />
+                        </button>
+                      ) : (
+                        <button 
+                          onClick={() => onViewDetail(ticket)}
+                          className="w-full flex items-center justify-center gap-2 px-4 py-2 border border-slate-200 rounded-lg text-slate-600 hover:bg-white hover:border-blue-400 hover:text-blue-600 text-sm font-medium transition bg-slate-50"
+                        >
+                          <Eye size={16} /> 查看详情
+                        </button>
+                      )}
+                    </div>
+                  </div>
+
+                  {/* 管理员最新回复摘要 (如果有) */}
+                  {ticket.admin_comment && (
+                    <div className="mt-4 bg-slate-100/80 border-l-4 border-blue-400 p-3 rounded-r text-sm text-slate-700 flex gap-2">
+                       <MessageSquare className="w-4 h-4 mt-0.5 text-blue-500 flex-shrink-0" />
+                       <div>
+                         <span className="font-bold text-slate-900 mr-1">最新反馈:</span>
+                         {ticket.admin_comment}
+                       </div>
+                    </div>
+                  )}
+                </div>
+              );
+            })}
+          </div>
+        )}
+        
+        {/* === 分页 === */}
+        <Pagination 
+          currentPage={page}
+          total={total}
+          pageSize={pageSize}
+          onPageChange={(p) => fetchTickets(p)}
+        />
+      </div>
+    </div>
+  );
+}

+ 142 - 0
src/components/dashboard/TicketModal.tsx

@@ -0,0 +1,142 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { Loader2, X, AlertTriangle } from 'lucide-react';
+
+interface TicketModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  onSuccess?: () => void; // 新增:成功回调,用于刷新列表
+  defaultOrderId?: string;
+}
+
+export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId = '' }: TicketModalProps) {
+  const [loading, setLoading] = useState<boolean>(false);
+  const [errorMsg, setErrorMsg] = useState<string>('');
+  
+  const [form, setForm] = useState({
+    order_id: '',
+    type: 'refund', // 对应 API Enum: refund | dispute | change_request
+    reason: ''
+  });
+
+  // 初始化
+  useEffect(() => {
+    if (isOpen) {
+      setForm({
+        order_id: defaultOrderId || '',
+        type: 'refund',
+        reason: ''
+      });
+      setErrorMsg('');
+    }
+  }, [isOpen, defaultOrderId]);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    setErrorMsg('');
+
+    try {
+      // API: /api/vas/ticket/create
+      await api.post('/api/vas/ticket/create', form);
+      
+      // 成功处理
+      if (onSuccess) onSuccess(); // 触发父组件刷新
+      onClose();
+      
+    } catch (error: any) {
+      console.error(error);
+      const msg = error.response?.data?.message || '提交失败,请稍后重试';
+      setErrorMsg(msg);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
+      <div className="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" onClick={onClose} />
+
+      <div className="relative w-full max-w-lg transform overflow-hidden rounded-xl bg-white text-left shadow-2xl transition-all animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
+          <h3 className="text-lg font-bold text-gray-900">提交工单</h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition">
+            <X size={24} />
+          </button>
+        </div>
+        
+        <div className="p-6">
+          {errorMsg && (
+            <div className="mb-4 p-3 bg-red-50 text-red-700 text-sm rounded-lg flex items-center">
+              <AlertTriangle size={16} className="mr-2 flex-shrink-0" />
+              {errorMsg}
+            </div>
+          )}
+
+          <form onSubmit={handleSubmit} className="space-y-5">
+            <div>
+              <label className="block text-xs font-bold uppercase text-gray-500 mb-1">关联订单号 <span className="text-red-500">*</span></label>
+              <input
+                type="text" required
+                className="w-full rounded-lg border border-gray-300 py-2.5 px-3 text-gray-900 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm bg-gray-50 focus:bg-white transition"
+                value={form.order_id}
+                onChange={e => setForm({ ...form, order_id: e.target.value })}
+                placeholder="请输入相关的订单编号"
+              />
+            </div>
+
+            <div>
+              <label className="block text-xs font-bold uppercase text-gray-500 mb-1">工单类型 <span className="text-red-500">*</span></label>
+              <select
+                className="w-full rounded-lg border border-gray-300 py-2.5 px-3 text-gray-900 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm bg-white"
+                value={form.type}
+                onChange={e => setForm({ ...form, type: e.target.value })}
+              >
+                {/* 仅保留 API 支持的 Enum */}
+                <option value="refund">申请退款 (Refund)</option>
+                <option value="dispute">交易纠纷 (Dispute)</option>
+                <option value="change_request">变更请求 (Change Request)</option>
+              </select>
+            </div>
+
+            <div>
+              <label className="block text-xs font-bold uppercase text-gray-500 mb-1">详细描述 <span className="text-red-500">*</span></label>
+              <textarea
+                required
+                rows={4}
+                className="w-full rounded-lg border border-gray-300 py-2.5 px-3 text-gray-900 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm resize-none"
+                placeholder="请详细描述您遇到的问题,以便我们更快为您处理..."
+                value={form.reason}
+                onChange={e => setForm({ ...form, reason: e.target.value })}
+              />
+            </div>
+
+            <div className="pt-2 flex items-center justify-end gap-3 border-t mt-6">
+              <button
+                type="button"
+                onClick={onClose}
+                className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-200 transition"
+              >
+                取消
+              </button>
+              <button
+                type="submit"
+                disabled={loading}
+                className="inline-flex items-center justify-center px-6 py-2 text-sm font-bold text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed shadow-md transition"
+              >
+                {loading ? <Loader2 className="animate-spin w-4 h-4 mr-2" /> : null}
+                提交工单
+              </button>
+            </div>
+          </form>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 175 - 0
src/components/dashboard/UserOrderDetailModal.tsx

@@ -0,0 +1,175 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { X, CreditCard, FileText, Package, Check, Clock } from 'lucide-react';
+import api from '@/lib/api';
+
+// 定义接口 (复用或简化)
+export interface UserOrder {
+  id: string;
+  created_at: string;
+  status: string;
+  base_amount: number;
+  base_currency: string;
+  product_title?: string;
+  user_inputs?: Record<string, any>; // 核心字段
+}
+
+interface UserOrderDetailModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  order: UserOrder | null;
+}
+
+export default function UserOrderDetailModal({ isOpen, onClose, order }: UserOrderDetailModalProps) {
+  const [payments, setPayments] = useState<any[]>([]);
+  const [loadingPayments, setLoadingPayments] = useState(false);
+
+  // 当弹窗打开时,尝试获取该订单的支付流水 (如果有权限)
+  useEffect(() => {
+    if (isOpen && order?.id) {
+      fetchPayments(order.id);
+    } else {
+      setPayments([]);
+    }
+  }, [isOpen, order]);
+
+  const fetchPayments = async (orderId: string) => {
+    setLoadingPayments(true);
+    try {
+      // 尝试调用支付列表接口 (假设用户也能查自己的支付记录)
+      // 如果后端没开放这个接口给普通用户,这里会失败,但不影响主界面显示
+      const res = await api.get('/api/vas/payment/list_by_order', { params: { order_id: orderId } });
+      const list = Array.isArray(res.data.data) ? res.data.data : [];
+      setPayments(list);
+    } catch (e) {
+      // 忽略错误,可能是没权限或接口不存在
+    } finally {
+      setLoadingPayments(false);
+    }
+  };
+
+  if (!isOpen || !order) return null;
+
+  const formatMoney = (amount: number, currency: string) => 
+    `${(amount / 100).toFixed(2)} ${currency}`;
+
+  const getStatusColor = (status: string) => {
+    switch (status) {
+      case 'paid': 
+      case 'succeeded': return 'text-green-600 bg-green-50 border-green-100';
+      case 'pending': return 'text-amber-600 bg-amber-50 border-amber-100';
+      case 'failed': return 'text-red-600 bg-red-50 border-red-100';
+      default: return 'text-slate-600 bg-slate-50 border-slate-100';
+    }
+  };
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50 rounded-t-xl">
+          <div>
+            <h3 className="font-bold text-gray-900 text-lg">订单详情</h3>
+            <p className="text-xs text-gray-500 mt-1 font-mono">#{order.id}</p>
+          </div>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition p-1 hover:bg-slate-200 rounded-full">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Content */}
+        <div className="flex-1 overflow-y-auto p-6 space-y-6">
+          
+          {/* 1. 状态与商品 */}
+          <div className="bg-white border rounded-xl p-4 shadow-sm flex flex-col sm:flex-row justify-between gap-4">
+            <div className="flex items-start gap-3">
+              <div className="bg-blue-100 p-2.5 rounded-lg text-blue-600">
+                <Package size={24} />
+              </div>
+              <div>
+                <h4 className="font-bold text-gray-900">{order.product_title || '未命名服务'}</h4>
+                <div className="text-sm text-gray-500 mt-1 flex items-center gap-1">
+                  <Clock size={12} />
+                  创建于: {new Date(order.created_at).toLocaleString()}
+                </div>
+              </div>
+            </div>
+            <div className="text-right">
+              <span className={`px-3 py-1 rounded-full text-xs font-bold border ${getStatusColor(order.status)}`}>
+                {order.status.toUpperCase()}
+              </span>
+              <div className="mt-2 text-xl font-bold text-slate-800">
+                {formatMoney(order.base_amount, order.base_currency)}
+              </div>
+            </div>
+          </div>
+
+          {/* 2. 申请资料 (User Inputs) - 核心需求 */}
+          <div className="border rounded-xl overflow-hidden">
+            <div className="bg-slate-50 px-4 py-3 border-b flex items-center gap-2">
+              <FileText size={18} className="text-slate-600" />
+              <h4 className="font-bold text-sm text-slate-800">申请资料 (Application Data)</h4>
+            </div>
+            <div className="p-5 bg-white">
+              {order.user_inputs && Object.keys(order.user_inputs).length > 0 ? (
+                <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-4">
+                  {Object.entries(order.user_inputs).map(([key, value]) => (
+                    <div key={key} className="border-b border-slate-100 pb-2">
+                      <span className="block text-xs font-bold text-slate-400 uppercase mb-1">{key}</span>
+                      <span className="block text-sm text-slate-800 font-medium break-words">
+                        {String(value)}
+                      </span>
+                    </div>
+                  ))}
+                </div>
+              ) : (
+                <div className="text-center py-4 text-gray-400 text-sm">
+                  未填写申请资料
+                </div>
+              )}
+            </div>
+          </div>
+
+          {/* 3. 支付记录 (如果有) */}
+          {payments.length > 0 && (
+            <div className="border rounded-xl overflow-hidden">
+              <div className="bg-slate-50 px-4 py-3 border-b flex items-center gap-2">
+                <CreditCard size={18} className="text-slate-600" />
+                <h4 className="font-bold text-sm text-slate-800">支付流水</h4>
+              </div>
+              <div className="divide-y divide-slate-100">
+                {payments.map((pay) => (
+                  <div key={pay.id} className="p-4 flex justify-between items-center text-sm">
+                    <div>
+                      <div className="font-bold text-slate-700 capitalize">{pay.provider} ({pay.channel})</div>
+                      <div className="text-xs text-gray-400 mt-0.5">{new Date(pay.created_at).toLocaleString()}</div>
+                    </div>
+                    <div className="text-right">
+                      <div className="font-mono font-medium">{formatMoney(pay.amount, pay.currency)}</div>
+                      <div className="text-xs text-green-600 flex items-center justify-end gap-1">
+                        {pay.status === 'succeeded' && <Check size={10} />} {pay.status}
+                      </div>
+                    </div>
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+
+        </div>
+        
+        {/* Footer */}
+        <div className="p-4 border-t bg-slate-50 rounded-b-xl flex justify-end">
+          <button 
+            onClick={onClose}
+            className="px-6 py-2 bg-white border border-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 text-slate-700 transition shadow-sm"
+          >
+            关闭
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 270 - 0
src/components/dashboard/UserTicketDetailModal.tsx

@@ -0,0 +1,270 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import api from '@/lib/api';
+import { X, Send, User, Headset, Paperclip, Loader2, Clock, CheckCircle, AlertCircle, XCircle } from 'lucide-react';
+
+// 定义工单类型(需导出给列表页使用)
+export interface UserTicket {
+  id: number;
+  order_id: string;
+  type: string;
+  status: string;
+  reason: string;
+  admin_comment?: string;
+  created_at: string;
+}
+
+interface Message {
+  id: number;
+  ticket_id: number;
+  sender_type: 'user' | 'admin' | 'system';
+  content: string;
+  created_at: string;
+}
+
+interface UserTicketDetailModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  ticket: UserTicket | null;
+}
+
+export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserTicketDetailModalProps) {
+  const [messages, setMessages] = useState<Message[]>([]);
+  const [loadingMsg, setLoadingMsg] = useState(false);
+  const [replyContent, setReplyContent] = useState('');
+  const [sending, setSending] = useState(false);
+  
+  // 滚动到底部的引用
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+
+  // 1. 初始化加载消息
+  useEffect(() => {
+    if (isOpen && ticket) {
+      fetchMessages();
+      // 可选:设置轮询,每10秒获取新消息
+      // const interval = setInterval(fetchMessages, 10000);
+      // return () => clearInterval(interval);
+    }
+  }, [isOpen, ticket]);
+
+  // 2. 滚动到底部
+  useEffect(() => {
+    scrollToBottom();
+  }, [messages]);
+
+  const fetchMessages = async () => {
+    if (!ticket) return;
+    try {
+      setLoadingMsg(true);
+      // API: GET /api/vas/tickets/fetch_message?ticket_id=123&page=1&size=50
+      const res = await api.get('/api/vas/tickets/fetch_message', {
+        params: { 
+          ticket_id: ticket.id,
+          page: 1,
+          size: 100 // 获取最近的100条
+        }
+      });
+      
+      const items = res.data.data?.items || [];
+      // 确保按时间正序排列 (旧 -> 新)
+      const sorted = items.sort((a: Message, b: Message) => 
+        new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
+      );
+      setMessages(sorted);
+    } catch (error) {
+      console.error("Fetch messages failed", error);
+    } finally {
+      setLoadingMsg(false);
+    }
+  };
+
+  const handleSend = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!replyContent.trim() || !ticket) return;
+
+    setSending(true);
+    try {
+      // API: POST /api/vas/tickets/send_message?ticket_id=123
+      await api.post(`/api/vas/tickets/send_message`, {
+        content: replyContent,
+        attachments: null // 暂不支持附件,留空
+      }, {
+        params: { ticket_id: ticket.id }
+      });
+
+      setReplyContent('');
+      fetchMessages(); // 发送成功后刷新列表
+    } catch (error) {
+      console.error("Send message failed", error);
+      alert("发送失败,请稍后重试");
+    } finally {
+      setSending(false);
+    }
+  };
+
+  const scrollToBottom = () => {
+    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+  };
+
+  // 辅助函数:渲染状态
+  const renderStatus = (status: string) => {
+    const map: any = {
+      pending: { color: 'text-yellow-600 bg-yellow-50', icon: Clock, text: '待处理' },
+      info_required: { color: 'text-orange-600 bg-orange-50', icon: AlertCircle, text: '需补充资料' },
+      resolved: { color: 'text-green-600 bg-green-50', icon: CheckCircle, text: '已解决' },
+      rejected: { color: 'text-red-600 bg-red-50', icon: XCircle, text: '已拒绝' },
+    };
+    const conf = map[status] || { color: 'text-gray-600 bg-gray-50', icon: Clock, text: status };
+    const Icon = conf.icon;
+    
+    return (
+      <span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${conf.color}`}>
+        <Icon size={12} /> {conf.text}
+      </span>
+    );
+  };
+
+  if (!isOpen || !ticket) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
+      {/* 遮罩层 */}
+      <div className="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity" onClick={onClose} />
+
+      {/* 弹窗主体 */}
+      <div className="relative w-full max-w-2xl h-[80vh] bg-white rounded-xl shadow-2xl overflow-hidden flex flex-col animate-in zoom-in duration-200">
+        
+        {/* 1. 顶部 Header */}
+        <div className="px-6 py-4 border-b bg-gray-50 flex justify-between items-center flex-shrink-0">
+          <div>
+            <div className="flex items-center gap-3">
+              <h3 className="font-bold text-gray-900 text-lg">
+                工单 #{ticket.id}
+              </h3>
+              {renderStatus(ticket.status)}
+            </div>
+            <p className="text-xs text-gray-500 mt-1">
+              关联订单: <span className="font-mono">{ticket.order_id}</span>
+            </p>
+          </div>
+          <button onClick={onClose} className="p-2 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100 transition">
+            <X size={20} />
+          </button>
+        </div>
+
+        {/* 2. 中间:消息列表 (Scrollable) */}
+        <div className="flex-1 overflow-y-auto p-4 sm:p-6 bg-slate-50 space-y-6">
+          
+          {/* 原始工单描述 (作为第一条展示) */}
+          <div className="flex justify-center">
+            <div className="bg-white border border-gray-200 text-gray-600 text-xs px-4 py-2 rounded-full shadow-sm">
+              工单创建于 {new Date(ticket.created_at).toLocaleString()}
+            </div>
+          </div>
+          <div className="flex justify-end">
+            <div className="flex flex-row-reverse items-end gap-2 max-w-[85%]">
+               <div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
+                 <User size={14} className="text-blue-600" />
+               </div>
+               <div>
+                  <div className="bg-blue-600 text-white px-4 py-3 rounded-2xl rounded-tr-none shadow-sm text-sm">
+                    <p className="font-bold text-xs text-blue-100 mb-1 border-b border-blue-500 pb-1">工单描述</p>
+                    {ticket.reason}
+                  </div>
+               </div>
+            </div>
+          </div>
+
+          {/* 会话记录 */}
+          {loadingMsg ? (
+            <div className="flex justify-center py-8"><Loader2 className="animate-spin text-gray-400" /></div>
+          ) : (
+            messages.map((msg) => {
+              const isMe = msg.sender_type === 'user';
+              const isSystem = msg.sender_type === 'system';
+
+              if (isSystem) {
+                return (
+                  <div key={msg.id} className="flex justify-center my-4">
+                    <span className="text-[10px] text-gray-400 bg-gray-100 px-2 py-1 rounded">
+                      系统消息: {msg.content} - {new Date(msg.created_at).toLocaleTimeString()}
+                    </span>
+                  </div>
+                );
+              }
+
+              return (
+                <div key={msg.id} className={`flex ${isMe ? 'justify-end' : 'justify-start'}`}>
+                  <div className={`flex items-end gap-2 max-w-[80%] ${isMe ? 'flex-row-reverse' : 'flex-row'}`}>
+                    
+                    {/* 头像 */}
+                    <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
+                      isMe ? 'bg-blue-100' : 'bg-purple-100'
+                    }`}>
+                      {isMe ? <User size={14} className="text-blue-600" /> : <Headset size={14} className="text-purple-600" />}
+                    </div>
+
+                    {/* 气泡 */}
+                    <div className={`flex flex-col ${isMe ? 'items-end' : 'items-start'}`}>
+                      <div className={`px-4 py-2 text-sm shadow-sm ${
+                        isMe 
+                          ? 'bg-blue-600 text-white rounded-2xl rounded-tr-none' 
+                          : 'bg-white text-gray-800 border border-gray-100 rounded-2xl rounded-tl-none'
+                      }`}>
+                        {msg.content}
+                      </div>
+                      <span className="text-[10px] text-gray-400 mt-1 px-1">
+                        {isMe ? '我' : '客服'} • {new Date(msg.created_at).toLocaleString([], {month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit'})}
+                      </span>
+                    </div>
+
+                  </div>
+                </div>
+              );
+            })
+          )}
+          
+          <div ref={messagesEndRef} />
+        </div>
+
+        {/* 3. 底部:输入框 */}
+        <div className="p-4 bg-white border-t border-gray-100 flex-shrink-0">
+          <form onSubmit={handleSend} className="flex items-end gap-2">
+            <button type="button" className="p-3 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-50 transition" title="上传附件(暂不可用)">
+              <Paperclip size={20} />
+            </button>
+            <div className="flex-1 bg-gray-50 rounded-xl border border-gray-200 focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500 focus-within:bg-white transition-all">
+              <textarea
+                value={replyContent}
+                onChange={(e) => setReplyContent(e.target.value)}
+                onKeyDown={(e) => {
+                  if (e.key === 'Enter' && !e.shiftKey) {
+                    e.preventDefault();
+                    handleSend(e);
+                  }
+                }}
+                placeholder="请输入回复内容..."
+                rows={1}
+                className="w-full bg-transparent border-0 focus:ring-0 p-3 text-sm resize-none max-h-32 min-h-[44px]"
+                style={{ height: 'auto', overflowY: 'hidden' }} 
+                // 简单的自动高度调整,实际项目中可使用 text-area-autosize 库
+              />
+            </div>
+            <button 
+              type="submit" 
+              disabled={sending || !replyContent.trim()}
+              className="p-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-md transition-all flex items-center justify-center"
+            >
+              {sending ? <Loader2 className="animate-spin w-5 h-5" /> : <Send size={20} />}
+            </button>
+          </form>
+          <div className="text-center mt-2">
+             <p className="text-[10px] text-gray-400">如需紧急处理,请发送邮件至 support@visafly.com</p>
+          </div>
+        </div>
+
+      </div>
+    </div>
+  );
+}

+ 102 - 0
src/components/knowledge/KnowledgeCard.tsx

@@ -0,0 +1,102 @@
+'use client';
+
+import { useState } from 'react';
+import { MapPin, ChevronDown, ChevronUp, Calendar } from 'lucide-react';
+
+interface CardData {
+  id: number;
+  title: string;
+  content: string; 
+  image: string | null;
+  label: string;
+  country: string;
+  created_at: string;
+}
+
+const getImageUrl = (fidString: string | null) => {
+  if (!fidString) return null;
+  const firstFid = fidString.split(' ')[0];
+  return `/api/resource/download_file?fid=${firstFid}`;
+};
+
+export default function KnowledgeCard({ data }: { data: CardData }) {
+  const [expanded, setExpanded] = useState(false);
+  const imageUrl = getImageUrl(data.image);
+
+  return (
+    // 修复:移除 h-full,让卡片高度由内容决定,不要撑满父容器高度
+    <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden hover:shadow-md transition-all duration-300 flex flex-col">
+      
+      {/* 图片区域 */}
+      {imageUrl && (
+        <div className="h-48 w-full bg-slate-100 relative overflow-hidden group flex-shrink-0">
+          <img 
+            src={imageUrl} 
+            alt={data.title} 
+            className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
+            loading="lazy"
+          />
+          <div className="absolute top-3 left-3 flex gap-2">
+             <span className="bg-black/60 text-white text-xs px-2 py-1 rounded backdrop-blur-sm flex items-center gap-1">
+                <MapPin size={10} /> {data.country}
+             </span>
+             {data.label && (
+               <span className="bg-blue-600/80 text-white text-xs px-2 py-1 rounded backdrop-blur-sm">
+                 {data.label}
+               </span>
+             )}
+          </div>
+        </div>
+      )}
+
+      <div className="p-5 flex flex-col">
+        {/* 标题 */}
+        <h3 className="text-lg font-bold text-slate-900 mb-3 line-clamp-2 leading-tight group-hover:text-blue-600 transition-colors">
+          {data.title}
+        </h3>
+
+        {/* 内容预览 (HTML) */}
+        <div 
+          className={`
+            text-sm text-slate-600 leading-relaxed overflow-hidden transition-all duration-500 ease-in-out
+            /* 
+               修复:使用具体的 max-height 数值而不是 none,这样 transition 动画才生效。
+               1000px 足够显示大部分文章,如果文章极长,可以调大这个值。
+            */
+            ${expanded ? 'max-h-[1000px]' : 'max-h-[80px] line-clamp-3'}
+            
+            /* === 样式修复:Tailwind Prose === */
+            [&_a]:text-blue-600 [&_a]:underline [&_a]:font-medium hover:[&_a]:text-blue-800
+            [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:my-2
+            [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:my-2
+            [&_p]:mb-2 last:[&_p]:mb-0
+            [&_img]:max-w-full [&_img]:rounded-lg [&_img]:my-2
+          `}
+          dangerouslySetInnerHTML={{ __html: data.content }}
+        />
+
+        {/* 底部信息 & 展开按钮 */}
+        <div className="mt-4 pt-4 flex items-center justify-between border-t border-slate-50">
+          <div className="flex items-center text-xs text-slate-400 gap-1">
+            <Calendar size={12} />
+            {new Date(data.created_at).toLocaleDateString()}
+          </div>
+          
+          <button 
+            onClick={(e) => {
+              e.stopPropagation();
+              setExpanded(!expanded);
+            }}
+            className="text-xs font-medium text-blue-600 hover:text-blue-800 flex items-center gap-1 bg-blue-50 px-2 py-1 rounded transition-colors select-none"
+          >
+            {expanded ? (
+              <>收起 <ChevronUp size={14} /></>
+            ) : (
+              <>阅读全文 <ChevronDown size={14} /></>
+            )}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 37 - 0
src/hooks/useAutoRegister.ts

@@ -0,0 +1,37 @@
+import { useEffect } from 'react';
+import api from '@/lib/api';
+
+export function useAutoRegister() {
+  useEffect(() => {
+    const initUser = async () => {
+      // 1. 检查本地是否有 Token
+      const token = localStorage.getItem('rsid');
+      if (token) return; // 已有账号,不做操作
+
+      try {
+        // 2. 调用自动注册
+        // 根据之前的 OpenAPI: POST /api/auth/auto-register
+        // Body: { register_ip: '...' } (如果不传IP,后端通常会自动获取)
+        const res = await api.post('/api/auth/auto-register', {
+          register_ip: '127.0.0.1' // 这里的IP后端通常会重写,传个占位符即可
+        });
+
+        // 3. 保存 Token 和用户信息
+        const data = res.data.data || res.data;
+        if (data.token) {
+          localStorage.setItem('rsid', data.token);
+          if (data.user) {
+            localStorage.setItem('user_info', JSON.stringify(data.user));
+          }
+          // 触发 storage 事件,更新 Navbar 状态
+          window.dispatchEvent(new Event('storage'));
+          console.log('Auto-register success');
+        }
+      } catch (error) {
+        console.error('Auto-register failed', error);
+      }
+    };
+
+    initUser();
+  }, []);
+}

+ 36 - 0
src/lib/api.js

@@ -0,0 +1,36 @@
+import axios from 'axios';
+
+// 留空,通过 next.config.js 转发
+const API_BASE_URL = ''; 
+
+const api = axios.create({
+  baseURL: API_BASE_URL,
+  headers: {
+    'Content-Type': 'application/json',
+  },
+  timeout: 15000,
+});
+
+api.interceptors.request.use((config) => {
+  if (typeof window !== 'undefined') {
+    const token = localStorage.getItem('rsid');
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`;
+    }
+  }
+  return config;
+});
+
+api.interceptors.response.use(
+  (response) => response,
+  (error) => {
+    if (error.response?.status === 401) {
+      if (typeof window !== 'undefined') {
+        localStorage.removeItem('rsid');
+      }
+    }
+    return Promise.reject(error);
+  }
+);
+
+export default api;

+ 39 - 0
src/lib/auth.ts

@@ -0,0 +1,39 @@
+// 定义用户接口 (参考 OpenAPI VasUserOut)
+export interface User {
+    id: string;
+    email: string;
+    nickname?: string;
+    role?: string; // 关键字段: 'admin' | 'user'
+    avatar_url?: string;
+  }
+  
+  // 获取当前用户
+  export const getCurrentUser = (): User | null => {
+    if (typeof window === 'undefined') return null;
+    
+    const userStr = localStorage.getItem('user_info');
+    if (!userStr) return null;
+  
+    try {
+      return JSON.parse(userStr);
+    } catch (e) {
+      return null;
+    }
+  };
+  
+  // 检查是否是管理员
+  export const isAdmin = (): boolean => {
+    const user = getCurrentUser();
+    // 注意:请确认你数据库里的角色字符串是 'admin' 还是 'administrator' 或 'superuser'
+    return user?.role === 'admin';
+  };
+  
+  // 退出登录
+  export const logout = () => {
+    if (typeof window === 'undefined') return;
+    localStorage.removeItem('rsid');
+    localStorage.removeItem('user_info');
+    // 触发 storage 事件以便其他组件更新
+    window.dispatchEvent(new Event('storage'));
+    window.location.href = '/login';
+  };

+ 26 - 0
src/types/products.ts

@@ -0,0 +1,26 @@
+// 商品基本信息
+export interface Product {
+    id: number;
+    title: string;
+    description: string;
+    country: string;
+    city: string;
+    price_amount: number;
+    price_currency: string;
+    provider: string; // e.g., 'TROOV'
+    visa_type: string;
+    schema_id: number;
+    enabled: number; // 1 or 0
+    // ... 其他字段
+  }
+  
+  // 商品路由信息 (One Product -> Many Routings)
+  export interface ProductRouting {
+    id: number;
+    product_id: number;
+    routing_key: string;    // e.g., "fr_tls_london_bot" (队列名/标识符)
+    script_version: string; // e.g., "v2.1" (脚本版本)
+    priority: number;       // e.g., 10 (优先级)
+    meta?: string;          // JSON string, 额外配置
+    enabled: boolean;
+  }

+ 12 - 0
tailwind.config.js

@@ -0,0 +1,12 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+    content: [
+      "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
+      "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
+      "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
+    ],
+    theme: {
+      extend: {},
+    },
+    plugins: [],
+  };

+ 38 - 0
tsconfig.json

@@ -0,0 +1,38 @@
+{
+  "compilerOptions": {
+    "allowJs": true,
+    "baseUrl": ".",            // <--- 必须有这一行
+    "paths": {
+      "@/*": ["./src/*"]       // <--- 必须有这一行
+    },
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "esnext"
+    ],
+    "skipLibCheck": true,
+    "strict": false,
+    "noEmit": true,
+    "incremental": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "plugins": [
+      {
+        "name": "next"
+      }
+    ]
+  },
+  "include": [
+    "next-env.d.ts",
+    ".next/types/**/*.ts",
+    "**/*.ts",
+    "**/*.tsx"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}