Bladeren bron

Merge branch 'release/2.4.0'

张田田 19 uur geleden
bovenliggende
commit
44f0ee19a2
64 gewijzigde bestanden met toevoegingen van 3806 en 431 verwijderingen
  1. 1 0
      @types/typed-router.d.ts
  2. 157 101
      package-lock.json
  3. 1 1
      package.json
  4. 15 0
      public/i18n/applet.develop.json
  5. 15 0
      public/i18n/applet.release.json
  6. 15 0
      public/i18n/applet.trial.json
  7. BIN
      src/assets/images/cancel.png
  8. 15 14
      src/components/ChatHistory.vue
  9. 3 2
      src/components/EditConfigured.vue
  10. 2 4
      src/components/EditOrganization.vue
  11. 50 59
      src/components/EditSupplier.vue
  12. 1 1
      src/components/UserPreview.vue
  13. 1 0
      src/model/care.model.ts
  14. 178 63
      src/model/order.model.ts
  15. 226 0
      src/order/Amount.vue
  16. 35 5
      src/order/DispatchOrderPanel.vue
  17. 6 21
      src/order/EditShipment.vue
  18. 466 0
      src/order/LogisticsDetails.vue
  19. 295 0
      src/order/Negotiations.vue
  20. 454 0
      src/order/Review.vue
  21. 105 0
      src/order/SeeAmount.vue
  22. 4 0
      src/pages/index/care/conditioningRecord.vue
  23. 17 2
      src/pages/index/care/configured.vue
  24. 4 0
      src/pages/index/care/institutionService.vue
  25. 14 25
      src/pages/index/care/serviceItems.vue
  26. 5 0
      src/pages/index/care/supplier.vue
  27. 4 0
      src/pages/index/care/systemService.vue
  28. 1 0
      src/pages/index/equipment/configured.vue
  29. 5 0
      src/pages/index/equipment/registe.vue
  30. 5 0
      src/pages/index/equipment/reportManagement.vue
  31. 5 0
      src/pages/index/follow/assessment.vue
  32. 4 0
      src/pages/index/follow/plan.vue
  33. 5 0
      src/pages/index/follow/task.vue
  34. 4 0
      src/pages/index/healthy/education.vue
  35. 306 0
      src/pages/index/order/afterSale.vue
  36. 4 0
      src/pages/index/order/management.vue
  37. 4 0
      src/pages/index/order/revenueSharing.vue
  38. 6 1
      src/pages/index/order/shipment.vue
  39. 4 0
      src/pages/index/patient/history.vue
  40. 54 30
      src/pages/index/system/institution.vue
  41. 52 29
      src/pages/index/system/organization.vue
  42. 4 0
      src/pages/index/system/role.vue
  43. 4 0
      src/pages/index/system/tag.vue
  44. 4 0
      src/pages/index/system/user.vue
  45. 2 6
      src/request/api/account.api.ts
  46. 0 1
      src/request/api/care.api.ts
  47. 99 2
      src/request/api/order.api.ts
  48. 1 0
      src/router/index.ts
  49. 4 0
      src/satisfaction/SendRecord.vue
  50. 4 0
      src/satisfaction/SurveyList.vue
  51. 33 5
      src/service/AddItems.vue
  52. 4 0
      src/service/NotifyManageList.vue
  53. 4 0
      src/service/NotifyManageRecord.vue
  54. 123 0
      src/service/ReviewMediaPreview.vue
  55. 42 27
      src/service/ServiceItemsConfirm.vue
  56. 5 0
      src/service/ServiceItemsList.vue
  57. 6 1
      src/service/ServiceItemsSystem.vue
  58. 12 1
      src/service/ServicePackageDetail.vue
  59. 551 30
      src/service/SingleItemDetail.vue
  60. 340 0
      src/service/seeEvaluate.vue
  61. 4 0
      src/widgets/PatientCareRecordsWidget.vue
  62. 4 0
      src/widgets/PatientDiagnosisRecordsWidget.vue
  63. 4 0
      src/widgets/PatientFollowUpRecordsWidget.vue
  64. 4 0
      src/widgets/PatientHealthRecordsWidget.vue

+ 1 - 0
@types/typed-router.d.ts

@@ -35,6 +35,7 @@ declare module 'vue-router/auto-routes' {
     '//healthy/education': RouteRecordInfo<'//healthy/education', '/healthy/education', Record<never, never>, Record<never, never>>,
     '//notify/manage': RouteRecordInfo<'//notify/manage', '/notify/manage', Record<never, never>, Record<never, never>>,
     '//online/onlineConsult': RouteRecordInfo<'//online/onlineConsult', '/online/onlineConsult', Record<never, never>, Record<never, never>>,
+    '//order/afterSale': RouteRecordInfo<'//order/afterSale', '/order/afterSale', Record<never, never>, Record<never, never>>,
     '//order/dispatchOrder': RouteRecordInfo<'//order/dispatchOrder', '/order/dispatchOrder', Record<never, never>, Record<never, never>>,
     '//order/management': RouteRecordInfo<'//order/management', '/order/management', Record<never, never>, Record<never, never>>,
     '//order/revenueSharing': RouteRecordInfo<'//order/revenueSharing', '/order/revenueSharing', Record<never, never>, Record<never, never>>,

+ 157 - 101
package-lock.json

@@ -8,23 +8,28 @@
       "name": "six-hms",
       "version": "0.0.0",
       "dependencies": {
+        "@alova/adapter-xhr": "^2.2.1",
         "@ant-design/icons-vue": "^7.0.1",
         "@logicflow/core": "^2.1.2",
         "@logicflow/extension": "^2.1.4",
         "@logicflow/layout": "^2.0.2",
         "@logicflow/vue-node-registry": "^1.1.3",
+        "@types/markdown-it": "^14.1.2",
         "@unocss/reset": "^0.61.0",
         "@vueuse/components": "^10.11.0",
         "@vueuse/core": "^10.11.0",
         "@vueuse/router": "^10.11.1",
+        "@wangeditor/core": "^1.1.19",
         "@wangeditor/editor": "^5.1.23",
-        "@wangeditor/editor-for-vue": "^5.1.12",
+        "@wangeditor/plugin-ctrl-enter": "^1.1.2",
+        "@wangeditor/plugin-md": "^1.0.0",
         "alova": "^3.2.10",
         "ant-design-vue": "^4.2.3",
         "china-division": "^2.7.0",
         "dayjs": "^1.11.11",
         "echarts": "^5.5.1",
         "element-plus": "^2.9.10",
+        "markdown-it": "^14.1.0",
         "pinia": "^2.1.7",
         "pinia-plugin-persistedstate": "^3.2.1",
         "swiper": "^8.4.7",
@@ -37,7 +42,7 @@
         "vue-virtual-scroller": "2.0.0-beta.8",
         "vue3-slider": "^1.10.1",
         "vuedraggable": "^4.1.0",
-        "vxe-pc-ui": "^4.6.42",
+        "vxe-pc-ui": "4.6.42",
         "vxe-table": "^4.7.62"
       },
       "devDependencies": {
@@ -71,6 +76,24 @@
         "vue-tsc": "^2.0.21"
       }
     },
+    "node_modules/@alova/adapter-xhr": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/@alova/adapter-xhr/-/adapter-xhr-2.3.1.tgz",
+      "integrity": "sha512-mhViueeSJ6yfyEUvHIWyZlb6V8+9GJCzwU1hUsvjXSZHpkzGCeNUefxjjFC++K9UBsvUzJ5KGeyeqoToUQg3gg==",
+      "license": "MIT",
+      "dependencies": {
+        "@alova/shared": "1.3.2"
+      },
+      "peerDependencies": {
+        "alova": "^3.0.20"
+      }
+    },
+    "node_modules/@alova/adapter-xhr/node_modules/@alova/shared": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/@alova/shared/-/shared-1.3.2.tgz",
+      "integrity": "sha512-1XvDLWgYpVZ99MmLl1f3Fw4T6S6pPYk5afz5cwRVjuq8JXEGsDn9IygDKfvRyWqkqCBx7Jif07LIct1O+MVEow==",
+      "license": "MIT"
+    },
     "node_modules/@alova/mock": {
       "version": "2.0.13",
       "dev": true,
@@ -189,7 +212,6 @@
       "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
         "@babel/code-frame": "^7.27.1",
@@ -432,7 +454,6 @@
       "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.2.tgz",
       "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@babel/types": "^7.27.1"
       },
@@ -1396,7 +1417,6 @@
       "resolved": "https://registry.npmmirror.com/@logicflow/core/-/core-2.1.4.tgz",
       "integrity": "sha512-cK+drgL57faFDUOb2Zn0vWDRtVJe6MswF3GwLb/Sn/9aVxiSIk+5Z6cpSdY+4iuTpxFDKj0DBpvMYEeeLPnzpg==",
       "license": "Apache-2.0",
-      "peer": true,
       "dependencies": {
         "classnames": "^2.3.2",
         "lodash-es": "^4.17.21",
@@ -2229,6 +2249,12 @@
       "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==",
       "license": "MIT"
     },
+    "node_modules/@types/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+      "license": "MIT"
+    },
     "node_modules/@types/lodash": {
       "version": "4.17.17",
       "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.17.tgz",
@@ -2240,11 +2266,26 @@
       "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
       "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@types/lodash": "*"
       }
     },
+    "node_modules/@types/markdown-it": {
+      "version": "14.1.2",
+      "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+      "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/linkify-it": "^5",
+        "@types/mdurl": "^2"
+      }
+    },
+    "node_modules/@types/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+      "license": "MIT"
+    },
     "node_modules/@types/mockjs": {
       "version": "1.0.10",
       "dev": true,
@@ -2254,7 +2295,6 @@
       "version": "20.17.29",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "undici-types": "~6.19.2"
       }
@@ -2305,7 +2345,6 @@
       "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
       "dev": true,
       "license": "BSD-2-Clause",
-      "peer": true,
       "dependencies": {
         "@typescript-eslint/scope-manager": "7.18.0",
         "@typescript-eslint/types": "7.18.0",
@@ -2962,7 +3001,6 @@
       "resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
       "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@transloadit/prettier-bytes": "0.0.7",
         "@uppy/store-default": "^2.1.1",
@@ -2994,7 +3032,6 @@
       "resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
       "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@uppy/companion-client": "^2.2.2",
         "@uppy/utils": "^4.1.2",
@@ -3383,7 +3420,6 @@
       "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
       "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@vue/reactivity": "3.5.13",
         "@vue/shared": "3.5.13"
@@ -3547,12 +3583,41 @@
         "vue": "^3.2.0"
       }
     },
+    "node_modules/@wangeditor/core": {
+      "version": "1.1.19",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
+      "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/event-emitter": "^0.3.3",
+        "event-emitter": "^0.3.5",
+        "html-void-elements": "^2.0.0",
+        "i18next": "^20.4.0",
+        "scroll-into-view-if-needed": "^2.2.28",
+        "slate-history": "^0.66.0"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^2.1.1",
+        "@uppy/xhr-upload": "^2.0.3",
+        "dom7": "^3.0.0",
+        "is-hotkey": "^0.2.0",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.debounce": "^4.0.8",
+        "lodash.foreach": "^4.5.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "lodash.toarray": "^4.4.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
     "node_modules/@wangeditor/editor": {
       "version": "5.1.23",
       "resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
       "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@uppy/core": "^2.1.1",
         "@uppy/xhr-upload": "^2.0.3",
@@ -3577,22 +3642,11 @@
         "snabbdom": "^3.1.0"
       }
     },
-    "node_modules/@wangeditor/editor-for-vue": {
-      "version": "5.1.12",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
-      "integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
-      "license": "MIT",
-      "peerDependencies": {
-        "@wangeditor/editor": ">=5.1.0",
-        "vue": "^3.0.5"
-      }
-    },
     "node_modules/@wangeditor/editor/node_modules/@wangeditor/basic-modules": {
       "version": "1.1.7",
       "resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
       "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "is-url": "^1.2.4"
       },
@@ -3620,37 +3674,6 @@
         "snabbdom": "^3.1.0"
       }
     },
-    "node_modules/@wangeditor/editor/node_modules/@wangeditor/core": {
-      "version": "1.1.19",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
-      "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@types/event-emitter": "^0.3.3",
-        "event-emitter": "^0.3.5",
-        "html-void-elements": "^2.0.0",
-        "i18next": "^20.4.0",
-        "scroll-into-view-if-needed": "^2.2.28",
-        "slate-history": "^0.66.0"
-      },
-      "peerDependencies": {
-        "@uppy/core": "^2.1.1",
-        "@uppy/xhr-upload": "^2.0.3",
-        "dom7": "^3.0.0",
-        "is-hotkey": "^0.2.0",
-        "lodash.camelcase": "^4.3.0",
-        "lodash.clonedeep": "^4.5.0",
-        "lodash.debounce": "^4.0.8",
-        "lodash.foreach": "^4.5.0",
-        "lodash.isequal": "^4.5.0",
-        "lodash.throttle": "^4.1.1",
-        "lodash.toarray": "^4.4.0",
-        "nanoid": "^3.2.0",
-        "slate": "^0.72.0",
-        "snabbdom": "^3.1.0"
-      }
-    },
     "node_modules/@wangeditor/editor/node_modules/@wangeditor/list-module": {
       "version": "1.0.5",
       "resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz",
@@ -3714,7 +3737,6 @@
       "resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
       "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "ssr-window": "^3.0.0-alpha.1"
       }
@@ -3725,13 +3747,30 @@
       "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==",
       "license": "MIT"
     },
+    "node_modules/@wangeditor/plugin-ctrl-enter": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/plugin-ctrl-enter/-/plugin-ctrl-enter-1.1.2.tgz",
+      "integrity": "sha512-TbYXcCbpwmgjb+Bwi+cy5q6BlNLtV8uIavxeUns+TRwXTNZDiItfDqduTH/fyMvhpKmt6cDMeXRgX4UVb0V75Q==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@wangeditor/editor": ">=5.0.0"
+      }
+    },
+    "node_modules/@wangeditor/plugin-md": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/plugin-md/-/plugin-md-1.0.0.tgz",
+      "integrity": "sha512-wpbusrhSNmb6keC5vmiaQ2nBBfiwwVWmfRp+zNB65nGrPHVRjaFQBNxTpGoL+rc02KCYdflzEbZAg+kzGtMXlQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@wangeditor/editor": ">=5.0.0"
+      }
+    },
     "node_modules/acorn": {
       "version": "8.14.1",
       "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz",
       "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -3776,7 +3815,6 @@
     "node_modules/alova": {
       "version": "3.2.10",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@alova/shared": "1.1.2",
         "rate-limiter-flexible": "^5.0.3"
@@ -3867,7 +3905,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true,
       "license": "Python-2.0"
     },
     "node_modules/array-tree-filter": {
@@ -4009,7 +4046,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "caniuse-lite": "^1.0.30001716",
         "electron-to-chromium": "^1.5.149",
@@ -4598,7 +4634,6 @@
     "node_modules/echarts": {
       "version": "5.6.0",
       "license": "Apache-2.0",
-      "peer": true,
       "dependencies": {
         "tslib": "2.3.0",
         "zrender": "5.6.1"
@@ -4755,7 +4790,6 @@
       "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
-      "peer": true,
       "bin": {
         "esbuild": "bin/esbuild"
       },
@@ -4822,7 +4856,6 @@
       "version": "8.57.1",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
@@ -4879,7 +4912,6 @@
       "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "bin": {
         "eslint-config-prettier": "bin/cli.js"
       },
@@ -4922,7 +4954,6 @@
       "version": "9.33.0",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "globals": "^13.24.0",
@@ -5745,8 +5776,7 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
       "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/is-inside-container": {
       "version": "1.0.0",
@@ -6018,6 +6048,15 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+      "license": "MIT",
+      "dependencies": {
+        "uc.micro": "^2.0.0"
+      }
+    },
     "node_modules/load-tsconfig": {
       "version": "0.2.5",
       "resolved": "https://registry.npmmirror.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
@@ -6066,15 +6105,13 @@
       "version": "4.17.21",
       "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash-es": {
       "version": "4.17.21",
       "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
       "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash-unified": {
       "version": "1.0.3",
@@ -6091,37 +6128,32 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
       "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash.clonedeep": {
       "version": "4.5.0",
       "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
       "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
       "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash.foreach": {
       "version": "4.5.0",
       "resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
       "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash.isequal": {
       "version": "4.5.0",
       "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
       "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
       "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
@@ -6134,15 +6166,13 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
       "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash.toarray": {
       "version": "4.4.0",
       "resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
       "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/loose-envify": {
       "version": "1.4.0",
@@ -6191,12 +6221,35 @@
         "url": "https://github.com/sponsors/sxzz"
       }
     },
+    "node_modules/markdown-it": {
+      "version": "14.1.1",
+      "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.1.tgz",
+      "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1",
+        "entities": "^4.4.0",
+        "linkify-it": "^5.0.0",
+        "mdurl": "^2.0.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^2.1.0"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.mjs"
+      }
+    },
     "node_modules/mdn-data": {
       "version": "2.0.30",
       "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz",
       "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
       "license": "CC0-1.0"
     },
+    "node_modules/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+      "license": "MIT"
+    },
     "node_modules/medium-editor": {
       "version": "5.23.3",
       "resolved": "https://registry.npmmirror.com/medium-editor/-/medium-editor-5.23.3.tgz",
@@ -6318,7 +6371,6 @@
       "resolved": "https://registry.npmmirror.com/mobx/-/mobx-5.15.7.tgz",
       "integrity": "sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==",
       "license": "MIT",
-      "peer": true,
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/mobx"
@@ -6403,7 +6455,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "bin": {
         "nanoid": "bin/nanoid.cjs"
       },
@@ -6794,7 +6845,6 @@
     "node_modules/pinia": {
       "version": "2.3.1",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@vue/devtools-api": "^6.6.3",
         "vue-demi": "^0.14.10"
@@ -6885,7 +6935,6 @@
       "resolved": "https://registry.npmmirror.com/preact/-/preact-10.27.2.tgz",
       "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
       "license": "MIT",
-      "peer": true,
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/preact"
@@ -6905,7 +6954,6 @@
       "version": "3.5.3",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "bin": {
         "prettier": "bin/prettier.cjs"
       },
@@ -6964,6 +7012,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/quansync": {
       "version": "0.2.10",
       "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.10.tgz",
@@ -7183,7 +7240,6 @@
       "version": "1.86.1",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "chokidar": "^4.0.0",
         "immutable": "^5.0.2",
@@ -7310,7 +7366,6 @@
       "resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
       "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "immer": "^9.0.6",
         "is-plain-object": "^5.0.0",
@@ -7352,7 +7407,6 @@
       "resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.3.tgz",
       "integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==",
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=12.17.0"
       }
@@ -8125,9 +8179,8 @@
     },
     "node_modules/typescript": {
       "version": "5.4.5",
-      "devOptional": true,
+      "dev": true,
       "license": "Apache-2.0",
-      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -8136,6 +8189,12 @@
         "node": ">=14.17"
       }
     },
+    "node_modules/uc.micro": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+      "license": "MIT"
+    },
     "node_modules/ufo": {
       "version": "1.6.1",
       "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz",
@@ -8802,7 +8861,6 @@
       "version": "5.4.16",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "esbuild": "^0.21.3",
         "postcss": "^8.4.43",
@@ -9446,7 +9504,6 @@
     "node_modules/vue": {
       "version": "3.5.13",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@vue/compiler-dom": "3.5.13",
         "@vue/compiler-sfc": "3.5.13",
@@ -9597,7 +9654,6 @@
     "node_modules/vue-router": {
       "version": "4.5.0",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@vue/devtools-api": "^6.6.4"
       },
@@ -9737,12 +9793,12 @@
       }
     },
     "node_modules/vxe-pc-ui": {
-      "version": "4.10.37",
-      "resolved": "https://registry.npmmirror.com/vxe-pc-ui/-/vxe-pc-ui-4.10.37.tgz",
-      "integrity": "sha512-brsAXxwKLRqlgLZvXvJf+Ys47rmBv85jLQ2WOqDNFqoiEpGNBuu0Ai9K69Z7o3Nz5J6UtF7MGnAPjFGCNx1lhQ==",
+      "version": "4.6.42",
+      "resolved": "https://registry.npmmirror.com/vxe-pc-ui/-/vxe-pc-ui-4.6.42.tgz",
+      "integrity": "sha512-grBaVbagoc5rbTq2jj1P/cWYP+sBo+VSXFRpNGYOe9Ka4EG9JP+LIa7h0lKfojDE5fGlPUYTkkYNe0fsQVDQ0g==",
       "license": "MIT",
       "dependencies": {
-        "@vxe-ui/core": "^4.2.15"
+        "@vxe-ui/core": "^4.1.5"
       }
     },
     "node_modules/vxe-table": {

+ 1 - 1
package.json

@@ -47,7 +47,7 @@
     "vue-virtual-scroller": "2.0.0-beta.8",
     "vue3-slider": "^1.10.1",
     "vuedraggable": "^4.1.0",
-    "vxe-pc-ui": "^4.6.42",
+    "vxe-pc-ui": "4.6.42",
     "vxe-table": "^4.7.62"
   },
   "devDependencies": {

+ 15 - 0
public/i18n/applet.develop.json

@@ -100,6 +100,21 @@
     },
     "healthTeach": {
       "title": "分享文章"
+    },
+    "orderText":{
+      "mineOrder": "我的服务" ,
+      "payStatusList": [
+        "待确认",
+        "已确认",
+        "已完成",
+        "全部"
+      ],
+      "paying": "待确认",
+      "paid": "已确认",
+      "paySuccess":"已完成",
+      "goPay":"去确认",
+      "selectGoods":"选择",
+      "payMeant": "确认"
     }
   }
 }

+ 15 - 0
public/i18n/applet.release.json

@@ -100,6 +100,21 @@
     },
     "healthTeach": {
       "title": "健康宣教"
+    },
+    "orderText":{
+      "mineOrder": "我的订单" ,
+      "payStatusList": [
+        "待付款",
+        "已付款",
+        "交易成功",
+        "全部"
+      ],
+      "paying": "待付款",
+      "paid": "已付款",
+      "paySuccess":"交易成功",
+      "goPay": "去购买",
+      "selectGoods":"选择商品",
+      "payMeant": "结算"
     }
   }
 }

+ 15 - 0
public/i18n/applet.trial.json

@@ -100,6 +100,21 @@
     },
     "healthTeach": {
       "title": "分享文章"
+    },
+    "orderText":{
+      "mineOrder": "我的服务" ,
+      "payStatusList": [
+        "待确认",
+        "已确认",
+        "已完成",
+        "全部"
+      ],
+      "paying": "待确认",
+      "paid": "已确认",
+      "paySuccess":"已完成",
+      "goPay":"去确认",
+      "selectGoods":"选择",
+      "payMeant": "确认"
     }
   }
 }

BIN
src/assets/images/cancel.png


+ 15 - 14
src/components/ChatHistory.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { ref } from 'vue';
 import { onMounted, onUnmounted, nextTick } from 'vue';
 import { Image, Input, Button } from 'ant-design-vue';
 import { UserOutlined, RobotOutlined, SearchOutlined, CustomerServiceOutlined } from '@ant-design/icons-vue';
@@ -82,10 +83,10 @@ onUnmounted(() => {
 // 获取搜索聊天记录列表
 async function getSearchChatRecordList(keyword: string) {
   if (loading.value) return;
-  
+
   loading.value = true;
   isSearchMode.value = true;
-  
+
   try {
     const patientId = (props.data as any)?.patientId || (props.data as any)?.id?.toString();
     if (!patientId) {
@@ -94,19 +95,19 @@ async function getSearchChatRecordList(keyword: string) {
       loading.value = false;
       return;
     }
-    
+
     // 重置分页
     currentPage.value = 1;
-    
+
     const res = await getConsultChatListMethod(currentPage.value, pageSize, patientId, {
       keyWord: keyword,
     });
-    
+
     if (res && res.data && res.data.length > 0) {
       totalRecords.value = res?.total || 0;
       // 搜索模式下,直接用搜索结果替换所有记录
       searchMessages.value = res.data;
-      
+
       // 检查是否还有更多数据
       if (searchMessages.value.length >= totalRecords.value || res.data.length < pageSize) {
         hasMore.value = false;
@@ -164,7 +165,7 @@ async function loadMoreMessages() {
   if (loading.value || !hasMore.value) return;
 
   currentPage.value++;
-  
+
   // 如果是搜索模式,加载更多搜索结果
   if (isSearchMode.value && searchKeyword.value) {
     await loadMoreSearchMessages();
@@ -176,9 +177,9 @@ async function loadMoreMessages() {
 // 加载更多搜索结果
 async function loadMoreSearchMessages() {
   if (loading.value) return;
-  
+
   loading.value = true;
-  
+
   try {
     const patientId = (props.data as any)?.patientId || (props.data as any)?.id?.toString();
     if (!patientId) {
@@ -187,15 +188,15 @@ async function loadMoreSearchMessages() {
       loading.value = false;
       return;
     }
-    
+
     const res = await getConsultChatListMethod(currentPage.value, pageSize, patientId, {
       keyWord: searchKeyword.value,
     });
-    
+
     if (res && res.data && res.data.length > 0) {
       // 追加到搜索结果
       searchMessages.value = [...searchMessages.value, ...res.data];
-      
+
       // 检查是否还有更多数据
       if (searchMessages.value.length >= totalRecords.value || res.data.length < pageSize) {
         hasMore.value = false;
@@ -301,7 +302,7 @@ watchDebounced(
       console.error('缺少患者ID');
       return;
     }
-    
+
     if (newVal && newVal.trim()) {
       // 有搜索关键词,执行搜索
       await getSearchChatRecordList(newVal.trim());
@@ -579,7 +580,7 @@ function handleClose() {
     overflow-wrap: break-word;
     width: 100%;
     line-height: 1.5;
-    
+
     // Markdown 渲染样式
     :deep(strong) {
       font-weight: bold;

+ 3 - 2
src/components/EditConfigured.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { ref } from 'vue';
 import { useRequest } from 'alova/client';
 import { getDeviceManageDetailMethod, updateDeviceManageMethod } from '@/request/api/device.api';
 import { branchMethod } from '@/request/api/system.api';
@@ -121,10 +122,10 @@ const reset = () => {
   flex: 1;
   display: flex;
   flex-direction: column;
-  min-height: 0; 
+  min-height: 0;
   > div:not(.title) {
     flex: auto;
-    overflow: auto; 
+    overflow: auto;
   }
   > .title {
     flex: none;

+ 2 - 4
src/components/EditOrganization.vue

@@ -1,11 +1,12 @@
 <script setup lang="ts">
+import { watchEffect, ref, computed } from 'vue';
 import { VxeUI, type VxeFormProps, type VxeFormListeners } from 'vxe-pc-ui';
 import { useRequest } from 'alova/client';
 import { branchMethod } from '@/request/api/system.api';
 import { notification } from 'ant-design-vue';
 import type { EquirementModel } from '@/model/device.model';
 type FollowModel = Partial<EquirementModel>;
-  (notification.config as any)({
+(notification.config as any)({
   zIndex: 10000, // 直接设置层级
 });
 const defaultModel = {};
@@ -138,13 +139,10 @@ const formEmits: VxeFormListeners = {
       notification.error({ message: '请选择需要修改的机构' });
       return;
     }
-    // const selectedOrg = branch.value.find((item) => item.value === model.value.orgId);
-
     emits('submit', {
       orgId: data?.orgId,
       institutionId: data?.institutionId,
     } as any);
-    // 或者先调接口再 emit
   },
 };
 

+ 50 - 59
src/components/EditSupplier.vue

@@ -289,6 +289,17 @@ initTimeRangeValues();
 
 const formRef = ref<any>(null);
 
+// 营业时间字段(始终存在于表单中,通过样式控制显示/隐藏,避免动态增删表单项导致下拉失焦收起)
+const businessHoursField = {
+  field: 'businessTime',
+  title: '营业时间',
+  span: 24,
+  slots: {
+    title: 'businessTimeTitle',
+    default: 'businessHours',
+  },
+  className: 'business-hours-item',
+} as const;
 
 const baseFormItems = [
   {
@@ -412,6 +423,14 @@ const baseFormItems = [
     span: 24,
     itemRender: { name: 'VxeInput', props: { placeholder: '请输入', type: 'number', min: 0, max: 100 } },
   },
+  // 账期(天)
+  {
+    field: 'paymentTerm',
+    title: '账期',
+    span: 24,
+    slots: { default: 'paymentTerm' },
+  },
+  businessHoursField as any,
   // 营业状态 单选,营业、休息、停业
   {
     field: 'businessStatus',
@@ -423,44 +442,12 @@ const baseFormItems = [
   { align: 'center', span: 24, slots: { default: 'active' } },
 ];
 
-// 营业时间字段
-const businessHoursField = {
-  field: 'businessTime',
-  title: '营业时间',
-  span: 24,
-  slots: {
-    title: 'businessTimeTitle',
-    default: 'businessHours',
-  },
-} as const;
-
-// 更新表单项
-function updateFormItems() {
-  const items = [...baseFormItems];
-  const shouldShow = showBusinessHours.value;
-  // 查找分账比例和营业时间
-  const profitSharingIndex = items.findIndex(item => item.field === 'profitSharing');
-  const businessTimeIndex = items.findIndex(item => item.field === 'businessTime');
-
-  if (shouldShow) {
-    if (businessTimeIndex === -1 && profitSharingIndex !== -1) {
-      items.splice(profitSharingIndex + 1, 0, businessHoursField as any);
-    }
-  } else {
-    if (businessTimeIndex !== -1) {
-      items.splice(businessTimeIndex, 1);
-    }
-  }
-
-  formProps.items = items as any;
-}
-
 const formProps = reactive<VxeFormProps<FormModel>>({
   titleWidth: 100,
   titleAlign: 'right',
   titleColon: true,
   data: { ...props.data },
-  items: baseFormItems as any,
+  items: [...baseFormItems] as any,
   rules: {
     name: [{ required: true, message: '请输入供应商名称' }],
     detailAddress: [{ required: true, message: '请输入地址' }],
@@ -484,6 +471,7 @@ const formProps = reactive<VxeFormProps<FormModel>>({
     wechatPaymentType: [{ required: true, message: '请选择账户类型' }],
     wechatPaymentAccount: [{ required: true, message: '请输入微信接收方账号' }],
     profitSharing: [{ required: true, message: '请输入分账比例' }],
+    paymentTerm: [{ required: true, message: '请输入账期' }],
     businessStatus: [{ required: true, message: '请选择营业状态' }],
     businessTime: [
       {
@@ -519,12 +507,12 @@ const formProps = reactive<VxeFormProps<FormModel>>({
 });
 
 // 判断是否显示营业时间(仅限"线下项目"有值时展示)
-const showBusinessHours = computed<boolean>(() => {
+const hasOfflineSelected = computed<boolean>(() => {
   const offlineCPTypes: string[] | undefined = formProps.data?.offlineCPTypes;
   return !!(offlineCPTypes && Array.isArray(offlineCPTypes) && offlineCPTypes.length > 0);
 });
+const showBusinessHours = hasOfflineSelected;
 
-const prevOfflineCPTypes = ref<string[] | undefined>(undefined);
 let wasCleared = false;
 
 // 重置营业时间为默认值
@@ -546,25 +534,20 @@ function resetBusinessHours() {
   });
 }
 
-// 监听线下项目变化,更新表单项,并在清除后再次选择时重置营业时间
-watch(() => formProps.data?.offlineCPTypes, (newVal) => {
-  const newHasValue = !!(newVal && Array.isArray(newVal) && newVal.length > 0);
-  const oldHasValue = !!(prevOfflineCPTypes.value && Array.isArray(prevOfflineCPTypes.value) && prevOfflineCPTypes.value.length > 0);
-
-
-  if (oldHasValue && !newHasValue) {
-    wasCleared = true;
-  }
-
-
-  if (wasCleared && !oldHasValue && newHasValue) {
-    resetBusinessHours();
-    wasCleared = false;
-  }
-
-  prevOfflineCPTypes.value = newVal ? [...newVal] : undefined;
-  updateFormItems();
-}, { deep: true });
+// 仅在“是否选中线下服务”发生变化时处理:
+// - 避免每次勾选/取消一个选项都触发表单 items 重建,导致下拉框自动收起
+// - 保持原逻辑:清空后再次选择时重置营业时间;并联动显示/隐藏营业时间字段
+watch(
+  hasOfflineSelected,
+  (newHasValue, oldHasValue) => {
+    if (oldHasValue && !newHasValue) wasCleared = true;
+    if (wasCleared && !oldHasValue && newHasValue) {
+      resetBusinessHours();
+      wasCleared = false;
+    }
+  },
+  { immediate: true }
+);
 
 const formEmits: VxeFormListeners<FormModel> = {
   submit({ data }) {
@@ -675,15 +658,11 @@ onBeforeMount(async () => {
   if (props.data?.id) load(props.data);
   await getOnlineCPList();
   await getOfflineCPList();
-  // 初始化线下服务
-  prevOfflineCPTypes.value = formProps.data?.offlineCPTypes ? [...formProps.data.offlineCPTypes] : undefined;
-  // 初始化表单
-  updateFormItems();
 });
 </script>
 
 <template>
-  <div class="form-container">
+  <div class="form-container" :class="{ 'business-hours-hidden': !showBusinessHours }">
     <vxe-form ref="formRef" v-bind="formProps" v-on="formEmits" :loading="submitting">
       <template #initiate>
         <a-tree-select style="width: 100%" :show-checked-strategy="SHOW_ALL" tree-check-strictly :tree-data="branch"
@@ -692,6 +671,12 @@ onBeforeMount(async () => {
           :dropdownMatchSelectWidth="false" :dropdownStyle="{ zIndex: 4000 }" placeholder="请选择" @change="onDeptChange"
           @dropdownVisibleChange="onVisibleChange" />
       </template>
+      <template #paymentTerm>
+        <div style="display: flex; align-items: center; gap: 4px;">
+          <a-input-number v-model:value="(formProps.data as any).paymentTerm" placeholder="请输入" style="flex: 1" :min="0" :precision="0" />
+          <span>天</span>
+        </div>
+      </template>
       <template #businessTimeTitle>
         <span style="color: #ff4d4f">*</span>营业时间
       </template>
@@ -728,6 +713,12 @@ onBeforeMount(async () => {
   padding: 20px;
 }
 
+.form-container.business-hours-hidden {
+  :deep(.business-hours-item) {
+    display: none !important;
+  }
+}
+
 .business-hours-container {
   width: 100%;
 }

+ 1 - 1
src/components/UserPreview.vue

@@ -18,7 +18,7 @@ const { data, loading } = useRequest(
 <template>
   <a-spin :spinning="loading">
     <a-descriptions bordered>
-      <a-descriptions-item label="系统账号">{{ data.userId }}</a-descriptions-item>
+      <a-descriptions-item label="系统账号">{{data.userName}}</a-descriptions-item>
       <a-descriptions-item label="角色" :span="2">{{ data.roles?.map(t => t.roleName)?.join(', ') }}
       </a-descriptions-item>
       <a-descriptions-item label="医院 / 科室" :span="2">{{ data.dept?.deptFullPathName }}</a-descriptions-item>

+ 1 - 0
src/model/care.model.ts

@@ -18,6 +18,7 @@ export interface SupplierModel {
   profitSharing?:string; //	分账比例
   businessTime?:string; //营业时间,比如:08:10,12:10,23:59
   businessStatus?:string; //	营业状态 1-营业 2-休息 3-停业	
+  paymentTerm:number;//账期
 }
 export type SupplierQuery = Partial<SupplierModel>;
 

+ 178 - 63
src/model/order.model.ts

@@ -7,43 +7,44 @@
 // }
 
 export interface OrderModel {
-id: number; //	线下服务ID
-patientConditioningProgramId: number; //患者调理方案id
-sequence: number; //次序
-liaison:string;//联系人
-phone: string; //联系电话
-provinceName: string; //省名称
-cityName: string; //市名称
-areaName: string; //区名称
-detailAddress: string; //详细地址
-orderNo: string; //订单编号
-institutionId: number; //订单所属机构ID
-institutionName: string; //订单所属机构名称
-conditioningProgramName: string; //项目名称
-conditioningProgramType: string; //项目类型
-applyTime:string;//用户操作时间
-offlineDuration:string;//	服务时长
-arrangeDate:string;//预约日期
-arrangeTime:string;//预约时间段-开始时间
-arrangeEndTime:string;//预约时间段-结束时间
-conditioningProgramSupplierId:number;//本次供应商ID
-conditioningProgramSupplierName:string;//本次供应商名称
-preConditioningProgramSupplierId:number;//上次供应商ID
-preConditioningProgramSupplierName:string;//上次供应商名称
-pieBy:string;//派单人
-pieTime:string;//派单时间
-operateBy:string;//操作者
-operateTime:string;//操作时间
-startTime:string;//操作开始时间
-endTime:string;//操作结束时间
-feedback:string;//治疗备注
-photo:string;//图片
-type:number;//订单类型 1-未派单 2-已派单但未核销 3-已核销
-// 实体商品
-receiptStatus:string;//收货状态 0-待发货 1-已发货 2-已收货
-types:string[];//订单类型 1-未派单 2-已派单但未核销 3-已核销	
-pieTimeStart:string;//派单时间开始
-pieTimeEnd:string;//派单时间结束
+  id: number; //	线下服务ID
+  patientConditioningProgramId: number; //患者调理方案id
+  sequence: number; //次序
+  liaison: string;//联系人
+  phone: string; //联系电话
+  provinceName: string; //省名称
+  cityName: string; //市名称
+  areaName: string; //区名称
+  detailAddress: string; //详细地址
+  orderNo: string; //订单编号
+  institutionId: number; //订单所属机构ID
+  institutionName: string; //订单所属机构名称
+  conditioningProgramName: string; //项目名称
+  conditioningProgramType: string; //项目类型
+  applyTime: string;//用户操作时间
+  offlineDuration: string;//	服务时长
+  arrangeDate: string;//预约日期
+  arrangeTime: string;//预约时间段-开始时间
+  arrangeEndTime: string;//预约时间段-结束时间
+  conditioningProgramSupplierId: number;//本次供应商ID
+  conditioningProgramSupplierName: string;//本次供应商名称
+  preConditioningProgramSupplierId: number;//上次供应商ID
+  preConditioningProgramSupplierName: string;//上次供应商名称
+  pieBy: string;//派单人
+  pieTime: string;//派单时间
+  operateBy: string;//操作者
+  operateTime: string;//操作时间
+  startTime: string;//操作开始时间
+  endTime: string;//操作结束时间
+  feedback: string;//治疗备注
+  photo: string;//图片
+  type: number;//订单类型 1-未派单 2-已派单但未核销 3-已核销
+  // 实体商品
+  receiptStatus: string;//收货状态 0-待发货 1-已发货 2-已收货
+  types: string[];//订单类型 1-未派单 2-已派单但未核销 3-已核销	
+  pieTimeStart: string;//派单时间开始
+  pieTimeEnd: string;//派单时间结束
+  orderByType: number;
 }
 export type OrderQuery = Partial<OrderModel>;
 //派单机构列表
@@ -65,6 +66,7 @@ export interface OrderLiaisonListModel {
       deptName: string; //调理方案供应商合作机构名称
     }
   ]; //合作机构
+
   onlineCPTypes: string[]; //可供应的实体商品类型
   offlineCPTypes: string[]; //	可供应的线下服务类型
   outlineCPTypes: string[]; //可供应的线上权益类型
@@ -90,23 +92,23 @@ export type OrderLiaisonListQuery = Partial<OrderLiaisonListModel>;
 
 
 
-export interface ShipmentModel{
+export interface ShipmentModel {
   id: number; //	患者调理方案ID
   orderNo: string; //订单ID
-  conditioningProgramSupplierId:number;//供应商ID
-  conditioningProgramSupplierName:string;//供应商名称
-  payTimeStart:string;//付款时间——起始时间	
-  payTimeEnd:string;//付款时间——截止时间
+  conditioningProgramSupplierId: number;//供应商ID
+  conditioningProgramSupplierName: string;//供应商名称
+  payTimeStart: string;//付款时间——起始时间	
+  payTimeEnd: string;//付款时间——截止时间
   payTime: string; //订单编号
   conditioningProgramName: string; //调理方案名称
   pricingUnit: string; //计价单位
   unitPrice: number; //单价
-  totalMeasure:number;//总用量
-  totalPrice:number;//总价格
-  receiptStatus:string;//发货状态 0-待发货 1-已发货 2-已收货
-  receiptType:string;//	发货形式 0-配送 1-线下取货
-  expressType:string;//快递类型 0-邮政速递 1-顺丰速运 2-京东快递 3-中通快递 4-圆通速递 5-申通快递 6-韵达快递 7-极兔速递
-  expressNo:string;//快递单号
+  totalMeasure: number;//总用量
+  totalPrice: number;//总价格
+  receiptStatus: string;//发货状态 0-待发货 1-已发货 2-已收货
+  receiptType: string;//	发货形式 0-配送 1-线下取货
+  expressType: string;//快递类型 0-邮政速递 1-顺丰速运 2-京东快递 3-中通快递 4-圆通速递 5-申通快递 6-韵达快递 7-极兔速递
+  expressNo: string;//快递单号
 
 }
 export type ShipmentQuery = Partial<ShipmentModel>;
@@ -128,23 +130,136 @@ export interface RevenueSharingDetailModel {
   id: number; //	主键ID
   orderNo: string; //订单ID
   payTime: string; //付款时间
-  profitSharingTimeStart:string;//分账时间开始
-  profitSharingTimeEnd:string;//分账时间结束
-  finishTime:string;//收货/核销/完成时间
-  profitSharingTime:string;//分账时间
+  profitSharingTimeStart: string;//分账时间开始
+  profitSharingTimeEnd: string;//分账时间结束
+  finishTime: string;//收货/核销/完成时间
+  profitSharingTime: string;//分账时间
   conditioningProgramName: string; //调理方案名称
-  conditioningProgramType:string;//调理方案类型
-  sellType:string;//	商品类型 1-实体商品 2-线下服务 3-线上权益
+  conditioningProgramType: string;//调理方案类型
+  sellType: string;//	商品类型 1-实体商品 2-线下服务 3-线上权益
   pricingUnit: string; //计价单位
   unitPrice: number; //单价
-  totalMeasure:number;//总用量
-  totalPrice:number;//总价格
-  conditioningProgramSupplierName:string;//供应商名称
-  profitSharing:string;//分账比例
-  profitSharingAmount:number;//预计分账金额分账金额
-  realAmount:string;//实际到账金额
-  profitSharingStatus:string;//分账状态 1-未分账 2-已分账 3-分账异常
-  
+  totalMeasure: number;//总用量
+  totalPrice: number;//总价格
+  conditioningProgramSupplierName: string;//供应商名称
+  profitSharing: string;//分账比例
+  profitSharingAmount: number;//预计分账金额分账金额
+  realAmount: string;//实际到账金额
+  profitSharingStatus: string;//分账状态 1-未分账 2-已分账 3-分账异常
+
+
+}
+export type RevenueSharingDetailQuery = Partial<RevenueSharingDetailModel>;
+
+
+export interface EvaluateDetailModel {
+  patientConditioningRecordId: number; //	患者调理记录ID
+  patientConditioningProgramId: string; //患者调理方案ID
+  lineId: string; //调理任务ID
+  complianceScore: string;//相符度评分
+  qualityScore: string;//质量评分
+  attitudeScore: string;//	态度评分
+  environmentScore: string;//环境评分
+  depict: string;//	评价描述
+  imageVideos: string[];//图片视频
+  createTime: string;//创建时间
+}
+
+export interface ApplyRecordModel {
+  id: number; // 主键ID
+  arrangeTime: string; // 预定时间
+  arrangeDuration: number; // 预定时长
+  arrangePeriod: string; // 服务时间段
+  applyTime: string; // 预约时间
+  updateTime: string; // 更新预约时间
+  cancelTime: string; // 取消预约时间
+  conditioningProgramSupplierId: number; // 供应商ID
+  conditioningProgramSupplierName: string; // 供应商名称
+  pieBy: string; // 派单人
+  pieTime: string; // 派单时间
+  operateTime: string; // 操作时间
+  status: string; // 服务状态
+}
+
+export interface LogisticsModel {
+  trailUrl: string;
+  arrivalTime: string;
+  totalTime: string;
+  remainTime: string;
+  tracks: [
+    {
+      time: string;
+      context: string;
+      ftime: string;
+      areaCode: string;
+      areaName: string;
+      status: string;
+      areaCenter: string;
+      areaPinYin: string;
+      statusCode: string;
+      location: string;
+    }
+  ]
+}
+
+// 售后
+export interface AfterSaleModel {
+  id: number; // 主键ID
+  orderNo: string; // 订单编号
+  type: string; // 售后类型
+  typeStr: string; // 售后类型文字
+  ref: string; // 关联标识
+  updateTime: string; // 更新时间
+  patientConditioningProgramId: number; // 患者调理方案ID
+  conditioningProgramName: string; // 商品名称
+  conditioningProgramPhoto: string; // 商品图片
+  sellType: string; // 商品类型 1-实体商品 2-线下服务 3-线上权益
+  sellTypeStr: string; // 商品类型文字
+  pricingUnit: string; // 计价单位
+  unitPrice: number; // 单价
+  convertUnit: string; // 换算单位
+  convertDose: number; // 换算剂量
+  totalMeasure: number; // 总用量
+  totalPrice: number; // 总价格
+  liaison: string; // 联系人
+  progress: string; // 售后进度
+  progressStr: string; // 售后进度文字
+  expressStatus: string; // 物流状态
+  sequence: number; // 次序
+  finishRatio: string; // 完成比例
+  refundAmount: number; // 退款金额
+  applyAmount: number; // 申请金额
+  reviewAmount: number; // 审核金额
+  reason: string; // 原因
+  voucher: string; // 凭证
+  applyTime: string; // 申请时间
+  finishTime: string; // 完结时间
+  receiptStatus: string; // 商品申请售后时的状态
+  receiptStatusStr: string; // 商品申请售后时的状态文字
+  receiptType: string; // 收货状态 1-已收到货 2-未收到货
+  receiptTypeStr: string; // 收货状态文字
+  remark: string; // 退款说明
+  voucherImgs: string[]; // 退款凭证图片集合
+  title: string; // 节点标题
+  content: string; // 节点内容描述
+  operaTime: string; // 操作时间
+  handleEndTime: string; // 处理截止时间
+}
+export type AfterSaleQuery = Partial<AfterSaleModel>;
+
+// 售后协商历史
+export interface AfterSaleNegotiateModel {
+  title: string; // 标题
+  progress: string;//售后进度 0-发起售后 1-撤销售后 2-审核拒绝 3-审核通过 4-退款完成
+  content: string; // 内容描述
+  createTime: string; // 操作时间
+  createBy: string;//操作者
+  aftersaleContent: Record<string, any>; // 售后申请信息
+}
 
+// 售后审核
+export interface AfterSaleAuditModel {
+  id: number; // 售后ID
+  isAgree: boolean; // 是否同意
+  reason: string; // 拒绝理由
 }
-export type RevenueSharingDetailQuery = Partial<RevenueSharingDetailModel>;

+ 226 - 0
src/order/Amount.vue

@@ -0,0 +1,226 @@
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue';
+import { Button, Input, message } from 'ant-design-vue';
+import { getConfirmAmountMethod } from '@/request/api/order.api';
+
+const props = defineProps<{
+  data?: any;
+}>();
+
+const emit = defineEmits<{
+  'submit': [amount: number];
+  'close': [];
+}>();
+
+// 商品总价
+const totalPrice = computed(() => {
+  return props.data?.totalPrice || 0;
+});
+
+// 最终退款金额 (双向绑定)
+const finalAmount = ref<string | number>(totalPrice.value.toFixed(2));
+
+// 监听数据变化同步初始值
+watch(() => props.data, (newVal) => {
+  if (newVal?.reviewAmount != null) {
+    finalAmount.value = newVal.reviewAmount.toFixed(2);
+  } else if (newVal?.applyAmount != null) {
+    finalAmount.value = newVal.applyAmount.toFixed(2);
+  }
+}, { immediate: true });
+
+// 校验金额
+function handleAmountChange(e: any) {
+  const value = e.target.value;
+  // 只允许数字和小数点
+  const formatted = value.replace(/[^\d.]/g, '');
+  finalAmount.value = formatted;
+}
+
+function handleBlur() {
+  let val = parseFloat(finalAmount.value as string);
+  if (isNaN(val)) {
+    val = 0;
+  }
+  
+  if (val > totalPrice.value) {
+    return;
+  }
+
+  finalAmount.value = val.toFixed(2);
+}
+
+// 提交
+const submitting = ref(false);
+async function handleConfirm() {
+  if (submitting.value) return;
+  const val = parseFloat(finalAmount.value as string);
+  if (isNaN(val) || val < 0) {
+    message.error('请输入有效的退款金额');
+    return;
+  }
+  if (val > totalPrice.value) {
+    message.warning(`退款金额不能超过商品总价 ¥${totalPrice.value.toFixed(2)}`);
+    return;
+  }
+  if (!props.data?.id) return;
+  submitting.value = true;
+  try {
+    await getConfirmAmountMethod(props.data.id, val);
+    message.success('金额确认成功');
+    emit('submit', val);
+  } finally {
+    submitting.value = false;
+  }
+}
+
+// 取消
+function handleCancel() {
+  emit('close');
+}
+</script>
+
+<template>
+  <div class="amount-popup-container">
+    <div class="amount-content">
+      <!-- 申请金额展示 -->
+      <div class="info-row apply-amount">
+        <span class="label">申请金额:</span>
+        <span class="symbol">¥</span>
+        <span class="value">{{ totalPrice.toFixed(2) }}</span>
+      </div>
+
+      <!-- 最终退款金额编辑 -->
+      <div class="info-row final-amount">
+        <span class="label">最终退款金额:</span>
+        <div class="input-wrapper">
+          <span class="symbol">¥</span>
+          <input 
+            type="text" 
+            v-model="finalAmount" 
+            class="amount-input"
+            @input="handleAmountChange"
+            @blur="handleBlur"
+          />
+        </div>
+      </div>
+
+      <!-- 提示文字 -->
+      <div class="note-text">
+        确认后,退款将退回原账户!
+      </div>
+
+      <!-- 底部按钮 -->
+      <div class="footer-actions">
+        <Button @click="handleCancel" class="btn-cancel">取消</Button>
+        <Button type="primary" :loading="submitting" @click="handleConfirm" class="btn-confirm">确定</Button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.amount-popup-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 40px 20px;
+  background: #fff;
+  min-height: 300px;
+}
+
+.amount-content {
+  width: 100%;
+  max-width: 400px;
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+
+  .info-row {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 24px;
+    
+    .label {
+      font-size: 16px;
+      color: #333;
+      font-weight: bold;
+    }
+
+    &.apply-amount {
+      .symbol {
+        font-size: 18px;
+        margin-left: 4px;
+        font-weight: bold;
+      }
+      .value {
+        font-size: 28px;
+        font-weight: bold;
+        color: #333;
+      }
+    }
+
+    &.final-amount {
+      .input-wrapper {
+        display: flex;
+        align-items: center;
+        border: 1px solid #d9d9d9;
+        border-radius: 2px;
+        padding: 4px 8px;
+        margin-left: 8px;
+        width: 140px;
+        
+        .symbol {
+          font-size: 16px;
+          color: #333;
+          margin-right: 4px;
+        }
+
+        .amount-input {
+          border: none;
+          outline: none;
+          width: 100%;
+          font-size: 20px;
+          color: #ff4d4f; // 红色文字
+          font-weight: bold;
+          text-align: left;
+        }
+      }
+    }
+  }
+
+  .note-text {
+    font-size: 15px;
+    color: #333;
+    margin-top: 8px;
+    margin-bottom: 40px;
+  }
+
+  .footer-actions {
+    display: flex;
+    gap: 40px;
+    justify-content: center;
+    width: 100%;
+
+    .ant-btn {
+      height: 36px;
+      min-width: 80px;
+      border-radius: 4px;
+      font-size: 14px;
+    }
+
+    .btn-cancel {
+      border-color: #1890ff;
+      color: #1890ff;
+      background: #fff;
+    }
+
+    .btn-confirm {
+      background-color: #2f74ff;
+      border-color: #2f74ff;
+    }
+  }
+}
+</style>

+ 35 - 5
src/order/DispatchOrderPanel.vue

@@ -133,7 +133,10 @@ function calculateEndTime(order: OrderModel): string | null {
 function isOrderVerified(order: OrderModel): boolean {
   return props.orderType === 'offline' && order.type === 3;
 }
-
+// 判断订单是否取消预约派单(线下服务)
+function isOrderCancelPie(order: OrderModel): boolean {
+  return props.orderType === 'offline' && order.type === 4;
+}
 // 判断订单是否已发货(实体商品)
 function isOrderShipped(order: OrderModel): boolean {
   return props.orderType === 'physical' && order.type === 3;
@@ -141,11 +144,17 @@ function isOrderShipped(order: OrderModel): boolean {
 
 // 右侧机构表格配置
 const gridRef = ref<VxeGridInstance<Institution>>();
+// 机构列表排序:1-评分升序 2-评分降序 3-订单数升序 4-订单数降序
+const institutionOrderByType = ref<number | undefined>(undefined);
 const gridOptions = reactive<VxeGridProps<Institution>>({
   border: true,
   autoResize: true,
   height: '100%',
   scrollY: { enabled: true },
+  sortConfig: {
+    remote: true,
+    orders: ['asc', 'desc'],
+  },
   columns: [
     {
       field: 'action',
@@ -170,7 +179,8 @@ const gridOptions = reactive<VxeGridProps<Institution>>({
     { field: 'name', title: '机构名称' },
     { field: 'detailAddress', title: '地址' },
     { field: 'phone', title: '联系电话' },
-    { field: 'todayOrderQuantity', title: '当日订单数' },
+    { field: 'todayOrderQuantity', title: '当日订单数', sortable: true, sortType: 'number' },
+    { field: 'evaluateScore', title: '机构评分', sortable: true, sortType: 'number' },
   ],
   data: [],
   pagerConfig: {
@@ -178,6 +188,18 @@ const gridOptions = reactive<VxeGridProps<Institution>>({
   },
 });
 
+function mapInstitutionSortToOrderByType(field: string | undefined, order: string | undefined | null) {
+  if (!field || !order) return undefined;
+  if (field === 'evaluateScore') return order === 'asc' ? 1 : 2;
+  if (field === 'todayOrderQuantity') return order === 'asc' ? 3 : 4;
+  return undefined;
+}
+
+async function handleInstitutionSortChange({ field, order }: { field?: string; order?: 'asc' | 'desc' | null }) {
+  institutionOrderByType.value = mapInstitutionSortToOrderByType(field, order);
+  await loadInstitutionList();
+}
+
 // 初始化
 onMounted(async () => {
   await loadOrderCounts();
@@ -458,7 +480,8 @@ async function loadInstitutionList() {
         ? { timeStart: `${selectedOrder.arrangeDate} ${selectedOrder.arrangeTime}` }
         : {}),
       // 线下服务 type=2,实体商品 type=1
-      type: props.orderType === 'offline' ? 2 : 1
+      type: props.orderType === 'offline' ? 2 : 1,
+      orderByType: institutionOrderByType.value,
     };
 
     // 获取可派单机构列表
@@ -1100,9 +1123,11 @@ defineExpose({
                 <!-- 已核销/已发货印章 -->
                 <!-- 线下服务:已核销状态显示已核销图标 -->
                 <!-- 实体商品:已发货状态显示已发货图标 -->
-                <div v-if="isOrderVerified(order) || isOrderShipped(order)" class="verified-badge">
+                <div v-if="isOrderVerified(order) || isOrderShipped(order) || isOrderCancelPie(order)" class="verified-badge">
                   <img v-if="isOrderVerified(order)" src="@/assets/images/verify.png" alt="已核销"
                     style="width: 100px; height: 100px;" />
+                  <img v-else-if="isOrderCancelPie(order)" src="@/assets/images/cancel.png" alt="已取消派单"
+                    style="width: 100px; height: 100px;" />
                   <img v-else-if="isOrderShipped(order)" src="@/assets/images/shipment.png" alt="已发货"
                     style="width: 100px; height: 100px;" />
                 </div>
@@ -1218,7 +1243,12 @@ defineExpose({
       <div class="institution-panel">
         <div class="panel-title">可派单机构</div>
         <div class="institution-grid-wrapper">
-          <vxe-grid ref="gridRef" v-bind="gridOptions" :loading="institutionLoading" />
+          <vxe-grid
+            ref="gridRef"
+            v-bind="gridOptions"
+            :loading="institutionLoading"
+            @sort-change="handleInstitutionSortChange"
+          />
         </div>
       </div>
     </div>

+ 6 - 21
src/order/EditShipment.vue

@@ -31,33 +31,18 @@ const formData = ref<ShipmentForm>({
   expressType: '',
   expressNo: '',
 });
-
-// 快递类型选项
-const expressTypeOptionsList = [
-  { label: '邮政速递', value: '0' },
-  { label: '顺丰速运', value: '1' },
-  { label: '京东快递', value: '2' },
-  { label: '中通快递', value: '3' },
-  { label: '圆通速递', value: '4' },
-  { label: '申通快递', value: '5' },
-  { label: '韵达快递', value: '6' },
-  { label: '极兔速递', value: '7' },
-];
-
 onBeforeMount(async () => {
   try {
-    const res = await getDictionaryMethod('logistics_company').catch(() => null);
+    const res = await getDictionaryMethod('express_type').catch(() => null);
+    console.log(res,"获取数据")
     if (res && Array.isArray(res) && res.length > 0) {
       expressTypeOptions.value = res;
-    } else {
-      // 如果没有字典数据,使用默认选项
-      expressTypeOptions.value = expressTypeOptionsList;
+     
     }
   } catch (error) {
     // 使用默认数据
-    expressTypeOptions.value = expressTypeOptionsList;
+   console.log(error)
   }
-
   if (props.data) {
     formData.value.receiptType = props.data.receiptType || '1';
     formData.value.expressType = props.data.expressType || '';
@@ -114,8 +99,8 @@ const handleSubmit = async () => {
     id,
     conditioningProgramSupplierId: props.data?.conditioningProgramSupplierId,
     receiptType,
-    expressType : expressType || '',
-    expressNo : expressNo || '',
+    expressType: expressType || '',
+    expressNo: expressNo || '',
   } as any;
   try {
     await submit(submitData);

+ 466 - 0
src/order/LogisticsDetails.vue

@@ -0,0 +1,466 @@
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { notification } from 'ant-design-vue';
+import { DownOutlined, UpOutlined, StarOutlined } from '@ant-design/icons-vue';
+import { getLogisticsMethod } from '@/request/api/order.api';
+
+const props = defineProps<{
+  data?: any;
+}>();
+
+const showAllLogistics = ref(false);
+
+const expressTypeText: Record<string, string> = {
+  '0': '邮政速递',
+  '1': '顺丰速运',
+  '2': '京东快递',
+  '3': '中通快递',
+  '4': '圆通速递',
+  '5': '申通快递',
+  '6': '韵达快递',
+  '7': '极兔速递',
+};
+
+const displayData = computed(() => props.data || {});
+
+const logisticsTracks = ref<any[]>([]);
+
+// 获取物流信息
+async function getLogistics(id: string) {
+  const res = await getLogisticsMethod(id);
+  if (!res) return;
+  logisticsTracks.value = res?.tracks ?? [];
+}
+
+// 物流信息
+const logisticsInfo = computed(() => {
+  const d = displayData.value;
+  return {
+    trackingNumber: `${expressTypeText[d.expressType ?? ''] ?? ''} ${d.expressNo ?? ''}`.trim(),
+    recipientName: d.liaison ? d.liaison + ', ' : '',
+    recipientPhone: d.phone ? d.phone + ', ' : '',
+    recipientAddress: [d.provinceName, d.cityName, d.areaName, d.detailAddress].filter((v) => v != null).join(' '),
+  };
+});
+
+type LogisticsTimelineItem = {
+  title: string;
+  time: string;
+  detail: string;
+};
+
+function normalizeLogisticsItem(item: any): LogisticsTimelineItem | null {
+  if (!item || typeof item !== 'object') return null;
+  const time = String(item.time ?? item.acceptTime ?? item.ftime ?? item.createTime ?? item.updateTime ?? '').trim();
+  const title = String(item.title ?? item.status ?? item.statusName ?? item.statusDesc ?? '').trim();
+  const detail = String(item.content ?? item.context ?? item.remark ?? item.description ?? item.message ?? '').trim();
+  const fallbackTitle = title || detail;
+  if (!time && !fallbackTitle && !detail) return null;
+  return {
+    title: title || '-',
+    time,
+    detail,
+  };
+}
+
+function normalizeList(list: any[]): LogisticsTimelineItem[] {
+  if (!Array.isArray(list)) return [];
+  return list.map((item: any) => normalizeLogisticsItem(item)).filter((item): item is LogisticsTimelineItem => Boolean(item));
+}
+
+const timelineData = computed<LogisticsTimelineItem[]>(() => {
+  const fromApi = normalizeList(logisticsTracks.value);
+  if (fromApi.length > 0) return fromApi;
+  const source = displayData.value;
+  const listCandidate =
+    source?.expressDetails ??
+    source?.logisticsDetails ??
+    source?.logisticsList ??
+    source?.expressRouteList ??
+    source?.expressRoutes ??
+    source?.traces ??
+    source?.traceList ??
+    source?.routeList ??
+    source?.tracks ??
+    [];
+  return normalizeList(listCandidate);
+});
+
+const visibleTimeline = computed(() => (showAllLogistics.value ? timelineData.value : timelineData.value.slice(0, 2)));
+
+const canExpandLogistics = computed(() => timelineData.value.length > 2);
+
+function toggleLogisticsExpand() {
+  showAllLogistics.value = !showAllLogistics.value;
+}
+
+function handleCopyTracking() {
+  const text = logisticsInfo.value.trackingNumber;
+  if (!text) return;
+  navigator.clipboard
+    .writeText(text)
+    .then(() => {
+      notification.success({ message: '复制成功' });
+    })
+    .catch(() => {
+      notification.error({ message: '复制失败' });
+    });
+}
+
+const packageItems = computed(() => {
+  const real = displayData.value.sameExpress;
+  if (real && real.length > 0) return real;
+  return [
+    { conditioningProgramName: '元气茶', convertDose: '30', convertUnit: 'g', unitPrice: '32.00', totalMeasure: 3, conditioningProgramPhoto: '' },
+    { conditioningProgramName: '芡实米仁燕麦粥', convertDose: '500', convertUnit: 'ml', unitPrice: '45.00', totalMeasure: 1, conditioningProgramPhoto: '' },
+    { conditioningProgramName: '枸杞养生茶', convertDose: '15', convertUnit: 'g', unitPrice: '28.00', totalMeasure: 2, conditioningProgramPhoto: '' },
+  ];
+});
+
+onMounted(async () => {
+  await getLogistics('1203');
+});
+</script>
+
+<template>
+  <div class="logistics-details-container">
+    <div v-if="displayData.receiptType === '0'">
+      <!-- 通道/单号 -->
+      <div class="header-line" v-if="logisticsInfo.trackingNumber">
+        <div class="header-left">
+          <span class="tracking-no">{{ logisticsInfo.trackingNumber }}</span>
+          <a @click="handleCopyTracking" class="copy-link">复制</a>
+        </div>
+        <!-- <div class="header-right">联系电话:9554</div> -->
+      </div>
+
+      <!-- 物流线 -->
+      <div class="timeline-box" v-if="timelineData.length > 0">
+        <div class="timeline-row" v-for="(item, index) in visibleTimeline" :key="index">
+          <div class="dot-col">
+            <span class="dot" :class="{ 'dot-active': index === 0 }"></span>
+            <div class="line" v-if="index !== visibleTimeline.length - 1 || canExpandLogistics"></div>
+          </div>
+          <div class="content-col">
+            <div class="status-title" :class="{ 'text-active': index === 0 }">
+              <span class="title-text">{{ item.title }}</span>
+              <span class="time-text">{{ item.time }}</span>
+            </div>
+            <div class="status-detail" v-if="item.detail">{{ item.detail }}</div>
+          </div>
+        </div>
+
+        <div class="expand-row" v-if="canExpandLogistics">
+          <div class="dot-col">
+            <span class="dot"></span>
+          </div>
+          <div class="content-col">
+            <a @click="toggleLogisticsExpand" class="expand-link">
+              {{ showAllLogistics ? '收起更多物流明细' : '展开更多物流明细' }}
+              <UpOutlined v-if="showAllLogistics" class="arrow-icon" />
+              <DownOutlined v-else class="arrow-icon" />
+            </a>
+          </div>
+        </div>
+      </div>
+      <div v-else class="empty-box">暂无物流轨迹</div>
+
+      <!-- 收货人行 -->
+      <div class="recipient-line" v-if="logisticsInfo.recipientName || logisticsInfo.recipientPhone || logisticsInfo.recipientAddress">
+        <span class="receive-badge">收</span>
+        <span class="recipient-text"> {{ logisticsInfo.recipientName }} {{ logisticsInfo.recipientPhone }} {{ logisticsInfo.recipientAddress }} </span>
+      </div>
+
+      <!-- 包裹商品 -->
+      <div class="package-items-wrapper" v-if="packageItems.length > 0">
+        <div class="package-header">
+          <StarOutlined class="star-icon" />
+          <span class="title">包裹内商品</span>
+        </div>
+        <div class="package-list">
+          <div class="package-item" v-for="(item, index) in packageItems" :key="index">
+            <div class="item-thumb">
+              <img v-if="item.conditioningProgramPhoto" :src="item.conditioningProgramPhoto" />
+              <div v-else class="img-placeholder">📦</div>
+            </div>
+            <div class="item-main">
+              <div class="item-name">{{ item.conditioningProgramName }}</div>
+              <div class="item-spec">{{ item.convertDose }} {{ item.convertUnit }}</div>
+            </div>
+            <div class="item-side">
+              <div class="item-price">¥{{ item.unitPrice }}</div>
+              <div class="item-qty">x{{ item.totalMeasure }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 线下 -->
+    <div v-else-if="displayData.receiptType === '1'">
+      <div class="offline-box">线下取货</div>
+    </div>
+
+    <div v-else class="empty-box">暂无物流数据</div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.logistics-details-container {
+  padding: 8px 10px;
+  background: #fff;
+  color: #333;
+  font-size: 12px;
+}
+
+.header-line {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+
+  .header-left {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    .tracking-no {
+      font-weight: 600;
+      color: #333;
+    }
+
+    .copy-link {
+      color: #666;
+      font-size: 13px;
+      cursor: pointer;
+      text-decoration: none;
+
+      &:hover {
+        color: #333;
+        text-decoration: underline;
+      }
+    }
+  }
+
+  .header-right {
+    color: #999;
+    font-size: 13px;
+  }
+}
+
+.timeline-box {
+  margin-bottom: 16px;
+}
+
+.timeline-row {
+  display: flex;
+  gap: 12px;
+}
+
+.dot-col {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 16px;
+
+  .dot {
+    width: 8px;
+    height: 8px;
+    background: #e8e8e8;
+    border-radius: 50%;
+    margin-top: 6px;
+    flex-shrink: 0;
+  }
+
+  .dot-active {
+    background: #ff6a00;
+    box-shadow: 0 0 0 4px rgba(255, 106, 0, 0.1);
+  }
+
+  .line {
+    width: 1px;
+    background: #e8e8e8;
+    flex: 1;
+    min-height: 40px;
+  }
+}
+
+.content-col {
+  flex: 1;
+  padding-bottom: 14px;
+
+  .status-title {
+    margin-bottom: 4px;
+    font-size: 13px;
+    display: flex;
+    gap: 8px;
+    color: #666;
+    align-items: center;
+
+    &.text-active {
+      color: #ff6a00;
+      font-weight: 500;
+    }
+
+    .time-text {
+      font-size: 12px;
+    }
+    .title-text {
+      font-size: 15px;
+      font-weight: 500;
+      align-items: center;
+    }
+  }
+
+  .status-detail {
+    font-size: 13px;
+    color: #999;
+    line-height: 1.5;
+  }
+}
+
+.expand-row {
+  display: flex;
+  gap: 12px;
+  margin-top: -10px;
+
+  .expand-link {
+    color: #009966;
+    font-size: 12px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+
+    &:hover {
+      color: #006633;
+    }
+
+    .arrow-icon {
+      font-size: 10px;
+    }
+  }
+}
+
+.recipient-line {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  margin-bottom: 24px;
+
+  .receive-badge {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 20px;
+    height: 20px;
+    background: #ff6a00;
+    color: #fff;
+    border-radius: 50%;
+    font-size: 12px;
+    font-weight: 500;
+    flex-shrink: 0;
+  }
+
+  .recipient-text {
+    color: #333;
+    font-weight: 500;
+    font-size: 14px;
+    flex: 1;
+  }
+}
+
+.package-items-wrapper {
+  border: 1px solid #e8e8e8;
+  border-radius: 2px;
+  padding: 10px;
+
+  .package-header {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    // margin-bottom: 16px;
+
+    .star-icon {
+      color: #ff6a00;
+      font-size: 18px;
+    }
+
+    .title {
+      font-weight: bold;
+      font-size: 15px;
+    }
+  }
+}
+
+.package-item {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 12px 0;
+
+  &:not(:last-child) {
+    border-bottom: 1px solid #f5f5f5;
+  }
+
+  .item-thumb {
+    width: 60px;
+    height: 60px;
+    background: #f5f5f5;
+    border-radius: 4px;
+    overflow: hidden;
+    flex-shrink: 0;
+
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+
+    .img-placeholder {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      height: 100%;
+      font-size: 24px;
+    }
+  }
+
+  .item-main {
+    // flex: 1;
+    width: 20%;
+
+    .item-name {
+      font-weight: 500;
+      font-size: 15px;
+      margin-bottom: 6px;
+    }
+
+    .item-spec {
+      color: #999;
+      font-size: 13px;
+    }
+  }
+
+  .item-side {
+    text-align: right;
+
+    .item-price {
+      font-size: 15px;
+      margin-bottom: 4px;
+    }
+
+    .item-qty {
+      color: #999;
+      font-size: 14px;
+    }
+  }
+}
+
+.offline-box,
+.empty-box {
+  padding: 40px;
+  text-align: center;
+  color: #999;
+}
+</style>

+ 295 - 0
src/order/Negotiations.vue

@@ -0,0 +1,295 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { Avatar, Image } from 'ant-design-vue';
+import type { AfterSaleNegotiateModel } from '@/model/order.model';
+import { getAftersaleLogMethod } from '@/request/api/order.api';
+
+const props = defineProps<{
+  data?: any;
+}>();
+
+// 协商历史条目类型
+interface NegotiationItem {
+  name: string;
+  time: string;
+  action: string;
+  title?: string;
+  content?: string;
+  fields?: { label: string; value: string }[];
+  images?: string[];
+}
+
+
+// aftersaleContent 字段中文映射
+const fieldLabelMap: Record<string, string> = {
+  applyAmount: '退款金额',
+  reason: '退款原因',
+  receiptStatus: '收货状态',
+  remark: '退款说明',
+  type: '退款类型',
+  finishRatio: '完成比例',
+};
+
+// 需要过滤掉的字段(ID类字段)
+const hiddenFields = new Set([
+  'patientConditioningProgramId',
+  'patientConditioningRecordId',
+  'id',
+  'orderNo',
+]);
+
+// 特殊值的中文映射
+const valueMap: Record<string, Record<string, string>> = {
+  receiptStatus: { '0': '待发货', '1': '已发货', '2': '已收货' },
+  type: { '0': '退款', '1': '退货退款' },
+};
+
+const logList = ref<AfterSaleNegotiateModel[]>([]);
+const loading = ref(false);
+
+onMounted(async () => {
+  if (props.data?.id) {
+    loading.value = true;
+    try {
+      const res = await getAftersaleLogMethod(props.data.id);
+      logList.value = (res as any) || [];
+    } finally {
+      loading.value = false;
+    }
+  }
+});
+
+// 将接口数据转换为展示格式
+const displayHistory = ref<NegotiationItem[]>([]);
+
+// 监听 logList 变化后转换
+function transformData() {
+  displayHistory.value = logList.value.map(item => {
+    const result: NegotiationItem = {
+      name: item.createBy || '',
+      time: item.createTime || '',
+      action: item.title || '',
+      title: item.title || undefined,
+      content: item.content || undefined,
+    };
+    // 解析 aftersaleContent 中的字段和图片
+    let contentObj = item.aftersaleContent;
+    if (typeof contentObj === 'string') {
+      try { contentObj = JSON.parse(contentObj); } catch { contentObj = null; }
+    }
+    if (contentObj && typeof contentObj === 'object') {
+      const fields: { label: string; value: string }[] = [];
+      const images: string[] = [];
+      Object.entries(contentObj).forEach(([key, val]) => {
+        if (key === 'images' || key === 'voucherImgs') {
+          if (Array.isArray(val)) {
+            images.push(...val);
+          }
+        } else if (hiddenFields.has(key)) {
+          // 过滤掉ID类字段
+          return;
+        } else if (fieldLabelMap[key] && val !== undefined && val !== null && val !== '') {
+          const label = fieldLabelMap[key];
+          let displayVal = String(val);
+          // 特殊值转换
+          if (valueMap[key] && valueMap[key][String(val)]) {
+            displayVal = valueMap[key][String(val)];
+          }
+          // 金额字段加前缀
+          if (key === 'applyAmount') {
+            displayVal = `¥${displayVal}`;
+          }
+          fields.push({ label, value: displayVal });
+        }
+      });
+      if (fields.length) {
+        // 确保收货状态显示在退款金额上方
+        const receiptIdx = fields.findIndex(f => f.label === '收货状态');
+        const amountIdx = fields.findIndex(f => f.label === '退款金额');
+        if (receiptIdx > -1 && amountIdx > -1 && receiptIdx > amountIdx) {
+          [fields[receiptIdx], fields[amountIdx]] = [fields[amountIdx], fields[receiptIdx]];
+        }
+        result.fields = fields;
+      }
+      if (images.length) result.images = images;
+    }
+    return result;
+  });
+}
+
+// 数据加载后转换
+import { watch } from 'vue';
+watch(logList, () => transformData(), { immediate: true });
+</script>
+
+<template>
+  <div class="negotiations-container">
+    <div class="negotiation-list">
+      <div v-for="(item, index) in displayHistory" :key="index" class="negotiation-item">
+        <!-- 头部信息: 头像、名称、时间 -->
+        <div class="item-header">
+          <Avatar :size="40" class="user-avatar">{{ item.name?.charAt(0) }}</Avatar>
+          <div class="header-info">
+            <span class="user-name">{{ item.name }}</span>
+            <span class="action-time">{{ item.time }}</span>
+          </div>
+        </div>
+
+        <!-- 内容区域 -->
+        <div class="item-content">
+          <!-- 简单动作标题 -->
+          <div class="action-title" v-if="item.action">{{ item.action }}</div>
+
+          <!-- 平台拒绝等详情(content 为 JSON 字符串时不显示) -->
+          <div class="detail-box" v-if="item.content && !item.content.startsWith('{')">
+            <div class="detail-row">{{ item.content }}</div>
+          </div>
+
+          <!-- 发起申请详情 -->
+          <div class="fields-list" v-if="item.fields">
+            <div v-for="(field, fIdx) in item.fields" :key="fIdx" class="field-item">
+              <span class="field-label">{{ field.label }}:</span>
+              <span class="field-value">{{ field.value }}</span>
+            </div>
+          </div>
+
+          <!-- 图片凭证:点击任意一张,预览里可轮播 -->
+          <div class="image-gallery" v-if="item.images && item.images.length > 0">
+            <a-image-preview-group>
+              <div
+                v-for="(img, iIdx) in item.images"
+                :key="iIdx"
+                class="image-wrapper"
+              >
+                <a-image
+                  :src="img"
+                  :width="64"
+                  :height="64"
+                  :preview="true"
+                  style="object-fit: cover; border-radius: 4px"
+                />
+              </div>
+            </a-image-preview-group>
+          </div>
+        </div>
+
+        <!-- 分割线 (最后一条不显示) -->
+        <div class="item-divider" v-if="index !== displayHistory.length - 1"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.negotiations-container {
+  background: #fff;
+  padding: 20px;
+  min-height: 400px;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
+}
+
+.negotiation-list {
+  display: flex;
+  flex-direction: column;
+}
+
+.negotiation-item {
+  padding-bottom: 24px;
+}
+
+.item-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 16px;
+
+  .header-info {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    gap: 10px;
+  }
+
+  .user-name {
+    font-weight: 600;
+    font-size: 15px;
+    color: #333;
+  }
+
+  .action-time {
+    font-size: 13px;
+    color: #999;
+  }
+}
+
+.item-content {
+  padding-left: 52px; // 40px avatar + 12px gap
+
+  .action-title {
+    font-weight: bold;
+    font-size: 15px;
+    color: #333;
+    margin-bottom: 12px;
+  }
+
+  .detail-box {
+    margin-bottom: 12px;
+    .detail-row {
+      font-size: 14px;
+      line-height: 1.8;
+      color: #333;
+    }
+  }
+
+  .fields-list {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    margin-bottom: 16px;
+
+    .field-item {
+      display: flex;
+      font-size: 14px;
+      line-height: 1.6;
+
+      .field-label {
+        color: #333;
+        width: 70px;
+        flex-shrink: 0;
+      }
+
+      .field-value {
+        color: #333;
+      }
+    }
+  }
+
+  .image-gallery {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .image-wrapper {
+      border: 1px solid #f0f0f0;
+      border-radius: 4px;
+      overflow: hidden;
+      background: #fafafa;
+      width: 64px;
+      height: 64px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-right: 8px;
+      margin-bottom: 8px;
+    }
+  }
+}
+
+.item-divider {
+  height: 1px;
+  background-color: #f0f0f0;
+  margin-top: 24px;
+  margin-left: -20px;
+  margin-right: -20px;
+}
+</style>

+ 454 - 0
src/order/Review.vue

@@ -0,0 +1,454 @@
+<script setup lang="ts">
+import { ref, computed, h, onMounted } from 'vue';
+import { Avatar, Image, Radio, Button, Input, message } from 'ant-design-vue';
+import { VxeUI } from 'vxe-pc-ui';
+import Negotiations from './Negotiations.vue';
+import LogisticsDetails from './LogisticsDetails.vue';
+import type { AfterSaleModel } from '@/model/order.model';
+import { getAfterSaleDetailMethod, getAftersaleReviewMethod } from '@/request/api/order.api';
+
+const props = defineProps<{
+  data?: any;
+}>();
+
+const emit = defineEmits<{
+  'submit': [status: string, reason: string];
+  'close': [];
+}>();
+
+// 售后详情数据
+const detailData = ref<AfterSaleModel>();
+const loading = ref(false);
+
+// 获取售后详情
+onMounted(async () => {
+  if (props.data?.id) {
+    loading.value = true;
+    try {
+      const res = await getAfterSaleDetailMethod(props.data.id);
+      detailData.value = res as any;
+    } finally {
+      loading.value = false;
+    }
+  }
+});
+
+// 是否收起协商历史
+const isNegotiationCollapsed = ref(false);
+
+// 审核决策: 'agree' | 'reject'
+const reviewStatus = ref('agree');
+// 理由
+const reviewReason = ref('');
+const reasonValidated = ref(false);
+
+// 商品信息
+const productInfo = computed(() => {
+  const d = detailData.value;
+  return {
+    name: d?.conditioningProgramName || '',
+    price: d?.unitPrice || 0,
+    qty: d?.totalMeasure || 0,
+    totalPrice: d?.totalPrice || 0,
+    specs: d?.convertDose ? `${d.convertDose}${d.convertUnit || ''}` : '',
+    imageUrl: d?.conditioningProgramPhoto || '',
+  };
+});
+
+// 处理提交
+const submitting = ref(false);
+async function handleConfirm() {
+  if (submitting.value) return;
+  if (!detailData.value?.id) return;
+  if (reviewStatus.value === 'reject' && !reviewReason.value.trim()) {
+    reasonValidated.value = true;
+    return;
+  }
+  submitting.value = true;
+  try {
+    await getAftersaleReviewMethod({
+      id: detailData.value.id,
+      isAgree: reviewStatus.value === 'agree',
+      reason: reviewStatus.value === 'reject' ? reviewReason.value : '',
+    });
+    message.success('审核成功');
+    emit('submit', reviewStatus.value, reviewReason.value);
+  } finally {
+    submitting.value = false;
+  }
+}
+
+// 查看物流
+function viewLogistics() {
+  VxeUI.modal.open({
+    title: '物流详情',
+    height: 900,
+    width: 1000,
+    escClosable: true,
+    destroyOnClose: true,
+    id: 'logistics-modal',
+    slots: {
+      default() {
+        return h(LogisticsDetails, {
+          data: detailData.value
+        });
+      }
+    }
+  });
+}
+
+// 处理取消
+function handleCancel() {
+  emit('close');
+}
+</script>
+
+<template>
+  <div class="review-page-container" v-if="!loading">
+    <!-- 商品信息头部 -->
+    <div class="product-info-section">
+      <div class="product-card">
+        <Image v-if="productInfo.imageUrl" :src="productInfo.imageUrl" :width="70" :height="70" class="product-image" />
+        <div class="product-details">
+          <div class="detail-row header-row">
+            <span class="product-name">{{ productInfo.name }}</span>
+            <span class="product-price">¥{{ productInfo.price }}</span>
+          </div>
+          <div class="detail-row sub-row">
+            <span class="product-specs">{{ productInfo.specs }}</span>
+            <span class="product-qty">x{{ productInfo.qty }}</span>
+          </div>
+          <div class="product-total">
+            <span>商品总价:</span>
+            <span class="price-val">¥{{ productInfo.totalPrice }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="content-divider"></div>
+
+    <!-- 中部内容分栏 -->
+    <div class="middle-columns">
+      <!-- 左侧:申请信息 -->
+      <div class="refund-info-column">
+        <div class="info-list">
+          <div class="info-item">
+            <span class="label">申请时间:</span>
+            <span class="value">{{ detailData?.applyTime }}</span>
+          </div>
+          <div class="info-item" style="gap: 20px;">
+            <span>
+              <span class="label">商品状态:</span>
+              <span class="value">{{ detailData?.receiptStatusStr }}</span>
+            </span>
+            <span>
+              <span class="label">发货方式:</span>
+              <span class="value">{{ detailData?.receiptTypeStr }}</span>
+            </span>
+          </div>
+          <div class="info-item">
+            <span class="label">退款类型:</span>
+            <span class="value">{{ detailData?.typeStr }}</span>
+          </div>
+          <div class="info-item">
+            <span class="label">退款金额:</span>
+            <span class="value amount-value">¥{{ detailData?.applyAmount }}</span>
+          </div>
+          <div class="info-item">
+            <span class="label">退款原因:</span>
+            <span class="value">{{ detailData?.reason }}</span>
+          </div>
+          <div class="info-item">
+            <span class="label">退款说明:</span>
+            <span class="value">{{ detailData?.remark }}</span>
+          </div>
+        </div>
+
+        <!-- 凭证图片 -->
+        <div class="proof-images" v-if="detailData?.voucherImgs?.length">
+          <Image.PreviewGroup>
+            <div v-for="(img, index) in detailData.voucherImgs" :key="index" class="image-wrapper">
+              <Image :src="img" :width="40" :height="40" />
+            </div>
+          </Image.PreviewGroup>
+        </div>
+      </div>
+
+      <!-- 中间竖向分割线 -->
+      <div class="vertical-divider"></div>
+
+      <!-- 右侧:协商历史 -->
+      <div class="negotiation-column">
+        <div class="column-header">
+          <span class="title">协商历史</span>
+          <a href="javascript:;" class="collapse-toggle" @click="isNegotiationCollapsed = !isNegotiationCollapsed">
+            {{ isNegotiationCollapsed ? '展开' : '收起' }}
+          </a>
+        </div>
+        <div class="negotiation-content" v-show="!isNegotiationCollapsed">
+          <Negotiations :data="detailData" />
+        </div>
+      </div>
+    </div>
+
+    <div class="content-divider" style="margin-top: 0;"></div>
+
+    <!-- 底部:审核区 -->
+    <div class="review-action-section">
+      <div class="action-title">审核</div>
+      <div class="review-form">
+        <div class="radio-row">
+          <Radio.Group v-model:value="reviewStatus">
+            <Radio value="agree">同意</Radio>
+            <Radio value="reject">拒绝</Radio>
+          </Radio.Group>
+        </div>
+        <div class="reason-row" v-if="reviewStatus === 'reject'">
+          <div class="reason-label">拒绝理由<span style="color: #ff4d4f;"> *</span></div>
+          <Input.TextArea
+            v-model:value="reviewReason"
+            placeholder="请输入拒绝退款的理由"
+            :rows="4"
+            :status="reviewReason.trim() === '' && reasonValidated ? 'error' : undefined" />
+          <div v-if="reviewReason.trim() === '' && reasonValidated" class="reason-error">请输入拒绝理由</div>
+        </div>
+      </div>
+      <div class="action-buttons">
+        <Button @click="handleCancel">取消</Button>
+        <Button type="primary" :loading="submitting" @click="handleConfirm" class="confirm-btn">确定</Button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.review-page-container {
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+  color: #333;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
+  overflow: hidden;
+}
+
+.product-info-section {
+  padding: 16px 20px;
+
+  .product-card {
+    display: flex;
+    gap: 16px;
+
+    .product-image {
+      border: 1px solid #f0f0f0;
+      border-radius: 4px;
+    }
+
+    .product-details {
+      flex: 1;
+
+      .detail-row {
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: 4px;
+        width: 20%;
+
+        &.header-row {
+          .product-name {
+            font-size: 14px;
+            font-weight: bold;
+          }
+          .product-price {
+            font-size: 14px;
+            color: #333;
+          }
+        }
+
+        &.sub-row {
+          .product-specs {
+            color: #999;
+            font-size: 12px;
+          }
+          .product-qty {
+            color: #666;
+            font-size: 13px;
+          }
+        }
+      }
+
+      .product-total {
+        margin-top: 8px;
+        text-align: left;
+        font-size: 14px;
+        .price-val {
+          font-size: 20px;
+          font-weight: bold;
+          color: #333;
+        }
+      }
+    }
+  }
+}
+
+.content-divider {
+  height: 1px;
+  background-color: #f0f0f0;
+  margin: 0 20px;
+}
+
+.middle-columns {
+  display: flex;
+  padding: 20px;
+  min-height: 380px;
+
+  .refund-info-column {
+    flex: 1;
+    padding-right: 20px;
+
+    .info-list {
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+
+      .info-item {
+        font-size: 13px;
+        display: flex;
+        align-items: baseline;
+
+        .label {
+          color: #333;
+          width: 70px;
+          flex-shrink: 0;
+        }
+
+        .value {
+          color: #333;
+        }
+
+        &.methods-item {
+          .method-label {
+            margin-left: 20px;
+          }
+          .shipping-link {
+            color: #1890ff;
+            margin-left: 12px;
+            text-decoration: none;
+            &:hover {
+              text-decoration: underline;
+            }
+          }
+        }
+
+        .amount-value {
+          color: #ff4d4f;
+          font-size: 18px;
+          font-weight: bold;
+        }
+      }
+    }
+
+    .proof-images {
+      display: flex;
+      gap: 10px;
+      margin-top: 20px;
+
+      .image-wrapper {
+        border: 1px solid #f0f0f0;
+        border-radius: 4px;
+        overflow: hidden;
+        background: #fafafa;
+        width: 40px;
+        height: 40px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+    }
+  }
+
+  .vertical-divider {
+    width: 1px;
+    background-color: #f0f0f0;
+    margin: 0 10px;
+  }
+
+  .negotiation-column {
+    flex: 1;
+    padding-left: 20px;
+    display: flex;
+    flex-direction: column;
+
+    .column-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 12px;
+
+      .title {
+        font-weight: bold;
+        font-size: 14px;
+      }
+
+      .collapse-toggle {
+        font-size: 13px;
+        color: #1890ff;
+        cursor: pointer;
+        text-decoration: none;
+      }
+    }
+
+    .negotiation-content {
+      flex: 1;
+      overflow-y: auto;
+      max-height: 340px;
+
+      :deep(.negotiations-container) {
+        padding: 5px 0;
+      }
+    }
+  }
+}
+
+.review-action-section {
+  padding: 24px 20px;
+  border-top: 1px solid #f0f0f0;
+  background: #fff;
+
+  .action-title {
+    font-weight: bold;
+    font-size: 16px;
+    margin-bottom: 20px;
+  }
+
+  .review-form {
+    .radio-row {
+      margin-bottom: 16px;
+    }
+
+    .reason-row {
+      .reason-label {
+        font-size: 14px;
+        margin-bottom: 12px;
+        font-weight: 500;
+      }
+
+      .reason-error {
+        color: #ff4d4f;
+        font-size: 12px;
+        margin-top: 4px;
+      }
+    }
+  }
+
+  .action-buttons {
+    display: flex;
+    justify-content: flex-end;
+    gap: 12px;
+    margin-top: 24px;
+
+    .confirm-btn {
+      min-width: 80px;
+    }
+  }
+}
+</style>

+ 105 - 0
src/order/SeeAmount.vue

@@ -0,0 +1,105 @@
+<script setup lang="ts">
+import { computed } from 'vue';
+
+const props = defineProps<{
+  data?: any;
+}>();
+
+const totalPrice = computed(() => {
+  return props.data?.totalPrice || 0;
+});
+
+const applyAmount = computed(() => {
+  return props.data?.applyAmount || 0;
+});
+
+const finalAmount = computed(() => {
+  return props.data?.refundAmount || 0;
+});
+
+
+</script>
+
+<template>
+  <div class="see-amount-container">
+    <div class="amount-content">
+      <!-- 申请金额展示 -->
+      <div class="info-row apply-amount">
+        <span class="label">申请金额:</span>
+        <span class="symbol">¥</span>
+        <span class="value">{{ applyAmount.toFixed(2) }}</span>
+      </div>
+
+      <!-- 最终退款金额展示 -->
+      <div class="info-row final-amount">
+        <span class="label">最终退款金额:</span>
+        <span class="symbol">¥</span>
+        <span class="value">{{ finalAmount.toFixed(2) }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.see-amount-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 60px 20px;
+  background: #fff;
+  min-height: 240px;
+}
+
+.amount-content {
+  width: 100%;
+  max-width: 400px;
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+
+  .info-row {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 24px;
+    
+    .label {
+      font-size: 16px;
+      color: #333;
+      font-weight: bold;
+    }
+
+    .symbol {
+      font-size: 18px;
+      margin-left: 4px;
+      font-weight: bold;
+    }
+
+    &.apply-amount {
+      .value {
+        font-size: 28px;
+        font-weight: bold;
+        color: #333;
+      }
+    }
+
+    &.final-amount {
+      .symbol {
+        color: #ff4d4f;
+      }
+      .value {
+        font-size: 28px;
+        font-weight: bold;
+        color: #ff4d4f; // 红色文字
+      }
+    }
+  }
+
+  .warning-text {
+    color: #ff4d4f;
+    font-size: 14px;
+    margin-top: 4px;
+  }
+}
+</style>

+ 4 - 0
src/pages/index/care/conditioningRecord.vue

@@ -75,6 +75,10 @@ const gridOptions = reactive<VxeGridProps<ConditioningRecordListModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 17 - 2
src/pages/index/care/configured.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { ref, reactive, computed, shallowRef, onMounted, toRaw } from 'vue';
 import { type VxeFormListeners, type VxeFormProps, type VxeGridInstance, type VxeGridListeners, type VxeGridProps, VxeUI } from 'vxe-pc-ui';
 import { usePagination, useRequest } from 'alova/client';
 import { getDictionaryMethod } from '@/request/api/dictionary.api';
@@ -139,6 +140,10 @@ const gridOptions = reactive<VxeGridProps<ConditioningSchemeModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -153,6 +158,7 @@ const gridOptions = reactive<VxeGridProps<ConditioningSchemeModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'orgName', title: '组织名称' },
     { field: 'insName', title: '机构名称' },
     // {
@@ -202,7 +208,17 @@ const gridOptions = reactive<VxeGridProps<ConditioningSchemeModel>>({
 const gridEvents: VxeGridListeners = {};
 
 // 获取设备分页列表
-const { loading, page, pageSize, total, onSuccess, replace, refresh, remove, send: sendRefresh } = usePagination((page, size) => getConditioningSchemeMethod(page, size, model.value), {
+const {
+  loading,
+  page,
+  pageSize,
+  total,
+  onSuccess,
+  replace,
+  refresh,
+  remove,
+  send: sendRefresh,
+} = usePagination((page, size) => getConditioningSchemeMethod(page, size, model.value), {
   initialData: { data: [], total: 0 },
   initialPage: 1,
   initialPageSize: 100,
@@ -256,7 +272,6 @@ function editConfigured(model?: ConditioningSchemeModel, index?: number) {
         return h(EditConfigured, <any>{
           data: model,
           onSubmit(success: boolean, data?: ConditioningSchemeModel) {
-            console.log(success, 'success');
             // 只有成功时才刷新列表
             if (success) {
               // 使用 sendRefresh 强制刷新,避免页面闪烁

+ 4 - 0
src/pages/index/care/institutionService.vue

@@ -94,6 +94,10 @@ const gridOptions = reactive<VxeGridProps<SystemCwModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 14 - 25
src/pages/index/care/serviceItems.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
-import { ref, unref, shallowReactive, defineAsyncComponent } from 'vue';
+import { ref, unref, computed, defineAsyncComponent, nextTick } from 'vue';
 import { usePermission } from '@/core/usePermission';
 
-const panels = shallowReactive([
+const allPanels = [
   {
     id: 'service-items-list',
     title: '项目列表',
@@ -19,26 +19,20 @@ const panels = shallowReactive([
     component: defineAsyncComponent(() => import('@/service/ServiceItemsSystem.vue')),
     hide: usePermission('fdhb:service_items_system:list', true),
   },
-].filter(item => !unref(item.hide)));
+];
 
-const activePanel = ref(panels[0].id);
+const panels = computed(() => allPanels.filter((item) => !unref(item.hide)));
+const activePanel = ref(panels.value[0]?.id);
+const activePanelData = computed(() =>
+  panels.value.find((panel) => panel.id === activePanel.value)
+);
 const currentComponent = ref<any>(null);
 
-// 获取当前激活的组件
-const getCurrentComponent = () => {
-  return panels.find(panel => panel.id === activePanel.value);
-};
-
-// 切换面板
-function handleChange(panelId: string) {
+async function handleChange(panelId: string) {
   activePanel.value = panelId;
-  // 延迟执行,确保新组件已经渲染完成
-  setTimeout(() => {
-    if (currentComponent.value && typeof currentComponent.value.send === 'function') {
-      currentComponent.value?.send();
-    } 
-  }, 100);
-};
+  await nextTick();
+  currentComponent.value?.send?.();
+}
 </script>
 
 <template>
@@ -51,15 +45,10 @@ function handleChange(panelId: string) {
         </a-radio-button>
       </a-radio-group>
     </div>
-    
+
     <!-- 内容区域 -->
     <div class="content-wrapper">
-      <component 
-        :is="getCurrentComponent()?.component" 
-        :title="getCurrentComponent()?.title" 
-        ref="currentComponent"
-        :key="activePanel"
-      ></component>
+      <component :is="activePanelData?.component" :title="activePanelData?.title" ref="currentComponent" :key="activePanel"></component>
     </div>
   </div>
 </template>

+ 5 - 0
src/pages/index/care/supplier.vue

@@ -119,6 +119,10 @@ const gridOptions = reactive<VxeGridProps<SupplierModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -133,6 +137,7 @@ const gridOptions = reactive<VxeGridProps<SupplierModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'name', title: '供应商' },
     { field: 'detailAddress', title: '地址' },
     { field: 'kahuna', title: '负责人' },

+ 4 - 0
src/pages/index/care/systemService.vue

@@ -80,6 +80,10 @@ const gridOptions = reactive<VxeGridProps<SystemCwModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 1 - 0
src/pages/index/equipment/configured.vue

@@ -190,6 +190,7 @@ const gridOptions = reactive<VxeGridProps<DeviceManageModel>>({
   },
   columns: [
     { type: 'checkbox', width: 60, fixed: 'left', title: '', align: 'center' },
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'orgName', title: '组织名称' },
     { field: 'warrant', title: '设备ID' },
     {

+ 5 - 0
src/pages/index/equipment/registe.vue

@@ -186,6 +186,10 @@ const gridOptions = reactive<VxeGridProps<EquirementModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -201,6 +205,7 @@ const gridOptions = reactive<VxeGridProps<EquirementModel>>({
   },
   columns: [
     { type: 'checkbox', width: 100, fixed: 'left', title: '', align: 'center' },
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'deviceType', title: '设备名称' },
     { field: 'orgName', title: '组织名称' },
     { field: 'institutionName', title: '机构名称' },

+ 5 - 0
src/pages/index/equipment/reportManagement.vue

@@ -156,6 +156,10 @@ const gridOptions = reactive<any>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -170,6 +174,7 @@ const gridOptions = reactive<any>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'orgName', title: '组织名称' },
     { field: 'deviceType', title: '设备名称' },
     { field: 'deviceCode', title: '设备ID' },

+ 5 - 0
src/pages/index/follow/assessment.vue

@@ -100,6 +100,10 @@ const gridOptions = reactive<VxeGridProps<EvaluationModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -111,6 +115,7 @@ const gridOptions = reactive<VxeGridProps<EvaluationModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'followupPlanName', title: '随访计划' },
     { field: 'patientName', title: '姓名' },
     { field: 'sex', title: '性别', slots: { default: 'sex' } },

+ 4 - 0
src/pages/index/follow/plan.vue

@@ -129,6 +129,10 @@ const gridOptions = reactive<VxeGridProps<PlanModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 5 - 0
src/pages/index/follow/task.vue

@@ -114,6 +114,10 @@ const gridOptions = reactive<VxeGridProps<TaskModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -126,6 +130,7 @@ const gridOptions = reactive<VxeGridProps<TaskModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'followupTaskName', title: '随访计划' },
     { field: 'patientName', title: '姓名' },
     { field: 'sex', title: '性别', slots: { default: 'sex' } },

+ 4 - 0
src/pages/index/healthy/education.vue

@@ -80,6 +80,10 @@ const gridOptions = reactive<VxeGridProps<EducationModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 306 - 0
src/pages/index/order/afterSale.vue

@@ -0,0 +1,306 @@
+<script setup lang="ts">
+import { h, ref, reactive, shallowRef, onMounted, toRaw } from 'vue';
+import OrderDetail from '@/service/OrderDetail.vue';
+import Negotiations from '@/order/Negotiations.vue';
+import SeeAmount from '@/order/SeeAmount.vue';
+import Amount from '@/order/Amount.vue';
+import Review from '@/order/Review.vue';
+import type { AfterSaleModel, AfterSaleQuery } from '@/model/order.model';
+defineOptions({
+  name: 'AfterSalePage',
+});
+// 接口数据
+import { getAfterSaleMethod } from '@/request/api/order.api';
+import { usePagination } from 'alova/client';
+
+import { type VxeFormListeners, type VxeFormProps, VxeUI } from 'vxe-pc-ui';
+import type { VxeGridInstance, VxeGridListeners, VxeGridProps } from 'vxe-table';
+
+const progressOptions = [
+  { label: '退款申请中', value: '0' },
+  { label: '撤销退款', value: '1' },
+  { label: '退款已拒绝', value: '2' },
+  { label: '退款已同意', value: '3' },
+  { label: '退款完成', value: '4' },
+];
+
+const model = shallowRef<AfterSaleQuery>();
+const searchFormProps = reactive<VxeFormProps<AfterSaleQuery>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: {},
+  items: [
+    {
+      field: 'orderNo',
+      title: '订单编号',
+      span: 4,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'liaison',
+      title: '收货人',
+      span: 4,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'progress',
+      title: '退款状态',
+      span: 4,
+      itemRender: {
+        name: 'VxeSelect',
+        options: progressOptions,
+        props: { placeholder: '请选择', clearable: true },
+      },
+    },
+    {
+      span: 8,
+      itemRender: {
+        name: 'VxeButtonGroup',
+        options: [
+          { name: 'submits', type: 'submit', content: '查询', status: 'primary' },
+          { name: 'reset', type: 'reset', content: '重置' },
+        ],
+        events: {},
+      },
+    },
+  ],
+});
+const searchFormEmits: VxeFormListeners<AfterSaleQuery> = {
+  submit({ data }) {
+    model.value = { ...data } as any;
+  },
+  reset({ data }) {
+    model.value = { ...data } as any;
+  },
+};
+
+const gridRef = ref<VxeGridInstance<AfterSaleModel>>();
+const gridOptions = reactive<VxeGridProps<AfterSaleModel>>({
+  id: 'after-sale-list',
+  border: true,
+  showOverflow: true,
+  height: 'auto',
+  autoResize: false,
+  syncResize: true,
+  scrollY: { enabled: false },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
+  toolbarConfig: {
+    custom: true,
+    zoom: true,
+    slots: {
+      tools: 'toolbar-extra',
+    },
+  },
+  columnConfig: {
+    resizable: true,
+  },
+  customConfig: {
+    storage: true,
+  },
+  columns: [
+    { type: 'seq', width: 70, fixed: 'left' },
+    { field: 'orderNo', title: '订单编号' },
+    { field: 'typeStr', title: '售后类型' },
+    { field: 'updateTime', title: '退款更新时间' },
+    { field: 'conditioningProgramName', title: '项目名称' },
+    { field: 'sellTypeStr', title: '商品类型' },
+    { field: 'totalPrice', title: '总价(元)' },
+    { field: 'liaison', title: '收货人' },
+    { field: 'progressStr', title: '退款状态' },
+    { field: 'expressStatus', title: '物流状态', slots: { default: 'expressStatusCell' } },
+    { field: 'finishRatio', title: '核销情况', slots: { default: 'finishRatioCell' } },
+    { field: 'refundAmount', title: '退款金额' },
+    { field: 'reason', title: '退款原因' },
+    { field: 'remark', title: '退款说明', slots: { default: 'refundReasonCell' } },
+    {
+      field: 'action',
+      title: '操作',
+      align: 'center',
+      width: 200,
+      showOverflow: false,
+      slots: { default: 'action' },
+    },
+  ],
+  data: [],
+});
+const gridEvents: VxeGridListeners = {};
+
+const { loading, page, pageSize, total, onSuccess, refresh } = usePagination((page, size) => getAfterSaleMethod(page, size, model.value), {
+  initialData: { data: [], total: 0 },
+  initialPage: 1,
+  initialPageSize: 100,
+  watchingStates: [model],
+  immediate: false,
+});
+onSuccess(({ data: { data } }) => {
+  gridRef.value?.loadData(data);
+});
+
+onMounted(() => {
+  model.value = toRaw(searchFormProps.data);
+});
+
+// 点击订单编号查看订单详情
+function serviceDetail(row?: AfterSaleModel) {
+  if (!row) return;
+  const types = 'record';
+  VxeUI.modal.open({
+    id: 'servicePackageDetail-modal',
+    title: '调养记录详情',
+    height: window.innerHeight,
+    width: window.innerWidth,
+    fullscreen: true,
+    escClosable: true,
+    destroyOnClose: true,
+    slots: {
+      default() {
+        return h(OrderDetail, {
+          data: {
+            ...row,
+            id: row.patientConditioningProgramId,
+            types,
+          },
+          onVoidSubmit() {
+            refresh(page.value);
+          },
+        } as any);
+      },
+    },
+  });
+}
+
+async function goShipment(row?: AfterSaleModel) {
+  if (!row) return;
+  const isReview = row.progress === '0'; // 退款申请中 → 审核
+  const isEditAmount = row.progress === '3'; // 退款已同意 → 金额
+  if (isReview) {
+    VxeUI.modal.open({
+      title: '审核',
+      height: 900,
+      width: 1200,
+      escClosable: true,
+      destroyOnClose: true,
+      id: `review-modal`,
+      remember: true,
+      storage: true,
+      slots: {
+        default() {
+          return h(Review, {
+            data: row,
+            onSubmit: () => {
+              refresh(page.value);
+              VxeUI.modal.close('review-modal');
+            },
+            onClose: () => {
+              VxeUI.modal.close('review-modal');
+            },
+          });
+        },
+      },
+    });
+  } else {
+    // 金额 or 查看金额
+    const title = isEditAmount ? '退款金额' : '退款金额查看';
+    const component = isEditAmount ? Amount : SeeAmount;
+    VxeUI.modal.open({
+      title,
+      height: 500,
+      width: 600,
+      escClosable: true,
+      destroyOnClose: true,
+      id: isEditAmount ? 'amount-edit-modal' : 'amount-see-modal',
+      slots: {
+        default() {
+          return h(component, {
+            data: row,
+            onSubmit: (amount: number) => {
+              refresh(page.value);
+              VxeUI.modal.close('amount-edit-modal');
+            },
+            onClose: () => {
+              VxeUI.modal.close(isEditAmount ? 'amount-edit-modal' : 'amount-see-modal');
+            },
+          });
+        },
+      },
+    });
+  }
+}
+
+function goAfterSale(row?: AfterSaleModel) {
+  if (!row) return;
+  VxeUI.modal.open({
+    title: '协商历史',
+    height: 800,
+    width: 1000,
+    escClosable: true,
+    destroyOnClose: true,
+    id: 'negotiations-modal',
+    slots: {
+      default() {
+        return h(Negotiations, {
+          data: row,
+        });
+      },
+    },
+  });
+}
+</script>
+<template>
+  <div class="page-container flex flex-col">
+    <header class="flex-none mt-4">
+      <vxe-form v-bind="searchFormProps" v-on="searchFormEmits"></vxe-form>
+    </header>
+    <main class="flex-auto overflow-hidden">
+      <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents" :loading="loading">
+        <template #refundReasonCell="{ row }">
+          <span class="order-no-link" @click="goAfterSale(row)">{{ row.remark }}</span>
+        </template>
+        <template #expressStatusCell="{ row }">
+          {{ row.expressStatus || '/' }}
+        </template>
+        <template #finishRatioCell="{ row }">
+          {{ row.sellType === '2' ? row.finishRatio || '/' : '/' }}
+        </template>
+        <template #action="{ row }">
+          <vxe-button mode="text" status="primary" @click="goAfterSale(row)"> 协商历史 </vxe-button>
+          <vxe-button v-if="row.progress === '0'" mode="text" status="primary" @click="goShipment(row)"> 审核 </vxe-button>
+          <vxe-button v-if="row.progress === '3'" mode="text" status="primary" @click="goShipment(row)"> 金额 </vxe-button>
+          <vxe-button v-if="row.progress === '4'" mode="text" status="primary" @click="goShipment(row)"> 查看金额 </vxe-button>
+        </template>
+        <template #toolbar-extra>
+          <vxe-button style="margin-right: 12px" icon="vxe-icon-repeat" circle @click="refresh(page)"></vxe-button>
+        </template>
+      </vxe-grid>
+    </main>
+    <footer class="flex-none">
+      <vxe-pager
+        v-model:current-page="page"
+        v-model:page-size="pageSize"
+        :total="total"
+        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']"
+      />
+    </footer>
+  </div>
+</template>
+<style scoped lang="scss">
+.page-container {
+  padding: 0 24px;
+  max-height: var(--page-main-container);
+}
+
+.order-no-link {
+  color: #1890ff;
+  cursor: pointer;
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: underline;
+    color: #40a9ff;
+  }
+}
+</style>

+ 4 - 0
src/pages/index/order/management.vue

@@ -130,6 +130,10 @@ const gridOptions = reactive<VxeGridProps<ConditioningRecordListModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/order/revenueSharing.vue

@@ -122,6 +122,10 @@ const gridOptions = reactive<VxeGridProps<RevenueSharingDetailModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 6 - 1
src/pages/index/order/shipment.vue

@@ -180,6 +180,10 @@ const gridOptions = reactive<VxeGridProps<ShipmentModel>>({
 
     return { rowspan: 1, colspan: 1 };
   },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -350,7 +354,8 @@ function disabledPayEndDate(current: any) {
           <div>{{ row.receiptType === '0' ? '配送' : row.receiptType === '1' ? '线下取货' : '' }}</div>
         </template>
         <template #action="{ row }">
-          <vxe-button mode="text" status="primary" @click="goShipment(row)">
+          <vxe-button mode="text" status="primary" @click="goShipment(row)"
+            :disabled="row.isShowButton === false && row.receiptStatus === '1'">
             {{ row.receiptStatus === '1' ? '修改' : row.receiptStatus === '0' ? '去发货' : '' }}
           </vxe-button>
         </template>

+ 4 - 0
src/pages/index/patient/history.vue

@@ -169,6 +169,10 @@ const gridOptions = reactive<VxeGridProps<PatientReportModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 54 - 30
src/pages/index/system/institution.vue

@@ -1,30 +1,45 @@
 <script setup lang="ts">
 import InstitutionEdit from '@/components/InstitutionEdit.vue';
-import UserPassword from '@/components/UserPassword.vue';
-import UserPreview from '@/components/UserPreview.vue';
-import UserQRCode from '@/components/UserQRCode.vue';
 import { type InstitutionModel, type InstitutionQuery } from '@/model/system.model';
 
-import { branchMethod, institutionMethod, editInstitutionMethod, deleteInstitutionMethod } from '@/request/api/system.api';
+import { branchMethod, institutionMethod, deleteInstitutionMethod } from '@/request/api/system.api';
 import { usePagination, useRequest } from 'alova/client';
 import { notification } from 'ant-design-vue';
-
+import UserQRCode from '@/components/UserQRCode.vue';
 import { VxeButton, type VxeFormListeners, type VxeFormProps, type VxeGridInstance, type VxeGridListeners, type VxeGridProps, VxeUI } from 'vxe-pc-ui';
 
-const { data: branch, loading: branchLoading } = useRequest(branchMethod);
-const organizationOptions = ref<{ label: string; value: string }[]>([
-  { label: 'liuzhi', value: '1' },
-  { label: 'alice', value: '2' },
-]);
+const { data: branch, loading: branchLoading } = useRequest(branchMethod(0, 1, 1));
 const model = shallowRef<InstitutionQuery>();
+function findDeptLabelById(id: any, nodes?: any[]): string | undefined {
+  if (!id || !Array.isArray(nodes)) return;
+  const target = String(id);
+  for (const n of nodes) {
+    if (n?.id !== undefined && String(n.id) === target) return n?.label;
+    const hit = findDeptLabelById(id, n?.children);
+    if (hit) return hit;
+  }
+}
 const searchFormProps = reactive<VxeFormProps<InstitutionQuery>>({
   titleWidth: 100,
   titleAlign: 'right',
   titleColon: true,
   data: {},
   items: [
-    { field: 'userName', title: '组织名称', span: 8, itemRender: { name: 'VxeSelect', props: { options: organizationOptions, optionProps: { value: 'value', label: 'label' } } } },
-    { field: 'nickName', title: '机构名称', span: 8, itemRender: { name: 'VxeInput' } },
+    {
+      field: 'orgName',
+      title: '组织名称',
+      span: 8,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+      },
+    },
+    { field: 'name', title: '机构名称', span: 8, itemRender: { name: 'VxeInput' } },
     {
       span: 8,
       itemRender: {
@@ -39,13 +54,15 @@ const searchFormProps = reactive<VxeFormProps<InstitutionQuery>>({
 });
 const searchFormEmits: VxeFormListeners<InstitutionQuery> = {
   submit({ data }) {
-    model.value = { ...data };
+    const orgName = findDeptLabelById((data as any)?.orgName, branch.value as any) ?? (data as any)?.orgName;
+    model.value = { ...(data as any), orgName };
   },
   reset({ data }) {
     model.value = { ...data };
   },
 };
 
+const gridEvents: VxeGridListeners = {};
 const gridRef = ref<VxeGridInstance<InstitutionModel>>();
 const gridOptions = reactive<VxeGridProps<InstitutionModel>>({
   id: 'user-list',
@@ -55,6 +72,10 @@ const gridOptions = reactive<VxeGridProps<InstitutionModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -92,7 +113,7 @@ const gridOptions = reactive<VxeGridProps<InstitutionModel>>({
           { content: '小程序码', status: 'primary', name: 'QRCode' },
         ],
         events: {
-          click({ row, rowIndex }, { name }) {
+          click({ row, rowIndex }: any, { name }: any) {
             let method;
             if (name === 'editInstitution') {
               method = editInstitution;
@@ -153,19 +174,23 @@ function editInstitution(model?: InstitutionModel, index?: number) {
     storage: true,
     slots: {
       default() {
-        return h(InstitutionEdit, <any>{
-          data: model,
-          onSubmit(data?: InstitutionModel) {
-            refresh(page.value);
-            VxeUI.modal.close(`institution-edit-modal`);
-          },
-        });
+        return h(
+          InstitutionEdit,
+          {
+            data: model,
+            onSubmit(data?: InstitutionModel) {
+              refresh(page.value);
+              VxeUI.modal.close(`institution-edit-modal`);
+            },
+          } as any
+        );
       },
     },
   });
 }
 
 function QRCode(model: InstitutionModel) {
+  console.log(model,111);
   const { name } = model;
   VxeUI.modal.open({
     title: `${name} 专属小程序码`,
@@ -178,9 +203,12 @@ function QRCode(model: InstitutionModel) {
     width: 256 + 12 * 2,
     slots: {
       default() {
-        return h(UserQRCode, <any>{
-          dataset: model,
-        });
+        return h(
+          UserQRCode,
+          {
+            dataset: model,
+          } as any
+        );
       },
     },
   });
@@ -202,12 +230,8 @@ function QRCode(model: InstitutionModel) {
       </vxe-grid>
     </main>
     <footer class="flex-none">
-      <vxe-pager
-        v-model:current-page="page"
-        v-model:page-size="pageSize"
-        :total="total"
-        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']"
-      />
+      <vxe-pager v-model:current-page="page" v-model:page-size="pageSize" :total="total"
+        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']" />
     </footer>
   </div>
 </template>

+ 52 - 29
src/pages/index/system/organization.vue

@@ -1,17 +1,23 @@
 <script setup lang="ts">
 import OrganizationManagement from '@/components/OrganizationManagement.vue';
 import type { OrganizationModel, OrganizationQuery } from '@/model/system.model';
-
 import { organizationMethod, deleteOrganizationMethod } from '@/request/api/system.api';
-import { usePagination } from 'alova/client';
+import { usePagination, useRequest } from 'alova/client';
 import { notification } from 'ant-design-vue';
 import PatientBelong from '@/components/PatientBelong.vue';
 import { type VxeFormListeners, type VxeFormProps, type VxeGridInstance, type VxeGridListeners, type VxeGridProps, VxeUI } from 'vxe-pc-ui';
-const organizationOptions = ref<{ label: string; value: string }[]>([
-  { label: 'liuzhi', value: '1' },
-  { label: 'alice', value: '2' },
-]);
+import { branchMethod } from '@/request/api/system.api';
 const model = shallowRef<OrganizationQuery>();
+const { data: branch, loading: branchLoading } = useRequest(branchMethod(0, 1, 1));
+function findDeptLabelById(id: any, nodes?: any[]): string | undefined {
+  if (!id || !Array.isArray(nodes)) return;
+  const target = String(id);
+  for (const n of nodes) {
+    if (n?.id !== undefined && String(n.id) === target) return n?.label;
+    const hit = findDeptLabelById(id, n?.children);
+    if (hit) return hit;
+  }
+}
 const searchFormProps = reactive<VxeFormProps<OrganizationQuery>>({
   titleWidth: 100,
   titleAlign: 'right',
@@ -20,8 +26,18 @@ const searchFormProps = reactive<VxeFormProps<OrganizationQuery>>({
     name: '',
   },
   items: [
-    { field: 'name', title: '组织名称', span: 8, itemRender: { name: 'VxeSelect', props: { options: organizationOptions, optionProps: { value: 'value', label: 'label' } } } },
-
+    {
+      field: 'name', title: '组织名称', span: 8,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+      },
+    },
     {
       span: 16,
       itemRender: {
@@ -39,7 +55,8 @@ const searchFormProps = reactive<VxeFormProps<OrganizationQuery>>({
 });
 const searchFormEmits: VxeFormListeners<OrganizationQuery> = {
   submit({ data }) {
-    model.value = { ...data };
+    const name = findDeptLabelById((data as any)?.name, branch.value as any) ?? (data as any)?.name;
+    model.value = { ...(data as any), name };
   },
   reset({ data }) {
     model.value = { ...data };
@@ -55,6 +72,10 @@ const gridOptions = reactive<VxeGridProps<OrganizationModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -160,13 +181,16 @@ function editOrganization(model?: OrganizationModel, index?: number) {
     height: 600,
     slots: {
       default() {
-        return h(OrganizationManagement, <any>{
-          data: model,
-          onSubmit(data: OrganizationModel) {
-            refresh(page.value);
-            VxeUI.modal.close(`organization-edit-modal`);
-          },
-        });
+        return h(
+          OrganizationManagement,
+          {
+            data: model,
+            onSubmit(data: OrganizationModel) {
+              refresh(page.value);
+              VxeUI.modal.close(`organization-edit-modal`);
+            },
+          } as any
+        );
       },
     },
   });
@@ -184,13 +208,16 @@ function setPatientBelong(model: OrganizationModel, index: number) {
     height: 600,
     slots: {
       default() {
-        return h(PatientBelong, <any>{
-          data: model,
-          onSubmit(data: OrganizationModel) {
-            refresh(page.value);
-            VxeUI.modal.close(`patient-belong-modal`);
-          },
-        });
+        return h(
+          PatientBelong,
+          {
+            data: model,
+            onSubmit(data: OrganizationModel) {
+              refresh(page.value);
+              VxeUI.modal.close(`patient-belong-modal`);
+            },
+          } as any
+        );
       },
     },
   });
@@ -212,12 +239,8 @@ function setPatientBelong(model: OrganizationModel, index: number) {
       </vxe-grid>
     </main>
     <footer class="flex-none">
-      <vxe-pager
-        v-model:current-page="page"
-        v-model:page-size="pageSize"
-        :total="total"
-        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']"
-      />
+      <vxe-pager v-model:current-page="page" v-model:page-size="pageSize" :total="total"
+        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']" />
     </footer>
   </div>
 </template>

+ 4 - 0
src/pages/index/system/role.vue

@@ -59,6 +59,10 @@ const gridOptions = reactive<VxeGridProps<RoleModel>>({
   showOverflow: true,
   height: 'auto', autoResize: false, syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/system/tag.vue

@@ -92,6 +92,10 @@ const gridOptions = reactive<VxeGridProps<TagModel>>({
   showOverflow: true,
   height: 'auto', autoResize: false, syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/system/user.vue

@@ -66,6 +66,10 @@ const gridOptions = reactive<VxeGridProps<UserModel>>({
   showOverflow: true,
   height: 'auto', autoResize: false, syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 2 - 6
src/request/api/account.api.ts

@@ -71,12 +71,8 @@ export function getMenusMethod(account: AccountModel) {
     transform(data) {
       // data[5].children.push(
       //   {
-      //     path: 'shipment',
-      //     meta: { title: '发货' },
-      //   },
-      //   {
-      //     path: 'revenueSharing',
-      //     meta: { title: '分账' },
+      //     path: 'afterSale',
+      //     meta: { title: '售后' },
       //   },
       // );
       //   console.log(data, 'push之后的data', transformMenus(data));

+ 0 - 1
src/request/api/care.api.ts

@@ -291,7 +291,6 @@ export function addConditioningSchemeMethod(data: Partial<OpenConditioningScheme
 
 // 根据调理包id获取调理包详情
 export function getConditioningRecordDetailMethod(data: Partial<SystemCwModel>) {
-  console.log('data===types', data.types,"data数据",data);
   if (data.types === 'institution' || data.types === 'system') {
     // 机构调理包详情
     return request.Post(`/fdhb-pc/conditioningManage/wrap/getCwDetailById/${data.id}`, {

+ 99 - 2
src/request/api/order.api.ts

@@ -1,5 +1,11 @@
 import type { List, Tree } from '@/model';
-import type { OrderQuery, OrderModel, OrderLiaisonListModel, OrderLiaisonListQuery, ShipmentModel, ShipmentQuery, PieOrderCountModel,RevenueSharingDetailModel,RevenueSharingDetailQuery } from '@/model/order.model';
+import type {
+  OrderQuery,
+  OrderModel, OrderLiaisonListModel, ShipmentModel, ShipmentQuery, 
+  PieOrderCountModel, RevenueSharingDetailModel, RevenueSharingDetailQuery,
+   EvaluateDetailModel, ApplyRecordModel,LogisticsModel,
+   AfterSaleModel,AfterSaleNegotiateModel,AfterSaleAuditModel
+} from '@/model/order.model';
 import request from '@/request/alova';
 
 // 线下服务  今日指派订单分页列表
@@ -81,7 +87,8 @@ export function getOrderLiaisonListMethod(data: Partial<OrderModel>) {
       collaborateDeptId: data.institutionId,
       conditioningProgramTypes: [data.conditioningProgramType],
       timeStart: data?.timeStart ?? '',
-      type: data?.type ?? ''
+      type: data?.type ?? '',
+      orderByType:data.orderByType
     },
     {
       hitSource: /order$/, // 匹配失效源
@@ -163,4 +170,94 @@ export function getRevenueSharingDetailListMethod(page: number, size: number, qu
       hitSource: /revenueSharing$/, // 匹配失效源
     }
   );
+}
+
+
+// 获取订单商品-线下核销记录的患者个人评价   type 	1-商品 2-线下核销记录
+export function getEvaluateDetailMethod(type: string, id: string) {
+  return request.Post<EvaluateDetailModel>(
+    `/fdhb-pc/patientCrManage/getEvaluate/${type}/${id}`,
+    {},
+    {
+      hitSource: /order$/, // 匹配失效源
+    }
+  );
+}
+
+// 获取线下服务的预约派单记录 	线下服务id
+export function getApplyRecordMethod(id: string) {
+  return request.Post<ApplyRecordModel[]>(
+    `/fdhb-pc/patientCrManage/getApplyRecord/${id}`,
+    {},
+    {
+      hitSource: /order$/, // 匹配失效源
+    }
+  );
 } 
+
+// 获取物流信息 id患者调理方案ID
+export function getLogisticsMethod(id: string) {
+  return request.Post<LogisticsModel[]>(
+    `/fdhb-pc/patientCrManage/getLogistics/${id}`,
+    {},
+    {
+      hitSource: /order$/, // 匹配失效源
+    }
+  );
+} 
+
+// 获取售后分页列表
+export function getAfterSaleMethod(page: number, size: number, query?: AfterSaleQuery) {
+  return request.Post<List<AfterSaleModel>>(
+    '/fdhb-pc/patientCrManage/pageAftersale',
+    query ?? {},
+    {
+      hitSource: /order$/, // 匹配失效源
+      params: { pageNum: page, pageSize: size },
+    }
+  );
+}
+
+// 售后审核
+export function getAftersaleReviewMethod(data: AfterSaleAuditModel) {
+  return request.Post(
+    '/fdhb-pc/patientCrManage/aftersaleReview',
+    data,
+    {
+      hitSource: /order$/,
+    }
+  );
+}
+
+// 售后确定金额
+export function getConfirmAmountMethod(id: number, amount: number) {
+  return request.Post(
+    `/fdhb-pc/patientCrManage/confirmAmount/${id}/${amount}`,
+    {},
+    {
+      hitSource: /order$/,
+    }
+  );
+}
+
+// 售后详情 id患者调理方案售后ID
+export function getAfterSaleDetailMethod(id: number) {
+  return request.Post<AfterSaleModel>(
+    `/fdhb-pc/patientCrManage/detailAftersale/${id}`,
+    {},
+    {
+      hitSource: /order$/,
+    }
+  );
+}
+
+// 售后协商历史 id患者调理方案售后ID
+export function getAftersaleLogMethod(id: number) {
+  return request.Post<AfterSaleNegotiateModel[]>(
+    `/fdhb-pc/patientCrManage/getAftersaleLog/${id}`,
+    {},
+    {
+      hitSource: /order$/,
+    }
+  );
+}

+ 1 - 0
src/router/index.ts

@@ -89,6 +89,7 @@ const router = createRouter({
             { path: 'dispatchOrder', component: () => import(`@/pages/index/order/dispatchOrder.vue`) },
             { path: 'shipment', component: () => import(`@/pages/index/order/shipment.vue`) },
             { path: 'revenueSharing', component: () => import(`@/pages/index/order/revenueSharing.vue`) },
+            { path: 'afterSale', component: () => import(`@/pages/index/order/afterSale.vue`) },
           ],
         },
       ],

+ 4 - 0
src/satisfaction/SendRecord.vue

@@ -87,6 +87,10 @@ const gridOptions = reactive<VxeGridProps<SatisfactionSendRecordModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/satisfaction/SurveyList.vue

@@ -89,6 +89,10 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 33 - 5
src/service/AddItems.vue

@@ -184,11 +184,16 @@ const isDeliveryRequired = computed(() => {
   return checkedList.value.includes('1') || form.sellType === '1';
 });
 
-// 线下服务:itemsList 和 system 类型都需要服务所需时间”
+// 线下服务:itemsList 和 system 类型都需要服务所需时间”
 const showServiceRequiredTime = computed(() => {
   return form.sellType === '2';
 });
 
+// 线下服务+一口价:固定计价单位、相当于、使用单位、建议频率单位
+const isOfflineServiceFixedPrice = computed(() => {
+  return form.sellType === '2' && form.pricingType === '0';
+});
+
 // 获取所有的供应商
 async function getSupplier(params: any) {
   supplierOptions.value = [];
@@ -768,6 +773,29 @@ watch(
   }
 );
 
+// 线下服务+一口价:自动填充固定值;切换到其他情况时清空
+watch(
+  isOfflineServiceFixedPrice,
+  (val) => {
+    if (val) {
+      if (!form.cpFixedPricingRule) {
+        form.cpFixedPricingRule = { unitPrice: '', pricingUnit: '', convertDose: '', convertUnit: '' };
+      }
+      form.cpFixedPricingRule.pricingUnit = '次';
+      form.cpFixedPricingRule.convertDose = '1';
+      form.cpFixedPricingRule.convertUnit = '次';
+      suggestedFrequencyUnit.value = '次';
+    } else {
+      if (form.cpFixedPricingRule) {
+        form.cpFixedPricingRule.pricingUnit = '';
+        form.cpFixedPricingRule.convertDose = '';
+        form.cpFixedPricingRule.convertUnit = '';
+      }
+      suggestedFrequencyUnit.value = '';
+    }
+  }
+);
+
 // 监听一口价单位变化,同步到建议频率单位
 watch(
   () => form.cpFixedPricingRule?.convertUnit,
@@ -946,13 +974,13 @@ function handleDerivation() {
         <span style="margin: 0 8px">元</span>
 
         <span class="label" style="margin-left: 32px">计价单位:</span>
-        <a-input v-model:value="form.cpFixedPricingRule.pricingUnit" placeholder="请输入" style="width: 100px" />
+        <a-input v-model:value="form.cpFixedPricingRule.pricingUnit" placeholder="请输入" style="width: 100px" :disabled="isOfflineServiceFixedPrice" />
 
         <span style="margin-left: 32px">相当于</span>
         <a-input v-model:value="form.cpFixedPricingRule.convertDose" placeholder="请输入"
-          style="width: 100px; margin-left: 8px" />
+          style="width: 100px; margin-left: 8px" :disabled="isOfflineServiceFixedPrice" />
         <vxe-select v-model="form.cpFixedPricingRule.convertUnit" :options="unitOptions" placeholder="请选择" clearable
-          transfer style="width: 100px; margin-left: 8px" />
+          transfer style="width: 100px; margin-left: 8px" :disabled="isOfflineServiceFixedPrice" />
 
         <span style="color: #aaa; margin-left: 8px">(使用单位)</span>
       </div>
@@ -992,7 +1020,7 @@ function handleDerivation() {
           <span>天</span>
           <a-input v-model:value="form.frequencyMeasure" placeholder="请输入" style="width: 100px; margin: 0 8px" />
           <vxe-select v-model="suggestedFrequencyUnit" :options="unitOptions" placeholder="请选择"
-            :clearable="form.pricingType !== '0'" transfer style="width: 100px; margin-left: 8px" />
+            :clearable="form.pricingType !== '0'" transfer style="width: 100px; margin-left: 8px" :disabled="isOfflineServiceFixedPrice" />
         </div>
       </a-form-item>
       <a-form-item label="服务所需时间:"

+ 4 - 0
src/service/NotifyManageList.vue

@@ -86,6 +86,10 @@ const gridOptions = reactive<VxeGridProps<NotifyModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/service/NotifyManageRecord.vue

@@ -87,6 +87,10 @@ const gridOptions = reactive<VxeGridProps<NotifyRecordModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 123 - 0
src/service/ReviewMediaPreview.vue

@@ -0,0 +1,123 @@
+<script setup lang="ts">
+import { ref } from 'vue';
+// @ts-expect-error swiper 模块导出与类型声明解析
+import { Navigation, Pagination } from 'swiper';
+import { Swiper, SwiperSlide } from 'swiper/vue';
+import 'swiper/css';
+import 'swiper/css/navigation';
+import 'swiper/css/pagination';
+
+export type MediaItem = { type: 'image' | 'video'; url: string };
+
+defineProps<{
+  mediaList: MediaItem[];
+  initialIndex: number;
+}>();
+
+const carouselRef = ref<HTMLElement | null>(null);
+const swiperModules = [Navigation, Pagination];
+
+function onSlideChange() {
+  carouselRef.value?.querySelectorAll('video').forEach((v) => v.pause());
+}
+</script>
+
+<template>
+  <div ref="carouselRef" class="review-preview-carousel">
+    <swiper
+      v-if="mediaList?.length"
+      :modules="swiperModules"
+      :initial-slide="initialIndex"
+      :slides-per-view="1"
+      :space-between="0"
+      navigation
+      pagination
+      class="review-preview-swiper"
+      @slide-change="onSlideChange"
+    >
+      <swiper-slide v-for="(media, i) in mediaList" :key="i" class="review-preview-slide">
+        <div v-if="media.type === 'image'" class="review-preview-inner">
+          <img :src="media.url" alt="" class="review-preview-img" />
+        </div>
+        <div v-else class="review-preview-inner review-preview-video-wrap">
+          <video :src="media.url" controls class="review-preview-video" />
+        </div>
+      </swiper-slide>
+    </swiper>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.review-preview-carousel {
+  width: 100%;
+  height: 70vh;
+  min-height: 300px;
+
+  .review-preview-swiper {
+    width: 100%;
+    height: 100%;
+    --swiper-navigation-size: 36px;
+    --swiper-theme-color: #1890ff;
+  }
+
+  .review-preview-slide {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background: #000;
+    box-sizing: border-box;
+  }
+
+  .review-preview-inner {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    box-sizing: border-box;
+  }
+
+  .review-preview-img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+    display: block;
+  }
+
+  .review-preview-video-wrap {
+    width: 100%;
+    height: 100%;
+  }
+
+  .review-preview-video {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+    display: block;
+  }
+
+  :deep(.swiper-wrapper) {
+    height: 100%;
+  }
+
+  :deep(.swiper-slide) {
+    height: 100%;
+  }
+
+  :deep(.swiper-button-prev),
+  :deep(.swiper-button-next) {
+    color: #fff;
+  }
+
+  :deep(.swiper-pagination-bullet) {
+    background: #fff;
+    opacity: 0.5;
+  }
+
+  :deep(.swiper-pagination-bullet-active) {
+    opacity: 1;
+  }
+}
+</style>

+ 42 - 27
src/service/ServiceItemsConfirm.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { onMounted, toRaw, shallowRef, reactive } from 'vue';
 import ConfirmItems from './ConfirmItems.vue';
 import type { SystemItemModel, SystemIteQuery } from '@/model/care.model';
 
@@ -66,6 +67,10 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -81,10 +86,11 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'name', title: '项目名称' },
     { field: 'conditioningProgramType', title: '方案类型' },
     { field: 'cpFixedPricingRule.unitPrice', title: '单价(元)', slots: { default: 'unitPriceCell' } },
-    { field: 'cpFixedPricingRule.pricingUnit', title: '计价单位',slots: { default: 'pricingUnitCell' } },
+    { field: 'cpFixedPricingRule.pricingUnit', title: '计价单位', slots: { default: 'pricingUnitCell' } },
     { field: 'cpFixedPricingRule.convertDose', title: '计价说明', slots: { default: 'convertDoseCell' } },
     { field: 'conditioningProgramSupplierName', title: '供应商' },
     {
@@ -120,7 +126,16 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
 });
 const gridEvents: VxeGridListeners = {};
 
-const { loading, page, pageSize, total, onSuccess, refresh, remove,send:sendRefresh } = usePagination((page, size) => pageOrgConfirmMethod(page, size, model.value), {
+const {
+  loading,
+  page,
+  pageSize,
+  total,
+  onSuccess,
+  refresh,
+  remove,
+  send: sendRefresh,
+} = usePagination((page, size) => pageOrgConfirmMethod(page, size, model.value), {
   initialData: { data: [], total: 0 },
   initialPage: 1,
   initialPageSize: 100,
@@ -171,7 +186,7 @@ function sureItem(model?: SystemItemModel, index?: number) {
       slots: {
         default() {
           return h(HealthEvaluation, <any>{
-            data: {...model,addType},
+            data: { ...model, addType },
             onChange: (data: SystemItemModel) => {
               // 确认成功之后刷新页面
               refresh(page.value);
@@ -182,34 +197,34 @@ function sureItem(model?: SystemItemModel, index?: number) {
     });
   } else {
     VxeUI.modal.open({
-    title: `确认项目`,
-    height: 700,
-    width: 1000,
-    position: {
-      top: Math.min(100, window.innerHeight * 0.1),
-    },
-    escClosable: true,
-    destroyOnClose: true,
-    id: `confirm-item-modal`,
-    remember: true,
-    storage: true,
-    slots: {
-      default() {
-        return h(ConfirmItems, <any>{
-          data: model,
-          onSubmit(data: SystemItemModel) {
-            refresh(page.value);
-            VxeUI.modal.close(`confirm-item-modal`);
-          },
-        });
+      title: `确认项目`,
+      height: 700,
+      width: 1000,
+      position: {
+        top: Math.min(100, window.innerHeight * 0.1),
       },
-    },
-  });
-}
+      escClosable: true,
+      destroyOnClose: true,
+      id: `confirm-item-modal`,
+      remember: true,
+      storage: true,
+      slots: {
+        default() {
+          return h(ConfirmItems, <any>{
+            data: model,
+            onSubmit(data: SystemItemModel) {
+              refresh(page.value);
+              VxeUI.modal.close(`confirm-item-modal`);
+            },
+          });
+        },
+      },
+    });
+  }
 }
 defineExpose({
   send: sendRefresh,
-})
+});
 </script>
 <template>
   <div class="page-container flex flex-col">

+ 5 - 0
src/service/ServiceItemsList.vue

@@ -118,6 +118,10 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -133,6 +137,7 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'name', title: '项目名称' },
     { field: 'conditioningProgramType', title: '方案类型' },
     { field: 'isForWrapCell', title: '项目应用', slots: { default: 'isForWrapCell' } },

+ 6 - 1
src/service/ServiceItemsSystem.vue

@@ -105,6 +105,10 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -121,6 +125,7 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   },
   columns: [
     { type: 'checkbox', width: 100, fixed: 'left', title: '批量' },
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'name', title: '项目名称' },
     { field: 'conditioningProgramType', title: '方案类型' },
     { field: 'cpFixedPricingRule.unitPrice', title: '单价(元)', slots: { default: 'unitPriceCell' }, width: 150 },
@@ -285,7 +290,7 @@ const onCheckboxAll = (params: any) => {
 };
 defineExpose({
   send: sendRefresh,
-})
+});
 </script>
 <template>
   <div class="page-container flex flex-col">

+ 12 - 1
src/service/ServicePackageDetail.vue

@@ -286,7 +286,18 @@ watch(
         </vxe-column>
         <vxe-column field="totalPrice" title="总价(元)" align="center" />
         <vxe-column field="conditioningProgramDetail.medicineNames" title="组成" align="center" />
-        <vxe-column field="acuPointNames" title="穴位/经络/部位" align="center" />
+        <vxe-column field="acuPointNames" title="穴位/经络/部位" align="center" >
+          <template #default="{ row }">
+            {{
+              (() => {
+                const acuPointNames = Array.isArray(row?.acuPointNames) ? row.acuPointNames : [];
+                const acuMeridianNames = Array.isArray(row?.acuMeridianNames) ? row.acuMeridianNames : [];
+                const combined = acuPointNames.concat(acuMeridianNames).join('、');
+                return combined === '' ? '-' : combined;
+              })()
+            }}
+          </template>
+        </vxe-column>
         <vxe-column field="remark" title="说明" align="center" />
       </vxe-table>
     </div>

+ 551 - 30
src/service/SingleItemDetail.vue

@@ -1,10 +1,35 @@
 <script setup lang="ts">
 import type { SystemCwModel } from '@/model/care.model';
-import { computed } from 'vue';
+import type { ApplyRecordModel } from '@/model/order.model';
+import { DownOutlined, PlayCircleOutlined, UpOutlined } from '@ant-design/icons-vue';
+import { computed, h, onMounted, ref } from 'vue';
 import { notification } from 'ant-design-vue';
+import VxeUI from 'vxe-table';
+import ReviewMediaPreview from '@/service/ReviewMediaPreview.vue';
+import type { MediaItem } from '@/service/ReviewMediaPreview.vue';
+import seeEvaluate from '@/service/seeEvaluate.vue';
+import { getEvaluateDetailMethod, getApplyRecordMethod, getLogisticsMethod } from '@/request/api/order.api';
+
 const props = defineProps<{
   data: SystemCwModel['items'][number];
 }>();
+// 服务记录--查看评价
+function openSeeEvaluate(row: any) {
+  VxeUI.modal.open({
+    title: '用户评价',
+    width: 900,
+    height: 600,
+    escClosable: true,
+    destroyOnClose: true,
+    slots: {
+      default() {
+        return h(seeEvaluate, {
+          data: row,
+        });
+      },
+    },
+  });
+}
 // 复制物流信息
 function handleCopyTracking() {
   const trackingNumber = mockLogisticsData.value.trackingNumber;
@@ -22,28 +47,41 @@ const mockSplitAccountList = computed(() => {
 });
 // 物流信息数据
 const mockLogisticsData = computed(() => {
-  console.log('props.data===', props.data);
   return {
-    trackingNumber: `${expressTypeText[props.data.expressType || '']} ${props.data.expressNo || ''}`,
+    expressType: props.data.expressType || '',
+    trackingNumber: `${props.data.expressNo || ''}`,
     recipientName: props.data.liaison ? props.data.liaison + ', ' : '',
     recipientPhone: props.data.phone ? props.data.phone + ', ' : '',
     recipientAddress: (props.data.provinceName !== null ? props.data.provinceName + ' ' : '') + (props.data.cityName !== null ? props.data.cityName + ' ' : '') + (props.data.areaName !== null ? props.data.areaName + ' ' : '') + (props.data.detailAddress !== null ? props.data.detailAddress : '')
   };
 });
-const expressTypeText: Record<string, string> = {
-  '0': '邮政速递',
-  '1': '顺丰速运',
-  '2': '京东快递',
-  '3': '中通快递',
-  '4': '圆通速递',
-  '5': '申通快递',
-  '6': '韵达快递',
-  '7': '极兔速递',
+type LogisticsTimelineItem = {
+  status: string;
+  time: string;
+  context: string;
 };
+
+const showAllLogistics = ref(false);
+let mockLogisticsTimeline = ref<string[]>([]);
+
+const logisticsTimeline = computed<LogisticsTimelineItem[]>(() => {
+  return mockLogisticsTimeline.value
+});
+
+const visibleLogisticsTimeline = computed(() => {
+  if (showAllLogistics.value) return logisticsTimeline.value;
+  return logisticsTimeline.value.slice(0, 2);
+});
+
+const canExpandLogistics = computed(() => logisticsTimeline.value.length > 2);
+
+function toggleLogisticsExpand() {
+  showAllLogistics.value = !showAllLogistics.value;
+}
+
 // 包裹数据
 const mockPackageItems = computed(() => {
-  console.log('props.data===', props.data);
-
+  console.log(props.data as any,"props.data");
   return (props.data as any).sameExpress ?? []
 });
 // 商品状态映射 - 收货状态(实体商品使用)
@@ -64,6 +102,107 @@ const sellTypeText: Record<string, string> = {
   '2': '线下服务',
   '3': '线上权益',
 };
+
+function toScore(value: unknown) {
+  const n = Number(value);
+  return Number.isFinite(n) ? n : 0;
+}
+
+function normalizeMediaList(input: unknown): MediaItem[] {
+  if (!Array.isArray(input)) return [];
+  return input
+    .map((it) => {
+      if (typeof it === 'string') {
+        const url = it;
+        const type: MediaItem['type'] = /\.(mp4|webm|ogg)(\?|#|$)/i.test(url) ? 'video' : 'image';
+        return { type, url } as MediaItem;
+      }
+      if (it && typeof it === 'object' && 'url' in it) {
+        const anyIt = it as any;
+        const url = String(anyIt.url ?? '');
+        if (!url) return null;
+        const type: MediaItem['type'] =
+          anyIt.type === 'video' || anyIt.type === 'image'
+            ? anyIt.type
+            : /\.(mp4|webm|ogg)(\?|#|$)/i.test(url)
+              ? 'video'
+              : 'image';
+        return { type, url } as MediaItem;
+      }
+      return null;
+    })
+    .filter((x): x is MediaItem => Boolean(x));
+}
+
+// 用户评价(与 seeEvaluate.vue 逻辑一致)
+const scroeType = ref([
+  { name: '描述相符', score: 0 },
+]);
+
+const evaluateDetail = ref({
+  depict: '',
+  mediaList: [] as MediaItem[],
+});
+
+const hasEvaluate = computed(() => {
+  const depict = (evaluateDetail.value.depict ?? '').trim();
+  const hasMedia = (evaluateDetail.value.mediaList?.length ?? 0) > 0;
+  const hasScore = scroeType.value.some((i) => toScore(i.score) > 0);
+  return Boolean(depict) || hasMedia || hasScore;
+});
+
+async function getEvaluateDetail() {
+  const id = (props.data as any)?.id;
+  if (!id) return;
+  const res = await getEvaluateDetailMethod('1', id);
+  if (!res) return;
+  if (scroeType.value[0]) scroeType.value[0].score = toScore(res.complianceScore);
+  evaluateDetail.value.depict = res.depict ?? '';
+  evaluateDetail.value.mediaList = normalizeMediaList(res.imageVideos);
+}
+const applyRecordList = ref<ApplyRecordModel[]>([]);
+// 获取预约
+async function getApplyRecord() {
+  const id = (props.data as any)?.id;
+  if (!id) return;
+  const res = await getApplyRecordMethod(id);
+  if (!res) return;
+  applyRecordList.value = res ?? [];
+}
+// 获取物流信息
+async function getLogistics() {
+  const id = (props.data as any)?.id;
+  if (!id) return;
+  const res = await getLogisticsMethod(id);
+  if (!res) return;
+  mockLogisticsTimeline.value = res?.tracks
+}
+const REVIEW_PREVIEW_MODAL_ID = 'review-media-preview-modal';
+//预览图片/视频
+function openPreview(list: MediaItem[], index: number) {
+  if (!list?.length) return;
+  VxeUI.modal.open({
+    id: REVIEW_PREVIEW_MODAL_ID,
+    title: '预览',
+    width: 720,
+    escClosable: true,
+    destroyOnClose: true,
+    slots: {
+      default() {
+        return h(ReviewMediaPreview, {
+          mediaList: list,
+          initialIndex: index,
+        });
+      },
+    },
+  });
+}
+
+onMounted(async () => {
+  await getEvaluateDetail();
+  await getApplyRecord();
+  await getLogistics()
+});
 </script>
 
 <template>
@@ -130,16 +269,13 @@ const sellTypeText: Record<string, string> = {
     <!-- 分账信息 -->
     <div class="info-section">
       <h3 class="info-title">分账信息</h3>
-      <vxe-table
-        class="split-account-table"
-        :data="mockSplitAccountList"
-        border
-      >
+      <vxe-table class="split-account-table" :data="mockSplitAccountList" border>
         <vxe-column field="profitSharingTime" title="分账时间" align="center" />
         <vxe-column field="conditioningProgramSupplierName" title="供应商" align="center" />
-        <vxe-column field="profitSharingStatus" title="分账状态" align="center" >
+        <vxe-column field="profitSharingStatus" title="分账状态" align="center">
           <template #default="{ row }">
-            {{ row.profitSharingStatus === '1' ? '未分账' : row.profitSharingStatus === '2' ? '已分账' : row.profitSharingStatus === '3' ? '分账异常' : '' }}
+            {{ row.profitSharingStatus === '1' ? '未分账' : row.profitSharingStatus === '2' ? '已分账' :
+              row.profitSharingStatus === '3' ? '分账异常' : '' }}
           </template>
         </vxe-column>
         <vxe-column field="profitSharing" title="分账比例" align="center">
@@ -147,7 +283,7 @@ const sellTypeText: Record<string, string> = {
             {{ row.profitSharing || '-' }}%
           </template>
         </vxe-column>
-        <vxe-column field="profitSharingAmount" title="预计分账金额" align="center" >
+        <vxe-column field="profitSharingAmount" title="预计分账金额" align="center">
           <template #default="{ row }">
             {{ row.profitSharingAmount ? row.profitSharingAmount + '元' : '' }}
           </template>
@@ -164,14 +300,55 @@ const sellTypeText: Record<string, string> = {
       <!-- receiptType	收货方式 0-快递 1-线下取货 -->
       <!-- <h3 class="info-title">{{ data.receiptType === '0' ? '物流信息' : data.receiptType === '1' ? '线下取货' : '' }}</h3> -->
       <h3 class="info-title">物流信息</h3>
-    
+
       <div class="info-content-wrapper" v-if="data.receiptType">
         <!-- 物流信息 -->
         <div class="logistics-content" v-if="data.receiptType === '0'">
           <template v-if="mockPackageItems.length > 0">
-            <div class="logistics-tracking">
-              <span class="tracking-number">{{ mockLogisticsData.trackingNumber }}</span>
-              <a @click="handleCopyTracking" class="copy-link">复制</a>
+            <div class="flex align-items-center justify-between">
+              <div class="logistics-tracking">
+                <span class="tracking-number">{{ mockLogisticsData.expressType }}</span>
+                <span class="tracking-number">{{ mockLogisticsData.trackingNumber }}</span>
+                <a @click="handleCopyTracking" class="copy-link">复制</a>
+              </div>
+              <!-- <div class="text-gray-500">联系电话:9554</div> -->
+            </div>
+            <!-- 物流详情 -->
+            <div class="logistics-detail">
+              <template v-if="logisticsTimeline.length > 0">
+                <div class="logistics-timeline">
+                  <div class="timeline-item" v-for="(item, index) in visibleLogisticsTimeline"
+                    :key="`${item.time}-${index}`">
+                    <div class="timeline-dot-wrap">
+                      <span class="timeline-dot" :class="{ active: index === 0 }" />
+                      <span class="timeline-line"
+                        v-if="index !== visibleLogisticsTimeline.length - 1 || canExpandLogistics" />
+                    </div>
+                    <div class="timeline-content">
+                      <div class="timeline-head">
+                        <span class="timeline-title" :class="{ active: index === 0 }">{{ item.status || '-' }}</span>
+                        <span class="timeline-time" :class="{ active: index === 0 }">{{ item.time || '-' }}</span>
+                      </div>
+                      <div class="timeline-detail" v-if="item.context">{{ item.context }}</div>
+                    </div>
+                  </div>
+                  <div class="timeline-item timeline-expand-item" v-if="canExpandLogistics">
+                    <div class="timeline-dot-wrap">
+                      <span class="timeline-dot" />
+                    </div>
+                    <div class="timeline-content timeline-expand-content">
+                      <a @click="toggleLogisticsExpand" class="expand-link">
+                        {{ showAllLogistics ? '收起物流明细' : '展开更多物流明细' }}
+                        <UpOutlined v-if="showAllLogistics" class="expand-arrow" />
+                        <DownOutlined v-else class="expand-arrow" />
+                      </a>
+                    </div>
+                  </div>
+                </div>
+              </template>
+              <template v-else>
+                <div class="logistics-empty">暂无物流轨迹</div>
+              </template>
             </div>
             <div class="logistics-recipient">
               <span class="receive-icon">收</span>
@@ -196,7 +373,7 @@ const sellTypeText: Record<string, string> = {
             包裹内商品
           </h3>
           <div class="package-items">
-          
+
             <div v-for="(item, index) in mockPackageItems" :key="index" class="package-item">
               <div class="package-item-image" v-if="item.conditioningProgramPhoto">
                 <a-image v-if="item.conditioningProgramPhoto" :width="60" :height="60" style="border-radius: 4px;"
@@ -221,7 +398,29 @@ const sellTypeText: Record<string, string> = {
         暂无
       </div>
     </div>
-
+    <!-- 预约派单记录 线下服务才显示  1-实体商品 2-线下服务 3-线上权益-->
+    <div class="info-section" v-if="data?.sellType === '2'">
+      <h3 class="info-title">
+        预约派单记录
+      </h3>
+      <vxe-table :data="applyRecordList" border>
+        <vxe-column type="seq" title="序号" width="60" align="center" />
+        <vxe-column field="applyTime" title="预约时间" align="center" />
+        <vxe-column field="updateTime" title="最新修改预约时间" align="center" />
+        <vxe-column field="cancelTime" title="取消预约时间" align="center" />
+        <vxe-column field="arrangeTime" title="预约服务日期" align="center" />
+        <vxe-column field="arrangePeriod" title="预约服务时间段" align="center" />
+        <vxe-column field="pieTime" title="是否派单" align="center">
+          <template #default="{ row }">
+            {{ row.pieTime ? '是' : '否' }}
+          </template>
+        </vxe-column>
+        <vxe-column field="pieBy" title="派单员" align="center" />
+        <vxe-column field="pieTime" title="派单时间" align="center" />
+        <vxe-column field="conditioningProgramSupplierName" title="服务机构" align="center" />
+        <vxe-column field="status" title="服务状态" align="center" />
+      </vxe-table>
+    </div>
     <!-- 服务记录 线下服务才显示  1-实体商品 2-线下服务 3-线上权益-->
     <div class="info-section" v-if="data?.sellType === '2'">
       <h3 class="info-title">
@@ -233,7 +432,7 @@ const sellTypeText: Record<string, string> = {
         <vxe-column type="seq" title="序号" width="60" align="center" />
         <vxe-column field="arrangeDate" title="服务日期" align="center" />
         <vxe-column field="arrangePeriod" title="服务时间段" align="center" />
-        <vxe-column field="operateTime" title="服务状态" align="center">
+        <vxe-column field="verifyStatus" title="服务状态" align="center">
           <template #default="{ row }">
             {{ row.operateTime ? '已核销' : '已预约' }}
           </template>
@@ -257,8 +456,55 @@ const sellTypeText: Record<string, string> = {
         </vxe-column>
         <vxe-column field="operateBy" title="操作人" align="center" />
         <vxe-column field="feedback" title="治疗备注" align="center" />
+        <vxe-column field="acuPointNames" title="用户评价" align="center">
+          <template #default="{ row }">
+            <vxe-button @click="openSeeEvaluate(row)" content="查看" status="primary" />
+          </template>
+        </vxe-column>
       </vxe-table>
     </div>
+    <!-- 用户评价 -->
+    <div class="info-section">
+      <div class="info-title-row">
+        <h3 class="info-title">用户评价</h3>
+      </div>
+      <div class="info-content-wrapper">
+        <template v-if="hasEvaluate">
+          <div class="review-item">
+            <div class="review-top-row">
+              <div class="review-rating-box">
+                <div class="review-rating-row" v-for="scroe in scroeType" :key="scroe.name">
+                  <span class="review-criterion">{{ scroe.name }}</span>
+                  <a-rate :value="scroe.score" disabled allow-half class="review-stars" />
+                  <span class="review-score">{{ scroe.score }}分</span>
+                </div>
+              </div>
+              <div class="review-content-block">
+                <div class="review-content-line">
+                  <span class="review-content-label">评价内容:</span>
+                  <span class="review-content-text">{{ evaluateDetail.depict }}</span>
+                </div>
+                <div v-if="evaluateDetail.mediaList?.length" class="review-media-row">
+                  <div v-for="(media, i) in evaluateDetail.mediaList" :key="i" class="review-media-thumb"
+                    :class="{ 'is-video': media.type === 'video' }" role="button" tabindex="0"
+                    @click.stop.prevent="openPreview(evaluateDetail.mediaList, i)"
+                    @keydown.enter.space.prevent="openPreview(evaluateDetail.mediaList, i)">
+                    <img v-if="media.type === 'image'" :src="media.url" class="review-thumb-img" alt="" />
+                    <div v-else class="review-thumb-video">
+                      <video :src="media.url" muted preload="metadata" />
+                      <span class="review-thumb-play">
+                        <PlayCircleOutlined />
+                      </span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </template>
+        <div v-else class="empty-text">无</div>
+      </div>
+    </div>
   </div>
 </template>
 
@@ -299,6 +545,159 @@ const sellTypeText: Record<string, string> = {
     color: #333;
   }
 
+  .info-title-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 12px;
+
+    .info-title {
+      margin-bottom: 0;
+    }
+  }
+
+  .review-item {
+    padding: 12px 0;
+    border-bottom: 1px solid #f0f0f0;
+
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+
+  .review-top-row {
+    display: flex;
+    align-items: flex-start;
+    gap: 24px;
+    margin-bottom: 12px;
+  }
+
+  .review-rating-box {
+    flex-shrink: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 12px;
+    min-width: 240px;
+  }
+
+  .review-rating-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    flex-shrink: 0;
+  }
+
+  .review-criterion {
+    font-size: 14px;
+    color: #333;
+    width: 72px;
+    text-align: left;
+    white-space: nowrap;
+  }
+
+  .review-stars {
+    font-size: 16px;
+
+    :deep(.ant-rate-star-full .ant-rate-star-second) {
+      color: #fadb14;
+    }
+
+    :deep(.ant-rate-star-full .ant-rate-star-first) {
+      color: #fadb14;
+    }
+  }
+
+  .review-score {
+    font-size: 13px;
+    color: #999;
+  }
+
+  .review-content-block {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .review-content-line {
+    display: flex;
+    align-items: flex-start;
+    gap: 6px;
+  }
+
+  .review-content-label {
+    font-size: 14px;
+    color: #333;
+    margin-right: 4px;
+    flex-shrink: 0;
+  }
+
+  .review-content-text {
+    font-size: 14px;
+    color: #333;
+    line-height: 22px;
+    margin: 0;
+    white-space: pre-wrap;
+    max-width: 560px;
+    word-break: break-word;
+  }
+
+  .review-media-row {
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+    margin-top: 12px;
+    margin-left: 70px;
+  }
+
+  .review-media-thumb {
+    width: 72px;
+    height: 72px;
+    border-radius: 6px;
+    border: 1px solid #e8e8e8;
+    overflow: hidden;
+    flex-shrink: 0;
+    cursor: pointer;
+    background: #f5f5f5;
+    transition: border-color 0.2s, box-shadow 0.2s;
+
+    &:hover {
+      border-color: #1890ff;
+      box-shadow: 0 0 0 1px #1890ff;
+    }
+
+    .review-thumb-img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      display: block;
+    }
+
+    .review-thumb-video {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      background: #2a2a2a;
+
+      video {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+        display: block;
+      }
+
+      .review-thumb-play {
+        position: absolute;
+        inset: 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: rgba(255, 255, 255, 0.9);
+        font-size: 28px;
+        pointer-events: none;
+      }
+    }
+  }
+
   .info-content-wrapper .info-title {
     margin-top: 20px;
     margin-bottom: 12px;
@@ -320,6 +719,7 @@ const sellTypeText: Record<string, string> = {
 
   /* 分账表格仅按内容自适应高度,避免一行数据时出现多余空白 */
   .split-account-table {
+
     :deep(.vxe-table--wrapper),
     :deep(.vxe-table) {
       height: auto !important;
@@ -333,6 +733,13 @@ const sellTypeText: Record<string, string> = {
     }
   }
 
+  .empty-text {
+    color: #999;
+    font-size: 14px;
+    line-height: 22px;
+    padding: 8px 0;
+  }
+
   .product-info {
     display: flex;
     align-items: flex-start;
@@ -461,6 +868,120 @@ const sellTypeText: Record<string, string> = {
     text-decoration: underline;
   }
 
+  .logistics-detail {
+    max-width: 760px;
+  }
+
+  .logistics-timeline {
+    margin-top: 2px;
+  }
+
+  .timeline-item {
+    display: flex;
+    align-items: flex-start;
+    gap: 10px;
+  }
+
+  .timeline-dot-wrap {
+    width: 14px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    flex-shrink: 0;
+    padding-top: 5px;
+  }
+
+  .timeline-dot {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    background: #d9d9d9;
+  }
+
+  .timeline-dot.active {
+    background: #ff7a45;
+  }
+
+  .timeline-line {
+    width: 1px;
+    min-height: 44px;
+    background: #e8e8e8;
+    margin-top: 4px;
+  }
+
+  .timeline-content {
+    flex: 1;
+    min-width: 0;
+    padding-bottom: 12px;
+  }
+
+  .timeline-head {
+    display: flex;
+    align-items: baseline;
+    gap: 8px;
+    line-height: 22px;
+  }
+
+  .timeline-title {
+    color: #666;
+    font-size: 15px;
+    font-weight: 500;
+  }
+
+  .timeline-title.active {
+    color: #ff7a45;
+  }
+
+  .timeline-time {
+    color: #8c8c8c;
+    font-size: 13px;
+  }
+
+  .timeline-time.active {
+    color: #ff7a45;
+  }
+
+  .timeline-detail {
+    margin-top: 2px;
+    line-height: 22px;
+    color: #8c8c8c;
+    font-size: 14px;
+    word-break: break-all;
+  }
+
+  .timeline-expand-item {
+    margin-top: -2px;
+  }
+
+  .timeline-expand-content {
+    padding-bottom: 0;
+  }
+
+  .expand-link {
+    color: #666;
+    font-size: 14px;
+    text-decoration: none;
+    cursor: pointer;
+    user-select: none;
+    display: flex;
+    align-items: center;
+  }
+
+  .expand-link:hover {
+    color: #1890ff;
+  }
+
+  .expand-arrow {
+    margin-left: 4px;
+    font-size: 10px;
+    transform: translateY(1px);
+  }
+
+  .logistics-empty {
+    color: #999;
+    line-height: 22px;
+  }
+
   .logistics-recipient {
     display: flex;
     align-items: flex-start;
@@ -476,7 +997,7 @@ const sellTypeText: Record<string, string> = {
 
   .recipient-info {
     color: #333;
-    font-weight: 400;
+    font-weight: 500;
     flex: 1;
   }
 

+ 340 - 0
src/service/seeEvaluate.vue

@@ -0,0 +1,340 @@
+<script setup lang="ts">
+import { PlayCircleOutlined } from '@ant-design/icons-vue';
+import { computed, h, onMounted, ref } from 'vue';
+import VxeUI from 'vxe-table';
+import ReviewMediaPreview from '@/service/ReviewMediaPreview.vue';
+import type { MediaItem } from '@/service/ReviewMediaPreview.vue';
+import { getEvaluateDetailMethod } from '@/request/api/order.api';
+const props = defineProps<{
+    data: any;
+}>();
+
+function toScore(value: unknown) {
+    const n = Number(value);
+    return Number.isFinite(n) ? n : 0;
+}
+
+function normalizeMediaList(input: unknown): MediaItem[] {
+    if (!Array.isArray(input)) return [];
+    return input
+        .map((it) => {
+            if (typeof it === 'string') {
+                const url = it;
+                const type: MediaItem['type'] = /\.(mp4|webm|ogg)(\?|#|$)/i.test(url) ? 'video' : 'image';
+                return { type, url } as MediaItem;
+            }
+            if (it && typeof it === 'object' && 'url' in it) {
+                const anyIt = it as any;
+                const url = String(anyIt.url ?? '');
+                if (!url) return null;
+                const type: MediaItem['type'] =
+                    anyIt.type === 'video' || anyIt.type === 'image'
+                        ? anyIt.type
+                        : /\.(mp4|webm|ogg)(\?|#|$)/i.test(url)
+                            ? 'video'
+                            : 'image';
+                return { type, url } as MediaItem;
+            }
+            return null;
+        })
+        .filter((x): x is MediaItem => Boolean(x));
+}
+const scroeType = ref([{
+    name: '服务质量',
+    score: 0,
+}, {
+    name: '服务态度',
+    score: 0,
+}, {
+    name: '环境',
+    score: 0,
+}]);
+// 用户评价假数据(淘宝式:每条评价有多张图/多个视频,先展示缩略图排列,点击可左右滑动预览)
+const evaluateDetail = ref(
+    {
+        depict: '',
+        // 图片/视频列表
+        mediaList: [] as MediaItem[],
+    });
+
+const hasEvaluate = computed(() => {
+    const depict = String(evaluateDetail.value.depict ?? '').trim();
+    const hasMedia = (evaluateDetail.value.mediaList?.length ?? 0) > 0;
+    const hasScore = scroeType.value.some((i) => toScore(i.score) > 0);
+    return Boolean(depict) || hasMedia || hasScore;
+});
+
+const REVIEW_PREVIEW_MODAL_ID = 'review-media-preview-modal';
+async function getEvaluateDetail() {
+    console.log(props.data, "传过来的参数");
+    if (props.data && props.data.id) {
+        const res = await getEvaluateDetailMethod('2', props.data.id);
+        console.log(res, "获取评价");
+        if (res) {
+            if (scroeType.value[0]) scroeType.value[0].score = toScore(res.qualityScore); //服务质量
+            if (scroeType.value[1]) scroeType.value[1].score = toScore(res.attitudeScore); //服务态度
+            if (scroeType.value[2]) scroeType.value[2].score = toScore(res.environmentScore); //环境
+            evaluateDetail.value.depict = res.depict;
+            evaluateDetail.value.mediaList = normalizeMediaList(res.imageVideos);
+        }
+    }
+    console.log(evaluateDetail.value, "evaluateDetail");
+}
+
+onMounted(() => {
+    // 获取用户评价
+    getEvaluateDetail();
+});
+
+
+
+// 预览图片/视频
+function openPreview(list: MediaItem[], index: number) {
+    if (!list?.length) return;
+    VxeUI.modal.open({
+        id: REVIEW_PREVIEW_MODAL_ID,
+        title: '预览',
+        width: 720,
+        escClosable: true,
+        destroyOnClose: true,
+        slots: {
+            default() {
+                return h(ReviewMediaPreview, {
+                    mediaList: list,
+                    initialIndex: index,
+                });
+            },
+        },
+    });
+}
+</script>
+
+<template>
+    <div class="service-detail">
+        <!-- 用户评价 -->
+        <div class="info-section">
+            <div class="info-content-wrapper">
+                <template v-if="hasEvaluate">
+                    <div class="review-item">
+                        <div class="review-top-container">
+                            <div class="review-rating-box">
+                                <div class="review-rating-row" v-for="scroe in scroeType" :key="scroe.name">
+                                    <span class="review-criterion">{{ scroe.name }}</span>
+                                    <a-rate :value="scroe.score" disabled allow-half class="review-stars" />
+                                    <span class="review-score">{{ scroe.score }}分</span>
+                                </div>
+                            </div>
+                            <div class="review-content-block">
+                                <div class="review-content-line">
+                                    <span class="review-content-label">评价内容:</span>
+                                    <span class="review-content-text">{{ evaluateDetail.depict }}</span>
+                                </div>
+                                <div v-if="evaluateDetail.mediaList?.length" class="review-media-row">
+                                    <div v-for="(media, i) in evaluateDetail.mediaList" :key="i" class="review-media-thumb"
+                                        :class="{ 'is-video': media.type === 'video' }" role="button" tabindex="0"
+                                        @click.stop.prevent="openPreview(evaluateDetail.mediaList, i)"
+                                        @keydown.enter.space.prevent="openPreview(evaluateDetail.mediaList, i)">
+                                        <img v-if="media.type === 'image'" :src="media.url" class="review-thumb-img" alt="" />
+                                        <div v-else class="review-thumb-video">
+                                            <video :src="media.url" muted preload="metadata" />
+                                            <span class="review-thumb-play">
+                                                <PlayCircleOutlined />
+                                            </span>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </template>
+                <div v-else class="empty-text">无</div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style scoped lang="scss">
+.service-detail {
+    padding: 20px;
+    color: black;
+    background: #fff;
+
+    .info-section {
+        margin-top: 24px;
+        margin-bottom: 24px;
+        background: #fff;
+    }
+
+    .info-title {
+        font-size: 16px;
+        font-weight: 600;
+        margin-bottom: 12px;
+        color: #333;
+    }
+
+    .info-title-row {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-bottom: 12px;
+
+        .info-title {
+            margin-bottom: 0;
+        }
+    }
+
+    .info-content-wrapper {
+        border: 1px solid #e8e8e8;
+        border-radius: 4px;
+        padding: 10px 20px;
+    }
+
+    .empty-text {
+        color: #999;
+        font-size: 14px;
+        line-height: 22px;
+        padding: 8px 0;
+    }
+
+    .review-item {
+        padding: 12px 0;
+        border-bottom: 1px solid #f0f0f0;
+
+        &:last-child {
+            border-bottom: none;
+        }
+    }
+
+    .review-top-container {
+        display: flex;
+        align-items: flex-start;
+        gap: 24px;
+        margin-bottom: 12px;
+    }
+
+    .review-rating-box {
+        flex-shrink: 0;
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 12px;
+        min-width: 240px;
+    }
+
+    .review-rating-row {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        flex-shrink: 0;
+    }
+
+    .review-criterion {
+        font-size: 14px;
+        color: #333;
+        width: 72px;
+        text-align: left;
+        white-space: nowrap;
+    }
+
+    .review-stars {
+        font-size: 16px;
+
+        :deep(.ant-rate-star-full .ant-rate-star-second) {
+            color: #fadb14;
+        }
+
+        :deep(.ant-rate-star-full .ant-rate-star-first) {
+            color: #fadb14;
+        }
+    }
+
+    .review-score {
+        font-size: 13px;
+        color: #999;
+    }
+
+    .review-content-block {
+        flex: 1;
+        min-width: 0;
+    }
+
+    .review-content-line {
+        display: flex;
+        align-items: flex-start;
+        gap: 6px;
+    }
+
+    .review-content-label {
+        font-size: 14px;
+        color: #333;
+        margin-right: 4px;
+        flex-shrink: 0;
+    }
+
+    .review-content-text {
+        font-size: 14px;
+        color: #333;
+        line-height: 22px;
+        margin: 0;
+        white-space: pre-wrap;
+        word-break: break-word;
+    }
+
+    .review-media-row {
+        display: flex;
+        gap: 8px;
+        flex-wrap: wrap;
+        margin-top: 12px;
+        margin-left: 70px;
+    }
+
+    .review-media-thumb {
+        width: 72px;
+        height: 72px;
+        border-radius: 6px;
+        border: 1px solid #e8e8e8;
+        overflow: hidden;
+        flex-shrink: 0;
+        cursor: pointer;
+        background: #f5f5f5;
+        transition: border-color 0.2s, box-shadow 0.2s;
+
+        &:hover {
+            border-color: #1890ff;
+            box-shadow: 0 0 0 1px #1890ff;
+        }
+
+        .review-thumb-img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+            display: block;
+        }
+
+        .review-thumb-video {
+            position: relative;
+            width: 100%;
+            height: 100%;
+            background: #2a2a2a;
+
+            video {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+                display: block;
+            }
+
+            .review-thumb-play {
+                position: absolute;
+                inset: 0;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                color: rgba(255, 255, 255, 0.9);
+                font-size: 28px;
+                pointer-events: none;
+            }
+        }
+    }
+}
+</style>

+ 4 - 0
src/widgets/PatientCareRecordsWidget.vue

@@ -23,6 +23,10 @@ const gridOptions = reactive<VxeGridProps<ConditioningRecordListModel>>({
   height: 'auto',
   headerAlign: 'center',
   align: 'center',
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   columnConfig: {
     resizable: true,
   },

+ 4 - 0
src/widgets/PatientDiagnosisRecordsWidget.vue

@@ -21,6 +21,10 @@ const gridOptions = reactive<VxeGridProps<DiagnosisReportVO>>({
   height: 'auto',
   headerAlign: 'center',
   align: 'center',
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   columnConfig: {
     resizable: true,
   },

+ 4 - 0
src/widgets/PatientFollowUpRecordsWidget.vue

@@ -23,6 +23,10 @@ const gridOptions = reactive<VxeGridProps<DiagnosisReportVO>>({
   height: 'auto',
   headerAlign: 'center',
   align: 'center',
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   columnConfig: {
     resizable: true,
   },

+ 4 - 0
src/widgets/PatientHealthRecordsWidget.vue

@@ -100,6 +100,10 @@ const gridOptions = reactive<VxeGridProps<Model>>({
   cellConfig: {
     height: 126,
   },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   columns: [
     { field: 'date', title: '日期', width: '150px', align: 'center', slots: { default: 'records' } },
     {