Huanyi пре 5 часа
родитељ
комит
7f7c7b1ab7
35 измењених фајлова са 1189 додато и 1963 уклоњено
  1. 1 1
      .env.production
  2. 42 0
      .env.test
  3. 74 2
      package-lock.json
  4. 2 0
      package.json
  5. 1 1
      src/api/archieves/customer/index.ts
  6. 6 0
      src/api/system/appSetting/types.ts
  7. 9 0
      src/api/system/oss/index.ts
  8. 25 0
      src/api/system/region/index.ts
  9. 9 0
      src/api/system/region/types.ts
  10. 13 0
      src/api/system/tenant/index.ts
  11. 125 56
      src/components/DispatchDialog/index.vue
  12. 38 0
      src/components/RegionCascader/index.vue
  13. 47 0
      src/hooks/useIntervalRefresh.ts
  14. 70 0
      src/hooks/useRegionData.ts
  15. 3 10
      src/layout/components/Navbar.vue
  16. 1 0
      src/types/components.d.ts
  17. 6 9
      src/views/archieves/customer/index.vue
  18. 3 14
      src/views/fulfiller/pool/index.vue
  19. 18 3
      src/views/login.vue
  20. 0 715
      src/views/order/dispatch/components/DispatchDialog.vue
  21. 8 57
      src/views/order/dispatch/index.vue
  22. 0 720
      src/views/order/orderList/components/DispatchDialog.vue
  23. 340 21
      src/views/order/orderList/components/OrderDetailDrawer.vue
  24. 8 133
      src/views/order/orderList/index.vue
  25. 2 8
      src/views/order/purchase/components/AddUserDialog.vue
  26. 3 3
      src/views/order/purchase/components/FeedingForm.vue
  27. 6 6
      src/views/order/purchase/components/TransportForm.vue
  28. 3 3
      src/views/order/purchase/components/WashingForm.vue
  29. 4 7
      src/views/order/purchase/index.vue
  30. 19 5
      src/views/system/store/index.vue
  31. 63 2
      src/views/system/tenant/index.vue
  32. 40 74
      src/views/system/user/index.vue
  33. 102 53
      src/views/systemConfig/app/index.vue
  34. 96 59
      src/views/systemConfig/platform/components/CustomerConfig.vue
  35. 2 1
      src/views/systemConfig/protocol/index.vue

+ 1 - 1
.env.production

@@ -39,4 +39,4 @@ VITE_APP_PLATFORM_CODE = '4pwuAzDBzUd6hekvGHHKedT4VX5WHERAXHpeztPFAzRaUsBUrD'
 VITE_APP_WEBSOCKET = false
 VITE_APP_WEBSOCKET = false
 
 
 # sse 开关
 # sse 开关
-VITE_APP_SSE = true
+VITE_APP_SSE = false

+ 42 - 0
.env.test

@@ -0,0 +1,42 @@
+# 页面标题
+VITE_APP_TITLE = 一站护萌
+VITE_APP_LOGO_TITLE = 一站护萌
+
+# 生产环境配置
+VITE_APP_ENV = 'production'
+
+# 应用访问路径 例如使用前缀 /admin/
+VITE_APP_CONTEXT_PATH = '/'
+
+# 监控地址
+VITE_APP_MONITOR_ADMIN = '/admin/applications'
+
+# SnailJob 控制台地址
+VITE_APP_SNAILJOB_ADMIN = '/snail-job'
+
+# 生产环境
+VITE_APP_BASE_API = 'http://111.228.46.254/api'
+
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip
+
+VITE_APP_PORT = 80
+
+# 接口加密功能开关(如需关闭 后端也必须对应关闭)
+VITE_APP_ENCRYPT = true
+# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
+
+# 客户端id
+VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
+
+# 平台号
+VITE_APP_PLATFORM_CODE = '4pwuAzDBzUd6hekvGHHKedT4VX5WHERAXHpeztPFAzRaUsBUrD'
+
+# websocket 开关 默认使用sse推送
+VITE_APP_WEBSOCKET = false
+
+# sse 开关
+VITE_APP_SSE = false

+ 74 - 2
package-lock.json

@@ -22,6 +22,7 @@
         "element-plus": "2.11.7",
         "element-plus": "2.11.7",
         "file-saver": "2.0.5",
         "file-saver": "2.0.5",
         "highlight.js": "11.11.1",
         "highlight.js": "11.11.1",
+        "html2canvas": "^1.4.1",
         "image-conversion": "2.1.1",
         "image-conversion": "2.1.1",
         "js-cookie": "3.0.5",
         "js-cookie": "3.0.5",
         "jsencrypt": "3.5.4",
         "jsencrypt": "3.5.4",
@@ -120,6 +121,7 @@
       "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
       "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "@babel/code-frame": "^7.29.0",
         "@babel/code-frame": "^7.29.0",
         "@babel/generator": "^7.29.0",
         "@babel/generator": "^7.29.0",
@@ -516,6 +518,7 @@
       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
       "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
       "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "@babel/types": "^7.29.0"
         "@babel/types": "^7.29.0"
       },
       },
@@ -2390,6 +2393,7 @@
       "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
       "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
       "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
       "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "@types/lodash": "*"
         "@types/lodash": "*"
       }
       }
@@ -2400,6 +2404,7 @@
       "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==",
       "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "undici-types": "~6.21.0"
         "undici-types": "~6.21.0"
       }
       }
@@ -2462,6 +2467,7 @@
       "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
       "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "@typescript-eslint/scope-manager": "8.54.0",
         "@typescript-eslint/scope-manager": "8.54.0",
         "@typescript-eslint/types": "8.54.0",
         "@typescript-eslint/types": "8.54.0",
@@ -3425,6 +3431,7 @@
       "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
       "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
       "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
       "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "@babel/parser": "^7.28.4",
         "@babel/parser": "^7.28.4",
         "@vue/compiler-core": "3.5.22",
         "@vue/compiler-core": "3.5.22",
@@ -3695,6 +3702,7 @@
       "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz",
       "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz",
       "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==",
       "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==",
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "@types/web-bluetooth": "^0.0.21",
         "@types/web-bluetooth": "^0.0.21",
         "@vueuse/metadata": "13.9.0",
         "@vueuse/metadata": "13.9.0",
@@ -3747,6 +3755,7 @@
       "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
       "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "bin": {
       "bin": {
         "acorn": "bin/acorn"
         "acorn": "bin/acorn"
       },
       },
@@ -3928,6 +3937,15 @@
       "dev": true,
       "dev": true,
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/baseline-browser-mapping": {
     "node_modules/baseline-browser-mapping": {
       "version": "2.9.19",
       "version": "2.9.19",
       "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
       "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
@@ -4010,6 +4028,7 @@
         }
         }
       ],
       ],
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "baseline-browser-mapping": "^2.9.0",
         "baseline-browser-mapping": "^2.9.0",
         "caniuse-lite": "^1.0.30001759",
         "caniuse-lite": "^1.0.30001759",
@@ -4276,6 +4295,15 @@
       "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
       "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "license": "MIT",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/css-select": {
     "node_modules/css-select": {
       "version": "5.2.2",
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@@ -4983,6 +5011,7 @@
       "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
       "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@eslint-community/eslint-utils": "^4.4.0",
         "globals": "^13.24.0",
         "globals": "^13.24.0",
@@ -5647,6 +5676,7 @@
       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
       "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
       "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
       "license": "BSD-3-Clause",
       "license": "BSD-3-Clause",
+      "peer": true,
       "engines": {
       "engines": {
         "node": ">=12.0.0"
         "node": ">=12.0.0"
       }
       }
@@ -5657,6 +5687,19 @@
       "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
       "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "license": "MIT",
+      "dependencies": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/ignore": {
     "node_modules/ignore": {
       "version": "5.3.2",
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5829,6 +5872,7 @@
       "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
       "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "bin": {
       "bin": {
         "jiti": "lib/jiti-cli.mjs"
         "jiti": "lib/jiti-cli.mjs"
       }
       }
@@ -6033,13 +6077,15 @@
       "version": "4.17.23",
       "version": "4.17.23",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
       "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
       "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
-      "license": "MIT"
+      "license": "MIT",
+      "peer": true
     },
     },
     "node_modules/lodash-es": {
     "node_modules/lodash-es": {
       "version": "4.17.23",
       "version": "4.17.23",
       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
       "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
       "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
-      "license": "MIT"
+      "license": "MIT",
+      "peer": true
     },
     },
     "node_modules/lodash-unified": {
     "node_modules/lodash-unified": {
       "version": "1.0.3",
       "version": "1.0.3",
@@ -6562,6 +6608,7 @@
         }
         }
       ],
       ],
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "nanoid": "^3.3.11",
         "nanoid": "^3.3.11",
         "picocolors": "^1.1.1",
         "picocolors": "^1.1.1",
@@ -6608,6 +6655,7 @@
       "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
       "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "bin": {
       "bin": {
         "prettier": "bin/prettier.cjs"
         "prettier": "bin/prettier.cjs"
       },
       },
@@ -6860,6 +6908,7 @@
       "integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==",
       "integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "chokidar": "^4.0.0",
         "chokidar": "^4.0.0",
         "immutable": "^5.0.2",
         "immutable": "^5.0.2",
@@ -7113,6 +7162,15 @@
         "url": "https://opencollective.com/synckit"
         "url": "https://opencollective.com/synckit"
       }
       }
     },
     },
+    "node_modules/text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "license": "MIT",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/tinybench": {
     "node_modules/tinybench": {
       "version": "2.9.0",
       "version": "2.9.0",
       "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
       "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -7171,6 +7229,7 @@
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "engines": {
       "engines": {
         "node": ">=12"
         "node": ">=12"
       },
       },
@@ -7282,6 +7341,7 @@
       "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
       "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
       "devOptional": true,
       "devOptional": true,
       "license": "Apache-2.0",
       "license": "Apache-2.0",
+      "peer": true,
       "bin": {
       "bin": {
         "tsc": "bin/tsc",
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
         "tsserver": "bin/tsserver"
@@ -7884,12 +7944,22 @@
       "dev": true,
       "dev": true,
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "license": "MIT",
+      "dependencies": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
     "node_modules/vite": {
     "node_modules/vite": {
       "version": "6.4.1",
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
       "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
       "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
       "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "esbuild": "^0.25.0",
         "esbuild": "^0.25.0",
         "fdir": "^6.4.4",
         "fdir": "^6.4.4",
@@ -8199,6 +8269,7 @@
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "engines": {
       "engines": {
         "node": ">=12"
         "node": ">=12"
       },
       },
@@ -8311,6 +8382,7 @@
       "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
       "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
       "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
       "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
       "license": "MIT",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
       "dependencies": {
         "@vue/compiler-dom": "3.5.22",
         "@vue/compiler-dom": "3.5.22",
         "@vue/compiler-sfc": "3.5.22",
         "@vue/compiler-sfc": "3.5.22",

+ 2 - 0
package.json

@@ -10,6 +10,7 @@
     "dev": "vite serve --mode development",
     "dev": "vite serve --mode development",
     "prod": "vite serve --mode production",
     "prod": "vite serve --mode production",
     "build:prod": "vite build --mode production",
     "build:prod": "vite build --mode production",
+    "build:test": "vite build --mode test",
     "build:dev": "vite build --mode development",
     "build:dev": "vite build --mode development",
     "preview": "vite preview",
     "preview": "vite preview",
     "lint:eslint": "eslint",
     "lint:eslint": "eslint",
@@ -27,6 +28,7 @@
     "crypto-js": "4.2.0",
     "crypto-js": "4.2.0",
     "echarts": "5.6.0",
     "echarts": "5.6.0",
     "element-china-area-data": "^6.1.0",
     "element-china-area-data": "^6.1.0",
+    "html2canvas": "^1.4.1",
     "element-plus": "2.11.7",
     "element-plus": "2.11.7",
     "file-saver": "2.0.5",
     "file-saver": "2.0.5",
     "highlight.js": "11.11.1",
     "highlight.js": "11.11.1",

+ 1 - 1
src/api/archieves/customer/index.ts

@@ -84,7 +84,7 @@ export const changeCustomerStatus = (id: string | number, status: number) => {
   return request({
   return request({
     url: '/archieves/customer/changeStatus',
     url: '/archieves/customer/changeStatus',
     method: 'put',
     method: 'put',
-    params: { id, status }
+    data: { id, status }
   });
   });
 };
 };
 
 

+ 6 - 0
src/api/system/appSetting/types.ts

@@ -12,6 +12,10 @@ export interface AppSettingVO {
   loginBackground: number | string;
   loginBackground: number | string;
   /** 登录页背景URL */
   /** 登录页背景URL */
   loginBackgroundUrl?: string;
   loginBackgroundUrl?: string;
+  /** 首页轮播图OSS ID,多个用逗号分隔 */
+  homeBanner?: string;
+  /** 首页轮播图URL,多个用逗号分隔 */
+  homeBannerUrls?: string;
 }
 }
 
 
 /**
 /**
@@ -24,4 +28,6 @@ export interface AppSettingForm {
   loginIcon: number | string;
   loginIcon: number | string;
   /** 登录页背景ID */
   /** 登录页背景ID */
   loginBackground: number | string;
   loginBackground: number | string;
+  /** 首页轮播图OSS ID,多个用逗号分隔 */
+  homeBanner?: string;
 }
 }

+ 9 - 0
src/api/system/oss/index.ts

@@ -19,6 +19,15 @@ export function listByIds(ossId: string | number): AxiosPromise<OssVO[]> {
   });
   });
 }
 }
 
 
+// 通过后端代理下载OSS文件为Blob @Author: Antigravity
+export function downloadOssBlob(ossId: string | number): Promise<Blob> {
+  return request({
+    url: '/resource/oss/download/' + ossId,
+    method: 'get',
+    responseType: 'blob'
+  });
+}
+
 // 删除OSS对象存储
 // 删除OSS对象存储
 export function delOss(ossId: string | number | Array<string | number>) {
 export function delOss(ossId: string | number | Array<string | number>) {
   return request({
   return request({

+ 25 - 0
src/api/system/region/index.ts

@@ -0,0 +1,25 @@
+import request from '@/utils/request';
+
+/**
+ * 查询地区树结构
+ * @Author: Antigravity
+ */
+export function listRegionTree() {
+  return request({
+    url: '/system/region/listTree',
+    method: 'get'
+  });
+}
+
+/**
+ * 上传地区数据接口
+ * @param data 地区数据
+ * @Author: Antigravity
+ */
+export function uploadRegion(data: any[]) {
+  return request({
+    url: '/system/region/upload',
+    method: 'post',
+    data: data
+  });
+}

+ 9 - 0
src/api/system/region/types.ts

@@ -0,0 +1,9 @@
+/**
+ * 地区数据类型定义
+ * @Author: Antigravity
+ */
+export interface RegionVO {
+  value: string;
+  label: string;
+  children?: RegionVO[];
+}

+ 13 - 0
src/api/system/tenant/index.ts

@@ -41,6 +41,19 @@ export function updateTenant(data: TenantForm) {
   });
   });
 }
 }
 
 
+// 重置租户管理员密码
+export function resetTenantAdminPwd(data: any) {
+  return request({
+    url: '/system/tenant/resetAdminPwd',
+    method: 'put',
+    headers: {
+      isEncrypt: true,
+      repeatSubmit: false
+    },
+    data: data
+  });
+}
+
 // 租户状态修改
 // 租户状态修改
 export function changeTenantStatus(id: string | number, tenantId: string | number, status: string) {
 export function changeTenantStatus(id: string | number, tenantId: string | number, status: string) {
   const data = {
   const data = {

+ 125 - 56
src/components/DispatchDialog/index.vue

@@ -2,38 +2,46 @@
     <el-dialog v-model="dialogVisible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
     <el-dialog v-model="dialogVisible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
         <div class="dispatch-dialog-content">
         <div class="dispatch-dialog-content">
             <!-- Top: Order Info (OrderDispatch Style) -->
             <!-- Top: Order Info (OrderDispatch Style) -->
-            <div class="dispatch-order-info" v-if="order">
+            <div class="dispatch-order-info" v-if="localOrder">
                 <div class="list-card order-card" style="margin:0; box-shadow:none; cursor:default; border:none;">
                 <div class="list-card order-card" style="margin:0; box-shadow:none; cursor:default; border:none;">
                     <div class="card-left">
                     <div class="card-left">
-                        <div class="type-tag" :class="order.typeCode">
-                            {{ getShortType(order.typeCode) }}
+                        <div class="type-tag" :class="localOrder.typeCode">
+                            {{ getShortType(localOrder.typeCode) }}
                         </div>
                         </div>
                     </div>
                     </div>
                     <div class="card-main">
                     <div class="card-main">
-                        <template v-if="order.typeCode === 'transport'">
-                            <div class="row-addr" :title="order.pickAddr">
-                                <span class="tag pick">取</span> {{ order.pickAddr }}
+                        <template v-if="localOrder.typeCode === 'transport'">
+                            <div class="row-addr" :title="localOrder.pickAddr">
+                                <span class="tag pick">取</span> {{ localOrder.pickAddr }}
                             </div>
                             </div>
-                            <div class="row-addr" :title="order.dropAddr">
-                                <span class="tag drop">送</span> {{ order.dropAddr }}
+                            <div class="row-addr" :title="localOrder.dropAddr">
+                                <span class="tag drop">送</span> {{ localOrder.dropAddr }}
                             </div>
                             </div>
                         </template>
                         </template>
                         <template v-else>
                         <template v-else>
-                            <div class="row-addr" :title="order.address">
-                                <span class="tag home">址</span> {{ order.address }}
+                            <div class="row-addr" :title="localOrder.address">
+                                <span class="tag home">址</span> {{ localOrder.address }}
                             </div>
                             </div>
                         </template>
                         </template>
                         <div class="row-time" style="margin-top: 4px;">
                         <div class="row-time" style="margin-top: 4px;">
                             <el-icon>
                             <el-icon>
                                 <Clock />
                                 <Clock />
-                            </el-icon> {{ order.time }}
-                            <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
+                            </el-icon> {{ localOrder.time }}
+                            <span class="days-tag" v-if="localOrder.daysLater">{{ localOrder.daysLater }}</span>
+                        </div>
+                        <div class="row-remark" style="margin-top: 4px; font-size: 13px; color: #606266;">
+                            备注: {{ localOrder.remark || '-' }}
                         </div>
                         </div>
                     </div>
                     </div>
                     <!-- 新增右侧按钮组 -->
                     <!-- 新增右侧按钮组 -->
-                    <div class="card-right" style="display: flex; align-items: center; gap: 10px; padding-left: 20px;">
-                        <el-button type="primary" size="small" plain round icon="User" @click="openCustomerDetail" :loading="orderInfoLoading">用户档案</el-button>
-                        <el-button type="success" size="small" plain round @click="openPetDetail" :loading="orderInfoLoading" style="margin-left: 0;">宠物档案</el-button>
+                    <div class="card-right" style="display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 8px; padding-left: 20px;">
+                        <div style="display: flex; gap: 10px;">
+                            <el-button type="primary" size="small" plain round icon="User" @click="openCustomerDetail" :loading="orderInfoLoading">用户档案</el-button>
+                            <el-button type="success" size="small" plain round @click="openPetDetail" :loading="orderInfoLoading" style="margin-left: 0;">宠物档案</el-button>
+                        </div>
+                        <el-button type="warning" size="small" plain disabled style="width: 100%; border-color: #f3d19e; color: #e6a23c; opacity: 1; cursor: default; justify-content: center;">
+                            团购套餐: {{ localOrder.groupPurchasePackageName || '-' }}
+                        </el-button>
                     </div>
                     </div>
                 </div>
                 </div>
             </div>
             </div>
@@ -168,6 +176,8 @@ import { listAllTag } from '@/api/fulfiller/tag'
 import { getSubOrderInfo } from '@/api/order/subOrder/index'
 import { getSubOrderInfo } from '@/api/order/subOrder/index'
 import { listAllService } from '@/api/service/list/index'
 import { listAllService } from '@/api/service/list/index'
 import { listAreaStation } from '@/api/system/areaStation/index'
 import { listAreaStation } from '@/api/system/areaStation/index'
+import { dispatchSubOrder } from '@/api/order/subOrder/index'
+import { getFulfiller } from '@/api/fulfiller/fulfiller/index'
 import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
 import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
 import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 
 
@@ -175,7 +185,7 @@ const props = defineProps({
     visible: Boolean,
     visible: Boolean,
     order: Object
     order: Object
 })
 })
-const emit = defineEmits(['update:visible', 'submit'])
+const emit = defineEmits(['update:visible', 'success'])
 
 
 const { proxy } = getCurrentInstance();
 const { proxy } = getCurrentInstance();
 const { sys_user_sex } = toRefs(proxy.useDict('sys_user_sex'));
 const { sys_user_sex } = toRefs(proxy.useDict('sys_user_sex'));
@@ -204,6 +214,7 @@ const dispatchSearchQuery = ref('')
 const selectedRiderId = ref(null)
 const selectedRiderId = ref(null)
 const dispatchFee = ref(0)
 const dispatchFee = ref(0)
 const orderCommission = ref(0)
 const orderCommission = ref(0)
+const localOrder = ref(null)
 
 
 const customerDialogVisible = ref(false)
 const customerDialogVisible = ref(false)
 const petDialogVisible = ref(false)
 const petDialogVisible = ref(false)
@@ -250,7 +261,8 @@ const loadRiders = async () => {
             content: dispatchSearchQuery.value || undefined,
             content: dispatchSearchQuery.value || undefined,
             pageNum: pageNum.value,
             pageNum: pageNum.value,
             pageSize: pageSize.value,
             pageSize: pageSize.value,
-            service: props.order?.service
+            service: localOrder.value?.service,
+            orderId: localOrder.value?.id
         })
         })
         const list = res?.rows || []
         const list = res?.rows || []
         ridersList.value = list.map(r => ({
         ridersList.value = list.map(r => ({
@@ -260,8 +272,11 @@ const loadRiders = async () => {
         }))
         }))
         total.value = res?.total || 0
         total.value = res?.total || 0
 
 
-        if (props.order?.riderId) {
-            currentRider.value = ridersList.value.find(r => r.id === props.order.riderId) || null
+        if (localOrder.value?.riderId) {
+            const found = ridersList.value.find(r => r.id === localOrder.value.riderId)
+            if (found) {
+                currentRider.value = { ...found }
+            }
         }
         }
     } catch {
     } catch {
         ridersList.value = []
         ridersList.value = []
@@ -275,54 +290,101 @@ const handlePageSizeChange = (size) => {
     loadRiders()
     loadRiders()
 }
 }
 
 
-watch(() => props.visible, (val) => {
+watch(() => props.visible, async (val) => {
     if (val && props.order) {
     if (val && props.order) {
+        // 1. 初始化 localOrder,处理模型转换逻辑 @Author: Antigravity
+        const raw = { ...props.order }
+        
+        // 确保基本属性存在,如果不完整则从 props.order 推导 (统一两个页面的逻辑)
+        const isTransport = raw.mode === 1 || raw.mode === '1' || raw.type === 'transport' || raw.typeCode === 'transport'
+        if (!raw.typeCode) raw.typeCode = isTransport ? 'transport' : 'feeding'
+        
+        const toAddr = raw.toAddress || raw.address || ''
+        if (isTransport) {
+            if (!raw.pickAddr) raw.pickAddr = raw.fromAddress || toAddr
+            if (!raw.dropAddr) raw.dropAddr = toAddr
+        } else {
+            if (!raw.address) raw.address = toAddr
+        }
+        
+        if (!raw.time) raw.time = raw.serviceTime || raw.appointTime || raw.createTime
+        if (!raw.service && raw.serviceId) raw.service = raw.serviceId
+        if (!raw.riderId && raw.fulfiller) raw.riderId = raw.fulfiller
+        
+        localOrder.value = raw
+
         currentRider.value = null
         currentRider.value = null
         dispatchSearchQuery.value = ''
         dispatchSearchQuery.value = ''
         selectedRiderId.value = null
         selectedRiderId.value = null
-        // price 单位为分,转成元显示
-        dispatchFee.value = props.order?.fulfillmentCommission ? Number((props.order.fulfillmentCommission / 100).toFixed(2)) : 0
-        orderCommission.value = props.order?.orderCommission ? Number((props.order.orderCommission / 100).toFixed(2)) : 0
-        if (props.order?.riderId) {
+
+        // 金额单位转换
+        dispatchFee.value = localOrder.value?.fulfillmentCommission ? Number((localOrder.value.fulfillmentCommission / 100).toFixed(2)) : 0
+        orderCommission.value = localOrder.value?.orderCommission ? Number((localOrder.value.orderCommission / 100).toFixed(2)) : 0
+        
+        if (localOrder.value?.riderId) {
             currentRider.value = {
             currentRider.value = {
-                id: props.order.riderId,
-                gender: props.order.riderGender ?? props.order.riderSex
+                id: localOrder.value.riderId,
+                gender: localOrder.value.riderGender ?? localOrder.value.riderSex
             }
             }
         }
         }
+
         pageNum.value = 1
         pageNum.value = 1
         loadAllTags()
         loadAllTags()
-        loadServiceOptions()
+        await loadServiceOptions() // 等待加载以便后面 getServiceTypeText 准确
         loadAreaStationList()
         loadAreaStationList()
         loadRiders()
         loadRiders()
 
 
-        // 获取订单详细信息
+        // 2. 获取订单详细信息
         customerId.value = null
         customerId.value = null
         petId.value = null
         petId.value = null
         orderInfoLoading.value = true
         orderInfoLoading.value = true
-        getSubOrderInfo(props.order.id).then((res) => {
+        getSubOrderInfo(localOrder.value.id).then((res) => {
             if(res.data) {
             if(res.data) {
-                // 如果 usrCustomer / usrPet 是对象则取其 id,如果是 ID 直接取
-                customerId.value = res.data.usrCustomer?.id || res.data.usrCustomer
-                petId.value = res.data.usrPet?.id || res.data.usrPet
+                const info = res.data
+                customerId.value = info.usrCustomer?.id || info.usrCustomer
+                petId.value = info.usrPet?.id || info.usrPet
                 
                 
-                // 接到详情后,把真实的金额放进去(后端金额单位为分)
-                if (res.data.fulfillmentCommission !== undefined && res.data.fulfillmentCommission !== null) {
-                    dispatchFee.value = Number((res.data.fulfillmentCommission / 100).toFixed(2))
+                // 详情覆盖
+                if (info.fulfillmentCommission !== undefined) {
+                    dispatchFee.value = Number((info.fulfillmentCommission / 100).toFixed(2))
                 }
                 }
-                if (res.data.orderCommission !== undefined && res.data.orderCommission !== null) {
-                    orderCommission.value = Number((res.data.orderCommission / 100).toFixed(2))
+                if (info.orderCommission !== undefined) {
+                    orderCommission.value = Number((info.orderCommission / 100).toFixed(2))
+                }
+                
+                // 再次修正 typeCode
+                const detailIsTransport = info.mode === 1 || info.mode === '1'
+                const serviceName = getServiceTypeText(info.service)
+                const typeCode = detailIsTransport ? 'transport' : (serviceName?.includes('洗') ? 'washing' : 'feeding')
+                
+                localOrder.value = {
+                    ...localOrder.value,
+                    ...info,
+                    typeCode,
+                    time: info.serviceTime || localOrder.value.time,
+                    pickAddr: (detailIsTransport ? (info.fromAddress || info.pickAddr) : undefined) || localOrder.value.pickAddr,
+                    dropAddr: (detailIsTransport ? (info.toAddress || info.dropAddr) : undefined) || localOrder.value.dropAddr,
+                    address: (!detailIsTransport ? (info.address || info.toAddress) : undefined) || localOrder.value.address
                 }
                 }
 
 
-                // 如果已经有履约者且不是在列表中找到的,从详情中补全性别
-                if (props.order?.riderId && !currentRider.value) {
-                    currentRider.value = {
-                        id: props.order.riderId,
-                        name: res.data.fulfillerName,
-                        gender: res.data.fulfillerGender ?? res.data.fulfillerSex,
-                        status: res.data.fulfillerStatus
-                    }
-                } else if (currentRider.value && (res.data.fulfillerGender || res.data.fulfillerSex)) {
-                    currentRider.value.gender = res.data.fulfillerGender ?? res.data.fulfillerSex
+                // 显式补全当前履约者信息 (同步两个页面的显示) @Author: Antigravity
+                const fId = info.fulfiller || info.riderId || info.fulfillerId
+                if (fId) {
+                    getFulfiller(fId).then(riderRes => {
+                        if (riderRes.data) {
+                            const r = riderRes.data
+                            currentRider.value = {
+                                id: fId,
+                                name: r.name || info.fulfillerName,
+                                phone: r.phone || info.fulfillerPhone,
+                                avatar: r.avatar || info.fulfillerAvatar,
+                                gender: r.gender ?? r.sex ?? (info.fulfillerGender ?? info.fulfillerSex),
+                                status: r.status || info.fulfillerStatus,
+                                serviceTypes: r.serviceTypes || info.fulfillerServiceTypes,
+                                nextOrderTime: r.nextOrderTime || '-'
+                            }
+                        }
+                    })
                 }
                 }
             }
             }
         }).catch((e) => {
         }).catch((e) => {
@@ -397,7 +459,7 @@ const canSubmit = computed(() => {
     return !!selectedRiderId.value && !!dispatchFee.value
     return !!selectedRiderId.value && !!dispatchFee.value
 })
 })
 
 
-const handleDispatchSubmit = () => {
+const handleDispatchSubmit = async () => {
     if (!selectedRiderId.value) {
     if (!selectedRiderId.value) {
         ElMessage.warning('请选择履约者')
         ElMessage.warning('请选择履约者')
         return
         return
@@ -406,14 +468,21 @@ const handleDispatchSubmit = () => {
         ElMessage.warning('请输入服务费用')
         ElMessage.warning('请输入服务费用')
         return
         return
     }
     }
-    const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
-    emit('submit', {
-        riderId: rider.id,
-        riderName: rider.name,
-        fee: dispatchFee.value,
-        orderCommission: orderCommission.value
-    })
-    dialogVisible.value = false
+    
+    try {
+        const payload = {
+            orderId: localOrder.value.id,
+            fulfiller: selectedRiderId.value,
+            fulfillmentCommission: Math.round(dispatchFee.value * 100),
+            orderCommission: Math.round(orderCommission.value * 100)
+        }
+        await dispatchSubOrder(payload)
+        ElMessage.success('派单成功')
+        emit('success')
+        dialogVisible.value = false
+    } catch(e) {
+        // 请求拦截器已处理异常,如需特殊处理可在此增加
+    }
 }
 }
 </script>
 </script>
 
 

+ 38 - 0
src/components/RegionCascader/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <el-cascader
+    :model-value="modelValue"
+    :options="regionOptions"
+    :placeholder="placeholder"
+    :style="cascaderStyle"
+    :clearable="clearable"
+    v-bind="$attrs"
+    @update:model-value="$emit('update:modelValue', $event)"
+    @change="$emit('change', $event)"
+  />
+</template>
+
+<script setup lang="ts">
+/**
+ * 地区级联选择器公共组件 - 使用后端接口数据替代 element-china-area-data
+ * @Author: Antigravity
+ */
+import { onMounted } from 'vue'
+import { useRegionData } from '@/hooks/useRegionData'
+
+defineOptions({ name: 'RegionCascader', inheritAttrs: false })
+
+defineProps({
+  modelValue: { type: Array, default: () => [] },
+  placeholder: { type: String, default: '请选择省/市/区' },
+  cascaderStyle: { type: String, default: 'width: 100%' },
+  clearable: { type: Boolean, default: true }
+})
+
+defineEmits(['update:modelValue', 'change'])
+
+const { regionOptions, loadRegionData } = useRegionData()
+
+onMounted(() => {
+  loadRegionData()
+})
+</script>

+ 47 - 0
src/hooks/useIntervalRefresh.ts

@@ -0,0 +1,47 @@
+/**
+ * @Author: Antigravity
+ * 统一定时刷新 Hook
+ * 用于页面数据的静默轮询更新
+ */
+import { onMounted, onUnmounted } from 'vue';
+
+/**
+ * @param callback 刷新回调函数
+ * @param interval 轮询时间间隔(ms),默认为 5000ms
+ * @param immediate 是否在挂载时立即执行一次回调,默认为 false (因为通常 onMounted 已经调用过一次了)
+ */
+export function useIntervalRefresh(callback: () => void, interval = 5000, immediate = false) {
+  let timer: any = null;
+
+  const start = () => {
+    stop();
+    if (immediate) {
+      callback();
+    }
+    timer = setInterval(() => {
+      callback();
+    }, interval);
+    console.log(`[IntervalRefresh] 已启动,频率: ${interval}ms`);
+  };
+
+  const stop = () => {
+    if (timer) {
+      clearInterval(timer);
+      timer = null;
+      console.log('[IntervalRefresh] 已停止');
+    }
+  };
+
+  onMounted(() => {
+    start();
+  });
+
+  onUnmounted(() => {
+    stop();
+  });
+
+  return {
+    start,
+    stop
+  };
+}

+ 70 - 0
src/hooks/useRegionData.ts

@@ -0,0 +1,70 @@
+import { ref } from 'vue'
+import { listRegionTree } from '@/api/system/region'
+
+/**
+ * 地区数据组合式函数 - 全局缓存,替代 element-china-area-data
+ * @Author: Antigravity
+ */
+
+// 全局缓存,避免重复请求
+const regionOptions = ref<any[]>([])
+const regionMap = ref<Record<string, string>>({})
+let loaded = false
+let loading: Promise<void> | null = null
+
+/** 递归转换后端树数据为 el-cascader 所需格式 { value, label, children } */
+function transformTree(nodes: any[]): any[] {
+  if (!nodes || nodes.length === 0) return []
+  return nodes.map(node => {
+    const item: any = { value: node.code, label: node.name }
+    if (node.children && node.children.length > 0) {
+      item.children = transformTree(node.children)
+    }
+    return item
+  })
+}
+
+/** 递归构建 code -> name 映射 */
+function buildMap(nodes: any[], map: Record<string, string>) {
+  for (const node of nodes) {
+    if (node.code && node.name) {
+      map[node.code] = node.name
+    }
+    if (node.children && node.children.length > 0) {
+      buildMap(node.children, map)
+    }
+  }
+}
+
+export function useRegionData() {
+  /** 加载地区数据(自动缓存,多次调用不重复请求) */
+  const loadRegionData = () => {
+    if (loaded) return Promise.resolve()
+    if (loading) return loading
+    loading = listRegionTree().then((res: any) => {
+      const data = res.data || res || []
+      regionOptions.value = transformTree(data)
+      const map: Record<string, string> = {}
+      buildMap(data, map)
+      regionMap.value = map
+      loaded = true
+    }).catch((err: any) => {
+      console.error('获取地区树异常:', err)
+    }).finally(() => {
+      loading = null
+    })
+    return loading
+  }
+
+  /** 根据 code 获取地区名称(替代 codeToText) */
+  const codeToName = (code: string): string => {
+    return regionMap.value[code] || ''
+  }
+
+  /** 将 code 数组转换为名称字符串(如 "北京 北京市 东城区") */
+  const codesToText = (codes: string[], separator = ''): string => {
+    return codes.map(c => regionMap.value[c] || '').filter(Boolean).join(separator)
+  }
+
+  return { regionOptions, regionMap, codeToName, codesToText, loadRegionData }
+}

+ 3 - 10
src/layout/components/Navbar.vue

@@ -95,6 +95,7 @@ import { useAppStore } from '@/store/modules/app';
 import { useUserStore } from '@/store/modules/user';
 import { useUserStore } from '@/store/modules/user';
 import { useSettingsStore } from '@/store/modules/settings';
 import { useSettingsStore } from '@/store/modules/settings';
 import { useNoticeStore } from '@/store/modules/notice';
 import { useNoticeStore } from '@/store/modules/notice';
+import { useIntervalRefresh } from '@/hooks/useIntervalRefresh';
 import { getTenantList } from '@/api/login';
 import { getTenantList } from '@/api/login';
 import { dynamicClear, dynamicTenant } from '@/api/system/tenant';
 import { dynamicClear, dynamicTenant } from '@/api/system/tenant';
 import { TenantVO } from '@/api/types';
 import { TenantVO } from '@/api/types';
@@ -149,20 +150,12 @@ const fetchUnreadCount = async () => {
   }
   }
 };
 };
 
 
-let pollingTimer: any = null;
-
 onMounted(() => {
 onMounted(() => {
   fetchUnreadCount();
   fetchUnreadCount();
-  // 每10秒轮询一次后台获取未读消息,减少视觉频率偏差带来的困惑
-  pollingTimer = setInterval(fetchUnreadCount, 5000);
 });
 });
 
 
-onUnmounted(() => {
-  if (pollingTimer) {
-    clearInterval(pollingTimer);
-    pollingTimer = null;
-  }
-});
+// 统一消息轮询 @Author: Antigravity
+useIntervalRefresh(fetchUnreadCount, 5000);
 
 
 // 动态切换
 // 动态切换
 const dynamicTenantEvent = async (tenantId: string) => {
 const dynamicTenantEvent = async (tenantId: string) => {

+ 1 - 0
src/types/components.d.ts

@@ -95,6 +95,7 @@ declare module 'vue' {
     PermiSelect: typeof import('./../components/PermiSelect/index.vue')['default']
     PermiSelect: typeof import('./../components/PermiSelect/index.vue')['default']
     PetDetailDrawer: typeof import('./../components/PetDetailDrawer/index.vue')['default']
     PetDetailDrawer: typeof import('./../components/PetDetailDrawer/index.vue')['default']
     ProcessMeddle: typeof import('./../components/Process/processMeddle.vue')['default']
     ProcessMeddle: typeof import('./../components/Process/processMeddle.vue')['default']
+    RegionCascader: typeof import('./../components/RegionCascader/index.vue')['default']
     RightToolbar: typeof import('./../components/RightToolbar/index.vue')['default']
     RightToolbar: typeof import('./../components/RightToolbar/index.vue')['default']
     RoleSelect: typeof import('./../components/RoleSelect/index.vue')['default']
     RoleSelect: typeof import('./../components/RoleSelect/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterLink: typeof import('vue-router')['RouterLink']

+ 6 - 9
src/views/archieves/customer/index.vue

@@ -36,7 +36,7 @@
         </el-table-column>
         </el-table-column>
         <el-table-column label="住址" show-overflow-tooltip min-width="150">
         <el-table-column label="住址" show-overflow-tooltip min-width="150">
           <template #default="scope">
           <template #default="scope">
-            {{ [scope.row.regionCode ? scope.row.regionCode.split('/').map(c => codeToText[c] || '').filter(Boolean).join(' ') : '', scope.row.address].filter(Boolean).join(' ') || '-' }}
+            {{ [scope.row.regionCode ? scope.row.regionCode.split('/').map(c => codeToName(c)).filter(Boolean).join(' ') : '', scope.row.address].filter(Boolean).join(' ') || '-' }}
           </template>
           </template>
         </el-table-column>
         </el-table-column>
         <el-table-column label="用户标签" width="200">
         <el-table-column label="用户标签" width="200">
@@ -172,13 +172,7 @@
           <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
           <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
           <el-col :span="24">
           <el-col :span="24">
             <el-form-item label="所在地区">
             <el-form-item label="所在地区">
-              <el-cascader
-                v-model="regionCascaderValue"
-                :options="regionData"
-                placeholder="请选择省/市/区"
-                style="width: 100%"
-                clearable
-              />
+              <RegionCascader v-model="regionCascaderValue" />
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
           <el-col :span="24">
           <el-col :span="24">
@@ -399,11 +393,13 @@ import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
 import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
 import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
 import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 import { listAllService } from '@/api/service/list/index'
 import { listAllService } from '@/api/service/list/index'
-import { regionData, codeToText } from 'element-china-area-data'
+import RegionCascader from '@/components/RegionCascader/index.vue'
+import { useRegionData } from '@/hooks/useRegionData'
 import PageSelect from '@/components/PageSelect/index.vue'
 import PageSelect from '@/components/PageSelect/index.vue'
 import { useUserStore } from '@/store/modules/user'
 import { useUserStore } from '@/store/modules/user'
 
 
 const userStore = useUserStore()
 const userStore = useUserStore()
+const { codeToName, loadRegionData } = useRegionData()
 
 
 const { proxy } = getCurrentInstance()
 const { proxy } = getCurrentInstance()
 const { sys_user_sex, sys_customer_status, sys_house_type, sys_entry_method, sys_pet_gender, sys_pet_size, sys_pet_type, sys_pet_breed } = toRefs(
 const { sys_user_sex, sys_customer_status, sys_house_type, sys_entry_method, sys_pet_gender, sys_pet_size, sys_pet_type, sys_pet_breed } = toRefs(
@@ -978,6 +974,7 @@ onMounted(() => {
   getList()
   getList()
   loadTags()
   loadTags()
   loadAreaStation()
   loadAreaStation()
+  loadRegionData()
   getBrandList()
   getBrandList()
   getServiceList()
   getServiceList()
 })
 })

+ 3 - 14
src/views/fulfiller/pool/index.vue

@@ -461,9 +461,6 @@
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
         </el-row>
         </el-row>
-        <el-form-item label="登录密码">
-          <el-input v-model="editDialog.form.password" type="password" placeholder="不修改请留空" show-password />
-        </el-form-item>
         <el-row :gutter="20">
         <el-row :gutter="20">
           <el-col :span="12">
           <el-col :span="12">
             <el-form-item label="性别">
             <el-form-item label="性别">
@@ -716,6 +713,7 @@ import fulfillerEnums from '@/json/fulfiller.json'
 import ImageUpload from '@/components/ImageUpload/index.vue'
 import ImageUpload from '@/components/ImageUpload/index.vue'
 import { listAllLevelRights, addLevelRights, updateLevelRights, delLevelRights, changeLevelRightsStatus } from '@/api/fulfiller/levelRights';
 import { listAllLevelRights, addLevelRights, updateLevelRights, delLevelRights, changeLevelRightsStatus } from '@/api/fulfiller/levelRights';
 import { listAllLevelConfig, addLevelConfig, updateLevelConfig, delLevelConfig } from '@/api/fulfiller/levelConfig';
 import { listAllLevelConfig, addLevelConfig, updateLevelConfig, delLevelConfig } from '@/api/fulfiller/levelConfig';
+import { useIntervalRefresh } from '@/hooks/useIntervalRefresh';
 
 
 // 获取全局实例,用于调用 proxy.download
 // 获取全局实例,用于调用 proxy.download
 const { proxy } = getCurrentInstance() as any
 const { proxy } = getCurrentInstance() as any
@@ -1289,24 +1287,15 @@ const handleCreateCascaderChange = (val: any[]) => {
   }
   }
 }
 }
 
 
-let timer: any = null;
-
 onMounted(() => {
 onMounted(() => {
   getList()
   getList()
   loadAllTags()
   loadAllTags()
   loadAreaStations()
   loadAreaStations()
   loadServiceOptions()
   loadServiceOptions()
-  timer = setInterval(() => {
-    getList(true);
-  }, 5000);
 })
 })
 
 
-onUnmounted(() => {
-  if (timer) {
-    clearInterval(timer);
-    timer = null;
-  }
-})
+// 统一数据轮询 @Author: Antigravity
+useIntervalRefresh(() => getList(true), 5000);
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>

+ 18 - 3
src/views/login.vue

@@ -94,16 +94,31 @@ const { t } = useI18n();
 const loginForm = ref<LoginData>({
 const loginForm = ref<LoginData>({
   platformId: 0,
   platformId: 0,
   tenantId: '000000',
   tenantId: '000000',
-  username: 'admin',
-  password: 'admin123',
+  username: '',
+  password: '',
   rememberMe: false,
   rememberMe: false,
   code: '',
   code: '',
   uuid: ''
   uuid: ''
 } as LoginData);
 } as LoginData);
 
 
+const validateUsername = (rule: any, value: any, callback: any) => {
+  if (!value) {
+    callback(new Error(t('login.rule.username.required')));
+  } else if (value !== 'admin') {
+    const phoneReg = /^1[3456789][0-9]\d{8}$/;
+    if (!phoneReg.test(value)) {
+      callback(new Error('请使用正确的账号进行登录'));
+    } else {
+      callback();
+    }
+  } else {
+    callback();
+  }
+};
+
 const loginRules: ElFormRules = {
 const loginRules: ElFormRules = {
   tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
   tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
-  username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }],
+  username: [{ required: true, trigger: 'blur', validator: validateUsername }],
   password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
   password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
   code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
   code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
 };
 };

+ 0 - 715
src/views/order/dispatch/components/DispatchDialog.vue

@@ -1,715 +0,0 @@
-<template>
-    <el-dialog v-model="dialogVisible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
-        <div class="dispatch-dialog-content">
-            <!-- Top: Order Info (OrderDispatch Style) -->
-            <div class="dispatch-order-info" v-if="order">
-                <div class="list-card order-card" style="margin:0; box-shadow:none; cursor:default; border:none;">
-                    <div class="card-left">
-                        <div class="type-tag" :class="order.typeCode">
-                            {{ getShortType(order.typeCode) }}
-                        </div>
-                    </div>
-                    <div class="card-main">
-                        <template v-if="order.typeCode === 'transport'">
-                            <div class="row-addr" :title="order.pickAddr">
-                                <span class="tag pick">取</span> {{ order.pickAddr }}
-                            </div>
-                            <div class="row-addr" :title="order.dropAddr">
-                                <span class="tag drop">送</span> {{ order.dropAddr }}
-                            </div>
-                        </template>
-                        <template v-else>
-                            <div class="row-addr" :title="order.address">
-                                <span class="tag home">址</span> {{ order.address }}
-                            </div>
-                        </template>
-                        <div class="row-time" style="margin-top: 4px;">
-                            <el-icon>
-                                <Clock />
-                            </el-icon> {{ order.time }}
-                            <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
-                        </div>
-                        <div class="row-remark" style="margin-top: 4px; font-size: 13px; color: #909399;">
-                            备注: {{ orderDetail?.remark || '-' }}
-                        </div>
-                    </div>
-                    <!-- 新增右侧按钮组 -->
-                    <div class="card-right" style="display: flex; flex-direction: column; gap: 8px; justify-content: center; padding-left: 20px;">
-                        <div style="display: flex; gap: 10px;">
-                            <el-button type="primary" size="small" plain round icon="User" @click="openCustomerDetail" :loading="orderInfoLoading">用户档案</el-button>
-                            <el-button type="success" size="small" plain round @click="openPetDetail" :loading="orderInfoLoading" style="margin-left: 0;">宠物档案</el-button>
-                        </div>
-                        <div style="font-size: 12px; color: #ff9900; background: #fff8e6; padding: 2px 6px; border-radius: 4px; border: 1px solid #ffd591; text-align: center;">
-                            团购套餐: {{ orderDetail?.groupPurchasePackageName || '-' }}
-                        </div>
-                    </div>
-                </div>
-            </div>
-
-            <!-- Current Rider Info (If Exists) -->
-            <div class="current-rider-section" v-if="currentRider">
-                <div class="select-header" style="margin-bottom:8px;">
-                    <span class="tit">当前派单履约者</span>
-                </div>
-                <div class="list-card rider-card"
-                    style="margin-bottom: 20px; border: 1px solid #e4e7ed; background:#fafafa; cursor:default;">
-                    <div class="card-left relative">
-                        <el-avatar :src="currentRider.avatar" :size="40" />
-                    </div>
-                    <div class="card-main">
-                        <div class="row-1"
-                            style="justify-content: space-between; align-items: flex-start; display: flex;">
-                            <div style="display:flex; align-items:baseline; gap:8px;">
-                                <span class="r-name">{{ currentRider.name || '--' }}</span>
-                                <span class="r-phone">{{ currentRider.phone || '--' }}</span>
-                                <dict-tag :options="sys_user_sex" :value="currentRider.gender" />
-                                <el-tag v-if="currentRider.status" size="small" :type="getStatusType(currentRider.status)" effect="plain">
-                                    {{ getStatusText(currentRider.status) }}
-                                </el-tag>
-                            </div>
-                        </div>
-
-                        <div class="row-2 categories-row"
-                            style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
-                            <el-tag v-for="typeId in (currentRider.serviceTypes ? String(currentRider.serviceTypes).split(',') : [])" :key="typeId" size="small"
-                                type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
-                        </div>
-                        <div class="row-3 time-row" style="margin-top: 4px;">
-                            <span class="last-time">下一单: {{ currentRider.nextOrderTime || '-' }}</span>
-                        </div>
-                    </div>
-                </div>
-            </div>
-
-            <!-- Middle: Rider Selection -->
-            <div class="dispatch-rider-select">
-                <div class="select-header">
-                    <span class="tit">选择履约者</span>
-                    <el-input v-model="dispatchSearchQuery" placeholder="搜索履约者姓名/手机号" prefix-icon="Search" clearable
-                        style="width: 240px" />
-                </div>
-
-                <div class="rider-grid-wrapper">
-                    <el-scrollbar class="rider-scroll">
-                        <div class="rider-grid">
-                            <div v-for="rider in filteredDispatchRiders" :key="rider.id"
-                                class="list-card rider-card select-card"
-                                :class="{ active: selectedRiderId === rider.id }" @click="selectedRiderId = rider.id">
-                                <!-- Reusing Rider Card Layout -->
-                                <div class="card-left relative">
-                                    <el-avatar :src="rider.avatar" :size="40" />
-                                </div>
-                                <div class="card-main">
-                                    <div class="row-1" style="justify-content: space-between; align-items: flex-start; display: flex;">
-                                        <div style="display:flex; align-items:baseline; gap:8px;">
-                                            <span class="r-name">{{ rider.name || '--' }}</span>
-                                            <span class="r-phone">{{ rider.phone || '--' }}</span>
-                                            <dict-tag :options="sys_user_sex" :value="rider.gender" />
-                                        </div>
-                                        <el-tag v-if="rider.status" size="small" :type="getStatusType(rider.status)" effect="plain">
-                                            {{ getStatusText(rider.status) }}
-                                        </el-tag>
-                                    </div>
-
-                                    <div class="row-2 categories-row"
-                                        style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
-                                        <el-tag v-for="typeId in (rider.serviceTypes ? String(rider.serviceTypes).split(',') : [])" :key="typeId" size="small"
-                                            type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
-                                    </div>
-                                    <div class="row-3 time-row" style="margin-top: 4px">
-                                        <span class="last-time">下一单: {{ rider.nextOrderTime || '-' }}</span>
-                                    </div>
-                                </div>
-
-                                <!-- Selected Check -->
-                                <div class="selected-mark" v-if="selectedRiderId === rider.id">
-                                    <el-icon>
-                                        <Check />
-                                    </el-icon>
-                                </div>
-                            </div>
-
-                            <div v-if="filteredDispatchRiders.length === 0" class="empty-text">暂无符合条件的履约者</div>
-                        </div>
-                    </el-scrollbar>
-                </div>
-
-                <div class="rider-pagination" style="margin-top: 20px;">
-                    <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize"
-                        :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" :total="total"
-                        @current-change="loadRiders" @size-change="handlePageSizeChange" />
-                </div>
-            </div>
-
-            <div class="dispatch-footer">
-                <div class="fee-inputs" style="display: flex; align-items: center;">
-                    <div class="fee-input">
-                        <span class="label">履约佣金:</span>
-                        <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入"
-                            style="width: 130px;" />
-                        <span class="unit">元</span>
-                    </div>
-                    <div class="fee-input" style="margin-left: 20px;">
-                        <span class="label">订单佣金:</span>
-                        <el-input-number v-model="orderCommission" :min="0" :precision="2" :step="10" placeholder="请输入"
-                            style="width: 130px;" />
-                        <span class="unit">元</span>
-                    </div>
-                </div>
-                <div class="btns">
-                    <el-button @click="dialogVisible = false">取消</el-button>
-                    <el-button type="primary" @click="handleDispatchSubmit">确认派单</el-button>
-                </div>
-            </div>
-        </div>
-    </el-dialog>
-
-    <CustomerDetailDrawer v-model:visible="customerDialogVisible" :customer-id="customerId" :service-list="serviceList" :area-station-list="areaStationList" />
-    <PetDetailDrawer v-model:visible="petDialogVisible" :pet-id="petId" :service-list="serviceOptions" />
-</template>
-
-<script setup>
-import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
-import { ElMessage } from 'element-plus'
-import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
-import { listAllTag } from '@/api/fulfiller/tag'
-import { getSubOrderInfo } from '@/api/order/subOrder/index'
-import { listAreaStation } from '@/api/system/areaStation/index'
-import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
-import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
-
-const props = defineProps({
-    visible: Boolean,
-    order: Object,
-    // 父组件传入的服务列表,避免重复请求接口
-    serviceList: {
-        type: Array,
-        default: () => []
-    }
-})
-const emit = defineEmits(['update:visible', 'submit'])
-
-const { proxy } = getCurrentInstance();
-const { sys_user_sex } = toRefs(proxy.useDict('sys_user_sex'));
-
-const dialogVisible = computed({
-    get: () => props.visible,
-    set: (val) => emit('update:visible', val)
-})
-
-const ridersList = ref([])
-const total = ref(0)
-const pageNum = ref(1)
-const pageSize = ref(10)
-
-const allTags = ref([])
-const tagMap = computed(() => {
-    const map = {}
-    for (const t of (allTags.value || [])) {
-        if (t && t.id !== undefined && t.id !== null) map[t.id] = t
-    }
-    return map
-})
-
-const currentRider = ref(null)
-const dispatchSearchQuery = ref('')
-const selectedRiderId = ref(null)
-const dispatchFee = ref(0)
-const orderCommission = ref(0)
-
-const customerDialogVisible = ref(false)
-const petDialogVisible = ref(false)
-const customerId = ref(null)
-const petId = ref(null)
-const orderInfoLoading = ref(false)
-const orderDetail = ref(null)
-
-const loadAllTags = async () => {
-    if (allTags.value && allTags.value.length > 0) return
-    try {
-        const res = await listAllTag({ category: 'fulfiller' })
-        allTags.value = res?.data || []
-    } catch {
-        allTags.value = []
-    }
-}
-
-// 直接使用父组件传入的服务列表数据
-const serviceOptions = computed(() => props.serviceList || [])
-
-const areaStationList = ref([])
-const loadAreaStationList = async () => {
-    if (areaStationList.value.length > 0) return
-    try {
-        const res = await listAreaStation()
-        areaStationList.value = res.data || []
-    } catch { /* ignore */ }
-}
-
-const getServiceTypeText = (id) => {
-    const s = serviceOptions.value.find(item => String(item.id) === String(id))
-    return s ? s.name : String(id)
-}
-
-const loadRiders = async () => {
-    try {
-        const res = await pageFulfillerOnOrder({
-            content: dispatchSearchQuery.value || undefined,
-            pageNum: pageNum.value,
-            pageSize: pageSize.value,
-            service: props.order?.service,
-            orderId: props.order?.id
-        })
-        const list = res?.rows || []
-        ridersList.value = list.map(r => ({
-            ...r,
-            nextOrderTime: r.nextOrderTime || '-',
-            gender: r.gender ?? r.sex
-        }))
-        total.value = res?.total || 0
-
-        if (props.order?.riderId) {
-            currentRider.value = ridersList.value.find(r => r.id === props.order.riderId) || null
-        }
-    } catch {
-        ridersList.value = []
-        total.value = 0
-    }
-}
-
-const handlePageSizeChange = (size) => {
-    pageSize.value = size
-    pageNum.value = 1
-    loadRiders()
-}
-
-watch(() => props.visible, (val) => {
-    if (val && props.order) {
-        currentRider.value = null
-        dispatchSearchQuery.value = ''
-        selectedRiderId.value = null
-        orderDetail.value = null
-        // price 单位为分,转成元显示
-        dispatchFee.value = props.order?.fulfillmentCommission ? Number((props.order.fulfillmentCommission / 100).toFixed(2)) : 0
-        orderCommission.value = props.order?.orderCommission ? Number((props.order.orderCommission / 100).toFixed(2)) : 0
-        if (props.order?.riderId) {
-            currentRider.value = {
-                id: props.order.riderId,
-                gender: props.order.riderGender ?? props.order.riderSex
-            }
-        }
-        pageNum.value = 1
-        loadAllTags()
-        loadAreaStationList()
-        loadRiders()
-
-        // 获取订单详细信息
-        customerId.value = null
-        petId.value = null
-        orderInfoLoading.value = true
-        getSubOrderInfo(props.order.id).then((res) => {
-            if(res.data) {
-                orderDetail.value = res.data;
-                // 如果 usrCustomer / usrPet 是对象则取其 id,如果是 ID 直接取
-                customerId.value = res.data.usrCustomer?.id || res.data.usrCustomer
-                petId.value = res.data.usrPet?.id || res.data.usrPet
-                
-                // 接到详情后,把真实的金额放进去(后端金额单位为分)
-                if (res.data.fulfillmentCommission !== undefined && res.data.fulfillmentCommission !== null) {
-                    dispatchFee.value = Number((res.data.fulfillmentCommission / 100).toFixed(2))
-                }
-                if (res.data.orderCommission !== undefined && res.data.orderCommission !== null) {
-                    orderCommission.value = Number((res.data.orderCommission / 100).toFixed(2))
-                }
-
-                // 如果已经有履约者且不是在列表中找到的,从详情中补全性别
-                if (props.order?.riderId && !currentRider.value) {
-                    currentRider.value = {
-                        id: props.order.riderId,
-                        name: res.data.fulfillerName,
-                        gender: res.data.fulfillerGender ?? res.data.fulfillerSex,
-                        status: res.data.fulfillerStatus
-                    }
-                } else if (currentRider.value && (res.data.fulfillerGender || res.data.fulfillerSex)) {
-                    currentRider.value.gender = res.data.fulfillerGender ?? res.data.fulfillerSex
-                }
-            }
-        }).catch((e) => {
-            console.error('获取订单详细信息失败', e)
-        }).finally(() => {
-            orderInfoLoading.value = false
-        })
-    }
-})
-
-const openCustomerDetail = () => {
-    if (!customerId.value) {
-        ElMessage.warning('未能获取到用户信息')
-        return
-    }
-    customerDialogVisible.value = true
-}
-
-const openPetDetail = () => {
-    if (!petId.value) {
-        ElMessage.warning('未能获取到宠物信息')
-        return
-    }
-    petDialogVisible.value = true
-}
-
-const getTagText = (tagId) => {
-    const t = tagMap.value?.[tagId]
-    return t?.name || String(tagId)
-}
-
-const getTagType = (tagId) => {
-    const t = tagMap.value?.[tagId]
-    const type = t?.colorType
-    if (type === 'success' || type === 'warning' || type === 'danger' || type === 'info') return type
-    return ''
-}
-
-watch(dispatchSearchQuery, () => {
-    pageNum.value = 1
-    loadRiders()
-})
-
-const getShortType = (code) => {
-    const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' }
-    return map[code] || '订单'
-}
-
-const getStatusText = (status) => {
-    const statusMap = {
-        resting: '休息',
-        busy: '接单中',
-        disabled: '禁用'
-    }
-    return statusMap[status] || status
-}
-
-const getStatusType = (status) => {
-    const typeMap = {
-        resting: 'info',
-        busy: 'success',
-        disabled: 'danger'
-    }
-    return typeMap[status] || 'info'
-}
-
-const filteredDispatchRiders = computed(() => {
-    return ridersList.value || []
-})
-
-const handleDispatchSubmit = () => {
-    if (!selectedRiderId.value) {
-        ElMessage.warning('请选择履约者')
-        return
-    }
-    if (!dispatchFee.value) {
-        ElMessage.warning('请输入服务费用')
-        return
-    }
-    const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
-    emit('submit', {
-        riderId: rider.id,
-        riderName: rider.name,
-        fee: dispatchFee.value,
-        orderCommission: orderCommission.value
-    })
-    dialogVisible.value = false
-}
-</script>
-
-<style scoped>
-/* Dispatch Dialog Styles */
-.list-card {
-    background: #fff;
-    border: 1px solid #ebeef5;
-    border-radius: 8px;
-    padding: 12px;
-    margin-bottom: 10px;
-    display: flex;
-    align-items: stretch;
-    gap: 12px;
-    transition: all 0.2s;
-    cursor: pointer;
-}
-
-.list-card:hover {
-    border-color: #c6e2ff;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-}
-
-.card-left {
-    flex-shrink: 0;
-    display: flex;
-    align-items: center;
-}
-
-.order-card .type-tag {
-    width: 40px;
-    height: 40px;
-    border-radius: 8px;
-    color: #fff;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 12px;
-    font-weight: bold;
-}
-
-.type-tag.transport {
-    background: #e6a23c;
-}
-
-.type-tag.feeding {
-    background: #67c23a;
-}
-
-.type-tag.washing {
-    background: #409eff;
-}
-
-.card-main {
-    flex: 1;
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    gap: 4px;
-}
-
-.row-addr {
-    font-size: 13px;
-    color: #303133;
-    display: flex;
-    align-items: center;
-    gap: 4px;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    line-height: 1.5;
-}
-
-.row-addr .tag {
-    font-size: 11px;
-    color: #fff;
-    padding: 1px 4px;
-    border-radius: 4px;
-    flex-shrink: 0;
-    transform: scale(0.9);
-}
-
-.tag.pick {
-    background: #409eff;
-}
-
-.tag.drop {
-    background: #e6a23c;
-}
-
-.tag.home {
-    background: #67c23a;
-}
-
-.row-time {
-    font-size: 12px;
-    color: #909399;
-    display: flex;
-    align-items: center;
-    gap: 4px;
-}
-
-.days-tag {
-    color: #f56c6c;
-    background: #fef0f0;
-    padding: 0 4px;
-    border-radius: 4px;
-    font-size: 11px;
-    border: 1px solid #fde2e2;
-    transform: scale(0.95);
-}
-
-.dispatch-order-info {
-    background: #f5f7fa;
-    padding: 10px;
-    border-radius: 4px;
-    margin-bottom: 20px;
-    border: 1px solid #e4e7ed;
-    display: block;
-}
-
-.dispatch-rider-select .select-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 10px;
-}
-
-.dispatch-rider-select .tit {
-    font-weight: bold;
-    font-size: 14px;
-}
-
-.rider-scroll {
-    height: 320px;
-}
-
-.rider-grid {
-    display: grid;
-    grid-template-columns: repeat(2, 1fr);
-    gap: 12px;
-    padding-right: 10px;
-}
-
-.rider-pagination {
-    margin-top: 10px;
-}
-
-.rider-card.select-card {
-    cursor: pointer;
-    border: 1px solid #dcdfe6;
-    position: relative;
-    transition: all 0.2s;
-    margin-bottom: 0;
-}
-
-.rider-card.select-card:hover {
-    border-color: #409eff;
-}
-
-.rider-card.select-card.active {
-    border-color: #409eff;
-    background-color: #ecf5ff;
-}
-
-.selected-mark {
-    position: absolute;
-    top: 0;
-    right: 0;
-    background: #409eff;
-    color: #fff;
-    border-bottom-left-radius: 6px;
-    width: 20px;
-    height: 20px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 12px;
-}
-
-.rider-card .card-left .dot {
-    position: absolute;
-    bottom: 0;
-    right: 0;
-    width: 10px;
-    height: 10px;
-    border-radius: 50%;
-    border: 2px solid #fff;
-}
-
-.dot.online {
-    background: #67c23a;
-}
-
-.dot.busy {
-    background: #409eff;
-}
-
-.dot.offline {
-    background: #909399;
-}
-
-.r-name {
-    font-weight: bold;
-    font-size: 14px;
-    color: #303133;
-    margin-right: 8px;
-}
-
-.r-phone {
-    font-size: 12px;
-    color: #909399;
-}
-
-.status-badge {
-    font-size: 11px;
-    padding: 2px 6px;
-    border-radius: 4px;
-    display: inline-block;
-    font-weight: bold;
-}
-
-.status-badge.online {
-    background: #f0f9eb;
-    color: #67c23a;
-}
-
-.status-badge.busy {
-    background: #ecf5ff;
-    color: #409eff;
-}
-
-.status-badge.offline {
-    background: #f4f4f5;
-    color: #909399;
-}
-
-.cat-tag {
-    background: #f4f4f5;
-    color: #909399;
-    font-size: 10px;
-    padding: 1px 4px;
-    border-radius: 2px;
-    margin-right: 4px;
-}
-
-.cat-tag.cat-transport {
-    background: #e6f7ff;
-    color: #1890ff;
-    border: 1px solid #91d5ff;
-}
-
-.cat-tag.cat-feeding {
-    background: #f6ffed;
-    color: #52c41a;
-    border: 1px solid #b7eb8f;
-}
-
-.cat-tag.cat-washing {
-    background: #fff0f6;
-    color: #eb2f96;
-    border: 1px solid #ffadd2;
-}
-
-.last-time {
-    font-size: 11px;
-    color: #999;
-}
-
-.empty-text {
-    text-align: center;
-    color: #909399;
-    padding: 20px;
-    width: 100%;
-    grid-column: span 2;
-}
-
-.dispatch-footer {
-    margin-top: 20px;
-    padding-top: 20px;
-    border-top: 1px solid #ebeef5;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-}
-
-.dispatch-footer .fee-input {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    font-size: 14px;
-}
-</style>

+ 8 - 57
src/views/order/dispatch/index.vue

@@ -76,12 +76,12 @@
     <RiderOrdersDialog v-model="riderOrdersVisible" :riderInfo="currentRiderInfo" :orders="currentRiderOrders" />
     <RiderOrdersDialog v-model="riderOrdersVisible" :riderInfo="currentRiderInfo" :orders="currentRiderOrders" />
 
 
     <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder"
     <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder"
-      :service-list="serviceOnOrderList" @submit="handleDispatchSubmit" />
+      @success="getOrdersList" />
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, computed, reactive, onMounted, watch, getCurrentInstance, ComponentInternalInstance } from 'vue';
+import { ref, computed, reactive, onMounted, onUnmounted, watch, getCurrentInstance, ComponentInternalInstance } from 'vue';
 import { checkPermi } from "@/utils/permission";
 import { checkPermi } from "@/utils/permission";
 import { ElMessage } from 'element-plus';
 import { ElMessage } from 'element-plus';
 import { listAllService } from '@/api/service/list/index'
 import { listAllService } from '@/api/service/list/index'
@@ -89,7 +89,7 @@ import { listAreaStation } from '@/api/system/areaStation'
 import { getMapSetting } from '@/api/system/mapSetting';
 import { getMapSetting } from '@/api/system/mapSetting';
 import { CircleCloseFilled } from '@element-plus/icons-vue';
 import { CircleCloseFilled } from '@element-plus/icons-vue';
 import { listStoreOnDispatch } from '@/api/system/store/index'
 import { listStoreOnDispatch } from '@/api/system/store/index'
-import { listSubOrderOnDispatch, dispatchSubOrder } from '@/api/order/subOrder/index';
+import { listSubOrderOnDispatch } from '@/api/order/subOrder/index';
 import { listFulfillerOnDispatch } from '@/api/fulfiller/pool/index'
 import { listFulfillerOnDispatch } from '@/api/fulfiller/pool/index'
 import { getFulfillerGps } from '@/api/fulfiller/fulfiller/index'
 import { getFulfillerGps } from '@/api/fulfiller/fulfiller/index'
 
 
@@ -97,6 +97,7 @@ import OrderListPanel from './components/OrderListPanel.vue';
 import RiderListPanel from './components/RiderListPanel.vue';
 import RiderListPanel from './components/RiderListPanel.vue';
 import RiderOrdersDialog from './components/RiderOrdersDialog.vue';
 import RiderOrdersDialog from './components/RiderOrdersDialog.vue';
 import DispatchDialog from '@/components/DispatchDialog/index.vue';
 import DispatchDialog from '@/components/DispatchDialog/index.vue';
+import { useIntervalRefresh } from '@/hooks/useIntervalRefresh';
 
 
 // Mock Data
 // Mock Data
 import dispatchMockData from '@/mock/dispatch.json';
 import dispatchMockData from '@/mock/dispatch.json';
@@ -163,6 +164,9 @@ const getOrdersList = () => {
   })
   })
 }
 }
 
 
+// 统一数据轮询 @Author: Antigravity
+useIntervalRefresh(getOrdersList, 5000);
+
 const getServiceName = (serviceId) => {
 const getServiceName = (serviceId) => {
   const item = serviceOnOrderList.value.find((i) => i.id === serviceId);
   const item = serviceOnOrderList.value.find((i) => i.id === serviceId);
   return item ? item.name : '未知服务';
   return item ? item.name : '未知服务';
@@ -512,63 +516,10 @@ const currentDispatchOrder = ref(null);
 const currentRider = ref(null);
 const currentRider = ref(null);
 
 
 const openDispatchDialog = (order) => {
 const openDispatchDialog = (order) => {
-  const typeName = getServiceName(order?.service);
-  const isTransport = order?.mode === 1 || order?.mode === '1';
-  const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
-
-  const t = order?.subOrderType ?? order?.type;
-  const transportType =
-    t === 0 || t === '0' || t === 1 || t === '1'
-      ? 'round'
-      : t === 2 || t === '2'
-        ? 'pick'
-        : t === 3 || t === '3'
-          ? 'drop'
-          : order?.splitType || order?.transportType;
-
-  const toAddress = order?.toAddress || '';
-  const pickAddr = isTransport ? toAddress : '';
-  const dropAddr = isTransport ? toAddress : '';
-  const address = isTransport ? '' : toAddress;
-
-  const orderObj = {
-    id: order.id,
-    typeCode,
-    transportType,
-    time: order.serviceTime || order.appointTime || order.createTime,
-    status: order.status,
-    address,
-    pickAddr,
-    dropAddr,
-    service: order.service,
-    riderId: order.riderId || order.fulfiller || null,
-    riderGender: order.fulfillerGender,
-    fulfillmentCommission: order.fulfillmentCommission,
-    orderCommission: order.orderCommission
-  };
-  currentDispatchOrder.value = orderObj;
+  currentDispatchOrder.value = order;
   dispatchDialogVisible.value = true;
   dispatchDialogVisible.value = true;
 };
 };
 
 
-const handleDispatchSubmit = async (data) => {
-  if (!currentDispatchOrder.value) return;
-  try {
-    const fulfillmentCommissionFen = Math.round(Number(data.fee || 0) * 100);
-    const orderCommissionFen = Math.round(Number(data.orderCommission || 0) * 100);
-    await dispatchSubOrder({
-      orderId: currentDispatchOrder.value.id,
-      fulfiller: data.riderId,
-      fulfillmentCommission: fulfillmentCommissionFen,
-      orderCommission: orderCommissionFen
-    });
-    ElMessage.success('派单成功');
-    dispatchDialogVisible.value = false;
-    getOrdersList(); // 重新加载订单列表
-  } catch (error) {
-    // 错误由请求拦截器处理
-  }
-};
-
 const filteredOrders = computed(() => {
 const filteredOrders = computed(() => {
   let result = ordersList.value;
   let result = ordersList.value;
   const statusMap = {
   const statusMap = {

+ 0 - 720
src/views/order/orderList/components/DispatchDialog.vue

@@ -1,720 +0,0 @@
-<template>
-    <el-dialog v-model="dialogVisible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
-        <div class="dispatch-dialog-content">
-            <!-- Top: Order Info (OrderDispatch Style) -->
-            <div class="dispatch-order-info" v-if="order">
-                <div class="list-card order-card" style="margin:0; box-shadow:none; cursor:default; border:none;">
-                    <div class="card-left">
-                        <div class="type-tag" :class="order.typeCode">
-                            {{ getShortType(order.typeCode) }}
-                        </div>
-                    </div>
-                    <div class="card-main">
-                        <template v-if="order.typeCode === 'transport'">
-                            <div class="row-addr" :title="order.pickAddr">
-                                <span class="tag pick">取</span> {{ order.pickAddr }}
-                            </div>
-                            <div class="row-addr" :title="order.dropAddr">
-                                <span class="tag drop">送</span> {{ order.dropAddr }}
-                            </div>
-                        </template>
-                        <template v-else>
-                            <div class="row-addr" :title="order.address">
-                                <span class="tag home">址</span> {{ order.address }}
-                            </div>
-                        </template>
-                        <div class="row-time" style="margin-top: 4px;">
-                            <el-icon>
-                                <Clock />
-                            </el-icon> {{ order.time }}
-                            <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
-                        </div>
-                        <div class="row-remark" style="margin-top: 4px; font-size: 13px; color: #909399;">
-                            备注: {{ orderDetail?.remark || '-' }}
-                        </div>
-                    </div>
-                    <!-- 新增右侧按钮组 -->
-                    <div class="card-right" style="display: flex; flex-direction: column; gap: 8px; justify-content: center; padding-left: 20px;">
-                        <div style="display: flex; gap: 10px;">
-                            <el-button type="primary" size="small" plain round icon="User" @click="openCustomerDetail" :loading="orderInfoLoading">用户档案</el-button>
-                            <el-button type="success" size="small" plain round @click="openPetDetail" :loading="orderInfoLoading" style="margin-left: 0;">宠物档案</el-button>
-                        </div>
-                        <div style="font-size: 12px; color: #ff9900; background: #fff8e6; padding: 2px 6px; border-radius: 4px; border: 1px solid #ffd591; text-align: center;">
-                            团购套餐: {{ orderDetail?.groupPurchasePackageName || '-' }}
-                        </div>
-                    </div>
-                </div>
-            </div>
-
-            <!-- Current Rider Info (If Exists) -->
-            <div class="current-rider-section" v-if="currentRider">
-                <div class="select-header" style="margin-bottom:8px;">
-                    <span class="tit">当前派单履约者</span>
-                </div>
-                <div class="list-card rider-card"
-                    style="margin-bottom: 20px; border: 1px solid #e4e7ed; background:#fafafa; cursor:default;">
-                    <div class="card-left relative">
-                        <el-avatar :src="currentRider.avatar" :size="40" />
-                    </div>
-                    <div class="card-main">
-                        <div class="row-1"
-                            style="justify-content: space-between; align-items: flex-start; display: flex;">
-                            <div style="display:flex; align-items:baseline; gap:8px;">
-                                <span class="r-name">{{ currentRider.name || '--' }}</span>
-                                <span class="r-phone">{{ currentRider.phone || '--' }}</span>
-                                <dict-tag :options="sys_user_sex" :value="currentRider.gender" />
-                                <el-tag v-if="currentRider.status" size="small" :type="getStatusType(currentRider.status)" effect="plain">
-                                    {{ getStatusText(currentRider.status) }}
-                                </el-tag>
-                            </div>
-                        </div>
-
-                        <div class="row-2 categories-row"
-                            style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
-                            <el-tag v-for="typeId in (currentRider.serviceTypes ? String(currentRider.serviceTypes).split(',') : [])" :key="typeId" size="small"
-                                type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
-                        </div>
-                        <div class="row-3 time-row" style="margin-top: 4px;">
-                            <span class="last-time">下一单: {{ currentRider.nextOrderTime || '-' }}</span>
-                        </div>
-                    </div>
-                </div>
-            </div>
-
-            <!-- Middle: Rider Selection -->
-            <div class="dispatch-rider-select">
-                <div class="select-header">
-                    <span class="tit">选择履约者</span>
-                    <el-input v-model="dispatchSearchQuery" placeholder="搜索履约者姓名/手机号" prefix-icon="Search" clearable
-                        style="width: 240px" />
-                </div>
-
-                <div class="rider-grid-wrapper">
-                    <el-scrollbar class="rider-scroll">
-                        <div class="rider-grid">
-                            <div v-for="rider in filteredDispatchRiders" :key="rider.id"
-                                class="list-card rider-card select-card"
-                                :class="{ active: selectedRiderId === rider.id }" @click="selectedRiderId = rider.id">
-                                <!-- Reusing Rider Card Layout -->
-                                <div class="card-left relative">
-                                    <el-avatar :src="rider.avatar" :size="40" />
-                                </div>
-                                <div class="card-main">
-                                    <div class="row-1" style="justify-content: space-between; align-items: flex-start; display: flex;">
-                                        <div style="display:flex; align-items:baseline; gap:8px;">
-                                            <span class="r-name">{{ rider.name || '--' }}</span>
-                                            <span class="r-phone">{{ rider.phone || '--' }}</span>
-                                            <dict-tag :options="sys_user_sex" :value="rider.gender" />
-                                        </div>
-                                        <el-tag v-if="rider.status" size="small" :type="getStatusType(rider.status)" effect="plain">
-                                            {{ getStatusText(rider.status) }}
-                                        </el-tag>
-                                    </div>
-
-                                    <div class="row-2 categories-row"
-                                        style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
-                                        <el-tag v-for="typeId in (rider.serviceTypes ? String(rider.serviceTypes).split(',') : [])" :key="typeId" size="small"
-                                            type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
-                                    </div>
-                                    <div class="row-3 time-row" style="margin-top: 4px">
-                                        <span class="last-time">下一单: {{ rider.nextOrderTime || '-' }}</span>
-                                    </div>
-                                </div>
-
-                                <!-- Selected Check -->
-                                <div class="selected-mark" v-if="selectedRiderId === rider.id">
-                                    <el-icon>
-                                        <Check />
-                                    </el-icon>
-                                </div>
-                            </div>
-
-                            <div v-if="filteredDispatchRiders.length === 0" class="empty-text">暂无符合条件的履约者</div>
-                        </div>
-                    </el-scrollbar>
-                </div>
-
-                <div class="rider-pagination" style="margin-top: 20px;">
-                    <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize"
-                        :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" :total="total"
-                        @current-change="loadRiders" @size-change="handlePageSizeChange" />
-                </div>
-            </div>
-
-            <div class="dispatch-footer">
-                <div class="fee-inputs" style="display: flex; align-items: center;">
-                    <div class="fee-input">
-                        <span class="label">履约佣金:</span>
-                        <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入"
-                            style="width: 130px;" />
-                        <span class="unit">元</span>
-                    </div>
-                    <div class="fee-input" style="margin-left: 20px;">
-                        <span class="label">订单佣金:</span>
-                        <el-input-number v-model="orderCommission" :min="0" :precision="2" :step="10" placeholder="请输入"
-                            style="width: 130px;" />
-                        <span class="unit">元</span>
-                    </div>
-                </div>
-                <div class="btns">
-                    <el-button @click="dialogVisible = false">取消</el-button>
-                    <el-button type="primary" @click="handleDispatchSubmit">确认派单</el-button>
-                </div>
-            </div>
-        </div>
-    </el-dialog>
-
-    <CustomerDetailDrawer v-model:visible="customerDialogVisible" :customer-id="customerId" :area-station-list="areaStationList" />
-    <PetDetailDrawer v-model:visible="petDialogVisible" :pet-id="petId" :service-list="serviceOptions" />
-</template>
-
-<script setup>
-import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
-import { ElMessage } from 'element-plus'
-import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
-import { listAllTag } from '@/api/fulfiller/tag'
-import { getSubOrderInfo } from '@/api/order/subOrder/index'
-import { listAllService } from '@/api/service/list/index'
-import { listAreaStation } from '@/api/system/areaStation/index'
-import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
-import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
-
-const props = defineProps({
-    visible: Boolean,
-    order: Object
-})
-const emit = defineEmits(['update:visible', 'submit'])
-
-const { proxy } = getCurrentInstance();
-const { sys_user_sex } = toRefs(proxy.useDict('sys_user_sex'));
-
-const dialogVisible = computed({
-    get: () => props.visible,
-    set: (val) => emit('update:visible', val)
-})
-
-const ridersList = ref([])
-const total = ref(0)
-const pageNum = ref(1)
-const pageSize = ref(10)
-
-const allTags = ref([])
-const tagMap = computed(() => {
-    const map = {}
-    for (const t of (allTags.value || [])) {
-        if (t && t.id !== undefined && t.id !== null) map[t.id] = t
-    }
-    return map
-})
-
-const currentRider = ref(null)
-const dispatchSearchQuery = ref('')
-const selectedRiderId = ref(null)
-const dispatchFee = ref(0)
-const orderCommission = ref(0)
-
-const customerDialogVisible = ref(false)
-const petDialogVisible = ref(false)
-const customerId = ref(null)
-const petId = ref(null)
-const orderInfoLoading = ref(false)
-const orderDetail = ref(null)
-
-const loadAllTags = async () => {
-    if (allTags.value && allTags.value.length > 0) return
-    try {
-        const res = await listAllTag({ category: 'fulfiller' })
-        allTags.value = res?.data || []
-    } catch {
-        allTags.value = []
-    }
-}
-
-const serviceOptions = ref([])
-const loadServiceOptions = async () => {
-    if (serviceOptions.value.length > 0) return
-    try {
-        const res = await listAllService()
-        serviceOptions.value = res?.data || []
-    } catch { /* ignore */ }
-}
-
-const areaStationList = ref([])
-const loadAreaStationList = async () => {
-    if (areaStationList.value.length > 0) return
-    try {
-        const res = await listAreaStation()
-        areaStationList.value = res.data || []
-    } catch { /* ignore */ }
-}
-
-const getServiceTypeText = (id) => {
-    const s = serviceOptions.value.find(item => String(item.id) === String(id))
-    return s ? s.name : String(id)
-}
-
-const loadRiders = async () => {
-    try {
-        const res = await pageFulfillerOnOrder({
-            content: dispatchSearchQuery.value || undefined,
-            pageNum: pageNum.value,
-            pageSize: pageSize.value,
-            service: props.order?.service,
-            orderId: props.order?.id
-        })
-        const list = res?.rows || []
-        ridersList.value = list.map(r => ({
-            ...r,
-            nextOrderTime: r.nextOrderTime || '-',
-            gender: r.gender ?? r.sex
-        }))
-        total.value = res?.total || 0
-
-        if (props.order?.riderId) {
-            currentRider.value = ridersList.value.find(r => r.id === props.order.riderId) || null
-        }
-    } catch {
-        ridersList.value = []
-        total.value = 0
-    }
-}
-
-const handlePageSizeChange = (size) => {
-    pageSize.value = size
-    pageNum.value = 1
-    loadRiders()
-}
-
-watch(() => props.visible, (val) => {
-    if (val && props.order) {
-        currentRider.value = null
-        dispatchSearchQuery.value = ''
-        selectedRiderId.value = null
-        orderDetail.value = null
-        // price 单位为分,转成元显示
-        dispatchFee.value = props.order?.fulfillmentCommission ? Number((props.order.fulfillmentCommission / 100).toFixed(2)) : 0
-        orderCommission.value = props.order?.orderCommission ? Number((props.order.orderCommission / 100).toFixed(2)) : 0
-        if (props.order?.riderId) {
-            currentRider.value = {
-                id: props.order.riderId,
-                gender: props.order.riderGender ?? props.order.riderSex
-            }
-        }
-        pageNum.value = 1
-        loadAllTags()
-        loadServiceOptions()
-        loadAreaStationList()
-        loadRiders()
-
-        // 获取订单详细信息
-        customerId.value = null
-        petId.value = null
-        orderInfoLoading.value = true
-        getSubOrderInfo(props.order.id).then((res) => {
-            if(res.data) {
-                orderDetail.value = res.data;
-                // 如果 usrCustomer / usrPet 是对象则取其 id,如果是 ID 直接取
-                customerId.value = res.data.usrCustomer?.id || res.data.usrCustomer
-                petId.value = res.data.usrPet?.id || res.data.usrPet
-                
-                // 接到详情后,把真实的金额放进去(后端金额单位为分)
-                if (res.data.fulfillmentCommission !== undefined && res.data.fulfillmentCommission !== null) {
-                    dispatchFee.value = Number((res.data.fulfillmentCommission / 100).toFixed(2))
-                }
-                if (res.data.orderCommission !== undefined && res.data.orderCommission !== null) {
-                    orderCommission.value = Number((res.data.orderCommission / 100).toFixed(2))
-                }
-
-                // 如果已经有履约者且不是在列表中找到的,从详情中补全性别
-                if (props.order?.riderId && !currentRider.value) {
-                    currentRider.value = {
-                        id: props.order.riderId,
-                        name: res.data.fulfillerName,
-                        gender: res.data.fulfillerGender ?? res.data.fulfillerSex,
-                        status: res.data.fulfillerStatus
-                    }
-                } else if (currentRider.value && (res.data.fulfillerGender || res.data.fulfillerSex)) {
-                    currentRider.value.gender = res.data.fulfillerGender ?? res.data.fulfillerSex
-                }
-            }
-        }).catch((e) => {
-            console.error('获取订单详细信息失败', e)
-        }).finally(() => {
-            orderInfoLoading.value = false
-        })
-    }
-})
-
-const openCustomerDetail = () => {
-    if (!customerId.value) {
-        ElMessage.warning('未能获取到用户信息')
-        return
-    }
-    customerDialogVisible.value = true
-}
-
-const openPetDetail = () => {
-    if (!petId.value) {
-        ElMessage.warning('未能获取到宠物信息')
-        return
-    }
-    petDialogVisible.value = true
-}
-
-const getTagText = (tagId) => {
-    const t = tagMap.value?.[tagId]
-    return t?.name || String(tagId)
-}
-
-const getTagType = (tagId) => {
-    const t = tagMap.value?.[tagId]
-    const type = t?.colorType
-    if (type === 'success' || type === 'warning' || type === 'danger' || type === 'info') return type
-    return ''
-}
-
-watch(dispatchSearchQuery, () => {
-    pageNum.value = 1
-    loadRiders()
-})
-
-const getShortType = (code) => {
-    const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' }
-    return map[code] || '订单'
-}
-
-const getStatusText = (status) => {
-    const statusMap = {
-        resting: '休息',
-        busy: '接单中',
-        disabled: '禁用'
-    }
-    return statusMap[status] || status
-}
-
-const getStatusType = (status) => {
-    const typeMap = {
-        resting: 'info',
-        busy: 'success',
-        disabled: 'danger'
-    }
-    return typeMap[status] || 'info'
-}
-
-const filteredDispatchRiders = computed(() => {
-    return ridersList.value || []
-})
-
-const handleDispatchSubmit = () => {
-    if (!selectedRiderId.value) {
-        ElMessage.warning('请选择履约者')
-        return
-    }
-    if (!dispatchFee.value) {
-        ElMessage.warning('请输入服务费用')
-        return
-    }
-    const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
-    emit('submit', {
-        riderId: rider.id,
-        riderName: rider.name,
-        riderPhone: rider.phone,
-        fee: dispatchFee.value
-        fee: dispatchFee.value,
-        orderCommission: orderCommission.value
-    })
-    dialogVisible.value = false
-}
-</script>
-
-<style scoped>
-/* Dispatch Dialog Styles */
-.list-card {
-    background: #fff;
-    border: 1px solid #ebeef5;
-    border-radius: 8px;
-    padding: 12px;
-    margin-bottom: 10px;
-    display: flex;
-    align-items: stretch;
-    gap: 12px;
-    transition: all 0.2s;
-    cursor: pointer;
-}
-
-.list-card:hover {
-    border-color: #c6e2ff;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-}
-
-.card-left {
-    flex-shrink: 0;
-    display: flex;
-    align-items: center;
-}
-
-.order-card .type-tag {
-    width: 40px;
-    height: 40px;
-    border-radius: 8px;
-    color: #fff;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 12px;
-    font-weight: bold;
-}
-
-.type-tag.transport {
-    background: #e6a23c;
-}
-
-.type-tag.feeding {
-    background: #67c23a;
-}
-
-.type-tag.washing {
-    background: #409eff;
-}
-
-.card-main {
-    flex: 1;
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    gap: 4px;
-}
-
-.row-addr {
-    font-size: 13px;
-    color: #303133;
-    display: flex;
-    align-items: center;
-    gap: 4px;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    line-height: 1.5;
-}
-
-.row-addr .tag {
-    font-size: 11px;
-    color: #fff;
-    padding: 1px 4px;
-    border-radius: 4px;
-    flex-shrink: 0;
-    transform: scale(0.9);
-}
-
-.tag.pick {
-    background: #409eff;
-}
-
-.tag.drop {
-    background: #e6a23c;
-}
-
-.tag.home {
-    background: #67c23a;
-}
-
-.row-time {
-    font-size: 12px;
-    color: #909399;
-    display: flex;
-    align-items: center;
-    gap: 4px;
-}
-
-.days-tag {
-    color: #f56c6c;
-    background: #fef0f0;
-    padding: 0 4px;
-    border-radius: 4px;
-    font-size: 11px;
-    border: 1px solid #fde2e2;
-    transform: scale(0.95);
-}
-
-.dispatch-order-info {
-    background: #f5f7fa;
-    padding: 10px;
-    border-radius: 4px;
-    margin-bottom: 20px;
-    border: 1px solid #e4e7ed;
-    display: block;
-}
-
-.dispatch-rider-select .select-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 10px;
-}
-
-.dispatch-rider-select .tit {
-    font-weight: bold;
-    font-size: 14px;
-}
-
-.rider-scroll {
-    height: 320px;
-}
-
-.rider-grid {
-    display: grid;
-    grid-template-columns: repeat(2, 1fr);
-    gap: 12px;
-    padding-right: 10px;
-}
-
-.rider-pagination {
-    margin-top: 10px;
-}
-
-.rider-card.select-card {
-    cursor: pointer;
-    border: 1px solid #dcdfe6;
-    position: relative;
-    transition: all 0.2s;
-    margin-bottom: 0;
-}
-
-.rider-card.select-card:hover {
-    border-color: #409eff;
-}
-
-.rider-card.select-card.active {
-    border-color: #409eff;
-    background-color: #ecf5ff;
-}
-
-.selected-mark {
-    position: absolute;
-    top: 0;
-    right: 0;
-    background: #409eff;
-    color: #fff;
-    border-bottom-left-radius: 6px;
-    width: 20px;
-    height: 20px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 12px;
-}
-
-.rider-card .card-left .dot {
-    position: absolute;
-    bottom: 0;
-    right: 0;
-    width: 10px;
-    height: 10px;
-    border-radius: 50%;
-    border: 2px solid #fff;
-}
-
-.dot.online {
-    background: #67c23a;
-}
-
-.dot.busy {
-    background: #409eff;
-}
-
-.dot.offline {
-    background: #909399;
-}
-
-.r-name {
-    font-weight: bold;
-    font-size: 14px;
-    color: #303133;
-    margin-right: 8px;
-}
-
-.r-phone {
-    font-size: 12px;
-    color: #909399;
-}
-
-.status-badge {
-    font-size: 11px;
-    padding: 2px 6px;
-    border-radius: 4px;
-    display: inline-block;
-    font-weight: bold;
-}
-
-.status-badge.online {
-    background: #f0f9eb;
-    color: #67c23a;
-}
-
-.status-badge.busy {
-    background: #ecf5ff;
-    color: #409eff;
-}
-
-.status-badge.offline {
-    background: #f4f4f5;
-    color: #909399;
-}
-
-.cat-tag {
-    background: #f4f4f5;
-    color: #909399;
-    font-size: 10px;
-    padding: 1px 4px;
-    border-radius: 2px;
-    margin-right: 4px;
-}
-
-.cat-tag.cat-transport {
-    background: #e6f7ff;
-    color: #1890ff;
-    border: 1px solid #91d5ff;
-}
-
-.cat-tag.cat-feeding {
-    background: #f6ffed;
-    color: #52c41a;
-    border: 1px solid #b7eb8f;
-}
-
-.cat-tag.cat-washing {
-    background: #fff0f6;
-    color: #eb2f96;
-    border: 1px solid #ffadd2;
-}
-
-.last-time {
-    font-size: 11px;
-    color: #999;
-}
-
-.empty-text {
-    text-align: center;
-    color: #909399;
-    padding: 20px;
-    width: 100%;
-    grid-column: span 2;
-}
-
-.dispatch-footer {
-    margin-top: 20px;
-    padding-top: 20px;
-    border-top: 1px solid #ebeef5;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-}
-
-.dispatch-footer .fee-input {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    font-size: 14px;
-}
-</style>

+ 340 - 21
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -91,12 +91,13 @@
                         </div>
                         </div>
                         <el-descriptions :column="2" size="small" class="pet-desc" border>
                         <el-descriptions :column="2" size="small" class="pet-desc" border>
                             <el-descriptions-item label="宠物品种">{{ order.petBreed || '-'
                             <el-descriptions-item label="宠物品种">{{ order.petBreed || '-'
-                            }}</el-descriptions-item>
+                                }}</el-descriptions-item>
                             <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '-'
                             <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '-'
-                            }}</span></el-descriptions-item>
+                                    }}</span></el-descriptions-item>
                             <el-descriptions-item label="性格特点">{{ order.petCharacter || '-' }}</el-descriptions-item>
                             <el-descriptions-item label="性格特点">{{ order.petCharacter || '-' }}</el-descriptions-item>
                             <el-descriptions-item label="健康状况">{{ order.petHealth || '-' }}</el-descriptions-item>
                             <el-descriptions-item label="健康状况">{{ order.petHealth || '-' }}</el-descriptions-item>
-                            <el-descriptions-item label="性格特点">{{ order.petPersonality || order.petCharacter || '-' }}</el-descriptions-item>
+                            <el-descriptions-item label="性格特点">{{ order.petPersonality || order.petCharacter || '-'
+                                }}</el-descriptions-item>
                             <el-descriptions-item label="健康状况">{{ order.petHealth || '-' }}</el-descriptions-item>
                             <el-descriptions-item label="健康状况">{{ order.petHealth || '-' }}</el-descriptions-item>
                         </el-descriptions>
                         </el-descriptions>
                     </div>
                     </div>
@@ -109,7 +110,7 @@
                         <div class="user-content">
                         <div class="user-content">
                             <div class="u-row">
                             <div class="u-row">
                                 <el-avatar :size="40" :src="order.userAvatar">{{ (order.userName || '').charAt(0)
                                 <el-avatar :size="40" :src="order.userAvatar">{{ (order.userName || '').charAt(0)
-                                }}</el-avatar>
+                                    }}</el-avatar>
                                 <div class="u-info">
                                 <div class="u-info">
                                     <div class="nm">{{ order.userName }}</div>
                                     <div class="nm">{{ order.userName }}</div>
                                     <div class="ph">{{ order.contactPhone }}</div>
                                     <div class="ph">{{ order.contactPhone }}</div>
@@ -142,18 +143,19 @@
                                     <el-descriptions-item label="归属门店">{{ order.merchantName }}
                                     <el-descriptions-item label="归属门店">{{ order.merchantName }}
                                         ({{ Number(order.platformId) === 1 ? '门店下单' : '平台代下单' }})</el-descriptions-item>
                                         ({{ Number(order.platformId) === 1 ? '门店下单' : '平台代下单' }})</el-descriptions-item>
                                     <el-descriptions-item label="宠主信息">{{ order.userName }} / {{ order.contactPhone
                                     <el-descriptions-item label="宠主信息">{{ order.userName }} / {{ order.contactPhone
-                                    }}</el-descriptions-item>
+                                        }}</el-descriptions-item>
                                     <el-descriptions-item label="履约佣金" label-class-name="money-label">
                                     <el-descriptions-item label="履约佣金" label-class-name="money-label">
                                         <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.fulfillerFee }}</span>
                                         <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.fulfillerFee }}</span>
                                     </el-descriptions-item>
                                     </el-descriptions-item>
 
 
                                     <el-descriptions-item label="预约时间">{{ getServiceTimeRange(order.serviceTime)
                                     <el-descriptions-item label="预约时间">{{ getServiceTimeRange(order.serviceTime)
-                                    }}</el-descriptions-item>
+                                        }}</el-descriptions-item>
                                     <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '-'
                                     <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '-'
-                                    }}</el-descriptions-item>
+                                        }}</el-descriptions-item>
                                     <el-descriptions-item label="创建时间">{{ order.createTime }}</el-descriptions-item>
                                     <el-descriptions-item label="创建时间">{{ order.createTime }}</el-descriptions-item>
                                     <el-descriptions-item label="订单佣金" label-class-name="money-label">
                                     <el-descriptions-item label="订单佣金" label-class-name="money-label">
-                                        <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.orderCommission || 0 }}</span>
+                                        <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.orderCommission || 0
+                                            }}</span>
                                     </el-descriptions-item>
                                     </el-descriptions-item>
 
 
                                     <el-descriptions-item label="订单备注" :span="3">
                                     <el-descriptions-item label="订单备注" :span="3">
@@ -173,7 +175,7 @@
                                     <div class="t-row">
                                     <div class="t-row">
                                         <span class="t-k">起点</span>
                                         <span class="t-k">起点</span>
                                         <span class="t-v">{{ order.detail?.fromAddress || order.detail?.pickAddr || '-'
                                         <span class="t-v">{{ order.detail?.fromAddress || order.detail?.pickAddr || '-'
-                                            }}</span>
+                                        }}</span>
                                     </div>
                                     </div>
                                     <div class="t-row">
                                     <div class="t-row">
                                         <span class="t-k">终点</span>
                                         <span class="t-k">终点</span>
@@ -193,7 +195,7 @@
                                 <div class="sec-title-bar">服务执行要求</div>
                                 <div class="sec-title-bar">服务执行要求</div>
                                 <el-descriptions :column="2" border size="default" class="custom-desc">
                                 <el-descriptions :column="2" border size="default" class="custom-desc">
                                     <el-descriptions-item label="服务地址" :span="2">{{ order.detail.area || order.address
                                     <el-descriptions-item label="服务地址" :span="2">{{ order.detail.area || order.address
-                                    }}</el-descriptions-item>
+                                        }}</el-descriptions-item>
                                 </el-descriptions>
                                 </el-descriptions>
                             </div>
                             </div>
                         </div>
                         </div>
@@ -205,12 +207,14 @@
                             <div v-if="order.fulfillerName" class="fulfiller-card">
                             <div v-if="order.fulfillerName" class="fulfiller-card">
                                 <div class="f-left">
                                 <div class="f-left">
                                     <el-avatar :size="60" :src="order.fulfillerAvatar">{{ order.fulfillerName.charAt(0)
                                     <el-avatar :size="60" :src="order.fulfillerAvatar">{{ order.fulfillerName.charAt(0)
-                                    }}</el-avatar>
+                                        }}</el-avatar>
                                 </div>
                                 </div>
                                 <div class="f-right">
                                 <div class="f-right">
                                     <div class="f-row1">
                                     <div class="f-row1">
                                         <span class="f-name">{{ order.fulfillerName }}</span>
                                         <span class="f-name">{{ order.fulfillerName }}</span>
-                                        <el-tag size="small" type="primary" effect="plain" round>{{ order.fulfillerLevelName || '普通履约者' }}</el-tag>
+                                        <el-tag size="small" type="primary" effect="plain" round>{{
+                                            order.fulfillerLevelName ||
+                                            '普通履约者' }}</el-tag>
                                     </div>
                                     </div>
                                     <div class="f-row2">
                                     <div class="f-row2">
                                         <span>联系电话:{{ order.fulfillerPhone || '138****0000' }}</span>
                                         <span>联系电话:{{ order.fulfillerPhone || '138****0000' }}</span>
@@ -234,6 +238,13 @@
                     <!-- Tab 3: Service Progress -->
                     <!-- Tab 3: Service Progress -->
                     <el-tab-pane label="服务进度" name="service">
                     <el-tab-pane label="服务进度" name="service">
                         <div class="tab-pane-content">
                         <div class="tab-pane-content">
+                            <!-- 导出流程图按钮:与订单日志导出Excel位置一致 -->
+                            <div style="display: flex; justify-content: flex-end; margin-bottom: 15px;">
+                                <el-button type="primary" size="small" icon="Picture"
+                                    v-hasPermi="['order:orderList:queryExportExcel']"
+                                    @click="handleExportProgressImage">导出流程图</el-button>
+                            </div>
+
                             <div v-if="serviceProgressSteps.length === 0" class="empty-progress"
                             <div v-if="serviceProgressSteps.length === 0" class="empty-progress"
                                 style="padding:40px; text-align:center; color:#909399;">
                                 style="padding:40px; text-align:center; color:#909399;">
                                 <el-result icon="info" title="待接单" sub-title="履约者接单后将在此记录服务进度"></el-result>
                                 <el-result icon="info" title="待接单" sub-title="履约者接单后将在此记录服务进度"></el-result>
@@ -315,6 +326,23 @@
         </div>
         </div>
     </el-drawer>
     </el-drawer>
 
 
+    <!-- 流程图预览弹窗 -->
+    <el-dialog v-model="processImageVisible" title="服务流程图预览" width="620px" destroy-on-close append-to-body>
+        <div v-loading="isProcessing" element-loading-text="正在绘制流程图...">
+            <div v-if="processImageUrl" style="text-align: center;">
+                <el-image :src="processImageUrl"
+                    style="max-width: 100%; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"
+                    :preview-src-list="[processImageUrl]" :preview-teleported="true" />
+                <div style="margin-top: 20px;">
+                    <el-button type="primary" icon="Download" @click="downloadProcessImage">下载并保存图片</el-button>
+                </div>
+            </div>
+            <div v-else-if="!isProcessing" style="text-align: center; color: #909399; padding: 40px;">
+                生成图片失败,请稍后重试
+            </div>
+        </div>
+    </el-dialog>
+
     <!-- 视频播放弹窗 -->
     <!-- 视频播放弹窗 -->
     <el-dialog v-model="videoPreview.visible" title="视频播放" width="800px" append-to-body @closed="videoPreview.url = ''">
     <el-dialog v-model="videoPreview.visible" title="视频播放" width="800px" append-to-body @closed="videoPreview.url = ''">
         <div
         <div
@@ -323,17 +351,107 @@
                 style="max-width: 100%; max-height: 70vh;"></video>
                 style="max-width: 100%; max-height: 70vh;"></video>
         </div>
         </div>
     </el-dialog>
     </el-dialog>
+
+    <!-- 隐藏截图区域(离屏渲染模式) @Author: Antigravity -->
+    <div style="position: fixed; left: -9999px; top: 0; pointer-events: none; background: #fff;">
+        <div id="order-process-capture-area"
+            style="width: 650px; padding: 40px; background: #fff; border-radius: 8px; box-sizing: border-box; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;">
+            <!-- 报告头部 -->
+            <div style="margin-bottom: 30px; border-bottom: 2px solid #f0f2f5; padding-bottom: 20px;">
+                <div
+                    style="font-size: 20px; font-weight: bold; color: #303133; display: flex; align-items: center; gap: 12px;">
+                    <span
+                        style="font-size: 14px; padding: 4px 10px; border-radius: 4px; background: #409eff; color: #fff;">{{
+                        getStatusName(order?.status) }}</span>
+                    <span>服务流程:{{ order?.orderNo || order?.code }}</span>
+                </div>
+                <div style="margin-top: 10px; font-size: 13px; color: #909399;">
+                    报告生成时间:{{ captureTime }}
+                </div>
+            </div>
+
+            <!-- 进度步骤列表 -->
+            <div style="padding-left: 10px;">
+                <div v-if="serviceProgressSteps.length === 0"
+                    style="padding: 40px; text-align: center; color: #909399;">
+                    暂无服务进度记录
+                </div>
+                <div v-else>
+                    <div v-for="(step, index) in serviceProgressSteps" :key="index"
+                        style="position: relative; padding-bottom: 35px; padding-left: 40px;">
+                        <!-- 垂线 -->
+                        <div v-if="index < serviceProgressSteps.length - 1"
+                            style="position: absolute; left: 13px; top: 13px; bottom: 0; width: 2px; background-color: #e4e7ed;">
+                        </div>
+                        <!-- 圆点 -->
+                        <div :style="{
+                            position: 'absolute', left: '0', top: '0',
+                            width: '28px', height: '28px', borderRadius: '50%',
+                            backgroundColor: '#fff',
+                            border: '3px solid ' + (step.color || '#ff9900'),
+                            display: 'flex', alignItems: 'center', justifyContent: 'center',
+                            zIndex: '1', color: step.color || '#ff9900', fontSize: '12px', fontWeight: 'bold'
+                        }">●</div>
+                        <!-- 时间 -->
+                        <div style="font-size: 14px; color: #909399; margin-bottom: 10px; font-weight: 500;">{{
+                            step.time
+                            }}</div>
+                        <!-- 进度卡片 -->
+                        <div
+                            style="background: #f8fcfb; border-radius: 8px; padding: 20px; border: 1px solid #ebeef5; width: calc(100% - 10px); box-sizing: border-box;">
+                            <h4 style="margin: 0 0 12px; font-size: 17px; font-weight: bold; color: #303133;">{{
+                                step.title
+                                }}</h4>
+                            <p
+                                style="margin: 0 0 18px; color: #606266; font-size: 14px; line-height: 1.7; text-align: justify;">
+                                {{ step.desc }}</p>
+                            <!-- 图片展示(截图区域内用 base64 DataURL,避免 html2canvas 跨域问题) -->
+                            <div v-if="step.media && step.media.length"
+                                style="display: flex; gap: 12px; flex-wrap: wrap;">
+                                <div v-for="(item, i) in step.media" :key="i">
+                                    <!-- 图片:用预加载的 base64 -->
+                                    <img v-if="item.type === 'image' && captureBase64Cache[item.url]"
+                                        :src="captureBase64Cache[item.url]"
+                                        decoding="sync"
+                                        loading="eager"
+                                        style="width: 140px; height: 140px; border-radius: 6px; border: 1px solid #e4e7ed; object-fit: cover; display: block;" />
+                                    <!-- 占位白块(当加载失败或还未加载完时) -->
+                                    <div v-else-if="item.type === 'image'"
+                                        style="width: 140px; height: 140px; border-radius: 6px; background: #f5f7fa; border: 1px solid #e4e7ed;">
+                                    </div>
+                                    <!-- 视频: html2canvas 无法渲染 video,展示占位标识 -->
+                                    <div v-else-if="item.type === 'video'"
+                                        style="width: 140px; height: 140px; border-radius: 6px; border: 1px solid #e4e7ed; background: #1a1a2e; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 12px; flex-direction: column; gap: 6px;">
+                                        <span style="font-size: 28px;">▶</span>
+                                        <span>视频</span>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 页脚 -->
+            <div style="margin-top: 50px; text-align: center; border-top: 1px solid #f0f2f5; padding-top: 25px;">
+                <div style="font-size: 13px; color: #c0c4cc; font-style: italic; letter-spacing: 1px;">Powered by
+                    宠宝管理系统 · 官方服务证书</div>
+            </div>
+        </div>
+    </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, reactive, computed, watch, getCurrentInstance } from 'vue'
+import { ref, reactive, computed, watch, nextTick, getCurrentInstance } from 'vue'
 import { ElMessage } from 'element-plus'
 import { ElMessage } from 'element-plus'
+import html2canvas from 'html2canvas'
 import { getPet } from '@/api/archieves/pet'
 import { getPet } from '@/api/archieves/pet'
 import { getCustomer } from '@/api/archieves/customer'
 import { getCustomer } from '@/api/archieves/customer'
 import { getSubOrderInfo } from '@/api/order/subOrder/index'
 import { getSubOrderInfo } from '@/api/order/subOrder/index'
 import { getFulfiller } from '@/api/fulfiller/fulfiller/index'
 import { getFulfiller } from '@/api/fulfiller/fulfiller/index'
 import { listSubOrderLog, exportSubOrderLogUrl } from '@/api/order/subOrderLog/index'
 import { listSubOrderLog, exportSubOrderLogUrl } from '@/api/order/subOrderLog/index'
 import { listComplaintByOrder } from '@/api/fulfiller/complaint'
 import { listComplaintByOrder } from '@/api/fulfiller/complaint'
+import { listByIds, downloadOssBlob } from '@/api/system/oss'
 
 
 const { proxy } = getCurrentInstance()
 const { proxy } = getCurrentInstance()
 
 
@@ -389,7 +507,24 @@ const loadOrderLogs = async (order) => {
         const list = res?.data?.data || res?.data || []
         const list = res?.data?.data || res?.data || []
         const arr = Array.isArray(list) ? list : []
         const arr = Array.isArray(list) ? list : []
         orderLogs.value = arr.filter(i => Number(i?.logType) === 0)
         orderLogs.value = arr.filter(i => Number(i?.logType) === 0)
-        fulfillerLogs.value = arr.filter(i => Number(i?.logType) === 1)
+
+        // 对履约者日志中含有 photos(ossId 串)的条目,调用 listByIds 解析出可访问的 OSS URL
+        const fLogs = arr.filter(i => Number(i?.logType) === 1)
+        await Promise.all(fLogs.map(async (item) => {
+            const photoIds = item?.photos
+            if (photoIds) {
+                try {
+                    const ossRes = await listByIds(photoIds)
+                    const ossList = ossRes?.data || []
+                    item._resolvedUrls = ossList.map(o => ({ type: isVideo(o.url) ? 'video' : 'image', url: o.url, ossId: o.ossId }))
+                } catch {
+                    item._resolvedUrls = []
+                }
+            } else {
+                item._resolvedUrls = []
+            }
+        }))
+        fulfillerLogs.value = fLogs
     } catch {
     } catch {
         orderLogs.value = []
         orderLogs.value = []
         fulfillerLogs.value = []
         fulfillerLogs.value = []
@@ -597,13 +732,8 @@ const currentOrderSteps = computed(() => {
 const serviceProgressSteps = computed(() => {
 const serviceProgressSteps = computed(() => {
     const list = fulfillerLogs.value || []
     const list = fulfillerLogs.value || []
     return list.map((i) => {
     return list.map((i) => {
-        // 使用 photoUrls 展示,而非 photos
-        const rawUrls = i?.photoUrls || [];
-        const urlList = Array.isArray(rawUrls) ? rawUrls : (typeof rawUrls === 'string' ? rawUrls.split(',').filter(Boolean) : []);
-        const media = urlList.map(url => {
-            const type = isVideo(url) ? 'video' : 'image';
-            return { type, url }
-        });
+        // 使用 _resolvedUrls(通过 listByIds 解析后的 OSS 可访问 URL),而非直接使用 photoUrls
+        const media = Array.isArray(i?._resolvedUrls) ? i._resolvedUrls : []
 
 
         return {
         return {
             title: i?.title || '--',
             title: i?.title || '--',
@@ -628,6 +758,195 @@ const handleExportLogs = () => {
         `OrderLogs_${props.order.orderNo}_${new Date().getTime()}.xlsx`
         `OrderLogs_${props.order.orderNo}_${new Date().getTime()}.xlsx`
     );
     );
 }
 }
+
+// =================== 服务流程图导出(仿 platform-admin/OrderList.vue handleProcessImage 实现) ===================
+
+/** 流程图预览弹窗状态 */
+const processImageVisible = ref(false)
+/** 生成的流程图 base64 DataURL */
+const processImageUrl = ref('')
+/** 是否正在截图中 */
+const isProcessing = ref(false)
+/** 截图报告时间戳(用于隐藏 DOM 渲染) */
+const captureTime = ref('')
+/**
+ * 截图区域的图片 base64 缓存表
+ * key: ossId(如果有)或图片 url(如果无 ossId)
+ * value: base64 DataURL
+ */
+const captureBase64Cache = ref({})
+
+/**
+ * 将图片/OSS对象转换为 base64 DataURL @Author: Antigravity
+ * 优先采用后端代理下载以解决跨域问题,如果无 ossId 则尝试直接 fetch
+ */
+const loadImageAsBase64 = async (item) => {
+    const { ossId, url } = item;
+    try {
+        let blob;
+        if (ossId) {
+            blob = await downloadOssBlob(ossId);
+        } else if (url) {
+            const response = await fetch(url);
+            blob = await response.blob();
+        }
+        
+        if (!blob || blob.size === 0) {
+            console.warn('[FlowChart] 下载到的 Blob 为空:', url || ossId);
+            return '';
+        }
+        
+        console.log(`[FlowChart] 成功获取 Blob: 尺寸=${blob.size}, 类型=${blob.type}, ID=${ossId || 'N/A'}`);
+        
+        // 如果是报错返回的 JSON
+        if (blob.type === 'application/json') {
+            const text = await blob.text();
+            console.error('[FlowChart] 图片下载返回了错误 JSON:', text);
+            return '';
+        }
+        
+        return new Promise((resolve) => {
+            const reader = new FileReader();
+            reader.onloadend = () => {
+                const result = reader.result;
+                // 只要是有效的 DataURL 且长度合理就通过,不再严格限制 image/ 前缀
+                if (typeof result === 'string' && result.length > 100) {
+                    resolve(result);
+                } else {
+                    console.warn('[FlowChart] 生成的 Base64 长度不足或无效:', url || ossId);
+                    resolve('');
+                }
+            };
+            reader.onerror = () => {
+                console.error('[FlowChart] FileReader 读取失败');
+                resolve('');
+            };
+            reader.readAsDataURL(blob);
+        });
+    } catch (err) {
+        console.error('[FlowChart] 图片加载异常:', url || ossId, err);
+        return '';
+    }
+}
+
+/**
+ * 点击「导出流程图」:
+ * 1. 预加载所有图片为 base64(以解决 html2canvas 跨域无法捕获问题)
+ * 2. 更新 captureTime,等 DOM 刷新
+ * 3. 用 html2canvas 截取隐藏区域
+ * 4. 弹窗展示预览图
+ */
+const handleExportProgressImage = async () => {
+    if (serviceProgressSteps.value.length === 0) {
+        ElMessage.warning('暂无服务进度记录,无法导出流程图');
+        return;
+    }
+    processImageUrl.value = ''
+    isProcessing.value = true
+    processImageVisible.value = true
+    captureTime.value = new Date().toLocaleString()
+    captureBase64Cache.value = {}
+
+    // 收集所有需要预加载的图片 @Author: Antigravity
+    const itemsToLoad = []
+    for (const step of serviceProgressSteps.value) {
+        if (!step.media || !step.media.length) continue
+        for (const mediaItem of step.media) {
+            if (mediaItem.type === 'image' && mediaItem.url) {
+                // 仅收集还未加载过的
+                if (!captureBase64Cache.value[mediaItem.url]) {
+                    itemsToLoad.push(mediaItem)
+                }
+            }
+        }
+    }
+
+    // 并行下载所有图片为 base64 @Author: Antigravity
+    if (itemsToLoad.length > 0) {
+        console.log(`[FlowChart] 开始预加载 ${itemsToLoad.length} 张图片...`);
+        const results = await Promise.all(
+            itemsToLoad.map(async (item) => {
+                const b64 = await loadImageAsBase64(item);
+                return { url: item.url, b64 };
+            })
+        );
+        
+        // 批量更新缓存,确保触发响应式
+        const newCache = { ...captureBase64Cache.value };
+        results.forEach(({ url, b64 }) => {
+            if (b64) newCache[url] = b64;
+        });
+        captureBase64Cache.value = newCache;
+        console.log('[FlowChart] 图片预加载完成,当前有效缓存数:', Object.keys(newCache).length);
+    }
+
+    // 等待 Vue DOM 更新
+    await nextTick()
+    
+    // 获取截图区域并等待内部所有图片 load 完成
+    const el = document.getElementById('order-process-capture-area')
+    if (!el) {
+        ElMessage.error('截图区域未找到')
+        isProcessing.value = false
+        return
+    }
+
+    // 显式等待所有图片加载完毕
+    const imgs = el.querySelectorAll('img');
+    if (imgs.length > 0) {
+        console.log(`[FlowChart] 发现区域内有 ${imgs.length} 张图片,等待浏览器解码...`);
+        await Promise.all(Array.from(imgs).map(img => {
+            if (img.complete && img.naturalWidth > 0) return Promise.resolve();
+            return new Promise(resolve => {
+                img.onload = () => {
+                    console.log('[FlowChart] 图片加载成功:', img.src.substring(0, 50) + '...');
+                    resolve();
+                };
+                img.onerror = () => {
+                    console.error('[FlowChart] 图片加载失败:', img.src.substring(0, 50) + '...');
+                    resolve();
+                };
+                // 如果超过 3 秒还没加载完,强制继续,避免卡死
+                setTimeout(resolve, 3000);
+            });
+        }));
+    }
+
+    // 再给 300ms 缓冲时间
+    await new Promise(resolve => setTimeout(resolve, 300))
+
+    try {
+        const canvas = await html2canvas(el, {
+            scale: 2,
+            useCORS: true,
+            allowTaint: true,
+            backgroundColor: '#ffffff',
+            logging: true,
+            // 明确指定宽高,防止在某些浏览器下因离屏导致尺寸计算为 0
+            width: el.offsetWidth,
+            height: el.offsetHeight
+        })
+        processImageUrl.value = canvas.toDataURL('image/png')
+        console.log('[FlowChart] Canvas 绘制完成');
+    } catch (e) {
+        console.error('[FlowChart] 生成失败:', e)
+        ElMessage.error('流程图生成失败')
+    } finally {
+        isProcessing.value = false
+    }
+}
+
+/**
+ * 下载流程图到本地
+ */
+const downloadProcessImage = () => {
+    if (!processImageUrl.value) return
+    const orderNo = order.value?.orderNo || order.value?.code || 'order'
+    const link = document.createElement('a')
+    link.href = processImageUrl.value
+    link.download = `服务流程图_${orderNo}_${new Date().getTime()}.png`
+    link.click()
+}
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>

+ 8 - 133
src/views/order/orderList/index.vue

@@ -105,14 +105,6 @@
           </template>
           </template>
         </el-table-column>
         </el-table-column>
 
 
-        <el-table-column label="订单佣金" width="100">
-          <template #default="{ row }">
-            <span v-if="row.orderCommission !== null && row.orderCommission !== undefined"
-              style="color: #f56c6c; font-weight: bold;">¥{{ row.orderCommission / 100.0
-              }}</span>
-            <span v-else>-</span>
-          </template>
-        </el-table-column>
 
 
         <el-table-column label="订单佣金" width="100">
         <el-table-column label="订单佣金" width="100">
           <template #default="{ row }">
           <template #default="{ row }">
@@ -126,11 +118,6 @@
         <el-table-column label="履约者" width="120">
         <el-table-column label="履约者" width="120">
           <template #default="{ row }">
           <template #default="{ row }">
             <span v-if="row.fulfillerName" style="font-weight: 500; color: #333;">{{ row.fulfillerName }}</span>
             <span v-if="row.fulfillerName" style="font-weight: 500; color: #333;">{{ row.fulfillerName }}</span>
-            <div v-if="row.fulfillerName" class="fulfiller-info">
-              <span class="fulfiller-name">{{ row.fulfillerName }}</span>
-              <span class="fulfiller-fee" v-if="row.fulfillmentCommission !== null && row.fulfillmentCommission !== undefined">¥{{ row.fulfillmentCommission / 100.0
-              }}</span>
-            </div>
             <span v-else class="text-gray">暂未指派</span>
             <span v-else class="text-gray">暂未指派</span>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
@@ -147,9 +134,9 @@
 
 
         <el-table-column label="履约佣金" width="100">
         <el-table-column label="履约佣金" width="100">
           <template #default="{ row }">
           <template #default="{ row }">
-            <span v-if="row.price !== null && row.price !== undefined"
+            <span v-if="row.fulfillmentCommission !== null && row.fulfillmentCommission !== undefined"
               style="color: #f56c6c; font-size: 14px; font-weight: bold;">
               style="color: #f56c6c; font-size: 14px; font-weight: bold;">
-              ¥{{ (row.price / 100).toFixed(2) }}
+              ¥{{ (row.fulfillmentCommission / 100).toFixed(2) }}
             </span>
             </span>
             <span v-else>-</span>
             <span v-else>-</span>
           </template>
           </template>
@@ -166,8 +153,6 @@
                 @click="openDispatchDialog(row)" v-hasPermi="['order:orderList:redispatch']">重新派单</el-button>
                 @click="openDispatchDialog(row)" v-hasPermi="['order:orderList:redispatch']">重新派单</el-button>
               <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
               <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
                 @click="handleCancel(row)" v-hasPermi="['order:orderList:cancel']">取消</el-button>
                 @click="handleCancel(row)" v-hasPermi="['order:orderList:cancel']">取消</el-button>
-              <el-button v-if="row.fulfiller && ![1].includes(row.status)" link type="warning" size="small"
-                @click="openComplaintDialog(row)">投诉</el-button>
 
 
               <el-dropdown v-if="[3, 4].includes(row.status)" trigger="click"
               <el-dropdown v-if="[3, 4].includes(row.status)" trigger="click"
                 @command="(cmd) => handleCommand(cmd, row)">
                 @command="(cmd) => handleCommand(cmd, row)">
@@ -202,7 +187,7 @@
       @cancel="handleCancel" @command="handleCommand" @care-summary="openCareSummary" />
       @cancel="handleCancel" @command="handleCommand" @care-summary="openCareSummary" />
 
 
     <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder"
     <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder"
-      @submit="handleDispatchSubmit" />
+      @success="handleSearch" />
 
 
     <CareSummaryDrawer v-model:visible="careSummaryVisible" :order="careSummaryOrder" @submit="saveCareSummary" />
     <CareSummaryDrawer v-model:visible="careSummaryVisible" :order="careSummaryOrder" @submit="saveCareSummary" />
 
 
@@ -210,19 +195,6 @@
 
 
     <RemarkDialog v-model:visible="remarkDialogVisible" :order="currentOperateRow" @submit="handleRemarkSubmit" />
     <RemarkDialog v-model:visible="remarkDialogVisible" :order="currentOperateRow" @submit="handleRemarkSubmit" />
 
 
-    <!-- 投诉弹窗 -->
-    <el-dialog v-model="complaintDialogVisible" title="投诉" width="400px">
-      <el-form :model="complaintForm" label-width="60px">
-        <el-form-item label="原因" required>
-          <el-input v-model="complaintForm.reason" type="textarea" :rows="4" placeholder="请输入投诉原因" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="complaintDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submitComplaint">确认</el-button>
-      </template>
-    </el-dialog>
-
     <!-- 开通服务弹窗 -->
     <!-- 开通服务弹窗 -->
     <el-dialog v-model="activateDialogVisible" title="开通服务" width="400px" append-to-body>
     <el-dialog v-model="activateDialogVisible" title="开通服务" width="400px" append-to-body>
       <el-form :model="activateForm" label-width="80px">
       <el-form :model="activateForm" label-width="80px">
@@ -253,9 +225,9 @@ import DispatchDialog from '@/components/DispatchDialog/index.vue';
 import CareSummaryDrawer from './components/CareSummaryDrawer.vue';
 import CareSummaryDrawer from './components/CareSummaryDrawer.vue';
 import RewardDialog from './components/RewardDialog.vue';
 import RewardDialog from './components/RewardDialog.vue';
 import RemarkDialog from './components/RemarkDialog.vue';
 import RemarkDialog from './components/RemarkDialog.vue';
+import { useIntervalRefresh } from '@/hooks/useIntervalRefresh';
 import { listAllService } from '@/api/service/list/index';
 import { listAllService } from '@/api/service/list/index';
 import { listSubOrder } from '@/api/order/subOrder/index';
 import { listSubOrder } from '@/api/order/subOrder/index';
-import { dispatchSubOrder } from '@/api/order/subOrder/index';
 import { getSubOrderInfo } from '@/api/order/subOrder/index';
 import { getSubOrderInfo } from '@/api/order/subOrder/index';
 import { cancelSubOrder } from '@/api/order/subOrder/index';
 import { cancelSubOrder } from '@/api/order/subOrder/index';
 import { remarkSubOrder } from '@/api/order/subOrder/index';
 import { remarkSubOrder } from '@/api/order/subOrder/index';
@@ -271,7 +243,6 @@ import { reward } from '@/api/fulfiller/pool';
 const { proxy } = getCurrentInstance() as any;
 const { proxy } = getCurrentInstance() as any;
 import { getPet } from '@/api/archieves/pet';
 import { getPet } from '@/api/archieves/pet';
 import { getCustomer } from '@/api/archieves/customer';
 import { getCustomer } from '@/api/archieves/customer';
-import { addComplaint } from '@/api/fulfiller/complaint';
 
 
 const route = useRoute();
 const route = useRoute();
 const loading = ref(false);
 const loading = ref(false);
@@ -294,8 +265,6 @@ const areaStationList = ref([]);
 const areaStationMap = ref({});
 const areaStationMap = ref({});
 const storeMap = ref({});
 const storeMap = ref({});
 
 
-let timer: any = null;
-
 onMounted(() => {
 onMounted(() => {
   getServiceList();
   getServiceList();
   getAreaStationList();
   getAreaStationList();
@@ -304,25 +273,17 @@ onMounted(() => {
   if (route.query.id) {
   if (route.query.id) {
     handleDetail({ id: route.query.id as any });
     handleDetail({ id: route.query.id as any });
   }
   }
-
-  timer = setInterval(() => {
-    handleSearch(true);
-  }, 5000);
 });
 });
 
 
-// 监听路由参数变化,实现从消息直达详情
+// 监听路由参数变化,实现从消息直达详情 @Author: Antigravity
 watch(() => route.query.id, (newId) => {
 watch(() => route.query.id, (newId) => {
   if (newId) {
   if (newId) {
     handleDetail({ id: newId as any });
     handleDetail({ id: newId as any });
   }
   }
 });
 });
 
 
-onUnmounted(() => {
-  if (timer) {
-    clearInterval(timer);
-    timer = null;
-  }
-});
+// 统一数据轮询 @Author: Antigravity
+useIntervalRefresh(() => handleSearch(true), 5000);
 
 
 const getServiceList = () => {
 const getServiceList = () => {
   listAllService().then((res) => {
   listAllService().then((res) => {
@@ -490,9 +451,6 @@ const careSummaryOrder = ref(null);
 
 
 const rewardDialogVisible = ref(false);
 const rewardDialogVisible = ref(false);
 const remarkDialogVisible = ref(false);
 const remarkDialogVisible = ref(false);
-const complaintDialogVisible = ref(false);
-const complaintForm = reactive({ reason: '' });
-const currentComplaintOrder = ref(null);
 const currentOperateRow = ref(null);
 const currentOperateRow = ref(null);
 
 
 // 开通服务相关
 // 开通服务相关
@@ -655,65 +613,10 @@ const handleCancel = (row: any) => {
 
 
 // 派单
 // 派单
 const openDispatchDialog = (row) => {
 const openDispatchDialog = (row) => {
-  const typeName = getServiceName(row?.service);
-  const isTransport = row?.mode === 1 || row?.mode === '1';
-  const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
-
-  const t = row?.subOrderType ?? row?.type;
-  const transportType =
-    t === 0 || t === '0' || t === 1 || t === '1'
-      ? 'round'
-      : t === 2 || t === '2'
-        ? 'pick'
-        : t === 3 || t === '3'
-          ? 'drop'
-          : row?.splitType || row?.transportType;
-
-  const toAddress = row?.toAddress || '';
-  const pickAddr = isTransport ? toAddress : '';
-  const dropAddr = isTransport ? toAddress : '';
-  const address = isTransport ? '' : toAddress;
-
-  const orderObj = {
-    id: row.id,
-    typeCode,
-    transportType,
-    time: row.serviceTime || row.appointTime || row.createTime,
-    status: row.status,
-    address,
-    pickAddr,
-    dropAddr,
-    service: row.service,
-    riderId: row.riderId || row.fulfiller || null,
-    riderGender: row.fulfillerGender,
-    fulfillmentCommission: row.fulfillmentCommission,
-    orderCommission: row.orderCommission
-  };
-  currentDispatchOrder.value = orderObj;
+  currentDispatchOrder.value = row;
   dispatchDialogVisible.value = true;
   dispatchDialogVisible.value = true;
 };
 };
 
 
-const handleDispatchSubmit = (payload) => {
-  if (!currentDispatchOrder.value) return;
-  const fulfillmentCommissionFen = Math.round(Number(payload.fee || 0) * 100);
-  const orderCommissionFen = Math.round(Number(payload.orderCommission || 0) * 100);
-  dispatchSubOrder({
-    orderId: currentDispatchOrder.value.id,
-    fulfiller: payload.riderId,
-    fulfillmentCommission: fulfillmentCommissionFen,
-    orderCommission: orderCommissionFen
-  }).then(() => {
-    ElMessage.success('派单成功');
-    const row = tableData.value.find((r) => r.id === currentDispatchOrder.value.id);
-    if (row) {
-      row.status = 1;
-      row.fulfillerName = payload.riderName || 'Unknown';
-      row.price = payload.fee;
-    }
-    handleSearch();
-  });
-};
-
 // 护理小结
 // 护理小结
 const openCareSummary = async (row) => {
 const openCareSummary = async (row) => {
   const orderData = {
   const orderData = {
@@ -851,34 +754,6 @@ const handleRemarkSubmit = async (text) => {
   } catch { /* handled by interceptor */ }
   } catch { /* handled by interceptor */ }
 };
 };
 
 
-// 投诉
-const openComplaintDialog = (row) => {
-  currentComplaintOrder.value = row;
-  complaintForm.reason = '';
-  complaintDialogVisible.value = true;
-};
-
-const submitComplaint = async () => {
-  if (!complaintForm.reason.trim()) {
-    ElMessage.warning('请输入投诉原因');
-    return;
-  }
-  if (!currentComplaintOrder.value?.id || !currentComplaintOrder.value?.fulfiller) {
-    ElMessage.warning('订单信息不完整');
-    return;
-  }
-  try {
-    await addComplaint({
-      orderId: currentComplaintOrder.value.id,
-      fulfiller: currentComplaintOrder.value.fulfiller,
-      reason: complaintForm.reason
-    });
-    ElMessage.success('投诉提交成功');
-    complaintDialogVisible.value = false;
-    handleSearch();
-  } catch { /* handled by interceptor */ }
-};
-
 // 更多操作
 // 更多操作
 const handleCommand = (cmd, row) => {
 const handleCommand = (cmd, row) => {
   if (cmd === 'reward') openRewardDialog(row);
   if (cmd === 'reward') openRewardDialog(row);

+ 2 - 8
src/views/order/purchase/components/AddUserDialog.vue

@@ -33,13 +33,7 @@
         <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
         <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
         <el-col :span="24">
         <el-col :span="24">
           <el-form-item label="所在地区">
           <el-form-item label="所在地区">
-            <el-cascader
-              v-model="regionCascaderValue"
-              :options="regionData"
-              placeholder="请选择省/市/区"
-              style="width: 100%"
-              clearable
-            />
+            <RegionCascader v-model="regionCascaderValue" />
           </el-form-item>
           </el-form-item>
         </el-col>
         </el-col>
         <el-col :span="24">
         <el-col :span="24">
@@ -101,7 +95,7 @@ import { globalHeaders } from '@/utils/request'
 import { addCustomerOnOrder } from '@/api/archieves/customer'
 import { addCustomerOnOrder } from '@/api/archieves/customer'
 import { listAllTag } from '@/api/archieves/tag'
 import { listAllTag } from '@/api/archieves/tag'
 import { listAreaStation } from '@/api/system/areaStation'
 import { listAreaStation } from '@/api/system/areaStation'
-import { regionData } from 'element-china-area-data'
+import RegionCascader from '@/components/RegionCascader/index.vue'
 import PageSelect from '@/components/PageSelect/index.vue'
 import PageSelect from '@/components/PageSelect/index.vue'
 import { useUserStore } from '@/store/modules/user'
 import { useUserStore } from '@/store/modules/user'
 
 

+ 3 - 3
src/views/order/purchase/components/FeedingForm.vue

@@ -4,7 +4,7 @@
       <div class="section-label required">上门服务地址</div>
       <div class="section-label required">上门服务地址</div>
       <el-row :gutter="10">
       <el-row :gutter="10">
         <el-col :span="8">
         <el-col :span="8">
-          <el-cascader v-model="feedingData.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+          <RegionCascader v-model="feedingData.region" placeholder="省/市/区" />
         </el-col>
         </el-col>
         <el-col :span="16">
         <el-col :span="16">
           <el-input v-model="feedingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
           <el-input v-model="feedingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
@@ -52,10 +52,10 @@
 
 
 <script setup>
 <script setup>
 import { defineProps, defineEmits } from 'vue'
 import { defineProps, defineEmits } from 'vue'
+import RegionCascader from '@/components/RegionCascader/index.vue'
 
 
 const props = defineProps({
 const props = defineProps({
-  feedingData: { type: Object, required: true },
-  pcaOptions: { type: Array, default: () => [] }
+  feedingData: { type: Object, required: true }
 })
 })
 
 
 const emit = defineEmits(['change'])
 const emit = defineEmits(['change'])

+ 6 - 6
src/views/order/purchase/components/TransportForm.vue

@@ -17,7 +17,7 @@
             <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
             <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
               <el-col :span="2"><div class="addr-label required">起点</div></el-col>
               <el-col :span="2"><div class="addr-label required">起点</div></el-col>
               <el-col :span="6">
               <el-col :span="6">
-                <el-cascader v-model="transportData.pickStartRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+                <RegionCascader v-model="transportData.pickStartRegion" placeholder="省/市/区" />
               </el-col>
               </el-col>
               <el-col :span="16">
               <el-col :span="16">
                 <el-input v-model="transportData.pickStartDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
                 <el-input v-model="transportData.pickStartDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
@@ -26,7 +26,7 @@
             <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 20px;">
             <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 20px;">
               <el-col :span="2"><div class="addr-label required">终点</div></el-col>
               <el-col :span="2"><div class="addr-label required">终点</div></el-col>
               <el-col :span="6">
               <el-col :span="6">
-                <el-cascader v-model="transportData.pickEndRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+                <RegionCascader v-model="transportData.pickEndRegion" placeholder="省/市/区" />
               </el-col>
               </el-col>
               <el-col :span="16">
               <el-col :span="16">
                 <el-input v-model="transportData.pickEndDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
                 <el-input v-model="transportData.pickEndDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
@@ -55,7 +55,7 @@
             <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
             <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
               <el-col :span="2"><div class="addr-label">起点</div></el-col>
               <el-col :span="2"><div class="addr-label">起点</div></el-col>
               <el-col :span="6">
               <el-col :span="6">
-                <el-cascader v-model="transportData.dropStartRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+                <RegionCascader v-model="transportData.dropStartRegion" placeholder="省/市/区" />
               </el-col>
               </el-col>
               <el-col :span="16">
               <el-col :span="16">
                 <el-input v-model="transportData.dropStartDetail" placeholder="详细地址" prefix-icon="Location" />
                 <el-input v-model="transportData.dropStartDetail" placeholder="详细地址" prefix-icon="Location" />
@@ -64,7 +64,7 @@
             <el-row :gutter="10" align="middle" class="address-row">
             <el-row :gutter="10" align="middle" class="address-row">
               <el-col :span="2"><div class="addr-label">终点</div></el-col>
               <el-col :span="2"><div class="addr-label">终点</div></el-col>
               <el-col :span="6">
               <el-col :span="6">
-                <el-cascader v-model="transportData.dropEndRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+                <RegionCascader v-model="transportData.dropEndRegion" placeholder="省/市/区" />
               </el-col>
               </el-col>
               <el-col :span="16">
               <el-col :span="16">
                 <el-input v-model="transportData.dropEndDetail" placeholder="详细地址" prefix-icon="Location" />
                 <el-input v-model="transportData.dropEndDetail" placeholder="详细地址" prefix-icon="Location" />
@@ -92,10 +92,10 @@
 
 
 <script setup>
 <script setup>
 import { defineProps, defineEmits } from 'vue'
 import { defineProps, defineEmits } from 'vue'
+import RegionCascader from '@/components/RegionCascader/index.vue'
 
 
 const props = defineProps({
 const props = defineProps({
-  transportData: { type: Object, required: true },
-  pcaOptions: { type: Array, default: () => [] }
+  transportData: { type: Object, required: true }
 })
 })
 
 
 const emit = defineEmits(['change'])
 const emit = defineEmits(['change'])

+ 3 - 3
src/views/order/purchase/components/WashingForm.vue

@@ -4,7 +4,7 @@
       <div class="section-label required">上门服务地址</div>
       <div class="section-label required">上门服务地址</div>
       <el-row :gutter="10">
       <el-row :gutter="10">
         <el-col :span="8">
         <el-col :span="8">
-          <el-cascader v-model="washingData.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+          <RegionCascader v-model="washingData.region" placeholder="省/市/区" />
         </el-col>
         </el-col>
         <el-col :span="16">
         <el-col :span="16">
           <el-input v-model="washingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
           <el-input v-model="washingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
@@ -52,10 +52,10 @@
 
 
 <script setup>
 <script setup>
 import { defineProps, defineEmits } from 'vue'
 import { defineProps, defineEmits } from 'vue'
+import RegionCascader from '@/components/RegionCascader/index.vue'
 
 
 const props = defineProps({
 const props = defineProps({
-  washingData: { type: Object, required: true },
-  pcaOptions: { type: Array, default: () => [] }
+  washingData: { type: Object, required: true }
 })
 })
 
 
 const emit = defineEmits(['change'])
 const emit = defineEmits(['change'])

+ 4 - 7
src/views/order/purchase/index.vue

@@ -127,15 +127,15 @@
             <div class="divider"></div>
             <div class="divider"></div>
 
 
             <!-- A. 宠物接送表单 -->
             <!-- A. 宠物接送表单 -->
-            <TransportForm v-show="form.type === 'transport'" :transport-data="form.transport" :pca-options="pcaOptions"
+            <TransportForm v-show="form.type === 'transport'" :transport-data="form.transport"
               @change="calcPrice" />
               @change="calcPrice" />
 
 
             <!-- B. 上门喂遛表单 -->
             <!-- B. 上门喂遛表单 -->
-            <FeedingForm v-show="form.type === 'feeding'" :feeding-data="form.feeding" :pca-options="pcaOptions"
+            <FeedingForm v-show="form.type === 'feeding'" :feeding-data="form.feeding"
               @change="calcPrice" />
               @change="calcPrice" />
 
 
             <!-- C. 上门洗护表单 -->
             <!-- C. 上门洗护表单 -->
-            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing" :pca-options="pcaOptions"
+            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing"
               @change="calcPrice" />
               @change="calcPrice" />
 
 
           </div>
           </div>
@@ -199,7 +199,7 @@
 
 
     <!-- Dialogs -->
     <!-- Dialogs -->
     <!-- Add User Dialog -->
     <!-- Add User Dialog -->
-    <AddUserDialog v-model:visible="userDialogVisible" :pca-options="pcaOptions" :tenant-id="currentTenantId"
+    <AddUserDialog v-model:visible="userDialogVisible" :tenant-id="currentTenantId"
       @success="handleUserSuccess" />
       @success="handleUserSuccess" />
     <AddPetDialog v-model:visible="petDialogVisible" :user-id="form.userId" :user-options="userOptions"
     <AddPetDialog v-model:visible="petDialogVisible" :user-id="form.userId" :user-options="userOptions"
       @success="handlePetSuccess" />
       @success="handlePetSuccess" />
@@ -220,7 +220,6 @@ import { listStoreOnOrder } from '@/api/system/store'
 import { listAllService } from '@/api/service/list'
 import { listAllService } from '@/api/service/list'
 import { listCustomerOnOrder } from '@/api/archieves/customer'
 import { listCustomerOnOrder } from '@/api/archieves/customer'
 import { listPetByUser } from '@/api/archieves/pet'
 import { listPetByUser } from '@/api/archieves/pet'
-import { regionData as pcaOptions } from 'element-china-area-data'
 import { createOrder } from '@/api/order/order'
 import { createOrder } from '@/api/order/order'
 
 
 // --- State ---
 // --- State ---
@@ -451,8 +450,6 @@ const handleUserSuccess = (newUser) => {
   }
   }
 }
 }
 
 
-// Removed mocked pcaOptions since we now use element-china-area-data
-
 // Add Pet Logic
 // Add Pet Logic
 const petDialogVisible = ref(false)
 const petDialogVisible = ref(false)
 const openAddPet = () => { petDialogVisible.value = true }
 const openAddPet = () => { petDialogVisible.value = true }

+ 19 - 5
src/views/system/store/index.vue

@@ -206,8 +206,7 @@
         <el-form-item label="详细地址" prop="detailAddress">
         <el-form-item label="详细地址" prop="detailAddress">
           <el-row :gutter="10" style="margin-bottom: 10px">
           <el-row :gutter="10" style="margin-bottom: 10px">
             <el-col :span="24">
             <el-col :span="24">
-              <el-cascader v-model="addressCascaderValue" :options="regionData" placeholder="选择省市区"
-                style="width: 100%" />
+              <RegionCascader v-model="addressCascaderValue" placeholder="选择省市区" />
             </el-col>
             </el-col>
           </el-row>
           </el-row>
           <el-input v-model="form.detailAddress" type="textarea" placeholder="输入详细地址" rows="3" style="width: 100%" />
           <el-input v-model="form.detailAddress" type="textarea" placeholder="输入详细地址" rows="3" style="width: 100%" />
@@ -321,11 +320,13 @@ import { listAllService } from '@/api/service/list';
 import { listAreaStation } from '@/api/system/areaStation';
 import { listAreaStation } from '@/api/system/areaStation';
 import { getMapSetting } from '@/api/system/mapSetting';
 import { getMapSetting } from '@/api/system/mapSetting';
 import { AreaStationVO } from '@/api/system/areaStation/types';
 import { AreaStationVO } from '@/api/system/areaStation/types';
-import { regionData, codeToText, textToCode } from 'element-china-area-data';
+import RegionCascader from '@/components/RegionCascader/index.vue';
+import { useRegionData } from '@/hooks/useRegionData';
 import PageSelect from '@/components/PageSelect/index.vue';
 import PageSelect from '@/components/PageSelect/index.vue';
 import { checkPermi } from '@/utils/permission';
 import { checkPermi } from '@/utils/permission';
 
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { codeToName, loadRegionData } = useRegionData();
 
 
 const storeList = ref<StoreVO[]>([]);
 const storeList = ref<StoreVO[]>([]);
 const buttonLoading = ref(false);
 const buttonLoading = ref(false);
@@ -594,6 +595,17 @@ const handleUpdate = async (row: StoreVO) => {
   const res = await getStore(row.id);
   const res = await getStore(row.id);
   Object.assign(form.value, res.data);
   Object.assign(form.value, res.data);
 
 
+  // 确保 services 为数字数组类型,避免由于类型不匹配(如字符串或字符串数组)导致 el-checkbox-group 无法匹配和选中
+  if (form.value.services) {
+    if (typeof form.value.services === 'string') {
+      form.value.services = (form.value.services as string).split(',').filter(v => v !== '').map(Number);
+    } else if (Array.isArray(form.value.services)) {
+      form.value.services = form.value.services.map(Number);
+    }
+  } else {
+    form.value.services = [];
+  }
+
   if (form.value.startBusinessTime) {
   if (form.value.startBusinessTime) {
     form.value.startBusinessTime = formatTime(form.value.startBusinessTime);
     form.value.startBusinessTime = formatTime(form.value.startBusinessTime);
   }
   }
@@ -720,7 +732,7 @@ const getGeolocation = async () => {
   // 拼接完整地址(省市区 + 详细地址)
   // 拼接完整地址(省市区 + 详细地址)
   let areaText = '';
   let areaText = '';
   if (addressCascaderValue.value && addressCascaderValue.value.length > 0) {
   if (addressCascaderValue.value && addressCascaderValue.value.length > 0) {
-    areaText = addressCascaderValue.value.map((code: string) => codeToText[code] || '').join('');
+    areaText = addressCascaderValue.value.map((code: string) => codeToName(code)).join('');
   }
   }
   const detailAddr = form.value.detailAddress || '';
   const detailAddr = form.value.detailAddress || '';
   const fullAddress = (areaText + detailAddr).trim();
   const fullAddress = (areaText + detailAddr).trim();
@@ -910,7 +922,7 @@ const getFullAddress = (row: any): string => {
   let areaText = '';
   let areaText = '';
   if (row.areaCode) {
   if (row.areaCode) {
     const codes = typeof row.areaCode === 'string' ? row.areaCode.split(',') : row.areaCode;
     const codes = typeof row.areaCode === 'string' ? row.areaCode.split(',') : row.areaCode;
-    areaText = codes.map((code: string) => codeToText[code] || '').join('');
+    areaText = codes.map((code: string) => codeToName(code)).join('');
   }
   }
   return areaText ? `${areaText} ${row.detailAddress || ''}` : (row.detailAddress || '');
   return areaText ? `${areaText} ${row.detailAddress || ''}` : (row.detailAddress || '');
 };
 };
@@ -949,6 +961,8 @@ onMounted(() => {
   loadAMapScript().catch(() => {
   loadAMapScript().catch(() => {
     console.warn('高德地图预加载失败,将在首次使用时重试');
     console.warn('高德地图预加载失败,将在首次使用时重试');
   });
   });
+
+  loadRegionData();
 });
 });
 </script>
 </script>
 
 

+ 63 - 2
src/views/system/tenant/index.vue

@@ -65,10 +65,11 @@
           </template>
           </template>
         </el-table-column>
         </el-table-column>
         <el-table-column label="品牌简介" prop="intro" min-width="150" show-overflow-tooltip />
         <el-table-column label="品牌简介" prop="intro" min-width="150" show-overflow-tooltip />
-        <el-table-column label="操作" align="right" width="140" fixed="right">
+        <el-table-column label="操作" align="right" width="220" fixed="right">
           <template #default="scope">
           <template #default="scope">
             <div class="op-btns">
             <div class="op-btns">
               <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['system:tenant:edit']">编辑</el-button>
               <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['system:tenant:edit']">编辑</el-button>
+              <el-button link type="primary" @click="handleResetPwd(scope.row)" v-hasPermi="['system:tenant:edit']">重置密码</el-button>
               <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['system:tenant:remove']">删除</el-button>
               <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['system:tenant:remove']">删除</el-button>
             </div>
             </div>
           </template>
           </template>
@@ -121,6 +122,21 @@
         </div>
         </div>
       </template>
       </template>
     </el-dialog>
     </el-dialog>
+
+    <!-- 重置密码对话框 -->
+    <el-dialog v-model="resetPwdDialog.visible" :title="resetPwdDialog.title" width="400px" append-to-body>
+      <el-form ref="resetPwdFormRef" :model="resetPwdForm" :rules="resetPwdRules" label-width="100px">
+        <el-form-item label="新密码" prop="newPassword">
+          <el-input v-model="resetPwdForm.newPassword" type="password" placeholder="请输入新密码" maxlength="20" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="resetPwdDialog.visible = false">取 消</el-button>
+          <el-button :loading="resetPwdButtonLoading" type="primary" @click="submitResetPwdForm">确 定</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -131,7 +147,8 @@ import {
   delTenant,
   delTenant,
   addTenant,
   addTenant,
   updateTenant,
   updateTenant,
-  changeTenantStatus
+  changeTenantStatus,
+  resetTenantAdminPwd
 } from '@/api/system/tenant';
 } from '@/api/system/tenant';
 import { selectTenantPackage } from '@/api/system/tenantPackage';
 import { selectTenantPackage } from '@/api/system/tenantPackage';
 import { useUserStore } from '@/store/modules/user';
 import { useUserStore } from '@/store/modules/user';
@@ -150,12 +167,29 @@ const total = ref(0);
 
 
 const queryFormRef = ref<ElFormInstance>();
 const queryFormRef = ref<ElFormInstance>();
 const tenantFormRef = ref<ElFormInstance>();
 const tenantFormRef = ref<ElFormInstance>();
+const resetPwdFormRef = ref<ElFormInstance>();
 
 
 const dialog = reactive<DialogOption>({
 const dialog = reactive<DialogOption>({
   visible: false,
   visible: false,
   title: ''
   title: ''
 });
 });
 
 
+const resetPwdDialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+const resetPwdButtonLoading = ref(false);
+const resetPwdForm = ref({
+  id: undefined as number | undefined,
+  newPassword: ''
+});
+const resetPwdRules = {
+  newPassword: [
+    { required: true, message: '新密码不能为空', trigger: 'blur' },
+    { min: 5, max: 20, message: '新密码长度必须介于 5 和 20 之间', trigger: 'blur' }
+  ]
+};
+
 const initFormData: TenantForm = {
 const initFormData: TenantForm = {
   id: undefined,
   id: undefined,
   tenantId: undefined,
   tenantId: undefined,
@@ -256,6 +290,17 @@ const handleUpdate = async (row: TenantVO) => {
   dialog.title = '修改品牌';
   dialog.title = '修改品牌';
 };
 };
 
 
+/** 重置密码按钮操作 */
+const handleResetPwd = (row: TenantVO) => {
+  resetPwdForm.value.id = row.id as any;
+  resetPwdForm.value.newPassword = '';
+  resetPwdDialog.visible = true;
+  resetPwdDialog.title = '重置管理员密码';
+  nextTick(() => {
+    resetPwdFormRef.value?.clearValidate();
+  });
+};
+
 /** 提交按钮 */
 /** 提交按钮 */
 const submitForm = () => {
 const submitForm = () => {
   tenantFormRef.value?.validate(async (valid: boolean) => {
   tenantFormRef.value?.validate(async (valid: boolean) => {
@@ -273,6 +318,22 @@ const submitForm = () => {
   });
   });
 };
 };
 
 
+/** 提交重置密码按钮 */
+const submitResetPwdForm = () => {
+  resetPwdFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      resetPwdButtonLoading.value = true;
+      try {
+        await resetTenantAdminPwd(resetPwdForm.value);
+        proxy?.$modal.msgSuccess('密码重置成功');
+        resetPwdDialog.visible = false;
+      } finally {
+        resetPwdButtonLoading.value = false;
+      }
+    }
+  });
+};
+
 /** 删除按钮操作 */
 /** 删除按钮操作 */
 const handleDelete = async (row: TenantVO) => {
 const handleDelete = async (row: TenantVO) => {
   await proxy?.$modal.confirm(`是否确认删除品牌"${row.companyName}"?`);
   await proxy?.$modal.confirm(`是否确认删除品牌"${row.companyName}"?`);

+ 40 - 74
src/views/system/user/index.vue

@@ -86,12 +86,12 @@
           </el-col>
           </el-col>
           <el-col :span="12">
           <el-col :span="12">
             <el-form-item label="手机号码" prop="phonenumber">
             <el-form-item label="手机号码" prop="phonenumber">
-              <el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
+              <el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" @input="form.userName = form.phonenumber" />
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
         </el-row>
         </el-row>
 
 
-        <!-- 第二行:邮箱 / 登录账号 -->
+        <!-- 第二行:邮箱 / 用户性别 -->
         <el-row :gutter="20">
         <el-row :gutter="20">
           <el-col :span="12">
           <el-col :span="12">
             <el-form-item label="邮箱" prop="email">
             <el-form-item label="邮箱" prop="email">
@@ -99,85 +99,50 @@
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
           <el-col :span="12">
           <el-col :span="12">
-            <el-form-item label="登录账号" prop="userName">
-              <el-input v-model="form.userName" placeholder="请输入登录账号" maxlength="30" :disabled="form.userId !== undefined" />
+            <el-form-item label="用户性别">
+              <el-select v-model="form.sex" placeholder="请选择" style="width: 100%">
+                <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+              </el-select>
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
         </el-row>
         </el-row>
 
 
-        <!-- 第三行:登录密码(新增) or 用户性别(修改) / 用户性别(新增) or 状态(修改) -->
+        <!-- 第三行:新增模式下[密码+角色],修改模式下[角色占满] @Author: Antigravity -->
         <el-row :gutter="20">
         <el-row :gutter="20">
-          <template v-if="form.userId === undefined">
-            <el-col :span="12">
-              <el-form-item label="登录密码" prop="password">
-                <el-input v-model="form.password" placeholder="请输入登录密码" type="password" maxlength="20" show-password />
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="用户性别">
-                <el-select v-model="form.sex" placeholder="请对象" style="width: 100%">
-                  <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-          </template>
-          <template v-else>
-            <el-col :span="12">
-              <el-form-item label="用户性别">
-                <el-select v-model="form.sex" placeholder="请选择" style="width: 100%">
-                  <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="状态">
-                <el-radio-group v-model="form.status">
-                  <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
-                </el-radio-group>
-              </el-form-item>
-            </el-col>
-          </template>
+          <el-col :span="12" v-if="form.userId === undefined">
+            <el-form-item label="登录密码" prop="password">
+              <el-input v-model="form.password" placeholder="请输入登录密码" type="password" maxlength="20" show-password />
+            </el-form-item>
+          </el-col>
+          <el-col :span="form.userId === undefined ? 12 : 24">
+            <el-form-item label="角色" prop="roleIds">
+              <el-select v-model="form.roleIds" filterable multiple placeholder="请选择角色" style="width: 100%">
+                <el-option
+                  v-for="item in roleOptions"
+                  :key="item.roleId"
+                  :label="item.roleName"
+                  :value="item.roleId"
+                  :disabled="item.status == '1'"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" style="display: none;">
+            <el-form-item label="登录账号" prop="userName">
+              <el-input v-model="form.userName" />
+            </el-form-item>
+          </el-col>
         </el-row>
         </el-row>
 
 
-        <!-- 第四行:角色(新增) or 角色(修改) / 状态(新增) or 空白(修改时备注占满) -->
+        <!-- 第四行:状态单独一行 -->
         <el-row :gutter="20">
         <el-row :gutter="20">
-          <template v-if="form.userId === undefined">
-            <el-col :span="12">
-              <el-form-item label="角色" prop="roleIds">
-                <el-select v-model="form.roleIds" filterable multiple placeholder="请选择" style="width: 100%">
-                  <el-option
-                    v-for="item in roleOptions"
-                    :key="item.roleId"
-                    :label="item.roleName"
-                    :value="item.roleId"
-                    :disabled="item.status == '1'"
-                  ></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="状态">
-                <el-radio-group v-model="form.status">
-                  <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
-                </el-radio-group>
-              </el-form-item>
-            </el-col>
-          </template>
-          <template v-else>
-            <el-col :span="12">
-              <el-form-item label="角色" prop="roleIds">
-                <el-select v-model="form.roleIds" filterable multiple placeholder="请选择" style="width: 100%">
-                  <el-option
-                    v-for="item in roleOptions"
-                    :key="item.roleId"
-                    :label="item.roleName"
-                    :value="item.roleId"
-                    :disabled="item.status == '1'"
-                  ></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-          </template>
+          <el-col :span="24">
+            <el-form-item label="状态">
+              <el-radio-group v-model="form.status">
+                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
         </el-row>
         </el-row>
         <el-row :gutter="20">
         <el-row :gutter="20">
           <el-col :span="24">
           <el-col :span="24">
@@ -364,9 +329,10 @@ const initData: PageData<UserForm, UserQuery> = {
       }
       }
     ],
     ],
     phonenumber: [
     phonenumber: [
+      { required: true, message: '手机号码不能为空', trigger: 'blur' },
       {
       {
         pattern: /^1[3456789][0-9]\d{8}$/,
         pattern: /^1[3456789][0-9]\d{8}$/,
-        message: '请输入正确的手机号码',
+        message: '请输入正确的 11 位手机号码',
         trigger: 'blur'
         trigger: 'blur'
       }
       }
     ],
     ],

+ 102 - 53
src/views/systemConfig/app/index.vue

@@ -6,7 +6,7 @@
         <template #title>
         <template #title>
           <div class="alert-content">
           <div class="alert-content">
             <el-icon class="info-icon"><InfoFilled /></el-icon>
             <el-icon class="info-icon"><InfoFilled /></el-icon>
-            <span>在此配置移动端APP的展示信息,包括登录页图标及登录页背景图。</span>
+            <span>在此配置移动端APP的展示信息,分为多个端进行独立配置。</span>
           </div>
           </div>
         </template>
         </template>
       </el-alert>
       </el-alert>
@@ -14,31 +14,53 @@
 
 
     <!-- 配置表单区 -->
     <!-- 配置表单区 -->
     <div class="setting-body">
     <div class="setting-body">
-      <el-form ref="appFormRef" :model="form" :rules="rules" label-width="160px" label-position="right" class="premium-setting-form">
-        <!-- 登录图标 -->
-        <el-form-item label="登录页图标:" prop="loginIcon">
-          <image-upload v-model="form.loginIcon" :limit="1" :can-delete="false" />
-          <div class="form-tip">建议尺寸:200x200,支持 png/jpg 格式</div>
-        </el-form-item>
-
-        <!-- 登录页背景图 -->
-        <el-form-item label="登录页背景:" prop="loginBackground">
-          <image-upload v-model="form.loginBackground" :limit="1" :can-delete="false" />
-          <div class="form-tip">建议尺寸:1080x1920(9:16),支持 jpg/png 格式</div>
-        </el-form-item>
-
-        <!-- 保存按钮 -->
-        <el-form-item class="action-item">
-          <el-button type="primary" class="save-btn" :loading="buttonLoading" @click="submitForm">保存修改</el-button>
-        </el-form-item>
-      </el-form>
+      <el-tabs v-model="activeTab" type="border-card" class="premium-tabs">
+        <!-- 履约守护配置 -->
+        <el-tab-pane label="履约守护" name="fulfiller">
+          <el-form ref="fulfillerFormRef" :model="fulfillerForm" :rules="fulfillerRules" label-width="160px" label-position="right" class="premium-setting-form">
+            <!-- 登录图标 -->
+            <el-form-item label="登录页图标:" prop="loginIcon">
+              <image-upload v-model="fulfillerForm.loginIcon" :limit="1" :can-delete="false" />
+              <div class="form-tip">建议尺寸:200x200,支持 png/jpg 格式</div>
+            </el-form-item>
+
+            <!-- 登录页背景图 -->
+            <el-form-item label="登录页背景:" prop="loginBackground">
+              <image-upload v-model="fulfillerForm.loginBackground" :limit="1" :can-delete="false" />
+              <div class="form-tip">建议尺寸:1080x1920(9:16),支持 jpg/png 格式</div>
+            </el-form-item>
+
+            <!-- 保存按钮 -->
+            <el-form-item class="action-item">
+              <el-button type="primary" class="save-btn" :loading="buttonLoading" @click="submitFulfillerForm">保存修改</el-button>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+
+        <!-- 好萌友配置 -->
+        <el-tab-pane label="好萌友" name="merchant">
+          <el-form ref="merchantFormRef" :model="merchantForm" label-width="160px" label-position="right" class="premium-setting-form">
+            <!-- 首页轮播图 -->
+            <el-form-item label="首页轮播图:" prop="homeBanner">
+              <image-upload v-model="merchantForm.homeBanner" :limit="10" />
+              <div class="form-tip">建议尺寸:750x320,支持多张,用于商户端首页轮播展示</div>
+            </el-form-item>
+
+            <!-- 保存按钮 -->
+            <el-form-item class="action-item">
+              <el-button type="primary" class="save-btn" :loading="buttonLoading" @click="submitMerchantForm">保存修改</el-button>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup name="AppConfig" lang="ts">
 <script setup name="AppConfig" lang="ts">
+// @Author: Antigravity
 import { getAppSetting, updateAppSetting } from '@/api/system/appSetting';
 import { getAppSetting, updateAppSetting } from '@/api/system/appSetting';
-import { AppSettingVO, AppSettingForm } from '@/api/system/appSetting/types';
+import { AppSettingForm } from '@/api/system/appSetting/types';
 import { InfoFilled } from '@element-plus/icons-vue';
 import { InfoFilled } from '@element-plus/icons-vue';
 import ImageUpload from '@/components/ImageUpload/index.vue';
 import ImageUpload from '@/components/ImageUpload/index.vue';
 import { ref, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
 import { ref, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
@@ -46,15 +68,22 @@ import { ref, onMounted, getCurrentInstance, ComponentInternalInstance } from 'v
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 
 const buttonLoading = ref(false);
 const buttonLoading = ref(false);
-const appFormRef = ref<any>();
+const activeTab = ref('fulfiller');
+const fulfillerFormRef = ref<any>();
+const merchantFormRef = ref<any>();
 
 
-const form = ref<any>({
+const fulfillerForm = ref<any>({
   id: 1,
   id: 1,
   loginIcon: undefined,
   loginIcon: undefined,
   loginBackground: undefined
   loginBackground: undefined
 });
 });
 
 
-const rules = {
+const merchantForm = ref<any>({
+  id: 2,
+  homeBanner: undefined
+});
+
+const fulfillerRules = {
   loginIcon: [{ required: true, message: "登录页图标不能为空", trigger: "change" }],
   loginIcon: [{ required: true, message: "登录页图标不能为空", trigger: "change" }],
   loginBackground: [{ required: true, message: "登录页背景不能为空", trigger: "change" }],
   loginBackground: [{ required: true, message: "登录页背景不能为空", trigger: "change" }],
 };
 };
@@ -62,35 +91,32 @@ const rules = {
 /** 加载配置 */
 /** 加载配置 */
 const loadConfig = async () => {
 const loadConfig = async () => {
   try {
   try {
-    const res = await getAppSetting(1);
-    if (res.code === 200) {
-      // 接口返回的数据中 loginIcon 和 loginBackground 是 ID,可以直接给 form
-      // 如果 ImageUpload 内部处理字符串 ID,则需要转换一下
-      form.value.id = res.data.id;
-      form.value.loginIcon = res.data.loginIcon ? String(res.data.loginIcon) : undefined;
-      form.value.loginBackground = res.data.loginBackground ? String(res.data.loginBackground) : undefined;
+    // 加载履约守护配置 (ID 1)
+    const res1 = await getAppSetting(1);
+    if (res1.data) {
+      fulfillerForm.value.loginIcon = res1.data.loginIcon ? String(res1.data.loginIcon) : undefined;
+      fulfillerForm.value.loginBackground = res1.data.loginBackground ? String(res1.data.loginBackground) : undefined;
+    }
+    
+    // 加载好萌友配置 (ID 2)
+    const res2 = await getAppSetting(2);
+    if (res2.data) {
+      merchantForm.value.homeBanner = res2.data.homeBanner ? String(res2.data.homeBanner) : undefined;
     }
     }
   } catch (error) {
   } catch (error) {
     console.error('加载APP配置失败', error);
     console.error('加载APP配置失败', error);
   }
   }
 };
 };
 
 
-/** 提交保存 */
-const submitForm = () => {
-  appFormRef.value?.validate(async (valid: boolean) => {
+/** 提交履约守护配置 */
+const submitFulfillerForm = () => {
+  fulfillerFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
     if (valid) {
       buttonLoading.value = true;
       buttonLoading.value = true;
       try {
       try {
-        const submitData: AppSettingForm = {
-          id: form.value.id,
-          loginIcon: form.value.loginIcon,
-          loginBackground: form.value.loginBackground
-        };
-        await updateAppSetting(submitData);
-        proxy?.$modal.msgSuccess("保存成功");
+        await updateAppSetting(fulfillerForm.value);
+        proxy?.$modal.msgSuccess("履约守护配置保存成功");
         loadConfig();
         loadConfig();
-      } catch (error) {
-        console.error('保存APP配置失败', error);
       } finally {
       } finally {
         buttonLoading.value = false;
         buttonLoading.value = false;
       }
       }
@@ -98,6 +124,18 @@ const submitForm = () => {
   });
   });
 };
 };
 
 
+/** 提交好萌友配置 */
+const submitMerchantForm = async () => {
+  buttonLoading.value = true;
+  try {
+    await updateAppSetting(merchantForm.value);
+    proxy?.$modal.msgSuccess("好萌友配置保存成功");
+    loadConfig();
+  } finally {
+    buttonLoading.value = false;
+  }
+};
+
 onMounted(() => {
 onMounted(() => {
   loadConfig();
   loadConfig();
 });
 });
@@ -109,7 +147,7 @@ onMounted(() => {
 }
 }
 
 
 .setting-hint {
 .setting-hint {
-  margin-bottom: 32px;
+  margin-bottom: 24px;
 
 
   .custom-alert {
   .custom-alert {
     background-color: #e8f4ff;
     background-color: #e8f4ff;
@@ -133,10 +171,30 @@ onMounted(() => {
 }
 }
 
 
 .setting-body {
 .setting-body {
-  padding-left: 20px;
+  padding: 0 20px;
+}
+
+.premium-tabs {
+  border: none;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
+  border-radius: 12px;
+  overflow: hidden;
+
+  :deep(.el-tabs__header) {
+    background-color: #f8fafc;
+    border-bottom: 1px solid #e5e6eb;
+  }
+
+  :deep(.el-tabs__item.is-active) {
+    background-color: #fff;
+    border-right-color: #e5e6eb;
+    border-left-color: #e5e6eb;
+  }
 }
 }
 
 
 .premium-setting-form {
 .premium-setting-form {
+  padding: 24px 0;
+  
   :deep(.el-form-item__label) {
   :deep(.el-form-item__label) {
     font-weight: 500;
     font-weight: 500;
     color: #4e5969;
     color: #4e5969;
@@ -152,15 +210,6 @@ onMounted(() => {
     width: 100%;
     width: 100%;
   }
   }
 
 
-  :deep(.el-input) {
-    max-width: 440px;
-    .el-input__wrapper {
-      box-shadow: 0 0 0 1px #e5e6eb inset;
-      padding: 4px 12px;
-      border-radius: 6px;
-    }
-  }
-
   .action-item {
   .action-item {
     margin-top: 40px;
     margin-top: 40px;
   }
   }

+ 96 - 59
src/views/systemConfig/platform/components/CustomerConfig.vue

@@ -1,56 +1,64 @@
 <template>
 <template>
   <div class="setting-container">
   <div class="setting-container">
-    <el-form ref="customerFormRef" :model="form" :rules="rules" label-width="160px" class="premium-setting-form">
-      <el-form-item label="客服微信号:" prop="wechatAccount">
-        <el-input v-model="form.wechatAccount" placeholder="请输入客服微信号" class="config-input" />
-      </el-form-item>
-
-      <el-form-item label="客服电话:" prop="phoneNumber">
-        <el-input v-model="form.phoneNumber" placeholder="请输入客服联系电话" class="config-input" />
-      </el-form-item>
-
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="客服开始时间:" prop="startServiceTime">
-            <el-time-picker
-              v-model="form.startServiceTime"
-              placeholder="选择开始时间"
-              format="HH:mm"
-              value-format="HH:mm"
-              class="config-time-picker"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="客服结束时间:" prop="endServiceTime">
-            <el-time-picker
-              v-model="form.endServiceTime"
-              placeholder="选择结束时间"
-              format="HH:mm"
-              value-format="HH:mm"
-              class="config-time-picker"
-            />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-form-item label="企微链接:" prop="enterpriseWechatLink">
-        <el-input v-model="form.enterpriseWechatLink" placeholder="请输入企业微信链接" class="config-input" />
-      </el-form-item>
-
-      <el-form-item label="客服二维码:" prop="qrCode">
-        <image-upload v-model="form.qrCode" :limit="1" :can-delete="false" />
-        <div class="form-tip">支持 jpg/png 格式</div>
-      </el-form-item>
-
-      <el-form-item class="action-item">
-        <el-button type="primary" class="save-btn" :loading="loading" @click="submitForm">保存修改</el-button>
-      </el-form-item>
-    </el-form>
+    <el-tabs v-model="activeId" class="premium-tabs" @tab-change="handleTabChange">
+      <el-tab-pane label="履约守护" :name="1" />
+      <el-tab-pane label="好萌友" :name="2" />
+    </el-tabs>
+
+    <div class="tab-content">
+      <el-form ref="customerFormRef" :model="form" :rules="rules" label-width="160px" class="premium-setting-form">
+        <el-form-item label="客服微信号:" prop="wechatAccount">
+          <el-input v-model="form.wechatAccount" placeholder="请输入客服微信号" class="config-input" />
+        </el-form-item>
+
+        <el-form-item label="客服电话:" prop="phoneNumber">
+          <el-input v-model="form.phoneNumber" placeholder="请输入客服联系电话" class="config-input" />
+        </el-form-item>
+
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="客服开始时间:" prop="startServiceTime">
+              <el-time-picker
+                v-model="form.startServiceTime"
+                placeholder="选择开始时间"
+                format="HH:mm"
+                value-format="HH:mm"
+                class="config-time-picker"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="客服结束时间:" prop="endServiceTime">
+              <el-time-picker
+                v-model="form.endServiceTime"
+                placeholder="选择结束时间"
+                format="HH:mm"
+                value-format="HH:mm"
+                class="config-time-picker"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="企微链接:" prop="enterpriseWechatLink">
+          <el-input v-model="form.enterpriseWechatLink" placeholder="请输入企业微信链接" class="config-input" />
+        </el-form-item>
+
+        <el-form-item label="客服二维码:" prop="qrCode">
+          <image-upload v-model="form.qrCode" :limit="1" :can-delete="false" />
+          <div class="form-tip">支持 jpg/png 格式</div>
+        </el-form-item>
+
+        <el-form-item class="action-item">
+          <el-button type="primary" class="save-btn" :loading="loading" @click="submitForm">保存修改</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
+// @Author: Antigravity
 import { getCustomerServiceSetting, updateCustomerServiceSetting } from '@/api/system/customerServiceSetting';
 import { getCustomerServiceSetting, updateCustomerServiceSetting } from '@/api/system/customerServiceSetting';
 import { CustomerServiceSettingForm } from '@/api/system/customerServiceSetting/types';
 import { CustomerServiceSettingForm } from '@/api/system/customerServiceSetting/types';
 import ImageUpload from '@/components/ImageUpload/index.vue';
 import ImageUpload from '@/components/ImageUpload/index.vue';
@@ -59,6 +67,7 @@ import { ref, onMounted, getCurrentInstance, ComponentInternalInstance } from 'v
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const loading = ref(false);
 const loading = ref(false);
 const customerFormRef = ref<any>();
 const customerFormRef = ref<any>();
+const activeId = ref(1);
 
 
 const form = ref<any>({
 const form = ref<any>({
   id: 1,
   id: 1,
@@ -78,19 +87,36 @@ const rules = {
   qrCode: [{ required: true, message: "二维码不能为空", trigger: "change" }]
   qrCode: [{ required: true, message: "二维码不能为空", trigger: "change" }]
 };
 };
 
 
+const handleTabChange = (val: any) => {
+  activeId.value = val;
+  loadData();
+};
+
 const loadData = async () => {
 const loadData = async () => {
   try {
   try {
-    const res = await getCustomerServiceSetting(1);
+    const res = await getCustomerServiceSetting(activeId.value);
     if (res.code === 200) {
     if (res.code === 200) {
-      form.value = { ...res.data };
-      // 提取时分:后端返回 1970-01-01 08:00:00,前端需要 HH:mm
-      if (form.value.startServiceTime && form.value.startServiceTime.includes(' ')) {
-        form.value.startServiceTime = form.value.startServiceTime.split(' ')[1].substring(0, 5);
-      }
-      if (form.value.endServiceTime && form.value.endServiceTime.includes(' ')) {
-        form.value.endServiceTime = form.value.endServiceTime.split(' ')[1].substring(0, 5);
+      if (res.data) {
+        form.value = { ...res.data };
+        // 提取时分:后端返回 1970-01-01 08:00:00,前端需要 HH:mm
+        if (form.value.startServiceTime && form.value.startServiceTime.includes(' ')) {
+          form.value.startServiceTime = form.value.startServiceTime.split(' ')[1].substring(0, 5);
+        }
+        if (form.value.endServiceTime && form.value.endServiceTime.includes(' ')) {
+          form.value.endServiceTime = form.value.endServiceTime.split(' ')[1].substring(0, 5);
+        }
+        if (typeof form.value.qrCode === 'number') form.value.qrCode = String(form.value.qrCode);
+      } else {
+        form.value = {
+          id: activeId.value,
+          wechatAccount: '',
+          phoneNumber: '',
+          startServiceTime: '',
+          endServiceTime: '',
+          qrCode: undefined,
+          enterpriseWechatLink: ''
+        };
       }
       }
-      if (typeof form.value.qrCode === 'number') form.value.qrCode = String(form.value.qrCode);
     }
     }
   } catch (err) {}
   } catch (err) {}
 };
 };
@@ -101,12 +127,12 @@ const submitForm = () => {
       loading.value = true;
       loading.value = true;
       try {
       try {
         const submitData: CustomerServiceSettingForm = {
         const submitData: CustomerServiceSettingForm = {
-            id: form.value.id,
+            id: activeId.value,
             wechatAccount: form.value.wechatAccount,
             wechatAccount: form.value.wechatAccount,
             phoneNumber: form.value.phoneNumber,
             phoneNumber: form.value.phoneNumber,
             // 提交时补全日期前缀,以匹配后端的日期时间格式
             // 提交时补全日期前缀,以匹配后端的日期时间格式
-            startServiceTime: form.value.startServiceTime ? `1970-01-01 ${form.value.startServiceTime}:00` : '',
-            endServiceTime: form.value.endServiceTime ? `1970-01-01 ${form.value.endServiceTime}:00` : '',
+            startServiceTime: form.value.startServiceTime ? (form.value.startServiceTime.includes(' ') ? form.value.startServiceTime : `1970-01-01 ${form.value.startServiceTime}:00`) : '',
+            endServiceTime: form.value.endServiceTime ? (form.value.endServiceTime.includes(' ') ? form.value.endServiceTime : `1970-01-01 ${form.value.endServiceTime}:00`) : '',
             qrCode: form.value.qrCode,
             qrCode: form.value.qrCode,
             enterpriseWechatLink: form.value.enterpriseWechatLink
             enterpriseWechatLink: form.value.enterpriseWechatLink
         };
         };
@@ -130,6 +156,17 @@ onMounted(() => {
   padding-top: 10px;
   padding-top: 10px;
 }
 }
 
 
+.premium-tabs {
+  margin-bottom: 20px;
+  :deep(.el-tabs__header) {
+    border-bottom: 1px solid #e5e6eb;
+  }
+}
+
+.tab-content {
+  padding: 10px 0;
+}
+
 .premium-setting-form {
 .premium-setting-form {
   :deep(.el-form-item__label) {
   :deep(.el-form-item__label) {
     font-weight: 500;
     font-weight: 500;

+ 2 - 1
src/views/systemConfig/protocol/index.vue

@@ -8,7 +8,7 @@
             <el-icon class="info-icon">
             <el-icon class="info-icon">
               <InfoFilled />
               <InfoFilled />
             </el-icon>
             </el-icon>
-            <span>协议配置:目前支持用户协议、隐私政策、履约者说明、托运协议;请确保内容准确合规。</span>
+            <span>协议配置:目前支持用户协议、隐私政策、履约者说明、托运协议、宠物洗护服务规范;请确保内容准确合规。</span>
           </div>
           </div>
         </template>
         </template>
       </el-alert>
       </el-alert>
@@ -26,6 +26,7 @@
             <el-radio :label="2" border>隐私政策</el-radio>
             <el-radio :label="2" border>隐私政策</el-radio>
             <el-radio :label="3" border>履约者说明</el-radio>
             <el-radio :label="3" border>履约者说明</el-radio>
             <el-radio :label="4" border>托运协议</el-radio>
             <el-radio :label="4" border>托运协议</el-radio>
+            <el-radio :label="5" border>宠物洗护服务规范</el-radio>
           </el-radio-group>
           </el-radio-group>
         </el-form-item>
         </el-form-item>