Jelajahi Sumber

Merge branch 'release/2.3.0'

张田田 3 bulan lalu
induk
melakukan
4959578e66
37 mengubah file dengan 6748 tambahan dan 886 penghapusan
  1. 4 1
      @types/typed-router.d.ts
  2. TEMPAT SAMPAH
      src/assets/images/shipment.png
  3. TEMPAT SAMPAH
      src/assets/images/verify.png
  4. 566 129
      src/components/EditSupplier.vue
  5. 14 5
      src/components/SearchableSelect.vue
  6. 27 3
      src/components/SupplyProject.vue
  7. 68 4
      src/model/care.model.ts
  8. 37 3
      src/model/options.ts
  9. 150 0
      src/model/order.model.ts
  10. 8 0
      src/model/people.model.ts
  11. 626 0
      src/order/DateTimePicker.vue
  12. 1838 0
      src/order/DispatchOrderPanel.vue
  13. 283 0
      src/order/EditShipment.vue
  14. 1 0
      src/pages/index.vue
  15. 59 37
      src/pages/index/care/issueService.vue
  16. 21 3
      src/pages/index/care/supplier.vue
  17. 0 291
      src/pages/index/care/text.vue
  18. 14 0
      src/pages/index/equipment/configured.vue
  19. 128 0
      src/pages/index/order/dispatchOrder.vue
  20. 346 0
      src/pages/index/order/management.vue
  21. 285 0
      src/pages/index/order/revenueSharing.vue
  22. 398 0
      src/pages/index/order/shipment.vue
  23. 10 10
      src/request/api/account.api.ts
  24. 1 0
      src/request/api/care.api.ts
  25. 166 0
      src/request/api/order.api.ts
  26. 9 1
      src/router/index.ts
  27. 349 334
      src/service/AddItems.vue
  28. 83 13
      src/service/ConfirmItems.vue
  29. 11 8
      src/service/EditSystemService.vue
  30. 1 1
      src/service/IntroduceProjectList.vue
  31. 476 0
      src/service/OrderDetail.vue
  32. 36 4
      src/service/ServiceDetail.vue
  33. 4 7
      src/service/ServiceItemsList.vue
  34. 4 7
      src/service/ServiceItemsSystem.vue
  35. 1 25
      src/service/ServicePackageDetail.vue
  36. 723 0
      src/service/SingleItemDetail.vue
  37. 1 0
      vite.config.ts

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

@@ -26,7 +26,6 @@ declare module 'vue-router/auto-routes' {
     '//care/serviceItems': RouteRecordInfo<'//care/serviceItems', '/care/serviceItems', Record<never, never>, Record<never, never>>,
     '//care/supplier': RouteRecordInfo<'//care/supplier', '/care/supplier', Record<never, never>, Record<never, never>>,
     '//care/systemService': RouteRecordInfo<'//care/systemService', '/care/systemService', Record<never, never>, Record<never, never>>,
-    '//care/text': RouteRecordInfo<'//care/text', '/care/text', Record<never, never>, Record<never, never>>,
     '//equipment/configured': RouteRecordInfo<'//equipment/configured', '/equipment/configured', Record<never, never>, Record<never, never>>,
     '//equipment/registe': RouteRecordInfo<'//equipment/registe', '/equipment/registe', Record<never, never>, Record<never, never>>,
     '//equipment/reportManagement': RouteRecordInfo<'//equipment/reportManagement', '/equipment/reportManagement', Record<never, never>, Record<never, never>>,
@@ -36,6 +35,10 @@ 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/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>>,
+    '//order/shipment': RouteRecordInfo<'//order/shipment', '/order/shipment', Record<never, never>, Record<never, never>>,
     '//patient/history': RouteRecordInfo<'//patient/history', '/patient/history', Record<never, never>, Record<never, never>>,
     '//patient/room': RouteRecordInfo<'//patient/room', '/patient/room', Record<never, never>, Record<never, never>>,
     '//satisfaction/survey': RouteRecordInfo<'//satisfaction/survey', '/satisfaction/survey', Record<never, never>, Record<never, never>>,

TEMPAT SAMPAH
src/assets/images/shipment.png


TEMPAT SAMPAH
src/assets/images/verify.png


+ 566 - 129
src/components/EditSupplier.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { nextTick } from 'vue';
+import { nextTick, watch, computed, ref, watchEffect, reactive, onBeforeMount } from 'vue';
 import { VxeUI, type VxeFormProps, type VxeFormListeners } from 'vxe-pc-ui';
 import type { SupplierModel } from '@/model/care.model';
 import { useRequest } from 'alova/client';
@@ -7,7 +7,8 @@ import { supplierMethod, supplierEditMethod } from '@/request/api/care.api';
 import { getDictionaryMethod } from '@/request/api/dictionary.api';
 import { branchMethod } from '@/request/api/system.api';
 import { notification } from 'ant-design-vue';
-import { TreeSelect } from 'ant-design-vue';
+import { TreeSelect, Checkbox } from 'ant-design-vue';
+import dayjs, { type Dayjs } from 'dayjs';
 const SHOW_ALL = TreeSelect.SHOW_ALL;
 
 type FormModel = Partial<SupplierModel>;
@@ -19,12 +20,71 @@ const emits = defineEmits<{
   submit: [data?: SupplierModel];
 }>();
 
-const model = ref<Record<string, any>>({});
+const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
+const dayLabels = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期天'];
+
+// 营业时间
+function parseBusinessTime(businessTime?: string): Record<string, { enabled: boolean; start: string; end: string }> {
+  const defaultTime = { start: '08:00', end: '20:00' };
+  const result: Record<string, { enabled: boolean; start: string; end: string }> = {};
+
+  // 如果没有数据,默认全选
+  if (!businessTime) {
+    days.forEach((day) => {
+      result[day] = {
+        enabled: true,
+        start: defaultTime.start,
+        end: defaultTime.end,
+      };
+    });
+    return result;
+  }
+
+
+  const parts = businessTime.split(',').map(s => s.trim());
+
+  days.forEach((day, i) => {
+    const start = parts[i * 2] || '';
+    const end = parts[i * 2 + 1] || '';
+    const enabled = !!(start && end);
+
+    result[day] = {
+      enabled,
+      start: enabled ? start.padStart(5, '0') : defaultTime.start,
+      end: enabled ? end.padStart(5, '0') : defaultTime.end,
+    };
+  });
+
+  return result;
+}
+
+// 营业时间
+function formatBusinessTime(businessHours: Record<string, { enabled: boolean; start: string; end: string }>): string {
+  return days.flatMap(day => {
+    const { enabled, start, end } = businessHours[day] || {};
+    return enabled && start && end ? [start, end] : ['', ''];
+  }).join(',');
+}
+
+// 初始化营业时间
+function initBusinessHours(data?: any) {
+  return data?.businessTime ? parseBusinessTime(data.businessTime) : parseBusinessTime();
+}
+
+const model = ref<Record<string, any>>({
+  collaborateDepts: [],
+  businessHours: initBusinessHours(),
+  dayLabels: dayLabels,
+});
+
 watchEffect(() => {
   const data = props.data;
   const collaborateDepts = data?.collaborateDepts?.map((item: any) => ({ value: String(item.deptId), label: item.deptName })) ?? [];
+
   model.value = {
-    collaborateDepts
+    ...model.value,
+    collaborateDepts,
+    businessHours: initBusinessHours(data),
   }
 });
 
@@ -33,28 +93,30 @@ const { loading: branchLoading } = useRequest(branchMethod).onSuccess(({ data })
   const to = (data?: any[]): any[] => {
     return Array.isArray(data)
       ? data.map(item => ({
-          ...item,
-          value: String(item.id),
-          key: String(item.id),
-          label: item.label,
-          title: item.label,
-          children: to(item.children)
-        }))
+        ...item,
+        value: String(item.id),
+        key: String(item.id),
+        label: item.label,
+        title: item.label,
+        children: to(item.children)
+      }))
       : [];
   }
   branch.value = to(data);
 });
 
-// 安全挂载弹层,避免 document 不可用时报错
-function getSafePopupContainer(triggerNode?: HTMLElement) {
+
+function getSafePopupContainer(triggerNode?: HTMLElement): HTMLElement {
   try {
-    // @ts-ignore
+
     if (typeof document !== 'undefined' && document?.body) return document.body;
-  } catch (e) {}
-  return triggerNode?.parentNode as HTMLElement | undefined;
+  } catch (e) {
+    console.log(e);
+  }
+  return (triggerNode?.parentNode as HTMLElement) || document.body;
 }
 
-// 查找节点/子孙/标签
+
 function findNode(nodes: any[], id: string | number): any | undefined {
   for (const n of nodes) {
     if (String(n.value) === String(id)) return n;
@@ -85,7 +147,6 @@ function findLabel(nodes: any[], id: string | number): string {
   return '';
 }
 
-// 收集整棵树的所有节点 key,用于一次性展开整个下拉树
 function collectAllKeys(nodes: any[]): string[] {
   const keys: string[] = [];
   const stack = [...nodes];
@@ -112,112 +173,428 @@ const { loading: submitting, send: submit } = useRequest(supplierEditMethod, { i
 });
 
 const projectList = ref<Array<{ label: string; value: string }>>([]);
+// 线上权益
+const onlineCPList = ref<Array<{ label: string; value: string }>>([]);
+// 营业时间
+// 全选/取消全选
+const selectAllBusinessHours = computed({
+  get: () => {
+    if (!model.value.businessHours) return false;
+    return days.every(day => model.value.businessHours[day]?.enabled);
+  },
+  set: (value: boolean) => {
+    if (!model.value.businessHours) return;
+    days.forEach(day => {
+      if (model.value.businessHours[day]) {
+        model.value.businessHours[day].enabled = value;
+        // 初始化时间范围值
+        if (value && !timeRangeValues.value[day]) {
+          const dayData = model.value.businessHours[day];
+          if (dayData.start && dayData.end) {
+            try {
+              timeRangeValues.value[day] = [dayjs(dayData.start, 'HH:mm'), dayjs(dayData.end, 'HH:mm')];
+            } catch {
+              timeRangeValues.value[day] = [dayjs('08:00', 'HH:mm'), dayjs('20:00', 'HH:mm')];
+            }
+          } else {
+            timeRangeValues.value[day] = [dayjs('08:00', 'HH:mm'), dayjs('20:00', 'HH:mm')];
+          }
+        }
+      }
+    });
+    // 触发表单验证
+    nextTick(() => {
+      formRef?.value?.validateField('businessTime');
+    });
+  }
+});
 
-const formProps = reactive<VxeFormProps<FormModel>>({
-  titleWidth: 100,
-  titleAlign: 'right',
-  titleColon: true,
-  data: { ...props.data },
-  items: [
-    {
-      field: 'name',
-      title: '供应商',
-      span: 24,
-      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
-    },
-    {
-      field: 'detailAddress',
-      title: '地址',
-      span: 24,
-      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
-    },
-    {
-      field: 'kahuna',
-      title: '负责人',
-      span: 24,
-      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
-    },
-    {
-      field: 'phone',
-      title: '联系电话',
-      span: 24,
-      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+// 处理单个日期选择
+const handleDayToggle = (day: string) => {
+  if (!model.value.businessHours || !model.value.businessHours[day]) return;
+  const wasEnabled = model.value.businessHours[day].enabled;
+  model.value.businessHours[day].enabled = !wasEnabled;
+
+  // 如果启用日期,初始化时间范围值
+  if (!wasEnabled && !timeRangeValues.value[day]) {
+    const dayData = model.value.businessHours[day];
+    if (dayData.start && dayData.end) {
+      try {
+        timeRangeValues.value[day] = [dayjs(dayData.start, 'HH:mm'), dayjs(dayData.end, 'HH:mm')];
+      } catch {
+        timeRangeValues.value[day] = [dayjs('08:00', 'HH:mm'), dayjs('20:00', 'HH:mm')];
+      }
+    } else {
+      timeRangeValues.value[day] = [dayjs('08:00', 'HH:mm'), dayjs('20:00', 'HH:mm')];
+    }
+  }
+
+  // 触发表单验证
+  nextTick(() => {
+    formRef?.value?.validateField('businessTime');
+  });
+};
+
+// 处理时间范围变化
+const handleTimeRangeChange = (day: string, timeRange: [Dayjs, Dayjs] | null) => {
+  if (!model.value.businessHours || !model.value.businessHours[day]) return;
+  if (timeRange && timeRange.length === 2) {
+    model.value.businessHours[day].start = timeRange[0].format('HH:mm');
+    model.value.businessHours[day].end = timeRange[1].format('HH:mm');
+  }
+  // 触发表单验证
+  nextTick(() => {
+    formRef?.value?.validateField('businessTime');
+  });
+};
+
+const timeRangeValues = ref<Record<string, [Dayjs, Dayjs] | null>>({});
+
+function initTimeRangeValues() {
+  days.forEach(day => {
+    if (!(day in timeRangeValues.value)) {
+      timeRangeValues.value[day] = null;
+    }
+  });
+}
+
+watch(() => model.value.businessHours, (newVal) => {
+  if (newVal) {
+    days.forEach(day => {
+      const dayData = newVal[day];
+      if (dayData?.enabled) {
+        if (dayData?.start && dayData?.end) {
+          try {
+            timeRangeValues.value[day] = [dayjs(dayData.start, 'HH:mm'), dayjs(dayData.end, 'HH:mm')];
+          } catch {
+            timeRangeValues.value[day] = [dayjs('08:00', 'HH:mm'), dayjs('20:00', 'HH:mm')];
+          }
+        } else {
+          timeRangeValues.value[day] = [dayjs('08:00', 'HH:mm'), dayjs('20:00', 'HH:mm')];
+        }
+      } else {
+        if (!timeRangeValues.value[day]) {
+          timeRangeValues.value[day] = [dayjs('08:00', 'HH:mm'), dayjs('20:00', 'HH:mm')];
+        }
+      }
+    });
+  } else {
+    // 初始化所有日期
+    initTimeRangeValues();
+  }
+}, { immediate: true, deep: true });
+
+// 初始化所有日期的时间范围值
+initTimeRangeValues();
+
+const formRef = ref<any>(null);
+
+
+const baseFormItems = [
+  {
+    field: 'name',
+    title: '供应商',
+    span: 24,
+    itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+  },
+  {
+    field: 'detailAddress',
+    title: '地址',
+    span: 24,
+    itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+  },
+  {
+    field: 'kahuna',
+    title: '负责人',
+    span: 24,
+    itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+  },
+  {
+    field: 'phone',
+    title: '联系电话',
+    span: 24,
+    itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+  },
+  {
+    field: 'collaborateDepts',
+    title: '合作机构',
+    span: 24,
+    slots: {
+      default: 'initiate',
     },
-    {
-      field: 'collaborateDepts',
-      title: '合作机构',
-      span: 24,
-      slots: {
-        default: 'initiate',
+  },
+  {
+    field: 'onlineCPTypes',
+    title: '实体商品',
+    span: 24,
+    itemRender: {
+      name: 'VxeSelect',
+      props: {
+        placeholder: '请选择',
+        options: computed(() => projectList.value),
+        optionProps: {
+          value: 'value',
+          label: 'label',
+        },
+        multiple: true,
+        collapseTags: true,
+        clearable: true,
       },
     },
-    {
-      field: 'onlineCPTypes',
-      title: '居家项目',
-      span: 24,
-      itemRender: {
-        name: 'VxeSelect',
-        props: {
-          placeholder: '请选择',
-          options: computed(() => projectList.value),
-          optionProps: {
-            value: 'value',
-            label: 'label',
-          },
-          multiple: true,
-          collapseTags: true,
-          clearable: true,
+  },
+  {
+    field: 'offlineCPTypes',
+    title: '线下服务',
+    span: 24,
+    itemRender: {
+      name: 'VxeSelect',
+      props: {
+        placeholder: '请选择',
+        options: computed(() => projectList.value),
+        optionProps: {
+          value: 'value',
+          label: 'label',
         },
+        multiple: true,
+        collapseTags: true,
+        clearable: true,
       },
     },
-    {
-      field: 'offlineCPTypes',
-      title: '线下项目',
-      span: 24,
-      itemRender: {
-        name: 'VxeSelect',
-        props: {
-          placeholder: '请选择',
-          options: computed(() => projectList.value),
-          optionProps: {
-            value: 'value',
-            label: 'label',
-          },
-          multiple: true,
-          collapseTags: true,
-          clearable: true,
+  },
+  // 开始新增项
+  {
+    field: 'outlineCPTypes',
+    title: '线上权益',
+    span: 24,
+    itemRender: {
+      name: 'VxeSelect',
+      props: {
+        placeholder: '请选择',
+        options: computed(() => onlineCPList.value),
+        optionProps: {
+          value: 'value',
+          label: 'label',
         },
+        multiple: true,
+        collapseTags: true,
+        clearable: true,
       },
     },
-    { align: 'center', span: 24, slots: { default: 'active' } },
-  ],
+  },
+  //账户类型,单选,商户号还是个人号,默认选中商户号
+  {
+    field: 'wechatPaymentType',
+    title: '账户类型',
+    span: 24,
+    itemRender: {
+      name: 'VxeRadioGroup',
+      props: {
+        placeholder: '请选择',
+        options: [
+          { label: '商户号', value: '2' },
+          { label: '个人号', value: '1' },
+        ],
+        modelValue: '2',
+      },
+    },
+  },
+  // 微信账户
+  {
+    field: 'wechatPaymentAccount',
+    title: '微信账户',
+    span: 24,
+    itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+  },
+  // 分账比例 0-100
+  {
+    field: 'profitSharing',
+    title: '分账比例',
+    span: 24,
+    itemRender: { name: 'VxeInput', props: { placeholder: '请输入', type: 'number', min: 0, max: 100 } },
+  },
+  // 营业状态 单选,营业、休息、停业
+  {
+    field: 'businessStatus',
+    title: '营业状态',
+    span: 24,
+    itemRender: { name: 'VxeRadioGroup', props: { placeholder: '请选择' }, options: [{ label: '营业', value: '1' }, { label: '休息', value: '2' }, { label: '停业', value: '3' }] },
+  },
+
+  { 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,
   rules: {
     name: [{ required: true, message: '请输入供应商名称' }],
-    phone: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }],
+    detailAddress: [{ required: true, message: '请输入地址' }],
+    kahuna: [{ required: true, message: '请输入负责人' }],
+    phone: [{ required: true, pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }],
+    // 合作机构
+    collaborateDepts: [
+      {
+        required: true,
+        validator: () => {
+          const depts = model.value.collaborateDepts;
+          if (!depts || !Array.isArray(depts) || depts.length === 0) {
+            notification.error({
+              message: '请选择合作机构',
+            });
+            return;
+          }
+        },
+      },
+    ],
+    wechatPaymentType: [{ required: true, message: '请选择账户类型' }],
+    wechatPaymentAccount: [{ required: true, message: '请输入微信接收方账号' }],
+    profitSharing: [{ required: true, message: '请输入分账比例' }],
+    businessStatus: [{ required: true, message: '请选择营业状态' }],
+    businessTime: [
+      {
+        validator: () => {
+          if (!showBusinessHours.value) return;
+          if (!model.value.businessHours) {
+            notification.error({
+              message: '请至少选择一天营业时间',
+            });
+            return;
+          }
+          // 检查至少选择了一天
+          const hasSelectedDay = days.some(day => model.value.businessHours[day]?.enabled);
+          if (!hasSelectedDay) {
+            notification.error({
+              message: '请至少选择一天营业时间',
+            });
+            return;
+          }
+          // 检查选中的天都有时间范围
+          for (const day of days) {
+            const dayData = model.value.businessHours[day];
+            if (dayData?.enabled) {
+              if (!dayData.start || !dayData.end) {
+                throw new Error('请完善所有选中日期的营业时间');
+              }
+            }
+          }
+        },
+      },
+    ],
   },
 });
 
-function handleSelect(value: string, node: any, extra: any) {
-  const get = (children?: any[]) => {
-    if (!Array.isArray(children)) return;
-    children.forEach(child => {
-      if (!model.value.collaborateDepts.find((item: any) => item.value === child.value)) {
-        model.value.collaborateDepts.push(child)
-      }
-      get(child.children)
-    })
-  }
-  get(node.children)
+// 判断是否显示营业时间(仅限"线下项目"有值时展示)
+const showBusinessHours = computed<boolean>(() => {
+  const offlineCPTypes: string[] | undefined = formProps.data?.offlineCPTypes;
+  return !!(offlineCPTypes && Array.isArray(offlineCPTypes) && offlineCPTypes.length > 0);
+});
+
+const prevOfflineCPTypes = ref<string[] | undefined>(undefined);
+let wasCleared = false;
+
+// 重置营业时间为默认值
+function resetBusinessHours() {
+  const defaultTime = { start: '08:00', end: '20:00' };
+  const businessHoursData: Record<string, { enabled: boolean; start: string; end: string }> = {};
+  days.forEach((day) => {
+    businessHoursData[day] = {
+      enabled: true,
+      start: defaultTime.start,
+      end: defaultTime.end,
+    };
+  });
+  model.value.businessHours = businessHoursData;
+
+  // 重置时间范围值
+  days.forEach(day => {
+    timeRangeValues.value[day] = [dayjs('08:00', 'HH:mm'), dayjs('20:00', 'HH:mm')];
+  });
 }
 
+// 监听线下项目变化,更新表单项,并在清除后再次选择时重置营业时间
+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 });
+
 const formEmits: VxeFormListeners<FormModel> = {
   submit({ data }) {
-    data.collaborateDepts = model.value.collaborateDepts.map((item: any) =>{
+    // 验证至少选择一个项目类型
+    const onlineCPTypes = data?.onlineCPTypes;
+    const offlineCPTypes = data?.offlineCPTypes;
+    const outlineCPTypes = data?.outlineCPTypes;
+    const hasOnline = onlineCPTypes && Array.isArray(onlineCPTypes) && onlineCPTypes.length > 0;
+    const hasOffline = offlineCPTypes && Array.isArray(offlineCPTypes) && offlineCPTypes.length > 0;
+    const hasOutline = outlineCPTypes && Array.isArray(outlineCPTypes) && outlineCPTypes.length > 0;
+
+    if (!hasOnline && !hasOffline && !hasOutline) {
+      notification.error({
+        message: '实体商品、线下服务、线上权益至少选择一项',
+      });
+      return;
+    }
+
+    data.collaborateDepts = model.value.collaborateDepts.map((item: any) => {
       return {
         deptId: item.value,
         deptName: item.label
       }
     })
+
+    // 格式化营业时间数据为字符串格式
+    if (showBusinessHours.value && model.value.businessHours) {
+      (data as any).businessTime = formatBusinessTime(model.value.businessHours);
+    }
+
     submit(data).then(() => {
       notification.success({
         message: '操作成功',
@@ -227,7 +604,6 @@ const formEmits: VxeFormListeners<FormModel> = {
   },
 };
 
-// 统一用 change 事件集中处理(父选子,取消独立),避免在 select 阶段改值导致弹层关闭
 function onDeptChange(
   newVal: Array<{ value: string | number; label: string }>,
   _labels: Array<string>,
@@ -240,21 +616,20 @@ function onDeptChange(
 
   const final = new Set(current);
 
-  // 优先使用组件提供的 triggerValue 判定是否为新增(选中)以及哪一个父节点被点击
+  // 判定是否为新增(选中)
   const triggerValue = String(extra?.triggerValue ?? '');
   const isChecked = !!extra?.checked; // true: 勾选, false: 取消
 
   if (triggerValue && isChecked) {
-    // 勾选:为该节点补齐所有子孙节点
+    // 勾选
     const node = findNode(branch.value, triggerValue);
     if (node) {
       for (const cid of collectChildren(node)) final.add(cid);
     }
   } else if (!isChecked) {
-    // 取消:独立行为,保持 current 即可(不联动移除子)
-    // 无其他处理
+    // 取消
   } else {
-    // 回退策略:没有 extra(某些版本或场景),使用 diff 判定新增的父节点并补齐
+    // 回退
     const added = [...current].filter((v) => !prev.has(v));
     for (const v of added) {
       const node = findNode(branch.value, v);
@@ -266,7 +641,7 @@ function onDeptChange(
 
   const finalIds = [...final];
   const nextList = finalIds.map((id) => ({ value: String(id), label: findLabel(branch.value, id) }));
-  // 延后设置,避免同一事件循环重绘导致“没反应”
+
   nextTick(() => {
     model.value.collaborateDepts = nextList;
   });
@@ -274,40 +649,70 @@ function onDeptChange(
 function cancel() {
   VxeUI.modal.close('supplier-modal');
 }
-onBeforeMount(async () => {
-  if (props.data?.id) load(props.data);
+// 获取线上权益
+async function getOnlineCPList() {
+  try {
+    const res = await getDictionaryMethod('condition_type_outline');
+    if (res?.length > 0) {
+      onlineCPList.value = res;
+    }
+  } catch (error) {
+    console.error('获取项目列表失败:', error);
+  };
+}
+// 获取线下项目
+async function getOfflineCPList() {
   try {
     const res = await getDictionaryMethod('condition_type');
     if (res?.length > 0) {
-      projectList.value = res; // 直接使用返回的数据
+      projectList.value = res;
     }
   } catch (error) {
     console.error('获取项目列表失败:', error);
-  }
+  };
+}
+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">
-    <vxe-form v-bind="formProps" v-on="formEmits" :loading="submitting">
+    <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"
-          tree-checkable
-          allow-clear
-          :labelInValue="true"
-          v-model:value="model.collaborateDepts"
-          v-model:treeExpandedKeys="expandedKeys"
-          :getPopupContainer="getSafePopupContainer"
-          :dropdownMatchSelectWidth="false"
-          :dropdownStyle="{ zIndex: 4000 }"
-          placeholder="请选择"
-          @change="onDeptChange"
-          @dropdownVisibleChange="onVisibleChange"
-        />
+        <a-tree-select style="width: 100%" :show-checked-strategy="SHOW_ALL" tree-check-strictly :tree-data="branch"
+          tree-checkable allow-clear :labelInValue="true" v-model:value="model.collaborateDepts"
+          v-model:treeExpandedKeys="expandedKeys" :getPopupContainer="getSafePopupContainer"
+          :dropdownMatchSelectWidth="false" :dropdownStyle="{ zIndex: 4000 }" placeholder="请选择" @change="onDeptChange"
+          @dropdownVisibleChange="onVisibleChange" />
+      </template>
+      <template #businessTimeTitle>
+        <span style="color: #ff4d4f">*</span>营业时间
+      </template>
+      <template #businessHours>
+        <div class="business-hours-container">
+          <div class="business-hours-header">
+            <Checkbox v-model:checked="selectAllBusinessHours">全选</Checkbox>
+          </div>
+          <div class="business-hours-list">
+            <div v-for="(day, index) in days" :key="day" class="business-hours-item">
+              <Checkbox :checked="model.businessHours?.[day]?.enabled || false" @change="() => handleDayToggle(day)">
+                {{ model.dayLabels?.[index] || dayLabels[index] }}
+              </Checkbox>
+              <div class="time-range-wrapper">
+                <a-time-range-picker :value="timeRangeValues[day]" format="HH:mm"
+                  :disabled="!model.businessHours?.[day]?.enabled"
+                  @change="(timeRange: any) => handleTimeRangeChange(day, timeRange)" style="width: 200px" />
+              </div>
+            </div>
+          </div>
+        </div>
       </template>
       <template #active>
         <vxe-button type="reset" content="取消" :disabled="submitting" @click="cancel"></vxe-button>
@@ -322,4 +727,36 @@ onBeforeMount(async () => {
 .form-container {
   padding: 20px;
 }
+
+.business-hours-container {
+  width: 100%;
+}
+
+.business-hours-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 12px;
+
+  .business-hours-label {
+    font-weight: 500;
+  }
+}
+
+.business-hours-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.business-hours-item {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+
+  .time-range-wrapper {
+    display: flex;
+    align-items: center;
+  }
+}
 </style>

+ 14 - 5
src/components/SearchableSelect.vue

@@ -63,7 +63,7 @@ interface Option {
 }
 
 interface Props {
-  modelValue?: string | number;
+  modelValue?: string | number | undefined;
   options?: Option[];
   placeholder?: string;
   width?: string;
@@ -94,12 +94,12 @@ const props = withDefaults(defineProps<Props>(), {
 });
 
 const emit = defineEmits<{
-  'update:modelValue': [value: string | number];
-  change: [value: string | number, option: Option | null];
+  'update:modelValue': [value: string | number | undefined];
+  change: [value: string | number | undefined, option: Option | null];
   search: [keyword: string];
 }>();
 
-const selectedValue = ref<string | number | { value: string | number; label: string }>('');
+const selectedValue = ref<string | number | { value: string | number; label: string } | undefined>(undefined);
 const searchKeyword = ref('');
 const currentPage = ref(1);
 const allOptions = ref<Option[]>([]);
@@ -174,7 +174,8 @@ watch(
         selectedValue.value = { value: newValue, label: newValue };
       }
     } else {
-      selectedValue.value = '';
+      // 当 labelInValue 为 true 时,空值应该是 undefined,而不是空字符串
+      selectedValue.value = undefined;
     }
   },
   { immediate: true }
@@ -263,6 +264,14 @@ const handleChange = (value: any, option: any) => {
   if (value === '__load_more__' || value === '__initial_loading__' || value === '__loading__' || value === undefined) return;
 
   // 使用 labelInValue 时,value 是 { value, label } 格式
+  // 当用户清空选择时,value 可能是 null
+  if (value === null) {
+    selectedValue.value = undefined;
+    emit('update:modelValue', undefined);
+    emit('change', undefined, null);
+    return;
+  }
+
   if (typeof value === 'object' && value !== null) {
     selectedValue.value = value.value;
     emit('update:modelValue', value.value);

+ 27 - 3
src/components/SupplyProject.vue

@@ -8,13 +8,15 @@ const props = defineProps<{ data: FollowModel }>();
 const homeData = ref<Array<{ project: string }>>([]);
 // 定义线下项目表格数据
 const offlineData = ref<Array<{ project: string }>>([]);
+// 定义线上权益表格数据
+const onlineData = ref<Array<{ project: string }>>([]);
 // 居家项目表格配置
 const homeGridOptions = reactive<VxeGridProps>({
   border: true,
   autoResize: true,
   columns: [
     { type: 'seq', title: '序号', width: 80 },
-    { field: 'project', title: '居家项目', width: 320 },
+    { field: 'project', title: '实体商品', width: 246 },
   ],
   data: homeData.value,
 });
@@ -24,13 +26,24 @@ const offlineGridOptions = reactive<VxeGridProps>({
   autoResize: true,
   columns: [
     { type: 'seq', title: '序号', width: 80 },
-    { field: 'project', title: '线下项目', width: 320 },
+    { field: 'project', title: '线下服务', width: 246 },
   ],
   data: offlineData.value,
 });
+// 线上权益表格配置
+const onlineGridOptions = reactive<VxeGridProps>({
+  border: true,
+  autoResize: true,
+  columns: [
+    { type: 'seq', title: '序号', width: 80 },
+    { field: 'project', title: '线上权益', width: 246 },
+  ],
+  data: onlineData.value,
+});
 // 表格引用
 const homeGridRef = ref<VxeGridInstance>();
 const offlineGridRef = ref<VxeGridInstance>();
+const onlineGridRef = ref<VxeGridInstance>();
 onMounted(() => {
   if (props?.data?.offlineCPTypes?.length > 0) {
     props.data.offlineCPTypes.forEach((item: string) => {
@@ -43,9 +56,17 @@ onMounted(() => {
     props.data.onlineCPTypes.forEach((item: string) => {
       homeData.value.push({ project: item });
     });
+
   }else{
     homeData.value = [];
   }
+  if (props?.data?.outlineCPTypes?.length > 0) {
+    props.data.outlineCPTypes.forEach((item: string) => {
+      onlineData.value.push({ project: item });
+    });
+  }else{
+    onlineData.value = [];
+  }
 });
 </script>
 
@@ -57,13 +78,16 @@ onMounted(() => {
     <div class="table-wrapper">
       <vxe-grid ref="offlineGridRef" v-bind="offlineGridOptions" />
     </div>
+    <!-- 线上权益 -->
+    <div class="table-wrapper">
+      <vxe-grid ref="onlineGridRef" v-bind="onlineGridOptions" />
+    </div>
   </div>
 </template>
 
 <style scoped lang="scss">
 .tables-container {
   display: flex;
-  padding: 20px;
   height: 600px;
 }
 

+ 68 - 4
src/model/care.model.ts

@@ -12,6 +12,12 @@ export interface SupplierModel {
   }[];
   onlineCPTypes?: string[]; // 居家项目
   offlineCPTypes?: string[]; // 线下项目
+  outlineCPTypes?:string[];//线上权益
+  wechatPaymentType?:string; //1-个人openid 2-商户号	
+  wechatPaymentAccount?:string; //微信接收方账号
+  profitSharing?:string; //	分账比例
+  businessTime?:string; //营业时间,比如:08:10,12:10,23:59
+  businessStatus?:string; //	营业状态 1-营业 2-休息 3-停业	
 }
 export type SupplierQuery = Partial<SupplierModel>;
 
@@ -23,11 +29,18 @@ export interface SystemItemModel {
   conditioningProgramType?: string; // 	调理方案类型
   collaborateDeptId?: number; // 合作机构ID
   conditioningProgramTypes?: string[]; // 调理方案类型
+  offlineDuration?: string; // 服务所需时间(分钟)
+  frequencyType?: string; // 建议频率天数
+  frequencyMeasure?: number; // 建议频率次数/用量
   pricingType?: string; // 计价方式
   institutionId?: string; // 所属机构ID
   institutionName?: string; // 所属机构名称
   isSystemic?: string; // 是否系统项目方案
   status?: string; // 项目状态
+  sellType?: string; // 商品类型
+  conditioningProgramPhoto?: string; // 项目图片
+  conditioningProgramName?: string; // 项目名称
+
   conditioningProgramSupplierName?: string; // 供应商名称
   conditioningProgramSupplierId?: string; // 供应商ID
   cpPatientMatchRule: {
@@ -63,7 +76,7 @@ export interface SystemItemModel {
   miniProgramCode: string; // 小程序码
   itemVideoFirst: string; // 操作视频
   attrFirst: string; // 特色
-  isOffline?: string | null; // 是否线下
+  // isOffline?: string | null; // 是否线下
   isDelivery?: string | null; // 是否配送
   photo: string; // 商品图片
   attrSeventh: string; //使用注意
@@ -145,10 +158,27 @@ export interface SystemCwModel {
   updateTime?: string; // 更新时间
   status?: string; // 系统开关
   items: {
+    
+    expressType?: string; // 快递类型 0-邮政速递 1-顺丰速运 2-京东快递 3-中通快递 4-圆通速递 5-申通快递 6-韵达快递 7-极兔速递
+    expressNo?: string; // 快递单号
+    liaison?: string; // 联系人
+    phone?: string; // 联系电话
+    provinceName?: string; // 省名称
+    cityName?: string; // 市名称
+    areaName?: string; // 区名称
+    detailAddress?: string; // 详细地址
+    receiptType?: string; // 收货方式 0-快递 1-线下取货
     id: string; //调理包明细id
     sourceId?: string; // 来源id
     conditioningWrapId: string; // 系统调理包ID
     conditioningProgramId: number; // 调理方案ID
+    conditioningProgramPhoto: string; //调理方案图片
+    conditioningProgramName: string; //调理方案名称
+    conditioningProgramType:string; //调理方案类型
+    // isOffline?: string | null; // 是否线下
+    sellType:string; //商品类型 1-实体商品 2-线下服务 3-线上权益
+    convertDose: number; // 转换剂量
+    convertUnit: string; // 转换单位
     conditioningProgramDetail: {
       id: number;
       name: string;
@@ -172,13 +202,26 @@ export interface SystemCwModel {
         dosage: string; // 剂量
       }[];
       effect: string; // 功效
-      isOffline?: string | null; // 是否线下
+      // isOffline?: string | null; // 是否线下
+      sellType:string; //商品类型 1-实体商品 2-线下服务 3-线上权益
       isDelivery?: string | null; // 是否配送
       photo: string; // 图片
       conditioningProgramSupplierName: string; // 供应商名称
       institutionId: string; // 所属机构ID
       institutionName: string; // 所属机构名称
     };
+    // 服务记录列表
+    patientConditioningOfflines: {
+      id: string; // 线下项目ID
+      arrangeDate: string; // 预约日期
+      nextArrangeDate: string; // 下次预约日期
+      operateBy: string; // 操作人
+      operateTime: string; // 操作时间
+      startTime: string; // 开始时间
+      endTime: string; // 结束时间
+      feedback: string; // 治疗备注
+      photo: string; // 图片
+    }[];
     cwcpAcuMeridians: {
       id: number;
       name: string;
@@ -204,6 +247,10 @@ export interface SystemCwModel {
     initialDay: string; // 初始天数
     totalMeasure: string; // 总用量
     remark: string; // 说明
+    unitPrice:number; // 单价
+    receiptStatus:string; //收货状态 0-待发货 1-已发货 2-已收货
+    receiptTime:string; //收货时间
+    progress?:string; //进度 0-进行中 1-已完成 2-未开始
   }[];
 }
 
@@ -239,8 +286,23 @@ export interface ConditioningRecordListModel {
   constitutionGroupName: string; // 体质
   operateBy: string; // 开具人
   operateTime: string; // 开具时间
+  orderNo?: string; // 订单编号
+  createTime?: string; // 创建时间
+  payTime?: string; // 付款时间
+  orderStatus?: string; // 订单状态
+  orderAmount?: number | string; // 订单金额
+  createBy?: string; // 创建人
+  estimatedStartDate?: string; // 开始调养日期
+  realProfitSharing?: string; // 实际分账比例
+  profitSharing?:string; // 预计分账比例
 }
-export type ConditioningRecordListQuery = Partial<ConditioningRecordListModel>;
+export type ConditioningRecordListQuery = Partial<ConditioningRecordListModel> & {
+  orderStatus?: string; // 订单状态
+  orderTimeStart?: string; // 下单开始时间
+  orderTimeEnd?: string; // 下单结束时间
+  payTimeStart?: string; // 付款开始时间
+  payTimeEnd?: string; // 付款结束时间
+};
 
 // 开立调养方案
 export interface OpenConditioningSchemeModel {
@@ -317,7 +379,8 @@ export interface OpenConditioningSchemeModel {
         },
       ];
       effect: string; // 功效
-      isOffline?: string | null; // 是否线下
+      // isOffline?: string | null; // 是否线下
+      sellType:string; //商品类型 1-实体商品 2-线下服务 3-线上权益
       isDelivery?: string | null; // 是否配送
       photo: string; // 图片
       conditioningProgramSupplierName: string; // 供应商名称
@@ -346,6 +409,7 @@ export interface OpenConditioningSchemeModel {
     frequencyType: string | string[]; // 多少天
     frequencyTypeing: string[]; // 多少天
     frequencyMeasure: string; // 频次用量
+    offlineDuration?: string; // 服务所需时间(分钟)
     totalPrice: string; //总价格
     initialDay: string; // 初始天数
     totalMeasure: string; // 总用量

+ 37 - 3
src/model/options.ts

@@ -11,14 +11,29 @@ export const planStatus = [
 ];
 export const conditioningStatus = [
   { label: '状态', value: void 0 },
-  { label: '待付款', value: '0' },
-  { label: '已作废', value: '1' },
-  { label: '用户取消', value: '2' },
+  // { label: '待付款', value: '0' },
+  // { label: '已作废', value: '1' },
+  // { label: '用户取消', value: '2' },
   { label: '未开始', value: '3' },
   { label: '调理中', value: '4' },
   { label: '已完结', value: '5' },
 
 ];
+//营业状态
+export const businessStatus = [
+  // { label: '请选择', value: void 0 },
+  { label: '营业', value: '1' },
+  { label: '休息', value: '2' },
+  { label: '停业', value: '3' },
+];
+export const orderStatus = [
+  { label: '请选择', value: void 0 },
+  { label: '交易关闭(已作废)', value: '1' },
+  { label: '待付款', value: '0' },
+  { label: '交易关闭(用户取消)', value: '2' },
+  { label: '已付款', value: '6' },
+  { label: '交易成功', value: '345' },
+];
 export const enabledStatus = [
   { label: '状态', value: void 0 },
   { label: '启用', value: '0' },
@@ -64,3 +79,22 @@ export const statusLabel = [
   { value: 1012, label: 'checkbox' },
   { value: 1013, label: 'group' },
 ];
+// 发货状态
+export const receiptStatus = [
+  { label: '待发货', value: '0' },
+  { label: '已发货', value: '1' },
+  { label: '已收货', value: '2' },
+];
+// 发货形式
+export const receiptType = [
+  { label: '配送', value: '0' },
+  { label: '线下取货', value: '1' },
+];
+
+// 分账状态
+export const profitSharingStatus = [
+  { label: '状态', value: void 0 },
+  { label: '未分账', value: '1' },
+  { label: '已分账', value: '2' },
+  { label: '分账异常', value: '3' },
+];

+ 150 - 0
src/model/order.model.ts

@@ -0,0 +1,150 @@
+// export interface OrderQuery {
+//   liaison: string; //联系人
+//   phone:number;//电话
+//   types:string[];//订单类型 1-未派单 2-已派单但未核销 3-已核销	
+//   pieTimeStart:string;//派单时间开始
+//   pieTimeEnd:string;//派单时间结束
+// }
+
+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;//派单时间结束
+}
+export type OrderQuery = Partial<OrderModel>;
+//派单机构列表
+export interface OrderLiaisonListModel {
+  id: number; //	调理方案供应商ID
+  name: string; //调理方案供应商名称
+  provinceCode: string; //省编码
+  cityCode: string; //市编码
+  cityName: string; //市名称
+  areaCode: string; //区编码
+  areaName: string; //区名称
+  detailAddress: string; //详细地址
+  phone: string; //联系电话
+  liaison: string; //联系人
+  kahuna: string; //负责人
+  collaborateDeptIds: [
+    {
+      deptId: string; //调理方案供应商合作机构id
+      deptName: string; //调理方案供应商合作机构名称
+    }
+  ]; //合作机构
+  onlineCPTypes: string[]; //可供应的实体商品类型
+  offlineCPTypes: string[]; //	可供应的线下服务类型
+  outlineCPTypes: string[]; //可供应的线上权益类型
+  wechatPaymentType: string; //微信支付类型 1-个人openid 2-商户号
+  wechatPaymentAccount: string; //微信接收方账号
+  profitSharing: string; //分账比例
+  businessTime: string; //营业时间
+  businessStatus: string; //营业状态 1-营业 2-休息 3-停业
+  businessHours: {
+    [key: string]: {
+      enabled: boolean;
+      start: string;
+      end: string;
+    };
+  }; //营业时间
+  createBy: string; //创建者
+  createTime: string; //创建时间
+  updateBy: string; //更新者
+  updateTime: string; //更新时间
+  todayOrderQuantity: number; //当日订单数量
+}
+export type OrderLiaisonListQuery = Partial<OrderLiaisonListModel>;
+
+
+
+export interface ShipmentModel{
+  id: number; //	患者调理方案ID
+  orderNo: string; //订单ID
+  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;//快递单号
+
+}
+export type ShipmentQuery = Partial<ShipmentModel>;
+
+
+export interface PieOrderCountModel {
+  offlineCount: number; //线下服务数量
+  allPieOfflineOrderCount: number; //线下服务——全部指派订单数量
+  pendPieOfflineOrderCount: number; //线下服务——待指派订单数量
+  todayPieOfflineOrderCount: number; //线下服务——今日指派订单数量
+  onlineCount: number; //实体商品数量
+  allPieOnlineOrderCount: number; //实体商品——全部指派订单数量
+  pendPieOnlineOrderCount: number; //实体商品——待指派订单数量
+  todayPieOnlineOrderCount: number; //实体商品——今日指派订单数量
+}
+
+// 分账详情
+export interface RevenueSharingDetailModel {
+  id: number; //	主键ID
+  orderNo: string; //订单ID
+  payTime: string; //付款时间
+  profitSharingTimeStart:string;//分账时间开始
+  profitSharingTimeEnd:string;//分账时间结束
+  finishTime:string;//收货/核销/完成时间
+  profitSharingTime:string;//分账时间
+  conditioningProgramName: string; //调理方案名称
+  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-分账异常
+  
+
+}
+export type RevenueSharingDetailQuery = Partial<RevenueSharingDetailModel>;

+ 8 - 0
src/model/people.model.ts

@@ -4,6 +4,10 @@ export interface PeopleModel {
   avatar?: string;
   gender?: '0' | '1';
   age?: number;
+  dept?:{
+    deptId: string;
+    deptName: string;
+  };
 }
 
 export interface PatientTagModel {
@@ -16,5 +20,9 @@ export function transformAccount(data: any): PeopleModel {
     id: data?.userId,
     name: data?.nickName,
     avatar: data?.avatar,
+    dept: {
+      deptId: data?.dept?.deptId,
+      deptName: data?.dept?.deptName,
+    },
   };
 }

+ 626 - 0
src/order/DateTimePicker.vue

@@ -0,0 +1,626 @@
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue';
+import { message } from 'ant-design-vue';
+import dayjs, { type Dayjs } from 'dayjs';
+
+defineOptions({
+  name: 'DateTimePicker',
+});
+
+interface DateOption {
+  label: string;
+  date: Dayjs;
+  dateStr: string;
+  displayDate: string;
+  isToday: boolean;
+}
+
+interface Props {
+  visible: boolean;
+  pickerType?: 'date' | 'time';
+  initialDate?: string | null;
+  initialTime?: string | null;
+  isFromTimeButton?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  pickerType: 'date',
+  initialDate: null,
+  initialTime: null,
+  isFromTimeButton: false,
+});
+
+const emit = defineEmits<{
+  'update:visible': [value: boolean];
+  'date-select': [date: Dayjs];
+  'time-select': [time: string, date: Dayjs];
+  'date-confirm': [date: Dayjs];
+  'close': [];
+}>();
+
+const internalPickerType = ref<'date' | 'time'>(props.pickerType);
+const selectedDate = ref<Dayjs>(dayjs());
+const selectedTime = ref<string>('');
+// 起始日期
+const startDate = ref<Dayjs>(dayjs());
+
+const dateUpdateKey = ref(0);
+const datePickerVisible = ref(false);
+const datePickerValue = ref<Dayjs | undefined>(undefined);
+
+// 初始化选中日期
+function initializeSelectedDate() {
+  if (props.initialDate) {
+    const initialDate = dayjs(props.initialDate);
+    selectedDate.value = initialDate;
+    startDate.value = initialDate; 
+  } else {
+    const today = dayjs();
+    selectedDate.value = today; // 默认今天
+    startDate.value = today; 
+  }
+}
+
+// 初始化选中时间
+function initializeSelectedTime() {
+  selectedTime.value = props.initialTime || '';
+}
+
+
+watch(() => props.visible, (newVal) => {
+  if (newVal) {
+    internalPickerType.value = props.pickerType;
+    initializeSelectedDate();
+    initializeSelectedTime();
+  }
+});
+
+
+watch(() => props.initialDate, () => {
+  if (props.visible) {
+    initializeSelectedDate();
+  }
+}, { immediate: true });
+
+watch(() => props.pickerType, (newVal) => {
+  internalPickerType.value = newVal;
+});
+
+// 获取5天的日期列表
+const dateOptions = computed(() => {
+  const dates: DateOption[] = [];
+  const today = dayjs();
+  const tomorrow = today.add(1, 'day');
+  const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
+  const start = dayjs(startDate.value);
+
+  for (let i = 0; i < 5; i++) {
+    const date = start.clone().add(i, 'day');
+    const dayOfWeek = date.day();
+    const isToday = date.isSame(today, 'day');
+    const isTomorrow = date.isSame(tomorrow, 'day');
+
+    let label = '';
+    if (isToday) {
+      label = '今天';
+    } else if (isTomorrow) {
+      label = '明天';
+    } else {
+      label = weekdays[dayOfWeek];
+    }
+
+    dates.push({
+      label,
+      date: date.clone(), 
+      dateStr: date.format('YYYY-MM-DD'),
+      displayDate: date.format('M月D日'),
+      isToday: isToday,
+    });
+  }
+
+  return dates;
+});
+
+
+const timeOptions = computed(() => {
+  const times: string[] = [];
+  for (let hour = 8; hour <= 20; hour++) {
+    times.push(`${hour.toString().padStart(2, '0')}:00`);
+    if (hour < 20) {
+      times.push(`${hour.toString().padStart(2, '0')}:30`);
+    }
+  }
+  return times;
+});
+
+// 判断某个时间是否被禁用
+function isTimeDisabled(time: string): boolean {
+  if (!selectedDate.value) return false;
+  
+  const now = dayjs();
+  const selectedDateStr = selectedDate.value.format('YYYY-MM-DD');
+  const todayStr = now.format('YYYY-MM-DD');
+  
+  // 如果选择的日期是今天,禁用当前时间之前的时间点
+  if (selectedDateStr === todayStr) {
+    const [hour, minute] = time.split(':').map(Number);
+    const timeDate = now.hour(hour).minute(minute).second(0).millisecond(0);
+    // 如果时间早于当前时间,禁用
+    return timeDate.isBefore(now);
+  }
+  
+  // 如果选择的日期是过去的日期,禁用
+  if (selectedDate.value.isBefore(now, 'day')) {
+    return true;
+  }
+  
+  return false;
+}
+
+
+function handleDateSelect(dateItem: DateOption) {
+  selectedDate.value = dateItem.date;
+  const selectedDateStr = dateItem.dateStr;
+  const currentStartStr = startDate.value.format('YYYY-MM-DD');
+  const currentEnd = startDate.value.add(4, 'day');
+  const currentEndStr = currentEnd.format('YYYY-MM-DD');
+  
+
+  if (selectedDateStr < currentStartStr || selectedDateStr > currentEndStr) {
+    startDate.value = dateItem.date;
+    dateUpdateKey.value += 1;
+  }
+}
+
+// 打开日期选择弹窗
+function handleOpenDatePicker() {
+  datePickerValue.value = selectedDate.value.clone();
+  datePickerVisible.value = true;
+}
+
+// 日期选择
+function handleDatePickerChange(value: string | Dayjs | null) {
+  if (value) {
+    const date = dayjs.isDayjs(value) ? value : dayjs(value);
+    datePickerValue.value = date;
+  } else {
+    datePickerValue.value = undefined;
+  }
+}
+
+// 确认日期选择
+function handleConfirmDatePicker() {
+  if (!datePickerValue.value) {
+    message.warning('请先选择日期');
+    return;
+  }
+  
+  // 获取选中的日期值
+  const selectedDateValue = datePickerValue.value;
+  
+  let dateStr: string;
+  if (dayjs.isDayjs(selectedDateValue)) {
+    dateStr = selectedDateValue.format('YYYY-MM-DD');
+  } else if (typeof selectedDateValue === 'string') {
+    dateStr = selectedDateValue;
+  } else {
+    dateStr = dayjs(selectedDateValue).format('YYYY-MM-DD');
+  }
+  
+  // 创建新的日期对象
+  const newDate = dayjs(dateStr);
+  
+  startDate.value = newDate.clone();
+  
+
+  selectedDate.value = newDate.clone();
+
+  dateUpdateKey.value += 1;
+  
+  datePickerVisible.value = false;
+  
+  emit('date-select', newDate.clone());
+  
+  if (props.isFromTimeButton) {
+    internalPickerType.value = 'time';
+  }
+}
+
+// 关闭日期选择弹窗
+function handleCloseDatePicker() {
+  datePickerVisible.value = false;
+}
+
+// 处理时间选择
+function handleTimeSelect(time: string) {
+  if (isTimeDisabled(time)) {
+    return;
+  }
+  selectedTime.value = time;
+}
+
+// 确认时间选择
+function handleConfirmTimeSelect() {
+  if (!selectedDate.value) {
+    message.warning('请先选择日期');
+    return;
+  }
+  
+  if (!selectedTime.value) {
+    message.warning('请先选择时间');
+    return;
+  }
+  
+  emit('date-select', selectedDate.value.clone());
+  
+  emit('time-select', selectedTime.value, selectedDate.value);
+  handleClose();
+}
+
+// 关闭弹窗
+function handleClose() {
+  emit('update:visible', false);
+  emit('close');
+}
+</script>
+
+<template>
+  <a-modal 
+    :open="visible" 
+    title="请选择开始服务时间" 
+    :width="internalPickerType === 'date' ? 600 : 800"
+    @cancel="handleClose" 
+    @ok="handleConfirmTimeSelect"
+    class="date-time-picker-modal">
+    <div class="date-time-picker-content">
+      <!-- 时间选择区域 -->
+      <div class="time-selection-section">
+        <div class="date-header">
+          <div class="date-options-header" :key="`${startDate.format('YYYY-MM-DD')}-${dateUpdateKey}`">
+            <div v-for="dateItem in dateOptions" :key="dateItem.dateStr" class="date-option-header"
+              :class="{ active: selectedDate.format('YYYY-MM-DD') === dateItem.dateStr }"
+              @click="handleDateSelect(dateItem)">
+              <div class="date-label">{{ dateItem.label }}</div>
+              <div class="date-value">{{ dateItem.displayDate }}</div>
+            </div>
+          </div>
+          <div class="calendar-icon" @click="handleOpenDatePicker">📅</div>
+        </div>
+        <div class="time-grid">
+          <template v-if="timeOptions && timeOptions.length > 0">
+            <div v-for="time in timeOptions" :key="time" class="time-slot" 
+              :class="{ 
+                active: selectedTime === time,
+                disabled: isTimeDisabled(time)
+              }"
+              @click="handleTimeSelect(time)">
+              {{ time }}
+            </div>
+          </template>
+          <div v-else class="time-grid-empty">暂无时间选项</div>
+        </div>
+      </div>
+    </div>
+  </a-modal>
+
+  <!-- 日期选择弹窗 -->
+  <a-modal
+    :open="datePickerVisible"
+    title="请选择日期"
+    :width="400"
+    @cancel="handleCloseDatePicker"
+    @ok="handleConfirmDatePicker"
+    class="date-picker-modal">
+    <div class="date-picker-content">
+      <a-date-picker
+        v-model:value="datePickerValue"
+        format="YYYY-MM-DD"
+        placeholder="请选择日期"
+        style="width: 100%"
+        @change="handleDatePickerChange" />
+    </div>
+  </a-modal>
+</template>
+
+<style scoped lang="scss">
+.date-time-picker-content {
+  .date-selection-section {
+    .date-options {
+      display: flex;
+      gap: 12px;
+      margin-bottom: 24px;
+      justify-content: center;
+
+      .date-option {
+        flex: 1;
+        padding: 16px 12px;
+        border: 1px solid #e8e8e8;
+        border-radius: 4px;
+        text-align: center;
+        cursor: pointer;
+        transition: all 0.3s;
+        background: #fff;
+
+        .date-label {
+          font-size: 14px;
+          color: #333;
+          margin-bottom: 4px;
+          font-weight: 500;
+        }
+
+        .date-value {
+          font-size: 13px;
+          color: #666;
+        }
+
+        &:hover {
+          border-color: #8bc34a;
+          background: #f9fff9;
+        }
+
+        &.active {
+          background: #8bc34a;
+          border-color: #8bc34a;
+
+          .date-label,
+          .date-value {
+            color: #fff;
+          }
+        }
+      }
+    }
+
+    .picker-actions {
+      display: flex;
+      justify-content: flex-end;
+      gap: 12px;
+      padding-top: 16px;
+      border-top: 1px solid #f0f0f0;
+    }
+  }
+
+  .time-selection-section {
+    .date-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 20px;
+      padding-bottom: 16px;
+      border-bottom: 1px solid #f0f0f0;
+
+      .date-options-header {
+        display: flex;
+        gap: 12px;
+        flex: 1;
+
+        .date-option-header {
+          flex: 1;
+          padding: 12px 8px;
+          border: 1px solid #e8e8e8;
+          border-radius: 4px;
+          text-align: center;
+          cursor: pointer;
+          transition: all 0.3s;
+          background: #fff;
+
+          .date-label {
+            font-size: 13px;
+            color: #333;
+            margin-bottom: 2px;
+            font-weight: 500;
+          }
+
+          .date-value {
+            font-size: 12px;
+            color: #666;
+          }
+
+          &:hover {
+            border-color: #8bc34a;
+            background: #f9fff9;
+          }
+
+          &.active {
+            background: #8bc34a;
+            border-color: #8bc34a;
+
+            .date-label,
+            .date-value {
+              color: #fff;
+            }
+          }
+        }
+      }
+
+      .calendar-icon {
+        font-size: 20px;
+        margin-left: 16px;
+        cursor: pointer;
+        transition: transform 0.2s;
+
+        &:hover {
+          transform: scale(1.1);
+        }
+      }
+    }
+
+    .time-grid {
+      display: grid;
+      grid-template-columns: repeat(5, 1fr);
+      gap: 8px;
+      max-height: 400px;
+      overflow-y: auto;
+      padding-right: 8px;
+      min-height: 200px;
+
+      &::-webkit-scrollbar {
+        width: 6px;
+      }
+
+      &::-webkit-scrollbar-thumb {
+        background: #d9d9d9;
+        border-radius: 3px;
+      }
+
+      &::-webkit-scrollbar-thumb:hover {
+        background: #bfbfbf;
+      }
+
+      .time-slot {
+        padding: 12px 8px;
+        border: 1px solid #e8e8e8;
+        border-radius: 4px;
+        text-align: center;
+        font-size: 14px;
+        color: #333;
+        cursor: pointer;
+        transition: all 0.3s;
+        background: #fff;
+        min-height: 40px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        box-sizing: border-box;
+
+        &:hover:not(.disabled) {
+          border-color: #8bc34a;
+          background: #f9fff9;
+        }
+
+        &.active {
+          background: #8bc34a;
+          border-color: #8bc34a;
+          color: #fff;
+        }
+
+        &.disabled {
+          opacity: 0.4;
+          cursor: not-allowed;
+          background: #f5f5f5;
+          color: #999;
+          border-color: #e8e8e8;
+
+          &:hover {
+            border-color: #e8e8e8;
+            background: #f5f5f5;
+          }
+        }
+      }
+
+      .time-grid-empty {
+        grid-column: 1 / -1;
+        text-align: center;
+        padding: 40px;
+        color: #999;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.date-time-picker-modal {
+  .ant-modal-header {
+    padding: 16px 24px;
+    border-bottom: 1px solid #f0f0f0;
+
+    .ant-modal-title {
+      font-size: 16px;
+      font-weight: 500;
+      text-align: center;
+      position: relative;
+    }
+  }
+
+  .ant-modal-body {
+    padding: 24px;
+  }
+
+  .date-time-picker-content {
+    .time-selection-section {
+      .time-grid {
+        display: grid !important;
+        grid-template-columns: repeat(5, 1fr) !important;
+        gap: 8px !important;
+        max-height: 400px;
+        overflow-y: auto;
+        padding-right: 8px;
+        min-height: 200px;
+
+        &::-webkit-scrollbar {
+          width: 6px;
+        }
+
+        &::-webkit-scrollbar-thumb {
+          background: #d9d9d9;
+          border-radius: 3px;
+        }
+
+        &::-webkit-scrollbar-thumb:hover {
+          background: #bfbfbf;
+        }
+
+        .time-slot {
+          padding: 12px 8px !important;
+          border: 1px solid #e8e8e8 !important;
+          border-radius: 4px !important;
+          text-align: center !important;
+          font-size: 14px !important;
+          color: #333 !important;
+          cursor: pointer !important;
+          transition: all 0.3s;
+          background: #fff !important;
+          min-height: 40px;
+          display: flex !important;
+          align-items: center !important;
+          justify-content: center !important;
+          box-sizing: border-box;
+
+          &:hover:not(.disabled) {
+            // border-color: #8bc34a !important;
+            // background: #f9fff9 !important;
+          }
+
+          &.active {
+            background: #8bc34a !important;
+            border-color: #8bc34a !important;
+            color: #fff !important;
+          }
+
+          &.disabled {
+            opacity: 0.4 !important;
+            cursor: not-allowed !important;
+            background: #f5f5f5 !important;
+            color: #999 !important;
+            border-color: #e8e8e8 !important;
+
+            &:hover {
+              border-color: #e8e8e8 !important;
+              background: #f5f5f5 !important;
+            }
+          }
+        }
+
+        .time-grid-empty {
+          grid-column: 1 / -1;
+          text-align: center;
+          padding: 40px;
+          color: #999;
+        }
+      }
+    }
+  }
+}
+
+.date-picker-content {
+  padding: 20px 0;
+}
+</style>
+
+<style lang="scss">
+.date-picker-modal {
+  .ant-modal-body {
+    padding: 24px;
+  }
+}
+</style>

+ 1838 - 0
src/order/DispatchOrderPanel.vue

@@ -0,0 +1,1838 @@
+<script setup lang="ts">
+import { ref, computed, watch, reactive, onMounted } from 'vue';
+import { message } from 'ant-design-vue';
+import dayjs, { type Dayjs } from 'dayjs';
+import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
+import {
+  todayOrderMethod, pendingOrderMethod, allPieOrderMethod, todayPhysicalOrderMethod, pendingPhysicalOrderMethod, allPhysicalOrderMethod,
+  getOrderLiaisonListMethod, confirmPieOrderMethod, confirmPhysicalOrderMethod, getPieOrderCountMethod
+} from '@/request/api/order.api';
+import type { OrderModel, OrderQuery, OrderLiaisonListModel } from '@/model/order.model';
+import DateTimePicker from './DateTimePicker.vue';
+
+defineOptions({
+  name: 'DispatchOrderPanel',
+});
+
+interface Institution extends OrderLiaisonListModel {
+  address: string;
+}
+
+const props = defineProps<{
+  orderType: 'offline' | 'physical'; // 线下服务 | 实体商品
+}>();
+
+// 当前选中的子tab
+const activeSubTab = ref<'pending' | 'today' | 'all'>('pending');
+
+// 订单列表数据
+const orderList = ref<OrderModel[]>([]);
+// 可派单机构列表
+const institutionList = ref<Institution[]>([]);
+// 当前选中的订单
+const selectedOrderId = ref<number | null>(null);
+
+// 订单计数
+const orderCounts = ref({
+  offlineCount: 0, // 线下服务订单总和
+  allPieOfflineOrderCount: 0, // 线下服务——全部指派订单数量
+  pendPieOfflineOrderCount: 0, // 线下服务——待指派订单数量
+  todayPieOfflineOrderCount: 0, // 线下服务——今日指派订单数量
+  onlineCount: 0, // 实体商品数量订单总和
+  allPieOnlineOrderCount: 0, // 实体商品——全部指派订单数量
+  pendPieOnlineOrderCount: 0, // 实体商品——待指派订单数量
+  todayPieOnlineOrderCount: 0, // 实体商品——今日指派订单数量
+});
+
+// 当前订单的分配机构输入值
+const assignedInstitutionMap = ref<Record<number, string>>({});
+// 记录是否从右边列表选择指派(用于控制按钮显示)
+const assignedFromRightList = ref<Record<number, boolean>>({});
+
+// 日期时间选择弹窗相关
+const dateTimePickerVisible = ref(false);
+const currentPickerOrderId = ref<number | null>(null);
+const pickerType = ref<'date' | 'time'>('date'); // 选择器类型:日期或时间
+const isFromTimeButton = ref(false); // 是否从时间按钮进入
+
+// 加载状态
+const loading = ref(false);
+const institutionLoading = ref(false);
+
+// 订单列表分页配置
+const orderPageConfig = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+// 机构列表分页配置
+const institutionPageConfig = reactive({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+// 搜索条件
+const searchForm = reactive({
+  customerName: '',
+  contactPhone: '',
+  orderTypes: [] as string[], // 线下服务: 'booked' | 'verified' | 实体商品: 'pendingShip' | 'shipped'
+});
+
+// 获取可派单机构名称列表
+const availableInstitutionNames = computed(() => {
+  return allInstitutionData.value.map(inst => inst.name);
+});
+
+// 获取订单的完整地址
+function getOrderAddress(order: OrderModel): string {
+  const addressParts = [
+    order.provinceName,
+    order.cityName,
+    order.areaName,
+    order.detailAddress,
+  ].filter(Boolean);
+  return addressParts.join('');
+}
+
+function formatCustomerInfo(order: OrderModel): string {
+  const parts: string[] = [];
+  if (order.liaison) {
+    parts.push(order.liaison);
+  }
+  if (order.phone) {
+    parts.push(order.phone);
+  }
+  const address = getOrderAddress(order);
+  if (address) {
+    parts.push(address);
+  }
+  return parts.join(',');
+}
+
+// 计算结束时间
+function calculateEndTime(order: OrderModel): string | null {
+  if (!order.arrangeTime) return null;
+
+  // 如果没有服务时长,无法计算结束时间
+  if (!order.offlineDuration) return null;
+
+  // 根据开始时间和服务时长计算结束时间
+  const durationMinutes = parseDurationToMinutes(order.offlineDuration);
+  if (durationMinutes > 0) {
+    const start = dayjs(order.arrangeTime, 'HH:mm');
+    const end = start.add(durationMinutes, 'minute');
+    return end.format('HH:mm');
+  }
+
+  return null;
+}
+
+// 判断订单是否已核销(线下服务)
+function isOrderVerified(order: OrderModel): boolean {
+  return props.orderType === 'offline' && order.type === 3;
+}
+
+// 判断订单是否已发货(实体商品)
+function isOrderShipped(order: OrderModel): boolean {
+  return props.orderType === 'physical' && order.type === 3;
+}
+
+// 右侧机构表格配置
+const gridRef = ref<VxeGridInstance<Institution>>();
+const gridOptions = reactive<VxeGridProps<Institution>>({
+  border: true,
+  autoResize: true,
+  height: '100%',
+  scrollY: { enabled: true },
+  columns: [
+    {
+      field: 'action',
+      title: '操作',
+      width: 150,
+      align: 'center',
+      showOverflow: false,
+      cellRender: {
+        name: 'VxeButton',
+        props: {
+          type: 'primary',
+          size: 'large',
+          content: '指派',
+        },
+        events: {
+          click({ row }: { row: Institution }) {
+            handleAssignInstitution(row.name);
+          },
+        },
+      },
+    },
+    { field: 'name', title: '机构名称' },
+    { field: 'detailAddress', title: '地址' },
+    { field: 'phone', title: '联系电话' },
+    { field: 'todayOrderQuantity', title: '当日订单数' },
+  ],
+  data: [],
+  pagerConfig: {
+    enabled: false,
+  },
+});
+
+// 初始化
+onMounted(async () => {
+  await loadOrderCounts();
+  await loadOrderList();
+  if (orderList.value.length > 0) {
+    selectedOrderId.value = getOrderId(orderList.value[0]);
+    if (activeSubTab.value !== 'pending') {
+      orderList.value.forEach(order => {
+        if (order.conditioningProgramSupplierName) {
+          const orderId = getOrderId(order);
+          assignedInstitutionMap.value[orderId] = order.conditioningProgramSupplierName;
+        }
+      });
+    }
+    // 选中订单后再加载机构列表
+    await loadInstitutionList();
+  }
+});
+
+// 监听子tab切换
+watch(activeSubTab, async () => {
+  orderPageConfig.currentPage = 1;
+  allInstitutionData.value = [];
+  institutionList.value = [];
+  gridRef.value?.loadData([]);
+  // 切换tab时,清空所有按钮显示标记,这样按钮就不会显示
+  assignedFromRightList.value = {};
+  await loadOrderCounts();
+  await loadOrderList();
+  if (orderList.value.length > 0) {
+    selectedOrderId.value = getOrderId(orderList.value[0]);
+    if (activeSubTab.value !== 'pending') {
+      orderList.value.forEach(order => {
+        if (order.conditioningProgramSupplierName) {
+          const orderId = getOrderId(order);
+          assignedInstitutionMap.value[orderId] = order.conditioningProgramSupplierName;
+        }
+      });
+    } else {
+      orderList.value.forEach(order => {
+        const orderId = getOrderId(order);
+        assignedInstitutionMap.value[orderId] = '';
+        assignedFromRightList.value[orderId] = false;
+      });
+    }
+    await loadInstitutionList();
+  } else {
+    selectedOrderId.value = null;
+    allInstitutionData.value = [];
+    institutionList.value = [];
+    gridRef.value?.loadData([]);
+  }
+});
+
+// 监听订单类型切换
+watch(() => props.orderType, async () => {
+  activeSubTab.value = 'pending';
+  selectedOrderId.value = null;
+  assignedInstitutionMap.value = {};
+  // 切换订单类型时,清空所有按钮显示标记
+  assignedFromRightList.value = {};
+  orderPageConfig.currentPage = 1;
+  searchForm.customerName = '';
+  searchForm.contactPhone = '';
+  searchForm.orderTypes = [];
+  allInstitutionData.value = [];
+  institutionList.value = [];
+  gridRef.value?.loadData([]);
+
+  await loadOrderCounts();
+  await loadOrderList();
+
+  if (orderList.value.length > 0) {
+    const firstOrder = orderList.value[0];
+    const orderId = firstOrder.id ?? firstOrder.patientConditioningProgramId;
+    selectedOrderId.value = orderId;
+    const verifyOrder = orderList.value.find(order => {
+      const orderId = order.id ?? order.patientConditioningProgramId;
+      return orderId === selectedOrderId.value;
+    });
+    if (!verifyOrder) {
+      console.error('错误:选中的订单ID不在订单列表中!', {
+        selectedOrderId: selectedOrderId.value,
+        orderListIds: orderList.value.map(o => o.id ?? o.patientConditioningProgramId),
+      });
+    }
+
+    if (activeSubTab.value !== 'pending') {
+      orderList.value.forEach(order => {
+        if (order.conditioningProgramSupplierName) {
+          const orderId = getOrderId(order);
+          assignedInstitutionMap.value[orderId] = order.conditioningProgramSupplierName;
+        }
+      });
+    } else {
+      // 待指派订单tab:清空所有已分配的机构
+      orderList.value.forEach(order => {
+        const orderId = getOrderId(order);
+        assignedInstitutionMap.value[orderId] = '';
+        assignedFromRightList.value[orderId] = false;
+      });
+    }
+    // 选中订单后必须加载机构列表(切换订单类型时)
+    await loadInstitutionList();
+  } else {
+    // 如果没有订单,清空机构列表
+    allInstitutionData.value = [];
+    institutionPageConfig.total = 0;
+    institutionList.value = [];
+    gridRef.value?.loadData([]);
+  }
+}, { immediate: false });
+
+
+function buildOrderQuery(): OrderQuery | undefined {
+  if (activeSubTab.value !== 'all') {
+    return undefined;
+  }
+
+  const query: Partial<OrderQuery> = {};
+
+  // 联系人(客户姓名)
+  if (searchForm.customerName) {
+    query.liaison = searchForm.customerName;
+  }
+
+  // 电话
+  if (searchForm.contactPhone) {
+    query.phone = searchForm.contactPhone;
+  }
+
+  // 订单类型转换
+  if (searchForm.orderTypes && searchForm.orderTypes.length > 0) {
+    const types: string[] = [];
+    if (props.orderType === 'offline') {
+      // 线下服务
+      if (searchForm.orderTypes.includes('booked')) {
+        types.push('2'); // 已派单但未核销
+      }
+      if (searchForm.orderTypes.includes('verified')) {
+        types.push('3'); // 已核销
+      }
+    } else if (props.orderType === 'physical') {
+      // 实体商品
+      if (searchForm.orderTypes.includes('pendingShip')) {
+        types.push('2'); // 已派单但未核销(待发货)
+      }
+      if (searchForm.orderTypes.includes('shipped')) {
+        types.push('3'); // 已核销(已发货)
+      }
+    }
+    if (types.length > 0) {
+      query.types = types;
+    }
+  }
+
+  return Object.keys(query).length > 0 ? (query as OrderQuery) : undefined;
+}
+
+// 加载订单列表
+async function loadOrderList() {
+  loading.value = true;
+  try {
+    let response;
+
+    if (props.orderType === 'offline') {
+      // 线下服务接口
+      if (activeSubTab.value === 'pending') {
+        // 待指派订单
+        response = await pendingOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize);
+      } else if (activeSubTab.value === 'today') {
+        // 今日指派订单
+        response = await todayOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize);
+      } else if (activeSubTab.value === 'all') {
+        // 全部指派订单
+        const query = buildOrderQuery();
+        response = await allPieOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize, query);
+      } else {
+        return;
+      }
+    } else {
+      // 实体商品接口
+      if (activeSubTab.value === 'pending') {
+        // 待指派订单
+        response = await pendingPhysicalOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize);
+      } else if (activeSubTab.value === 'today') {
+        // 今日指派订单
+        response = await todayPhysicalOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize);
+      } else if (activeSubTab.value === 'all') {
+        // 全部指派订单
+        const query = buildOrderQuery();
+        response = await allPhysicalOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize, query);
+      } else {
+        return;
+      }
+    }
+
+    orderList.value = response.data;
+    orderPageConfig.total = response.total;
+  } catch (error) {
+    orderList.value = [];
+    orderPageConfig.total = 0;
+  } finally {
+    loading.value = false;
+  }
+}
+
+const allInstitutionData = ref<Institution[]>([]);
+function getInstitutionAddress(institution: OrderLiaisonListModel): string {
+  const addressParts = [
+    institution.cityName,
+    institution.areaName,
+    institution.detailAddress,
+  ].filter(Boolean);
+  return addressParts.join('');
+}
+
+function transformToInstitution(item: OrderLiaisonListModel): Institution {
+  return {
+    ...item,
+    address: getInstitutionAddress(item),
+  };
+}
+
+// 加载可派单机构列表
+async function loadInstitutionList() {
+  institutionLoading.value = true;
+  allInstitutionData.value = [];
+  institutionList.value = [];
+  gridRef.value?.loadData([]);
+
+  try {
+    const selectedOrder = selectedOrderId.value
+      ? orderList.value.find(order => {
+        const orderId = order.id ?? order.patientConditioningProgramId;
+        return orderId === selectedOrderId.value;
+      })
+      : null;
+
+    if (!selectedOrder) {
+      allInstitutionData.value = [];
+      institutionPageConfig.total = 0;
+      institutionList.value = [];
+      gridRef.value?.loadData([]);
+      return;
+    }
+
+    if (selectedOrder.institutionId == null || selectedOrder.conditioningProgramType == null) {
+      allInstitutionData.value = [];
+      institutionPageConfig.total = 0;
+      institutionList.value = [];
+      gridRef.value?.loadData([]);
+      return;
+    }
+
+    if (props.orderType === 'offline') {
+      if (!selectedOrder.arrangeDate || !selectedOrder.arrangeTime) {
+        allInstitutionData.value = [];
+        institutionPageConfig.total = 0;
+        institutionList.value = [];
+        gridRef.value?.loadData([]);
+        // 提示用户需要先选择日期和时间(仅线下服务)
+        if (!selectedOrder.arrangeDate && !selectedOrder.arrangeTime) {
+          message.error('请先选择日期和时间');
+        } else if (!selectedOrder.arrangeDate) {
+          message.error('请先选择日期');
+        } else if (!selectedOrder.arrangeTime) {
+          message.error('请先选择时间');
+        }
+        return;
+      }
+    }
+
+    const data: Partial<any> = {
+      institutionId: selectedOrder.institutionId,
+      conditioningProgramType: selectedOrder.conditioningProgramType,
+      ...(props.orderType === 'offline' && selectedOrder.arrangeDate && selectedOrder.arrangeTime
+        ? { timeStart: `${selectedOrder.arrangeDate} ${selectedOrder.arrangeTime}` }
+        : {}),
+      // 线下服务 type=2,实体商品 type=1
+      type: props.orderType === 'offline' ? 2 : 1
+    };
+
+    // 获取可派单机构列表
+    const response = await getOrderLiaisonListMethod(data);
+    const responseData = Array.isArray(response) ? response : (response?.data || []);
+    const transformedData = responseData.map(transformToInstitution);
+
+    allInstitutionData.value = transformedData;
+
+
+    institutionList.value = transformedData;
+
+    // 更新表格数据
+    gridRef.value?.loadData(institutionList.value);
+
+  } catch (error) {
+    allInstitutionData.value = [];
+    institutionPageConfig.total = 0;
+    institutionList.value = [];
+  } finally {
+    institutionLoading.value = false;
+  }
+}
+
+
+// 处理日期按钮点击
+function handleDateBtnClick(orderId: number) {
+  const order = orderList.value.find(o => getOrderId(o) === orderId);
+  if (!order) return;
+
+  if (selectedOrderId.value !== orderId) {
+    selectedOrderId.value = orderId;
+    loadInstitutionList();
+  }
+
+  currentPickerOrderId.value = orderId;
+  pickerType.value = 'date';
+  isFromTimeButton.value = false;
+  dateTimePickerVisible.value = true;
+}
+
+// 处理时间按钮点击
+function handleTimeBtnClick(orderId: number) {
+  const order = orderList.value.find(o => getOrderId(o) === orderId);
+  if (!order) return;
+
+  if (selectedOrderId.value !== orderId) {
+    selectedOrderId.value = orderId;
+    loadInstitutionList();
+  }
+
+  currentPickerOrderId.value = orderId;
+  isFromTimeButton.value = true;
+
+  if (order.arrangeDate) {
+    pickerType.value = 'time';
+  } else {
+    pickerType.value = 'date'; // 先选择日期
+  }
+
+  dateTimePickerVisible.value = true;
+}
+
+// 处理日期时间选择器的事件
+function handleDateTimeDateSelect(date: Dayjs) {
+  if (currentPickerOrderId.value !== null) {
+    handleDateChange(currentPickerOrderId.value, date);
+
+
+    if (isFromTimeButton.value) {
+      pickerType.value = 'time';
+    }
+  }
+}
+
+function handleDateTimeTimeSelect(time: string, date: Dayjs) {
+  if (currentPickerOrderId.value !== null) {
+    const order = orderList.value.find(o => getOrderId(o) === currentPickerOrderId.value);
+    if (order) {
+      if (!order.arrangeDate) {
+        order.arrangeDate = date.format('YYYY-MM-DD');
+      }
+      // 更新时间
+      handleTimeChange(currentPickerOrderId.value, dayjs(time, 'HH:mm'), order);
+    }
+  }
+}
+
+function handleDateTimeDateConfirm(date: Dayjs) {
+  handleDateTimeDateSelect(date);
+}
+
+function handleDateTimeClose() {
+  dateTimePickerVisible.value = false;
+  currentPickerOrderId.value = null;
+  isFromTimeButton.value = false;
+}
+
+// 处理日期修改
+function handleDateChange(orderId: number, date: Dayjs | null) {
+  if (!date) return;
+
+  // 更新订单的日期
+  const orderIndex = orderList.value.findIndex(o => getOrderId(o) === orderId);
+  if (orderIndex !== -1) {
+    const order = orderList.value[orderIndex];
+    const newDateStr = date.format('YYYY-MM-DD');
+
+    // 如果日期相同,不需要更新
+    if (order.arrangeDate !== newDateStr) {
+      order.arrangeDate = newDateStr;
+
+      orderList.value = [...orderList.value];
+    }
+  }
+
+  // 清空分配机构
+  assignedInstitutionMap.value[orderId] = '';
+  assignedFromRightList.value[orderId] = false;
+
+  loadInstitutionList();
+}
+
+
+function handleTimeChange(orderId: number, startTime: any, order: OrderModel) {
+  if (!startTime) return;
+
+  // 更新订单的开始时间
+  const targetOrder = orderList.value.find(o => getOrderId(o) === orderId);
+  if (!targetOrder) return;
+
+  const durationSource = targetOrder.offlineDuration || order.offlineDuration;
+
+  const start = dayjs.isDayjs(startTime) ? startTime : dayjs(startTime);
+  targetOrder.arrangeTime = start.format('HH:mm');
+
+  if (durationSource) {
+    const durationMinutes = parseDurationToMinutes(durationSource);
+    if (durationMinutes > 0) {
+      const end = start.add(durationMinutes, 'minute');
+      targetOrder.arrangeEndTime = end.format('HH:mm');
+    }
+  }
+
+  // 清空分配机构
+  assignedInstitutionMap.value[orderId] = '';
+  assignedFromRightList.value[orderId] = false;
+
+  // 刷新可派单机构列表
+  loadInstitutionList();
+}
+
+// 解析服务时长为分钟数
+function parseDurationToMinutes(duration: string): number {
+  if (!duration) return 0;
+
+  const cleanDuration = duration.trim();
+
+  const numericValue = parseFloat(cleanDuration);
+  if (!isNaN(numericValue) && cleanDuration === numericValue.toString()) {
+    return numericValue;
+  }
+
+  const minutesMatch = cleanDuration.match(/([\d.]+)\s*分钟/);
+  if (minutesMatch) {
+    return Math.round(parseFloat(minutesMatch[1]));
+  }
+
+  const hoursMatch = cleanDuration.match(/([\d.]+)\s*小时/);
+  if (hoursMatch) {
+    return Math.round(parseFloat(hoursMatch[1]) * 60);
+  }
+
+  const hoursMinutesMatch = cleanDuration.match(/([\d.]+)\s*小时\s*([\d.]+)\s*分钟/);
+  if (hoursMinutesMatch) {
+    const hours = parseFloat(hoursMinutesMatch[1]);
+    const minutes = parseFloat(hoursMinutesMatch[2]);
+    return Math.round(hours * 60 + minutes);
+  }
+
+  return 0;
+}
+
+function handleLastInstitutionClick(orderId: number, institutionName: string) {
+  if (!institutionName) return;
+
+  if (selectedOrderId.value !== orderId) {
+    selectedOrderId.value = orderId;
+    loadInstitutionList();
+  }
+
+  if (!availableInstitutionNames.value.includes(institutionName)) {
+    message.warning('供应商不可选择,且不填充此机构');
+    return;
+  }
+
+  assignedInstitutionMap.value[orderId] = institutionName;
+  // 标记为已填充(可以显示按钮)
+  assignedFromRightList.value[orderId] = true;
+}
+
+function handleSupplierClick(orderId: number, supplierName: string) {
+  if (!supplierName) return;
+
+  if (selectedOrderId.value !== orderId) {
+    selectedOrderId.value = orderId;
+    loadInstitutionList();
+  }
+
+  if (!availableInstitutionNames.value.includes(supplierName)) {
+    message.warning('供应商不可选择,且不填充此机构');
+    return;
+  }
+
+  assignedInstitutionMap.value[orderId] = supplierName;
+  // 标记为已填充(可以显示按钮)
+  assignedFromRightList.value[orderId] = true;
+}
+
+function handleAssignInstitution(institutionName: string) {
+  if (selectedOrderId.value !== null) {
+    const selectedOrder = orderList.value.find(order => {
+      const orderId = order.id ?? order.patientConditioningProgramId;
+      return orderId === selectedOrderId.value;
+    });
+
+    if (selectedOrder && (isOrderVerified(selectedOrder))) {
+      message.warning('已核销的订单不能指派');
+      return;
+    }
+    if (selectedOrder && (isOrderShipped(selectedOrder))) {
+      message.warning('已发货的订单不能指派');
+      return;
+    }
+
+    assignedInstitutionMap.value[selectedOrderId.value] = institutionName;
+    // 标记为从右边列表选择
+    assignedFromRightList.value[selectedOrderId.value] = true;
+  }
+}
+
+function validateAndGetInstitution(orderId: number) {
+  const institutionName = assignedInstitutionMap.value[orderId];
+  if (!institutionName) {
+    message.warning('请先选择分配机构');
+    return null;
+  }
+
+  const institution = allInstitutionData.value.find(inst => inst.name === institutionName);
+  if (!institution) {
+    message.error('未找到对应的供应商信息');
+    return null;
+  }
+
+  return { institutionName, conditioningProgramSupplierId: institution.id };
+}
+
+
+function validateAndGetOrder(orderId: number) {
+  const orderIndex = orderList.value.findIndex(order => getOrderId(order) === orderId);
+  if (orderIndex === -1) {
+    message.error('未找到对应的订单');
+    return null;
+  }
+
+  return { orderIndex, order: orderList.value[orderIndex] };
+}
+
+// 验证线下服务订单的必要信息
+function validateOfflineOrder(order: OrderModel) {
+  if (!order.id) {
+    message.error('订单缺少必要信息(id)');
+    return null;
+  }
+
+  if (!order.arrangeDate || !order.arrangeTime) {
+    message.error('请先选择日期和时间');
+    return null;
+  }
+
+  const endTime = order.arrangeEndTime || (order.offlineDuration ? calculateEndTime(order) : null);
+  if (!endTime) {
+    message.error('无法计算服务结束时间,请检查服务时长设置');
+    return null;
+  }
+
+  return {
+    id: order.id,
+    pieTimeStart: `${order.arrangeDate} ${order.arrangeTime}`,
+    pieTimeEnd: `${order.arrangeDate} ${endTime}`,
+  };
+}
+
+// 验证实体商品订单的必要信息
+function validatePhysicalOrder(order: OrderModel) {
+  if (!order.patientConditioningProgramId) {
+    message.error('订单缺少必要信息(patientConditioningProgramId)');
+    return null;
+  }
+
+  return {
+    patientConditioningProgramId: order.patientConditioningProgramId,
+  };
+}
+
+async function callConfirmOrderAPI(order: OrderModel, conditioningProgramSupplierId: number) {
+  if (props.orderType === 'offline') {
+    const offlineData = validateOfflineOrder(order);
+    if (!offlineData) return false;
+
+    await confirmPieOrderMethod({
+      id: offlineData.id,
+      conditioningProgramSupplierId,
+      pieTimeStart: offlineData.pieTimeStart,
+      pieTimeEnd: offlineData.pieTimeEnd,
+    });
+  } else {
+    const physicalData = validatePhysicalOrder(order);
+    if (!physicalData) return false;
+
+    await confirmPhysicalOrderMethod({
+      patientConditioningProgramId: physicalData.patientConditioningProgramId,
+      conditioningProgramSupplierId,
+    });
+  }
+  return true;
+}
+
+// 处理订单列表更新
+async function handleOrderListAfterAssign(orderId: number, orderIndex: number) {
+  if (activeSubTab.value !== 'pending') {
+    // 如果当前在"今日指派订单"或"全部指派订单"tab,订单保留但显示已指派状态
+    return;
+  }
+
+  // 如果当前在"待指派订单"tab,指派成功后从列表中移除该订单
+  orderList.value.splice(orderIndex, 1);
+
+  // 如果移除后列表为空,取消选中
+  if (orderList.value.length === 0) {
+    selectedOrderId.value = null;
+    return;
+  }
+
+  // 如果移除的是当前选中的订单,选中第一个订单
+  if (selectedOrderId.value === orderId) {
+    selectedOrderId.value = orderList.value[0] ? getOrderId(orderList.value[0]) : null;
+    await loadInstitutionList();
+  }
+}
+
+// 处理确认指派
+async function handleConfirmAssign(orderId: number) {
+  // 如果订单未选中,自动选中该订单并加载机构列表
+  if (selectedOrderId.value !== orderId) {
+    selectedOrderId.value = orderId;
+    await loadInstitutionList();
+  }
+
+  // 验证并获取机构信息
+  const institutionInfo = validateAndGetInstitution(orderId);
+  if (!institutionInfo) return;
+
+  // 验证并获取订单信息
+  const orderInfo = validateAndGetOrder(orderId);
+  if (!orderInfo) return;
+
+  const { orderIndex, order } = orderInfo;
+  const originalOrder = { ...order };
+
+  // 先更新本地数据
+  order.conditioningProgramSupplierName = institutionInfo.institutionName;
+  order.conditioningProgramSupplierId = institutionInfo.conditioningProgramSupplierId;
+
+  try {
+    const success = await callConfirmOrderAPI(order, institutionInfo.conditioningProgramSupplierId);
+    if (!success) {
+      // 验证失败
+      orderList.value[orderIndex] = originalOrder;
+      return;
+    }
+
+    // 处理订单列表更新
+    await handleOrderListAfterAssign(orderId, orderIndex);
+
+    // 清空分配机构输入框
+    assignedInstitutionMap.value[orderId] = '';
+    assignedFromRightList.value[orderId] = false;
+
+    // 静默更新订单计数(不显示loading,避免闪烁)
+    loadOrderCounts().catch(err => {
+      console.error('更新订单计数失败:', err);
+    });
+
+    message.success('派单成功');
+  } catch (error) {
+    //API调用失败,恢复原始数据
+    orderList.value[orderIndex] = originalOrder;
+  }
+}
+
+// 处理取消
+function handleCancel(orderId: number) {
+  // 只清空分配机构,保持按钮显示(不清空 assignedFromRightList)
+  // 使用 null 表示用户主动清空,这样在非 pending tab 中也不会显示原有的供应商名称
+  assignedInstitutionMap.value[orderId] = null as any;
+}
+
+
+function getOrderId(order: OrderModel): number {
+  return order.id ?? order.patientConditioningProgramId;
+}
+
+// 判断订单是否被选中
+function isOrderSelected(order: OrderModel): boolean {
+  const orderId = getOrderId(order);
+  const result = selectedOrderId.value === orderId;
+  return result;
+}
+
+// 获取分配机构显示值
+function getAssignedInstitutionDisplayValue(order: OrderModel): string {
+  const orderId = getOrderId(order);
+  // 检查 key 是否存在(使用 hasOwnProperty 或 in 操作符)
+  const hasKey = orderId in assignedInstitutionMap.value;
+  const assignedValue = assignedInstitutionMap.value[orderId];
+  
+  
+  if (hasKey) {
+    if (assignedValue === null) {
+      return '';
+    }
+    return assignedValue || '';
+  }
+  
+  if (activeSubTab.value !== 'pending') {
+    return order.conditioningProgramSupplierName || '';
+  }
+  
+  return '';
+}
+
+// 判断是否应该显示确认指派按钮
+function shouldShowConfirmAssignButton(order: OrderModel): boolean {
+  // 如果订单已核销或已发货,不显示
+  if (isOrderVerified(order) || isOrderShipped(order)) {
+    return false;
+  }
+
+  const orderId = getOrderId(order);
+  // 检查是否已标记为可显示按钮(从右边列表选择、上次供应商或下面供应商填充)
+  // 只要标记为可显示,就显示按钮(即使后来点击取消清空了输入框)
+  const canShowButton = assignedFromRightList.value[orderId];
+  return canShowButton === true;
+}
+
+// 处理订单选中
+function handleOrderSelect(order: OrderModel) {
+  const orderId = getOrderId(order);
+
+  if (selectedOrderId.value === orderId) {
+    return;
+  }
+
+  selectedOrderId.value = orderId;
+
+  loadInstitutionList();
+}
+
+// 处理查询
+async function handleSearch() {
+  // 重置到第一页
+  orderPageConfig.currentPage = 1;
+  await loadOrderList();
+}
+
+// 处理重置
+function handleReset() {
+  searchForm.customerName = '';
+  searchForm.contactPhone = '';
+  searchForm.orderTypes = [];
+  orderPageConfig.currentPage = 1;
+  loadOrderList();
+}
+
+// 处理订单列表分页变化
+function handleOrderPageChange({ page, pageSize }: { page?: number; pageSize?: number }) {
+  if (page !== undefined) {
+    orderPageConfig.currentPage = page;
+  }
+  if (pageSize !== undefined) {
+    orderPageConfig.pageSize = pageSize;
+  }
+  loadOrderList();
+}
+
+
+
+// 获取订单数量
+const pendingCount = computed(() => {
+  return props.orderType === 'offline'
+    ? orderCounts.value.pendPieOfflineOrderCount
+    : orderCounts.value.pendPieOnlineOrderCount;
+});
+const todayCount = computed(() => {
+  return props.orderType === 'offline'
+    ? orderCounts.value.todayPieOfflineOrderCount
+    : orderCounts.value.todayPieOnlineOrderCount;
+});
+const allCount = computed(() => {
+  return props.orderType === 'offline'
+    ? orderCounts.value.allPieOfflineOrderCount
+    : orderCounts.value.allPieOnlineOrderCount;
+});
+
+// 加载订单计数 
+async function loadOrderCounts() {
+  try {
+    const response: any = await getPieOrderCountMethod();
+    const data = response?.data ?? response;
+
+    orderCounts.value = {
+      // offlineCount: Number(data?.offlineCount) || 0, // 线下服务订单总和
+      offlineCount: Number(data?.pendPieOfflineOrderCount) || 0,
+      allPieOfflineOrderCount: Number(data?.allPieOfflineOrderCount) || 0, // 线下服务——全部指派订单数量
+      pendPieOfflineOrderCount: Number(data?.pendPieOfflineOrderCount) || 0, // 线下服务——待指派订单数量
+      todayPieOfflineOrderCount: Number(data?.todayPieOfflineOrderCount) || 0, // 线下服务——今日指派订单数量
+      // onlineCount: Number(data?.onlineCount) || 0, // 实体商品数量订单总和
+      onlineCount: Number(data?.pendPieOnlineOrderCount) || 0, // 实体商品数量订单总和
+      allPieOnlineOrderCount: Number(data?.allPieOnlineOrderCount) || 0, // 实体商品——全部指派订单数量
+      pendPieOnlineOrderCount: Number(data?.pendPieOnlineOrderCount) || 0, // 实体商品——待指派订单数量
+      todayPieOnlineOrderCount: Number(data?.todayPieOnlineOrderCount) || 0, // 实体商品——今日指派订单数量
+    };
+  } catch (error) {
+    // 失败时使用默认值
+    orderCounts.value = {
+      offlineCount: 0, // 线下服务数量
+      allPieOfflineOrderCount: 0, // 线下服务——全部指派订单数量
+      pendPieOfflineOrderCount: 0, // 线下服务——待指派订单数量
+      todayPieOfflineOrderCount: 0, // 线下服务——今日指派订单数量
+      onlineCount: 0, // 实体商品数量
+      allPieOnlineOrderCount: 0, // 实体商品——全部指派订单数量
+      pendPieOnlineOrderCount: 0, // 实体商品——待指派订单数量
+      todayPieOnlineOrderCount: 0, // 实体商品——今日指派订单数量
+    };
+  }
+}
+
+// 计算总订单数(直接使用后端返回的总和)
+const offlineCount = computed(() => {
+  return orderCounts.value.offlineCount;
+});
+const onlineCount = computed(() => {
+  return orderCounts.value.onlineCount;
+});
+
+// 暴露方法给父组件
+defineExpose({
+  offlineCount,
+  onlineCount,
+  refresh: () => {
+    loadOrderList();
+    loadInstitutionList();
+  },
+});
+</script>
+
+<template>
+  <div class="dispatch-order-panel">
+    <!-- 主要内容区域 -->
+    <div class="main-content">
+      <!-- 左侧区域 -->
+      <div class="left-panel">
+        <!-- 子tab -->
+        <div class="sub-tabs">
+          <div class="sub-tab" :class="{ active: activeSubTab === 'pending' }" @click="activeSubTab = 'pending'">
+            待指派订单 ({{ pendingCount }})
+          </div>
+          <div class="sub-tab" :class="{ active: activeSubTab === 'today' }" @click="activeSubTab = 'today'">
+            今日指派订单 ({{ todayCount }})
+          </div>
+          <div class="sub-tab" :class="{ active: activeSubTab === 'all' }" @click="activeSubTab = 'all'">
+            全部指派订单 ({{ allCount }})
+          </div>
+        </div>
+
+        <!-- 订单列表 -->
+        <div class="order-list-panel">
+          <!-- 固定头部区域 -->
+          <div class="order-list-header">
+            <!-- 警告 - 今日指派和全部指派都显示 -->
+            <div v-if="(activeSubTab === 'today' || activeSubTab === 'all')" class="warning-banner">
+              <span class="warning-text">
+                {{ props.orderType === 'physical' ? '已发货不能修改' : '距离开始时间1小时内,修改请先联系商家和客户' }}
+              </span>
+            </div>
+            <!-- 搜索 - 仅全部指派时显示 -->
+            <div v-if="activeSubTab === 'all'" class="search-section">
+              <div class="search-form">
+                <div class="search-item">
+                  <span class="search-label">客户姓名:</span>
+                  <a-input v-model:value="searchForm.customerName" placeholder="请输入" class="search-input" />
+                </div>
+                <div class="search-item">
+                  <span class="search-label">联系电话:</span>
+                  <a-input v-model:value="searchForm.contactPhone" placeholder="请输入" class="search-input" />
+                </div>
+                <div class="search-item">
+                  <span class="search-label">订单类型:</span>
+                  <a-checkbox-group v-model:value="searchForm.orderTypes" class="checkbox-group">
+                    <!-- 线下服务显示:已预约、已核销 -->
+                    <template v-if="props.orderType === 'offline'">
+                      <a-checkbox value="booked">已预约</a-checkbox>
+                      <a-checkbox value="verified">已核销</a-checkbox>
+                    </template>
+                    <!-- 实体商品显示:待发货、已发货 -->
+                    <template v-else-if="props.orderType === 'physical'">
+                      <a-checkbox value="pendingShip">待发货</a-checkbox>
+                      <a-checkbox value="shipped">已发货</a-checkbox>
+                    </template>
+                  </a-checkbox-group>
+                </div>
+                <div class="search-actions">
+                  <a-button type="primary" @click="handleSearch">查询</a-button>
+                  <a-button @click="handleReset">重置</a-button>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 可滚动的订单列表内容 -->
+          <div class="order-list-content">
+
+            <div v-if="loading" class="loading">加载中...</div>
+            <div v-else-if="orderList.length === 0" class="empty">暂无订单</div>
+            <template v-else>
+              <div v-for="(order, index) in orderList" :key="getOrderId(order)" class="order-card"
+                :class="{ active: isOrderSelected(order) }" @click="handleOrderSelect(order)">
+                <!-- 已核销/已发货印章 -->
+                <!-- 线下服务:已核销状态显示已核销图标 -->
+                <!-- 实体商品:已发货状态显示已发货图标 -->
+                <div v-if="isOrderVerified(order) || isOrderShipped(order)" class="verified-badge">
+                  <img v-if="isOrderVerified(order)" src="@/assets/images/verify.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>
+                <!-- 左侧:图钉图标和数字 -->
+                <div class="order-header-left">
+                  <div class="pin-icon-wrapper">
+                    <div class="pin-icon-inner">
+                      <span class="pin-icon-number">{{ index + 1 }}</span>
+                    </div>
+                  </div>
+                  <span class="service-name">
+                    {{ order.conditioningProgramName }}
+                    <span v-if="order.conditioningProgramType">({{ order.conditioningProgramType }})</span>
+                  </span>
+                </div>
+
+                <!-- 右侧:日期和时间按钮(仅线下服务显示) -->
+                <div v-if="props.orderType === 'offline'" class="order-header-right">
+                  <!-- 日期按钮 -->
+                  <div v-if="isOrderVerified(order) || isOrderShipped(order)"
+                    class="date-display-btn date-display-btn-disabled">
+                    {{ order.arrangeDate || '--' }}
+                  </div>
+                  <template v-else>
+                    <div class="date-display-btn" @click.stop="handleDateBtnClick(getOrderId(order))">
+                      {{ order.arrangeDate || '选择日期' }}
+                    </div>
+                  </template>
+
+                  <!-- 分隔符 -->
+                  <span class="separator-dash">-</span>
+
+                  <!-- 时间范围按钮 -->
+                  <div v-if="isOrderVerified(order) || isOrderShipped(order)"
+                    class="time-range-display-btn time-range-display-btn-disabled">
+                    <template v-if="order.arrangeTime">
+                      {{ order.arrangeTime }}
+                      <template v-if="calculateEndTime(order)">-{{ calculateEndTime(order) }}</template>
+                    </template>
+                    <template v-else>--</template>
+                  </div>
+                  <div v-else class="time-range-display-btn" @click.stop="handleTimeBtnClick(getOrderId(order))">
+                    <template v-if="order.arrangeTime">
+                      {{ order.arrangeTime }}
+                      <template v-if="calculateEndTime(order)">-{{ calculateEndTime(order) }}</template>
+                    </template>
+                    <template v-else>选择时间</template>
+                  </div>
+                </div>
+
+                <!-- 客户信息 -->
+                <div class="customer-info">
+                  {{ formatCustomerInfo(order) }}
+                </div>
+
+                <!-- 分配机构 -->
+                <div class="assign-section">
+                  <span class="assign-label">分配机构:</span>
+                  <a-input
+                    :value="getAssignedInstitutionDisplayValue(order)"
+                    placeholder="请点击右侧指派按钮或点击上次/供应商选择分配机构" class="assign-input" readonly
+                    :disabled="isOrderVerified(order) || isOrderShipped(order)" />
+                  <a-button 
+                    v-if="shouldShowConfirmAssignButton(order)" 
+                    type="default" 
+                    size="small" 
+                    @click.stop="handleCancel(getOrderId(order))">
+                    取消
+                  </a-button>
+                  <a-button v-if="shouldShowConfirmAssignButton(order)" type="primary" size="small"
+                    @click.stop="handleConfirmAssign(getOrderId(order))">
+                    确认指派
+                  </a-button>
+                </div>
+
+                <!-- 上次机构和供应商 -->
+                <div class="institution-options">
+                  <div v-if="order.preConditioningProgramSupplierName" class="institution-option"
+                    :class="{ disabled: isOrderVerified(order) || isOrderShipped(order) }"
+                    @click.stop="!(isOrderVerified(order) || isOrderShipped(order)) && handleLastInstitutionClick(getOrderId(order), order.preConditioningProgramSupplierName)">
+                    上次:{{ order.preConditioningProgramSupplierName }}
+                  </div>
+                  <div v-if="order.conditioningProgramSupplierName" class="institution-option"
+                    :class="{ disabled: isOrderVerified(order) || isOrderShipped(order) }"
+                    @click.stop="!(isOrderVerified(order) || isOrderShipped(order)) && handleSupplierClick(getOrderId(order), order.conditioningProgramSupplierName)">
+                    供应商:{{ order.conditioningProgramSupplierName }}
+                  </div>
+                </div>
+
+                <!-- 订单信息 -->
+                <div class="order-info">
+                  <div class="order-info-item" v-if="order.orderNo">订单编号: {{ order.orderNo }}</div>
+                  <div class="order-info-item" v-if="order.applyTime">用户操作时间: {{ order.applyTime }}</div>
+                  <div class="order-info-item" v-if="activeSubTab !== 'pending' && order.pieBy">派单员: {{ order.pieBy }}
+                  </div>
+                  <div class="order-info-item" v-if="order.offlineDuration">服务时长: {{ order.offlineDuration }}分钟</div>
+                </div>
+              </div>
+            </template>
+          </div>
+
+          <!-- 订单列表分页器 -->
+          <div class="order-pager-wrapper">
+            <vxe-pager v-model:current-page="orderPageConfig.currentPage" v-model:page-size="orderPageConfig.pageSize"
+              :total="orderPageConfig.total"
+              :layouts="['PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'Sizes', 'FullJump', 'Total']"
+              @page-change="handleOrderPageChange" />
+          </div>
+        </div>
+      </div>
+
+      <!-- 右侧可派单机构列表 -->
+      <div class="institution-panel">
+        <div class="panel-title">可派单机构</div>
+        <div class="institution-grid-wrapper">
+          <vxe-grid ref="gridRef" v-bind="gridOptions" :loading="institutionLoading" />
+        </div>
+      </div>
+    </div>
+
+    <!-- 日期时间选择弹窗 -->
+    <DateTimePicker v-model:visible="dateTimePickerVisible" :picker-type="pickerType"
+      :initial-date="currentPickerOrderId ? (orderList.find(o => getOrderId(o) === currentPickerOrderId)?.arrangeDate || null) : null"
+      :initial-time="currentPickerOrderId ? (orderList.find(o => getOrderId(o) === currentPickerOrderId)?.arrangeTime || null) : null"
+      :is-from-time-button="isFromTimeButton" @date-select="handleDateTimeDateSelect"
+      @time-select="handleDateTimeTimeSelect" @date-confirm="handleDateTimeDateConfirm" @close="handleDateTimeClose">
+    </DateTimePicker>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.dispatch-order-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  min-height: 0;
+
+  .main-content {
+    display: flex;
+    gap: 16px;
+    flex: 1;
+    min-height: 0;
+    overflow: hidden;
+    align-items: stretch;
+
+    .left-panel {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      min-height: 0;
+      overflow: hidden;
+      border: 1px solid #8bc34a;
+
+      .sub-tabs {
+        display: flex;
+        gap: 0;
+        margin-bottom: 0;
+        background: #8bc34a;
+        padding: 0;
+        align-items: flex-end;
+        box-sizing: border-box;
+
+        .sub-tab {
+          padding: 12px 20px;
+          cursor: pointer;
+          color: #fff;
+          background: #8bc34a;
+          border: none;
+          border-bottom: 3px solid transparent;
+          transition: all 0.3s;
+          font-size: 14px;
+          position: relative;
+          white-space: nowrap;
+
+          &.active {
+            background: #fff;
+            color: #52c41a;
+            border-bottom-color: #52c41a;
+            border-bottom-width: 3px;
+            font-weight: 500;
+          }
+
+          &:hover:not(.active) {
+            background: #a5d6a7;
+          }
+        }
+      }
+
+      .order-list-panel {
+        flex: 1;
+        min-height: 0;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+
+
+        .order-list-header {
+          flex-shrink: 0;
+          padding: 16px;
+          padding-bottom: 0;
+          background: #fff;
+
+          .warning-banner {
+            margin-bottom: 16px;
+            border-radius: 4px;
+
+            .warning-text {
+              color: #ff4d4f;
+              font-size: 14px;
+              font-weight: 500;
+            }
+          }
+
+          .search-section {
+            margin-bottom: 16px;
+            padding: 16px;
+            background: #fff;
+            border: 1px solid #e8e8e8;
+            border-radius: 4px;
+
+            .search-form {
+              display: flex;
+              align-items: center;
+              gap: 16px;
+              flex-wrap: wrap;
+
+              .search-item {
+                display: flex;
+                align-items: center;
+                gap: 8px;
+
+                .search-label {
+                  white-space: nowrap;
+                  font-size: 14px;
+                  color: #333;
+                }
+
+                .search-input {
+                  width: 180px;
+                }
+              }
+
+              .checkbox-group {
+                display: flex;
+                gap: 8px;
+              }
+
+              .search-actions {
+                display: flex;
+                gap: 8px;
+
+                :deep(.ant-btn) {
+                  height: 32px;
+                  padding: 4px 15px;
+                  font-size: 14px;
+                }
+              }
+            }
+          }
+        }
+
+        // 可滚动的订单列表内容
+        .order-list-content {
+          flex: 1;
+          min-height: 0;
+          overflow-y: auto;
+          overflow-x: hidden;
+          padding: 16px;
+          padding-top: 0;
+          padding-right: 24px;
+
+          .loading,
+          .empty {
+            text-align: center;
+            padding: 40px;
+            color: #999;
+          }
+
+          .order-card {
+            padding: 16px;
+            margin-bottom: 16px;
+            border: 1px solid #e8e8e8;
+            border-radius: 4px;
+            background: #fff;
+            cursor: pointer;
+            transition: all 0.3s;
+            position: relative;
+            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
+            overflow: visible;
+
+            &.active {
+              background: rgba(24, 144, 255, 0.08);
+              box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
+            }
+
+            .verified-badge {
+              position: absolute;
+              bottom: 35px;
+              right: 16px;
+              width: 100px;
+              height: 100px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              pointer-events: none;
+              z-index: 1;
+
+              &::before {
+                content: '';
+                position: absolute;
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+              }
+
+              &::after {
+                content: '';
+                position: absolute;
+                top: 12px;
+                left: 12px;
+                width: calc(100% - 24px);
+                height: calc(100% - 24px);
+              }
+
+              span {
+                position: relative;
+                z-index: 1;
+                display: inline-block;
+
+                &::before {
+                  content: '';
+                  position: absolute;
+                  top: 50%;
+                  left: 50%;
+                  transform: translate(-50%, -50%);
+                  width: 50px;
+                  height: 50px;
+                  border: 1px solid #5a9fe3;
+                  border-radius: 50%;
+                  opacity: 0.6;
+                }
+              }
+            }
+
+            .order-header-left {
+              display: flex;
+              align-items: center;
+              gap: 12px;
+              margin-bottom: 12px;
+
+              .pin-icon-wrapper {
+                width: 24px;
+                height: 32px;
+                position: relative;
+
+                .pin-icon-inner {
+                  position: absolute;
+                  top: 0;
+                  left: 0;
+                  width: 25px;
+                  height: 25px;
+                  transform: rotate(-45deg);
+                  transform-origin: 15px 14px;
+
+
+                  &::before {
+                    content: '';
+                    position: absolute;
+                    width: 25px;
+                    height: 25px;
+                    background: #5a9fe3;
+                    border-radius: 50% 50% 50% 0;
+                    top: 0;
+                    left: 0;
+                  }
+
+                  .pin-icon-number {
+                    position: absolute;
+                    top: 50%;
+                    left: 50%;
+                    transform: translate(-50%, -50%) rotate(45deg);
+                    z-index: 1;
+                    color: #fff;
+                    font-size: 12px;
+                    font-weight: 600;
+                    line-height: 1;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    width: 16px;
+                    height: 16px;
+                  }
+                }
+
+                &::after {
+                  content: '';
+                  position: absolute;
+                  width: 0;
+                  height: 0;
+                  border-left: 4px solid transparent;
+                  border-right: 4px solid transparent;
+                  border-top: 6px solid #5a9fe3;
+                  bottom: 0;
+                  left: 50%;
+                  transform: translateX(-50%);
+                  z-index: 0;
+                }
+              }
+
+              .service-name {
+                font-size: 16px;
+                font-weight: 500;
+                color: #333;
+              }
+            }
+
+            .order-header-right {
+              position: absolute;
+              top: 16px;
+              right: 16px;
+              display: flex;
+              gap: 8px;
+              align-items: center;
+              z-index: 10;
+              visibility: visible;
+              opacity: 1;
+
+
+              .date-display-btn {
+                background: #8bc34a;
+                color: #fff;
+                padding: 4px 11px;
+                border-radius: 4px;
+                font-size: 14px;
+                line-height: 1.5715;
+                cursor: pointer;
+                user-select: none;
+                white-space: nowrap;
+                min-width: 110px;
+                text-align: center;
+                transition: background-color 0.3s;
+
+                &:hover {
+                  background: #7cb342;
+                }
+
+                &.date-display-btn-disabled {
+                  cursor: default;
+                  opacity: 0.8;
+                }
+              }
+
+
+              .separator-dash {
+                color: #999;
+                font-size: 14px;
+                margin: 0 4px;
+              }
+
+
+              .time-range-display-btn {
+                background: #8bc34a;
+                color: #fff;
+                padding: 4px 11px;
+                border-radius: 4px;
+                font-size: 14px;
+                line-height: 1.5715;
+                cursor: pointer;
+                user-select: none;
+                white-space: nowrap;
+                min-width: 100px;
+                text-align: center;
+                transition: background-color 0.3s;
+
+                &:hover {
+                  background: #7cb342;
+                }
+
+                &.time-range-display-btn-disabled {
+                  cursor: default;
+                  opacity: 0.8;
+                }
+              }
+
+
+              .hidden-date-picker,
+              .hidden-time-picker {
+                position: absolute;
+                opacity: 0;
+                pointer-events: none;
+                width: 0;
+                height: 0;
+                overflow: hidden;
+              }
+            }
+
+            .customer-info {
+              font-size: 14px;
+              color: #5da1f9;
+              margin-bottom: 12px;
+              line-height: 1.6;
+            }
+
+            .assign-section {
+              display: flex;
+              align-items: center;
+              gap: 8px;
+              margin-bottom: 8px;
+              flex-wrap: wrap;
+
+              .assign-label {
+                white-space: nowrap;
+                color: #333;
+                font-size: 14px;
+                margin-right: 4px;
+                width: 72px;
+                flex-shrink: 0;
+              }
+
+              .assign-input {
+                flex: 1;
+                min-width: 200px;
+                max-width: 300px;
+
+                :deep(.ant-input) {
+                  border-color: #52c41a;
+                  cursor: default;
+
+                  &[readonly] {
+                    background-color: #fafafa;
+                    cursor: not-allowed;
+                  }
+
+                  &:disabled {
+                    background-color: #f5f5f5;
+                    cursor: not-allowed;
+                  }
+                }
+              }
+
+              :deep(.ant-btn) {
+                height: 32px;
+                padding: 4px 15px;
+                font-size: 14px;
+              }
+            }
+
+            .assigned-institution {
+              font-size: 14px;
+              color: #666;
+              margin-bottom: 12px;
+              padding-left: 80px;
+            }
+
+            .institution-options {
+              display: flex;
+              gap: 12px;
+              margin-bottom: 12px;
+              flex-wrap: wrap;
+              margin-left: 85px;
+
+              .institution-option {
+                padding: 4px 12px;
+                border-radius: 4px;
+                font-size: 13px;
+                color: #5da1f9;
+                cursor: pointer;
+                transition: all 0.3s;
+
+                &.disabled {
+                  opacity: 0.5;
+                  cursor: not-allowed;
+                  color: #999;
+                }
+              }
+            }
+
+            .order-info {
+              font-size: 13px;
+              color: black;
+              line-height: 1.8;
+              display: flex;
+
+              .order-info-item {
+                margin-right: 30px;
+              }
+
+              div {
+                margin-bottom: 4px;
+              }
+            }
+          }
+
+
+          .order-pager-wrapper {
+            flex-shrink: 0;
+            border-top: 1px solid #e8e8e8;
+            background: #fff;
+            padding: 8px 16px;
+            margin-top: 16px;
+          }
+        }
+      }
+    }
+
+    .institution-panel {
+      flex: 1;
+      min-height: 0;
+      border: 1px solid #e8e8e8;
+      border-radius: 4px;
+      background: #fff;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+
+      .panel-title {
+        padding: 12px 16px;
+        font-size: 16px;
+        font-weight: 500;
+        border-bottom: 1px solid #e8e8e8;
+        background: #5a9fe3;
+        color: white;
+        height: 48px;
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        flex-shrink: 0;
+      }
+
+      .institution-grid-wrapper {
+        flex: 1;
+        min-height: 0;
+        overflow: hidden;
+        display: flex;
+        flex-direction: column;
+      }
+
+      .institution-pager-wrapper {
+        flex-shrink: 0;
+        border-top: 1px solid #e8e8e8;
+        background: #fff;
+        padding: 8px 16px;
+      }
+
+      :deep(.vxe-grid) {
+        flex: 1;
+        overflow: hidden;
+        display: flex;
+        flex-direction: column;
+        min-height: 0;
+        height: 100%;
+
+        // 表格主体区域
+        .vxe-table--wrapper {
+          flex: 1;
+          overflow: hidden;
+          min-height: 0;
+          height: 100%;
+          display: flex;
+          flex-direction: column;
+        }
+
+        .vxe-table {
+          height: 100%;
+          display: flex;
+          flex-direction: column;
+        }
+
+        .vxe-table--header-wrapper {
+          flex-shrink: 0;
+          position: relative;
+          z-index: 10;
+        }
+
+        .vxe-table--header {
+          background: #fafafa;
+
+          .vxe-header--column {
+            font-weight: 500;
+            color: #333;
+          }
+        }
+
+        .vxe-table--body-wrapper {
+          flex: 1;
+          min-height: 0;
+          position: relative;
+        }
+
+        :deep(.vxe-table--body-wrapper),
+        :deep(.vxe-table--body),
+        :deep(.vxe-table--body-wrapper > *),
+        :deep([class*="scroll"]) {
+          &::-webkit-scrollbar {
+            width: 8px;
+          }
+
+          &::-webkit-scrollbar-thumb {
+            background: #d9d9d9;
+            border-radius: 4px;
+          }
+
+          &::-webkit-scrollbar-thumb:hover {
+            background: #bfbfbf;
+          }
+        }
+
+        .vxe-table--body {
+          .vxe-body--row {
+            &:hover {
+              background: #f5f5f5;
+            }
+
+            .vxe-body--column {
+              padding: 12px 8px;
+            }
+          }
+        }
+
+        .vxe-button {
+          padding: 4px 15px;
+          height: 35px;
+          font-size: 14px;
+          width: 100px;
+          background: #1890ff;
+          color: white;
+        }
+      }
+    }
+  }
+}
+</style>

+ 283 - 0
src/order/EditShipment.vue

@@ -0,0 +1,283 @@
+<script setup lang="ts">
+import { ref, computed, onBeforeMount } from 'vue';
+import { VxeUI } from 'vxe-pc-ui';
+import { notification } from 'ant-design-vue';
+import { getDictionaryMethod } from '@/request/api/dictionary.api';
+import { confirmShipmentMethod } from '@/request/api/order.api';
+import type { ShipmentModel } from '@/model/order.model';
+import { useRequest } from 'alova/client';
+type FollowModel = Partial<ShipmentModel>;
+interface ShipmentForm {
+  receiptType: string; // 发货形式 0-配送 1-线下取货
+  expressType?: string; // 快递类型 0-邮政速递 1-顺丰速运 2-京东快递 3-中通快递 4-圆通速递 5-申通快递 6-韵达快递 7-极兔速递
+  expressNo?: string; // 快递单号
+}
+
+const props = defineProps<{ data: FollowModel }>();
+
+const emits = defineEmits<{
+  submit: [data?: ShipmentModel];
+}>();
+
+// 提交状态
+const { loading: submitting, send: submit } = useRequest(confirmShipmentMethod, { immediate: false }).onSuccess(() => {
+
+})
+// 快递类型选项列表
+const expressTypeOptions = ref<{ label: string; value: string }[]>([]);
+
+const formData = ref<ShipmentForm>({
+  receiptType: '1', // 默认选择线下取货
+  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);
+    if (res && Array.isArray(res) && res.length > 0) {
+      expressTypeOptions.value = res;
+    } else {
+      // 如果没有字典数据,使用默认选项
+      expressTypeOptions.value = expressTypeOptionsList;
+    }
+  } catch (error) {
+    // 使用默认数据
+    expressTypeOptions.value = expressTypeOptionsList;
+  }
+
+  if (props.data) {
+    formData.value.receiptType = props.data.receiptType || '1';
+    formData.value.expressType = props.data.expressType || '';
+    formData.value.expressNo = props.data.expressNo || '';
+  }
+});
+
+
+const showDeliveryFields = computed(() => formData.value.receiptType === '0');
+
+
+const handleReceiptTypeChange = (e: any) => {
+  formData.value.receiptType = e.target.value;
+  if (formData.value.receiptType === '1') {
+    formData.value.expressType = '';
+    formData.value.expressNo = '';
+  }
+};
+
+// 提交表单
+const handleSubmit = async () => {
+  if (submitting.value) return;
+
+  if (formData.value.receiptType === '0') {
+    if (!formData.value.expressType) {
+      notification.warning({
+        message: '提示',
+        description: '请选择快递类型',
+      });
+      return;
+    }
+    if (!formData.value.expressNo?.trim()) {
+      notification.warning({
+        message: '提示',
+        description: '请输入快递单号',
+      });
+      return;
+    }
+  }
+
+
+  const receiptType = formData.value.receiptType;
+  const expressType = formData.value.receiptType === '0' ? formData.value.expressType : '';
+  const expressNo = formData.value.receiptType === '0' ? formData.value.expressNo : '';
+
+  const id = props.data?.id;
+  if (!id) {
+    notification.error({
+      message: '缺少患者调理方案ID',
+    });
+    return;
+  }
+  const submitData = {
+    id,
+    conditioningProgramSupplierId: props.data?.conditioningProgramSupplierId,
+    receiptType,
+    expressType : expressType || '',
+    expressNo : expressNo || '',
+  } as any;
+  try {
+    await submit(submitData);
+    notification.success({
+      message: '操作成功',
+      description: '发货成功',
+    });
+    emits('submit');
+  } catch (error) {
+    console.error('发货失败:', error);
+  }
+}
+// 取消操作
+const handleCancel = () => {
+  VxeUI.modal.close('shipment-modal');
+};
+
+</script>
+
+<template>
+  <div class="shipment-form">
+    <!-- 标题 -->
+    <div class="form-title">发货</div>
+    <!-- 发货类型选择 -->
+    <div class="form-section">
+      <a-radio-group v-model:value="formData.receiptType" @change="handleReceiptTypeChange" class="delivery-type-group">
+        <a-radio value="1">线下取货</a-radio>
+        <a-radio value="0">配送</a-radio>
+      </a-radio-group>
+    </div>
+
+    <!-- 配送相关字段(仅在选择配送时显示) -->
+    <div v-if="showDeliveryFields" class="form-section delivery-fields-container">
+      <!-- 快递类型 -->
+      <div class="form-item">
+        <label class="form-label">快递类型</label>
+        <a-select v-model:value="formData.expressType" placeholder="请选择" class="form-select"
+          :options="expressTypeOptions" show-search :filter-option="(input, option) =>
+            (option?.label ?? '').toLowerCase().includes(input.toLowerCase())" />
+      </div>
+
+      <!-- 快递单号 -->
+      <div class="form-item">
+        <label class="form-label">快递单号</label>
+        <a-input v-model:value="formData.expressNo" placeholder="请输入" class="form-input" />
+      </div>
+
+      <!-- 提示信息 -->
+      <div class="form-tip">
+        请检查物流信息是否正确,每条商品仅支持一次修改
+      </div>
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="form-actions">
+      <vxe-button @click="handleCancel" content="取消" class="cancel-btn" :disabled="submitting">取消</vxe-button>
+      <vxe-button type="submit" @click="handleSubmit" status="primary" class="confirm-btn"
+        :loading="submitting">确定</vxe-button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.shipment-form {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  min-height: 400px;
+  padding: 24px 24px 100px 24px;
+  background: #fff;
+}
+
+.form-title {
+  font-size: 16px;
+  font-weight: bold;
+  text-align: center;
+  margin-bottom: 24px;
+  color: #000;
+}
+
+.form-section {
+  margin-bottom: 20px;
+}
+
+.delivery-type-group {
+  display: flex;
+  justify-content: center;
+  gap: 24px;
+  margin-bottom: 20px;
+
+  :deep(.ant-radio-wrapper) {
+    font-size: 14px;
+  }
+}
+
+.delivery-fields-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.form-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16px;
+  width: 100%;
+  justify-content: center;
+
+  .form-label {
+    min-width: 80px;
+    font-size: 14px;
+    color: #000;
+    margin-right: 12px;
+    text-align: left;
+  }
+
+  .form-select,
+  .form-input {
+    flex: 1;
+    max-width: 300px;
+  }
+}
+
+.form-tip {
+  font-size: 14px;
+  color: #000;
+  margin-bottom: 0;
+  line-height: 1.5;
+  text-align: center;
+  width: 100%;
+}
+
+.form-actions {
+  display: flex;
+  justify-content: center;
+  gap: 12px;
+  margin-top: auto;
+  padding-top: 24px;
+
+  .cancel-btn {
+    min-width: 80px;
+    border: 1px solid #1890ff;
+    background: #fff;
+    color: #1890ff;
+
+    &:hover {
+      border-color: #40a9ff;
+      color: #40a9ff;
+    }
+  }
+
+  .confirm-btn {
+    min-width: 80px;
+    background: #1890ff;
+    border-color: #1890ff;
+    color: #fff;
+
+    &:hover {
+      background: #40a9ff;
+      border-color: #40a9ff;
+    }
+  }
+}
+</style>

+ 1 - 0
src/pages/index.vue

@@ -120,6 +120,7 @@ function updateUserPassword(model: UserModel) {
       <div class="app-header-menus-wrapper flex-auto">
         <a-menu mode="horizontal" :items="menus" :selectedKeys="selectedKeys" @click="selectMenuItem" />
       </div>
+      <div class="mr-5">{{ local?.dept?.deptName }}</div>
       <div class="account">
         <a-avatar size="large" :src="local?.avatar">{{ local?.name?.slice(0, 1) }}</a-avatar>
         <span class="m-x-2 cursor-pointer" @click="openUserPreview()">{{ local?.name }}</span>

+ 59 - 37
src/pages/index/care/issueService.vue

@@ -157,7 +157,7 @@ function getPatientRecord(id: any) {
   });
 }
 async function getCpRecordDetail(id: string) {
-  await getCpDetailMethod({ id }).then((res) => {
+  await getCpDetailMethod({ id }).then((res: any) => {
     formData.items = res?.items ?? [];
     form.conditioningWrapName = res?.conditioningWrapName;
     form.estimatedStartDate = res?.estimatedStartDate;
@@ -296,7 +296,7 @@ const formData = reactive<FollowModel>({
       },
     },
   ], // 适用情况
-  items: [], // Initialize as empty array
+  items: [], 
 });
 const emptyRow = {
   id: '',
@@ -306,6 +306,7 @@ const emptyRow = {
   frequencyType: '',
   frequencyTypeing: [],
   frequencyMeasure: '',
+  offlineDuration: '',
   totalMeasure: '',
   totalPrice: '',
   initialDay: '',
@@ -324,6 +325,7 @@ const emptyRow = {
     cpMedicines: [],
     effect: '',
     isOffline: null,
+    sellType: '',
     isDelivery: null,
     photo: '',
     institutionId: '',
@@ -351,12 +353,8 @@ watch(displayTableData, (newValue, oldValue) => {
     isShowDelivery.value = newValue.some((item) => {
       return item.conditioningProgramDetail?.isDelivery === 'Y';
     });
+    console.log(newValue, 'newValue');
     console.log(isShowDelivery.value, 'isShowDelivery.value');
-    // if (isShowDelivery.value) {
-    //   deliveryChecked.value = true;
-    // } else {
-    //   deliveryChecked.value = false;
-    // }
     newValue.forEach((row: any) => {
       row.frequencyTypeing = row.frequencyType ? [row.frequencyType] : [];
     });
@@ -382,11 +380,15 @@ function onSelectProject({ row }: any) {
   formData.items.push({
     id: '',
     conditioningProgramId: row.id || '',
+    conditioningWrapId: form.conditioningWrapId || '',
+    patientId: currentPatient?.value?.patientId || '',
+    patientConditioningRecordId: currentPatient?.value?.id || '',
+    offlineDuration: row?.offlineDuration || '',
+    frequencyType: row?.frequencyType || '',
+    frequencyMeasure: row?.frequencyMeasure || '',
     // conditioningProgramId: 0,
     days: '',
-    frequencyType: '',
     frequencyTypeing: [],
-    frequencyMeasure: '',
     totalMeasure: '',
     totalPrice: '',
     initialDay: '',
@@ -408,6 +410,7 @@ function onSelectProject({ row }: any) {
       cpDynamicPricingRule: row?.cpDynamicPricingRule || [],
       cpMedicines: row?.cpMedicines || [],
       isOffline: row?.isOffline || null,
+      sellType: row?.sellType || '',
       isDelivery: row?.isDelivery || null,
       photo: row?.photo || '',
       conditioningProgramSupplierName: row?.conditioningProgramSupplierName || '',
@@ -513,21 +516,6 @@ function editPart(row: any) {
     },
   });
 }
-// 已开状态下显示穴位/经络/部位的只读文本
-function getAcupointText(row: any): string {
-  const meridians: string[] = Array.isArray(row?.acuMeridianNames)
-    ? row.acuMeridianNames
-    : Array.isArray(row?.cwcpAcuMeridians)
-      ? row.cwcpAcuMeridians.map((it: any) => it?.name).filter(Boolean)
-      : [];
-  const points: string[] = Array.isArray(row?.acuPointNames)
-    ? row.acuPointNames
-    : Array.isArray(row?.cwcpAcuPoints)
-      ? row.cwcpAcuPoints.map((it: any) => it?.name).filter(Boolean)
-      : [];
-  const parts = [...meridians, ...points].filter(Boolean);
-  return parts.length ? parts.join('、') : '-';
-}
 const allProjects = ref<
   Array<{
     name: string;
@@ -563,16 +551,30 @@ function calculateCount(row: any) {
     } else {
       const convertDose = Number(row.conditioningProgramDetail.cpFixedPricingRule.convertDose) || 0;
       const frequencyType = Number(row.frequencyType) || 0;
-      row.totalMeasure = Math.ceil(((period / frequencyType) * frequency) / convertDose);
+      // 防止除零和 NaN
+      if (frequencyType > 0 && convertDose > 0 && !isNaN(frequencyType) && !isNaN(convertDose) && !isNaN(frequency) && !isNaN(period)) {
+        row.totalMeasure = Math.ceil(((period / frequencyType) * frequency) / convertDose);
+      } else {
+        row.totalMeasure = 0;
+      }
     }
     // 获取单价
     const unitPrice = Number(row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice) || 0;
-    // 计算总价
-    row.totalPrice = (row.totalMeasure * unitPrice).toFixed(2);
+    // 计算总价,防止 NaN
+    if (!isNaN(row.totalMeasure) && !isNaN(unitPrice)) {
+      row.totalPrice = (row.totalMeasure * unitPrice).toFixed(2);
+    } else {
+      row.totalPrice = '0.00';
+    }
   } else if (pricingType === '1') {
     // 按穴位计价
     const frequencyType = Number(row.frequencyType) || 0;
-    row.totalMeasure = Math.ceil(period / frequencyType) * frequency;
+    // 防止除零和 NaN
+    if (frequencyType > 0 && !isNaN(frequencyType) && !isNaN(frequency) && !isNaN(period)) {
+      row.totalMeasure = Math.ceil(period / frequencyType) * frequency;
+    } else {
+      row.totalMeasure = 0;
+    }
     // console.log(frequencyType, 'frequencyType', frequency, period, row.totalMeasure);
     // console.log(acCount, maxCount, 'acCount, maxCount', row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType);
     if (acCount > maxCount) {
@@ -581,7 +583,12 @@ function calculateCount(row: any) {
         if (acCount > 0) {
           let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * acCount;
           row.unitPrice = unitPrice;
-          row.totalPrice = row.totalMeasure * unitPrice;
+          // 防止 NaN
+          if (!isNaN(row.totalMeasure) && !isNaN(unitPrice)) {
+            row.totalPrice = row.totalMeasure * unitPrice;
+          } else {
+            row.totalPrice = 0;
+          }
         } else {
           row.unitPrice = '-';
           row.totalPrice = 0;
@@ -591,7 +598,12 @@ function calculateCount(row: any) {
         // row.unitPrice = '-';
         let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[1].price;
         row.unitPrice = unitPrice;
-        row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * row.totalMeasure;
+        // 防止 NaN
+        if (!isNaN(row.totalMeasure) && !isNaN(unitPrice)) {
+          row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * row.totalMeasure;
+        } else {
+          row.totalPrice = 0;
+        }
       }
     } else {
       // console.log(row.conditioningProgramDetail.cpDynamicPricingRule, 'row.conditioningProgramDetail.cpDynamicPricingRule?.length');
@@ -601,8 +613,12 @@ function calculateCount(row: any) {
           if (acCount > 0) {
             let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * acCount;
             row.unitPrice = unitPrice;
-
-            row.totalPrice = row.totalMeasure * unitPrice;
+            // 防止 NaN
+            if (!isNaN(row.totalMeasure) && !isNaN(unitPrice)) {
+              row.totalPrice = row.totalMeasure * unitPrice;
+            } else {
+              row.totalPrice = 0;
+            }
             // console.log(unitPrice, 'unitPrice', row.totalPrice, 'row.totalPrice', period, frequencyType, frequency, acCount);
           } else {
             row.unitPrice = '-';
@@ -613,7 +629,12 @@ function calculateCount(row: any) {
           // row.unitPrice = '-';
           let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[0].price;
           row.unitPrice = unitPrice;
-          row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * row.totalMeasure;
+          // 防止 NaN
+          if (!isNaN(row.totalMeasure) && !isNaN(unitPrice)) {
+            row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * row.totalMeasure;
+          } else {
+            row.totalPrice = 0;
+          }
           // console.log(row.totalPrice, 'row.totalPrice', row.conditioningProgramDetail.cpDynamicPricingRule[0].price, row.totalMeasure);
         }
       }
@@ -1166,10 +1187,11 @@ function openPatientHealthRecord(row: { id: string }, showType: 'analysis' | 'sc
                 </div>
               </template>
             </vxe-column>
-            <vxe-column field="conditioningProgramDetail.isOffline" title="线下项目" width="100">
+            <vxe-column field="conditioningProgramDetail.sellType" title="商品类型" width="100">
+              <!-- 商品类型 1-实体商品 2-线下服务 3-线上权益 -->
               <template #default="{ row }">
-                <span v-if="row.conditioningProgramDetail?.isOffline">
-                  {{ row.conditioningProgramDetail.isOffline === 'Y' ? '是' : '否' }}
+                <span v-if="row.conditioningProgramDetail?.sellType">
+                  {{ row.conditioningProgramDetail.sellType === '1' ? '实体商品' : row.conditioningProgramDetail.sellType === '2' ? '线下服务' : row.conditioningProgramDetail.sellType === '3' ? '线上权益' : '-' }}
                 </span>
                 <span v-else>-</span>
               </template>
@@ -1210,7 +1232,7 @@ function openPatientHealthRecord(row: { id: string }, showType: 'analysis' | 'sc
         </div>
 
         <div style="display: flex; justify-content: flex-end; margin-top: 16px">
-          <span style="font-weight: bold">合计:{{ totalPrice }}元</span>
+          <span style="font-weight: bold">合计:{{ totalPrice.toFixed(2) }}元</span>
         </div>
         <!-- 调养日期 -->
         <div class="delivery-row">

+ 21 - 3
src/pages/index/care/supplier.vue

@@ -4,7 +4,7 @@ import Institution from '@/components/institution.vue';
 import SupplyProject from '@/components/SupplyProject.vue';
 import EditSupplier from '@/components/EditSupplier.vue';
 import type { SupplierModel, SupplierQuery } from '@/model/care.model';
-
+import { businessStatus } from '@/model/options';
 // 接口数据
 import { supplierMethod, supplierDeleteMethod } from '@/request/api/care.api';
 import { branchMethod } from '@/request/api/system.api';
@@ -63,6 +63,19 @@ const searchFormProps = reactive<VxeFormProps<SupplierQuery>>({
         },
       },
     },
+    {
+      field: 'businessStatus',
+      title: '营业状态',
+      span: 6,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          placeholder: '请选择',
+          options: businessStatus,
+        },
+      },
+    },
+
     {
       span: 4,
       itemRender: {
@@ -124,6 +137,8 @@ const gridOptions = reactive<VxeGridProps<SupplierModel>>({
     { field: 'detailAddress', title: '地址' },
     { field: 'kahuna', title: '负责人' },
     { field: 'phone', title: '联系电话' },
+    { field: 'profitSharing', title: '分账比例' },
+    { field: 'businessStatus', title: '营业状态' ,slots: { default: 'businessStatusCell' }},
     {
       field: 'collaborateDepts',
       title: '合作机构',
@@ -250,7 +265,7 @@ function seeSupplyProject(model?: SupplierModel, index?: number) {
     id: 'supplyProject-modal',
     title: '供应项目',
     height: 700,
-    width: 850,
+    width:1000,
     position: {
       top: Math.min(100, window.innerHeight * 0.1),
     },
@@ -289,7 +304,7 @@ function editSupplier(model?: SupplierModel, index?: number) {
   VxeUI.modal.open({
     id: 'supplier-modal',
     title: model?.id ? `修改供应商` : `新增供应商`,
-    height: 700,
+    height: 850,
     width: 850,
     position: {
       top: Math.min(100, window.innerHeight * 0.1),
@@ -319,6 +334,9 @@ function editSupplier(model?: SupplierModel, index?: number) {
     </header>
     <main class="flex-auto overflow-hidden">
       <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents" :loading="loading">
+        <template #businessStatusCell="{ row }">
+          <span>{{ row.businessStatus === '1' ? '营业' : row.businessStatus === '2' ? '休息' : row.businessStatus === '3' ? '停业' : '' }}</span>
+        </template>
         <template #toolbar-extra>
           <vxe-button style="margin-right: 12px" icon="vxe-icon-repeat" circle @click="refresh(page)"></vxe-button>
         </template>

+ 0 - 291
src/pages/index/care/text.vue

@@ -1,291 +0,0 @@
-<template>
-  <div class="scrollbar-container">
-    <div class="scrollbar-track" ref="trackRef" @click="handleTrackClick">
-      <!-- 固定分割点20:不可拖动、不可删除、不显示标签 -->
-      <div
-        class="scrollbar-node"
-        :style="{ left: fixedNode.position + '%' }"
-      >
-        <div class="node-dot fixed">
-          <span class="dot-value">{{ fixedNode.value }}个</span>
-        </div>
-      </div>
-      <!-- 新增节点 -->
-      <div
-        v-for="(node, index) in nodes"
-        :key="index"
-        class="scrollbar-node"
-        :style="{ left: node.position + '%' }"
-      >
-        <div class="node-label-x">
-          <span class="node-label-text">计价{{ index + 3 }}</span>
-          <span class="node-x" @click.stop="removeNode(index)">×</span>
-        </div>
-        <div
-          class="node-dot"
-          :class="{ dragging: draggingIndex === index }"
-          @mousedown="handleNodeMouseDown(index, $event)"
-        >
-          <span class="dot-value">{{ node.value }}个</span>
-        </div>
-      </div>
-      <!-- 计价1标记在最左 -->
-      <div class="section-label label-left">计价1</div>
-      <!-- 计价2标记在最右 -->
-      <div class="section-label label-right">计价2</div>
-    </div>
-    <div class="price-section-list">
-      <!-- 固定区间 -->
-      <div class="price-section">
-        <span class="section-label">计价1:</span>
-        <span>当"穴位/经络/部位" ≤ 20个时,</span>
-        <input v-model="fixedPrices[0]" class="section-input" placeholder="请输入" />
-        <span>元</span>
-      </div>
-      <div class="price-section">
-        <span class="section-label">计价2:</span>
-        <span>当"穴位/经络/部位" > 20个时,</span>
-        <input v-model="fixedPrices[1]" class="section-input" placeholder="请输入" />
-        <span>元</span>
-      </div>
-      <!-- 新增节点的独立区间 -->
-      <div
-        v-for="(node, idx) in nodes"
-        :key="idx"
-        class="price-section"
-      >
-        <span class="section-label">计价{{ idx + 3 }}:</span>
-        <span>当"穴位/经络/部位" = {{ node.value }}个时,</span>
-        <input v-model="node.price" class="section-input" placeholder="请输入" />
-        <span>元</span>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref } from 'vue'
-
-interface ScrollbarNode {
-  position: number // 百分比
-  value: number    // 实际数值
-  price: string    // 该节点的价格
-}
-
-const min = 1
-const max = 50
-
-export default defineComponent({
-  name: 'InteractiveScrollbar',
-  setup() {
-    const trackRef = ref<HTMLElement | null>(null)
-    // 固定分割点20
-    const fixedNode: ScrollbarNode = {
-      value: 20,
-      position: ((20 - min) / (max - min)) * 100,
-      price: ''
-    }
-    // 新增节点
-    const nodes = ref<ScrollbarNode[]>([])
-    const draggingIndex = ref<number | null>(null)
-    // 固定区间的价格
-    const fixedPrices = ref(['', ''])
-
-    // 轨道点击新增节点
-    const handleTrackClick = (event: MouseEvent) => {
-      if (!trackRef.value) return
-      const trackRect = trackRef.value.getBoundingClientRect()
-      const clickPosition = (event.clientX - trackRect.left) / trackRect.width
-      const positionPercentage = Math.min(Math.max(clickPosition * 100, 0), 100)
-      const value = Math.round(positionPercentage / 100 * (max - min) + min)
-      if (value === 20) return // 不允许和固定点重叠
-      if (nodes.value.some(n => n.value === value)) return
-      nodes.value.push({ position: positionPercentage, value, price: '' })
-    }
-
-    // 删除节点
-    const removeNode = (index: number) => {
-      nodes.value.splice(index, 1)
-    }
-
-    // 节点圆点鼠标按下,开始拖动
-    const handleNodeMouseDown = (index: number, event: MouseEvent) => {
-      event.stopPropagation()
-      draggingIndex.value = index
-      document.addEventListener('mousemove', handleMouseMove)
-      document.addEventListener('mouseup', handleMouseUp)
-    }
-
-    // 拖动中
-    const handleMouseMove = (event: MouseEvent) => {
-      if (draggingIndex.value === null || !trackRef.value) return
-      const node = nodes.value[draggingIndex.value]
-      const trackRect = trackRef.value.getBoundingClientRect()
-      let position = (event.clientX - trackRect.left) / trackRect.width
-      position = Math.min(Math.max(position, 0), 1)
-      const positionPercentage = position * 100
-      const value = Math.round(position * (max - min) + min)
-      if (value === 20) return // 不允许和固定点重叠
-      if (nodes.value.some((n, idx) => idx !== draggingIndex.value && n.value === value)) return
-      node.position = positionPercentage
-      node.value = value
-    }
-
-    // 拖动结束
-    const handleMouseUp = () => {
-      draggingIndex.value = null
-      document.removeEventListener('mousemove', handleMouseMove)
-      document.removeEventListener('mouseup', handleMouseUp)
-    }
-
-    return {
-      min,
-      max,
-      fixedNode,
-      trackRef,
-      nodes,
-      draggingIndex,
-      handleTrackClick,
-      handleNodeMouseDown,
-      removeNode,
-      fixedPrices
-    }
-  }
-})
-</script>
-
-<style scoped>
-.scrollbar-container {
-  width: 100%;
-  padding: 20px 0;
-}
-
-.scrollbar-track {
-  position: relative;
-  height: 8px;
-  background-color: #e0e0e0;
-  border-radius: 4px;
-  cursor: pointer;
-}
-
-.section-label.label-left {
-  position: absolute;
-  left: 0;
-  top: -28px;
-  color: #faad14;
-  font-weight: bold;
-  font-size: 14px;
-  z-index: 2;
-}
-.section-label.label-right {
-  position: absolute;
-  right: 0;
-  top: -28px;
-  color: #faad14;
-  font-weight: bold;
-  font-size: 14px;
-  z-index: 2;
-}
-
-.scrollbar-node {
-  position: absolute;
-  width: 0;
-  height: 0;
-  top: 50%;
-  transform: translate(-50%, -50%);
-  transition: left 0.1s linear;
-}
-
-.node-label-x {
-  position: absolute;
-  left: 50%;
-  top: -38px;
-  transform: translateX(-50%);
-  display: flex;
-  align-items: center;
-  z-index: 3;
-}
-.node-label-text {
-  background: #faad14;
-  color: #fff;
-  font-size: 12px;
-  border-radius: 4px 0 0 4px;
-  padding: 2px 8px;
-  font-weight: bold;
-  user-select: none;
-}
-.node-x {
-  background: #fff;
-  color: #ff4d4f;
-  font-size: 16px;
-  font-weight: bold;
-  border-radius: 0 4px 4px 0;
-  border: 1px solid #ff4d4f;
-  width: 20px;
-  height: 20px;
-  line-height: 18px;
-  text-align: center;
-  cursor: pointer;
-  margin-left: 2px;
-  transition: background 0.2s;
-  user-select: none;
-}
-.node-x:hover {
-  background: #ffeded;
-}
-
-.node-dot {
-  position: absolute;
-  width: 24px;
-  height: 24px;
-  background-color: #ff6b6b;
-  border-radius: 50%;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  cursor: grab;
-  border: 2px solid #fff;
-  box-shadow: 0 0 2px #888;
-  z-index: 2;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  transition: background 0.2s;
-}
-.node-dot.fixed {
-  background-color: #bdbdbd;
-  cursor: not-allowed;
-}
-.node-dot.dragging {
-  background-color: #ffa726;
-}
-
-.dot-value {
-  color: #fff;
-  font-size: 13px;
-  font-weight: bold;
-  pointer-events: none;
-  user-select: none;
-}
-
-.price-section-list {
-  margin-top: 32px;
-}
-.price-section {
-  display: flex;
-  align-items: center;
-  margin-bottom: 12px;
-}
-.section-label {
-  font-weight: 500;
-  color: #222;
-  margin-right: 8px;
-}
-.section-input {
-  width: 120px;
-  margin: 0 8px;
-  padding: 4px 8px;
-  border: 1px solid #d9d9d9;
-  border-radius: 4px;
-}
-</style>

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

@@ -171,6 +171,10 @@ const gridOptions = reactive<VxeGridProps<DeviceManageModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -361,6 +365,16 @@ function importOrganization() {
   padding: 0 24px;
   max-height: var(--page-main-container);
 }
+
+/* 行 hover & 选中高亮背景 */
+:deep(.vxe-table .vxe-body--row.row--hover) {
+  background-color: #f5f7ff !important;
+}
+
+:deep(.vxe-table .vxe-body--row.row--current),
+:deep(.vxe-table .vxe-body--row.row--checked) {
+  background-color: #e6f7ff !important;
+}
 .date-range-container {
   display: flex;
   align-items: center;

+ 128 - 0
src/pages/index/order/dispatchOrder.vue

@@ -0,0 +1,128 @@
+<script setup lang="ts">
+import DispatchOrderPanel from '@/order/DispatchOrderPanel.vue';
+import { ref, computed } from 'vue';
+
+defineOptions({
+  name: 'DispatchOrder',
+});
+
+// 当前选中的派单类型
+const activeOrderType = ref<'offline' | 'physical'>('offline');
+
+// 订单面板
+const panelRef = ref<InstanceType<typeof DispatchOrderPanel> | null>(null);
+
+// 获取订单数量
+const offlineCount = computed(() => panelRef.value?.offlineCount);
+const onlineCount = computed(() => panelRef.value?.onlineCount);
+
+</script>
+
+<template>
+  <div class="dispatch-order-page">
+    <!-- 顶部tab -->
+    <div class="main-tabs">
+      <div class="main-tab" :class="{ active: activeOrderType === 'offline' }" @click="activeOrderType = 'offline'">
+        线下服务 <span class="count">({{ offlineCount }})</span>
+      </div>
+      <div class="main-tab" :class="{ active: activeOrderType === 'physical' }" @click="activeOrderType = 'physical'">
+        实体商品 <span class="count">({{ onlineCount }})</span>
+      </div>
+    </div>
+    <!-- 派单 -->
+    <div class="panel-wrapper">
+      <DispatchOrderPanel ref="panelRef" :order-type="activeOrderType" />
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.dispatch-order-page {
+  display: flex;
+  flex-direction: column;
+  height: var(--page-main-container, calc(100vh - 60px));
+  max-height: var(--page-main-container, calc(100vh - 60px));
+  padding: 24px;
+  overflow: hidden;
+
+  .main-tabs {
+    display: inline-flex;
+    gap: 0;
+    margin-bottom: 16px;
+    border-radius: 4px;
+    overflow: hidden;
+    flex-shrink: 0; 
+
+    .main-tab {
+      padding: 10px 24px;
+      cursor: pointer;
+      transition: all 0.3s;
+      font-size: 16px;
+      font-weight: 500;
+      position: relative;
+      border: 1px solid #1890ff;
+
+      &:first-child {
+        border-radius: 4px 0 0 4px;
+        border-right: none;
+
+        &.active {
+          background: #1890ff;
+          color: #fff;
+          z-index: 1;
+        }
+
+        &:not(.active) {
+          background: #fff;
+          color: #1890ff;
+
+          .count {
+            color: #ff4d4f;
+          }
+        }
+      }
+
+     
+      &:last-child {
+        border-radius: 0 4px 4px 0;
+
+        &.active {
+          background: #1890ff;
+          color: #fff;
+          z-index: 1;
+
+          .count {
+            color: #fff;
+          }
+        }
+
+        &:not(.active) {
+          background: #fff;
+          color: #1890ff;
+
+          .count {
+            color: #ff4d4f;
+          }
+        }
+      }
+
+      .count {
+        font-weight: 500;
+
+      }
+
+      &:hover {
+        opacity: 0.9;
+      }
+    }
+  }
+
+  .panel-wrapper {
+    flex: 1;
+    min-height: 0; 
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+  }
+}
+</style>

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

@@ -0,0 +1,346 @@
+<script setup lang="ts">
+import { h } from 'vue';
+import { conditioningStatus, orderStatus } from '@/model/options';
+import { getConditioningRecordMethod } from '@/request/api/care.api';
+import type { ConditioningRecordListModel, ConditioningRecordListQuery } from '@/model/care.model';
+
+import OrderDetail from '@/service/OrderDetail.vue';
+import CareProcess from '@/service/CareProgress.vue';
+import { usePagination } from 'alova/client';
+import dayjs from 'dayjs';
+
+import { type VxeFormListeners, type VxeFormProps, type VxeGridInstance, type VxeGridListeners, type VxeGridProps, VxeUI } from 'vxe-table';
+
+defineOptions({
+  name: 'OrderManagement',
+});
+
+const model = shallowRef<ConditioningRecordListQuery>();
+
+const orderTimeStart = ref<string>('');
+const orderTimeEnd = ref<string>('');
+const payTimeStart = ref<string>('');
+const payTimeEnd = ref<string>('');
+
+// 禁用结束时间的日期
+function disabledOrderEndDate(current: any) {
+  if (!orderTimeStart.value) return false;
+  return current && current < dayjs(orderTimeStart.value);
+}
+
+function disabledPayEndDate(current: any) {
+  if (!payTimeStart.value) return false;
+  return current && current < dayjs(payTimeStart.value);
+}
+
+const searchFormProps = reactive<VxeFormProps<ConditioningRecordListQuery>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: {},
+  items: [
+    {
+      field: 'conditioningWrapName',
+      title: '服务包名称',
+      span: 6,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'patientName',
+      title: '姓名',
+      span: 6,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'progress',
+      title: '调理状态',
+      span: 6,
+      itemRender: {
+        name: 'VxeSelect',
+        options: conditioningStatus,
+        props: { placeholder: '请选择', clearable: true },
+      },
+    },
+    {
+      field: 'orderStatus',
+      title: '订单状态',
+      span: 6,
+      itemRender: {
+        name: 'VxeSelect',
+        options: orderStatus,
+        props: { placeholder: '请选择', clearable: true },
+      },
+    },
+    {
+      field: 'orderTime',
+      title: '下单时间',
+      span: 10,
+      slots: {
+        default: 'orderTimeRange',
+      },
+    },
+    {
+      field: 'payTime',
+      title: '付款时间',
+      span: 10,
+      slots: {
+        default: 'payTimeRange',
+      },
+    },
+    {
+      span: 6,
+      itemRender: {
+        name: 'VxeButtonGroup',
+        options: [
+          { name: 'submits', type: 'submit', content: '查询', status: 'primary' },
+          { name: 'reset', type: 'reset', content: '清空' },
+        ],
+        events: {
+          click() { },
+        },
+      },
+    },
+  ],
+});
+const searchFormEmits: VxeFormListeners<ConditioningRecordListQuery> = {
+  submit({ data }) {
+    model.value = {
+      ...data,
+      orderTimeStart: orderTimeStart.value ? dayjs(orderTimeStart.value).format('YYYY-MM-DD HH:mm:ss') : '',
+      orderTimeEnd: orderTimeEnd.value ? dayjs(orderTimeEnd.value).format('YYYY-MM-DD HH:mm:ss') : '',
+      payTimeStart: payTimeStart.value ? dayjs(payTimeStart.value).format('YYYY-MM-DD HH:mm:ss') : '',
+      payTimeEnd: payTimeEnd.value ? dayjs(payTimeEnd.value).format('YYYY-MM-DD HH:mm:ss') : '',
+    } as any;
+  },
+  // 重置
+  reset({ data }) {
+    model.value = { ...data } as any;
+    orderTimeStart.value = '';
+    orderTimeEnd.value = '';
+    payTimeStart.value = '';
+    payTimeEnd.value = '';
+  },
+};
+const gridRef = ref<VxeGridInstance<ConditioningRecordListModel>>();
+const gridOptions = reactive<VxeGridProps<ConditioningRecordListModel>>({
+  id: 'tag-list',
+  border: true,
+  showOverflow: true,
+  height: 'auto',
+  autoResize: false,
+  syncResize: true,
+  scrollY: { enabled: true, gt: 0 },
+  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: 'createTime', title: '创建时间' },
+    { field: 'payTime', title: '付款时间' },
+    { field: 'patientName', title: '姓名' },
+    { field: 'conditioningWrapName', title: '服务包名称' },
+    { field: 'diagnosis', title: '疾病名称' },
+    { field: 'symptom', title: '证型' },
+    { field: 'constitutionGroupName', title: '体质' },
+    { field: 'createBy', title: '开具医生' },
+    { field: 'estimatedStartDate', title: '开始调养日期' },
+    { field: 'progress', title: '调理状态', slots: { default: 'patients' } },
+    { field: 'orderStatus', title: '订单状态', slots: { default: 'orderStatus' } },
+    { field: 'cost', title: '订单金额' },
+    { field: 'profitSharing', title: '分账' },
+    {
+      field: 'action',
+      title: '操作',
+      align: 'center',
+      width: 200,
+      cellRender: {
+        name: 'VxeButtonGroup',
+        props: {
+          mode: 'text',
+        },
+        options: [
+          { content: '详情', status: 'primary', name: 'serviceDetail' },
+          { content: '调养过程', status: 'primary', name: 'conditioningProcess' },
+        ],
+        events: {
+          click({ row }: { row: ConditioningRecordListModel }, { name }: { name: string }) {
+            if (name === 'serviceDetail') {
+              serviceDetail(row);
+            } else if (name === 'conditioningProcess') {
+              conditioningProcess(row);
+            }
+          },
+        },
+      },
+    },
+  ],
+  data: [],
+});
+const gridEvents: VxeGridListeners = {};
+
+const { loading, page, pageSize, total, onSuccess, refresh } = usePagination((page, size) => getConditioningRecordMethod(page, size, model.value), {
+  initialData: { data: [], total: 0 },
+  initialPage: 1,
+  initialPageSize: 100,
+  watchingStates: [model],
+  immediate: true,
+});
+onSuccess(({ data: { data } }) => {
+  gridRef.value?.loadData(data);
+});
+
+onMounted(() => {
+  model.value = toRaw(searchFormProps.data);
+});
+
+function conditioningProcess(model?: ConditioningRecordListModel) {
+  VxeUI.modal.open({
+    title: model?.id ? `调养过程` : `新增调养过程`,
+    height: window.innerHeight,
+    width: window.innerWidth,
+    fullscreen: true,
+    escClosable: true,
+    destroyOnClose: true,
+    id: `conditioning-process-modal`,
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(CareProcess, {
+          data: model,
+          onSubmit() {
+            refresh(page.value);
+            VxeUI.modal.close(`conditioning-process-modal`);
+          },
+        } as any);
+      },
+    },
+  });
+}
+// 点击详情
+function serviceDetail(model?: ConditioningRecordListModel) {
+  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: {
+            ...model,
+            types,
+          },
+          onVoidSubmit() {
+            refresh(page.value);
+          },
+        } as any);
+      },
+    },
+  });
+}
+const progressTextMap: Record<string, string> = {
+  '3': '未开始',
+  '4': '调理中',
+  '5': '已完结',
+};
+
+const orderStatusTextMap: Record<string, string> = {
+  '1': '交易关闭(已作废)',
+  '0': '待付款',
+  '2': '交易关闭(用户取消)',
+  '6': '已付款',
+  '345': '交易成功',
+};
+
+
+</script>
+<template>
+  <div class="page-container flex flex-col">
+    <header class="flex-none mt-4">
+      <vxe-form v-bind="searchFormProps" v-on="searchFormEmits">
+        <template #orderTimeRange>
+          <div class="date-range-container">
+            <a-date-picker v-model:value="orderTimeStart" placeholder="请选择开始时间" style="flex: 1"
+              :show-time="{ format: 'HH:mm' }" />
+            <span class="date-separator">至</span>
+            <a-date-picker v-model:value="orderTimeEnd" placeholder="请选择结束时间" style="flex: 1"
+              :disabledDate="disabledOrderEndDate" :show-time="{ format: 'HH:mm' }" />
+          </div>
+        </template>
+        <template #payTimeRange>
+          <div class="date-range-container">
+            <a-date-picker v-model:value="payTimeStart" placeholder="请选择开始时间" style="flex: 1"
+              :show-time="{ format: 'HH:mm' }" />
+            <span class="date-separator">至</span>
+            <a-date-picker v-model:value="payTimeEnd" placeholder="请选择结束时间" style="flex: 1"
+              :disabledDate="disabledPayEndDate" :show-time="{ format: 'HH:mm' }" />
+          </div>
+        </template>
+      </vxe-form>
+    </header>
+    <main class="flex-auto overflow-hidden">
+      <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents" :loading="loading">
+        <template #cell="{ row }">{{ row.progress === 1 ? '已开始' : row.progress === 2 ? '已结束' : '未开始' }} </template>
+        <template #patients="{ row }">
+          <div>
+            {{ progressTextMap[row.progress] || '' }}
+          </div>
+        </template>
+        <template #orderStatus="{ row }">
+          <div>
+            {{ orderStatusTextMap[row.orderStatus] || '' }}
+          </div>
+        </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);
+}
+
+.date-range-container {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  width: 100%;
+
+  .vxe-input {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .date-separator {
+    color: #666;
+    font-size: 14px;
+    font-weight: 500;
+    white-space: nowrap;
+    padding: 0 8px;
+  }
+}
+</style>

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

@@ -0,0 +1,285 @@
+<script setup lang="ts">
+import { ref, reactive, shallowRef, onMounted, toRaw, computed } from 'vue';
+import { profitSharingStatus } from '@/model/options';
+import dayjs from 'dayjs';
+import type { RevenueSharingDetailModel, RevenueSharingDetailQuery } from '@/model/order.model';
+import type { ConditioningRecordListModel } from '@/model/care.model';
+import OrderDetail from '@/service/OrderDetail.vue';
+import { getAllSupplierMethod } from '@/request/api/care.api';
+// 接口数据
+import {
+    getRevenueSharingDetailListMethod,
+  } 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 model = shallowRef<RevenueSharingDetailQuery>();
+const searchFormProps = reactive<VxeFormProps<RevenueSharingDetailQuery>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: {},
+  items: [
+    {
+      field: 'orderNo',
+      title: '订单编号',
+      span: 4,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'profitSharingStatus',
+      title: '分账状态',
+      span: 4,
+      itemRender: {
+        name: 'VxeSelect',
+        options: profitSharingStatus,
+        props: { placeholder: '状态' },
+      },
+    },
+    {
+      field: 'conditioningProgramSupplierName',
+      title: '供应商',
+      span: 4,
+      itemRender: {
+        name: 'VxeSelect',
+        optionProps: {
+          value: 'value', // 供应商id
+          label: 'label', // 供应商名称
+        },
+        props: { placeholder: '请选择', options: computed(() => supplierOptions.value as any[]), clearable: true },
+      },
+    },
+    {
+      field: 'profitSharingTime',
+      title: '分账时间',
+      span: 10,
+      slots: {
+        default: 'revenueTimeRange',
+      },
+    },
+    {
+      span: 8,
+      itemRender: {
+        name: 'VxeButtonGroup',
+        options: [
+          { name: 'submits', type: 'submit', content: '查询', status: 'primary' },
+          { name: 'reset', type: 'reset', content: '重置' },
+        ],
+        events: {
+            click() {},
+        },
+      },
+    },
+  ],
+});
+const profitSharingTimeStart = ref<string>('');
+const profitSharingTimeEnd = ref<string>('');
+function disabledRevenueEndDate(current: any) {
+  if (!profitSharingTimeStart.value) return false;
+  return current && current < dayjs(profitSharingTimeStart.value);
+}
+const searchFormEmits: VxeFormListeners<RevenueSharingDetailQuery> = {
+  submit({ data }) {
+    let supplierName = '';
+    if (data.conditioningProgramSupplierName) {
+      const supplier = supplierArr.value.find((item: any) => item.id === data.conditioningProgramSupplierName);
+      supplierName = supplier?.name || '';
+    }
+    
+    model.value = {
+      ...data,
+      conditioningProgramSupplierName: supplierName, 
+      profitSharingTimeStart: profitSharingTimeStart.value ? dayjs(profitSharingTimeStart.value).format('YYYY-MM-DD HH:mm:ss') : '',
+      profitSharingTimeEnd: profitSharingTimeEnd.value ? dayjs(profitSharingTimeEnd.value).format('YYYY-MM-DD HH:mm:ss') : '',
+    };
+  },
+  // 重置
+  reset({ data }) {
+    model.value = { ...data };
+    profitSharingTimeStart.value = '';
+    profitSharingTimeEnd.value = '';
+  },
+};
+const gridRef = ref<VxeGridInstance<RevenueSharingDetailModel>>();
+const gridOptions = reactive<VxeGridProps<RevenueSharingDetailModel>>({
+  id: 'tag-list',
+  border: true,
+  showOverflow: true,
+  height: 'auto',
+  autoResize: false,
+  syncResize: true,
+  scrollY: { enabled: true, gt: 0 },
+  toolbarConfig: {
+    custom: true,
+    zoom: true,
+    slots: {
+      // buttons: 'handle',
+      tools: 'toolbar-extra',
+    },
+  },
+  columnConfig: {
+    resizable: true,
+  },
+  customConfig: {
+    storage: true,
+  },
+  columns: [
+    { type: 'seq', width: 70, fixed: 'left' },
+    //点击订单编号查看详情
+    { field: 'orderNo', title: '订单编号', slots: { default: 'orderNoCell' }  },
+    { field: 'payTime', title: '付款时间' },
+    { field: 'finishTime', title: '收货/核销/完成时间' },
+    { field: 'profitSharingTime', title: '分账时间' },
+    { field: 'conditioningProgramName', title: '项目名称' },
+    { field: 'totalMeasure', title: '数量' },
+    { field: 'pricingUnit', title: '单位' },
+    { field: 'unitPrice', title: '单价(元)' },
+    { field: 'totalPrice', title: '总价(元)' },
+    { field: 'conditioningProgramSupplierName', title: '供应商' },
+    { field: 'profitSharingStatus', title: '分账状态', slots: { default: 'revenueStatusCell' } },
+    { field: 'profitSharing', title: '分账比例' },
+    { field: 'profitSharingAmount', title: '预计分账金额' },
+    { field: 'realAmount', title: '到账金额' },
+  ],
+  data: [],
+});
+const gridEvents: VxeGridListeners = {};
+
+const { loading, page, pageSize, total, onSuccess, refresh } = usePagination(
+  (page, size) => getRevenueSharingDetailListMethod(page, size, model.value),
+  {
+    initialData: { data: [], total: 0 },
+    initialPage: 1,
+    initialPageSize: 100,
+    watchingStates: [model],
+    immediate: false,
+  }
+);
+onSuccess(({ data: { data } }) => {
+  gridRef.value?.loadData(data);
+});
+
+const supplierOptions = ref<{ label: string; value: string }[]>([]);
+const supplierArr = ref<any[]>([]);
+async function getSupplier(params: any) {
+  supplierOptions.value = [];
+  const res = (await getAllSupplierMethod(params)) as any[];
+  if (Array.isArray(res) && res.length > 0) {
+    supplierArr.value = res;
+    supplierOptions.value = res.map((item: any) => ({
+      label: item.name,
+      value: item.id,
+    }));
+  }
+}
+
+onMounted(() => {
+  model.value = toRaw(searchFormProps.data);
+  getSupplier({});
+});
+// 点击项目名称查看商品详情
+function serviceDetail(model?: ConditioningRecordListModel) {
+  const types = 'record';
+  console.log('model===', model);
+  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: {
+            ...model,
+            types,
+          },
+          onVoidSubmit() {
+            refresh(page.value);
+          },
+        } as any);
+      },
+    },
+  });
+}
+</script>
+<template>
+  <div class="page-container flex flex-col">
+    <header class="flex-none mt-4">
+      <vxe-form v-bind="searchFormProps" v-on="searchFormEmits">
+        <template #revenueTimeRange>
+          <div class="date-range-container">
+            <a-date-picker v-model:value="profitSharingTimeStart" placeholder="请选择开始时间" style="flex: 1"
+              :show-time="{ format: 'HH:mm' }" />
+            <span class="date-separator">至</span>
+            <a-date-picker v-model:value="profitSharingTimeEnd" placeholder="请选择结束时间" style="flex: 1"
+              :disabledDate="disabledRevenueEndDate" :show-time="{ format: 'HH:mm' }" />
+          </div>
+        </template>
+      </vxe-form>
+    </header>
+    <main class="flex-auto overflow-hidden">
+        <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents" :loading="loading">
+        <template #orderNoCell="{ row }">
+          <span class="order-no-link" @click="serviceDetail(row as any)">{{ row.orderNo }}</span>
+        </template>
+        <template #revenueStatusCell="{ row }">
+          <span>{{ row.profitSharingStatus === '1' ? '未分账' : row.profitSharingStatus === '2' ? '已分账' : row.profitSharingStatus === '3' ? '分账异常' : '' }}</span>
+        </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);
+}
+
+.date-separator {
+  margin: 0 10px;
+}
+
+.order-no-link {
+  color: #1890ff;
+  cursor: pointer;
+  text-decoration: none;
+  
+  &:hover {
+    text-decoration: underline;
+    color: #40a9ff;
+  }
+}
+</style>

+ 398 - 0
src/pages/index/order/shipment.vue

@@ -0,0 +1,398 @@
+<script setup lang="ts">
+import { h, ref, reactive, shallowRef, onMounted, toRaw, computed } from 'vue';
+import EditShipment from '@/order/EditShipment.vue';
+import OrderDetail from '@/service/OrderDetail.vue';
+import type { ShipmentModel, ShipmentQuery } from '@/model/order.model';
+import { getAllSupplierMethod } from '@/request/api/care.api';
+defineOptions({
+  name: 'ShipmentPage',
+});
+import { receiptStatus, receiptType } from '@/model/options';
+import dayjs from 'dayjs';
+// 接口数据
+import {
+  getShipmentListMethod,
+} 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 model = shallowRef<ShipmentQuery>();
+const searchFormProps = reactive<VxeFormProps<ShipmentQuery>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: {
+    receiptStatus: '0', // 默认待发货
+  },
+  items: [
+    {
+      field: 'orderNo',
+      title: '订单编号',
+      span: 4,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'receiptStatus',
+      title: '发货状态',
+      span: 4,
+
+      itemRender: {
+        name: 'VxeSelect',
+        options: receiptStatus,
+        props: { placeholder: '请选择', clearable: true },
+      },
+    },
+    {
+      field: 'receiptType',
+      title: '发货形式',
+      span: 4,
+      itemRender: {
+        name: 'VxeSelect',
+        options: receiptType,
+        props: { placeholder: '请选择' },
+      },
+    },
+    {
+      field: 'conditioningProgramSupplierId',
+      title: '供应商',
+      span: 4,
+      itemRender: {
+        name: 'VxeSelect',
+        optionProps: {
+          value: 'value', // 供应商id
+          label: 'label', // 供应商名称
+        },
+        props: { placeholder: '请选择', options: computed(() => supplierOptions.value as any[]), clearable: true },
+      },
+    },
+    {
+      field: 'payTime',
+      title: '付款时间',
+      span: 10,
+      slots: {
+        default: 'payTimeRange',
+      },
+    },
+
+    {
+      span: 8,
+      itemRender: {
+        name: 'VxeButtonGroup',
+        options: [
+          { name: 'submits', type: 'submit', content: '查询', status: 'primary' },
+          { name: 'reset', type: 'reset', content: '重置' },
+        ],
+        events: {},
+      },
+    },
+  ],
+});
+const searchFormEmits: VxeFormListeners<ShipmentQuery> = {
+  submit({ data }) {
+    model.value = {
+      ...data,
+      payTimeStart: payTimeStart.value ? dayjs(payTimeStart.value).format('YYYY-MM-DD HH:mm:ss') : '',
+      payTimeEnd: payTimeEnd.value ? dayjs(payTimeEnd.value).format('YYYY-MM-DD HH:mm:ss') : '',
+    } as any;
+  },
+  // 重置
+  reset({ data }) {
+    model.value = { ...data } as any;
+    payTimeStart.value = '';
+    payTimeEnd.value = '';
+  },
+};
+const processedData = ref<ShipmentModel[]>([]);
+const orderGroupMap = ref<Map<string, { startIndex: number; count: number; rows: ShipmentModel[] }>>(new Map());
+
+function getGroupKey(item: ShipmentModel): string {
+  const orderNo = item.orderNo || '';
+  const supplierId = item.conditioningProgramSupplierId || '';
+  return `${orderNo}_${supplierId}`;
+}
+
+function processDataForMerge(data: ShipmentModel[]) {
+  const grouped = new Map<string, ShipmentModel[]>();
+  const result: ShipmentModel[] = [];
+
+  data.forEach((item) => {
+    const groupKey = getGroupKey(item);
+    if (!grouped.has(groupKey)) {
+      grouped.set(groupKey, []);
+    }
+    grouped.get(groupKey)!.push(item);
+  });
+
+  orderGroupMap.value.clear();
+  let currentIndex = 0;
+
+  grouped.forEach((rows, groupKey) => {
+    const count = rows.length;
+    orderGroupMap.value.set(groupKey, {
+      startIndex: currentIndex,
+      count,
+      rows,
+    });
+    result.push(...rows);
+    currentIndex += count;
+  });
+
+  processedData.value = result;
+  return result;
+}
+
+const gridRef = ref<VxeGridInstance<ShipmentModel>>();
+const gridOptions = reactive<VxeGridProps<ShipmentModel>>({
+  id: 'tag-list',
+  border: true,
+  showOverflow: true,
+  height: 'auto',
+  autoResize: false,
+  syncResize: true,
+  scrollY: { enabled: false },
+  spanMethod: ({ row, column, rowIndex }: any) => {
+    const groupKey = getGroupKey(row);
+    const groupInfo = orderGroupMap.value.get(groupKey);
+
+    if (!groupInfo) return { rowspan: 1, colspan: 1 };
+
+    const { startIndex, count } = groupInfo;
+    const isFirstRow = rowIndex === startIndex;
+
+    if (column.field === 'orderNo') {
+      return isFirstRow ? { rowspan: count, colspan: 1 } : { rowspan: 0, colspan: 0 };
+    }
+
+    if (column.field === 'action') {
+      return isFirstRow ? { rowspan: count, colspan: 1 } : { rowspan: 0, colspan: 0 };
+    }
+
+    return { rowspan: 1, colspan: 1 };
+  },
+  toolbarConfig: {
+    custom: true,
+    zoom: true,
+    slots: {
+      // buttons: 'handle',
+      tools: 'toolbar-extra',
+    },
+  },
+  columnConfig: {
+    resizable: true,
+  },
+  customConfig: {
+    storage: true,
+  },
+  columns: [
+    { type: 'seq', width: 70, fixed: 'left' },
+    { field: 'orderNo', title: '订单编号', slots: { default: 'orderNoCell' } },
+    { field: 'payTime', title: '付款时间' },
+    { field: 'conditioningProgramName', title: '项目名称' },
+    { field: 'totalMeasure', title: '数量' },
+    { field: 'pricingUnit', title: '单位' },
+    { field: 'unitPrice', title: '单价(元)' },
+    { field: 'totalPrice', title: '总价(元)' },
+    { field: 'receiptStatus', title: '发货状态', slots: { default: 'progressCell' } },
+    { field: 'receiptType', title: '发货形式', slots: { default: 'shipmentTypeCell' } },
+    { field: 'conditioningProgramSupplierName', title: '供应商' },
+    {
+      field: 'action',
+      title: '操作',
+      align: 'center',
+      width: 120,
+      showOverflow: false,
+      slots: { default: 'action' },
+    },
+  ],
+  data: [],
+});
+const gridEvents: VxeGridListeners = {};
+
+const { loading, page, pageSize, total, onSuccess, refresh } = usePagination(
+  (page, size) => getShipmentListMethod(page, size, model.value),
+  {
+    initialData: { data: [], total: 0 },
+    initialPage: 1,
+    initialPageSize: 100,
+    watchingStates: [model],
+    immediate: false,
+  }
+);
+onSuccess(({ data: { data } }) => {
+  const processed = processDataForMerge(data);
+  gridRef.value?.loadData(processed);
+});
+const supplierOptions = ref<{ label: string; value: string }[]>([]);
+const supplierArr = ref<any[]>([]);
+// 获取所有的供应商
+async function getSupplier(params: any) {
+  supplierOptions.value = [];
+  const res = (await getAllSupplierMethod(params)) as any[];
+  if (Array.isArray(res) && res.length > 0) {
+    supplierArr.value = res;
+    supplierOptions.value = res.map((item: any) => ({
+      label: item.name,
+      value: item.id,
+    }));
+  }
+}
+
+onMounted(() => {
+  model.value = toRaw(searchFormProps.data);
+  getSupplier({});
+});
+
+
+// 点击订单编号查看订单详情
+function serviceDetail(row?: ShipmentModel) {
+  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.id,
+            types,
+          },
+          onVoidSubmit() {
+            refresh(page.value);
+          },
+        } as any);
+      },
+    },
+  });
+}
+
+async function goShipment(row?: ShipmentModel) {
+  if (!row) return;
+
+  const groupKey = getGroupKey(row);
+  const groupInfo = orderGroupMap.value.get(groupKey);
+
+  if (!groupInfo) return;
+
+  const isShipped = row.receiptStatus === '1';
+
+  VxeUI.modal.open({
+    title: isShipped ? `修改发货信息` : `去发货`,
+    height: 500,
+    width: 850,
+    escClosable: true,
+    destroyOnClose: true,
+    id: `shipment-modal`,
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(EditShipment, {
+          data: row,
+          onSubmit: () => {
+            refresh(page.value);
+            VxeUI.modal.close('shipment-modal');
+          }
+        });
+      }
+    }
+  });
+}
+const payTimeStart = ref<string>('');
+const payTimeEnd = ref<string>('');
+function disabledPayEndDate(current: any) {
+  if (!payTimeStart.value) return false;
+  return current && current < dayjs(payTimeStart.value);
+}
+</script>
+<template>
+  <div class="page-container flex flex-col">
+    <header class="flex-none mt-4">
+      <vxe-form v-bind="searchFormProps" v-on="searchFormEmits">
+        <template #payTimeRange>
+          <div class="date-range-container">
+            <a-date-picker v-model:value="payTimeStart" placeholder="请选择开始时间" style="flex: 1"
+              :show-time="{ format: 'HH:mm' }" />
+            <span class="date-separator">至</span>
+            <a-date-picker v-model:value="payTimeEnd" placeholder="请选择结束时间" style="flex: 1"
+              :disabledDate="disabledPayEndDate" :show-time="{ format: 'HH:mm' }" />
+          </div>
+        </template>
+      </vxe-form>
+    </header>
+    <main class="flex-auto overflow-hidden">
+      <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents" :loading="loading">
+        <template #orderNoCell="{ row }">
+          <span class="order-no-link" @click="serviceDetail(row)">{{ row.orderNo }}</span>
+        </template>
+        <template #progressCell="{ row }">
+          <div>{{ row.receiptStatus === '0' ? '待发货' : row.receiptStatus === '1' ? '已发货' : row.receiptStatus === '2' ?
+            '已收货' : '' }}</div>
+        </template>
+        <template #shipmentTypeCell="{ row }">
+          <div>{{ row.receiptType === '0' ? '配送' : row.receiptType === '1' ? '线下取货' : '' }}</div>
+        </template>
+        <template #action="{ row }">
+          <vxe-button mode="text" status="primary" @click="goShipment(row)">
+            {{ row.receiptStatus === '1' ? '修改' : row.receiptStatus === '0' ? '去发货' : '' }}
+          </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">
+.date-separator {
+  margin: 0 10px;
+}
+
+.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>

+ 10 - 10
src/request/api/account.api.ts

@@ -69,16 +69,16 @@ export function getMenusMethod(account: AccountModel) {
   return request.Get<AccountModel, any[]>(`/system/menu/getRouters`, {
     headers: { Authorization: account.token },
     transform(data) {
-      // data.push({
-      //   path: '/',
-      //   children: [
-      //     {
-      //       path: 'satisfaction/survey',
-      //       name: 'satisfactionSurvey',
-      //       meta: { title: '满意度管理' },
-      //     },
-      //   ],
-      // });
+      // data[5].children.push(
+      //   {
+      //     path: 'shipment',
+      //     meta: { title: '发货' },
+      //   },
+      //   {
+      //     path: 'revenueSharing',
+      //     meta: { title: '分账' },
+      //   },
+      // );
       //   console.log(data, 'push之后的data', transformMenus(data));
       return { ...account, menus: transformMenus(data) };
     },

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

@@ -291,6 +291,7 @@ 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}`, {

+ 166 - 0
src/request/api/order.api.ts

@@ -0,0 +1,166 @@
+import type { List, Tree } from '@/model';
+import type { OrderQuery, OrderModel, OrderLiaisonListModel, OrderLiaisonListQuery, ShipmentModel, ShipmentQuery, PieOrderCountModel,RevenueSharingDetailModel,RevenueSharingDetailQuery } from '@/model/order.model';
+import request from '@/request/alova';
+
+// 线下服务  今日指派订单分页列表
+export function todayOrderMethod(page: number, size: number) {
+  return request.Post<List<OrderModel>>(
+    '/fdhb-pc/pieOrderManage/getTodayPieOrders',
+    {},
+    {
+      hitSource: /order$/, // 匹配失效源
+      params: { pageNum: page, pageSize: size },
+    }
+  );
+}
+
+// 线下服务  待指派订单分页列表
+export function pendingOrderMethod(page: number, size: number) {
+  return request.Post<List<OrderModel>>(
+    '/fdhb-pc/pieOrderManage/getPendPieOrders',
+    {},
+    {
+      hitSource: /order$/, // 匹配失效源
+      params: { pageNum: page, pageSize: size },
+    }
+  );
+}
+
+// 线下服务  全部指派订单分页列表
+export function allPieOrderMethod(page: number, size: number, query?: OrderQuery) {
+  return request.Post<List<OrderModel>>(
+    '/fdhb-pc/pieOrderManage/getAllPieOrders',
+    query ?? {},
+    {
+      hitSource: /order$/, // 匹配失效源
+      params: { pageNum: page, pageSize: size },
+    }
+  );
+}
+
+// 实体商品 今日指派订单分页列表
+export function todayPhysicalOrderMethod(page: number, size: number) {
+  return request.Post<List<OrderModel>>(
+    '/fdhb-pc/pieOrderManage/getTodayPieOnlineOrders',
+    {},
+    {
+      hitSource: /order$/, // 匹配失效源
+      params: { pageNum: page, pageSize: size },
+    }
+  );
+}
+
+// 实体商品 待指派订单分页列表
+export function pendingPhysicalOrderMethod(page: number, size: number) {
+  return request.Post<List<OrderModel>>(
+    '/fdhb-pc/pieOrderManage/getPendPieOnlineOrders',
+    {},
+    {
+      hitSource: /order$/, // 匹配失效源
+      params: { pageNum: page, pageSize: size },
+    }
+  );
+}
+
+// 实体商品 全部指派订单分页列表
+export function allPhysicalOrderMethod(page: number, size: number, query?: OrderQuery) {
+  return request.Post<List<OrderModel>>(
+    '/fdhb-pc/pieOrderManage/getAllPieOnlineOrders',
+    query ?? {},
+    {
+      hitSource: /order$/, // 匹配失效源
+      params: { pageNum: page, pageSize: size },
+    }
+  );
+}
+// 获取可派单机构
+export function getOrderLiaisonListMethod(data: Partial<OrderModel>) {
+  return request.Post<List<OrderLiaisonListModel>>(
+    '/fdhb-pc/pieOrderManage/getPieCpSupplier',
+    {
+      collaborateDeptId: data.institutionId,
+      conditioningProgramTypes: [data.conditioningProgramType],
+      timeStart: data?.timeStart ?? '',
+      type: data?.type ?? ''
+    },
+    {
+      hitSource: /order$/, // 匹配失效源
+    }
+  );
+}
+
+// 线下服务 确认指派
+export function confirmPieOrderMethod(data: Partial<OrderModel>) {
+  return request.Post<List<OrderModel>>(
+    `/fdhb-pc/pieOrderManage/pieOrder/${data.id}/${data.conditioningProgramSupplierId}`, {}
+    ,
+    {
+      params: {
+        startTime: data?.pieTimeStart ?? '',
+        endTime: data?.pieTimeEnd ?? '',
+      },
+      hitSource: /order$/, // 匹配失效源
+    }
+  );
+}
+// 实体商品 确认指派
+export function confirmPhysicalOrderMethod(data: Partial<OrderModel>) {
+  return request.Post(
+    `/fdhb-pc/pieOrderManage/pieOnlineOrder/${data.patientConditioningProgramId}/${data.conditioningProgramSupplierId}`,
+    {
+      hitSource: /order$/, // 匹配失效源
+    },
+  );
+}
+
+// 获取发货列表
+export function getShipmentListMethod(page: number, size: number, query?: ShipmentQuery) {
+  return request.Post<List<ShipmentModel>>(
+    '/fdhb-pc/deliveryManage/getDelivery',
+    query ?? {},
+    {
+      hitSource: /shipment$/, // 匹配失效源
+      params: { pageNum: page, pageSize: size },
+    }
+  );
+}
+
+// 确认发货
+export function confirmShipmentMethod(data: Partial<ShipmentModel>) {
+  // 如果是配送(receiptType === '0'),传递快递信息;如果是线下取货(receiptType === '1'),不传递
+  const params: any = {};
+  if (data.receiptType === '0') {
+    params.expressType = data.expressType;
+    params.expressNo = data.expressNo;
+  }
+  return request.Post<List<ShipmentModel>>(
+    `/fdhb-pc/deliveryManage/confirmDelivery/${data.id}/${data.conditioningProgramSupplierId}/${data.receiptType}`,
+    {},
+    {
+      params,
+      hitSource: /shipment$/, // 匹配失效源
+    }
+  );
+}
+
+// 获取指派订单数量
+export function getPieOrderCountMethod() {
+  return request.Post<PieOrderCountModel>(
+    `/fdhb-pc/pieOrderManage/getPieCount`,
+    {
+      hitSource: /order$/, // 匹配失效源
+    }
+  );
+}
+
+// 获取分账记录列表
+export function getRevenueSharingDetailListMethod(page: number, size: number, query?: RevenueSharingDetailQuery) {
+  return request.Post<List<RevenueSharingDetailModel>>(
+    '/fdhb-pc/profitSharingManage/getProfitSharing',
+    query ?? {},
+    {
+      params: { pageNum: page, pageSize: size },
+      hitSource: /revenueSharing$/, // 匹配失效源
+    }
+  );
+} 

+ 9 - 1
src/router/index.ts

@@ -41,7 +41,6 @@ const router = createRouter({
           children: [
             { path: 'supplier', component: () => import(`@/pages/index/care/supplier.vue`) },
             { path: 'serviceItems', component: () => import(`@/pages/index/care/serviceItems.vue`) },
-            { path: 'text', component: () => import(`@/pages/index/care/text.vue`) },
             { path: 'systemService', component: () => import(`@/pages/index/care/systemService.vue`) },
             { path: 'institutionService', component: () => import(`@/pages/index/care/institutionService.vue`) },
             { path: 'issueService', component: () => import(`@/pages/index/care/issueService.vue`) },
@@ -83,6 +82,15 @@ const router = createRouter({
           path: 'satisfaction',
           children: [{ path: 'survey', component: () => import(`@/pages/index/satisfaction/survey.vue`) }],
         },
+        {
+          path: 'order',
+          children: [
+            { path: 'management', component: () => import(`@/pages/index/order/management.vue`) },
+            { 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`) },
+          ],
+        },
       ],
       beforeEnter(to, from, next) {
         if (useAccountStore(pinia).token) {

File diff ditekan karena terlalu besar
+ 349 - 334
src/service/AddItems.vue


+ 83 - 13
src/service/ConfirmItems.vue

@@ -124,6 +124,47 @@ watch(
     }
   }
 );
+
+// 建议频率单位
+const suggestedFrequencyUnit = ref<string>('');
+
+// 监听一口价单位变化,同步到建议频率单位
+watch(
+  () => props.data.cpFixedPricingRule?.convertUnit,
+  (newVal) => {
+    if (props.data.pricingType === '0') {
+      suggestedFrequencyUnit.value = newVal || '';
+    }
+  }
+);
+
+// 监听建议频率单位变化,回写到一口价使用单位(保证双向一致)
+watch(
+  () => suggestedFrequencyUnit.value,
+  (newVal) => {
+    if (props.data.pricingType === '0' && props.data.cpFixedPricingRule && newVal) {
+      props.data.cpFixedPricingRule.convertUnit = newVal;
+    }
+  }
+);
+
+// 监听计价规则变化,同步建议频率单位
+watch(
+  () => props.data.pricingType,
+  (val) => {
+    if (val === '0') {
+      // 一口价模式:建议频率单位与一口价单位保持一致
+      if (props.data.cpFixedPricingRule?.convertUnit) {
+        suggestedFrequencyUnit.value = props.data.cpFixedPricingRule.convertUnit;
+      } else {
+        suggestedFrequencyUnit.value = '';
+      }
+    } else if (val === '1') {
+      // 按穴位模式:建议频率单位默认为"次"
+      suggestedFrequencyUnit.value = '次';
+    }
+  }
+);
 const uploadProps = reactive({ showRemoveIcon: true });
 function customUpload(e: any) {
   // uploadApi 你的二次封装上传接口
@@ -282,14 +323,7 @@ function validate(): boolean {
     });
     return false;
   }
-  // 供应商 必填
-  if (!d.conditioningProgramSupplierId) {
-    notification.error({
-      message: '请选择供应商',
-      description: '请选择供应商',
-    });
-    return false;
-  }
+  // 供应商 不必填
   return true;
 }
 
@@ -298,6 +332,10 @@ function handleOk() {
   if (showOffLine.value && !props.data.isOffline) {
     props.data.isOffline = 'N';
   }
+  // 同步建议频率单位到一口价使用单位
+  if (props.data.pricingType === '0' && props.data.cpFixedPricingRule && suggestedFrequencyUnit.value) {
+    props.data.cpFixedPricingRule.convertUnit = suggestedFrequencyUnit.value;
+  }
   props.data.photo = fileList.value[0]?.response?.url || fileList.value[0]?.url || '';
   confirmOrgConfirmMethod(props.data as SystemItemModel).then(() => {
     console.log(props.data, 'props.data');
@@ -311,15 +349,17 @@ function handleOk() {
   });
 }
 const isOffline = ref<boolean>(false);
-function toggleOnlineStatus() {
-  props.data.isOffline = isOffline.value ? 'Y' : 'N';
-}
 onMounted(async () => {
   console.log(props.data, 'props.data11111');
   if (props.data.id) {
     const res = await getConditioningSchemeDetailMethod(props.data);
     Object.assign(props.data, res);
     getIsonline(props.data.conditioningProgramSupplierId);
+    // 初始化建议频率单位
+    suggestedFrequencyUnit.value = res.cpFixedPricingRule?.convertUnit || '';
+  } else {
+    // 如果没有数据,从现有数据中获取
+    suggestedFrequencyUnit.value = props.data.cpFixedPricingRule?.convertUnit || '';
   }
   
   fileList.value = props.data?.photo
@@ -357,6 +397,10 @@ function handleInstitutionChange(value: string | number, node: any) {
       <label>方案类型:</label>
       <div>{{ data.conditioningProgramType }}</div>
     </div>
+    <div class="form-row">
+      <label>商品类型:</label>
+      <div>{{ data.sellType === '1' ? '实体商品' : data.sellType === '2' ? '线下服务' : data.sellType === '3' ? '线上权益' : '-' }}</div>
+    </div>
     <div class="form-row">
       <label><span class="required-star">*</span>计价规则:</label>
       <div>{{ data.pricingType === '0' ? '一口价' : '按穴位/经络/部位' }}</div>
@@ -407,6 +451,29 @@ function handleInstitutionChange(value: string | number, node: any) {
         <a-input v-model:value="data.cpDynamicPricingRule![1].price" style="width: 80px; margin: 0 4px" placeholder="请输入" />
         <span>元</span>
       </div>
+
+    </div>
+    <div class="form-row">
+      <label>建议频率:</label>
+      <div class="suggested-frequency-row">
+        <span>每</span>
+        <a-input v-model:value="data.frequencyType" placeholder="请输入" style="width: 100px; margin: 0 8px" />
+        <span>天</span>
+        <a-input v-model:value="data.frequencyMeasure" placeholder="请输入" style="width: 100px; margin: 0 8px" />
+        <vxe-select
+          v-model="suggestedFrequencyUnit"
+          :options="unitOptions"
+          placeholder="请选择"
+          :clearable="data.pricingType !== '0'"
+          transfer
+          style="width: 100px; margin-left: 8px"
+        />
+      </div>
+    </div>
+    <div class="form-row" v-if="data.sellType === '2'">
+      <label>服务所需时间:</label>
+      <a-input v-model:value="data.offlineDuration" placeholder="请输入" style="width: 200px" />
+      <span style="margin-left: 8px">分钟</span>
     </div>
     <div class="form-row">
       <label>功效:</label>
@@ -442,9 +509,8 @@ function handleInstitutionChange(value: string | number, node: any) {
       ></a-tree-select>
     </div>
     <div class="form-row">
-      <label><span class="required-star">*</span>供应商:</label>
+      <label>供应商:</label>
       <vxe-select v-model="data.conditioningProgramSupplierId" :options="supplierOptions" placeholder="请选择供应商" style="width: 400px" clearable class="mr-10" />
-      <a-checkbox v-model:checked="isOffline" style="margin-right: 8px" @change="toggleOnlineStatus" v-show="showOffLine"> 线下项目 </a-checkbox>
     </div>
     <div class="form-row">
       <label>图片:</label>
@@ -495,6 +561,10 @@ html,
   align-items: center;
   margin-bottom: 10px;
 }
+.suggested-frequency-row {
+  display: flex;
+  align-items: center;
+}
 .label {
   font-weight: 500;
   color: #222;

+ 11 - 8
src/service/EditSystemService.vue

@@ -94,6 +94,7 @@ const emptyRow = {
   frequencyType: '',
   frequencyTypeing: [],
   frequencyMeasure: '',
+  sellType: '',
   totalMeasure: '',
   totalPrice: '',
   initialDay: '',
@@ -111,7 +112,7 @@ const emptyRow = {
     cpDynamicPricingRule: [],
     cpMedicines: [],
     effect: '',
-    isOffline: null,
+    sellType: '',
     isDelivery: null,
     photo: '',
     institutionId: '',
@@ -224,10 +225,11 @@ function onSelectProject({ row }: any) {
     id: '',
     conditioningWrapId: '',
     conditioningProgramId: row.id,
+    sellType: row?.sellType,
     unitPrice: '-',
     days: '',
-    frequencyType: '',
-    frequencyMeasure: '',
+    frequencyType: row?.frequencyType,
+    frequencyMeasure: row?.frequencyMeasure,
     totalMeasure: '',
     totalPrice: '',
     initialDay: '',
@@ -250,7 +252,7 @@ function onSelectProject({ row }: any) {
       },
       cpDynamicPricingRule: row?.cpDynamicPricingRule,
       cpMedicines: row?.cpMedicines,
-      isOffline: row?.isOffline,
+      sellType: row?.sellType,
       isDelivery: row?.isDelivery,
       photo: row?.photo,
       conditioningProgramSupplierName: row?.conditioningProgramSupplierName,
@@ -932,10 +934,11 @@ function openPopover() {
             </div>
           </template>
         </vxe-column>
-        <vxe-column field="conditioningProgramDetail.isOffline" title="线下项目" width="100">
+        <vxe-column field="conditioningProgramDetail.sellType" title="商品类型" width="100">
+          <!-- 商品类型 1-实体商品 2-线下服务 3-线上权益 -->
           <template #default="{ row }">
-            <span v-if="row.conditioningProgramDetail?.isOffline">
-              {{ row.conditioningProgramDetail.isOffline === 'Y' ? '是' : '否' }}
+            <span v-if="row.conditioningProgramDetail?.sellType">
+              {{ row.conditioningProgramDetail.sellType === '1' ? '实体商品' : row.conditioningProgramDetail.sellType === '2' ? '线下服务' : row.conditioningProgramDetail.sellType === '3' ? '线上权益' : '-' }}
             </span>
             <span v-else>-</span>
           </template>
@@ -967,7 +970,7 @@ function openPopover() {
       :src="previewImg"
     />
     <div style="display: flex; justify-content: flex-end">
-      <span style="font-weight: bold">合计:{{ formData.price }}元</span>
+      <span style="font-weight: bold">合计:{{ formData.price?.toFixed(2) }}元</span>
     </div>
 
     <div style="display: flex; justify-content: center">

+ 1 - 1
src/service/IntroduceProjectList.vue

@@ -83,7 +83,7 @@ function addProject(row: any) {
       id: 'add-items-modal',
       title: '新增项目',
       width: 1000,
-      height: 700,
+      height: 1000,
       escClosable: true,
       destroyOnClose: true,
       slots: {

+ 476 - 0
src/service/OrderDetail.vue

@@ -0,0 +1,476 @@
+<script setup lang="ts">
+import { ref, computed, h } from 'vue';
+import { getConditioningRecordDetailMethod } from '@/request/api/care.api';
+import type { ConditioningRecordListModel, SystemCwModel } from '@/model/care.model';
+import VxeUI from 'vxe-table';
+import SingleItemDetail from '@/service/SingleItemDetail.vue';
+const props = defineProps<{
+  data: ConditioningRecordListModel;
+}>();
+const emit = defineEmits<{
+  (e: 'submit', data: SystemCwModel): void;
+  (e: 'voidSubmit', data: SystemCwModel): void;
+}>();
+let tableData = ref<any>({});
+async function getRecordDetail() {
+  try {
+    const res: any = await getConditioningRecordDetailMethod(props.data);
+    if (res) {
+      tableData.value = res;
+    }
+  } catch (error) {
+    console.error('获取数据失败', error);
+  }
+}
+const isShowDelivery = ref<boolean>(false);
+watch(tableData, (newValue) => {
+  if (newValue.items?.length > 0) {
+    isShowDelivery.value = newValue.items.some((item: any) => {
+      return item.conditioningProgramDetail?.isDelivery === 'Y';
+    });
+  }
+});
+onMounted(() => {
+  getRecordDetail();
+});
+
+
+const progressTextMap: Record<string, string> = {
+  // '0': '待付款',
+  // '1': '已作废',
+  // '2': '用户取消',
+  '3': '未开始',
+  '4': '调理中',
+  '5': '已完结',
+  // '6': '待收货',
+};
+
+const progressText = computed(() => {
+  return progressTextMap[tableData.value?.progress] || '';
+});
+
+// 订单状态映射
+const orderStatusTextMap: Record<string, string> = {
+  '1': '交易关闭(已作废)',
+  '0': '待付款',
+  '2': '交易关闭(用户取消)',
+  '6': '已付款',
+  '345': '交易成功',
+};
+
+const orderStatusText = computed(() => {
+  return orderStatusTextMap[tableData.value?.orderStatus] || '';
+});
+
+// 收货信息组合字段
+const deliveryInfo = computed(() => {
+  const parts = [
+    tableData.value?.provinceName,
+    tableData.value?.cityName,
+    tableData.value?.areaName,
+    tableData.value?.detailAddress,
+    ' ',
+    tableData.value?.liaison,
+    ' ',
+    tableData.value?.phone
+  ].filter(Boolean);
+  return parts.join(' ');
+});
+
+// 点击项目名称查看商品详情
+function handleItemNameClick(row: any) {
+  VxeUI.modal.open({
+    title: '商品详情',
+    fullscreen: true,
+    escClosable: true,
+    destroyOnClose: true,
+    id: 'item-detail-modal',
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(SingleItemDetail, {
+          data: {
+            ...row,
+          },
+        } as any);
+      },
+    },
+  });
+}
+// 添加计算数量的函数
+function calculateCount(row: any) {
+  const pricingType = row.conditioningProgramDetail.pricingType;
+  const period = Number(row.days) || 0;
+  const frequency = Number(row.frequencyMeasure) || 0;
+  const maxCount = row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.max;
+  const acCount = (row.acuMeridianNames?.length ?? 0) + (row.acuPointNames?.length ?? 0);
+  if (pricingType === '1') {
+    // 按穴位计价
+    const frequencyType = Number(row.frequencyType) || 0;
+    row.totalMeasure = Math.ceil((period / frequencyType) * frequency);
+    if (acCount > maxCount) {
+      if (row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType === 0) {
+        // 单价
+        if (acCount > 0) {
+          let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * acCount;
+          row.unitPrice = unitPrice;
+          row.totalPrice = Math.ceil((period / frequencyType) * frequency) * unitPrice;
+        } else {
+          row.unitPrice = '-';
+          row.totalPrice = 0;
+        }
+      } else if (row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType === 1) {
+        // 一口价
+        let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[1].price;
+        row.unitPrice = unitPrice;
+        row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * row.totalMeasure;
+      }
+    } else {
+      if (row.conditioningProgramDetail.cpDynamicPricingRule?.length > 0) {
+        if (row.conditioningProgramDetail.cpDynamicPricingRule?.[0]?.priceType === 0) {
+          // 单价
+          if (acCount > 0) {
+            let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * acCount;
+            row.unitPrice = unitPrice;
+            row.totalPrice = Math.ceil((period / frequencyType) * frequency) * unitPrice;
+          } else {
+            row.unitPrice = '-';
+            row.totalPrice = 0;
+          }
+        } else if (row.conditioningProgramDetail.cpDynamicPricingRule?.[0]?.priceType === 1) {
+          // 一口价
+          let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[0].price;
+          row.unitPrice = unitPrice;
+          row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * row.totalMeasure;
+        }
+      }
+    }
+  }
+}
+watch(
+  () => tableData?.value?.items,
+  (newData) => {
+    if (!newData) return;
+    newData.forEach((row: any) => {
+      if (row?.days || row?.frequencyType || row?.frequencyMeasure) {
+        calculateCount(row);
+      }
+    });
+  },
+  { deep: true }
+);
+</script>
+
+<template>
+  <div class="service-package">
+    <h2 class="title">{{ tableData?.name || tableData?.conditioningWrapName }}</h2>
+    <!-- 顶部信息 -->
+    <div class="header">
+      <span class="status" v-if="progressText">{{ progressText }}</span>
+      <div>
+        <!-- 调养记录详情 -->
+        <div style="line-height: 30px">
+          <span v-if="tableData?.healthAnalysisReport?.willillStateName" class="mr-8">欲病状态:{{
+            tableData?.healthAnalysisReport?.willillStateName }}</span>
+          <span v-if="tableData?.healthAnalysisReport?.willillDegreeName" class="mr-8">欲病程度:{{
+            tableData?.healthAnalysisReport?.willillDegreeName }}</span>
+          <span v-if="tableData?.healthAnalysisReport?.willillSocialName" class="mr-8">欲病类型:{{
+            tableData?.healthAnalysisReport?.willillSocialName }}</span>
+          <span v-if="tableData?.healthAnalysisReport?.willillFunctionName" class="mr-8">欲病表现:{{
+            tableData?.healthAnalysisReport?.willillFunctionName }}</span>
+          <span v-if="tableData?.healthAnalysisReport?.constitutionGroupName" class="mr-8">体质:{{
+            tableData?.healthAnalysisReport?.constitutionGroupName }}</span>
+          <span v-if="tableData?.diagnosis" class="mr-8">疾病名称:{{ tableData?.diagnosis }}</span>
+          <span v-if="tableData?.symptom" class="mr-8">证型:{{ tableData?.symptom }}</span>
+          <span v-if="tableData?.createBy" class="mr-8">开具医生:{{ tableData?.createBy }}</span>
+          <span v-if="tableData?.createTime" class="mr-8">开具时间:{{ tableData?.createTime }}</span>
+          <span v-if="tableData?.estimatedEndDate">调养周期:{{ tableData?.estimatedStartDate }} ~ {{
+            tableData?.estimatedEndDate }}</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 表格和作废字样包裹层 -->
+    <div class="table-wrapper">
+      <div v-if="tableData.progress === '1'" class="voided-stamp">作废</div>
+      <vxe-table :data="tableData?.items" border>
+        <vxe-column field="conditioningProgramDetail.name" title="项目名称" align="center">
+          <template #default="{ row }">
+            <a style="color: #1890ff; cursor: pointer;" @click="handleItemNameClick(row)">
+              {{ row.conditioningProgramDetail?.name }}
+            </a>
+          </template>
+        </vxe-column>
+        <vxe-column field="days" title="周期" align="center" :disabled="true" />
+        <vxe-column field="frequencyType" title="频率" width="auto">
+          <template #default="{ row }">
+            <div v-if="row.frequencyType === '不限'">不限</div>
+            <div style="display: flex; align-items: center" v-else>
+              <span>每</span>
+              <a-input v-model:value="row.frequencyType" style="width: 60px" @change="() => calculateCount(row)"
+                :disabled="true" />
+              <span>天</span>
+              <a-input v-model:value="row.frequencyMeasure" style="width: 60px" @change="() => calculateCount(row)"
+                :disabled="true" />
+              <span>{{ row.conditioningProgramDetail?.cpFixedPricingRule?.convertUnit || '次' }}</span>
+            </div>
+          </template>
+        </vxe-column>
+        <vxe-column field="conditioningProgramDetail.conditioningProgramType" title="方案类型" align="center" />
+        <vxe-column field="totalMeasure" title="数量" align="center" />
+        <vxe-column field="conditioningProgramDetail.cpFixedPricingRule.pricingUnit" title="单位" align="center">
+          <template #default="{ row }">
+            {{ row.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit ?
+              row.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit : '次' }}
+          </template>
+        </vxe-column>
+        <vxe-column field="conditioningProgramDetail.cpFixedPricingRule.unitPrice" title="单价(元)" align="center">
+          <template #default="{ row }">
+            {{ row.conditioningProgramDetail?.pricingType === '0' ?
+              row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice : row.unitPrice }}
+          </template>
+        </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="remark" title="说明" align="center" />
+      </vxe-table>
+    </div>
+
+    <!-- 合计 -->
+    <div class="total-row">
+      合计:<b>{{ tableData?.price || tableData?.cost || 0 }}元</b>
+    </div>
+
+    <!-- 订单信息 -->
+    <div class="info-section">
+      <h3 class="info-title">订单信息</h3>
+      <div class="info-content-wrapper">
+        <div class="info-content">
+          <div class="info-item" v-if="tableData?.orderNo">
+            <span class="info-label">订单编号:</span>
+            <span class="info-value">{{ tableData?.orderNo }}</span>
+          </div>
+          <div class="info-item" v-if="tableData?.createTime">
+            <span class="info-label">创建时间:</span>
+            <span class="info-value">{{ tableData?.createTime }}</span>
+          </div>
+          <div class="info-row">
+            <div class="info-item" v-if="orderStatusText">
+              <span class="info-label">订单状态:</span>
+              <span class="info-value">{{ orderStatusText }}</span>
+            </div>
+            <div class="info-item" v-if="tableData?.payTime">
+              <span class="info-label">付款时间:</span>
+              <span class="info-value">{{ tableData?.payTime }}</span>
+            </div>
+            <div class="info-item" v-if="tableData?.cost">
+              <span class="info-label">支付金额:</span>
+              <span class="info-value">¥{{ tableData?.cost }}</span>
+            </div>
+            <div class="info-item" v-if="tableData?.payType">
+              <span class="info-label">付款方式:</span>
+              <span class="info-value">{{ tableData?.payType === '0' ? '微信支付' : '' }}</span>
+            </div>
+            <div class="info-item" v-if="tableData?.payTransactionNo">
+              <span class="info-label">微信交易号:</span>
+              <span class="info-value">{{ tableData?.payTransactionNo }}</span>
+            </div>
+          </div>
+          <div class="info-row">
+            <div class="info-item mr-20" v-if="tableData?.finishTime">
+              <span class="info-label">成交时间:</span>
+              <span class="info-value">{{ tableData?.finishTime }}</span>
+            </div>
+            <div class="info-item" v-if="tableData?.cancelTime">
+              <span class="info-label">关闭时间:</span>
+              <span class="info-value">{{ tableData?.cancelTime }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 用户信息 -->
+    <div class="info-section" v-if="tableData?.patientName || (isShowDelivery && tableData.provinceName)">
+      <h3 class="info-title">用户信息</h3>
+      <div class="info-content-wrapper">
+        <div class="info-content">
+          <div class="info-item" v-if="tableData?.patientName">
+            <span class="info-label">用户姓名:</span>
+            <span class="info-value">{{ tableData?.patientName }}</span>
+          </div>
+          <div class="info-item" v-if="isShowDelivery && tableData.provinceName">
+            <span class="info-label">收货信息:</span>
+            <span class="info-value">{{ deliveryInfo }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 分账信息 -->
+    <div class="info-section"
+      v-if="tableData?.profitSharing !== null || tableData?.profitSharingAmount !== null">
+      <h3 class="info-title">分账信息</h3>
+      <div class="info-content-wrapper">
+        <div class="info-content-profit-sharing">
+          <div class="info-item" v-if="tableData?.profitSharing!==null">
+            <span class="info-label">分账比例:</span>
+            <span class="info-value">{{ tableData?.profitSharing }}</span>
+          </div>
+          <div class="info-item" v-if="tableData?.profitSharingAmount !== null">
+            <span class="info-label">分账金额:</span>
+            <span class="info-value">
+              {{ tableData?.profitSharingAmount }}元
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.table-wrapper {
+  position: relative;
+}
+.info-content-profit-sharing{
+  display: flex;
+  gap: 50px;
+}
+.voided-stamp {
+  position: absolute;
+  left: 50%;
+  top: 50px;
+  transform: translateX(-50%) rotate(-15deg);
+  color: #ff8f8d;
+  font-size: 40px;
+  font-weight: bold;
+  letter-spacing: 16px;
+  opacity: 1;
+  pointer-events: none;
+  z-index: 10;
+  user-select: none;
+  border-radius: 10px;
+  border: 5px solid #ff8f8d;
+}
+
+.service-package {
+  padding: 0 10px;
+  background: #fff;
+}
+
+.header {
+  /* display: flex;
+  align-items: center; */
+  gap: 24px;
+  margin-bottom: 12px;
+}
+
+.status {
+  background: #ff9800;
+  color: #fff;
+  padding: 8px 12px;
+  border-radius: 4px;
+  margin-right: 12px;
+  font-weight: bold;
+  width: 100px;
+  text-align: center;
+  height: 40px;
+  line-height: 40px;
+}
+
+.title {
+  text-align: center;
+  margin: 16px 0 24px 0;
+  font-size: 22px;
+  font-weight: bold;
+}
+
+.total-row {
+  display: flex;
+  justify-content: flex-end;
+  font-size: 18px;
+  font-weight: bold;
+  margin: 12px 0 24px 0;
+}
+
+.delivery {
+  display: flex;
+  align-items: center;
+  margin-bottom: 32px;
+  font-size: 16px;
+  gap: 8px;
+}
+
+.footer-btns {
+  display: flex;
+  justify-content: center;
+  margin-top: 24px;
+}
+
+.info-section {
+  margin-top: 24px;
+  margin-bottom: 24px;
+  background: #fff;
+}
+
+.info-title {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 12px;
+  color: #333;
+}
+
+.info-content-wrapper {
+  border: 1px solid #e8e8e8;
+  border-radius: 4px;
+  padding: 10px;
+}
+
+.info-content {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.info-row {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 24px;
+}
+
+.info-item {
+  display: flex;
+  align-items: center;
+  line-height: 24px;
+}
+
+.info-label {
+  color: #666;
+  margin-right: 8px;
+  white-space: nowrap;
+  /* min-width: 100px; */
+}
+
+.info-value {
+  color: #333;
+  font-weight: 400;
+}
+
+.split-count {
+  display: inline-block;
+  width: 18px;
+  height: 18px;
+  line-height: 18px;
+  text-align: center;
+  background: #ffc107;
+  color: #333;
+  border-radius: 2px;
+  margin-left: 6px;
+  font-size: 12px;
+  font-weight: 600;
+  vertical-align: middle;
+}
+</style>

+ 36 - 4
src/service/ServiceDetail.vue

@@ -76,6 +76,30 @@ const hasAnyDerivation = computed(() => {
       (rule.tabooCrowds && rule.tabooCrowds.length > 0)
   );
 });
+
+// 商品类型显示文本
+const sellTypeText = computed(() => {
+  const sellType = (props.data as any).sellType;
+  if (sellType === '1') return '实体商品';
+  if (sellType === '2') return '线下服务';
+  if (sellType === '3') return '线上权益';
+  return '';
+});
+
+// 建议频率显示文本
+const suggestedFrequencyText = computed(() => {
+  const frequencyType = (props.data as any).frequencyType; // 天数
+  const frequencyMeasure = (props.data as any).frequencyMeasure; // 次数/用量
+  const convertUnit = props.data?.cpFixedPricingRule?.convertUnit; // 单位
+  const pricingType = props.data?.pricingType;
+  
+  if (!frequencyType || !frequencyMeasure) return '';
+  
+  // 如果是一口价模式,使用 convertUnit;如果是按穴位模式,默认使用"次"
+  const unit = pricingType === '0' && convertUnit ? convertUnit : (pricingType === '1' ? '次' : '次');
+  
+  return `每 ${frequencyType} 天 ${frequencyMeasure} ${unit}`;
+});
 </script>
 
 <template>
@@ -92,6 +116,10 @@ const hasAnyDerivation = computed(() => {
       <div class="label">项目应用:</div>
       <div class="content">{{ projectApplication }}</div>
     </div>
+    <div class="detail-item" v-if="sellTypeText">
+      <div class="label">商品类型:</div>
+      <div class="content">{{ sellTypeText }}</div>
+    </div>
     <div class="detail-item">
       <div class="label">计价规则:</div>
       <div class="content">{{ data.pricingType === '0' ? '一口价' : '按穴位/脉络/部位计价' }}</div>
@@ -127,6 +155,14 @@ const hasAnyDerivation = computed(() => {
         </div>
       </div>
     </div>
+    <div class="detail-item" v-if="suggestedFrequencyText">
+      <div class="label">建议频率:</div>
+      <div class="content">{{ suggestedFrequencyText }}</div>
+    </div>
+    <div class="detail-item" v-if="(data as any)?.offlineDuration">
+      <div class="label">服务所需时间:</div>
+      <div class="content">{{ (data as any).offlineDuration }} 分钟</div>
+    </div>
     <div class="detail-item" v-if="data?.cpMedicines?.length > 0 && data?.cpMedicines[0]?.name">
       <div class="label">组成:</div>
       <div class="content" v-for="item in data?.cpMedicines" :key="item.id">{{ item.name }} {{ item.dosage ? `${item.dosage}g` : '' }};</div>
@@ -233,10 +269,6 @@ const hasAnyDerivation = computed(() => {
       <div class="label">供应商:</div>
       <div class="content">{{ data?.conditioningProgramSupplierName }}</div>
     </div>
-    <div class="detail-item" v-if="data?.isOffline === 'Y' || data?.isOffline === 'N'">
-      <div class="label">线下项目:</div>
-      <div class="content">{{ data?.isOffline === 'Y' ? '是' : '否' }}</div>
-    </div>
     <div class="detail-item" v-if="data?.isDelivery === 'Y' || data?.isDelivery === 'N'">
       <div class="label">配送:</div>
       <div class="content">{{ data?.isDelivery === 'Y' ? '支持' : '不支持' }}</div>

+ 4 - 7
src/service/ServiceItemsList.vue

@@ -247,11 +247,8 @@ function editConfirmed(model?: SystemItemModel, index?: number) {
   } else {
     VxeUI.modal.open({
       title: model?.id ? `编辑项目` : `新增项目`,
-      height: 800,
-      width: 850,
-      // position: {
-      //   top: Math.min(100, window.innerHeight * 0.1),
-      // },
+      height: 1000,
+      width: 900,
       escClosable: true,
       destroyOnClose: true,
       id: `add-items-modal`,
@@ -293,8 +290,8 @@ function seeDetail(model?: SystemItemModel, index?: number) {
     const addType = 'itemsList';
     VxeUI.modal.open({
       title: '查看',
-      height: 800,
-      width: 950,
+      height: 900,
+      width: 1000,
       escClosable: true,
       destroyOnClose: true,
       id: `service-detail-modal`,

+ 4 - 7
src/service/ServiceItemsSystem.vue

@@ -196,11 +196,8 @@ function seeItems(model?: SystemItemModel, index?: number) {
   } else {
     VxeUI.modal.open({
       title: '查看',
-      height: 600,
-      width: 850,
-      position: {
-        top: Math.min(100, window.innerHeight * 0.1),
-      },
+      height: 800,
+      width: 1000,
       escClosable: true,
       destroyOnClose: true,
       id: `service-detail-modal`,
@@ -249,8 +246,8 @@ function editItems(model?: SystemItemModel, index?: number) {
   } else {
     VxeUI.modal.open({
       title: model?.id ? `编辑项目` : `新增项目`,
-      height: 700,
-      width: 850,
+      height: 1000,
+      width: 900,
       position: {
         top: Math.min(100, window.innerHeight * 0.1),
       },

+ 1 - 25
src/service/ServicePackageDetail.vue

@@ -1,10 +1,9 @@
 <script setup lang="ts">
 import { ref, reactive, computed } from 'vue';
 import { getConditioningRecordDetailMethod, voidConditioningSchemeMethod } from '@/request/api/care.api';
-import { usePagination, useRequest } from 'alova/client';
 import type { ConditioningRecordListModel, ConditioningRecordListQuery, SystemCwModel } from '@/model/care.model';
 import CareProcess from '@/service/CareProgress.vue';
-import { message, notification } from 'ant-design-vue';
+import {  notification } from 'ant-design-vue';
 import VxeUI from 'vxe-table';
 const model = shallowRef<ConditioningRecordListQuery>();
 const props = defineProps<{
@@ -49,8 +48,6 @@ function handleChangePlan() {
 function handleViewRecord(model?: ConditioningRecordListModel) {
   VxeUI.modal.open({
     title: model?.id ? `调养过程` : `新增调养过程`,
-    // height: 700,
-    // width: 1200,
     width: window.innerWidth,
     height: window.innerHeight,
     fullscreen: true,
@@ -112,22 +109,6 @@ function calculateCount(row: any) {
   const frequency = Number(row.frequencyMeasure) || 0;
   const maxCount = row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.max;
   const acCount = (row.acuMeridianNames?.length ?? 0) + (row.acuPointNames?.length ?? 0);
-  // 一口价
-  // if (pricingType === '0') {
-  //   // 检查是否选择了"不限"
-  //   if (row.frequencyType === '不限') {
-  //     row.frequencyMeasure = ''; // 重置 frequencyMeasure
-  //     row.totalMeasure = 1;
-  //   } else {
-  //     const convertDose = Number(row.conditioningProgramDetail.cpFixedPricingRule.convertDose) || 0;
-  //     const frequencyType = Number(row.frequencyType) || 0;
-  //     row.totalMeasure = Math.ceil(((period / frequencyType) * frequency) / convertDose);
-  //   }
-  //   // 获取单价
-  //   const unitPrice = Number(row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice) || 0;
-  //   // 计算总价
-  //   row.totalPrice = (row.totalMeasure * unitPrice).toFixed(2);
-  // } else
   if (pricingType === '1') {
     // 按穴位计价
     const frequencyType = Number(row.frequencyType) || 0;
@@ -271,11 +252,6 @@ watch(
         </div>
         <!-- end -->
       </div>
-      <!-- <span v-if="tableData?.diagnosis || tableData?.diagnoseDiseaseNames?.length > 0">疾病名称:{{ tableData?.diagnosis || tableData?.diagnoseDiseaseNames?.join(',') }}</span>
-      <span v-if="tableData?.symptom || tableData?.diagnoseSyndromeNames?.length > 0">证型:{{ tableData?.symptom || tableData?.diagnoseSyndromeNames?.join(',') }}</span>
-      <span>开具医生:{{ tableData?.createBy }}</span>
-      <span>开具时间:{{ tableData?.createTime }}</span>
-      <span v-if="tableData?.estimatedEndDate">调养周期:{{ tableData?.estimatedStartDate }} - {{ tableData?.estimatedEndDate }}</span> -->
     </div>
 
     <!-- 表格和作废字样包裹层 -->

+ 723 - 0
src/service/SingleItemDetail.vue

@@ -0,0 +1,723 @@
+<script setup lang="ts">
+import type { SystemCwModel } from '@/model/care.model';
+import { computed } from 'vue';
+import { notification } from 'ant-design-vue';
+const props = defineProps<{
+  data: SystemCwModel['items'][number];
+}>();
+// 复制物流信息
+function handleCopyTracking() {
+  const trackingNumber = mockLogisticsData.value.trackingNumber;
+  if (trackingNumber) {
+    navigator.clipboard.writeText(trackingNumber).then(() => {
+      notification.success({ message: '复制成功' });
+    }).catch(() => {
+      notification.error({ message: '复制失败' });
+    });
+  }
+}
+// 分账信息数据
+const mockSplitAccountList = computed(() => {
+  return (props.data as any).profitSharings;
+});
+// 物流信息数据
+const mockLogisticsData = computed(() => {
+  console.log('props.data===', props.data);
+  return {
+    trackingNumber: `${expressTypeText[props.data.expressType || '']} ${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': '极兔速递',
+};
+// 包裹数据
+const mockPackageItems = computed(() => {
+  console.log('props.data===', props.data);
+
+  return (props.data as any).sameExpress ?? []
+});
+// 商品状态映射 - 收货状态(实体商品使用)
+const receiptStatusText: Record<string, string> = {
+  '0': '待发货',
+  '1': '已发货',
+  '2': '已收货',
+};
+// 进度状态映射(线下服务和线上权益使用)
+const progressText: Record<string, string> = {
+  '0': '进行中',
+  '1': '已完成',
+  '2': '未开始',
+};
+// 商品类型映射
+const sellTypeText: Record<string, string> = {
+  '1': '实体商品',
+  '2': '线下服务',
+  '3': '线上权益',
+};
+</script>
+
+<template>
+  <div class="service-detail">
+    <!-- 商品信息 -->
+    <div class="info-section">
+      <h3 class="info-title">商品信息</h3>
+      <div class="info-content-wrapper">
+        <div class="product-info">
+          <div class="product-image">
+            <!-- 商品图片 -->
+            <a-image v-if="data?.conditioningProgramPhoto" :width="80" :height="80" style="border-radius: 5px;"
+              :src="data?.conditioningProgramPhoto" class="product-img" />
+          </div>
+          <div class="product-details">
+            <!-- 商品名称 -->
+            <div class="product-name">{{ data?.conditioningProgramName }}</div>
+            <!-- 一口价计价 -->
+            <div class="product-spec" v-if="data?.conditioningProgramDetail?.pricingType === '0'">
+              {{ data?.convertDose }} {{ data?.convertUnit || '次' }}
+            </div>
+            <!-- 按穴位/经络/部位计价 -->
+            <div class="product-spec" v-if="data?.conditioningProgramDetail?.pricingType === '1'">
+              1次
+            </div>
+            <!-- 商品价格 -->
+            <div class="product-price" v-if="data?.unitPrice">
+              ¥{{ data?.unitPrice }}
+            </div>
+          </div>
+        </div>
+        <div class="info-content">
+          <div class="info-row">
+            <div class="info-item">
+              <span class="info-label">商品类型:</span>
+              <span class="info-value">{{ sellTypeText[data.sellType] || '-' }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">方案类型:</span>
+              <span class="info-value">{{ data.conditioningProgramType || '-' }}</span>
+            </div>
+          </div>
+          <div class="info-row">
+            <div class="info-item">
+              <span class="info-label">数量:</span>
+              <span class="info-value">{{ data?.totalMeasure || '-' }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">商品总价:</span>
+              <span class="info-value">¥{{ data?.totalPrice || '-' }}</span>
+            </div>
+            <div class="info-item" v-if="data?.sellType === '1' ? data?.receiptStatus : data?.progress">
+              <span class="info-label">商品状态:</span>
+              <span class="info-value">
+                {{ data?.sellType === '1'
+                  ? (data?.receiptStatus ? receiptStatusText[data.receiptStatus] : '-')
+                  : (data?.progress ? progressText[data.progress] : '-') }}
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 分账信息 -->
+    <div class="info-section">
+      <h3 class="info-title">分账信息</h3>
+      <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" >
+          <template #default="{ row }">
+            {{ row.profitSharingStatus === '1' ? '未分账' : row.profitSharingStatus === '2' ? '已分账' : row.profitSharingStatus === '3' ? '分账异常' : '' }}
+          </template>
+        </vxe-column>
+        <vxe-column field="profitSharing" title="分账比例" align="center">
+          <template #default="{ row }">
+            {{ row.profitSharing || '-' }}%
+          </template>
+        </vxe-column>
+        <vxe-column field="profitSharingAmount" title="预计分账金额" align="center" >
+          <template #default="{ row }">
+            {{ row.profitSharingAmount ? row.profitSharingAmount + '元' : '' }}
+          </template>
+        </vxe-column>
+        <vxe-column field="realAmount" title="到账金额" align="center">
+          <template #default="{ row }">
+            {{ row.realAmount ? row.realAmount + '元' : '' }}
+          </template>
+        </vxe-column>
+      </vxe-table>
+    </div>
+    <!-- 物流信息和包裹内商品 实体商品才显示 sellType==='1'-->
+    <div class="info-section" v-if="data.sellType === '1'">
+      <!-- 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>
+            <div class="logistics-recipient">
+              <span class="receive-icon">收</span>
+              <span class="recipient-info">
+                {{ mockLogisticsData.recipientName
+                  || '' }} {{ mockLogisticsData.recipientPhone || '' }} {{
+                  mockLogisticsData.recipientAddress || '' }}
+              </span>
+            </div>
+          </template>
+          <template v-else>
+            暂无
+          </template>
+        </div>
+        <div class="logistics-content" v-if="data.receiptType === '1'">
+          线下取货
+        </div>
+        <!-- 包裹内商品 -->
+        <div class="package-items-wrapper" v-if="mockPackageItems.length > 0">
+          <h3 class="info-title package-title" v-if="data.receiptType === '0'">
+            <span class="star-icon">★</span>
+            包裹内商品
+          </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;"
+                  :src="item.conditioningProgramPhoto" class="item-img" />
+              </div>
+              <div class="package-item-placeholder" v-else>
+                <text class="placeholder-icon">📦</text>
+              </div>
+              <div class="package-item-details">
+                <div class="package-item-name">{{ item.conditioningProgramName }}</div>
+                <div class="package-item-spec">{{ item.convertDose }} {{ item.convertUnit }}</div>
+              </div>
+              <div class="package-item-details">
+                <div class="package-item-price">¥{{ item.unitPrice }}</div>
+                <div class="package-item-quantity">x{{ item.totalMeasure }}</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="info-content-wrapper" v-else>
+        暂无
+      </div>
+    </div>
+
+    <!-- 服务记录 线下服务才显示  1-实体商品 2-线下服务 3-线上权益-->
+    <div class="info-section" v-if="data?.sellType === '2'">
+      <h3 class="info-title">
+        服务记录
+        <span class="title-count">({{ data?.patientConditioningOfflines?.length || 0 }}/{{ data?.totalMeasure || 0
+        }})</span>
+      </h3>
+      <vxe-table :data="data?.patientConditioningOfflines" border>
+        <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">
+          <template #default="{ row }">
+            {{ row.operateTime ? '已核销' : '已预约' }}
+          </template>
+        </vxe-column>
+        <vxe-column field="applyTime" title="预约时间" align="center" />
+        <vxe-column field="pieBy" title="派单员" align="center" />
+        <vxe-column field="pieTime" title="派单时间" align="center" />
+        <vxe-column field="conditioningProgramSupplierName" title="服务机构" align="center" />
+        <vxe-column field="operateTime" title="核销时间" align="center" />
+        <vxe-column field="startTime" title="操作开始时间" align="center" />
+        <vxe-column field="endTime" title="操作结束时间" align="center" />
+        <vxe-column field="operateDuration" title="治疗时长" align="center">
+          <template #default="{ row }">
+            {{ row.operateDuration ? row.operateDuration + '分钟' : '' }}
+          </template>
+        </vxe-column>
+        <vxe-column field="arrangeDuration" title="预定服务时长" align="center">
+          <template #default="{ row }">
+            {{ row.arrangeDuration ? row.arrangeDuration + '分钟' : '' }}
+          </template>
+        </vxe-column>
+        <vxe-column field="operateBy" title="操作人" align="center" />
+        <vxe-column field="feedback" title="治疗备注" align="center" />
+      </vxe-table>
+    </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;
+  }
+
+  .service-package-placeholder {
+    width: 120rpx;
+    height: 120rpx;
+    border-radius: 8rpx;
+    margin-right: 20rpx;
+    background-color: #f5f5f5;
+    border: 1px solid #e8e8e8;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+  }
+
+  .placeholder-icon {
+    font-size: 40rpx;
+    opacity: 0.3;
+  }
+
+  .info-title {
+    font-size: 16px;
+    font-weight: 600;
+    margin-bottom: 12px;
+    color: #333;
+  }
+
+  .info-content-wrapper .info-title {
+    margin-top: 20px;
+    margin-bottom: 12px;
+    padding-top: 20px;
+    border-top: 1px solid #f0f0f0;
+  }
+
+  .info-content-wrapper .info-title:first-child {
+    margin-top: 0;
+    padding-top: 0;
+    border-top: none;
+  }
+
+  .info-content-wrapper {
+    border: 1px solid #e8e8e8;
+    border-radius: 4px;
+    padding: 10px 20px;
+  }
+
+  /* 分账表格仅按内容自适应高度,避免一行数据时出现多余空白 */
+  .split-account-table {
+    :deep(.vxe-table--wrapper),
+    :deep(.vxe-table) {
+      height: auto !important;
+    }
+
+    :deep(.vxe-table--body-wrapper) {
+      height: auto !important;
+      min-height: 0 !important;
+      max-height: none !important;
+      flex: unset !important;
+    }
+  }
+
+  .product-info {
+    display: flex;
+    align-items: flex-start;
+    // margin-bottom: 20px;
+    gap: 16px;
+    padding-bottom: 10px;
+    // border-bottom: 1px solid #f0f0f0;
+  }
+
+  .product-image {
+    flex-shrink: 0;
+  }
+
+  .product-img {
+    border-radius: 4px;
+    object-fit: cover;
+  }
+
+  .product-details {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+  }
+
+  .product-name {
+    font-size: 14px;
+    font-weight: 500;
+    color: #333;
+    line-height: 20px;
+  }
+
+  .product-spec {
+    font-size: 13px;
+    color: #666;
+    line-height: 18px;
+  }
+
+  .product-price {
+    font-size: 14px;
+    font-weight: 500;
+    color: #333;
+    line-height: 20px;
+  }
+
+  .info-content {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .info-row {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 24px;
+    margin-top: 4px;
+  }
+
+  .info-item {
+    display: flex;
+    align-items: center;
+    line-height: 24px;
+  }
+
+  .info-label {
+    color: #666;
+    margin-right: 8px;
+    white-space: nowrap;
+    // min-width: 80px;
+  }
+
+  .info-value {
+    color: #333;
+    font-weight: 400;
+  }
+
+  .title-badge {
+    display: inline-block;
+    width: 18px;
+    height: 18px;
+    line-height: 18px;
+    text-align: center;
+    background: #ffc107;
+    color: #333;
+    border-radius: 2px;
+    margin-left: 6px;
+    font-size: 12px;
+    font-weight: 600;
+    vertical-align: middle;
+  }
+
+  .title-count {
+    margin-left: 8px;
+    font-size: 14px;
+    font-weight: 400;
+    color: #666;
+  }
+
+  .logistics-content {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .logistics-tracking {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    line-height: 24px;
+  }
+
+  .tracking-number {
+    font-weight: 600;
+    color: #333;
+  }
+
+  .copy-link {
+    color: #1890ff;
+    cursor: pointer;
+    text-decoration: none;
+    font-weight: 400;
+  }
+
+  .copy-link:hover {
+    text-decoration: underline;
+  }
+
+  .logistics-recipient {
+    display: flex;
+    align-items: flex-start;
+    line-height: 24px;
+  }
+
+  .receive-icon {
+    color: #ff4d4f;
+    font-weight: 500;
+    margin-right: 8px;
+    flex-shrink: 0;
+  }
+
+  .recipient-info {
+    color: #333;
+    font-weight: 400;
+    flex: 1;
+  }
+
+  .package-title {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+  }
+
+  .star-icon {
+    color: #ffc107;
+    font-size: 20px;
+    margin-right: 0;
+  }
+
+  .package-items {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    width: 25%;
+  }
+
+  .package-item {
+    display: flex;
+    align-items: flex-start;
+    gap: 12px;
+  }
+
+  .package-item-image {
+    flex-shrink: 0;
+  }
+
+  .package-item-placeholder {
+    flex-shrink: 0;
+    width: 60px;
+    height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: #f5f5f5;
+    border-radius: 4px;
+  }
+
+  .item-img {
+    border-radius: 4px;
+    object-fit: cover;
+  }
+
+  .package-item-details {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+    min-width: 0;
+  }
+
+  .package-item-name {
+    font-size: 14px;
+    font-weight: 500;
+    color: #333;
+    line-height: 20px;
+  }
+
+  .package-item-spec {
+    font-size: 13px;
+    color: #666;
+    line-height: 18px;
+  }
+
+  .package-item-price {
+    font-size: 14px;
+    font-weight: 500;
+    color: #333;
+    line-height: 20px;
+    text-align: end;
+  }
+
+  .package-item-quantity {
+    font-size: 14px;
+    color: #666;
+    // margin-left: auto;
+    flex-shrink: 0;
+    padding-top: 2px;
+    text-align: end;
+  }
+
+  .detail-item {
+    display: flex;
+    margin-bottom: 20px;
+
+    .label {
+      //   width: 80px;
+      margin-right: 10px;
+    }
+
+    .content {
+      //   flex: 1;
+
+      .service-image {
+        max-width: 200px;
+        height: auto;
+      }
+    }
+  }
+}
+
+.derivation-label {
+  font-weight: 500;
+  color: #222;
+  margin-right: 8px;
+  white-space: nowrap;
+  font-size: 13px;
+}
+
+.derivation-item {
+  display: flex;
+  align-items: center;
+  min-width: 200px;
+  max-width: 300px;
+  padding: 6px 12px;
+  background: #f5f5f5;
+  border-radius: 4px;
+  border: 1px solid #e8e8e8;
+}
+
+.derivation-content {
+  color: #555;
+  font-size: 13px;
+  flex: 1;
+  word-break: break-all;
+}
+
+.derivation-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+  align-items: flex-start;
+}
+
+.video-preview-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 120px;
+  height: 100%;
+  padding: 10px;
+  background: #fafafa;
+  border-radius: 8px;
+}
+
+.video-thumbnail {
+  position: relative;
+  width: 100%;
+  height: 100px;
+  border-radius: 6px;
+  overflow: hidden;
+  background: #000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 8px;
+}
+
+.video-thumbnail .video-preview {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.video-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.7);
+  border-radius: 6px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.video-preview-container:hover .video-overlay {
+  opacity: 1;
+}
+
+.video-overlay .ant-btn {
+  background: rgba(255, 255, 255, 0.95);
+  border: none;
+  color: #333;
+  font-size: 11px;
+  padding: 3px 8px;
+  border-radius: 4px;
+  transition: all 0.3s ease;
+  min-width: 40px;
+  height: 28px;
+  line-height: 1.2;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  white-space: nowrap;
+}
+
+.video-overlay .ant-btn:hover {
+  background: rgba(255, 255, 255, 1);
+  transform: scale(1.05);
+}
+
+.video-overlay .ant-btn-danger {
+  background: rgba(255, 77, 79, 0.95);
+  color: #fff;
+}
+
+.video-overlay .ant-btn-danger:hover {
+  background: rgba(255, 77, 79, 1);
+}
+
+.video-info {
+  text-align: center;
+  font-size: 11px;
+  color: #666;
+}
+
+.video-name {
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 2px;
+}
+
+.video-size {
+  color: #999;
+  font-size: 10px;
+}
+
+.package-items-wrapper {
+  border: 1px solid #e8e8e8;
+  padding: 10px;
+  margin-top: 10px;
+}
+</style>

+ 1 - 0
vite.config.ts

@@ -83,6 +83,7 @@ export default defineConfig((configEnv) => {
       proxy: {
         '/manager': {
           target: 'https://wx.hzliuzhi.com:4433',
+          // target:"https://test.hzliuzhi.com",
           secure: false,
           changeOrigin: true,
           logLevel: 'debug',

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini