Huanyi il y a 3 semaines
Parent
commit
ce6757b32e
38 fichiers modifiés avec 471 ajouts et 347 suppressions
  1. 1 1
      .env.production
  2. 13 1
      src/api/fulfiller/fulfiller/index.ts
  3. 7 0
      src/api/fulfiller/fulfiller/types.ts
  4. 6 4
      src/api/system/areaStation/types.ts
  5. 1 0
      src/assets/icons/svg/anamaly-upload.svg
  6. 1 0
      src/assets/icons/svg/archieves-management.svg
  7. 0 0
      src/assets/icons/svg/audit-management.svg
  8. 1 0
      src/assets/icons/svg/chain-brand.svg
  9. 1 0
      src/assets/icons/svg/customer-management.svg
  10. 1 0
      src/assets/icons/svg/fulfiller-management.svg
  11. 1 0
      src/assets/icons/svg/fulfiller-pool.svg
  12. 1 0
      src/assets/icons/svg/level-management.svg
  13. 1 0
      src/assets/icons/svg/merchant-classification.svg
  14. 0 0
      src/assets/icons/svg/order-dispatch.svg
  15. 0 0
      src/assets/icons/svg/order-list.svg
  16. 1 0
      src/assets/icons/svg/order-management.svg
  17. 1 0
      src/assets/icons/svg/pet-management.svg
  18. 1 0
      src/assets/icons/svg/platform-purchase.svg
  19. 1 0
      src/assets/icons/svg/service-list.svg
  20. 0 0
      src/assets/icons/svg/service-management.svg
  21. 0 0
      src/assets/icons/svg/store-info.svg
  22. 1 0
      src/assets/icons/svg/store-management.svg
  23. 1 0
      src/assets/icons/svg/tag-management.svg
  24. 8 0
      src/components/CustomerDetailDrawer/index.vue
  25. 8 8
      src/components/PetDetailDrawer/index.vue
  26. 95 61
      src/views/archieves/customer/index.vue
  27. 10 0
      src/views/archieves/pet/index.vue
  28. 76 86
      src/views/fulfiller/pool/index.vue
  29. 8 9
      src/views/order/dispatch/components/CustomerDetailDrawer.vue
  30. 9 12
      src/views/order/dispatch/components/DispatchDialog.vue
  31. 0 8
      src/views/order/dispatch/components/RiderListPanel.vue
  32. 43 20
      src/views/order/dispatch/index.vue
  33. 2 2
      src/views/order/orderList/components/CustomerDetailDrawer.vue
  34. 1 1
      src/views/order/orderList/index.vue
  35. 30 34
      src/views/order/purchase/components/AddUserDialog.vue
  36. 7 2
      src/views/system/areaStation/index.vue
  37. 132 97
      src/views/system/store/index.vue
  38. 1 1
      vite.config.ts

+ 1 - 1
.env.production

@@ -15,7 +15,7 @@ VITE_APP_MONITOR_ADMIN = '/admin/applications'
 VITE_APP_SNAILJOB_ADMIN = '/snail-job'
 
 # 生产环境
-VITE_APP_BASE_API = 'http://8.136.194.143/api'
+VITE_APP_BASE_API = 'http://www.hoomeng.pet/api'
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip

+ 13 - 1
src/api/fulfiller/fulfiller/index.ts

@@ -1,5 +1,6 @@
 import request from '@/utils/request';
-import type { FulfillerSearchQuery } from './types';
+import type { FulfillerSearchQuery, FulfillerGpsVO } from './types';
+import { AxiosPromise } from 'axios';
 
 /**
  * 模糊检索履约者(通过姓名和手机号)
@@ -12,3 +13,14 @@ export function listByNameAndPhoneNumber(query: FulfillerSearchQuery) {
         params: query
     });
 }
+
+/**
+ * 获取履约者实时 GPS 坐标
+ * @param id 履约者 ID
+ */
+export function getFulfillerGps(id: number | string): AxiosPromise<FulfillerGpsVO> {
+    return request({
+        url: `/fulfiller/fulfiller/gps/${id}`,
+        method: 'get'
+    });
+}

+ 7 - 0
src/api/fulfiller/fulfiller/types.ts

@@ -9,3 +9,10 @@ export interface FulfillerSearchResultVO {
     name: string;
     phoneNumber: string;
 }
+
+/** 履约者 GPS 坐标 */
+export interface FulfillerGpsVO {
+    id: number;
+    longitude: number;
+    latitude: number;
+}

+ 6 - 4
src/api/system/areaStation/types.ts

@@ -54,10 +54,12 @@ export interface AreaStationVO {
    */
   status: number;
 
-    /**
-     * 子对象
-     */
-    children: AreaStationVO[];
+  /**
+   * 子对象
+   */
+  children: AreaStationVO[];
+
+  parentId: number | string;
 }
 
 export interface AreaStationForm extends BaseEntity {

+ 1 - 0
src/assets/icons/svg/anamaly-upload.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774244269226" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18830" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M526.925944 110.523209c-212.498742 0-384.763131 172.264389-384.763131 384.763131s172.264389 384.763131 384.763131 384.763131 384.763131-172.264389 384.763131-384.763131S739.424686 110.523209 526.925944 110.523209zM494.184239 211.852012c7.05672-7.566326 16.136515-11.348466 27.237341-11.348466 13.619182 0 23.454178 4.043083 29.507034 12.105712 6.548136 9.587356 9.574053 21.444405 9.078773 35.55989l-9.834996 341.220429c-1.51347 12.613272-4.799306 21.940708-9.834996 27.993564-6.052856 6.560416-13.121855 9.834996-21.184485 9.834996-7.566326 0-13.879102-3.02694-18.914792-9.078773-5.556553-7.057743-8.073886-16.646122-7.566326-28.750811l-9.078773-341.220429C482.57483 233.036497 486.110353 220.930785 494.184239 211.852012zM563.033303 758.863411c-11.100826 11.088546-24.966625 16.645099-41.611723 16.645099-17.153682-0.50756-31.280424-6.312776-42.36897-17.401322-9.587356-11.609409-14.375405-24.459065-14.375405-38.585807 0.496304-16.137539 5.544273-29.754674 15.131629-40.8555 10.592242-10.593266 24.459065-15.888875 41.612747-15.888875 16.137539 0 29.754674 5.296633 40.856523 15.888875 10.591219 11.608386 15.886829 25.226544 15.886829 40.8555C577.657372 735.161592 572.60838 748.272191 563.033303 758.863411z" fill="currentColor" p-id="18831"></path></svg>

+ 1 - 0
src/assets/icons/svg/archieves-management.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774238400188" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6372" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M866.7 521.4c-4.1-4.7-10.1-6.2-16.2-7-5 0.1-9.9 1.8-14 4.8L791 562.8l88.7 87.5 40.1-43.7c4.7-4.1 6.3-9.9 7-16-0.1-6.2-2.8-12-7.4-16.2l-52.7-53z m-291.2-93.7l-257.7-0.2c-28.3 0.6-50.9-23.2-50.6-50.7-0.6-27.8 23.6-49.9 51.7-49.6l257.7 0.2c28.3-0.6 50.9 23.2 50.6 50.7-0.3 27.5-23.6 49.8-51.7 49.6zM472.6 628.6l-154.1-0.6c-28.3 0.6-50.9-23.2-50.6-50.7-0.6-27.8 23.6-49.9 51.7-49.6l153.3 0.3c28.3-0.6 50.8 23.2 50.6 50.7-0.3 27.5-22.6 49.3-50.9 49.9zM693.2 99l-469.7-1.8c-68-0.1-123.6 53.8-124.4 120.4l-1.8 586.3c0 66.8 55 121.3 123.6 122.6l170.5 0.4 0.5-108.5 422.9-422.8 0.1-173.6c2.4-68.9-52.8-122.7-121.7-123zM505.1 839.2l-0.2 87.3 88.9 0.3 258.1-254.3-88.7-87.5-258.1 254.2z m0 0" p-id="6373" fill="currentColor"></path></svg>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/icons/svg/audit-management.svg


+ 1 - 0
src/assets/icons/svg/chain-brand.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774238297079" class="icon" viewBox="0 0 1029 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3070" xmlns:xlink="http://www.w3.org/1999/xlink" width="1029" height="1024"><path d="M1027.583233 830.242636s-101.735397 20.449326-204.49326 41.409885l-137.010484 152.347479-147.235148-250.504244c-8.17973 0.511233-16.359461 1.533699-25.050424 1.5337s-16.870694-0.511233-25.050424-1.5337l-147.235148 250.504244-137.010484-152.347479c-102.757863-21.471792-204.49326-41.409885-204.49326-41.409885l160.015976-272.487269c-25.561658-51.634548-40.898652-109.403894-40.898652-170.240639C119.633158 173.308038 296.008595 0 513.793917 0s394.671992 173.308038 394.671992 387.514728c0 61.347978-14.825761 119.117324-40.898652 170.240639l160.015976 272.487269zM513.793917 160.015976c-124.229656 0-224.942586 100.201697-224.942586 223.92012s100.712931 223.92012 224.942586 223.92012 224.942586-100.201697 224.942586-223.92012-100.712931-223.92012-224.942586-223.92012z m72.083874 250.504244c-2.044933 1.533699-2.556166 4.089865-2.044932 6.134797l20.960559 88.443335c1.533699 5.623565-4.601098 10.224663-9.71343 6.646031l-77.707439-47.544683c-2.044933-1.533699-4.601098-1.533699-6.646031 0l-77.707439 47.544683c-5.112332 2.556166-11.247129-1.533699-9.713429-6.646031l20.960559-88.443335c0.511233-2.044933-0.511233-4.601098-2.044933-6.134797L372.693568 351.217174c-4.601098-3.578632-2.044933-10.735896 3.578632-11.247129l90.9995-7.157264c2.556166-0.511233 4.601098-1.533699 5.623565-4.089865l35.275087-83.842237c2.044933-5.623565 9.71343-5.623565 11.758363 0l35.275087 83.842237c1.022466 2.044933 3.067399 3.578632 5.623565 4.089865l90.999501 7.157264c5.623565 0.511233 8.17973 7.668497 3.578632 11.247129L585.877791 410.52022z" fill="currentColor" p-id="3071"></path></svg>

+ 1 - 0
src/assets/icons/svg/customer-management.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774244120239" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11794" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M502.496 63.136c125.888 0 227.936 100.384 227.936 224.192 0 123.84-102.048 224.224-227.936 224.224-125.888 0-227.936-100.384-227.936-224.224C274.56 163.488 376.64 63.136 502.496 63.136L502.496 63.136zM502.496 63.136c125.888 0 227.936 100.384 227.936 224.192 0 123.84-102.048 224.224-227.936 224.224-125.888 0-227.936-100.384-227.936-224.224C274.56 163.488 376.64 63.136 502.496 63.136L502.496 63.136zM417.024 586.304l189.984 0c162.624 0 294.432 129.632 294.432 289.6l0 18.656c0 63.04-131.84 65.44-294.432 65.44l-189.984 0c-162.624 0-294.432-0.096-294.432-65.44l0-18.656C122.592 715.936 254.4 586.304 417.024 586.304L417.024 586.304zM417.024 586.304" fill="currentColor" p-id="11795"></path></svg>

+ 1 - 0
src/assets/icons/svg/fulfiller-management.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774238417184" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7577" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M776.96 256a42.666667 42.666667 0 0 1 41.514667 32.789333l72.896 306.176a149.333333 149.333333 0 1 1-251.285334 114.176L640 704H384a149.333333 149.333333 0 0 1-298.581333 5.12L85.333333 704V490.666667a21.333333 21.333333 0 0 1 21.333334-21.333334h202.666666c80.554667 0 151.168 42.517333 190.656 106.346667l2.176-0.106667a148.821333 148.821333 0 0 0 72.042667-25.109333l3.882667-2.709333a149.994667 149.994667 0 0 0 24.746666-22.528l3.072-3.562667a149.76 149.76 0 0 0 16.128-23.914667l2.176-4.181333A148.693333 148.693333 0 0 0 640 426.666667v-152.362667l0.192 0.213333A21.333333 21.333333 0 0 1 661.333333 256h115.626667zM234.666667 618.666667a85.333333 85.333333 0 1 0 0 170.666666 85.333333 85.333333 0 0 0 0-170.666666z m554.666666 0a85.333333 85.333333 0 1 0 0 170.666666 85.333333 85.333333 0 0 0 0-170.666666zM362.666667 170.666667a64 64 0 0 1 64 64v106.666666a64 64 0 0 1-64 64H192a64 64 0 0 1-64-64v-106.666666a64 64 0 0 1 64-64h170.666667z m437.333333 0a32 32 0 0 1 0 64h-149.333333a32 32 0 0 1 0-64h149.333333z" fill="currentColor" p-id="7578"></path></svg>

+ 1 - 0
src/assets/icons/svg/fulfiller-pool.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774244208357" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15541" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M853.333333 597.333333h-85.333333v-51.2l-256-256-256 256V768h341.333333v85.333333H170.666667v-298.666666H128l384-384 384 384h-42.666667v42.666666z m-106.666666 128c-34.133333 0-64-29.866667-64-64s29.866667-64 64-64 64 29.866667 64 64-29.866667 64-64 64zM682.666667 725.333333h128l42.666666 42.666667v85.333333h-213.333333v-85.333333l42.666667-42.666667z" fill="currentColor" p-id="15542"></path></svg>

+ 1 - 0
src/assets/icons/svg/level-management.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774244230850" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16639" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M810.666667 789.333333l21.333333 149.333334-149.333333-64-149.333334 64 21.333334-149.333334-106.666667-106.666666 149.333333-21.333334 85.333334-128 85.333333 128 149.333333 21.333334zM448 469.333333a192 192 0 1 1 192-192 192 192 0 0 1-192 192z m0 42.666667h85.333333a199.466667 199.466667 0 0 1 84.266667 40.106667L576 618.666667l-192 42.666666 128 149.333334-23.253333 128H256c-106.666667 0-149.333333-70.613333-149.333333-170.666667s202.666667-256 256-256h85.333333z" p-id="16640" fill="currentColor"></path></svg>

+ 1 - 0
src/assets/icons/svg/merchant-classification.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774245665561" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1708" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M180.256 582.4c28.8 0 52.288-23.488 52.288-52.256 0-28.8-23.52-52.288-52.288-52.288-28.768 0-52.256 23.52-52.256 52.288 0 28.768 23.488 52.256 52.256 52.256z m0 298.656c28.8 0 52.288-23.488 52.288-52.256s-23.52-52.256-52.288-52.256C151.488 776.544 128 800 128 828.8s23.488 52.256 52.256 52.256z m0-597.312c28.8 0 52.288-23.52 52.288-52.288 0-28.768-23.52-52.256-52.288-52.256C151.488 179.2 128 202.688 128 231.456c0 28.8 23.488 52.288 52.256 52.288zM862.944 582.4c28.768 0 52.256-23.488 52.256-52.256 0-28.8-23.488-52.288-52.256-52.288h-512c-28.8 0-52.288 23.52-52.288 52.288 0 28.768 23.52 52.256 52.288 52.256h512z m0 298.656c28.768 0 52.256-23.488 52.256-52.256s-23.488-52.256-52.256-52.256h-512c-28.8 0-52.288 23.488-52.288 52.256s23.52 52.256 52.288 52.256h512zM298.656 231.456c0 28.8 23.52 52.288 52.288 52.288h512c28.768 0 52.256-23.52 52.256-52.288 0-28.768-23.488-52.256-52.256-52.256h-512c-28.8 0-52.288 23.488-52.288 52.256z" p-id="1709" fill="currentColor"></path></svg>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/icons/svg/order-dispatch.svg


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/icons/svg/order-list.svg


+ 1 - 0
src/assets/icons/svg/order-management.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774238437887" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8691" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M634.88 969.728H348.16v-81.92h286.72c177.152 0 239.616-155.648 239.616-301.056v-450.56H267.264v514.048h-81.92V136.192c0-45.056 36.864-81.92 81.92-81.92h608.256c45.056 0 81.92 36.864 81.92 81.92v449.536C957.44 819.2 830.464 969.728 634.88 969.728z" fill="currentColor" p-id="8692"></path><path d="M629.76 969.728h-399.36c-90.112 0-163.84-62.464-163.84-138.24v-49.152c0-76.8 73.728-138.24 163.84-138.24h402.432v81.92H230.4c-44.032 0-81.92 25.6-81.92 56.32v49.152c0 29.696 37.888 56.32 81.92 56.32h399.36v81.92zM717.824 328.704H423.936c-22.528 0-40.96-18.432-40.96-40.96s18.432-40.96 40.96-40.96h293.888c22.528 0 40.96 18.432 40.96 40.96s-18.432 40.96-40.96 40.96zM641.024 503.808H423.936c-22.528 0-40.96-18.432-40.96-40.96s18.432-40.96 40.96-40.96h217.088c22.528 0 40.96 18.432 40.96 40.96s-18.432 40.96-40.96 40.96z" fill="currentColor" p-id="8693"></path><path d="M632.832 951.296c-84.992 0-154.624-69.632-154.624-154.624s69.632-154.624 154.624-154.624 154.624 69.632 154.624 154.624-69.632 154.624-154.624 154.624z m0-226.304c-39.936 0-72.704 32.768-72.704 72.704s32.768 72.704 72.704 72.704c39.936 0 72.704-32.768 72.704-72.704s-32.768-72.704-72.704-72.704z" fill="currentColor" p-id="8694"></path></svg>

+ 1 - 0
src/assets/icons/svg/pet-management.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774244149150" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13110" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M314.9824 571.35104c78.2336-17.24416 103.2192-87.28576 68.9152-166.4-13.55776-31.3344-68.28032-138.69056-90.7264-182.59968-12.14464 10.19904-22.17984 21.9136-29.22496 34.93888-26.74688 49.27488-28.59008 158.65856-42.06592 295.23968 24.3712 16.1792 55.7056 27.01312 93.10208 18.8416z m558.98112-184.79104a31.47776 31.47776 0 0 0-34.67264-30.18752c-30.35136 3.072-77.08672 3.072-116.9408-17.08032-65.1264-32.99328-37.0688-80.60928-90.8288-130.8672-54.4768-50.93376-194.21184-56.48384-288.01024-16.26112 36.78208 74.71104 90.7264 186.20416 103.64928 222.28992 37.15072 104.18176-20.31616 188.8256-109.11744 215.67488-52.0192 15.7696-93.02016 7.22944-123.71968-9.216a2106.1632 2106.1632 0 0 1-9.07264 63.95904c-9.58464 60.2112-22.4256 110.08-35.10272 149.42208a59.55584 59.55584 0 0 0 57.89696 92.93824l0.53248 0.512h416.23552c0.96256 0.1024 2.02752 0.18432 2.99008 0.18432a35.75808 35.75808 0 0 0 31.86688-51.9168 237.85472 237.85472 0 0 0-97.50528-139.38688c1.8432-39.07584 10.99776-74.97728 33.25952-98.57024 46.36672-49.19296 162.97984-12.67712 226.24256-96.09216 31.15008-41.2672 33.8944-112.64 32.29696-155.40224z m-292.2496 15.29856a45.01504 45.01504 0 0 1-44.9536-45.056c0-24.90368 20.15232-45.056 44.9536-45.056 24.82176 0 44.97408 20.15232 44.97408 45.056 0 24.90368-20.15232 45.056-44.97408 45.056z" fill="currentColor" p-id="13111"></path></svg>

+ 1 - 0
src/assets/icons/svg/platform-purchase.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774244299057" class="icon" viewBox="0 0 1112 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19905" xmlns:xlink="http://www.w3.org/1999/xlink" width="1112" height="1024"><path d="M1112.702976 772.388571V81.92A105.325714 105.325714 0 0 0 1013.22869 0h-936.228571a111.177143 111.177143 0 0 0-58.514286 46.811429A128.731429 128.731429 0 0 0 0.931547 128.731429v643.657142a111.177143 111.177143 0 0 0 46.811429 87.771429 175.542857 175.542857 0 0 0 76.068571 11.702857h392.045715v76.068572H211.582976a35.108571 35.108571 0 0 0-35.108571 35.108571 40.96 40.96 0 0 0 35.108571 40.96h690.468571a40.96 40.96 0 0 0 35.108572-40.96 35.108571 35.108571 0 0 0-35.108572-35.108571h-304.274285v-76.068572h444.708571a99.474286 99.474286 0 0 0 70.217143-99.474286zM100.405833 76.068571z m-11.702857 5.851429z m0 0z m-5.851429 702.171429z m5.851429 5.851428z m942.08-11.702857v11.702857H77.000119V99.474286h5.851428a5.851429 5.851429 0 0 0 5.851429-5.851429h947.931429v678.765714h-5.851429z" p-id="19906" fill="currentColor"></path><path d="M416.382976 397.897143a40.96 40.96 0 0 0-40.96 40.96 40.96 40.96 0 0 0 81.92 0 40.96 40.96 0 0 0-40.96-40.96zM556.817262 397.897143a40.96 40.96 0 0 0-40.96 40.96 40.96 40.96 0 0 0 81.92 0 40.96 40.96 0 0 0-40.96-40.96zM697.251547 397.897143a40.96 40.96 0 0 0-40.96 40.96 40.96 40.96 0 0 0 81.92 0 40.96 40.96 0 0 0-40.96-40.96z" p-id="19907" fill="currentColor"></path></svg>

+ 1 - 0
src/assets/icons/svg/service-list.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774244079385" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10702" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M163.2 654.08a378.88 378.88 0 0 0 12.16-97.92V448a181.76 181.76 0 0 1 357.76-45.44 32 32 0 1 0 64-16 245.12 245.12 0 0 0-209.28-180.48 31.36 31.36 0 0 0 0-3.84 33.28 33.28 0 0 0-64 0 31.36 31.36 0 0 0 0 5.12A245.12 245.12 0 0 0 112 448v108.16A228.48 228.48 0 0 1 69.76 707.2a31.36 31.36 0 0 0-7.04 11.52 71.68 71.68 0 0 0-12.8 38.4c0 87.04 154.24 132.48 307.2 132.48s307.2-45.44 307.2-132.48c0-126.72-323.84-160.64-501.12-103.04z m192 171.52c-158.08 0-243.2-48.64-243.2-68.48a14.72 14.72 0 0 1 3.2-7.68 307.2 307.2 0 0 1 174.72-55.04 64 64 0 0 0 0 11.52 64 64 0 1 0 128 0 64 64 0 0 0 0-11.52c117.76 10.24 180.48 48.64 180.48 64s-86.4 67.2-241.28 67.2z m583.68-499.84a32 32 0 0 0-32-32H666.24a32 32 0 0 0 0 64h240.64a32 32 0 0 0 32-32z m-32 167.68H737.92a32 32 0 0 0 0 64h168.96a32 32 0 0 0 0-64z m0 199.04H786.56a32 32 0 0 0 0 64h120.32a32 32 0 0 0 0-64z" fill="currentColor" p-id="10703"></path></svg>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/icons/svg/service-management.svg


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/icons/svg/store-info.svg


+ 1 - 0
src/assets/icons/svg/store-management.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774238324639" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4190" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M871.850667 469.333333c-72.618667 0-125.184-38.186667-125.184-85.333333 0 47.146667-52.650667 85.333333-125.141334 85.333333C564.565333 469.333333 512 431.146667 512 384c0 47.189333-52.650667 85.333333-109.482667 85.333333C329.898667 469.333333 277.333333 431.146667 277.333333 384c0 47.104-52.650667 85.333333-109.482666 85.333333-32.512 0-61.013333-7.637333-82.517334-20.309333v489.344A42.624 42.624 0 0 0 127.658667 981.333333H554.666667v-255.872A42.666667 42.666667 0 0 1 597.333333 682.666667h128c23.594667 0 42.666667 18.688 42.666667 42.794666V981.333333h128.384c22.869333 0 42.24-19.242667 42.24-42.965333v-487.68a129.194667 129.194667 0 0 1-66.773333 18.645333M840.533333 74.666667c-5.632-15.744-34.858667-32-62.592-32H246.016c-27.946667 0-57.045333 16.256-62.549333 32L42.666667 298.666667c0 47.189333 52.522667 85.333333 125.141333 85.333333C224.64 384 277.333333 345.770667 277.333333 298.666667c0 47.146667 52.522667 85.333333 125.141334 85.333333C459.306667 384 512 345.856 512 298.666667c0 47.146667 52.522667 85.333333 109.482667 85.333333 72.533333 0 125.184-38.186667 125.184-85.333333 0 47.146667 52.522667 85.333333 125.141333 85.333333C928.64 384 981.333333 345.770667 981.333333 298.666667l-140.8-224z" fill="currentColor" p-id="4191"></path></svg>

+ 1 - 0
src/assets/icons/svg/tag-management.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774244167111" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14175" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M932.987763 96 720.625878 96C670.569149 96 600.654688 124.958956 565.258512 160.355132L208.570182 517.043462C173.175902 552.437741 173.175902 610.357549 208.570182 645.753725L474.246275 911.426026C509.640554 946.822202 567.560362 946.822202 602.956538 911.426026L959.641075 554.741488C995.041043 519.345312 1024 449.430851 1024 399.374122L1024 187.012236C1024 136.955506 983.044493 96 932.987763 96L932.987763 96ZM811.638115 399.374122C761.372816 399.374122 720.625878 358.627184 720.625878 308.361884 720.625878 258.096585 761.372816 217.349648 811.638115 217.349648 861.903414 217.349648 902.650352 258.096585 902.650352 308.361884 902.650352 358.627184 861.903414 399.374122 811.638115 399.374122L811.638115 399.374122ZM74.653261 602.847142 399.242714 927.436595C364.862842 945.60112 321.065098 940.273114 292.221804 911.426026L26.54571 645.753725C-8.84857 610.357549-8.84857 552.437741 26.54571 517.043462L383.234038 160.355132C418.630214 124.958956 488.544678 96 538.601408 96L74.653261 559.94625C62.8558 571.745606 62.8558 591.049683 74.653261 602.847142L74.653261 602.847142Z" p-id="14176"></path></svg>

+ 8 - 0
src/components/CustomerDetailDrawer/index.vue

@@ -139,6 +139,10 @@ const props = defineProps({
   editable: {
     type: Boolean,
     default: false
+  },
+  areaStationList: {
+    type: Array,
+    default: () => []
   }
 });
 
@@ -186,6 +190,10 @@ const getStatusTagType = (status) => {
 };
 
 const loadAreaStation = async () => {
+  if (props.areaStationList && props.areaStationList.length > 0) {
+    allNodes.value = props.areaStationList;
+    return;
+  }
   if (allNodes.value.length === 0) {
     const res = await listOnStore();
     allNodes.value = res.data || [];

+ 8 - 8
src/components/PetDetailDrawer/index.vue

@@ -120,7 +120,6 @@ import { ref, computed, watch, onMounted, getCurrentInstance, toRefs } from 'vue
 import { getPet, updatePet } from '@/api/archieves/pet'
 import { listAllChangeLog } from '@/api/archieves/changeLog'
 import { listSubOrderOnPet } from '@/api/order/subOrder/index'
-import { listAllService } from '@/api/service/list/index'
 import { ElMessage } from 'element-plus'
 
 const props = defineProps({
@@ -135,6 +134,11 @@ const props = defineProps({
   editable: {
     type: Boolean,
     default: false
+  },
+  // 父组件传入的服务列表,避免重复请求接口
+  serviceList: {
+    type: Array,
+    default: () => []
   }
 })
 
@@ -153,16 +157,12 @@ const { sys_pet_gender, sys_pet_size, sys_house_type, sys_entry_method } = toRef
 const currentPet = ref({})
 const changeLogs = ref([])
 const historyOrders = ref([])
-const serviceOptions = ref([])
 const detailActiveTab = ref('info')
 const remarkDialogVisible = ref(false)
 const remarkContent = ref('')
 
-const getServiceList = () => {
-  listAllService().then((res) => {
-    serviceOptions.value = res.data || []
-  })
-}
+// 直接使用父组件传入的服务列表数据
+const serviceOptions = computed(() => props.serviceList || [])
 
 const getServiceName = (serviceId) => {
   const item = serviceOptions.value.find((i) => i.id === serviceId)
@@ -225,7 +225,7 @@ watch(() => props.visible, (val) => {
 })
 
 onMounted(() => {
-  getServiceList()
+  // 服务列表数据由父组件传入,无需在这里请求
 })
 </script>
 

+ 95 - 61
src/views/archieves/customer/index.vue

@@ -5,12 +5,15 @@
         <div class="card-header">
           <span class="title">用户管理</span>
           <div class="header-actions">
-            <el-select v-model="searchForm.areaId" placeholder="所属区域" style="width: 150px; margin-right: 10px" clearable @change="onSearchAreaChange">
-              <el-option v-for="area in areaList" :key="area.id" :label="area.name" :value="area.id" />
-            </el-select>
-            <el-select v-model="searchForm.stationId" placeholder="所属站点" style="width: 150px; margin-right: 10px" clearable @change="handleSearch">
-              <el-option v-for="station in filteredStationList" :key="station.id" :label="station.name" :value="station.id" />
-            </el-select>
+            <el-cascader
+              v-model="searchAreaValue"
+              :options="areaTreeOptions"
+              :props="{ checkStrictly: true, value: 'id', label: 'name' }"
+              placeholder="所属站点"
+              style="width: 350px; margin-right: 10px"
+              clearable
+              @change="onSearchAreaChange"
+            />
             <el-input v-model="searchForm.keyword" placeholder="搜索姓名/手机号" style="width: 200px; margin-right: 10px;" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
             <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['archieves:customer:add']">新增用户</el-button>
           </div>
@@ -108,6 +111,8 @@
       v-model:visible="drawerVisible"
       :customer-id="currentUser.id"
       editable
+      :service-list="serviceList"
+      :area-station-list="allNodes"
       @add-pet="openAddPet"
       @pet-detail="handlePetDetail"
       @pet-edit="handlePetEdit"
@@ -119,7 +124,7 @@
     <PetDetailDrawer
       v-model:visible="petDrawerVisible"
       :pet-id="currentPet.id"
-      editable
+      :service-list="serviceList"
       @remark-saved="getList"
     />
 
@@ -144,19 +149,6 @@
                 @visible-change="handleBrandVisibleChange" />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item label="所属区域">
-              <el-cascader v-model="formAreaValue" :options="areaTreeOptions" placeholder="请选择区域"
-                style="width: 100%" clearable @change="handleFormAreaChange" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="所属站点">
-              <el-select v-model="form.stationId" style="width: 100%" filterable placeholder="请选择站点" clearable :disabled="!form.areaId">
-                <el-option v-for="station in formStationList" :key="station.id" :label="station.name" :value="station.id" />
-              </el-select>
-            </el-form-item>
-          </el-col>
           <el-col :span="12">
             <el-form-item label="姓名" required><el-input v-model="form.name" placeholder="请输入姓名" /></el-form-item>
           </el-col>
@@ -170,6 +162,12 @@
               </el-select>
             </el-form-item>
           </el-col>
+          <el-col :span="24">
+            <el-form-item label="所属站点">
+              <el-cascader v-model="formAreaValue" :options="areaTreeOptions" :props="{ value: 'id', label: 'name' }" placeholder="请选择站点"
+                style="width: 100%" clearable @change="handleFormAreaChange" />
+            </el-form-item>
+          </el-col>
 
           <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
           <el-col :span="24">
@@ -243,6 +241,17 @@
       </template>
     </el-dialog>
 
+    <!-- Pet Remark Dialog -->
+    <el-dialog v-model="petRemarkDialogVisible" title="添加宠物备注" width="400px" append-to-body>
+      <el-input v-model="remarkForm.content" type="textarea" :rows="3" placeholder="请输入宠物备注内容..." />
+      <template #footer>
+            <span class="dialog-footer">
+                <el-button @click="petRemarkDialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="savePetRemark">保存</el-button>
+            </span>
+      </template>
+    </el-dialog>
+
     <!-- Full Add/Edit Pet Dialog -->
     <el-dialog v-model="petDialogVisible" :title="petForm.id ? '编辑宠物' : '新增宠物'" width="800px">
       <el-tabs v-model="petDialogActiveTab">
@@ -389,12 +398,16 @@ import { listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer, ch
 import { listAllTag } from '@/api/archieves/tag'
 import { listPetByUser, addPet, updatePet, delPet } from '@/api/archieves/pet'
 import { listAllChangeLog } from '@/api/archieves/changeLog'
-import { listOnStore } from '@/api/system/areaStation'
+import { listAreaStation } from '@/api/system/areaStation'
 import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
 import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
 import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
+import { listAllService } from '@/api/service/list/index'
 import { regionData, codeToText } from 'element-china-area-data'
 import PageSelect from '@/components/PageSelect/index.vue'
+import { useUserStore } from '@/store/modules/user'
+
+const userStore = useUserStore()
 
 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(
@@ -406,32 +419,25 @@ const submitLoading = ref(false)
 const total = ref(0)
 
 const allNodes = ref([])
-const areaList = computed(() => allNodes.value.filter(n => n.type === 1))
-const filteredStationList = computed(() => {
-  const areaId = searchForm.areaId
-  const stations = allNodes.value.filter(n => n.type === 2)
-  if (areaId) {
-    return stations.filter(s => s.parentId === areaId)
-  }
-  return stations
-})
-const formStationList = computed(() => {
-  const areaId = form.areaId
-  const stations = allNodes.value.filter(n => n.type === 2)
-  if (areaId) {
-    return stations.filter(s => s.parentId === areaId)
-  }
-  return stations
-})
+// 废弃变量已清理,统一使用级联选择器展示全量树结构
 
 const loadAreaStation = () => {
-  listOnStore().then((res) => {
+  listAreaStation().then((res) => {
     allNodes.value = res.data || []
   })
 }
 
-const onSearchAreaChange = () => {
-  searchForm.stationId = undefined
+const searchAreaValue = ref([])
+const onSearchAreaChange = (value) => {
+  if (value && value.length > 0) {
+    const lastId = value[value.length - 1];
+    const node = allNodes.value.find(item => item.id === lastId);
+    searchForm.stationId = lastId;
+    searchForm.areaId = node ? node.parentId : undefined;
+  } else {
+    searchForm.areaId = undefined;
+    searchForm.stationId = undefined;
+  }
   handleSearch()
 }
 
@@ -454,6 +460,7 @@ const dialogVisible = ref(false)
 const drawerVisible = ref(false)
 const petDrawerVisible = ref(false)
 const remarkDialogVisible = ref(false)
+const petRemarkDialogVisible = ref(false)
 const petDialogVisible = ref(false)
 const isEdit = ref(false)
 const detailActiveTab = ref('info')
@@ -478,6 +485,13 @@ const formAreaValue = ref([])
 const regionCascaderValue = ref([])
 
 const customerDetailRef = ref(null)
+const serviceList = ref([])
+
+const getServiceList = () => {
+  listAllService().then((res) => {
+    serviceList.value = res.data || []
+  })
+}
 
 const form = reactive({
   id: undefined,
@@ -544,21 +558,23 @@ const areaTreeOptions = computed(() => {
       .filter(item => String(item.parentId) === String(parentId))
       .map(item => {
         const children = buildTree(data, item.id)
-        const node = { value: item.id, label: item.name }
+        const node = { id: item.id, name: item.name }
         if (children.length > 0) node.children = children
         return node
       })
   }
-  const areaData = allNodes.value.filter(n => n.type === 0 || n.type === 1)
-  return buildTree(areaData, 0)
+  return buildTree(allNodes.value, 0)
 })
 
 const handleFormAreaChange = (value) => {
-  form.stationId = undefined
   if (value && value.length > 0) {
-    form.areaId = value[value.length - 1]
+    const lastId = value[value.length - 1];
+    const node = allNodes.value.find(item => item.id === lastId);
+    form.stationId = lastId;
+    form.areaId = node ? node.parentId : undefined;
   } else {
-    form.areaId = undefined
+    form.stationId = undefined;
+    form.areaId = undefined;
   }
 }
 
@@ -643,21 +659,20 @@ const handleEdit = (row) => {
     })
     userAvatarDisplayUrl.value = data.avatarUrl || ''
     // Restore area cascader value path
-    if (data.areaId) {
-      const findPath = (nodes, targetId, path = []) => {
-        for (const node of nodes) {
-          const currentPath = [...path, node.id]
-          if (node.id === targetId) return currentPath
-          const children = allNodes.value.filter(n => (n.type === 0 || n.type === 1) && String(n.parentId) === String(node.id))
-          if (children.length > 0) {
-            const result = findPath(children, targetId, currentPath)
-            if (result) return result
-          }
+    if (data.stationId || data.areaId) {
+      const targetId = data.stationId || data.areaId;
+      const path = [];
+      let currentId = targetId;
+      while (currentId && String(currentId) !== '0') {
+        path.unshift(currentId);
+        const currentArea = allNodes.value.find((item) => String(item.id) === String(currentId));
+        if (currentArea) {
+          currentId = currentArea.parentId;
+        } else {
+          break;
         }
-        return null
       }
-      const roots = allNodes.value.filter(n => (n.type === 0 || n.type === 1) && String(n.parentId) === '0')
-      formAreaValue.value = findPath(roots, data.areaId) || []
+      formAreaValue.value = path;
     } else {
       formAreaValue.value = []
     }
@@ -728,6 +743,23 @@ const saveRemark = () => {
   })
 }
 
+const savePetRemark = () => {
+  if (!remarkForm.content) return ElMessage.warning('请输入内容')
+  const data = {
+    id: currentPet.value.id,
+    remark: remarkForm.content
+  }
+  updatePet(data).then(() => {
+    ElMessage.success('宠物备注添加成功')
+    petRemarkDialogVisible.value = false
+    if (drawerVisible.value) {
+      loadDetailPets(currentUser.value.id)
+      loadDetailLogs(currentUser.value.id, 'customer')
+    }
+    getList()
+  })
+}
+
 const saveUser = () => {
   if (!form.name) return ElMessage.warning('请输入姓名')
   if (!form.phone) return ElMessage.warning('请输入电话')
@@ -893,7 +925,8 @@ const handlePetEdit = (row) => {
 
 const handlePetRemark = (row) => {
   currentPet.value = row
-  petDrawerVisible.value = true
+  remarkForm.content = ''
+  petRemarkDialogVisible.value = true
 }
 
 const handlePetDelete = (row) => {
@@ -926,6 +959,7 @@ onMounted(() => {
   loadTags()
   loadAreaStation()
   getBrandList()
+  getServiceList()
 })
 </script>
 

+ 10 - 0
src/views/archieves/pet/index.vue

@@ -209,6 +209,7 @@
     <PetDetailDrawer
       v-model:visible="drawerVisible"
       :pet-id="currentPet.id"
+      :service-list="serviceList"
       editable
       @remark-saved="getList"
     />
@@ -223,6 +224,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
 import { listPet, getPet, addPet, updatePet, delPet } from '@/api/archieves/pet'
 import { listAllTag } from '@/api/archieves/tag'
 import { listAllCustomer } from '@/api/archieves/customer'
+import { listAllService } from '@/api/service/list/index'
 import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 
 const { proxy } = getCurrentInstance();
@@ -253,6 +255,13 @@ const currentPet = ref({});
 
 const allPetTags = ref([]);
 const userList = ref([]);
+const serviceList = ref([]);
+
+const getServiceList = () => {
+  listAllService().then((res) => {
+    serviceList.value = res.data || []
+  })
+}
 
 const avatarDisplayUrl = ref('')
 const vaccineCertDisplayUrl = ref('')
@@ -447,6 +456,7 @@ onMounted(() => {
   getList();
   loadTags();
   loadUsers();
+  getServiceList();
 });
 </script>
 

+ 76 - 86
src/views/fulfiller/pool/index.vue

@@ -11,14 +11,15 @@
             <el-button type="primary" icon="Plus" style="margin-right: 15px" @click="handleCreate" v-hasPermi="['fulfiller:pool:add']">新增履约者</el-button>
             <el-input v-model="searchKey" placeholder="搜索姓名/手机号/身份证" class="search-input" prefix-icon="Search" clearable
               @keyup.enter="handleSearch" @clear="handleSearch" />
-            <el-cascader v-model="filterCascaderValue" :options="cityCascaderOptions" :props="{ checkStrictly: true }"
-              placeholder="所属城市/区域" clearable style="width: 200px; margin-left: 10px;"
-              @change="handleFilterCascaderChange" />
-            <el-select v-model="queryParams.stationId" placeholder="所属站点" style="width: 150px; margin-left: 10px;"
-              clearable @change="getList">
-              <el-option v-for="station in stationOptions" :key="station.id" :label="station.name"
-                :value="station.id" />
-            </el-select>
+            <el-cascader
+              v-model="filterCascaderValue"
+              :options="areaTreeOptions"
+              :props="{ checkStrictly: true, value: 'id', label: 'name' }"
+              placeholder="所属站点"
+              clearable
+              style="width: 350px; margin-left: 10px;"
+              @change="handleFilterCascaderChange"
+            />
           </div>
         </div>
 
@@ -478,23 +479,11 @@
           </el-col>
         </el-row>
 
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="服务城市">
-              <el-cascader v-model="editDialog.cascaderValue" :options="cityCascaderOptions"
-                :props="{ checkStrictly: true }" placeholder="请选择城市/区域" clearable style="width: 100%"
-                @change="handleEditCascaderChange" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="归属站点">
-              <el-select v-model="editDialog.form.stationId" placeholder="请选择站点" style="width: 100%">
-                <el-option v-for="station in editDialog.stationOptions" :key="station.id" :label="station.name"
-                  :value="station.id" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row>
+        <el-form-item label="所属站点">
+          <el-cascader v-model="editDialog.cascaderValue" :options="areaTreeOptions"
+            :props="{ value: 'id', label: 'name' }" placeholder="请选择站点" clearable style="width: 100%"
+            @change="handleEditCascaderChange" />
+        </el-form-item>
 
         <el-row :gutter="20">
           <el-col :span="12">
@@ -583,17 +572,11 @@
             <el-radio label="1">女</el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label="服务城市">
-          <el-cascader v-model="createDialog.cascaderValue" :options="cityCascaderOptions"
-            :props="{ checkStrictly: true }" placeholder="请选择城市/区域" clearable style="width: 100%"
+        <el-form-item label="所属站点">
+          <el-cascader v-model="createDialog.cascaderValue" :options="areaTreeOptions"
+            :props="{ value: 'id', label: 'name' }" placeholder="请选择站点" clearable style="width: 100%"
             @change="handleCreateCascaderChange" />
         </el-form-item>
-        <el-form-item label="归属站点">
-          <el-select v-model="createDialog.form.stationId" placeholder="请选择站点" style="width: 100%">
-            <el-option v-for="station in createDialog.stationOptions" :key="station.id" :label="station.name"
-              :value="station.id" />
-          </el-select>
-        </el-form-item>
         <el-form-item label="技能标签">
           <el-select v-model="createDialog.form.tagIds" multiple placeholder="请选择技能标签" style="width: 100%">
             <el-option v-for="tag in allTags" :key="tag.id" :label="tag.name" :value="tag.id" />
@@ -716,8 +699,8 @@ import type {
 } from '@/api/fulfiller/pool/types'
 import { listAllTag } from '@/api/fulfiller/tag'
 import type { FlfTagVO } from '@/api/fulfiller/tag/types'
-import { listOnStore } from '@/api/system/areaStation'
-import type { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types'
+import { listAreaStation as listOnStore } from '@/api/system/areaStation'
+import type { AreaStationVO as SysAreaStationOnStoreVo } from '@/api/system/areaStation/types'
 import fulfillerEnums from '@/enums/fulfiller.json'
 import ImageUpload from '@/components/ImageUpload/index.vue'
 import { listAllLevelRights, addLevelRights, updateLevelRights, delLevelRights, changeLevelRightsStatus } from '@/api/fulfiller/levelRights';
@@ -730,8 +713,19 @@ const total = ref(0)
 const tableData = ref<FlfFulfillerVO[]>([])
 const allTags = ref<FlfTagVO[]>([])
 const areaStationList = ref<SysAreaStationOnStoreVo[]>([])
-const cityCascaderOptions = ref<any[]>([])
-const stationOptions = ref<SysAreaStationOnStoreVo[]>([])
+const areaTreeOptions = computed(() => {
+  const buildTree = (data: any[], parentId: any): any[] => {
+    return data
+      .filter(item => String(item.parentId) === String(parentId))
+      .map(item => {
+        const children = buildTree(data, item.id);
+        const res: any = { id: item.id, name: item.name };
+        if (children && children.length > 0) res.children = children;
+        return res;
+      });
+  };
+  return buildTree(areaStationList.value, 0);
+});
 const filterCascaderValue = ref<any[]>([])
 
 const queryParams = reactive<FlfFulfillerQuery>({
@@ -804,25 +798,12 @@ const loadAllTags = async () => {
 const loadAreaStations = async () => {
   try {
     const res = await listOnStore()
-    const list = res.data || []
-    areaStationList.value = list
-    // 构建城市→区域的级联树(不含站点type=2)
-    const cities = list.filter(item => item.type === 0)
-    cityCascaderOptions.value = cities.map(city => {
-      const districts = list.filter(d => d.parentId == city.id && d.type === 1)
-      return {
-        value: city.id,
-        label: city.name,
-        children: districts.length > 0 ? districts.map(d => ({ value: d.id, label: d.name })) : undefined
-      }
-    })
+    areaStationList.value = res.data || []
   } catch { /* ignore */ }
 }
 
-/** 根据级联选择的最后一级ID加载站点列表 */
-const loadStationsByAreaId = (areaId: string | number) => {
-  stationOptions.value = areaStationList.value.filter(item => item.parentId == areaId && item.type === 2)
-}
+// stationOptions, loadStationsByAreaId 已废弃,现通过级联选择器统一控制
+
 
 /** 根据级联值获取cityCode和cityName */
 const getCityInfoFromCascader = (cascaderValue: any[]) => {
@@ -1029,22 +1010,20 @@ const handleEdit = (row: FlfFulfillerVO) => {
     authQual: row.authQual,
     tagIds: row.tags ? row.tags.map(t => t.id) : []
   }
-  // 根据cityCode构建级联选择器的值
-  editDialog.cascaderValue = []
-  editDialog.stationOptions = []
-  if (row.cityCode) {
-    const item = areaStationList.value.find(i => String(i.id) === row.cityCode)
-    if (item) {
-      if (item.type === 1 && item.parentId) {
-        // 区域级:cascaderValue = [城市ID, 区域ID]
-        editDialog.cascaderValue = [item.parentId, item.id]
+  if (row.stationId || row.cityCode) {
+    const targetId = row.stationId || Number(row.cityCode)
+    const path: any[] = []
+    let currentId = targetId
+    while (currentId && String(currentId) !== '0') {
+      path.unshift(currentId)
+      const node = areaStationList.value.find(i => String(i.id) === String(currentId))
+      if (node) {
+        currentId = node.parentId
       } else {
-        // 城市级:cascaderValue = [城市ID]
-        editDialog.cascaderValue = [item.id]
+        break
       }
-      loadStationsByAreaId(item.id)
-      editDialog.stationOptions = stationOptions.value
     }
+    editDialog.cascaderValue = path
   }
   editDialog.visible = true
 }
@@ -1218,44 +1197,55 @@ const handleViewImage = (url: string) => {
 const handleFilterCascaderChange = (val: any[]) => {
   if (val && val.length > 0) {
     const lastId = val[val.length - 1]
-    queryParams.cityCode = String(lastId)
-    loadStationsByAreaId(lastId)
+    const node = areaStationList.value.find(item => item.id === lastId)
+    if (node && node.type === 2) {
+      queryParams.stationId = lastId
+      queryParams.cityCode = String(node.parentId)
+    } else {
+      queryParams.cityCode = String(lastId)
+      queryParams.stationId = undefined
+    }
   } else {
     queryParams.cityCode = undefined
-    stationOptions.value = []
+    queryParams.stationId = undefined
   }
-  queryParams.stationId = undefined
-  // 要求:只选择城市/区域不触发搜索,只有选择站点时触发
+  handleSearch()
 }
 
 /** 编辑对话框:级联选择变化时 */
 const handleEditCascaderChange = (val: any[]) => {
-  const { cityCode, cityName } = getCityInfoFromCascader(val)
-  editDialog.form.cityCode = cityCode
-  editDialog.form.cityName = cityName
   if (val && val.length > 0) {
     const lastId = val[val.length - 1]
-    loadStationsByAreaId(lastId)
-    editDialog.stationOptions = stationOptions.value
+    const node = areaStationList.value.find(item => item.id === lastId)
+    if (node) {
+      editDialog.form.stationId = lastId
+      editDialog.form.cityCode = String(node.parentId)
+      const { cityName } = getCityInfoFromCascader(val)
+      editDialog.form.cityName = cityName
+    }
   } else {
-    editDialog.stationOptions = []
+    editDialog.form.stationId = undefined
+    editDialog.form.cityCode = undefined
+    editDialog.form.cityName = undefined
   }
-  editDialog.form.stationId = undefined
 }
 
 /** 新增对话框:级联选择变化时 */
 const handleCreateCascaderChange = (val: any[]) => {
-  const { cityCode, cityName } = getCityInfoFromCascader(val)
-  createDialog.form.cityCode = cityCode
-  createDialog.form.cityName = cityName
   if (val && val.length > 0) {
     const lastId = val[val.length - 1]
-    loadStationsByAreaId(lastId)
-    createDialog.stationOptions = stationOptions.value
+    const node = areaStationList.value.find(item => item.id === lastId)
+    if (node) {
+      createDialog.form.stationId = lastId
+      createDialog.form.cityCode = String(node.parentId)
+      const { cityName } = getCityInfoFromCascader(val)
+      createDialog.form.cityName = cityName
+    }
   } else {
-    createDialog.stationOptions = []
+    createDialog.form.stationId = undefined
+    createDialog.form.cityCode = undefined
+    createDialog.form.cityName = undefined
   }
-  createDialog.form.stationId = undefined
 }
 
 onMounted(() => {

+ 8 - 9
src/views/order/dispatch/components/CustomerDetailDrawer.vue

@@ -111,7 +111,6 @@ import { listPetByUser } from '@/api/archieves/pet'
 import { listAllChangeLog } from '@/api/archieves/changeLog'
 import { listOnStore } from '@/api/system/areaStation'
 import { listSubOrderOnCustomer } from '@/api/order/subOrder/index'
-import { listAllService } from '@/api/service/list/index'
 
 const props = defineProps({
   visible: {
@@ -121,6 +120,11 @@ const props = defineProps({
   customerId: {
     type: [String, Number],
     default: null
+  },
+  // 父组件传入的服务列表,避免重复请求接口
+  serviceList: {
+    type: Array,
+    default: () => []
   }
 })
 
@@ -141,15 +145,10 @@ const currentPets = ref([])
 const changeLogs = ref([])
 const detailActiveTab = ref('info')
 const allNodes = ref([])
-
 const historyOrders = ref([])
-const serviceOptions = ref([])
 
-const getServiceList = () => {
-  listAllService().then((res) => {
-    serviceOptions.value = res.data || []
-  })
-}
+// 直接使用父组件传入的服务列表数据
+const serviceOptions = computed(() => props.serviceList || [])
 
 const getServiceName = (serviceId) => {
   const item = serviceOptions.value.find((i) => i.id === serviceId)
@@ -218,7 +217,7 @@ watch(() => props.visible, async (val) => {
 })
 
 onMounted(() => {
-  getServiceList()
+  // 服务列表数据由父组件传入,无需在这里请求
 })
 </script>
 

+ 9 - 12
src/views/order/dispatch/components/DispatchDialog.vue

@@ -149,7 +149,7 @@
         </div>
     </el-dialog>
 
-    <CustomerDetailDrawer v-model:visible="customerDialogVisible" :customer-id="customerId" />
+    <CustomerDetailDrawer v-model:visible="customerDialogVisible" :customer-id="customerId" :service-list="serviceList" />
     <PetDetailDrawer v-model:visible="petDialogVisible" :pet-id="petId" />
 </template>
 
@@ -159,13 +159,17 @@ 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 CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
 import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 
 const props = defineProps({
     visible: Boolean,
-    order: Object
+    order: Object,
+    // 父组件传入的服务列表,避免重复请求接口
+    serviceList: {
+        type: Array,
+        default: () => []
+    }
 })
 const emit = defineEmits(['update:visible', 'submit'])
 
@@ -212,14 +216,8 @@ const loadAllTags = async () => {
     }
 }
 
-const serviceOptions = ref([])
-const loadServiceOptions = async () => {
-    if (serviceOptions.value.length > 0) return
-    try {
-        const res = await listAllService()
-        serviceOptions.value = res?.data || []
-    } catch { /* ignore */ }
-}
+// 直接使用父组件传入的服务列表数据
+const serviceOptions = computed(() => props.serviceList || [])
 
 const getServiceTypeText = (id) => {
     const s = serviceOptions.value.find(item => String(item.id) === String(id))
@@ -272,7 +270,6 @@ watch(() => props.visible, (val) => {
         }
         pageNum.value = 1
         loadAllTags()
-        loadServiceOptions()
         loadRiders()
 
         // 获取订单详细信息

+ 0 - 8
src/views/order/dispatch/components/RiderListPanel.vue

@@ -55,14 +55,6 @@
             </div>
           </div>
           <div class="card-right-stats">
-            <div class="stat-box">
-              <span class="lbl">待接</span>
-              <span class="val danger">{{ rider.pendingCount }}</span>
-            </div>
-            <div class="stat-box">
-              <span class="lbl">待服</span>
-              <span class="val warning">{{ rider.todoCount }}</span>
-            </div>
             <!-- <el-button link type="primary" size="small" style="margin-top: 4px; padding: 0" @click.stop="$emit('view-orders', rider)"
               >查看订单</el-button> -->
           </div>

+ 43 - 20
src/views/order/dispatch/index.vue

@@ -65,7 +65,7 @@
     <RiderOrdersDialog v-model="riderOrdersVisible" :riderInfo="currentRiderInfo" :orders="currentRiderOrders" />
 
     <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder"
-      @submit="handleDispatchSubmit" />
+      :service-list="serviceOnOrderList" @submit="handleDispatchSubmit" />
   </div>
 </template>
 
@@ -74,10 +74,11 @@ import { ref, computed, reactive, onMounted, watch, getCurrentInstance, Componen
 import { checkPermi } from "@/utils/permission";
 import { ElMessage } from 'element-plus';
 import { listAllService } from '@/api/service/list/index'
-import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation'
+import { listAreaStation } from '@/api/system/areaStation'
 import { listStoreOnDispatch } from '@/api/system/store/index'
 import { listSubOrderOnDispatch, dispatchSubOrder } from '@/api/order/subOrder/index';
 import { listFulfillerOnDispatch } from '@/api/fulfiller/pool/index'
+import { getFulfillerGps } from '@/api/fulfiller/fulfiller/index'
 
 import OrderListPanel from './components/OrderListPanel.vue';
 import RiderListPanel from './components/RiderListPanel.vue';
@@ -154,13 +155,14 @@ const getServiceName = (serviceId) => {
   return item ? item.name : '未知服务';
 };
 
-const getRidersList = () => {
+const getRidersList = async () => {
   if (!filters.station) return;
-  listFulfillerOnDispatch({
-    service: filters.orderType !== 'all' ? filters.orderType : undefined,
-    site: filters.station
-  }).then(res => {
-    ridersList.value = (res.data || []).map(r => ({
+  try {
+    const res = await listFulfillerOnDispatch({
+      service: filters.orderType !== 'all' ? filters.orderType : undefined,
+      site: filters.station
+    });
+    const baseList = (res.data || []).map(r => ({
       ...r,
       uiStatus: r.status === 'busy' ? 'busy' : r.status === 'resting' ? 'offline' : 'disabled',
       maskPhone: r.phone ? r.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
@@ -169,13 +171,33 @@ const getRidersList = () => {
         return s ? s.name.substring(0, 2) : '服务';
       }),
       nextOrderTime: r.nextOrderTime || '14:30',
-      pendingCount: Math.floor(Math.random() * 3),
-      todoCount: Math.floor(Math.random() * 5),
-      lng: 116.4 + Math.random() * 0.1,
-      lat: 39.9 + Math.random() * 0.05
+      // GPS 坐标初始为 null,等待并发请求后覆盖
+      lng: null,
+      lat: null
     }));
+
+    // 并发请求所有骑手的实时 GPS 坐标
+    const gpsResults = await Promise.allSettled(
+      baseList.map(r => getFulfillerGps(r.id))
+    );
+
+    ridersList.value = baseList.map((r, idx) => {
+      const result = gpsResults[idx];
+      // 只有当 data 不为空且含有经纬度数据时才赋値
+      if (result.status === 'fulfilled' && result.value?.data) {
+        const gps = result.value.data;
+        if (gps.longitude && gps.latitude) {
+          return { ...r, lng: gps.longitude, lat: gps.latitude };
+        }
+      }
+      return r; // 无数据保持 lng/lat 为 null,不在地图上显示
+    });
+
     refreshMarkers();
-  })
+  } catch {
+    ridersList.value = [];
+    refreshMarkers();
+  }
 }
 
 const getMerchantList = () => {
@@ -185,10 +207,11 @@ const getMerchantList = () => {
     return;
   }
   listStoreOnDispatch({ site: filters.station }).then(res => {
+    // 保留全量门店数据(用于统计数字),有坐标的赋 lng/lat,无坐标则不在地图打点
     merchantList.value = (res?.data || []).map(item => ({
       ...item,
-      lng: item.longitude || (116.4 + Math.random() * 0.1),
-      lat: item.latitude || (39.9 + Math.random() * 0.05)
+      lng: item.longitude || null,
+      lat: item.latitude || null
     }));
     refreshMarkers();
   }).catch(() => {
@@ -239,7 +262,7 @@ const handleAreaChange = (value) => {
 
 const getAreaStationList = async () => {
   try {
-    const res = await listAreaStationOnStore()
+    const res = await listAreaStation()
     const data = res?.data || res
     areaStationList.value = Array.isArray(data) ? data : []
     const areaData = areaStationList.value.filter(item => Number(item.type) === 0 || Number(item.type) === 1)
@@ -334,9 +357,9 @@ const refreshMarkers = () => {
 
   const filter = activeMapFilter.value;
 
-  // 1. Merchants (商家)
+  // 1. Merchants (商家) - 仅对有经纬度的门店打点
   if (filter === 'all' || filter === 'merchants') {
-    merchantList.value.forEach((m) => {
+    merchantList.value.filter(m => m.lng && m.lat).forEach((m) => {
       const iconImg = m.icon || 'https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png';
 
       const content = `
@@ -367,9 +390,9 @@ const refreshMarkers = () => {
     });
   }
 
-  // 2. Fulfiller (履约者)
+  // 2. Fulfiller (履约者) - 仅对有真实 GPS 坐标的骑手打点
   if (filter === 'all' || filter === 'fulfillers') {
-    ridersList.value.forEach((r) => {
+    ridersList.value.filter(r => r.lng && r.lat).forEach((r) => {
       const borderColor = r.uiStatus === 'busy' ? '#67C23A' : r.uiStatus === 'offline' ? '#909399' : '#F56C6C';
       const avatar = r.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
 

+ 2 - 2
src/views/order/orderList/components/CustomerDetailDrawer.vue

@@ -109,7 +109,7 @@ import { ref, computed, watch, onMounted, getCurrentInstance, toRefs } from 'vue
 import { getCustomer } from '@/api/archieves/customer'
 import { listPetByUser } from '@/api/archieves/pet'
 import { listAllChangeLog } from '@/api/archieves/changeLog'
-import { listOnStore } from '@/api/system/areaStation'
+import { listAreaStation } from '@/api/system/areaStation'
 import { listSubOrderOnCustomer } from '@/api/order/subOrder/index'
 import { listAllService } from '@/api/service/list/index'
 
@@ -168,7 +168,7 @@ const getStatusType = (status) => {
 
 const loadAreaStation = async () => {
   if (allNodes.value.length === 0) {
-    const res = await listOnStore()
+    const res = await listAreaStation()
     allNodes.value = res.data || []
   }
 }

+ 1 - 1
src/views/order/orderList/index.vue

@@ -200,7 +200,7 @@ import { cancelSubOrder } from '@/api/order/subOrder/index';
 import { remarkSubOrder } from '@/api/order/subOrder/index';
 import { confirmSubOrder } from '@/api/order/subOrder/index';
 import { nursingSummarySubOrder } from '@/api/order/subOrder/index';
-import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation';
+import { listAreaStation as listAreaStationOnStore } from '@/api/system/areaStation';
 import { getStore } from '@/api/system/store';
 import { reward } from '@/api/fulfiller/pool';
 import { getPet } from '@/api/archieves/pet';

+ 30 - 34
src/views/order/purchase/components/AddUserDialog.vue

@@ -11,27 +11,14 @@
 
         <el-col :span="24"><div class="form-section-header">基本资料</div></el-col>
         <el-col :span="12">
-          <el-form-item label="录入来源">
-            <PageSelect v-model="form.source"
-              :options="brandList.map(item => ({ value: item.name, label: item.name }))"
+          <el-form-item label="所属品牌">
+            <PageSelect v-model="form.tenantId"
+              :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))"
               :total="brandTotal" :pageSize="10" placeholder="请选择所属品牌"
               @page-change="handleBrandPageChange"
               @visible-change="handleBrandVisibleChange" />
           </el-form-item>
         </el-col>
-        <el-col :span="12">
-          <el-form-item label="所属区域">
-            <el-cascader v-model="formAreaValue" :options="areaTreeOptions" placeholder="请选择区域"
-              style="width: 100%" clearable @change="handleFormAreaChange" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="所属站点">
-            <el-select v-model="form.stationId" style="width: 100%" filterable placeholder="请选择站点" clearable :disabled="!form.areaId">
-              <el-option v-for="station in formStationList" :key="station.id" :label="station.name" :value="station.id" />
-            </el-select>
-          </el-form-item>
-        </el-col>
         <el-col :span="12">
           <el-form-item label="姓名" required><el-input v-model="form.name" placeholder="请输入姓名" /></el-form-item>
         </el-col>
@@ -45,6 +32,12 @@
             </el-select>
           </el-form-item>
         </el-col>
+        <el-col :span="24">
+          <el-form-item label="所属站点">
+            <el-cascader v-model="formAreaValue" :options="areaTreeOptions" :props="{ value: 'id', label: 'name' }" placeholder="请选择站点"
+              style="width: 100%" clearable @change="handleFormAreaChange" />
+          </el-form-item>
+        </el-col>
 
         <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
         <el-col :span="24">
@@ -116,10 +109,13 @@ import { ElMessage } from 'element-plus'
 import { globalHeaders } from '@/utils/request'
 import { addCustomerOnOrder } from '@/api/archieves/customer'
 import { listAllTag } from '@/api/archieves/tag'
-import { listOnStore } from '@/api/system/areaStation'
+import { listAreaStation } from '@/api/system/areaStation'
 import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
 import { regionData } from 'element-china-area-data'
 import PageSelect from '@/components/PageSelect/index.vue'
+import { useUserStore } from '@/store/modules/user'
+
+const userStore = useUserStore()
 
 const props = defineProps({
   visible: { type: Boolean, default: false }
@@ -169,7 +165,8 @@ const form = reactive({
   memberLevel: 0,
   status: 0,
   remark: '',
-  tagIds: []
+  tagIds: [],
+  tenantId: undefined
 })
 
 watch(() => props.visible, (val) => {
@@ -188,7 +185,8 @@ const resetForm = () => {
     name: '', phone: '', avatar: undefined, gender: undefined, birthday: '', idCard: '',
     areaId: undefined, stationId: undefined, regionCode: '', region: [], address: '',
     houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', source: '',
-    emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: []
+    emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: [],
+    tenantId: undefined
   })
 }
 
@@ -198,32 +196,30 @@ const areaTreeOptions = computed(() => {
       .filter(item => String(item.parentId) === String(parentId))
       .map(item => {
         const children = buildTree(data, item.id)
-        const node = { value: item.id, label: item.name }
+        const node = { id: item.id, name: item.name }
         if (children.length > 0) node.children = children
         return node
       })
   }
-  const areaData = allNodes.value.filter(n => n.type === 0 || n.type === 1)
-  return buildTree(areaData, 0)
+  return buildTree(allNodes.value, 0)
 })
 
-const formStationList = computed(() => {
-  const areaId = form.areaId
-  const stations = allNodes.value.filter(n => n.type === 2)
-  if (areaId) {
-    return stations.filter(s => s.parentId === areaId)
-  }
-  return stations
-})
+// formStationList 已废弃
+
 
 const handleFormAreaChange = (value) => {
-  form.stationId = undefined
   if (value && value.length > 0) {
-    form.areaId = value[value.length - 1]
+    const lastId = value[value.length - 1];
+    const node = allNodes.value.find(item => item.id === lastId);
+    form.stationId = lastId;
+    form.areaId = node ? node.parentId : undefined;
   } else {
-    form.areaId = undefined
+    form.stationId = undefined;
+    form.areaId = undefined;
   }
 }
+// formStationList 已废弃
+
 
 const getBrandList = async (pageNum = 1) => {
   const res = await listBrandOnStore({ pageNum, pageSize: 10 })
@@ -250,7 +246,7 @@ const loadTags = () => {
 }
 
 const loadAreaStation = () => {
-  listOnStore().then((res) => {
+  listAreaStation().then((res) => {
     allNodes.value = res.data || []
   })
 }

+ 7 - 2
src/views/system/areaStation/index.vue

@@ -238,22 +238,26 @@ const resetQuery = () => {
 const handleAdd = (row?: AreaStationVO) => {
   reset();
   getTreeselect();
+  let typeLabel = '';
   if (row != null && row.id) {
     form.value.parentId = row.id;
     if (row.type === 0) {
       // 城市底下新增区域
       form.value.type = 1;
+      typeLabel = '区域';
     } else if (row.type === 1) {
       // 区域底下新增站点
       form.value.type = 2;
+      typeLabel = '站点';
     }
   } else {
     // 头部操作新增城市
     form.value.parentId = 0;
     form.value.type = 0;
+    typeLabel = '城市';
   }
   dialog.visible = true;
-  dialog.title = '添加区域站点';
+  dialog.title = `新增${typeLabel}`;
 };
 
 /** 展开/折叠操作 */
@@ -281,8 +285,9 @@ const handleUpdate = async (row: AreaStationVO) => {
   }
   const res = await getAreaStation(row.id as number);
   Object.assign(form.value, res.data);
+  const typeLabel = getTypeLabel(form.value.type);
   dialog.visible = true;
-  dialog.title = '修改区域站点';
+  dialog.title = `修改${typeLabel}`;
 };
 
 /** 提交按钮 */

+ 132 - 97
src/views/system/store/index.vue

@@ -18,14 +18,13 @@
             <el-cascader
               v-model="searchRegionValue"
               :options="areaOptions"
-              placeholder="所属城市"
-              class="region-select"
+              :props="{ checkStrictly: true, value: 'id', label: 'name' }"
+              placeholder="所属站点"
+              class="station-select"
+              style="width: 350px"
               clearable
               @change="handleSearchAreaChange"
             />
-            <el-select v-model="queryParams.station" placeholder="所属站点" class="station-select" clearable @change="handleQuery" :disabled="!queryParams.area">
-              <el-option v-for="site in searchSiteOptions" :key="site.value" :label="site.label" :value="site.value" />
-            </el-select>
             <el-select v-model="queryParams.status" placeholder="状态" class="status-select" clearable @change="handleQuery">
               <el-option v-for="item in statusList" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
@@ -210,20 +209,9 @@
         <el-form-item label="有效期至" prop="validity">
           <el-date-picker clearable v-model="form.validity" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期至" style="width: 100%" />
         </el-form-item>
-        <el-row :gutter="10">
-          <el-col :span="12">
-            <el-form-item label="所在区域" prop="regionId">
-              <el-cascader v-model="regionValue" :options="areaOptions" placeholder="选择区域" style="width: 100%" @change="handleAreaChange" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="归属站点" prop="site">
-              <el-select v-model="form.site" placeholder="选择站点" :disabled="!form.regionId" style="width: 100%">
-                <el-option v-for="site in siteOptions" :key="site.value" :label="site.label" :value="site.value" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row>
+        <el-form-item label="所属站点" prop="site">
+          <el-cascader v-model="regionValue" :options="areaOptions" :props="{ value: 'id', label: 'name' }" placeholder="选择站点" style="width: 100%" @change="handleAreaChange" />
+        </el-form-item>
         <el-form-item label="详细地址">
           <el-row :gutter="10" style="margin-bottom: 10px">
             <el-col :span="24">
@@ -338,8 +326,8 @@ import { StoreVO, StoreForm, StoreQuery, StoreStatusVO, SysStorePageBo } from '@
 import { listOnStore } from '@/api/system/tenant';
 import { listOnStore as listTenantCategoriesOnStore } from '@/api/system/tenantCategories';
 import { listAllService } from '@/api/service/list';
-import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation';
-import { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types';
+import { listAreaStation } from '@/api/system/areaStation';
+import { AreaStationVO } from '@/api/system/areaStation/types';
 import { regionData, codeToText, textToCode } from 'element-china-area-data';
 import PageSelect from '@/components/PageSelect/index.vue';
 import { checkPermi } from '@/utils/permission';
@@ -358,22 +346,21 @@ const searchSiteOptions = ref<any[]>([]); // 搜索的站点选项
 
 /** 处理搜索区域选择变化 */
 const handleSearchAreaChange = (value: any[]) => {
-  queryParams.value.station = undefined;
-
   if (value && value.length > 0) {
-    const areaId = value[value.length - 1];
-    queryParams.value.area = areaId;
-    searchSiteOptions.value = areaStationList.value
-      .filter((item: any) => item.type === 2 && String(item.parentId) === String(areaId))
-      .map((item: any) => ({
-        value: item.id,
-        label: item.name
-      }));
+    const lastId = value[value.length - 1];
+    const node = areaStationList.value.find(item => item.id === lastId);
+    if (node && node.type === 2) {
+      queryParams.value.station = lastId;
+      queryParams.value.area = node.parentId;
+    } else {
+      queryParams.value.area = lastId;
+      queryParams.value.station = undefined;
+    }
   } else {
     queryParams.value.area = undefined;
-    searchSiteOptions.value = [];
+    queryParams.value.station = undefined;
   }
-  // 移除 handleQuery(); 只选城市不发送请求
+  handleQuery();
 };
 
 const regionValue = ref<any[]>([]);
@@ -391,7 +378,7 @@ const serviceList = ref<any[]>([]); // 服务项目列表
 const statusList = ref<StoreStatusVO[]>([]); // 状态列表
 const tenantCategoriesList = ref<any[]>([]); // 商户分类列表
 const tenantCategoriesTotal = ref(0); // 商户分类总数
-const areaStationList = ref<SysAreaStationOnStoreVo[]>([]); // 区域站点列表
+const areaStationList = ref<AreaStationVO[]>([]); // 区域站点列表
 const areaOptions = ref<any[]>([]); // 所在区域树形选项
 const siteOptions = ref<any[]>([]); // 归属站点选项
 
@@ -617,8 +604,9 @@ const handleQuery = () => {
 
 /** 新增按钮操作 */
 const handleAdd = () => {
-  if (areaStationList.value.length === 0) {
-    proxy?.$modal.msgWarning("请先配置区域站点");
+  const hasStation = areaStationList.value.some(item => item.type === 2);
+  if (!hasStation) {
+    proxy?.$modal.msgWarning("请先配置站点");
     return;
   }
   reset();
@@ -663,30 +651,22 @@ const handleUpdate = async (row: StoreVO) => {
   }
 
   if (res.data.site) {
-    const siteData = areaStationList.value.find((item: any) => String(item.id) === String(res.data.site));
-    if (siteData) {
-      const regionId = siteData.parentId;
-      form.value.regionId = regionId;
-
-      const path: any[] = [];
-      let currentId = regionId;
-      while (currentId && String(currentId) !== '0') {
-        path.unshift(currentId);
-        const currentArea = areaStationList.value.find((item: any) => String(item.id) === String(currentId));
-        if (currentArea) {
-          currentId = currentArea.parentId;
-        } else {
-          break;
-        }
+    const path: any[] = [];
+    let currentId = res.data.site;
+    while (currentId && String(currentId) !== '0') {
+      path.unshift(currentId);
+      const currentArea = areaStationList.value.find((item: any) => String(item.id) === String(currentId));
+      if (currentArea) {
+        currentId = currentArea.parentId;
+      } else {
+        break;
       }
-      regionValue.value = path;
-
-      siteOptions.value = areaStationList.value
-        .filter((item: any) => item.type === 2 && String(item.parentId) === String(regionId))
-        .map((item: any) => ({
-          value: item.id,
-          label: item.name
-        }));
+    }
+    regionValue.value = path;
+    form.value.site = res.data.site;
+    const siteNode = areaStationList.value.find(n => n.id === res.data.site);
+    if (siteNode) {
+      form.value.regionId = siteNode.parentId;
     }
   }
 
@@ -711,28 +691,76 @@ const submitForm = () => {
   });
 }
 
-/** 获取经纬度 */
+/** 高德地图 Key 配置 */
+const amapKey = 'a30e76f457c14b6570925522be37565d';
+const securityJsCode = '531ae14ec1dff87e552e1ea51e848582';
+
+/** 动态加载高德地图脚本 */
+const loadAMapScript = (): Promise<any> => {
+  // 设置安全密钥
+  (window as any)._AMapSecurityConfig = {
+    securityJsCode: securityJsCode,
+  };
+  return new Promise((resolve, reject) => {
+    if ((window as any).AMap) {
+      resolve((window as any).AMap);
+      return;
+    }
+    const script = document.createElement('script');
+    script.src = `https://webapi.amap.com/maps?v=2.0&key=${amapKey}`;
+    script.onload = () => resolve((window as any).AMap);
+    script.onerror = reject;
+    document.head.appendChild(script);
+  });
+};
+
+/** 根据详细地址使用高德地图 Geocoder 获取经纬度 */
 const getGeolocation = () => {
-  if ('geolocation' in navigator) {
-    navigator.geolocation.getCurrentPosition(
-      (position) => {
-        form.value.longitude = position.coords.longitude.toFixed(6);
-        form.value.latitude = position.coords.latitude.toFixed(6);
-        proxy?.$modal.msgSuccess('获取经纬度成功');
-      },
-      (error) => {
-        let errorMessage = '获取位置失败';
-        switch (error.code) {
-          case error.PERMISSION_DENIED: errorMessage = '用户拒绝了地理定位请求'; break;
-          case error.POSITION_UNAVAILABLE: errorMessage = '位置信息不可用'; break;
-          case error.TIMEOUT: errorMessage = '获取位置超时'; break;
-          case error.UNKNOWN_ERROR: errorMessage = '未知错误'; break;
+  // 拼接完整地址(省市区 + 详细地址)
+  let areaText = '';
+  if (addressCascaderValue.value && addressCascaderValue.value.length > 0) {
+    areaText = addressCascaderValue.value.map((code: string) => codeToText[code] || '').join('');
+  }
+  const detailAddr = form.value.detailAddress || '';
+  const fullAddress = (areaText + detailAddr).trim();
+
+  if (!fullAddress) {
+    proxy?.$modal.msgWarning('请先填写省市区和详细地址');
+    return;
+  }
+
+  // 确保高德地图脚本已加载
+  const doGeocode = () => {
+    const AMap = (window as any).AMap;
+    if (!AMap) {
+      proxy?.$modal.msgError('高德地图脚本未加载,请稍后重试');
+      return;
+    }
+    AMap.plugin('AMap.Geocoder', () => {
+      const geocoder = new AMap.Geocoder();
+      geocoder.getLocation(fullAddress, (status: string, result: any) => {
+        if (status === 'complete' && result.info === 'OK') {
+          const location = result.geocodes[0]?.location;
+          if (location) {
+            form.value.longitude = location.lng.toFixed(6);
+            form.value.latitude = location.lat.toFixed(6);
+            proxy?.$modal.msgSuccess('获取经纬度成功');
+          } else {
+            proxy?.$modal.msgError('未能解析到该地址的坐标,请检查地址是否准确');
+          }
+        } else {
+          proxy?.$modal.msgError('地理编码失败:' + (result.info || status));
         }
-        proxy?.$modal.msgError(errorMessage);
-      }
-    );
+      });
+    });
+  };
+
+  if ((window as any).AMap) {
+    doGeocode();
   } else {
-    proxy?.$modal.msgError('您的浏览器不支持地理定位');
+    loadAMapScript().then(() => doGeocode()).catch(() => {
+      proxy?.$modal.msgError('高德地图加载失败,请检查网络');
+    });
   }
 };
 
@@ -760,11 +788,10 @@ const getServiceList = async () => {
 /** 获取区域站点列表 */
 const getAreaStationList = async () => {
   try {
-    const res = await listAreaStationOnStore();
+    const res = await listAreaStation();
     const data = res.data || res;
     areaStationList.value = data;
-    areaOptions.value = buildTree(data.filter((item: any) => item.type === 0 || item.type === 1), 0);
-    siteOptions.value = [];
+    areaOptions.value = buildTree(data, 0);
   } catch (error) {
     console.error('获取区域站点列表失败:', error);
   }
@@ -774,28 +801,31 @@ const getAreaStationList = async () => {
 const buildTree = (data: any[], parentId: any): any[] => {
   return data
     .filter(item => String(item.parentId) === String(parentId))
-    .map(item => ({
-      value: item.id,
-      label: item.name,
-      children: buildTree(data, item.id)
-    }));
+    .map(item => {
+      const children = buildTree(data, item.id);
+      const res: any = {
+        id: item.id,
+        name: item.name,
+      };
+      if (children && children.length > 0) {
+        res.children = children;
+      }
+      return res;
+    });
 };
 
 /** 处理所在区域选择变化 */
 const handleAreaChange = (value: any[]) => {
-  form.value.site = undefined;
   if (value && value.length > 0) {
-    const areaId = value[value.length - 1];
-    form.value.regionId = areaId;
-    siteOptions.value = areaStationList.value
-      .filter((item: any) => item.type === 2 && String(item.parentId) === String(areaId))
-      .map((item: any) => ({
-        value: item.id,
-        label: item.name
-      }));
+    const lastId = value[value.length - 1];
+    form.value.site = lastId;
+    const node = areaStationList.value.find(item => item.id === lastId);
+    if (node) {
+      form.value.regionId = node.parentId;
+    }
   } else {
+    form.value.site = undefined;
     form.value.regionId = undefined;
-    siteOptions.value = [];
   }
 };
 
@@ -889,6 +919,10 @@ onMounted(() => {
   getServiceList();
   getAreaStationList();
   getStatusList();
+  // 提前加载高德地图脚本,加快首次地理编码速度
+  loadAMapScript().catch(() => {
+    console.warn('高德地图预加载失败,将在首次使用时重试');
+  });
 });
 </script>
 
@@ -928,7 +962,8 @@ onMounted(() => {
   gap: 12px;
   
   .search-input { width: 200px; }
-  .region-select, .station-select, .status-select { width: 140px; }
+  .station-select { width: 420px; flex-shrink: 0; }
+  .status-select { width: 140px; }
   
   :deep(.el-input__wrapper) {
     background-color: #f4f5f7;

+ 1 - 1
vite.config.ts

@@ -26,7 +26,7 @@ export default defineConfig(({ mode, command }) => {
       proxy: {
         [env.VITE_APP_BASE_API]: {
           target: 'http://127.0.0.1:8080',
-          // target: 'http://8.136.194.143/api',
+          // target: 'http://www.hoomeng.pet/api',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff