Selaa lähdekoodia

Merge branch 'feature/equipment' of ssh://121.43.162.141:10022/six.fe/health.admin into release/2.0.1

# Conflicts:
#	src/pages/index/care/conditioningRecord.vue
#	src/service/CareProgress.vue
#	src/service/ServiceDetail.vue
#	src/service/ServicePackageDetail.vue
张田田 9 kuukautta sitten
vanhempi
commit
9779c252c3

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

@@ -20,12 +20,14 @@ declare module 'vue-router/auto-routes' {
   export interface RouteNamedMap {
     '/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
     '//care/conditioningRecord': RouteRecordInfo<'//care/conditioningRecord', '/care/conditioningRecord', Record<never, never>, Record<never, never>>,
+    '//care/configured': RouteRecordInfo<'//care/configured', '/care/configured', Record<never, never>, Record<never, never>>,
     '//care/institutionService': RouteRecordInfo<'//care/institutionService', '/care/institutionService', Record<never, never>, Record<never, never>>,
     '//care/issueService': RouteRecordInfo<'//care/issueService', '/care/issueService', Record<never, never>, Record<never, never>>,
     '//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/registe': RouteRecordInfo<'//equipment/registe', '/equipment/registe', Record<never, never>, Record<never, never>>,
     '//follow/assessment': RouteRecordInfo<'//follow/assessment', '/follow/assessment', Record<never, never>, Record<never, never>>,
     '//follow/plan': RouteRecordInfo<'//follow/plan', '/follow/plan', Record<never, never>, Record<never, never>>,
     '//follow/task': RouteRecordInfo<'//follow/task', '/follow/task', Record<never, never>, Record<never, never>>,

+ 685 - 0
src/components/EditEquirement.vue

@@ -0,0 +1,685 @@
+<script setup lang="ts">
+import { VxeUI, type VxeFormProps, type VxeFormListeners } from 'vxe-pc-ui';
+import { useRequest } from 'alova/client';
+import { addDeviceRegisterMethod, getDeviceRegisterDetailMethod } from '@/request/api/device.api';
+import { branchMethod } from '@/request/api/system.api';
+import { notification } from 'ant-design-vue';
+import { getDictionaryMethod } from '@/request/api/dictionary.api';
+import type { EquirementModel } from '@/model/device.model';
+
+type FollowModel = Partial<EquirementModel>;
+
+const defaultModel = {
+  deviceIds: [''],
+};
+
+const props = defineProps<{ data: FollowModel }>();
+
+const emits = defineEmits<{
+  submit: [data?: EquirementModel];
+}>();
+
+const model = ref<EquirementModel>({ ...defaultModel });
+
+watchEffect(() => {
+  if (props.data) {
+    model.value = { ...defaultModel, ...props.data };
+  }
+});
+// 确保processConfig始终存在
+// watchEffect(() => {
+//   if (!model.value.processConfig) {
+
+//     model.value.processConfig = {
+//       archiving: false,
+//       tongueDiagnosis: false,
+//       tongueReport: 'none' as const,
+//       pulseDiagnosis: false,
+//       pulseReport: 'none' as const,
+//       inquiry: false,
+//       healthReport: 'none' as const,
+//       conditioningPlan: 'none' as const
+//     };
+//   }
+// });
+// 获取详情
+const getDetail = async () => {
+  const res = await getDeviceRegisterDetailMethod(props.data);
+  if (res) {
+    model.value = { ...defaultModel, ...res };
+    model.value.deviceIds = [model.value.deviceCode ?? ''];
+  }
+};
+
+onMounted(() => {
+  if (props.data && props.data.id) {
+    getDetail();
+  }
+});
+
+const branch = ref<any[]>([]);
+const { loading: branchLoading } = useRequest(branchMethod(0, 1, 1)).onSuccess(({ data }) => {
+  const to = (data?: any[]): any[] => {
+    return Array.isArray(data)
+      ? data.map((item) => {
+          return {
+            ...item,
+            value: item.id,
+            key: item.id.toString(),
+            children: to(item.children),
+          };
+        })
+      : [];
+  };
+  branch.value = to(data);
+});
+
+const { loading: submitting, send: submit } = useRequest(addDeviceRegisterMethod, { immediate: false }).onSuccess(({ data }) => {
+  emits('submit');
+});
+// 获取设备类型
+const deviceTypes = ref<{ id: string; name: string }[]>([]);
+const deviceTypesLoading = ref(false);
+// 获取设备类型
+async function getDeviceType() {
+  deviceTypesLoading.value = true;
+  const res = await getDictionaryMethod('fdhb_device_type');
+  // console.log(res, '设备类型');
+  if (res && res.length > 0) {
+    deviceTypes.value = res.map((item: any) => ({
+      id: item.value,
+      name: item.label,
+    }));
+  }
+  deviceTypesLoading.value = false;
+}
+const showDept = ref(false);
+const insArr = ref<any[]>([]);
+const insLoading = ref(false);
+async function getInstitution(orgId: string | number) {
+  insLoading.value = true;
+  const res = await branchMethod(1, 0, Number(orgId));
+  console.log(res, '获取机构');
+  if (res && res.length > 0) {
+    insArr.value = res;
+  }
+  insLoading.value = false;
+}
+watch(
+  () => model.value.orgId,
+  async (newVal) => {
+    showDept.value = !!newVal;
+    if (showDept.value) {
+      // 请求获取机构
+      getInstitution(newVal ?? '');
+    }
+    if (!newVal || newVal !== model.value.institutionId) {
+      model.value.institutionId = ''; // 或者 ''
+    }
+  }
+);
+const formItems = computed(() => {
+  const baseItems: any[] = [
+    {
+      field: 'deviceType',
+      title: '设备名称',
+      span: 13,
+      itemRender: {
+        name: 'VxeSelect',
+        props: {
+          placeholder: '请选择',
+          loading: deviceTypesLoading.value,
+          options: computed(() => deviceTypes.value),
+          optionProps: { value: 'id', label: 'name' },
+          optionGroupProps: { options: 'groups' },
+          clearable: true,
+          filterable: true,
+        },
+      },
+    },
+    {
+      field: 'id',
+      title: '设备ID',
+      span: 24,
+      slots: {
+        title: 'deviceIdTitleSlot',
+        default: 'deviceIdSlot',
+      },
+    },
+    {
+      field: 'orgId',
+      title: '组织名称',
+      span: 13,
+      itemRender: {
+        name: 'VxeSelect',
+        props: {
+          placeholder: '请选择',
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: {
+            value: 'value',
+            label: 'label',
+          },
+          clearable: true,
+        },
+      },
+    },
+    // {
+    //   field: 'processConfig',
+    //   title: '',
+    //   span: 24,
+    //   slots: {
+    //     default: 'processConfigSlot',
+    //   },
+    // },
+    {
+      field: 'remarks',
+      title: '备注',
+      span: 24,
+      slots: {
+        default: 'remarksSlot',
+      },
+    },
+    { align: 'center', span: 24, slots: { default: 'active' } },
+  ];
+
+  // 如果 showDept 为 true,在 institutionId 后面插入 institutionId
+  if (showDept.value) {
+    baseItems.splice(3, 0, {
+      field: 'institutionId',
+      title: '机构名称',
+      span: 13,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          placeholder: '请选择',
+          loading: computed(() => insLoading.value),
+          options: computed(() => insArr.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+      },
+    });
+  }
+
+  return baseItems;
+});
+
+const formProps = reactive<VxeFormProps>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: computed(() => model.value),
+  items: [] as any, // 临时设置为空数组,我们将在模板中使用动态items
+  rules: {
+    deviceType: [{ required: true, message: '请选择设备名称' }],
+    deviceIds: [{ required: true, message: '请输入设备ID' }],
+    orgId: [{ required: true, message: '请选择组织名称' }],
+    institutionId: [{ required: true, message: '请选择机构名称' }],
+  },
+});
+
+const formEmits: VxeFormListeners = {
+  submit({ data }) {
+    console.log(data, '提交数据');
+
+    // 验证必填字段
+    if (!data.deviceType) {
+      notification.error({
+        message: '请选择设备名称',
+      });
+      return false; // 阻止表单提交
+    }
+
+    if (!data.deviceIds || data.deviceIds.length === 0 || data.deviceIds.every((id: string) => !id.trim())) {
+      notification.error({
+        message: '请输入设备ID',
+      });
+      return false; // 阻止表单提交
+    }
+
+    if (!data.orgId) {
+      notification.error({
+        message: '请选择组织名称',
+      });
+      return false; // 阻止表单提交
+    }
+    if (!data.institutionId) {
+      notification.error({
+        message: '请选择机构名称',
+      });
+      return false; // 阻止表单提交
+    }
+
+    if (data.id) {
+      data.deviceCode = data.deviceIds[0];
+    }
+    submit(data).then(() => {
+      notification.success({
+        message: '操作成功',
+      });
+      VxeUI.modal.close('equipment-modal');
+    });
+  },
+};
+
+function cancel() {
+  VxeUI.modal.close('equirement-modal');
+}
+
+function addDeviceId() {
+  if (!model.value.deviceIds) {
+    model.value.deviceIds = [''];
+  }
+  model.value.deviceIds.push('');
+}
+
+function removeDeviceId(index: number) {
+  if (model.value.deviceIds && model.value.deviceIds.length > 1) {
+    model.value.deviceIds.splice(index, 1);
+  }
+}
+
+onBeforeMount(async () => {
+  if (props.data) {
+    model.value = { ...defaultModel, ...props.data };
+  }
+  // 获取设备名称·
+  getDeviceType();
+});
+</script>
+
+<template>
+  <div class="form-container">
+    <vxe-form
+      :title-width="formProps.titleWidth"
+      :title-align="formProps.titleAlign"
+      :title-colon="formProps.titleColon"
+      :data="formProps.data"
+      :items="formItems"
+      :rules="formProps.rules"
+      v-on="formEmits"
+      :loading="submitting"
+    >
+      <template #deviceIdTitleSlot>
+        <span style="color: #f56c6c;font-size: 20px;">*</span> 设备ID
+      </template>
+
+      <template #deviceIdSlot>
+        <div class="device-ids-container">
+          <div v-for="(deviceId, index) in model.deviceIds || []" :key="index" class="device-id-item">
+            <vxe-input v-model="model.deviceIds[index]" placeholder="请输入" style="width: 200px" />
+            <vxe-button v-if="(model.deviceIds || []).length > 1" type="text" style="color: #ff4d4f; margin-left: 8px" @click="removeDeviceId(index)">
+              <template #default>×</template>
+            </vxe-button>
+          </div>
+          <vxe-button type="text" style="border: 1px dashed #d9d9d9; width: 32px; height: 32px; margin-bottom: 8px" @click="addDeviceId" v-if="!model?.id">
+            <template #default>+</template>
+          </vxe-button>
+        </div>
+      </template>
+
+      <!-- <template #processConfigSlot>
+        <div class="section-container">
+          <div class="section-title">流程配置</div>
+          <div class="section-divider"></div>
+          <div class="process-config">
+            <div class="config-item">
+              <span class="config-label">建档:</span>
+              <div class="radio-group">
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.archiving" 
+                    :value="true"
+                    :checked="model.processConfig?.archiving"
+                  />
+                  <span>有</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.archiving" 
+                    :value="false"
+                    :checked="!model.processConfig?.archiving"
+                  />
+                  <span>无</span>
+                </label>
+              </div>
+            </div>
+
+            <div class="config-item">
+              <span class="config-label">舌面诊:</span>
+              <div class="radio-group">
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.tongueDiagnosis" 
+                    :value="true"
+                    :checked="model.processConfig?.tongueDiagnosis"
+                    disabled
+                  />
+                  <span>有</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.tongueDiagnosis" 
+                    :value="false"
+                    :checked="!model.processConfig?.tongueDiagnosis"
+                  />
+                  <span>无</span>
+                </label>
+              </div>
+              <span class="report-label">舌面分析报告:</span>
+              <div class="radio-group">
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.tongueReport" 
+                    value="full"
+                    :checked="model.processConfig?.tongueReport === 'full'"
+                  />
+                  <span>完整展示</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.tongueReport" 
+                    value="scan"
+                    :checked="model.processConfig?.tongueReport === 'scan'"
+                  />
+                  <span>扫码查看</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.tongueReport" 
+                    value="none"
+                    :checked="model.processConfig?.tongueReport === 'none'"
+                  />
+                  <span>无</span>
+                </label>
+              </div>
+            </div>
+
+            <div class="config-item">
+              <span class="config-label">脉诊:</span>
+              <div class="radio-group">
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.pulseDiagnosis" 
+                    :value="true"
+                    :checked="model.processConfig?.pulseDiagnosis"
+                  />
+                  <span>有</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.pulseDiagnosis" 
+                    :value="false"
+                    :checked="!model.processConfig?.pulseDiagnosis"
+                  />
+                  <span>无</span>
+                </label>
+              </div>
+              <span class="report-label">脉象分析报告:</span>
+              <div class="radio-group">
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.pulseReport" 
+                    value="full"
+                    :checked="model.processConfig?.pulseReport === 'full'"
+                  />
+                  <span>完整展示</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.pulseReport" 
+                    value="scan"
+                    :checked="model.processConfig?.pulseReport === 'scan'"
+                  />
+                  <span>扫码查看</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.pulseReport" 
+                    value="none"
+                    :checked="model.processConfig?.pulseReport === 'none'"
+                  />
+                  <span>无</span>
+                </label>
+              </div>
+            </div>
+
+            <div class="config-item">
+              <span class="config-label">问诊:</span>
+              <div class="radio-group">
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.inquiry" 
+                    :value="true"
+                    :checked="model.processConfig?.inquiry"
+                  />
+                  <span>有</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.inquiry" 
+                    :value="false"
+                    :checked="!model.processConfig?.inquiry"
+                  />
+                  <span>无</span>
+                </label>
+              </div>
+            </div>
+
+            <div class="config-item">
+              <span class="config-label">健康分析报告:</span>
+              <div class="radio-group">
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.healthReport" 
+                    value="full"
+                    :checked="model.processConfig?.healthReport === 'full'"
+                  />
+                  <span>完整展示</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.healthReport" 
+                    value="scan"
+                    :checked="model.processConfig?.healthReport === 'scan'"
+                  />
+                  <span>扫码查看</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.healthReport" 
+                    value="none"
+                    :checked="model.processConfig?.healthReport === 'none'"
+                  />
+                  <span>无</span>
+                </label>
+              </div>
+            </div>
+
+            <div class="config-item">
+              <span class="config-label">调理方案:</span>
+              <div class="radio-group">
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.conditioningPlan" 
+                    value="full"
+                    :checked="model.processConfig?.conditioningPlan === 'full'"
+                  />
+                  <span>完整展示</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.conditioningPlan" 
+                    value="scan"
+                    :checked="model.processConfig?.conditioningPlan === 'scan'"
+                  />
+                  <span>扫码查看</span>
+                </label>
+                <label class="radio-item">
+                  <input 
+                    type="radio" 
+                    v-model="model.processConfig.conditioningPlan" 
+                    value="none"
+                    :checked="model.processConfig?.conditioningPlan === 'none'"
+                  />
+                  <span>无</span>
+                </label>
+              </div>
+            </div>
+          </div>
+        </div>
+      </template> -->
+
+      <template #remarksSlot>
+        <div class="section-container">
+          <textarea
+            v-model="model.remark"
+            placeholder="请输入"
+            rows="3"
+            style="width: 100%; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; resize: vertical; font-family: inherit"
+          />
+        </div>
+      </template>
+
+      <template #active>
+        <vxe-button type="reset" content="取消" :disabled="submitting" @click="cancel"></vxe-button>
+        <vxe-button type="submit" status="warning" content="确定" :loading="submitting"></vxe-button>
+      </template>
+    </vxe-form>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.form-container {
+  padding: 20px;
+}
+
+.device-ids-container {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: flex-start;
+
+  .device-id-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 8px;
+  }
+}
+
+.section-container {
+  margin-bottom: 20px;
+
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 10px;
+    color: #333;
+  }
+
+  .section-divider {
+    height: 1px;
+    background-color: #d9d9d9;
+    margin-bottom: 15px;
+  }
+}
+
+.process-config {
+  .config-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16px;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .config-label {
+      font-weight: bold;
+      min-width: 80px;
+    }
+
+    .report-label {
+      margin-left: 40px;
+      color: #666;
+      margin-right: 10px;
+    }
+
+    .radio-group {
+      display: flex;
+      align-items: center;
+      gap: 30px;
+
+      .radio-item {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        cursor: pointer;
+
+        input[type='radio'] {
+          margin: 0;
+          cursor: pointer;
+        }
+
+        span {
+          font-size: 14px;
+        }
+      }
+
+      .badge {
+        background-color: #faad14;
+        color: white;
+        border-radius: 12px;
+        padding: 2px 8px;
+        font-size: 12px;
+        min-width: 20px;
+        text-align: center;
+      }
+    }
+  }
+}
+
+:deep(.vxe-form--item) {
+  .vxe-form--item-wrapper {
+    .vxe-form--item-content {
+      .vxe-input {
+        width: 100%;
+      }
+    }
+  }
+}
+
+.required-field {
+  :deep(.vxe-form--item-title) {
+    position: relative;
+    
+    &::before {
+      content: '*';
+      color: #ff4d4f;
+      position: absolute;
+      left: -8px;
+      top: 0;
+    }
+  }
+}
+</style>

+ 111 - 0
src/components/EditMoreEquirement.vue

@@ -0,0 +1,111 @@
+<script setup lang="ts">
+import { h } from 'vue';
+import { VxeUI } from 'vxe-pc-ui';
+import EditOrganization from '@/components/EditOrganization.vue';
+import EditProcesses from '@/components/EditProcesses.vue';
+
+function editOrganization() {
+  console.log('修改所属组织');
+  VxeUI.modal.open({
+    title: '修改所属组织',
+    content: '请选择所属组织',
+    width: 800,
+    height: 750,
+    escClosable: true,
+    destroyOnClose: true,
+    id: 'edit-organization',
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(EditOrganization, {} as any);
+      },
+    },
+  });
+}
+
+function editProcessConfig() {
+  console.log('修改流程配置');
+  VxeUI.modal.open({
+    title: '修改流程配置',
+    content: '请选择流程配置',
+    width: 1400,
+    height: 750,
+    escClosable: true,
+    destroyOnClose: true,
+    id: 'edit-process-config',
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(EditProcesses, {} as any);
+      },
+    },
+  });
+}
+</script>
+
+<template>
+  <div class="form-container">
+    <div class="button-group">
+      <vxe-button status="primary" content="修改所属组织" @click="editOrganization"></vxe-button>
+      <vxe-button status="warning" content="修改流程配置" @click="editProcessConfig"></vxe-button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.form-container {
+  padding: 80px 20px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  gap: 100px;
+}
+
+.button-group {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  width: 100%;
+  max-width: 300px;
+  margin: 40px 0;
+
+  .vxe-button {
+    height: 44px;
+    font-size: 16px;
+    font-weight: 500;
+    border-radius: 8px;
+    transition: all 0.3s ease;
+    width: 100%;
+    min-width: 200px;
+    
+    &:first-child {
+      background-color: #1890ff;
+      border: 1px solid #1890ff;
+      color: #fff;
+      
+      &:hover {
+        background-color: #40a9ff;
+        border-color: #40a9ff;
+        transform: translateY(-1px);
+        box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
+      }
+    }
+    
+    &:last-child {
+      background-color: #fa8c16;
+      border: 1px solid #fa8c16;
+      color: #fff;
+      
+      &:hover {
+        background-color: #ff9c2a;
+        border-color: #ff9c2a;
+        transform: translateY(-1px);
+        box-shadow: 0 4px 12px rgba(250, 140, 22, 0.3);
+      }
+    }
+  }
+}
+</style>

+ 311 - 0
src/components/EditOrganization.vue

@@ -0,0 +1,311 @@
+<script setup lang="ts">
+import { VxeUI, type VxeFormProps, type VxeFormListeners } from 'vxe-pc-ui';
+import { useRequest } from 'alova/client';
+import { branchMethod } from '@/request/api/system.api';
+import { notification } from 'ant-design-vue';
+import type { EquirementModel } from '@/model/device.model';
+type FollowModel = Partial<EquirementModel>;
+
+const defaultModel = {};
+
+const props = defineProps<{ data: FollowModel }>();
+
+const emits = defineEmits<{
+  submit: [data?: EquirementModel];
+}>();
+
+const model = ref<EquirementModel>({ ...defaultModel });
+
+watchEffect(() => {
+  console.log(props.data, 'props.data');
+  if (props.data) {
+    model.value = { ...defaultModel, ...props.data };
+  }
+});
+// 获取组织名称
+const branch = ref<any[]>([]);
+const { loading: branchLoading } = useRequest(branchMethod(0, 1, 1)).onSuccess(({ data }) => {
+  const to = (data?: any[]): any[] => {
+    return Array.isArray(data)
+      ? data.map((item) => {
+          return {
+            ...item,
+            value: item.id,
+            key: item.id.toString(),
+            children: to(item.children),
+          };
+        })
+      : [];
+  };
+  branch.value = to(data);
+});
+// 获取机构名称
+const insArr = ref<any[]>([]);
+const insLoading = ref(false);
+async function getInstitution(orgId: string | number) {
+  insLoading.value = true;
+  const res = await branchMethod(1, 0, Number(orgId));
+  console.log(res, '获取机构');
+  if (res && res.length > 0) {
+    insArr.value = res;
+  }
+  insLoading.value = false;
+}
+const showDept = ref(false);
+watch(
+  () => model.value.orgId,
+  async (newVal) => {
+    showDept.value = !!newVal;
+    if (showDept.value) {
+      // 请求获取机构
+      getInstitution(newVal ?? '');
+    }
+    if (!newVal || newVal !== model.value.institutionId) {
+      model.value.institutionId = ''; // 或者 ''
+    }
+  }
+);
+// 表单配置
+const formItems = computed(() => {
+  const items = [
+    {
+      field: 'orgId',
+      title: '组织修改为',
+      span: 24,
+      itemRender: {
+        name: 'VxeSelect',
+        props: {
+          placeholder: '请选择',
+          options: computed(() => branch.value), // 组织列表
+          optionProps: {
+            value: 'value', // 组织id
+            label: 'label', // 组织名称
+          },
+          clearable: true,
+        },
+      },
+    },
+  ];
+
+  // 只有当选择了组织后才添加机构字段
+  if (model.value.orgId) {
+    items.push({
+      field: 'institutionId',
+      title: '机构修改为',
+      span: 24,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          placeholder: '请选择',
+          options: computed(() => insArr.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+      },
+    });
+  }
+
+  items.push(
+    {
+      field: 'devices',
+      title: '',
+      span: 24,
+      slots: {
+        default: 'devicesSlot',
+      },
+    },
+    { align: 'center', span: 24, slots: { default: 'active' } }
+  );
+
+  return items;
+});
+
+const formProps = reactive<VxeFormProps>({
+  // titleWidth: 120,
+  titleAlign: 'right',
+  titleColon: true,
+  data: computed(() => model.value),
+  items: formItems,
+});
+
+const formEmits: VxeFormListeners = {
+  submit({ data }) {
+    console.log(data, '提交数据');
+    if (!data?.orgId) {
+      notification.error({ message: '请选择需要修改的组织' });
+      return;
+    }
+    if (!data?.institutionId) {
+      notification.error({ message: '请选择需要修改的机构' });
+      return;
+    }
+    // const selectedOrg = branch.value.find((item) => item.value === model.value.orgId);
+    
+    emits('submit', {
+      orgId: data?.orgId,
+      institutionId: data?.institutionId,
+    } as any);
+    // 或者先调接口再 emit
+  },
+};
+
+// 取消
+function cancel() {
+  VxeUI.modal.close('import-organization');
+}
+
+// 添加submitting状态
+const submitting = ref(false);
+
+// const handleSubmit = () => {
+
+//   // 假设 model.value.newOrganization 是选中的组织id
+//   const selectedOrg = branch.value.find(item => item.value === model.value.newOrganization);
+//   console.log(selectedOrg, 'selectedOrg');
+//   emits('submit', {
+//     institutionId: selectedOrg.value,
+//     institutionName: selectedOrg.label
+//   });
+// };
+
+onBeforeMount(async () => {
+  // 不需要在这里处理 props.data,因为它是设备数组,不是表单数据
+});
+</script>
+
+<template>
+  <div class="form-container">
+    <vxe-form v-bind="formProps" v-on="formEmits" :loading="submitting">
+      <template #devicesSlot>
+        <div class="devices-section">
+          <div class="section-title">待修改设备</div>
+          <div class="table-container">
+            <table class="devices-table">
+              <thead>
+                <tr>
+                  <th>设备ID</th>
+                  <th>原组织</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="(device, index) in props.data" :key="index">
+                  <td>{{ device?.deviceCode }}</td>
+                  <td>{{ device?.institutionName }}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </div>
+      </template>
+
+      <template #active>
+        <div class="button-container">
+          <vxe-button content="取消" :disabled="submitting" @click="cancel"></vxe-button>
+          <vxe-button type="submit" status="warning" content="确定" :loading="submitting"></vxe-button>
+        </div>
+      </template>
+    </vxe-form>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.form-container {
+  padding: 20px;
+}
+
+.devices-section {
+  margin-top: 20px;
+
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 16px;
+    color: #333;
+  }
+
+  .table-container {
+    border: 1px solid #e8e8e8;
+    border-radius: 6px;
+    overflow: hidden;
+
+    .devices-table {
+      width: 100%;
+      border-collapse: collapse;
+      background-color: #fff;
+
+      thead {
+        background-color: #fafafa;
+
+        th {
+          padding: 12px 16px;
+          text-align: left;
+          font-weight: 500;
+          color: #333;
+          border-bottom: 1px solid #e8e8e8;
+          font-size: 14px;
+        }
+      }
+
+      tbody {
+        tr {
+          border-bottom: 1px solid #f0f0f0;
+
+          &:last-child {
+            border-bottom: none;
+          }
+
+          td {
+            padding: 12px 16px;
+            color: #666;
+            font-size: 14px;
+          }
+        }
+      }
+    }
+  }
+}
+
+.button-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 16px;
+  margin-top: 30px;
+  padding-top: 20px;
+
+  .vxe-button {
+    min-width: 80px;
+    height: 36px;
+    font-size: 14px;
+    border-radius: 6px;
+
+    &:first-child {
+      background-color: #fff;
+      border: 1px solid #d9d9d9;
+      color: #666;
+
+      &:hover {
+        border-color: #40a9ff;
+        color: #40a9ff;
+      }
+    }
+
+    &:last-child {
+      background-color: #fa8c16;
+      border: 1px solid #fa8c16;
+      color: #fff;
+
+      &:hover {
+        background-color: #ff9c2a;
+        border-color: #ff9c2a;
+      }
+
+      &:disabled {
+        background-color: #f5f5f5;
+        border-color: #d9d9d9;
+        color: #bfbfbf;
+      }
+    }
+  }
+}
+</style>

+ 556 - 0
src/components/EditProcesses.vue

@@ -0,0 +1,556 @@
+<script setup lang="ts">
+import { VxeUI, type VxeFormProps, type VxeFormListeners } from 'vxe-pc-ui';
+import { useRequest } from 'alova/client';
+import { supplierEditMethod } from '@/request/api/care.api';
+import { notification } from 'ant-design-vue';
+
+interface ProcessFormData {
+  processConfig: {
+    archiving: boolean;
+    tongueDiagnosis: boolean;
+    tongueReport: 'full' | 'scan' | 'none';
+    pulseDiagnosis: boolean;
+    pulseReport: 'full' | 'scan' | 'none';
+    inquiry: boolean;
+    healthReport: 'full' | 'scan' | 'none';
+    conditioningPlan: 'full' | 'scan' | 'none';
+  };
+  devices: Array<{
+    deviceId: string;
+    organization: string;
+  }>;
+}
+
+const defaultModel: ProcessFormData = {
+  processConfig: {
+    archiving: true,
+    tongueDiagnosis: true,
+    tongueReport: 'none',
+    pulseDiagnosis: true,
+    pulseReport: 'none',
+    inquiry: true,
+    healthReport: 'full',
+    conditioningPlan: 'full',
+  },
+  devices: [
+    { deviceId: 'sd394859285923', organization: '杭州六智科技' },
+    { deviceId: 'sd394859285922', organization: '杭州六智科技' },
+    { deviceId: 'sd394859285903', organization: '杭州六智科技' },
+  ],
+};
+
+const props = defineProps<{ data?: Partial<ProcessFormData> }>();
+const emits = defineEmits<{
+  submit: [data?: ProcessFormData];
+}>();
+
+const model = ref<ProcessFormData>({ ...defaultModel });
+
+watchEffect(() => {
+  if (props.data) {
+    model.value = { ...defaultModel, ...props.data };
+  }
+});
+
+const { loading: submitting, send: submit } = useRequest(supplierEditMethod, { immediate: false }).onSuccess(({ data }) => {
+  emits('submit');
+});
+
+const formProps = reactive<VxeFormProps>({
+  titleAlign: 'right',
+  titleColon: true,
+  data: computed(() => model.value),
+  items: [
+    {
+      field: 'content',
+      title: '',
+      span: 24,
+      slots: {
+        default: 'contentSlot',
+      },
+    },
+    { align: 'center', span: 24, slots: { default: 'active' } },
+  ],
+});
+
+const formEmits: VxeFormListeners = {
+  submit({ data }) {
+    console.log(data, '提交数据');
+    submit(data).then(() => {
+      notification.success({
+        message: '操作成功',
+      });
+      VxeUI.modal.close('edit-process-config');
+    });
+  },
+};
+
+function cancel() {
+  console.log('取消');
+  VxeUI.modal.close('edit-process-config');
+}
+
+onBeforeMount(async () => {
+  if (props.data) {
+    model.value = { ...defaultModel, ...props.data };
+  }
+});
+</script>
+
+<template>
+  <div class="form-container">
+    <vxe-form v-bind="formProps" v-on="formEmits" :loading="submitting">
+      <template #contentSlot>
+        <div class="content-layout">
+          <!-- 左侧:流程配置 -->
+          <div class="left-section">
+            <div class="section-title">流程配置</div>
+            <div class="process-config">
+              <!-- 建档 -->
+              <div class="config-item">
+                <span class="config-label">建档:</span>
+                <div class="radio-group">
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.archiving" 
+                      :value="true"
+                      :checked="model.processConfig.archiving"
+                    />
+                    <span>有</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.archiving" 
+                      :value="false"
+                      :checked="!model.processConfig.archiving"
+                    />
+                    <span>无</span>
+                  </label>
+                </div>
+              </div>
+
+              <!-- 舌面诊 -->
+              <div class="config-item">
+                <span class="config-label">舌面诊:</span>
+                <div class="radio-group">
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.tongueDiagnosis" 
+                      :value="true"
+                      :checked="model.processConfig.tongueDiagnosis"
+                      disabled
+                    />
+                    <span>有</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.tongueDiagnosis" 
+                      :value="false"
+                      :checked="!model.processConfig.tongueDiagnosis"
+                    />
+                    <span>无</span>
+                  </label>
+                </div>
+                <span class="report-label">舌面分析报告:</span>
+                <div class="radio-group">
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.tongueReport" 
+                      value="full"
+                      :checked="model.processConfig.tongueReport === 'full'"
+                    />
+                    <span>完整展示</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.tongueReport" 
+                      value="scan"
+                      :checked="model.processConfig.tongueReport === 'scan'"
+                    />
+                    <span>扫码查看</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.tongueReport" 
+                      value="none"
+                      :checked="model.processConfig.tongueReport === 'none'"
+                    />
+                    <span>无</span>
+                  </label>
+                </div>
+              </div>
+
+              <!-- 脉诊 -->
+              <div class="config-item">
+                <span class="config-label">脉诊:</span>
+                <div class="radio-group">
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.pulseDiagnosis" 
+                      :value="true"
+                      :checked="model.processConfig.pulseDiagnosis"
+                    />
+                    <span>有</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.pulseDiagnosis" 
+                      :value="false"
+                      :checked="!model.processConfig.pulseDiagnosis"
+                    />
+                    <span>无</span>
+                  </label>
+                </div>
+                <span class="report-label">脉象分析报告:</span>
+                <div class="radio-group">
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.pulseReport" 
+                      value="full"
+                      :checked="model.processConfig.pulseReport === 'full'"
+                    />
+                    <span>完整展示</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.pulseReport" 
+                      value="scan"
+                      :checked="model.processConfig.pulseReport === 'scan'"
+                    />
+                    <span>扫码查看</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.pulseReport" 
+                      value="none"
+                      :checked="model.processConfig.pulseReport === 'none'"
+                    />
+                    <span>无</span>
+                  </label>
+                </div>
+              </div>
+
+              <!-- 问诊 -->
+              <div class="config-item">
+                <span class="config-label">问诊:</span>
+                <div class="radio-group">
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.inquiry" 
+                      :value="true"
+                      :checked="model.processConfig.inquiry"
+                    />
+                    <span>有</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.inquiry" 
+                      :value="false"
+                      :checked="!model.processConfig.inquiry"
+                    />
+                    <span>无</span>
+                  </label>
+                </div>
+              </div>
+
+              <!-- 健康分析报告 -->
+              <div class="config-item">
+                <span class="config-label">健康分析报告:</span>
+                <div class="radio-group">
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.healthReport" 
+                      value="full"
+                      :checked="model.processConfig.healthReport === 'full'"
+                    />
+                    <span>完整展示</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.healthReport" 
+                      value="scan"
+                      :checked="model.processConfig.healthReport === 'scan'"
+                    />
+                    <span>扫码查看</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.healthReport" 
+                      value="none"
+                      :checked="model.processConfig.healthReport === 'none'"
+                    />
+                    <span>无</span>
+                  </label>
+                </div>
+              </div>
+
+              <!-- 调理方案 -->
+              <div class="config-item">
+                <span class="config-label">调理方案:</span>
+                <div class="radio-group">
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.conditioningPlan" 
+                      value="full"
+                      :checked="model.processConfig.conditioningPlan === 'full'"
+                    />
+                    <span>完整展示</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.conditioningPlan" 
+                      value="scan"
+                      :checked="model.processConfig.conditioningPlan === 'scan'"
+                    />
+                    <span>扫码查看</span>
+                  </label>
+                  <label class="radio-item">
+                    <input 
+                      type="radio" 
+                      v-model="model.processConfig.conditioningPlan" 
+                      value="none"
+                      :checked="model.processConfig.conditioningPlan === 'none'"
+                    />
+                    <span>无</span>
+                  </label>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 右侧:设备列表 -->
+          <div class="right-section">
+            <div class="section-title">待修改设备</div>
+            <div class="table-container">
+              <table class="devices-table">
+                <thead>
+                  <tr>
+                    <th>设备ID</th>
+                    <th>组织</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="(device, index) in model.devices" :key="index">
+                    <td>{{ device.deviceId }}</td>
+                    <td>{{ device.organization }}</td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </div>
+        </div>
+      </template>
+
+      <template #active>
+        <div class="button-container">
+          <vxe-button content="取消" :disabled="submitting" @click="cancel"></vxe-button>
+          <vxe-button type="submit" status="warning" content="确定" :loading="submitting"></vxe-button>
+        </div>
+      </template>
+    </vxe-form>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.form-container {
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+}
+
+.content-layout {
+  display: flex;
+  gap: 40px;
+  margin-bottom: 20px;
+  position: relative;
+
+  &::after {
+    content: '';
+    position: absolute;
+    left: 50%;
+    top: 0;
+    bottom: 0;
+    width: 1px;
+    background-color: #e8e8e8;
+    transform: translateX(-50%);
+  }
+
+  .left-section,
+  .right-section {
+    flex: 1;
+    
+    .section-title {
+      font-size: 16px;
+      font-weight: bold;
+      margin-bottom: 16px;
+      color: #333;
+    }
+  }
+}
+
+.process-config {
+  .config-item {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 30px;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .config-label {
+      font-weight: bold;
+      min-width: 80px;
+      color: #333;
+    }
+
+    .report-label {
+      margin-left: 40px;
+      color: #666;
+      margin-right: 10px;
+    }
+
+    .radio-group {
+      display: flex;
+      align-items: center;
+      gap: 30px;
+
+      .radio-item {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        cursor: pointer;
+
+        input[type="radio"] {
+          margin: 0;
+          cursor: pointer;
+          width: 16px;
+          height: 16px;
+        }
+
+        span {
+          font-size: 14px;
+          color: #333;
+        }
+      }
+    }
+  }
+}
+
+.table-container {
+  border: 1px solid #e8e8e8;
+  border-radius: 6px;
+  overflow: hidden;
+
+  .devices-table {
+    width: 100%;
+    border-collapse: collapse;
+    background-color: #fff;
+    border: 1px solid #e8e8e8;
+
+    thead {
+      background-color: #fafafa;
+
+      th {
+        padding: 12px 16px;
+        text-align: left;
+        font-weight: 500;
+        color: #333;
+        border-bottom: 1px solid #e8e8e8;
+        border-right: 1px solid #e8e8e8;
+        font-size: 14px;
+        
+        &:last-child {
+          border-right: none;
+        }
+      }
+    }
+
+    tbody {
+      tr {
+        border-bottom: 1px solid #f0f0f0;
+
+        &:last-child {
+          border-bottom: none;
+        }
+
+        td {
+          padding: 12px 16px;
+          color: #666;
+          font-size: 14px;
+          border-right: 1px solid #f0f0f0;
+          
+          &:last-child {
+            border-right: none;
+          }
+        }
+      }
+    }
+  }
+}
+
+.button-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 16px;
+  margin-top: 150px;
+  padding-top: 20px;
+  position: sticky;
+  bottom: 0;
+  background-color: #fff;
+  padding-bottom: 20px;
+
+  .vxe-button {
+    min-width: 80px;
+    height: 36px;
+    font-size: 14px;
+    border-radius: 6px;
+
+    &:first-child {
+      background-color: #fff;
+      border: 1px solid #d9d9d9;
+      color: #666;
+
+      &:hover {
+        border-color: #40a9ff;
+        color: #40a9ff;
+      }
+    }
+
+    &:last-child {
+      background-color: #fa8c16;
+      border: 1px solid #fa8c16;
+      color: #fff;
+
+      &:hover {
+        background-color: #ff9c2a;
+        border-color: #ff9c2a;
+      }
+
+      &:disabled {
+        background-color: #f5f5f5;
+        border-color: #d9d9d9;
+        color: #bfbfbf;
+      }
+    }
+  }
+}
+</style>

+ 360 - 0
src/components/SearchableSelect.vue

@@ -0,0 +1,360 @@
+<template>
+  <a-select
+    v-model:value="selectedValue"
+    :placeholder="placeholder"
+    :loading="isLoading"
+    :disabled="disabled"
+    :allow-clear="allowClear"
+    :show-search="true"
+    :filter-option="false"
+    :not-found-content="notFoundContent"
+    :style="{ width: width }"
+    @search="handleSearch"
+    @change="handleChange"
+    @dropdown-visible-change="handleDropdownVisibleChange"
+    @popup-scroll="handlePopupScroll"
+  >
+    <!-- 首次加载时显示加载状态 -->
+    <a-select-option v-if="isLoading && allOptions.length === 0" key="initial-loading" value="__initial_loading__" disabled>
+      <div style="text-align: center; color: #999">
+        <a-spin size="small" />
+        <span style="margin-left: 8px">加载中...</span>
+      </div>
+    </a-select-option>
+
+    <!-- 有数据时显示选项 -->
+    <a-select-option
+      v-for="item in allOptions"
+      :key="item.id"
+      :value="item.name"
+      v-memo="[item.id, item.name]"
+    >
+      {{ item.name }}
+    </a-select-option>
+
+    <a-select-option v-if="hasMore && !isPreloading" key="load-more" value="__load_more__" disabled>
+      <div @click="loadMore" style="text-align: center; color: #1890ff; cursor: pointer">
+        <span>点击加载更多</span>
+      </div>
+    </a-select-option>
+
+    <a-select-option v-if="isPreloading" key="loading" value="__loading__" disabled>
+      <div style="text-align: center; color: #999">
+        <a-spin size="small" />
+        <span style="margin-left: 8px">加载中...</span>
+      </div>
+    </a-select-option>
+
+    <a-select-option v-if="showCustomInput && customInputValue" key="custom-input" :value="customInputValue"> + 使用: "{{ customInputValue }}" </a-select-option>
+  </a-select>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, nextTick } from 'vue';
+import { cpMedicinesMethod } from '@/request/api/dictionary.api';
+interface Option {
+  [key: string]: any;
+}
+
+interface Props {
+  modelValue?: string | number;
+  options?: Option[];
+  placeholder?: string;
+  width?: string;
+  disabled?: boolean;
+  allowClear?: boolean;
+  loading?: boolean;
+  allowCustomInput?: boolean;
+  searchDebounce?: number;
+  notFoundContent?: string;
+  // 自定义字段名
+  valueKey?: string;
+  labelKey?: string;
+  keyKey?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  placeholder: '请选择或输入',
+  width: '100%',
+  disabled: false,
+  allowClear: true,
+  loading: false,
+  allowCustomInput: true,
+  searchDebounce: 300,
+  notFoundContent: '暂无数据',
+  valueKey: 'name',
+  labelKey: 'name',
+  keyKey: 'id',
+});
+
+const emit = defineEmits<{
+  'update:modelValue': [value: string | number];
+  change: [value: string | number, option: Option | null];
+  search: [keyword: string];
+}>();
+
+const selectedValue = ref<string | number>('');
+const searchKeyword = ref('');
+const currentPage = ref(1);
+const allOptions = ref<Option[]>([]);
+const hasMore = ref(false);
+const isLoading = ref(false);
+const isInitialized = ref(false);
+const isPreloading = ref(false);
+const isUpdating = ref(false); // 数据更新状态
+const lastViewedOptions = ref<Option[]>([]); // 保存上一次的浏览状态
+const lastViewedPage = ref(1); // 保存上一次的页码
+const lastViewedHasMore = ref(false); // 保存上一次的hasMore状态
+const hasInitialData = ref(false); // 标记是否已经有过初始数据
+
+// 节流函数
+const throttle = (func: Function, delay: number) => {
+  let timeoutId: number | null = null;
+  return (...args: any[]) => {
+    if (timeoutId) return;
+    timeoutId = setTimeout(() => {
+      func.apply(null, args);
+      timeoutId = null;
+    }, delay);
+  };
+};
+
+// 去重函数
+const deduplicateOptions = (options: Option[]) => {
+  const seen = new Set();
+  return options.filter((option) => {
+    const key = option.id;
+    if (seen.has(key)) {
+      return false;
+    }
+    seen.add(key);
+    return true;
+  });
+};
+
+// 批量更新数据
+const batchUpdateOptions = (newData: Option[]) => {
+  isUpdating.value = true;
+
+  // 使用 nextTick 确保批量更新
+  nextTick(() => {
+    const newOptions = [...allOptions.value, ...newData];
+    allOptions.value = deduplicateOptions(newOptions);
+    isUpdating.value = false;
+  });
+};
+
+const showCustomInput = computed(() => {
+  return props.allowCustomInput && searchKeyword.value && !allOptions.value.some((option) => option.name.toLowerCase().includes(searchKeyword.value.toLowerCase()));
+});
+
+const customInputValue = computed(() => searchKeyword.value.trim());
+
+watch(
+  () => props.modelValue,
+  (newValue) => {
+    selectedValue.value = newValue || '';
+  },
+  { immediate: true }
+);
+
+let searchTimer: number | null = null;
+const debouncedSearch = async (keyword: string) => {
+  console.log(keyword, 'keyword==>');
+  if (searchTimer) clearTimeout(searchTimer);
+
+  searchTimer = setTimeout(async () => {
+    if (!keyword.trim()) {
+      // keyword 为空时,直接请求第一页数据
+      isLoading.value = true;
+      try {
+        const result = await cpMedicinesMethod(1, 10, { keyword: '' });
+        allOptions.value = deduplicateOptions(result.data);
+        currentPage.value = 1;
+        hasMore.value = allOptions.value.length < result.total;
+
+        // 保存初始数据状态
+        hasInitialData.value = true;
+        lastViewedOptions.value = [...allOptions.value];
+        lastViewedPage.value = currentPage.value;
+        lastViewedHasMore.value = hasMore.value;
+      } catch (error) {
+        allOptions.value = [];
+      } finally {
+        isLoading.value = false;
+      }
+      return;
+    }
+
+    isLoading.value = true;
+    try {
+      const result = await cpMedicinesMethod(1, 10, { keyword });
+      console.log(result, 'result==>');
+      allOptions.value = deduplicateOptions(result.data);
+      currentPage.value = 1;
+      hasMore.value = allOptions.value.length < result.total;
+
+      // 保存搜索结果的浏览状态
+      hasInitialData.value = true;
+      lastViewedOptions.value = [...allOptions.value];
+      lastViewedPage.value = currentPage.value;
+      lastViewedHasMore.value = hasMore.value;
+      console.log('搜索成功保存状态:', lastViewedOptions.value.length, '条数据');
+    } catch (error) {
+      console.error('搜索失败:', error);
+      allOptions.value = [];
+    } finally {
+      isLoading.value = false;
+    }
+  }, props.searchDebounce);
+};
+
+const handleSearch = (value: string) => {
+  searchKeyword.value = value;
+  debouncedSearch(value);
+  emit('search', value);
+};
+
+const handleChange = (value: any) => {
+  console.log(value, 'value==>', allOptions.value);
+  if (value === '__load_more__' || value === '__initial_loading__' || value === '__loading__' || value === undefined) return;
+
+  selectedValue.value = value;
+  emit('update:modelValue', value);
+
+  const selectedOption = allOptions.value.find((option) => option.name === value) || null;
+  console.log(selectedOption, 'selectedOption==>', value);
+  emit('change', value, selectedOption);
+};
+
+const handleDropdownVisibleChange = (visible: boolean) => {
+  console.log(visible, 'visible==>', allOptions.value.length, isLoading.value);
+
+  if (visible) {
+    // 首次展开时,如果没有数据就显示加载状态
+    if (!isInitialized.value && allOptions.value.length === 0) {
+      isLoading.value = true;
+      cpMedicinesMethod(1, 10, { keyword: searchKeyword.value })
+        .then((result) => {
+          console.log(result, '首次展开获取的result');
+          if (result && result.data && result.data.length > 0) {
+            allOptions.value = deduplicateOptions(result.data);
+            currentPage.value = 1;
+            hasMore.value = allOptions.value.length < result.total;
+            isInitialized.value = true;
+
+            // 保存初始数据状态
+            hasInitialData.value = true;
+            lastViewedOptions.value = [...allOptions.value];
+            lastViewedPage.value = currentPage.value;
+            lastViewedHasMore.value = hasMore.value;
+            console.log('首次加载保存状态:', lastViewedOptions.value.length, '条数据');
+          }
+        })
+        .catch((error) => {
+          console.error('首次加载失败:', error);
+          allOptions.value = [];
+        })
+        .finally(() => {
+          isLoading.value = false;
+        });
+    }
+  } else {
+    // 关闭下拉框时,保存当前状态作为上一次浏览状态
+    console.log('关闭下拉框,当前数据条数:', allOptions.value.length);
+    if (allOptions.value.length > 0) {
+      hasInitialData.value = true;
+      lastViewedOptions.value = [...allOptions.value];
+      lastViewedPage.value = currentPage.value;
+      lastViewedHasMore.value = hasMore.value;
+      console.log('保存状态:', lastViewedOptions.value.length, '条数据');
+    }
+    searchKeyword.value = '';
+  }
+};
+
+// 节流处理的滚动事件
+const throttledScroll = throttle((e: Event) => {
+  const target = e.target as HTMLElement;
+  const { scrollTop, scrollHeight, clientHeight } = target;
+
+  // 提前触发加载(距离底部80px时就开始加载)
+  if (scrollHeight - scrollTop <= clientHeight + 80 && hasMore.value && !isLoading.value && !isPreloading.value && !isUpdating.value) {
+    loadMore();
+  }
+}, 50);
+
+const handlePopupScroll = (e: Event) => {
+  throttledScroll(e);
+};
+
+const loadMore = async () => {
+  if (isLoading.value || !hasMore.value || isPreloading.value || isUpdating.value) return;
+
+  isPreloading.value = true;
+  try {
+    const nextPage = currentPage.value + 1;
+    console.log('当前页:', currentPage.value, '下一页:', nextPage);
+    console.log('当前数据条数:', allOptions.value.length);
+
+    const result = await cpMedicinesMethod(nextPage, 10, { keyword: searchKeyword.value });
+    console.log('新获取数据:', result.data);
+    console.log('新数据条数:', result.data.length);
+
+    // 批量更新数据,避免逐条渲染
+    batchUpdateOptions(result.data);
+
+    console.log('合并后数据条数:', allOptions.value.length);
+
+    // 立即更新状态
+    currentPage.value = nextPage;
+    hasMore.value = allOptions.value.length < result.total;
+
+    // 更新上一次浏览状态
+    hasInitialData.value = true;
+    lastViewedOptions.value = [...allOptions.value];
+    lastViewedPage.value = currentPage.value;
+    lastViewedHasMore.value = hasMore.value;
+    console.log('loadMore保存状态:', lastViewedOptions.value.length, '条数据');
+  } catch (error) {
+    console.error('加载更多失败:', error);
+  } finally {
+    isPreloading.value = false;
+  }
+};
+
+const resetOptions = () => {
+  allOptions.value = [];
+  currentPage.value = 1;
+  hasMore.value = false;
+  isInitialized.value = false;
+  isPreloading.value = false;
+  isUpdating.value = false;
+  // 清空上一次浏览状态
+  hasInitialData.value = false;
+  lastViewedOptions.value = [];
+  lastViewedPage.value = 1;
+  lastViewedHasMore.value = false;
+};
+
+defineExpose({
+  resetOptions,
+  loadMore,
+  search: handleSearch,
+});
+</script>
+
+<style scoped>
+:deep(.ant-select-dropdown) {
+  max-height: 300px;
+}
+
+:deep(.ant-select-item-option-disabled) {
+  cursor: pointer !important;
+  color: inherit !important;
+}
+
+:deep(.ant-select-item-option-disabled:hover) {
+  background-color: #f5f5f5 !important;
+}
+</style>

+ 71 - 20
src/libs/v-select-page/RemoteSelect.vue

@@ -1,18 +1,20 @@
 <script setup lang="ts">
 import type { FetchDataCallback, FetchSelectedDataCallback, PageParameters, SelectPageKey } from 'v-selectpage';
 import { SelectPageList } from 'v-selectpage';
+import { ref } from 'vue';
 
 const props = withDefaults(
   defineProps<{
     load(page: number, size: number, query?: Record<string, any>): Promise<{ data: any[]; total: number }>;
     query?: Record<string, any>;
-
     keyProp?: string;
     labelProp?: string;
+    allowCustom?: boolean; // 是否允许自定义输入
   }>(),
   {
     keyProp: 'id',
     labelProp: 'name',
+    allowCustom: true,
   }
 );
 
@@ -27,22 +29,20 @@ const attrs = useAttrs() as {
   debounce?: number;
 };
 
-const [modelValue, modifiers] = defineModel<any, 'complex'>('value', {
+const [modelValue] = defineModel<any, 'complex'>('value', {
   get(v: any) {
     if (!v || !v?.length) return [];
-    //     if (!Array.isArray(v)) v = [v];
-
-    // return v;
     return Array.isArray(v) ? v : [v];
   },
   set(v: SelectPageKey[]) {
-    // return v[0];
     return attrs.multiple ? v : v[0];
   },
 });
 
+
 async function fetchData(data: PageParameters, callback: FetchDataCallback) {
   const { search: keyword, pageNumber, pageSize } = data;
+  
   try {
     const { data, total } = await props.load(pageNumber, pageSize, { ...props.query, keyword });
     callback(data, total);
@@ -52,12 +52,11 @@ async function fetchData(data: PageParameters, callback: FetchDataCallback) {
 }
 
 async function fetchSelectedData(keys: SelectPageKey[], callback: FetchSelectedDataCallback) {
- 
   if (attrs.multiple) {
     callback(
       keys.map((key) => ({
         [props.labelProp]: key,
-        [props.labelProp]: key,
+        [props.keyProp]: key,
       }))
     );
   } else {
@@ -77,21 +76,27 @@ function onSelect(items: Record<string, unknown>[]) {
     emits('update', items.length > 0 ? items[0] : null);
   }
 }
+
+
+
 </script>
+
 <template>
-  <SelectPageList ref="el"
-    language="zh-chs"
-    :debounce="attrs.debounce ?? 300"
-    :multiple="attrs.multiple"
-    :key-prop="props.keyProp"
-    :label-prop="props.labelProp"
-    v-model="modelValue"
-    @fetch-data="fetchData"
-    @fetch-selected-data="fetchSelectedData"
-    @selection-change="onSelect"
-    @visible-change="$event ? emits('focus') : emits('blur')"
-  />
+    <SelectPageList 
+      ref="el"
+      language="zh-chs"
+      :debounce="attrs.debounce ?? 300"
+      :multiple="attrs.multiple"
+      :key-prop="props.keyProp"
+      :label-prop="props.labelProp"
+      v-model="modelValue"
+      @fetch-data="fetchData"
+      @fetch-selected-data="fetchSelectedData"
+      @selection-change="onSelect"
+      @visible-change="$event ? emits('focus') : emits('blur')"
+    />
 </template>
+
 <style lang="scss">
 .v-dropdown-trigger {
   width: 100%;
@@ -118,4 +123,50 @@ function onSelect(items: Record<string, unknown>[]) {
     color: #e0e0e0 !important;
   }
 }
+
+.enhanced-remote-select {
+  position: relative;
+  
+  .custom-input-option {
+    position: absolute;
+    top: 100%;
+    left: 0;
+    right: 0;
+    background: white;
+    border: 1px solid #d9d9d9;
+    border-top: none;
+    border-radius: 0 0 6px 6px;
+    padding: 8px 12px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    color: #1890ff;
+    font-size: 14px;
+    z-index: 1000;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    
+    &:hover {
+      background-color: #f5f5f5;
+    }
+    
+    .anticon {
+      font-size: 12px;
+    }
+  }
+}
+
+// 改进下拉选项样式
+:deep(.sp-dropdown) {
+  .sp-option {
+    &:hover {
+      background-color: #f5f5f5;
+    }
+    
+    &.sp-selected {
+      background-color: #e6f7ff;
+      color: #1890ff;
+    }
+  }
+}
 </style>

+ 2 - 0
src/libs/vxe/plugin.ts

@@ -30,6 +30,7 @@ import {
   VxeUI,
   VxeRow,
   VxeCol,
+  VxeDateRangePicker,
 } from 'vxe-pc-ui';
 
 import zhCN from 'vxe-pc-ui/lib/language/zh-CN';
@@ -47,6 +48,7 @@ function LazyVxeUIForForm(app: App) {
   app.use(VxeNumberInput);
   app.use(VxeDatePanel);
   app.use(VxeDatePicker);
+  app.use(VxeDateRangePicker);
   app.use(VxeRadio);
   app.use(VxeSwitch);
   app.use(VxeSelect);

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

@@ -30,10 +30,19 @@ export interface SystemItemModel {
   status?: string; // 项目状态
   conditioningProgramSupplierName?: string; // 供应商名称
   conditioningProgramSupplierId?: string; // 供应商ID
+  cpPatientMatchRule: {
+    sex: string; // 性别
+    age: string; // 年龄
+    diagnoseDiseaseNames: string[]; // 疾病
+    diagnoseSyndromeNames: string[]; // 证型
+    constitutionGroupNames: string[]; // 体质
+    willillStateNames: string[]; // 欲病状态
+    tabooCrowds: string[]; // 禁忌
+  };
   cpFixedPricingRule: {
-    unitPrice: number; // 单价
+    unitPrice: string; // 单价
     pricingUnit: string; // 计价单位
-    convertDose: number; // 转换剂量
+    convertDose: string; // 转换剂量
     convertUnit: string; // 转换单位
   };
   cpDynamicPricingRule: {
@@ -47,10 +56,21 @@ export interface SystemItemModel {
     name: string; // 中药标准名称
     dosage: string; // 剂量
   }[];
+  isForWrap?: string | null; // 是否为服务包项目
+  isForInfer?: string | null; // 是否为调理方案项目
   effect: string; // 功效
+  itemImgFirst: string; // 操作图片
+  itemVideoFirst: string; // 操作视频
+  attrFirst: string; // 特色
   isOffline?: string | null; // 是否线下
   isDelivery?: string | null; // 是否配送
-  photo: string; // 图片
+  photo: string; // 商品图片
+  attrSeventh: string; //使用注意
+  attrSixth: string; // 疗程说明
+  attrFifth: string; // 操作方法
+  attrFourth: string; // 用法
+  attrThird: string; // 制法
+  attrSecond: string; // 功效
 }
 
 export type SystemIteQuery = Partial<SystemItemModel>;
@@ -232,7 +252,7 @@ export interface OpenConditioningSchemeModel {
   symptom: string; // 症型
   conditioningWrapId: string; // 调理包ID
   conditioningWrapName: string; // 调理包名称
-  photo: string; // 调理包照片	
+  photo: string; // 调理包照片
   isDelivery?: string | null; // 是否配送 Y N
   estimatedStartDate: string; // 调养日期
   estimatedEndDate: string; // 调养结束日期
@@ -327,3 +347,26 @@ export interface OpenConditioningSchemeModel {
     remark: string; // 说明
   }[];
 }
+
+// 调理方案配置
+export interface ConditioningSchemeModel {
+  id: string | number; // 调理方案配置ID
+  orgId: string | number; // 组织id
+  orgName: string; // 组织名称
+  isConfig: string; // 是否配置
+  configTimeStart: string; // 配置开始时间
+  configTimeEnd: string; // 配置结束时间
+  isHaveForInfer: string; // 是否定制项目
+  forInferCount: number; // 定制项目数量
+  status: string; // 启用状态
+  updateBy: string; // 修改人
+  updateTime: string; // 修改时间
+  items: {
+    conditioningProgramType: string; // 调理方案类型
+    isShowForInfer: string; // 是否展示定制项目
+    knowledgeCpShowType: string; // 智能推荐项目展示方式 1-展示 2-不展示 3-定制项目无结果时展示
+    showCount: number; // 展示数量
+    
+  }[];
+}
+export type ConditioningSchemeQuery = Partial<ConditioningSchemeModel>;

+ 24 - 0
src/model/device.model.ts

@@ -0,0 +1,24 @@
+export interface EquirementModel {
+  id?: string; // 设备ID
+  deviceType?: string; // 	健康管家设备类型
+  deviceCode?: string; // 设备ID(即设备编码)
+  deviceIds?: string[]; // 设备ID(即设备编码)
+  institutionId?: string; // 组织id
+  institutionName?: string; // 组织名称
+  orgId?: string; // 机构id
+  orgName?: string; // 机构名称
+  status?: number; // 状态
+  updateBy?: string; // 修改人
+  updateTime?: string; // 修改时间
+  remark?: string; // 备注
+  createTime?: string; // 创建时间
+  createBy?: string; // 创建人
+  createIdBy?: string; // 创建人id
+  updateTimeStart?: string; // 修改时间开始
+  updateTimeEnd?: string; // 修改时间结束
+  isHaveHealthAnalysisReport?: boolean; // 是否配置
+}
+export type EquirementQuery = Partial<EquirementModel>;
+
+
+

+ 11 - 17
src/pages/index/care/conditioningRecord.vue

@@ -207,6 +207,16 @@ function serviceDetail(model?: ConditioningRecordListModel, index?: number) {
     },
   });
 }
+const progressTextMap: Record<string, string> = {
+  '0': '待付款',
+  '1': '已作废',
+  '2': '用户取消',
+  '3': '未开始',
+  '4': '调理中',
+  '5': '已完结'
+};
+
+
 </script>
 <template>
   <div class="page-container flex flex-col">
@@ -218,23 +228,7 @@ function serviceDetail(model?: ConditioningRecordListModel, index?: number) {
         <template #cell="{ row }">{{ row.progress === 1 ? '已开始' : row.progress === 2 ? '已结束' : '未开始' }} </template>
         <template #patients="{ row }">
           <div>
-            {{
-              row.progress === '0'
-                ? '待付款'
-                : row.progress === '1'
-                  ? '已作废'
-                  : row.progress === '2'
-                    ? '用户取消'
-                    : row.progress === '3'
-                      ? '未开始'
-                      : row.progress === '4'
-                        ? '调理中'
-                        : row.progress === '5'
-                          ? '已完结'
-                          : row.progress === '6'
-                            ? '待收货'
-                            : ''
-            }}
+            {{ progressTextMap[row.progress] || '' }}
           </div>
         </template>
         <template #toolbar-extra>

+ 293 - 0
src/pages/index/care/configured.vue

@@ -0,0 +1,293 @@
+<script setup lang="ts">
+import { type VxeFormListeners, type VxeFormProps, type VxeGridInstance, type VxeGridListeners, type VxeGridProps, VxeUI } from 'vxe-pc-ui';
+import { usePagination, useRequest } from 'alova/client';
+import { notification } from 'ant-design-vue';
+import { getDictionaryMethod } from '@/request/api/dictionary.api';
+import dayjs from 'dayjs';
+import EditConfigured from '@/service/EditConfigured.vue';
+// model
+import type { ConditioningSchemeModel, ConditioningSchemeQuery } from '@/model/care.model';
+
+// 接口数据
+import { getConditioningSchemeMethod } from '@/request/api/care.api';
+import { branchMethod } from '@/request/api/system.api';
+
+// 获取组织名称
+const { data: branch, loading: branchLoading } = useRequest(branchMethod(0, 1, 1));
+
+const model = shallowRef<ConditioningSchemeQuery>();
+
+// 获取设备类型
+const deviceTypes = ref<{ id: string; name: string }[]>([]);
+const deviceTypesLoading = ref(false);
+
+// 日期验证
+const configTimeStart = ref<string>('');
+const configTimeEnd = ref<string>('');
+// 禁用结束时间的日期(早于开始时间的日期)
+function disabledEndDate(current: any) {
+  if (!configTimeStart.value) return false;
+  return current && current < dayjs(configTimeStart.value);
+}
+
+const searchFormProps = reactive<VxeFormProps<ConditioningSchemeQuery>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: {
+    // isConfig: 'Y',
+  },
+  items: [
+    {
+      field: 'orgId',
+      title: '组织名称',
+      span: 5,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+      },
+    },
+    {
+      field: 'configTimeStart',
+      title: '配置时间',
+      span: 10,
+      slots: {
+        default: 'createTimes',
+      },
+    },
+    {
+      field: 'isConfig',
+      title: '是否配置',
+      span: 6,
+      itemRender: {
+        name: 'VxeRadioGroup',
+        options: [
+          { label: '已配置', value: 'Y' },
+          { label: '未配置', value: 'N' },
+        ],
+        props: {
+          strict: false,
+        },
+      },
+    },
+    {
+      span: 3,
+      itemRender: {
+        name: 'VxeButtonGroup',
+        options: [
+          { name: 'submits', type: 'submit', content: '查询', status: 'primary' },
+          { name: 'reset', type: 'reset', content: '清空', status: 'warning' },
+        ],
+      },
+    },
+  ],
+});
+const searchFormEmits: VxeFormListeners<ConditioningSchemeQuery> = {
+  // 查询设备登记
+  submit({ data }) {
+    model.value = {
+      ...data,
+      configTimeStart: configTimeStart.value ? dayjs(configTimeStart.value).format('YYYY-MM-DD HH:mm:ss') : '',
+      configTimeEnd: configTimeEnd.value ? dayjs(configTimeEnd.value).format('YYYY-MM-DD HH:mm:ss') : '',
+    } as any;
+  },
+  // 重置
+  reset({ data }) {
+    model.value = { ...data } as any;
+    configTimeStart.value = '';
+    configTimeEnd.value = '';
+  },
+};
+// 设备列表
+const gridRef = ref<VxeGridInstance<ConditioningSchemeModel>>();
+const gridOptions = reactive<VxeGridProps<ConditioningSchemeModel>>({
+  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: [
+    { field: 'orgName', title: '组织名称' },
+    {
+      field: 'isHaveForInfer',
+      title: '是否定制项目',
+      slots: {
+        default: 'isHaveForInferSlot',
+      },
+    },
+    { field: 'forInferCount', title: '定制项目数' },
+
+    {
+      field: 'isConfig',
+      title: '是否配置',
+      slots: {
+        default: 'isConfiguredSlot',
+      },
+    },
+    { field: 'updateBy', title: '修改人' },
+    { field: 'updateTime', title: '修改时间' },
+    {
+      field: 'action',
+      title: '操作',
+      align: 'center',
+      width: 120,
+      showOverflow: false,
+      cellRender: {
+        name: 'VxeButtonGroup',
+        props: {
+          mode: 'text',
+        },
+        options: [{ content: '编辑', status: 'primary', name: 'EditConfigured' }],
+        events: {
+          click({ row, rowIndex }, { name }) {
+            let method;
+            if (name === 'EditConfigured') {
+              method = editConfigured;
+            }
+            method?.(row, rowIndex);
+          },
+        },
+      },
+    },
+  ],
+  data: [],
+});
+const gridEvents: VxeGridListeners = {};
+
+// 获取设备分页列表
+const { loading, page, pageSize, total, onSuccess, replace, refresh, remove } = usePagination((page, size) => getConditioningSchemeMethod(page, size, model.value), {
+  initialData: { data: [], total: 0 },
+  initialPage: 1,
+  initialPageSize: 100,
+  watchingStates: [model],
+  immediate: false,
+});
+onSuccess(({ data: { data } }) => {
+  gridRef.value?.loadData(data);
+});
+
+// 获取设备类型
+async function getDeviceType() {
+  deviceTypesLoading.value = true;
+  const res = await getDictionaryMethod('fdhb_device_type');
+  console.log(res, '设备类型');
+  if (res && res.length > 0) {
+    deviceTypes.value = res.map((item: any) => ({
+      id: item.label,
+      name: item.label,
+    }));
+  }
+  deviceTypesLoading.value = false;
+}
+
+onMounted(() => {
+  getDeviceType();
+  model.value = toRaw(searchFormProps.data);
+});
+
+// 编辑配置
+function editConfigured(model?: ConditioningSchemeModel, index?: number) {
+  VxeUI.modal.open({
+    title: model?.id ? `修改配置` : `新增配置`,
+    height: 750,
+    width: 1100,
+    escClosable: true,
+    destroyOnClose: true,
+    id: `configured-modal`,
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(EditConfigured, <any>{
+          data: model,
+          onSubmit(data: ConditioningSchemeModel) {
+            refresh(page.value);
+            VxeUI.modal.close(`configured-modal`);
+          },
+        });
+      },
+    },
+  });
+}
+</script>
+<template>
+  <div class="page-container flex flex-col">
+    <header class="flex-none mt-4">
+      <vxe-form v-bind="searchFormProps" v-on="searchFormEmits">
+        <template #createTimes>
+          <div class="date-range-container">
+            <a-date-picker v-model:value="configTimeStart" placeholder="请选择开始时间" style="flex: 1" :show-time="{ format: 'HH:mm' }" />
+            <span class="date-separator">至</span>
+            <a-date-picker v-model:value="configTimeEnd" placeholder="请选择结束时间" style="flex: 1" :disabledDate="disabledEndDate" :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 #isConfiguredSlot="{ row }">
+          <div :class="row.isConfig === 'Y' ? 'text-green-500' : 'text-red-500'">{{ row.isConfig === 'Y' ? '已配置' : '未配置' }}</div>
+        </template>
+        <template #isHaveForInferSlot="{ row }">
+          <div>{{ row.isHaveForInfer === 'Y' ? '定制' : '无定制' }}</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>

+ 7 - 6
src/pages/index/care/issueService.vue

@@ -159,6 +159,7 @@ function getPatientRecord(id: any) {
 // const report=ref({})
 async function getCpRecordDetail(id: string) {
   await getCpDetailMethod({ id }).then((res) => {
+    console.log(res,"获取调养记录");
     formData.items = res?.items ?? [];
     // form = res;
     form.conditioningWrapName = res?.conditioningWrapName;
@@ -169,10 +170,10 @@ async function getCpRecordDetail(id: string) {
     form.areaName = res?.areaName;
     form.detailAddress = res?.detailAddress;
     form.phone = res?.phone;
-    form.healthAnalysisReport = res.healthAnalysisReport;
-    selectedProvince.value=res.provinceCode;
-    selectedCity.value=res.cityCode;
-    selectedArea.value=res.areaCode
+    form.healthAnalysisReport = res?.healthAnalysisReport;
+    selectedProvince.value=res?.provinceCode;
+    selectedCity.value=res?.cityCode;
+    selectedArea.value=res?.areaCode
   });
 }
 function getPatientList(id: string) {
@@ -670,7 +671,7 @@ const selectedArea = ref('');
 
 async function loadProvinces() {
   const res: any = await getProvinceMethod();
-  provinceOptions.value = res.map((item) => ({
+  provinceOptions.value = res.map((item: any) => ({
     value: item.code,
     label: item.name,
   }));
@@ -685,7 +686,7 @@ async function loadCities(name: string, provincecode: string) {
     return;
   }
   const res: any = await getCityMethod(name, provincecode);
-  cityOptions.value = res.map((item) => ({
+  cityOptions.value = res.map((item: any) => ({
     value: item.code,
     label: item.name,
   }));

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

@@ -0,0 +1,440 @@
+<script setup lang="ts">
+import { type VxeFormListeners, type VxeFormProps, type VxeGridInstance, type VxeGridListeners, type VxeGridProps, VxeUI } from 'vxe-pc-ui';
+import { list2Groups } from '@/tools/data';
+import { usePagination, useRequest } from 'alova/client';
+import { notification } from 'ant-design-vue';
+import { DatePicker } from 'ant-design-vue';
+import { getDictionaryMethod } from '@/request/api/dictionary.api';
+import dayjs from 'dayjs';
+import EditEquirement from '@/components/EditEquirement.vue';
+import EditMoreEquirement from '@/components/EditMoreEquirement.vue';
+import EditOrganization from '@/components/EditOrganization.vue';
+
+// model
+import type { EquirementModel, EquirementQuery } from '@/model/device.model';
+
+// 接口数据
+import { planDeleteMethod, planUpdateStatusMethod, allTagsSearchMethod } from '@/request/api/follow.api';
+import { getDeviceRegisterMethod, deleteDeviceRegisterMethod, updateDeviceRegisterOrganizationMethod } from '@/request/api/device.api';
+import { branchMethod } from '@/request/api/system.api';
+// 获取设备列表
+const { data: tags, loading: tagsLoading } = useRequest(getDeviceRegisterMethod, {
+  initialData: { total: 0, data: [] },
+});
+const { data: branch, loading: branchLoading } = useRequest(branchMethod(0, 1, 1));
+
+// 获取组织名称
+const { data: organizationData, loading: organizationDataLoading } = useRequest(allTagsSearchMethod, {
+  initialData: { total: 0, data: [] },
+});
+
+const model = shallowRef<EquirementQuery>();
+
+// 获取设备类型
+const deviceTypes = ref<{ id: string; name: string }[]>([]);
+const deviceTypesLoading = ref(false);
+
+// 日期验证
+const updateTimeStart = ref<string>('');
+const updateTimeEnd = ref<string>('');
+// 禁用结束时间的日期(早于开始时间的日期)
+function disabledEndDate(current: any) {
+  if (!updateTimeStart.value) return false;
+  return current && current < dayjs(updateTimeStart.value);
+}
+
+const searchFormProps = reactive<VxeFormProps<EquirementQuery>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: {},
+  items: [
+    {
+      field: 'deviceType',
+      title: '设备名称',
+      span: 6,
+      itemRender: {
+        name: 'VxeSelect',
+        props: {
+          placeholder: '请选择',
+          loading: deviceTypesLoading.value,
+          options: computed(() => deviceTypes.value),
+          optionProps: { value: 'id', label: 'name' },
+          optionGroupProps: { options: 'groups' },
+          clearable: true,
+          filterable: true,
+        },
+      },
+    },
+    {
+      field: 'deviceCode',
+      title: '设备ID',
+      span: 6,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'orgId',
+      title: '组织名称',
+      span: 6,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+        events: {
+          change(val: any) {
+            insArr.value = [];
+            console.log(val.data.orgId, '组织');
+            if (val.data.orgId) {
+              // 清空表单中的机构名称字段
+              if (model.value) {
+                model.value.institutionId = '';
+              }
+              getInstitution(val.data.orgId);
+            }
+          }
+        }
+      }
+    },
+    {
+      span: 6,
+      itemRender: {
+        name: 'VxeButtonGroup',
+        options: [
+          { name: 'submits', type: 'submit', content: '查询', status: 'primary' },
+          { name: 'reset', type: 'reset', content: '清空', status: 'warning' },
+          { name: 'add', content: '新增', status: 'primary' },
+          { name: 'import', content: '批量修改', status: 'primary' },
+        ],
+        events: {
+          click(slotParams, { name }) {
+            if (name === 'add') {
+              // 新增
+              editEquirement();
+            } else if (name === 'import') {
+              // 批量修改
+              importOrganization();
+            }
+          },
+        },
+      },
+    },
+    {
+      field: 'institutionId',
+      title: '机构名称',
+      span: 6,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => insLoading.value),
+          options: computed(() => insArr.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+      },
+    },
+    {
+      field: 'updateBy',
+      title: '修改人',
+      span: 6,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'updateTime',
+      title: '修改时间',
+      span: 10,
+      slots: {
+        default: 'createTimes',
+      },
+    },
+  ],
+});
+const insLoading = ref(false);
+const insArr = ref<any[]>([]);
+async function getInstitution(orgId: string | number) {
+  insLoading.value = true;
+  const res = await branchMethod(1, 0, Number(orgId));
+  console.log(res, '获取机构');
+  if (res && res.length > 0) {
+    insArr.value = res;
+  }
+  insLoading.value = false;
+}
+const searchFormEmits: VxeFormListeners<EquirementQuery> = {
+  // 查询设备登记
+  submit({ data }) {
+    model.value = {
+      ...data,
+      updateTimeStart: updateTimeStart.value ? dayjs(updateTimeStart.value).format('YYYY-MM-DD HH:mm:ss') : '',
+      updateTimeEnd: updateTimeEnd.value ? dayjs(updateTimeEnd.value).format('YYYY-MM-DD HH:mm:ss') : '',
+    } as any;
+  },
+  // 重置
+  reset({ data }) {
+    model.value = { ...data } as any;
+    updateTimeStart.value = '';
+    updateTimeEnd.value = '';
+  },
+};
+// 设备列表
+const gridRef = ref<VxeGridInstance<EquirementModel>>();
+const gridOptions = reactive<VxeGridProps<EquirementModel>>({
+  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: 'checkbox', width: 100, fixed: 'left', title: '', align: 'center' },
+    { field: 'deviceType', title: '设备名称' },
+    { field: 'orgName', title: '组织名称' },
+    { field: 'institutionName', title: '机构名称' },
+    { field: 'deviceCode', title: '设备ID' },
+    // {
+    //   title: '流程配置',
+    //   align: 'center',
+    //   children: [
+    //     { field: 'name', title: '建档', align: 'center' },
+    //     { field: 'name', title: '舌面诊', align: 'center' },
+    //     { field: 'name', title: '舌面分析报告', align: 'center' },
+    //     { field: 'name', title: '脉诊', align: 'center' },
+    //     { field: 'name', title: '脉象分析报告', align: 'center' },
+    //     { field: 'name', title: '问诊', align: 'center' },
+    //     { field: 'name', title: '健康分析报告', align: 'center' },
+    //     { field: 'name', title: '调理方案', align: 'center' },
+    //   ],
+    // },
+    { field: 'remark', title: '备注' },
+    { field: 'updateBy', title: '修改人' },
+    { field: 'updateTime', title: '修改时间' },
+    {
+      field: 'action',
+      title: '操作',
+      align: 'center',
+      width: 120,
+      showOverflow: false,
+      cellRender: {
+        name: 'VxeButtonGroup',
+        props: {
+          mode: 'text',
+        },
+        options: [
+          { content: '编辑', status: 'primary', name: 'editEquirement' },
+          { content: '删除', status: 'primary', name: 'deleteEquirement' },
+        ],
+        events: {
+          click({ row, rowIndex }, { name }) {
+            let method;
+            if (name === 'editEquirement') {
+              method = editEquirement;
+            } else if (name === 'deleteEquirement') {
+              method = deleteEquirement;
+            }
+            method?.(row, rowIndex);
+          },
+        },
+      },
+    },
+  ],
+  data: [],
+});
+const gridEvents: VxeGridListeners = {};
+
+// 分页
+const { loading, page, pageSize, total, onSuccess, replace, refresh, remove } = usePagination((page, size) => getDeviceRegisterMethod(page, size, model.value), {
+  initialData: { data: [], total: 0 },
+  initialPage: 1,
+  initialPageSize: 100,
+  watchingStates: [model],
+  immediate: false,
+});
+onSuccess(({ data: { data } }) => {
+  gridRef.value?.loadData(data);
+});
+
+// 获取设备类型
+async function getDeviceType() {
+  deviceTypesLoading.value = true;
+  const res = await getDictionaryMethod('fdhb_device_type');
+  console.log(res, '设备类型');
+  if (res && res.length > 0) {
+    deviceTypes.value = res.map((item: any) => ({
+      id: item.value,
+      name: item.label,
+    }));
+  }
+  deviceTypesLoading.value = false;
+}
+
+onMounted(() => {
+  getDeviceType();
+  model.value = toRaw(searchFormProps.data);
+});
+
+// 删除设备
+function deleteEquirement(model: EquirementModel, index: number) {
+  const { deviceCode } = model;
+  VxeUI.modal.confirm({
+    title: `删除设备`,
+    content: `确认要删除 ${deviceCode} 设备吗?`,
+    showClose: false,
+    onConfirm() {
+      deleteDeviceRegisterMethod(model).then(() => {
+        notification.success({
+          message: `删除设备: ${deviceCode}`,
+          description: '操作成功',
+        });
+        refresh(page.value);
+      });
+    },
+  });
+}
+
+function editEquirement(model?: EquirementModel, index?: number) {
+  VxeUI.modal.open({
+    title: model?.id ? `修改设备` : `新增设备`,
+    height: 550,
+    width: 900,
+    escClosable: true,
+    destroyOnClose: true,
+    id: `equirement-modal`,
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(EditEquirement, <any>{
+          data: model,
+          onSubmit(data: EquirementModel) {
+            refresh(page.value);
+            VxeUI.modal.close(`equirement-modal`);
+          },
+        });
+      },
+    },
+  });
+}
+
+// 批量修改
+function importOrganization() {
+  const selectedRows = gridRef.value?.getCheckboxRecords() || [];
+  if (selectedRows.length === 0) {
+    notification.error({ message: '请选择要批量修改的设备' });
+    return;
+  }
+  VxeUI.modal.open({
+    title: '批量修改',
+    width: 700,
+    height: 700,
+    escClosable: true,
+    destroyOnClose: true,
+    id: 'import-organization',
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(EditOrganization, {
+          data: selectedRows,
+          onSubmit(org: any) {
+            // console.log(org, '传过来的参数');
+            // 1. 替换选中设备的组织信息
+            // const updatedDevices = selectedRows.map((row: any) => ({
+            //   ...row,
+            //   institutionId: org.institutionId,
+            //   orgId: org.orgId,
+            //   // institutionName: org.institutionName,
+            // }));
+            // 2. 调用批量更新接口(假设接口为 updateDeviceRegisterOrganizationMethod)
+            // 你需要根据接口实际参数调整
+            // console.log(selectedRows, 'updatedDevices');
+            const deviceIds = selectedRows.map((item: any) => item.id);
+            console.log(deviceIds, 'deviceIds');
+            updateDeviceRegisterOrganizationMethod({ deviceIds, orgId: org.orgId, institutionId: org.institutionId })
+              .then(() => {
+                notification.success({ message: '批量修改成功' });
+                refresh(page.value);
+                VxeUI.modal.close('import-organization');
+              })
+              .catch(() => {
+                notification.error({ message: '批量修改失败' });
+              });
+          },
+        });
+      },
+    },
+  });
+}
+</script>
+<template>
+  <div class="page-container flex flex-col">
+    <header class="flex-none mt-4">
+      <vxe-form v-bind="searchFormProps" v-on="searchFormEmits">
+        <template #createTimes>
+          <div class="date-range-container">
+            <a-date-picker v-model:value="updateTimeStart" placeholder="请选择开始时间" style="flex: 1" :show-time="{ format: 'HH:mm' }" />
+            <span class="date-separator">至</span>
+            <a-date-picker v-model:value="updateTimeEnd" placeholder="请选择结束时间" style="flex: 1" :disabledDate="disabledEndDate" :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 #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>

+ 15 - 30
src/request/api/account.api.ts

@@ -53,48 +53,33 @@ export function getMenusMethod(account: AccountModel) {
       if (children.length > 1) {
         menus.push({ key: path, title, label: title, children });
       } else if (children.length === 1) {
-        menus.push(children[0]);
+        if (path === '/') {
+          menus.push(children[0]);
+        } else {
+          menus.push({ key: path, title, label: title, children });
+        }
       } else if (routes.has(path)) {
         menus.push({ key: path, title, label: title });
       }
     }
-    console.log(menus, 'menus');
     return menus;
   };
 
   return request.Get<AccountModel, any[]>(`/system/menu/getRouters`, {
     headers: { Authorization: account.token },
     transform(data) {
-      console.log(data, '获取菜单');
       // data.push({
-      //   path: '/care',
-      //   meta: { title: '中医调养' },
+      //   path: '/equipment',
+      //   meta: { title: '设备管理' },
       //   children: [
-      //     {
-      //       path: 'supplier',
-      //       meta: { title: '项目供应商' },
-      //     },
-
-      //     {
-      //       path: 'serviceItems',
-      //       meta: { title: '服务项目维护' },
-      //     },
-      //     {
-      //       path: 'systemService',
-      //       meta: { title: '系统服务包' },
-      //     },
-      //     {
-      //       path: 'institutionService',
-      //       meta: { title: '机构服务包' },
-      //     },
-      //     {
-      //       path: 'issueService',
-      //       meta: { title: '开立调养方案' },
-      //     },
-      //     {
-      //       path: 'conditioningRecord',
-      //       meta: { title: '调养记录' },
-      //     }
+      //       {
+      //         path: 'registe',
+      //         meta: { title: '设备登记' },
+      //       },
+            // {
+            //   path: 'configured',
+            //   meta: { title: '调理方案配置' },
+            // },
       //   ],
       // });
       // console.log(data, 'push之后的data', transformMenus(data));

+ 51 - 23
src/request/api/care.api.ts

@@ -1,5 +1,5 @@
 import type { List } from '@/model';
-import type { SupplierQuery, SupplierModel, SystemItemModel, SystemIteQuery, SystemCwModel ,OpenConditioningSchemeModel} from '@/model/care.model';
+import type { SupplierQuery, SupplierModel, SystemItemModel, SystemIteQuery, SystemCwModel, OpenConditioningSchemeModel, ConditioningSchemeModel } from '@/model/care.model';
 import request from '@/request/alova';
 
 // 供应商搜索列表
@@ -35,22 +35,26 @@ export function pageSystemCpMethod(page: number, size: number, query?: SystemIte
 }
 // 获取所有的系统项目
 export function getAllSystemCpMethod() {
-  return request.Post('/fdhb-pc/conditioningManage/program/getAllSystemCp', {}, {
-    name: 'get-all-system-cp',
-    cacheFor: null,
-  });
+  return request.Post(
+    '/fdhb-pc/conditioningManage/program/getAllSystemCp',
+    {},
+    {
+      name: 'get-all-system-cp',
+      cacheFor: null,
+    }
+  );
 }
 // 新增和编辑系统项目和新增编辑项目列表。  项目列表就是机构项目
 export function systemCpEditMethod(data: Partial<SystemItemModel>) {
   // console.log(data, '新增和编辑项目1111111111');
-  if(data.addType === 'system'){
+  if (data.addType === 'system') {
     return data?.id
       ? request.Post(`/fdhb-pc/conditioningManage/program/updateSystemCp`, { ...data, id: data.id }, { name: 'edit-system-cp' })
-    : request.Post(`/fdhb-pc/conditioningManage/program/addSystemCp`, { ...data }, { name: 'edit-system-cp' });
-  }else if(data.addType === 'itemsList'){
+      : request.Post(`/fdhb-pc/conditioningManage/program/addSystemCp`, { ...data }, { name: 'edit-system-cp' });
+  } else if (data.addType === 'itemsList') {
     return data?.id
       ? request.Post(`/fdhb-pc/conditioningManage/program/updateInstitutionCp`, { ...data, id: data.id }, { name: 'edit-system-cp' })
-    : request.Post(`/fdhb-pc/conditioningManage/program/addInstitutionCp`, { ...data }, { name: 'edit-system-cp' });
+      : request.Post(`/fdhb-pc/conditioningManage/program/addInstitutionCp`, { ...data }, { name: 'edit-system-cp' });
   }
 }
 
@@ -105,8 +109,7 @@ export function pageConfirmedCpMethod(page: number, size: number, query?: System
 }
 // 获取服务包内容里的列表
 export function getCpContentListMethod() {
-  return request.Post(`/fdhb-pc/conditioningManage/program/getAllInstitutionCp`, {
-  });
+  return request.Post(`/fdhb-pc/conditioningManage/program/getAllInstitutionCp`, {});
 }
 // 已确认项目列表删除
 export function deleteConfirmedCpMethod(data: Partial<SystemItemModel>) {
@@ -281,7 +284,6 @@ export function deleteOrgCwMethod(data: Partial<SystemCwModel>) {
   });
 }
 
-
 // 为患者开具调理方案
 export function addConditioningSchemeMethod(data: Partial<OpenConditioningSchemeModel>) {
   return request.Post(`/fdhb-pc/patientCrManage/addPcr`, { ...data }, { name: 'add-conditioning-record' });
@@ -289,13 +291,13 @@ export function addConditioningSchemeMethod(data: Partial<OpenConditioningScheme
 
 // 根据调理包id获取调理包详情
 export function getConditioningRecordDetailMethod(data: Partial<SystemCwModel>) {
-  if(data.types === 'institution' || data.types === 'system'){
+  if (data.types === 'institution' || data.types === 'system') {
     // 机构调理包详情
     return request.Post(`/fdhb-pc/conditioningManage/wrap/getCwDetailById/${data.id}`, {
       name: 'get-conditioning-record-detail',
       cacheFor: null,
     });
-  }else if(data.types === 'record'){
+  } else if (data.types === 'record') {
     return request.Post(`/fdhb-pc/patientCrManage/getPcrDetailById/${data.id}`, {
       name: 'get-conditioning-record-detail',
       cacheFor: null,
@@ -311,7 +313,7 @@ export function getCopyCwMethod(data: Partial<SystemCwModel>) {
 }
 
 // 获取患者列表
-export function getPatientListMethod( query?: Record<string, any>) {
+export function getPatientListMethod(query?: Record<string, any>) {
   return request.Post<List<SystemCwModel>>('/fdhb-pc/patientCrManage/todayPcrs', query ?? {}, {
     hitSource: /plan$/, // 匹配失效源
   });
@@ -360,19 +362,19 @@ export function getProvinceMethod() {
   });
 }
 // 获取市
-export function getCityMethod(name: string,provincecode: string) {
+export function getCityMethod(name: string, provincecode: string) {
   return request.Get('/fdhb-pc/region/regionCascadeCity', {
     name: 'get-city',
     cacheFor: null,
-    params: { name,provincecode },
+    params: { name, provincecode },
   });
 }
 // 获取区
-export function getAreaMethod(name: string,citycode: string) {
+export function getAreaMethod(name: string, citycode: string) {
   return request.Get('/fdhb-pc/region/regionCascadeArea', {
     name: 'get-area',
     cacheFor: null,
-    params: { name,citycode },
+    params: { name, citycode },
   });
 }
 // 根据调理记录ID获取调理过程
@@ -380,12 +382,38 @@ export function getConditioningProcessMethod(id: number) {
   return request.Post(`/fdhb-pc/patientCrManage/getPcrProcessById/${id}`, {
     name: 'get-conditioning-process',
     cacheFor: null,
-  }); 
+  });
 }
 // 获取可用的服务包
-export function getAvailableCwMethod(patientId: string,patientConditioningRecordId:string) {
-  return request.Post('/fdhb-pc/patientCrManage/getPatUsableCw', { patientId,patientConditioningRecordId }, {
-    name: 'get-available-cw',
+export function getAvailableCwMethod(patientId: string, patientConditioningRecordId: string) {
+  return request.Post(
+    '/fdhb-pc/patientCrManage/getPatUsableCw',
+    { patientId, patientConditioningRecordId },
+    {
+      name: 'get-available-cw',
+      cacheFor: null,
+    }
+  );
+}
+
+// 调理方案配置分页列表
+export function getConditioningSchemeMethod(page: number, size: number, query?: Record<string, any>) {
+  return request.Post<List<ConditioningSchemeModel>>('/fdhb-pc/conditioningManage/config/pageCpConfig', query ?? {}, {
+    hitSource: /plan$/, // 匹配失效源
+    params: { pageNum: page, pageSize: size },
+  });
+}
+// 更新调理方案配置
+export function updateConditioningSchemeMethod(orgId: string | number, data: any) {
+  return request.Post(`/fdhb-pc/conditioningManage/config/updateCpConfig/${orgId}`, data, {
+    name: 'update-conditioning-scheme',
+    cacheFor: null,
+  });
+}
+// 根据调理方案id获取调理方案配置详情
+export function getConditioningDeviceDetailMethod(data: Partial<ConditioningSchemeModel>) {
+  return request.Post(`/fdhb-pc/conditioningManage/config/getCpConfigDetailById/${data.id || 0}/${data.orgId}`, {
+    name: 'get-conditioning-scheme-detail',
     cacheFor: null,
   });
 }

+ 48 - 0
src/request/api/device.api.ts

@@ -0,0 +1,48 @@
+import type { List } from '@/model';
+import type { EquirementModel } from '@/model/device.model';
+import request from '@/request/alova';
+// 设备登记分页列表
+export function getDeviceRegisterMethod(page: number, size: number, query?: Record<string, any>) {
+  return request.Post<List<EquirementModel>>('/fdhb-pc/deviceManage/device/register/page', query ?? {}, {
+    hitSource: /plan$/, // 匹配失效源
+    params: { pageNum: page, pageSize: size },
+  });
+}
+
+// 新增和编辑设备登记
+export function addDeviceRegisterMethod(data: Partial<EquirementModel>) {
+  return data?.id
+    ? request.Post(`/fdhb-pc/deviceManage/device/register/update`, { ...data, id: data.id }, { name: 'edit-device-register' })
+    : request.Post(`/fdhb-pc/deviceManage/device/register/add`, data.deviceIds, { name: 'add-device-register', params: data });
+}
+// 删除设备登记
+export function deleteDeviceRegisterMethod(data: Partial<EquirementModel>) {
+  return request.Post(`/fdhb-pc/deviceManage/device/register/delete/${data.id}`, {
+    name: 'delete-device-register',
+    cacheFor: null,
+  });
+}
+
+// 根据设备登记id获取设备登记详情
+export function getDeviceRegisterDetailMethod(data: Partial<EquirementModel>) {
+  return request.Post(`/fdhb-pc/deviceManage/device/register/detail/${data.id}`, {
+    name: 'get-device-register-detail',
+    cacheFor: null,
+  });
+}
+
+// 批量修改设备登记组织
+export function updateDeviceRegisterOrganizationMethod(data: any) {
+  return request.Post(
+    `/fdhb-pc/deviceManage/device/register/batchUpdateDept`,
+    data.deviceIds,
+    {
+      name: 'update-device-register-organization',
+      cacheFor: null,
+      params: { orgId: data.orgId, institutionId: data.institutionId },
+    }
+  );
+}
+
+
+

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

@@ -189,6 +189,7 @@ export function medicinesMethod(page: number, size: number, query?: Record<strin
 }
 
 export function cpMedicinesMethod(page: number, size: number, query?: Record<string, any>) {
+  console.log(page, size, query, 'query==>');
   return request.Post(
     `/fdhb-pc/common/pageMedicine`,
     { page, limit: size, keyWord: query?.keyword, ...query },

+ 12 - 2
src/request/api/system.api.ts

@@ -5,8 +5,10 @@ import { fromTag } from '@/model/system.model';
 import request from '@/request/alova';
 
 
-export function branchMethod() {
-  return request.Get<Tree<{ id: string, label: string }>>(`/system/user/deptTree`);
+export function branchMethod(one?: number, two?: number, three?: number) {
+  return request.Get<Tree<{ id: string, label: string }>>(
+    `/system/user/deptTree${one !== undefined && one !== null ? '/' + one : ''}${two !== undefined && two !== null ? '/' + two : ''}${three !== undefined && three !== null ? '/' + three : ''}`
+  );
 }
 
 export function usersMethod(page: number, size: number, query?: UserQuery) {
@@ -163,3 +165,11 @@ export function tagUpdateStatusMethod(data: Partial<TagModel>) {
     cacheFor: null,
   });
 }
+
+// 添加机构
+export function addInstitutionMethod(data: { name: string; parentId: string; description?: string }) {
+  return request.Post<{ id: string; name: string }>(`/system/dept/add`, data, { 
+    name: 'add-institution',
+    cacheFor: null,
+  });
+}

+ 6 - 0
src/router/index.ts

@@ -41,8 +41,14 @@ const router = createRouter({
             { path: 'institutionService', component: () => import(`@/pages/index/care/institutionService.vue`), },
             { path: 'issueService', component: () => import(`@/pages/index/care/issueService.vue`), },
             { path: 'conditioningRecord', component: () => import(`@/pages/index/care/conditioningRecord.vue`), },
+            { path: 'configured', component: () => import(`@/pages/index/care/configured.vue`), },
           ],
         },
+        {
+          path: 'equipment', children: [
+            { path: 'registe', component: () => import(`@/pages/index/equipment/registe.vue`) },
+          ],
+        },   
         {
           path: 'tcmRecuperation', children: [
             { path: 'preserve', component: () => import(`@/pages/index/tcmRecuperation/preserve.vue`) },

+ 841 - 34
src/service/AddItems.vue

@@ -1,6 +1,6 @@
 <script lang="ts" setup>
-import { ref, watch } from 'vue';
-import {  message } from 'ant-design-vue';
+import { ref, watch, reactive, onMounted, h, computed } from 'vue';
+import { message } from 'ant-design-vue';
 import { PlusOutlined } from '@ant-design/icons-vue'; // 确保导入
 import VxeUI from 'vxe-table';
 import { useRequest } from 'alova/client';
@@ -10,8 +10,10 @@ import { systemCpEditMethod, getAllSupplierMethod, getConditioningSchemeDetailMe
 import { UploadIFile } from '@/request/api/follow.api';
 import type { SystemItemModel } from '@/model/care.model';
 import RemoteSelect from '@/libs/v-select-page/RemoteSelect.vue';
+import SearchableSelect from '@/components/SearchableSelect.vue';
 import type { UploadFile } from 'ant-design-vue/es/upload/interface';
 import type { FormInstance } from 'ant-design-vue';
+import Derivation from '@/service/Derivation.vue';
 type SystemModel = Partial<SystemItemModel>;
 const props = defineProps<{ data: SystemModel }>();
 const formRef = ref<FormInstance>();
@@ -28,6 +30,14 @@ const unitOptions = [
   { label: '贴', value: '贴' },
   { label: '次', value: '次' },
 ];
+const herbList = ref<any[]>([]);
+// function getHerb(value: string) {
+//   console.log(value, 'value==>');
+//   cpMedicinesMethod(1, 10, { keyword: value }).then((res) => {
+//     herbList.value = res.data;
+//     console.log(herbList.value, 'herbList==>');
+//   });
+// }
 
 // 获取所有的机构
 const branch = ref<any[]>([]);
@@ -47,7 +57,7 @@ const { loading: branchLoading } = useRequest(branchMethod).onSuccess(({ data })
   branch.value = to(data);
   console.log(branch.value, '获取所有的机构');
 });
-const form = reactive<SystemModel>({
+const form = reactive<SystemItemModel>({
   institutionName: '',
   conditioningProgramType: '',
   conditioningProgramSupplierId: '',
@@ -58,24 +68,51 @@ const form = reactive<SystemModel>({
   ],
   pricingType: '0',
   cpFixedPricingRule: {
-    unitPrice: 0,
+    unitPrice: '',
     pricingUnit: '',
-    convertDose: 0,
+    convertDose: '',
     convertUnit: '',
   },
+  cpPatientMatchRule: {
+    sex: '', // 性别
+    age: '', // 年龄
+    diagnoseDiseaseNames: [], // 疾病
+    diagnoseSyndromeNames: [], // 证型
+    constitutionGroupNames: [], // 体质
+    willillStateNames: [], // 欲病状态
+    tabooCrowds: [], // 禁忌
+  },
   cpMedicines: [{ name: '', dosage: '', id: '' }],
   isOffline: null,
   isDelivery: null,
+  effect: '', // 功效
+  photo: '', // 商品图片
+  itemImgFirst: '', // 操作图片
+  itemVideoFirst: '', // 操作视频
+  attrFirst: '', // 特色
+  attrSeventh: '', //使用注意
+  attrSixth: '', // 疗程说明
+  attrFifth: '', // 操作方法
+  attrFourth: '', // 用法
+  attrThird: '', // 制法
+  status: '0', // 状态
+  isForWrap: null, // 是否服务包项目
+  isForInfer: null, // 是否调理方案项目
 });
+
+const checkedList = ref<string[]>(['1']); // 默认选中第一个
+
 const onlineArr = ref<string[]>([]);
 const deliverArr = ref<string[]>([]);
+
 const rules = {
   name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
   conditioningProgramType: [{ required: true, message: '请选择方案类型', trigger: 'change' }],
-  pricingType: [{ required: true, message: '请选择计价规则', trigger: 'change' }],
-  conditioningProgramSupplierId: [{ required: true, message: '请选择供应商', trigger: 'change' }],
+  // 移除 pricingType 验证,使用动态验证
+  // 移除 conditioningProgramSupplierId 验证,使用动态验证
   institutionId: [{ required: true, message: '请选择机构名称', trigger: 'change' }],
-  isOffline: [{ required: true, message: '请选择线下项目', trigger: 'change' }],
+  // 移除 isOffline 验证,使用动态验证
+  // 移除 projectType 验证,使用自定义验证
 };
 const isShowOnline = ref<boolean>(false);
 const isShowDelivery = ref<boolean>(false);
@@ -177,20 +214,112 @@ function removeHerb(idx: number) {
     form.cpMedicines.splice(idx, 1);
   }
 }
+
+// 新增:处理自定义药材输入
+function handleCustomHerb(value: string, idx: number) {
+  if (form.cpMedicines && form.cpMedicines[idx]) {
+    form.cpMedicines[idx].name = value;
+    form.cpMedicines[idx].id = value; // 自定义选项使用输入值作为ID
+  }
+}
+
 function cancel() {
   VxeUI.modal.close(`add-items-modal`);
 }
 function doSubmit() {
-  console.log(formRef.value, 'formRef==>');
-  console.log(form, 'Form Data Before Submit');
+  // 自定义验证:检查项目应用是否已选择
+  if (!checkedList.value || checkedList.value.length === 0) {
+    message.error('请选择项目应用');
+    return;
+  }
+  form.isForWrap = checkedList.value.includes('1') ? 'Y' : null;
+  form.isForInfer = checkedList.value.includes('2') ? 'Y' : null;
+  // 服务包项目相关必填
+  if (form.addType === 'itemsList' && checkedList.value.includes('1') || form.addType === 'system') {
+    // 计价规则
+    if (!form.pricingType) {
+      message.error('请选择计价规则');
+      return;
+    }
+    // 计价规则相关字段校验
+    if (form.pricingType === '0') {
+      const { unitPrice, pricingUnit, convertDose, convertUnit } = form.cpFixedPricingRule || {};
+      const allFilled = unitPrice && pricingUnit && convertDose && convertUnit;
+      if (!allFilled) {
+        message.error('请将单价、计价单位、相当于、使用单位全部填写');
+        return;
+      }
+    } else if (form.pricingType === '1') {
+      const rule1 = form.cpDynamicPricingRule[0] || {};
+      const rule2 = form.cpDynamicPricingRule[1] || {};
+      const allFilled1 = rule1.min && typeof rule1.priceType !== 'undefined' && rule1.price;
+      const allFilled2 = rule2.max && typeof rule2.priceType !== 'undefined' && rule2.price;
+      if (!(allFilled1 && allFilled2)) {
+        message.error('请将按穴位/经络/部位的所有计价字段全部填写');
+        return;
+      }
+    }
+    // 供应商
+    if (!form.conditioningProgramSupplierId) {
+      message.error('请选择供应商');
+      return;
+    }
+    // 线下项目
+    if (isShowOnline.value && !form.isOffline) {
+      message.error('请选择是否为线下项目');
+      return;
+    }
+    // 配送
+    if (isShowDelivery.value && (!deliverArr.value || deliverArr.value.length === 0)) {
+      message.error('请选择配送方式');
+      return;
+    }
+  } else if (form.addType === 'itemsList') {
+    // 计价规则非必填时,做全填/全空校验
+    if (form.pricingType === '0') {
+      const { unitPrice, pricingUnit, convertDose, convertUnit } = form.cpFixedPricingRule || {};
+      const allFilled = unitPrice && pricingUnit && convertDose && convertUnit;
+      const allEmpty = !unitPrice && !pricingUnit && !convertDose && !convertUnit;
+      if (!(allFilled || allEmpty)) {
+        message.error('单价、计价单位、相当于、使用单位要么全部填写,要么全部为空');
+        return;
+      }
+    } else if (form.pricingType === '1') {
+      const rule1 = form.cpDynamicPricingRule[0] || {};
+      const rule2 = form.cpDynamicPricingRule[1] || {};
+      const allFilled1 = rule1.min && typeof rule1.priceType !== 'undefined' && rule1.price;
+      const allEmpty1 = !rule1.min && (typeof rule1.priceType === 'undefined' || rule1.priceType === '' || rule1.priceType === null) && !rule1.price;
+      const allFilled2 = rule2.max && typeof rule2.priceType !== 'undefined' && rule2.price;
+      const allEmpty2 = !rule2.max && (typeof rule2.priceType === 'undefined' || rule2.priceType === '' || rule2.priceType === null) && !rule2.price;
+      if (!((allFilled1 && allFilled2) || (allEmpty1 && allEmpty2))) {
+        message.error('按穴位/经络/部位的所有计价字段要么全部填写,要么全部为空');
+        return;
+      }
+    }
+  }
+  // 调理方案项目相关必填
+  if (form.addType === 'itemsList' && checkedList.value.includes('2')) {
+    if (!hasDerivationLogic.value) {
+      message.error('请设置推导逻辑');
+      return;
+    }
+  }
+
   formRef.value
     ?.validate()
     .then(() => {
       form.photo = fileList.value[0]?.response?.url || fileList.value[0]?.url || '';
+      form.itemImgFirst = optionsList.value[0]?.response?.url || optionsList.value[0]?.url || '';
+      // form.itemVideoFirst = fileList.value[0]?.response?.url || fileList.value[0]?.url || '';
+      // 合并推导逻辑数据到表单数据中
+      if (hasDerivationLogic.value && derivationData.value.cpPatientMatchRule) {
+        form.cpPatientMatchRule = { ...derivationData.value.cpPatientMatchRule };
+      }
+      // console.log('提交的最后数据', form);
       submit(form);
     })
     .catch((error) => {
-      console.error('Validation Error:', error);
+      // console.error('Validation Error:', error);
       message.error('请完善必填项');
     });
 }
@@ -203,14 +332,15 @@ async function getConditioningProgramType() {
       typeOptions.value = res; // 直接使用返回的数据
     }
   } catch (error) {
-    console.error('获取方案类型列表失败:', error);
+    // console.error('获取方案类型列表失败:', error);
   } finally {
     typeOptionsLoading.value = false;
   }
 }
 
 onMounted(async () => {
-  console.log(props.data, '获取传来的数据');
+  // console.log(props.data, '获取传来的数据');
+  // getHerb('');
   const deptId = localStorage.getItem('deptId');
   if (props.data.addType === 'system' && deptId) {
     form.institutionId = deptId;
@@ -221,6 +351,11 @@ onMounted(async () => {
   form.addType = props.data.addType;
   if (props.data.id || props.data.sourceId) {
     const res: any = await getConditioningSchemeDetailMethod(props.data);
+    // console.log(res, 'res==>');
+    const checked: string[] = [];
+    if (res.isForWrap === 'Y') checked.push('1');
+    if (res.isForInfer === 'Y') checked.push('2');
+    checkedList.value = checked.length > 0 ? checked : ['1', '2'];
     Object.assign(form, res);
     form.cpMedicines = (res.cpMedicines ?? []).map((item: any) => ({
       name: item.name || item.herbName || item.medicineName || '',
@@ -228,6 +363,12 @@ onMounted(async () => {
       id: item.id,
     }));
 
+    // 处理推导逻辑数据回填
+    if (res.cpPatientMatchRule) {
+      derivationData.value = { cpPatientMatchRule: res.cpPatientMatchRule };
+      hasDerivationLogic.value = true;
+    }
+
     fileList.value = res.photo
       ? [
           {
@@ -239,10 +380,35 @@ onMounted(async () => {
           },
         ]
       : [];
-    console.log(props.data, 'form==>');
+    optionsList.value = res.itemImgFirst
+      ? [
+          {
+            uid: '-1',
+            name: 'image.png',
+            status: 'done',
+            url: res.itemImgFirst,
+            thumbUrl: res.itemImgFirst,
+          },
+        ]
+      : [];
+
+    // 处理视频数据
+    if (res.itemVideoFirst) {
+      videoFileList.value = [
+        {
+          uid: '-1',
+          name: '操作视频',
+          status: 'done',
+          url: res.itemVideoFirst,
+          response: { url: res.itemVideoFirst },
+        },
+      ];
+    }
+
+    // console.log(props.data, 'form==>');
     if (props.data.addType === 'itemsList' && props.data.sourceId) {
       form.sourceId = form.id;
-      form.institutionId='';
+      form.institutionId = '';
       delete form.id;
     }
   }
@@ -250,7 +416,7 @@ onMounted(async () => {
   // getSupplier({});
   // 获取方案类型
   getConditioningProgramType();
-  console.log(form, 'form==>');
+  // console.log(form, 'form==>');
 });
 const emits = defineEmits<{
   submit: [data?: SystemItemModel];
@@ -263,12 +429,15 @@ const { loading: submitting, send: submit } = useRequest(systemCpEditMethod, {
 });
 
 const visible = ref<boolean>(false);
-const setVisible = (value): void => {
+const setVisible = (value: boolean): void => {
   visible.value = value;
 };
 const previewImg = ref<string>('');
 const uploadProps = reactive({ showRemoveIcon: true });
+// 商品图片
 const fileList = ref<UploadFile[]>([]);
+// 操作图片
+const optionsList = ref<UploadFile[]>([]);
 // 预览图片
 const handlePreview = async (file: UploadFile) => {
   previewImg.value = file.response?.url ?? file.thumbUrl;
@@ -287,6 +456,152 @@ function customUpload(e: any) {
       e.onError(err);
     });
 }
+// 视频上传相关
+const accept = 'video/mp4,video/avi,video/mov,video/webm';
+const maxSize = 100 * 1024 * 1024; // 100MB
+
+const videoUrl = ref('');
+const uploading = ref(false);
+const progress = ref(0);
+const videoFileList = ref<UploadFile[]>([]);
+
+// 上传前校验
+function beforeVideoUpload(file: File) {
+  const isValidType = accept.split(',').includes(file.type);
+  if (!isValidType) {
+    message.error('仅支持mp4、avi、mov、webm格式视频');
+    return false;
+  }
+
+  const isValidSize = file.size <= maxSize;
+  if (!isValidSize) {
+    message.error('视频大小不能超过100MB');
+    return false;
+  }
+
+  return true;
+}
+
+// 视频预览
+function handleVideoPreview(file: UploadFile) {
+  const videoUrl = file.response?.url || file.url || form.itemVideoFirst;
+  if (videoUrl) {
+    // 创建一个模态框来预览视频
+    VxeUI.modal.open({
+      title: '视频预览',
+      width: 800,
+      height: 600,
+      escClosable: true,
+      destroyOnClose: true,
+      slots: {
+        default() {
+          return h(
+            'div',
+            {
+              style: {
+                display: 'flex',
+                justifyContent: 'center',
+                alignItems: 'center',
+                height: '100%',
+              },
+            },
+            [
+              h('video', {
+                src: videoUrl,
+                controls: true,
+                style: {
+                  maxWidth: '100%',
+                  maxHeight: '100%',
+                  borderRadius: '8px',
+                },
+              }),
+            ]
+          );
+        },
+      },
+    });
+  }
+}
+
+// 删除视频
+function removeVideo() {
+  form.itemVideoFirst = '';
+  videoFileList.value = [];
+  message.success('视频已删除');
+}
+
+// 自定义上传
+function customVideoRequest(e: any) {
+  const { file, onSuccess, onError, onProgress } = e;
+
+  // 验证文件
+  if (!beforeVideoUpload(file)) {
+    onError(new Error('文件验证失败'));
+    return;
+  }
+
+  uploading.value = true;
+  progress.value = 0;
+
+  // 模拟上传进度
+  const progressInterval = setInterval(() => {
+    if (progress.value < 90) {
+      progress.value += Math.random() * 10;
+      onProgress({ percent: progress.value });
+    }
+  }, 200);
+
+  // 上传文件
+  UploadIFile(file)
+    .then((res: any) => {
+      clearInterval(progressInterval);
+      progress.value = 100;
+      onProgress({ percent: 100 });
+
+      // 设置视频URL
+      form.itemVideoFirst = res?.url || res?.data?.url;
+
+      // 更新文件列表
+      videoFileList.value = [
+        {
+          uid: file.uid,
+          name: file.name,
+          status: 'done',
+          url: form.itemVideoFirst,
+          response: res,
+        },
+      ];
+
+      onSuccess(res, e);
+      uploading.value = false;
+      message.success('视频上传成功');
+
+      console.log('上传视频成功', form.itemVideoFirst);
+    })
+    .catch((err) => {
+      clearInterval(progressInterval);
+      uploading.value = false;
+      progress.value = 0;
+      onError(err);
+      message.error('视频上传失败,请重试');
+      console.error('视频上传失败:', err);
+    });
+}
+
+// 视频上传状态变化
+function handleVideoChange(info: any) {
+  const { file, fileList } = info;
+
+  if (file.status === 'uploading') {
+    uploading.value = true;
+  } else if (file.status === 'done') {
+    uploading.value = false;
+    videoFileList.value = fileList;
+  } else if (file.status === 'error') {
+    uploading.value = false;
+    message.error('视频上传失败');
+  }
+}
 
 watch(
   () => form.pricingType,
@@ -322,9 +637,65 @@ function getConditioningProgramSupplier(value: any) {
   form.isDelivery = null;
   isShowDelivery.value = false;
 }
-function handleSelect(value: string, node: any, extra: any) {
+// 修正参数以适配 a-tree-select 的 @select 事件
+function handleSelect(value: any, node: any) {
   form.institutionId = value;
-  form.institutionName = node.label;
+  if (node && node.label) {
+    form.institutionName = node.label;
+  } else if (node && node.title) {
+    form.institutionName = node.title;
+  }
+}
+
+// 项目应用数据
+const plainOptions = [
+  { id: '1', name: '服务包项目' },
+  { id: '2', name: '调理方案项目' },
+];
+
+// 推导逻辑
+const hasDerivationLogic = ref(false); // 添加推导逻辑状态跟踪
+const derivationData = ref<any>({});
+// 新增:判断推导逻辑内容是否为空
+const isDerivationEmpty = computed(() => {
+  const rule = derivationData.value.cpPatientMatchRule;
+  if (!rule) return true;
+  // 检查所有字段都为空或未填写
+  const fields = ['sex', 'age', 'diagnoseDiseaseNames', 'diagnoseSyndromeNames', 'constitutionGroupNames', 'willillStateNames', 'tabooCrowds'];
+  return fields.every((key) => {
+    const val = rule[key];
+    if (Array.isArray(val)) return val.length === 0;
+    return !val;
+  });
+});
+function handleDerivation() {
+  VxeUI.modal.open({
+    title: `推导逻辑`,
+    height: 750,
+    width: 750,
+    escClosable: true,
+    destroyOnClose: true,
+    id: `derivation-modal`,
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(Derivation, {
+          data: {
+            ...props.data,
+            // 传递当前的推导逻辑数据
+            cpPatientMatchRule: derivationData.value.cpPatientMatchRule || form.cpPatientMatchRule,
+            checkedList: checkedList.value,
+          },
+          onSubmit: (data: any) => {
+            console.log('推导逻辑传来的数据', data);
+            derivationData.value = data;
+            hasDerivationLogic.value = true; // 设置推导逻辑已编辑
+          },
+        });
+      },
+    },
+  });
 }
 </script>
 
@@ -345,7 +716,14 @@ function handleSelect(value: string, node: any, extra: any) {
           @change="bindchange"
         />
       </a-form-item>
-      <a-form-item label="计价规则:" name="pricingType" required>
+      <a-form-item label="项目应用:" required v-if="form.addType === 'itemsList'">
+        <a-checkbox-group v-model:value="checkedList">
+          <a-checkbox v-for="option in plainOptions" :key="option.id" :value="option.id">
+            {{ option.name }}
+          </a-checkbox>
+        </a-checkbox-group>
+      </a-form-item>
+      <a-form-item label="计价规则:" name="pricingType" :required="checkedList.includes('1')">
         <a-radio-group v-model:value="form.pricingType">
           <a-radio value="0">一口价</a-radio>
           <a-radio value="1">按穴位/经络/部位</a-radio>
@@ -410,12 +788,23 @@ function handleSelect(value: string, node: any, extra: any) {
         </div>
       </div>
 
-      <a-form-item label="中药组成:">
+      <a-form-item label="组成:">
         <div class="herb-list">
           <template v-for="(herb, idx) in form.cpMedicines" :key="herb.id">
             <div class="herb-item">
               <button class="herb-remove" v-if="form?.cpMedicines?.length > 1" @click="removeHerb(idx)" type="button">×</button>
-              <RemoteSelect :load="cpMedicinesMethod" key-prop="name" v-model:value="herb.name" />
+              <!-- <RemoteSelect :load="cpMedicinesMethod" key-prop="name" v-model:value="herb.name" /> -->
+              <SearchableSelect
+                v-model="herb.name"
+                :options="herbList"
+                placeholder="请选择"
+                value-key="name"
+                label-key="name"
+                key-key="id"
+                :fetch-options="cpMedicinesMethod"
+                style="width: 150px"
+                :allow-custom-input="true"
+              />
               <a-input v-model:value="herb.dosage" class="herb-dosage" placeholder="剂量" />
               <span>g</span>
             </div>
@@ -423,11 +812,160 @@ function handleSelect(value: string, node: any, extra: any) {
           <button style="margin-left: 8px" @click="addHerb" type="button">+</button>
         </div>
       </a-form-item>
+      <a-form-item label="特色:" v-if="form.addType === 'itemsList'">
+        <textarea
+          v-model="form.attrFirst"
+          placeholder="请输入"
+          rows="3"
+          style="width: 100%; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; resize: vertical; font-family: inherit"
+        />
+      </a-form-item>
       <a-form-item label="功效:">
         <a-input v-model:value="form.effect" placeholder="请输入" />
       </a-form-item>
+      <a-form-item label="制法:" v-if="form.addType === 'itemsList'">
+        <a-input v-model:value="form.attrThird" placeholder="请输入" />
+      </a-form-item>
+      <a-form-item label="用法:" v-if="form.addType === 'itemsList'">
+        <a-input v-model:value="form.attrFourth" placeholder="请输入" />
+      </a-form-item>
+      <a-form-item label="操作方法:" v-if="form.addType === 'itemsList'">
+        <textarea
+          v-model="form.attrFifth"
+          placeholder="请输入"
+          rows="3"
+          style="width: 100%; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; resize: vertical; font-family: inherit"
+        />
+      </a-form-item>
+
+      <div class="image-row">
+        <a-form-item label="操作图片:" class="image-form-item" v-if="form.addType === 'itemsList'">
+          <a-upload :showUploadList="uploadProps" v-model:file-list="optionsList" list-type="picture-card" @preview="handlePreview" :maxCount="1" :customRequest="customUpload">
+            <div v-if="optionsList.length < 1">
+              <PlusOutlined />
+              <div style="margin-top: 8px">上传</div>
+            </div>
+          </a-upload>
+        </a-form-item>
+        <a-form-item label="商品图片:" class="image-form-item">
+          <a-upload :showUploadList="uploadProps" v-model:file-list="fileList" list-type="picture-card" @preview="handlePreview" :maxCount="1" :customRequest="customUpload">
+            <div v-if="fileList.length < 1">
+              <PlusOutlined />
+              <div style="margin-top: 8px">上传</div>
+            </div>
+          </a-upload>
+        </a-form-item>
+      </div>
+      <a-form-item label="操作视频:" class="image-form-item" v-if="form.addType === 'itemsList'">
+        <div class="video-upload-wrapper">
+          <div class="video-upload-container">
+            <a-upload
+              :showUploadList="false"
+              :custom-request="customVideoRequest"
+              :max-count="1"
+              :multiple="false"
+              :before-upload="beforeVideoUpload"
+              @change="handleVideoChange"
+              :file-list="videoFileList"
+              accept="video/mp4,video/avi,video/mov,video/webm"
+            >
+              <div v-if="!form.itemVideoFirst && !uploading" class="video-upload-btn">
+                <PlusOutlined />
+                <div style="margin-top: 8px">上传视频</div>
+                <div class="video-upload-tip">mp4/avi/mov/webm<br />最大100MB</div>
+              </div>
+
+              <!-- 上传中状态 -->
+              <div v-else-if="uploading" class="video-uploading">
+                <div class="upload-progress">
+                  <a-progress :percent="progress" :show-info="false" size="small" stroke-color="#1890ff" />
+                  <div class="upload-text">上传中... {{ Math.round(progress) }}%</div>
+                </div>
+              </div>
+
+              <!-- 视频预览 -->
+              <div v-else class="video-preview-container">
+                <div class="video-thumbnail">
+                  <video :src="form.itemVideoFirst" preload="metadata" class="video-preview" />
+                  <div class="video-overlay">
+                    <a-button
+                      type="primary"
+                      size="small"
+                      @click.stop="
+                        handleVideoPreview({
+                          uid: 'video-preview',
+                          name: '操作视频',
+                          url: form.itemVideoFirst,
+                        })
+                      "
+                    >
+                      预览
+                    </a-button>
+                    <a-button danger size="small" @click.stop="removeVideo"> 删除 </a-button>
+                  </div>
+                </div>
+                <!-- <div class="video-info">
+                  <div class="video-name">操作视频</div>
+                  <div class="video-size">已上传</div>
+                </div> -->
+              </div>
+            </a-upload>
+          </div>
+        </div>
+      </a-form-item>
+      <a-form-item label="疗程说明:" v-if="form.addType === 'itemsList'">
+        <textarea
+          v-model="form.attrSixth"
+          placeholder="请输入"
+          rows="3"
+          style="width: 100%; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; resize: vertical; font-family: inherit"
+        />
+      </a-form-item>
+      <a-form-item label="使用注意:" v-if="form.addType === 'itemsList'">
+        <textarea
+          v-model="form.attrSeventh"
+          placeholder="请输入"
+          rows="3"
+          style="width: 100%; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; resize: vertical; font-family: inherit"
+        />
+      </a-form-item>
+      <a-form-item label="推导逻辑:" v-if="form.addType === 'itemsList'" :required="checkedList.includes('2')">
+        <button @click="handleDerivation" v-if="!hasDerivationLogic || isDerivationEmpty">编辑</button>
+        <div v-else @click="handleDerivation" style="cursor: pointer">
+          <div v-if="derivationData.cpPatientMatchRule" class="derivation-container">
+            <div v-if="derivationData.cpPatientMatchRule.sex" class="derivation-item">
+              <span class="derivation-label">性别限制:</span>
+              <span class="derivation-content">{{ derivationData.cpPatientMatchRule.sex }}</span>
+            </div>
+            <div v-if="derivationData.cpPatientMatchRule.age" class="derivation-item">
+              <span class="derivation-label">年龄限制:</span>
+              <span class="derivation-content">{{ derivationData.cpPatientMatchRule.age }}</span>
+            </div>
+            <div v-if="derivationData.cpPatientMatchRule.diagnoseDiseaseNames && derivationData.cpPatientMatchRule.diagnoseDiseaseNames.length > 0" class="derivation-item">
+              <span class="derivation-label">专病:</span>
+              <span class="derivation-content">{{ derivationData.cpPatientMatchRule.diagnoseDiseaseNames.join('、') }}</span>
+            </div>
+            <div v-if="derivationData.cpPatientMatchRule.diagnoseSyndromeNames && derivationData.cpPatientMatchRule.diagnoseSyndromeNames.length > 0" class="derivation-item">
+              <span class="derivation-label">证型:</span>
+              <span class="derivation-content">{{ derivationData.cpPatientMatchRule.diagnoseSyndromeNames.join('、') }}</span>
+            </div>
+            <div v-if="derivationData.cpPatientMatchRule.constitutionGroupNames && derivationData.cpPatientMatchRule.constitutionGroupNames.length > 0" class="derivation-item">
+              <span class="derivation-label">体质:</span>
+              <span class="derivation-content">{{ derivationData.cpPatientMatchRule.constitutionGroupNames.join('、') }}</span>
+            </div>
+            <div v-if="derivationData.cpPatientMatchRule.willillStateNames && derivationData.cpPatientMatchRule.willillStateNames.length > 0" class="derivation-item">
+              <span class="derivation-label">欲病状态:</span>
+              <span class="derivation-content">{{ derivationData.cpPatientMatchRule.willillStateNames.join('、') }}</span>
+            </div>
+            <div v-if="derivationData.cpPatientMatchRule.tabooCrowds && derivationData.cpPatientMatchRule.tabooCrowds.length > 0" class="derivation-item">
+              <span class="derivation-label">禁忌人群:</span>
+              <span class="derivation-content">{{ derivationData.cpPatientMatchRule.tabooCrowds.join('、') }}</span>
+            </div>
+          </div>
+        </div>
+      </a-form-item>
       <!-- 机构名称 -->
-      <a-form-item label="机构名称:" v-if="form?.addType === 'itemsList' && form?.id" required name="institutionId">
+      <a-form-item label="机构名称:" v-if="form?.addType === 'itemsList'" required name="institutionId">
         <a-tree-select
           v-model:value="form.institutionId"
           style="width: 100%"
@@ -439,28 +977,26 @@ function handleSelect(value: string, node: any, extra: any) {
           @select="handleSelect"
         ></a-tree-select>
       </a-form-item>
-      <a-form-item label="供应商:" name="conditioningProgramSupplierId" required>
+      <a-form-item label="供应商:" name="conditioningProgramSupplierId" :required="checkedList.includes('1')">
         <a-select v-model:value="form.conditioningProgramSupplierId" :options="supplierOptions" placeholder="请选择" allowClear @change="getConditioningProgramSupplier" />
       </a-form-item>
-      <a-form-item label="线下项目:" name="isOffline" v-if="isShowOnline" required>
+      <a-form-item label="线下项目:" name="isOffline" v-if="isShowOnline" :required="checkedList.includes('1')">
         <a-checkbox-group v-model:value="onlineArr" @change="onlineChange">
           <a-checkbox value="Y">是</a-checkbox>
           <a-checkbox value="N">否</a-checkbox>
         </a-checkbox-group>
       </a-form-item>
-      <a-form-item label="配送:" name="isDelivery" v-if="isShowDelivery">
+      <a-form-item label="配送:" name="isDelivery" v-if="isShowDelivery" :required="checkedList.includes('1')">
         <a-checkbox-group v-model:value="deliverArr" @change="deliveryChange">
           <a-checkbox value="Y">支持</a-checkbox>
           <a-checkbox value="N">不支持</a-checkbox>
         </a-checkbox-group>
       </a-form-item>
-      <a-form-item label="图片:">
-        <a-upload :showUploadList="uploadProps" v-model:file-list="fileList" list-type="picture-card" @preview="handlePreview" :maxCount="1" :customRequest="customUpload">
-          <div v-if="fileList.length < 1">
-            <PlusOutlined />
-            <div style="margin-top: 8px">上传</div>
-          </div>
-        </a-upload>
+      <a-form-item label="启用状态:" name="status" v-if="form.addType === 'itemsList'" required>
+        <a-radio-group v-model:value="form.status">
+          <a-radio value="0">启用</a-radio>
+          <a-radio value="1">停用</a-radio>
+        </a-radio-group>
       </a-form-item>
       <a-image
         :width="200"
@@ -471,6 +1007,7 @@ function handleSelect(value: string, node: any, extra: any) {
         }"
         :src="previewImg"
       />
+      <video :src="videoUrl" controls v-if="videoUrl" style="width: 100%; max-width: 500px; border-radius: 6px" preload="metadata" />
       <div class="form-actions-center">
         <a-button @click="cancel">取消</a-button>
         <a-button type="primary" style="background: #faad14; border: none" @click="doSubmit">确定</a-button>
@@ -513,7 +1050,7 @@ function handleSelect(value: string, node: any, extra: any) {
 }
 .herb-dosage {
   width: 70px;
-  padding: 2px 6px;
+  /* padding: 2px 6px; */
   font-size: 14px;
   margin-right: 10px;
 }
@@ -569,4 +1106,274 @@ function handleSelect(value: string, node: any, extra: any) {
   color: #faad14;
   font-weight: bold;
 }
+.upload-btn {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-height: 100px;
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  background: #fafafa;
+  cursor: pointer;
+  transition: border-color 0.3s;
+  width: 100px;
+}
+.upload-btn:hover {
+  border-color: #1890ff;
+  background: #f0f7ff;
+}
+.upload-btn .anticon {
+  font-size: 16px;
+  color: #1890ff;
+}
+.upload-btn .upload-text {
+  margin-top: 8px;
+  color: #666;
+  font-size: 15px;
+}
+.video-preview {
+  border-radius: 6px;
+  /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); */
+  background: #000;
+  width: 120px !important;
+  height: 100px !important;
+  /* outline: none; */
+}
+.video-preview video {
+  border-radius: 6px;
+  /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); */
+  background: #000;
+  width: 120px;
+  height: 100px;
+  /* outline: none; */
+}
+.video-preview .ant-btn-link {
+  padding: 0;
+  font-size: 14px;
+  color: #ff4d4f;
+}
+.image-row {
+  display: flex;
+  gap: 32px;
+  align-items: flex-start;
+  margin-bottom: 16px;
+}
+.image-form-item {
+  flex: 1;
+  margin-bottom: 0 !important;
+}
+.image-form-item .ant-upload-picture-card-wrapper {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+.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-label {
+  font-weight: 500;
+  color: #222;
+  margin-right: 8px;
+  white-space: nowrap;
+  font-size: 13px;
+}
+.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-upload-wrapper {
+  display: flex;
+  align-items: flex-start;
+}
+
+.video-upload-container {
+  position: relative;
+  width: 130px;
+  height: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 2px dashed #d9d9d9;
+  border-radius: 8px;
+  background: #fafafa;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  overflow: hidden;
+  margin-bottom: 20px;
+}
+
+.video-upload-container:hover {
+  border-color: #1890ff;
+  background: #f0f7ff;
+  transform: translateY(-1px);
+  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
+}
+
+.video-upload-btn {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 16px 12px;
+  text-align: center;
+}
+
+.video-upload-btn .anticon {
+  font-size: 20px;
+  color: #1890ff;
+  margin-bottom: 6px;
+}
+
+.video-upload-tip {
+  font-size: 11px;
+  color: #999;
+  margin-top: 6px;
+  line-height: 1.3;
+}
+
+.video-uploading {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(255, 255, 255, 0.95);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 10;
+  backdrop-filter: blur(2px);
+}
+
+.upload-progress {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+  width: 90%;
+  max-width: 160px;
+}
+
+.upload-text {
+  font-size: 12px;
+  color: #1890ff;
+  font-weight: 500;
+  text-align: center;
+}
+
+.video-preview-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  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;
+}
 </style>

+ 14 - 25
src/service/CareProgress.vue

@@ -175,6 +175,7 @@ interface Model extends SymptomItemVo {
 }
 // 查看健康评估
 function open(row: Model) {
+  // console.log(row, '查看健康评估');
   const component = defineAsyncComponent(() => import('@/components/ReportPreview.vue'));
   const id = `drawer:report:preview`;
   const onDestroy = () => {
@@ -200,28 +201,23 @@ function open(row: Model) {
     },
   });
 }
+const progressTextMap: Record<string, string> = {
+  '0': '待付款',
+  '1': '已作废',
+  '2': '用户取消',
+  '3': '未开始',
+  '4': '调理中',
+  '5': '已完结'
+};
+
 </script>
 
 <template>
   <div class="care-progress-card">
     <div class="header" v-if="careProcessList">
-      <span class="title">{{
-        careProcessList?.progress === '0'
-          ? '待付款'
-          : careProcessList?.progress === '1'
-            ? '已作废'
-            : careProcessList?.progress === '2'
-              ? '用户取消'
-              : careProcessList?.progress === '3'
-                ? '未开始'
-                : careProcessList?.progress === '4'
-                  ? '调理中'
-                  : careProcessList?.progress === '5'
-                    ? '已完结'
-                    : careProcessList?.progress === '6'
-                      ? '待收货'
-                      : ''
-      }}</span>
+      <span class="title">
+        {{ progressTextMap[careProcessList?.progress] || '' }}
+        </span>
       <span v-if="careProcessList?.patientName"
         >姓名:<b>{{ careProcessList?.patientName }}</b></span
       >
@@ -311,13 +307,6 @@ function open(row: Model) {
             <div>记录</div>
             <component :is="panel.component" :patient="patient" :healthAnalysisReports="item?.healthAnalysisReports" :type="type"></component>
           </a-tab-pane>
-          <!-- <template #renderTabBar>
-            <a-radio-group v-model:value="activePanel">
-              <a-radio-button v-for="panel in panels" :key="panel.id" :value="panel.id">
-                {{ panel.title }}
-              </a-radio-button>
-            </a-radio-group>
-          </template> -->
         </a-tabs>
       </div>
     </div>
@@ -420,7 +409,7 @@ function open(row: Model) {
   color: black !important;
 }
 .health-records-card :deep(.vxe-table--header th) {
-  background: #f8f8f9 !important;
+  background: #F8F8F9 !important;
   color: unset !important;
 }
 .project-card :deep(.vxe-table--border .vxe-body--row > td),

+ 387 - 0
src/service/Derivation.vue

@@ -0,0 +1,387 @@
+<script setup lang="ts">
+import { ref, nextTick, onMounted } from 'vue';
+import { getDictionaryMethod } from '@/request/api/dictionary.api';
+import { pageMedicineMethod, pageDiagnoseTypeMethod, getConditioningSchemeDetailMethod } from '@/request/api/care.api';
+import RemoteSelect from '@/libs/v-select-page/RemoteSelect.vue';
+import 'ant-design-vue/dist/reset.css';
+import type { SystemItemModel } from '@/model/care.model';
+import { VxeUI } from 'vxe-pc-ui';
+type FollowModel = Partial<SystemItemModel>;
+const props = defineProps<{ data: FollowModel }>();
+const emit = defineEmits<{
+  (e: 'submit', data: FollowModel): void;
+}>();
+const formData = reactive<FollowModel>({
+  cpPatientMatchRule: {
+    sex: '',
+    age: '',
+    diagnoseDiseaseNames: [],
+    diagnoseSyndromeNames: [],
+    constitutionGroupNames: [],
+    willillStateNames: [],
+    tabooCrowds: [],
+  },
+});
+
+// 取消
+function cancel() {
+  VxeUI.modal.close(`derivation-modal`);
+}
+// 确定
+function confirm() {
+  console.log(props.data, 'formData==>');
+  if(props.data?.checkedList.includes('2')){
+    const matchRule = formData.cpPatientMatchRule as {
+      diagnoseDiseaseNames?: any[];
+      willillStateNames?: any[];
+      constitutionGroupNames?: any[];
+      diagnoseSyndromeNames?: any[];
+    };
+    const isAllEmpty =
+      (!matchRule.diagnoseDiseaseNames || matchRule.diagnoseDiseaseNames.length === 0) &&
+      (!matchRule.willillStateNames || matchRule.willillStateNames.length === 0) &&
+      (!matchRule.constitutionGroupNames || matchRule.constitutionGroupNames.length === 0) &&
+      (!matchRule.diagnoseSyndromeNames || matchRule.diagnoseSyndromeNames.length === 0);
+    if (isAllEmpty) {
+      VxeUI.modal.message({ content: '专病、欲病状态、体质、证型至少填写一项', status: 'warning' });
+      return;
+    }
+    formData.isForWrap = 'Y';
+  }
+  VxeUI.modal.close(`derivation-modal`);
+  emit('submit', formData);
+}
+// 欲病状态
+const desiredConditions = ref<{ id: string; name: string }[]>([]);
+const constitutionGroups = ref<{ id: string; name: string }[]>([]);
+const tabooCrowds = ref<{ id: string; name: string }[]>([]);
+async function getDesiredConditions() {
+  const res = await getDictionaryMethod('conditioning_wrap_willill_state');
+  if (res?.length > 0) {
+    desiredConditions.value = res.map((item: any) => ({
+      id: item.value,
+      name: item.label,
+    }));
+  }
+}
+// 获取性别
+const genders = ref<{ id: string; name: string }[]>([]);
+const gendersLoading = ref(false);
+async function getGender() {
+  gendersLoading.value = true;
+  const res = await getDictionaryMethod('sys_user_sex');
+  if (res && res.length > 0) {
+    genders.value = res.map((item: any) => ({
+      id: item.label,
+      name: item.label,
+    }));
+  }
+  gendersLoading.value = false;
+}
+// 获取年龄
+const ages = ref<{ id: string; name: string }[]>([]);
+async function getAge() {
+  const res = await getDictionaryMethod('conditioning_wrap_rule_age');
+  if (res && res.length > 0) {
+    ages.value = res.map((item: any) => ({
+      id: item.label,
+      name: item.label,
+    }));
+  }
+}
+// 获取体质
+async function getConstitutionGroup() {
+  const res = await getDictionaryMethod('constitution_group');
+  if (res && res.length > 0) {
+    constitutionGroups.value = res.map((item: any) => ({
+      id: item.value,
+      name: item.label,
+    }));
+  }
+}
+// 获取禁忌
+async function getTabooCrowds() {
+  const res = await getDictionaryMethod('fdhb_taboo_crowd');
+  if (res && res.length > 0) {
+    tabooCrowds.value = res.map((item: any) => ({
+      id: item.value,
+      name: item.label,
+    }));
+  }
+}
+onMounted(async () => {
+  console.log('props.data', props.data);
+  // 获取欲病状态
+  getDesiredConditions();
+  // 获取性别
+  getGender();
+  // 获取年龄
+  getAge();
+  // 获取体质
+  getConstitutionGroup();
+  // 获取禁忌
+  getTabooCrowds();
+
+  // 如果没有传入数据且有id,则从接口获取
+  if (props.data.id) {
+    // 调编辑接口获取调理包详情  获取这些症状
+    const res: any = await getConditioningSchemeDetailMethod(props.data);
+    await nextTick(); // 确保视图更新
+    // 获取适用情况
+    const matchRule = res?.cpPatientMatchRule || {};
+    Object.assign(formData.cpPatientMatchRule!, {
+      sex: matchRule.sex ?? '',
+      age: matchRule.age ?? '',
+      diagnoseDiseaseNames: Array.isArray(matchRule.diagnoseDiseaseNames) ? matchRule.diagnoseDiseaseNames : [],
+      diagnoseSyndromeNames: Array.isArray(matchRule.diagnoseSyndromeNames) ? matchRule.diagnoseSyndromeNames : [],
+      constitutionGroupNames: Array.isArray(matchRule.constitutionGroupNames) ? matchRule.constitutionGroupNames : [],
+      willillStateNames: Array.isArray(matchRule.willillStateNames) ? matchRule.willillStateNames : [],
+      tabooCrowds: Array.isArray(matchRule.tabooCrowds) ? matchRule.tabooCrowds : [],
+    });
+  } else if (props.data.cpPatientMatchRule) {
+    // 在没保存之前回填上次的数据
+    const matchRule = props.data.cpPatientMatchRule;
+    Object.assign(formData.cpPatientMatchRule!, {
+      sex: matchRule.sex ?? '',
+      age: matchRule.age ?? '',
+      diagnoseDiseaseNames: Array.isArray(matchRule.diagnoseDiseaseNames) ? matchRule.diagnoseDiseaseNames : [],
+      diagnoseSyndromeNames: Array.isArray(matchRule.diagnoseSyndromeNames) ? matchRule.diagnoseSyndromeNames : [],
+      constitutionGroupNames: Array.isArray(matchRule.constitutionGroupNames) ? matchRule.constitutionGroupNames : [],
+      willillStateNames: Array.isArray(matchRule.willillStateNames) ? matchRule.willillStateNames : [],
+      tabooCrowds: Array.isArray(matchRule.tabooCrowds) ? matchRule.tabooCrowds : [],
+    });
+  }
+});
+let multiple = ref<boolean>(true);
+</script>
+
+<template>
+  <div class="derivation-container">
+    <!-- 左侧表单区域 -->
+    <div class="form-section">
+      <!-- 适用情况 -->
+      <div class="section-block">
+        <div class="section-title">适用情况</div>
+        <div class="form-content">
+          <!-- 专病 -->
+          <div class="form-item">
+            <span class="label">专病:</span>
+            <RemoteSelect :load="pageMedicineMethod" key-prop="name" v-model:value="formData.cpPatientMatchRule.diagnoseDiseaseNames" style="width: 100%" :multiple="multiple" />
+          </div>
+          <!-- 欲病状态 -->
+          <div class="form-item">
+            <span class="label">欲病状态:</span>
+            <a-select v-model:value="formData.cpPatientMatchRule.willillStateNames" placeholder="请选择" style="width: 100%" allowClear mode="multiple">
+              <a-select-option v-for="option in desiredConditions" :key="option.id" :value="option.id" placeholder="请选择">{{ option.name }}</a-select-option>
+            </a-select>
+          </div>
+
+          <!-- 体质 -->
+          <div class="form-item constitution-item">
+            <span class="label">体质:</span>
+            <a-select v-model:value="formData.cpPatientMatchRule.constitutionGroupNames" style="width: 100%" placeholder="请选择" allowClear mode="multiple">
+              <a-select-option v-for="option in constitutionGroups" :key="option.id" :value="option.id" placeholder="请选择">{{ option.name }}</a-select-option>
+            </a-select>
+          </div>
+
+          <!-- 证型 -->
+          <div class="form-item">
+            <span class="label">证型:</span>
+            <RemoteSelect
+              :load="pageDiagnoseTypeMethod"
+              key-prop="name"
+              v-model:value="formData.cpPatientMatchRule.diagnoseSyndromeNames"
+              style="width: 200px"
+              :multiple="multiple"
+            />
+          </div>
+
+          <!-- 性别限制 -->
+          <div class="form-item">
+            <span class="label">性别限制:</span>
+            <a-select v-model:value="formData.cpPatientMatchRule.sex" placeholder="请选择" style="width: 100%" allowClear>
+              <a-select-option v-for="option in genders" :key="option.id" :value="option.id" placeholder="请选择">
+                {{ option.name }}
+              </a-select-option>
+            </a-select>
+          </div>
+
+          <!-- 年龄限制 -->
+          <div class="form-item">
+            <span class="label">年龄限制:</span>
+            <a-select v-model:value="formData.cpPatientMatchRule.age" placeholder="请选择" style="width: 100%" allowClear>
+              <a-select-option v-for="option in ages" :key="option.id" :value="option.id" placeholder="请选择">{{ option.name }}</a-select-option>
+            </a-select>
+          </div>
+        </div>
+      </div>
+
+      <!-- 禁忌 -->
+      <div class="section-block">
+        <div class="section-title">禁忌</div>
+        <div class="form-content">
+          <div class="form-item">
+            <span class="label">禁忌人群:</span>
+            <a-select v-model:value="formData.cpPatientMatchRule.tabooCrowds" style="width: 100%" placeholder="请选择" allowClear mode="multiple">
+              <a-select-option v-for="option in tabooCrowds" :key="option.id" :value="option.id" placeholder="请选择">{{ option.name }}</a-select-option>
+            </a-select>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 底部按钮 -->
+  <div class="button-container">
+    <a-button @click="cancel">取消</a-button>
+    <a-button type="primary" @click="confirm">确定</a-button>
+  </div>
+</template>
+
+<style scoped>
+.derivation-container {
+  display: flex;
+  gap: 40px;
+  padding: 24px;
+  min-height: 500px;
+  position: relative;
+}
+
+.form-section {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  /* gap: 32px; */
+}
+
+.section-block {
+  background: #fff;
+  border-radius: 8px;
+  padding: 0 0 20px 0px;
+  /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); */
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.form-content {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.form-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  position: relative;
+}
+
+.constitution-item {
+  position: relative;
+}
+
+.label {
+  min-width: 80px;
+  font-size: 14px;
+  color: #333;
+  font-weight: 500;
+  text-align: right;
+}
+
+.info-section {
+  width: 300px;
+  display: flex;
+  align-items: flex-start;
+  padding-top: 80px; /* 对齐体质字段 */
+}
+
+.info-box {
+  background: #fff7e6;
+  border: 1px solid #ffd591;
+  border-radius: 8px;
+  padding: 20px;
+  width: 100%;
+  position: relative;
+}
+
+.info-box::before {
+  content: '';
+  position: absolute;
+  left: -10px;
+  top: 50%;
+  width: 10px;
+  height: 2px;
+  background: #d9d9d9;
+  transform: translateY(-50%);
+}
+
+.info-title {
+  font-size: 14px;
+  font-weight: bold;
+  color: #d46b08;
+  margin-bottom: 12px;
+}
+
+.info-content {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.rule-item {
+  display: flex;
+  align-items: flex-start;
+  gap: 4px;
+  font-size: 13px;
+  line-height: 1.5;
+  color: #666;
+}
+
+.rule-number {
+  color: #d46b08;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.rule-text {
+  flex: 1;
+}
+
+.info-signature {
+  text-align: right;
+  margin-top: 12px;
+  font-size: 12px;
+  color: #999;
+  font-style: italic;
+}
+
+.button-container {
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  margin-top: 32px;
+  padding-top: 20px;
+}
+
+.button-container .ant-btn {
+  min-width: 80px;
+  height: 32px;
+}
+
+.button-container .ant-btn-primary {
+  background: #faad14;
+  border-color: #faad14;
+}
+
+.button-container .ant-btn-primary:hover {
+  background: #ffc53d;
+  border-color: #ffc53d;
+}
+</style>

+ 704 - 0
src/service/EditConfigured.vue

@@ -0,0 +1,704 @@
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+import { type VxeTablePropTypes, type VxeTableEvents, VxeUI } from 'vxe-table';
+import { getDictionaryMethod } from '@/request/api/dictionary.api';
+import { getConditioningDeviceDetailMethod, updateConditioningSchemeMethod } from '@/request/api/care.api';
+import type { ConditioningSchemeModel } from '@/model/care.model';
+import { useRequest } from 'alova/client';
+import { notification } from 'ant-design-vue';
+type ConditioningModel = Partial<ConditioningSchemeModel>;
+
+// 定义表格行数据类型
+interface TableRowData {
+  isCustomize: boolean;
+  conditioningProgramType: string;
+  isHaveForInfer: string | null;
+  knowledgeCpShowType: string;
+  showCount: number;
+  isChecked: boolean;
+  isNewRow?: boolean; // 用于标识是否是新添加的行
+}
+const emits = defineEmits<{
+  submit: [data?: Array<TableRowData>];
+}>();
+const { loading: submitting, send: submit } = useRequest(updateConditioningSchemeMethod, {
+  immediate: false,
+}).onSuccess(({ data }) => {
+  emits('submit');
+});
+const props = defineProps<{ data: ConditioningModel }>();
+// 表格数据
+const tableData = ref<TableRowData[]>([]);
+const typeOptionsLoading = ref<boolean>(false);
+const typeOptions = ref<{ label: string; value: string }[]>([]);
+// 获取方案类型
+async function getConditioningProgramType() {
+  typeOptionsLoading.value = true;
+  try {
+    const res = await getDictionaryMethod('condition_type');
+    if (res?.length > 0) {
+      typeOptions.value = res; // 直接使用返回的数据
+    }
+  } catch (error) {
+    console.error('获取方案类型列表失败:', error);
+  } finally {
+    typeOptionsLoading.value = false;
+  }
+}
+// 监听方案类型 tableData 中的方案类型
+watch(
+  tableData,
+  (newVal) => {
+    // 获取所有已使用的方案类型值
+    const usedTypes = newVal.map((row) => row.conditioningProgramType).filter((type) => type && type !== '');
+    // 过滤掉已经在表格中使用的方案类型
+    typeOptions.value = typeOptions.value.filter((option) => !usedTypes.includes(option.value) && !usedTypes.includes(option.label));
+  },
+  { deep: true }
+);
+// 全局项目条数上限
+const globalItemLimit = ref(3);
+
+// 监听 globalItemLimit 变化,同步所有行的 showCount
+watch(globalItemLimit, (newVal) => {
+  tableData.value.forEach((row) => {
+    row.showCount = Number(newVal);
+  });
+});
+
+// 添加新行
+const addRow = () => {
+  tableData.value.push({
+    conditioningProgramType: '',
+    knowledgeCpShowType: '1', // 1-展示 2-不展示 3-定制项目无结果时展示
+    showCount: globalItemLimit.value,
+    isChecked: false,
+    isHaveForInfer: null,
+    isCustomize: false,
+    isNewRow: true, //新行
+  });
+};
+
+// 删除行
+const deleteRow = (row: any) => {
+  const index = tableData.value.indexOf(row);
+  if (index > -1) {
+    tableData.value.splice(index, 1);
+  }
+};
+
+// 保存
+const saveData = () => {
+  // 数据验证和格式化
+  const formattedData = tableData.value.map((row) => ({
+    ...row,
+    knowledgeCpShowType: row.knowledgeCpShowType,
+    showCount: row.showCount || globalItemLimit.value,
+    isHaveForInfer: row.isHaveForInfer,
+  }));
+
+  if (formattedData.length === 0) {
+    notification.error({
+      message: '请至少添加一个方案类型',
+    });
+    return;
+  }
+  if (formattedData.some((row) => !row.conditioningProgramType)) {
+    notification.error({
+      message: '请完善方案类型',
+    });
+    return;
+  }
+
+  if (!props.data.orgId) {
+    notification.error({
+      message: '未找到组织ID',
+    });
+    return;
+  }
+
+  submit(props.data.orgId, formattedData);
+  VxeUI.modal.close(`configured-modal`);
+};
+
+// 取消
+const cancelEdit = () => {
+  VxeUI.modal.close(`configured-modal`);
+};
+
+// 方案类型选择处理
+const handleSolutionTypeChange = (row: any) => {
+  const option = typeOptions.value.find((opt) => opt.value === row.conditioningProgramType);
+  if (option) {
+    row.conditioningProgramType = option.label;
+  }
+};
+
+const rowConfig = reactive<VxeTablePropTypes.RowConfig>({
+  drag: true,
+});
+const columnConfig = reactive<VxeTablePropTypes.ColumnConfig>({});
+const rowDragstartEvent: VxeTableEvents.RowDragstart = ({ row }) => {
+  console.log(`拖拽开始 ${row}`);
+};
+
+const rowDragendEvent: VxeTableEvents.RowDragend = ({ newRow, oldRow }) => {
+  const oldIndex = tableData.value.indexOf(oldRow as TableRowData);
+  const newIndex = tableData.value.indexOf(newRow as TableRowData);
+
+  if (oldIndex > -1 && newIndex > -1 && oldIndex !== newIndex) {
+    const moved = tableData.value.splice(oldIndex, 1)[0];
+    tableData.value.splice(newIndex, 0, moved);
+  }
+};
+const organizationName = ref('');
+onMounted(async () => {
+  getConditioningProgramType();
+  const res: any = await getConditioningDeviceDetailMethod(props.data);
+  organizationName.value = res.orgName ?? '';
+  tableData.value = res.items ?? [];
+  tableData.value.forEach((row) => {
+    // 只有 isHaveForInfer 不是 null 的情况下才是定制项目
+    if (row.isHaveForInfer === null || row.isHaveForInfer === undefined) {
+      // 不是定制项目,什么都不设置
+      row.isCustomize = false;
+    } else if (row.isHaveForInfer === 'Y') {
+      // 是定制项目且展示
+      row.isChecked = true;
+      row.isCustomize = true;
+    } else if (row.isHaveForInfer === 'N') {
+      // 是定制项目但不展示
+      row.isChecked = false;
+      row.isCustomize = true;
+    }
+  });
+});
+
+// 只允许输入数字的全局项目条数上限输入框 keydown 事件
+const handleGlobalItemLimitKeydown = (params: any) => {
+  const event = params.$event;
+  // 添加安全检查
+  if (!event) {
+    return;
+  }
+  const allowedKeys = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter'];
+  const isNumber = /^[0-9]$/.test(event.key);
+  const isAllowedKey = allowedKeys.includes(event.key);
+
+  // 只允许数字和功能键
+  if (!isNumber && !isAllowedKey) {
+    event.preventDefault();
+    event.stopPropagation();
+    return false;
+  }
+};
+// 全局项目条数上限输入框 keypress 事件
+const handleGlobalItemLimitKeypress = (params: any) => {
+  const event = params.$event;
+
+  if (!event) {
+    return;
+  }
+
+  // 只允许数字字符
+  const isNumber = /^[0-9]$/.test(event.key);
+
+  if (!isNumber) {
+    event.preventDefault();
+    event.stopPropagation();
+    return false;
+  }
+};
+
+// 全局项目条数上限输入框 beforeinput 事件
+const handleGlobalItemLimitBeforeInput = (params: any) => {
+  const event = params.$event;
+  if (!event || typeof event.inputType === 'undefined') {
+    return;
+  }
+
+  const inputType = event.inputType;
+  const data = event.data;
+
+  // 如果是插入文本,检查是否为数字
+  if (inputType === 'insertText' && data) {
+    if (!/^[0-9]+$/.test(data)) {
+      event.preventDefault();
+      return false;
+    }
+  }
+};
+
+// input 事件
+const handleGlobalItemLimitInput = (params: any) => {
+  // 只保留数字
+  const numericValue = String(params.value).replace(/[^0-9]/g, '');
+  let value = numericValue ? Number(numericValue) : 1;
+  if (value < 1) value = 1;
+  globalItemLimit.value = value;
+};
+
+const handleGlobalItemLimitChange = (params: any) => {
+  const value = params.value;
+  if (value < 1) {
+    globalItemLimit.value = 1;
+  } else {
+    globalItemLimit.value = value;
+  }
+};
+
+//paste 事件
+const handleGlobalItemLimitPaste = (params: any) => {
+  const event = params.$event;
+
+  if (!event) {
+    return;
+  }
+
+  const clipboardData = event.clipboardData || (window as any).clipboardData;
+  const pastedData = clipboardData.getData('Text');
+
+  // 检查粘贴的内容是否只包含数字
+  if (!/^[0-9]+$/.test(pastedData)) {
+    event.preventDefault();
+    event.stopPropagation();
+    return false;
+  }
+};
+
+//keydown 事件
+const handleItemLimitKeydown = (params: any) => {
+  const event = params.$event;
+
+  // 添加安全检查
+  if (!event) {
+    return;
+  }
+
+  const allowedKeys = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter'];
+  const isNumber = /^[0-9]$/.test(event.key);
+  const isAllowedKey = allowedKeys.includes(event.key);
+
+  // 严格限制:只允许数字和功能键,其他所有字符都阻止
+  if (!isNumber && !isAllowedKey) {
+    event.preventDefault();
+    event.stopPropagation();
+    return false;
+  }
+};
+
+// 行内项目条数上限输入框 keypress 事件
+const handleItemLimitKeypress = (params: any) => {
+  const event = params.$event;
+
+  // 添加安全检查
+  if (!event) {
+    return;
+  }
+
+  // 只允许数字字符
+  const isNumber = /^[0-9]$/.test(event.key);
+
+  if (!isNumber) {
+    event.preventDefault();
+    event.stopPropagation();
+    return false;
+  }
+};
+
+// beforeinput 事件
+const handleItemLimitBeforeInput = (params: any) => {
+  const event = params.$event;
+  if (!event || typeof event.inputType === 'undefined') {
+    return;
+  }
+
+  const inputType = event.inputType;
+  const data = event.data;
+
+  // 如果是插入文本,检查是否为数字
+  if (inputType === 'insertText' && data) {
+    if (!/^[0-9]+$/.test(data)) {
+      event.preventDefault();
+      return false;
+    }
+  }
+};
+
+// 行内项目条数上限 input 事件
+const handleItemLimitInput = (params: any, row: any) => {
+  // 获取当前输入的值
+  const currentValue = params.value;
+  // 只保留数字
+  const numericValue = String(currentValue).replace(/[^0-9]/g, '');
+  let finalValue = numericValue ? Number(numericValue) : 1;
+  if (finalValue < 1) finalValue = 1;
+
+  row.showCount = finalValue;
+};
+
+// 行内项目条数上限 paste 事件
+const handleItemLimitPaste = (params: any) => {
+  const event = params.$event;
+
+  if (!event) {
+    return;
+  }
+
+  const clipboardData = event.clipboardData || (window as any).clipboardData;
+  const pastedData = clipboardData.getData('Text');
+
+  // 检查粘贴的内容是否只包含数字
+  if (!/^[0-9]+$/.test(pastedData)) {
+    event.preventDefault();
+    event.stopPropagation();
+    return false;
+  }
+};
+</script>
+
+<template>
+  <div class="edit-configured">
+    <!-- 组织名称 -->
+    <div class="organization-name">组织名称:{{ organizationName }}</div>
+    <!-- 表格 -->
+    <vxe-table
+      border
+      :data="tableData"
+      :row-config="rowConfig"
+      :column-config="columnConfig"
+      @row-dragstart="rowDragstartEvent"
+      @row-dragend="rowDragendEvent"
+      class="solution-table"
+    >
+      <!-- 方案类型列 -->
+      <vxe-column field="conditioningProgramType" title="方案类型 (拖拽调整排序)" width="220" drag-sort>
+        <template #default="{ row }">
+          <div v-if="row.isCustomize" class="solution-type-row">
+            <span>{{ row.conditioningProgramType }}</span>
+          </div>
+          <div v-else-if="row.conditioningProgramType && !row.isNewRow" class="solution-type-row">
+            <span>{{ row.conditioningProgramType }}</span>
+          </div>
+          <div v-else class="solution-type-row">
+            <a-select
+              class="solution-type-select"
+              style="width: 100%"
+              v-model:value="row.conditioningProgramType"
+              :options="typeOptions"
+              placeholder="请选择"
+              allowClear
+              :loading="typeOptionsLoading"
+              @change="handleSolutionTypeChange(row)"
+            />
+          </div>
+        </template>
+      </vxe-column>
+
+      <!-- 定制项目列 -->
+      <vxe-column field="isHaveForInfer" title="定制项目" width="160" align="center">
+        <template #default="{ row }">
+          <div class="custom-project-indicator">
+            <div v-if="row.isCustomize" class="custom-project-true">
+              <a-checkbox v-model:checked="row.isChecked">展示</a-checkbox>
+            </div>
+            <div v-else class="custom-project-false">
+              <div class="red-x-icon">×</div>
+            </div>
+          </div>
+        </template>
+      </vxe-column>
+
+      <!-- 智能推荐项目列 -->
+      <vxe-column field="knowledgeCpShowType" title="智能推荐项目" width="330" align="center">
+        <template #default="{ row }">
+          <div class="smart-recommend-custom">
+            <label>
+              <input type="radio" v-model="row.knowledgeCpShowType" value="1" />
+              展示
+            </label>
+            <label v-if="row.isCustomize">
+              <input type="radio" v-model="row.knowledgeCpShowType" value="3" />
+              定制项目无结果时展示
+            </label>
+            <label>
+              <input type="radio" v-model="row.knowledgeCpShowType" value="2" />
+              不展示
+            </label>
+          </div>
+        </template>
+      </vxe-column>
+
+      <!-- 项目条数上限列 -->
+      <vxe-column field="showCount" title="项目条数上限" width="218" align="center">
+        <template #header>
+          <div class="item-limit-header">
+            项目条数上限
+            <vxe-input
+              v-model="globalItemLimit"
+              type="number"
+              size="mini"
+              style="width: 60px; margin-left: 8px"
+              min="1"
+              @keydown="handleGlobalItemLimitKeydown"
+              @keypress="handleGlobalItemLimitKeypress"
+              @beforeinput="handleGlobalItemLimitBeforeInput"
+              @input="handleGlobalItemLimitInput"
+              @change="handleGlobalItemLimitChange"
+              @paste="handleGlobalItemLimitPaste"
+            />
+            <span style="margin-left: 4px">条</span>
+          </div>
+        </template>
+        <template #default="{ row }">
+          <vxe-input
+            v-model="row.showCount"
+            type="number"
+            size="mini"
+            style="width: 80px"
+            @keydown="handleItemLimitKeydown"
+            @keypress="(params) => handleItemLimitKeypress(params)"
+            @beforeinput="(params) => handleItemLimitBeforeInput(params)"
+            @input="(params) => handleItemLimitInput(params, row)"
+            @paste="handleItemLimitPaste"
+            min="1"
+          />
+          <span style="margin-left: 4px">条</span>
+        </template>
+      </vxe-column>
+
+      <!-- 操作列 -->
+      <vxe-column title="操作" width="120" align="center">
+        <template #default="{ row }">
+          <vxe-button v-if="!row.isCustomize" mode="text" status="primary" @click="deleteRow(row)"> 删除 </vxe-button>
+        </template>
+      </vxe-column>
+    </vxe-table>
+
+    <!-- 添加按钮 -->
+    <div class="add-button-container">
+      <vxe-button type="primary" size="large" @click="addRow" class="add-button"> + </vxe-button>
+    </div>
+
+    <!-- 底部操作按钮 -->
+    <div class="action-buttons">
+      <vxe-button @click="cancelEdit">取消</vxe-button>
+      <vxe-button type="primary" status="warning" @click="saveData">保存</vxe-button>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.edit-configured {
+  padding: 15px;
+  background: #fff;
+}
+
+.organization-name {
+  font-size: 15px;
+  margin-bottom: 20px;
+  color: #333;
+}
+
+.solution-table {
+  margin-bottom: 20px;
+}
+
+.custom-project-indicator {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+
+.custom-project-true {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 4px;
+  transition: background-color 0.2s ease;
+}
+
+.custom-project-true:hover {
+  background-color: rgba(24, 144, 255, 0.05);
+}
+
+.blue-checkbox {
+  width: 16px;
+  height: 16px;
+  background: #1890ff;
+  border-radius: 2px;
+  position: relative;
+}
+
+.blue-checkbox::after {
+  content: '';
+  position: absolute;
+  left: 5px;
+  top: 2px;
+  width: 4px;
+  height: 8px;
+  border: 2px solid white;
+  border-top: none;
+  border-left: none;
+  transform: rotate(45deg);
+}
+
+.custom-project-false {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.red-x-icon {
+  width: 16px;
+  height: 16px;
+  background: #ff4d4f;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: white;
+  font-size: 12px;
+  font-weight: bold;
+}
+
+.smart-recommend-custom,
+.smart-recommend-non-custom {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.smart-recommend-custom label,
+.smart-recommend-non-custom label {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  margin-left: 10px;
+}
+
+.item-limit-header {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.add-button-container {
+  display: flex;
+  justify-content: center;
+  margin: 20px 0;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  margin-top: 80px;
+}
+
+/* vxe-table 样式调整 */
+:deep(.vxe-table) {
+  border: 1px solid #e8e8e8;
+}
+
+:deep(.vxe-table--header) {
+  background: #fafafa;
+}
+
+:deep(.vxe-table--body) {
+  background: #fff;
+}
+
+:deep(.vxe-table--row) {
+  border-bottom: 1px solid #f0f0f0;
+}
+
+:deep(.vxe-table--cell) {
+  padding: 12px 8px;
+}
+
+:deep(.vxe-table:not([data-calc-col]) .vxe-cell--wrapper) {
+  display: flex;
+}
+/* 下拉选择框样式 */
+select {
+  width: 100%;
+  padding: 4px 8px;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  background: #fff;
+  margin-left: 8px;
+}
+
+select:focus {
+  outline: none;
+  border-color: #1890ff;
+  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+}
+
+/* 添加按钮样式优化 */
+.add-button {
+  width: 60px;
+  height: 60px;
+  border-radius: 50%;
+  font-size: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: none;
+  color: #1890ff;
+  cursor: pointer;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  margin-top: 80px;
+}
+
+.action-buttons .vxe-button {
+  min-width: 80px;
+  height: 36px;
+  border-radius: 6px;
+  font-size: 14px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+}
+
+.action-buttons .vxe-button:first-child {
+  background: #f5f5f5;
+  border: 1px solid #d9d9d9;
+  color: #666;
+}
+
+.action-buttons .vxe-button:first-child:hover {
+  background: #e6e6e6;
+  border-color: #bfbfbf;
+  color: #333;
+}
+:deep(.vxe-input) {
+  border-radius: 4px;
+  transition: all 0.3s ease;
+}
+
+:deep(.vxe-input:focus) {
+  border-color: #1890ff;
+  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+}
+
+.solution-type-row {
+  display: flex;
+  align-items: center;
+  margin-left: 8px;
+  width: 100%;
+}
+
+:deep(.vxe-table .vxe-cell--wrapper) {
+  align-items: center;
+}
+</style>

+ 293 - 10
src/service/ServiceDetail.vue

@@ -1,7 +1,8 @@
 <script setup lang="ts">
 import type { SystemItemModel } from '@/model/care.model';
 import { getConditioningSchemeDetailMethod } from '@/request/api/care.api';
-
+import VxeUI from 'vxe-table';
+import { computed } from 'vue';
 const props = defineProps<{
   data: SystemItemModel;
 }>();
@@ -13,6 +14,68 @@ onMounted(async () => {
     console.log(error, 'getCpDetailMethod-error');
   }
 });
+function handleVideoPreview() {
+  const videoUrl = props.data.itemVideoFirst;
+  if (videoUrl) {
+    // 创建一个模态框来预览视频
+    VxeUI.modal.open({
+      title: '视频预览',
+      width: 800,
+      height: 600,
+      escClosable: true,
+      destroyOnClose: true,
+      slots: {
+        default() {
+          return h(
+            'div',
+            {
+              style: {
+                display: 'flex',
+                justifyContent: 'center',
+                alignItems: 'center',
+                height: '100%',
+              },
+            },
+            [
+              h('video', {
+                src: videoUrl,
+                controls: true,
+                style: {
+                  maxWidth: '100%',
+                  maxHeight: '100%',
+                  borderRadius: '8px',
+                },
+              }),
+            ]
+          );
+        },
+      },
+    });
+  }
+}
+
+const projectApplication = computed(() => {
+  const isWrap = props.data.isForWrap === 'Y';
+  const isInfer = props.data.isForInfer === 'Y';
+  if (isWrap && isInfer) return '服务包项目;调理方案项目';
+  if (isWrap) return '服务包项目';
+  if (isInfer) return '调理方案项目';
+  return '';
+});
+
+const hasAnyDerivation = computed(() => {
+  const rule = props.data.cpPatientMatchRule;
+  if (!rule) return false;
+  return Boolean(
+    rule.sex ||
+      rule.age ||
+      (rule.diagnoseDiseaseNames && rule.diagnoseDiseaseNames.length > 0) ||
+      (rule.diagnoseSyndromeNames && rule.diagnoseSyndromeNames.length > 0) ||
+      (rule.constitutionGroupNames && rule.constitutionGroupNames.length > 0) ||
+      (rule.willillStateNames && rule.willillStateNames.length > 0) ||
+      (rule.tabooCrowds && rule.tabooCrowds.length > 0)
+  );
+});
 </script>
 
 <template>
@@ -25,6 +88,10 @@ onMounted(async () => {
       <div class="label">方案类型:</div>
       <div class="content">{{ data.conditioningProgramType }}</div>
     </div>
+    <div class="detail-item" v-if="data?.isForWrap || data?.isForInfer">
+      <div class="label">项目应用:</div>
+      <div class="content">{{ projectApplication }}</div>
+    </div>
     <div class="detail-item">
       <div class="label">计价规则:</div>
       <div class="content">{{ data.pricingType === '0' ? '一口价' : '按穴位/脉络/部位计价' }}</div>
@@ -61,12 +128,94 @@ onMounted(async () => {
       </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>
+      <div class="label">组成:</div>
+      <div class="content" v-for="item in data?.cpMedicines" :key="item.id">{{ item.name }} ;</div>
+    </div>
+    <div class="detail-item" v-if="data?.attrFirst">
+      <div class="label">特色:</div>
+      <div class="content font-extrabold">{{ data?.attrFirst }}</div>
     </div>
-    <div class="detail-item" v-if="data?.effect">
+    <div class="detail-item" v-if="data?.attrSecond">
       <div class="label">功效:</div>
-      <div class="content font-extrabold">{{ data?.effect }}</div>
+      <div class="content font-extrabold">{{ data?.attrSecond }}</div>
+    </div>
+    <div class="detail-item" v-if="data?.attrThird">
+      <div class="label">制法:</div>
+      <div class="content font-extrabold">{{ data?.attrThird }}</div>
+    </div>
+    <div class="detail-item" v-if="data?.attrFourth">
+      <div class="label">用法:</div>
+      <div class="content font-extrabold">{{ data?.attrFourth }}</div>
+    </div>
+    <div class="detail-item" v-if="data?.attrFifth">
+      <div class="label">操作方法:</div>
+      <div class="content font-extrabold">{{ data?.attrFifth }}</div>
+    </div>
+    <div class="detail-item" v-if="data?.itemImgFirst">
+      <div class="label">操作图片:</div>
+      <div class="content">
+        <a-image :width="100" :height="100" :src="data?.itemImgFirst" class="service-image" />
+      </div>
+    </div>
+    <div class="detail-item" v-if="data?.photo">
+      <div class="label">商品图片:</div>
+      <div class="content">
+        <a-image :width="100" :height="100" :src="data?.photo" class="service-image" />
+      </div>
+    </div>
+    <div class="detail-item" v-if="data?.itemVideoFirst">
+      <div class="label">操作视频:</div>
+      <div class="video-preview-container">
+        <div class="video-thumbnail">
+          <video :src="data.itemVideoFirst" preload="metadata" class="video-preview" />
+          <div class="video-overlay">
+            <a-button type="primary" size="small" @click.stop="handleVideoPreview()"> 预览 </a-button>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="detail-item" v-if="data?.attrSixth">
+      <div class="label">疗程说明:</div>
+      <div class="content font-extrabold">{{ data?.attrSixth }}</div>
+    </div>
+    <div class="detail-item" v-if="data?.attrSeventh">
+      <div class="label">使用注意:</div>
+      <div class="content font-extrabold">{{ data?.attrSeventh }}</div>
+    </div>
+    <div class="detail-item" v-if="hasAnyDerivation">
+      <div class="label">推导逻辑:</div>
+      <div style="cursor: pointer">
+        <div class="derivation-container">
+          <div v-if="data.cpPatientMatchRule.sex" class="derivation-item">
+            <span class="derivation-label">性别限制:</span>
+            <span class="derivation-content">{{ data.cpPatientMatchRule.sex }}</span>
+          </div>
+          <div v-if="data.cpPatientMatchRule.age" class="derivation-item">
+            <span class="derivation-label">年龄限制:</span>
+            <span class="derivation-content">{{ data.cpPatientMatchRule.age }}</span>
+          </div>
+          <div v-if="data.cpPatientMatchRule.diagnoseDiseaseNames && data.cpPatientMatchRule.diagnoseDiseaseNames.length > 0" class="derivation-item">
+            <span class="derivation-label">专病:</span>
+            <span class="derivation-content">{{ data.cpPatientMatchRule.diagnoseDiseaseNames.join('、') }}</span>
+          </div>
+          <div v-if="data.cpPatientMatchRule.diagnoseSyndromeNames && data.cpPatientMatchRule.diagnoseSyndromeNames.length > 0" class="derivation-item">
+            <span class="derivation-label">证型:</span>
+            <span class="derivation-content">{{ data.cpPatientMatchRule.diagnoseSyndromeNames.join('、') }}</span>
+          </div>
+          <div v-if="data.cpPatientMatchRule.constitutionGroupNames && data.cpPatientMatchRule.constitutionGroupNames.length > 0" class="derivation-item">
+            <span class="derivation-label">体质:</span>
+            <span class="derivation-content">{{ data.cpPatientMatchRule.constitutionGroupNames.join('、') }}</span>
+          </div>
+          <div v-if="data.cpPatientMatchRule.willillStateNames && data.cpPatientMatchRule.willillStateNames.length > 0" class="derivation-item">
+            <span class="derivation-label">欲病状态:</span>
+            <span class="derivation-content">{{ data.cpPatientMatchRule.willillStateNames.join('、') }}</span>
+          </div>
+          <div v-if="data.cpPatientMatchRule.tabooCrowds && data.cpPatientMatchRule.tabooCrowds.length > 0" class="derivation-item">
+            <span class="derivation-label">禁忌人群:</span>
+            <span class="derivation-content">{{ data.cpPatientMatchRule.tabooCrowds.join('、') }}</span>
+          </div>
+        </div>
+      </div>
     </div>
     <div class="detail-item" v-if="data?.institutionName">
       <div class="label">机构名称:</div>
@@ -76,11 +225,17 @@ onMounted(async () => {
       <div class="label">供应商:</div>
       <div class="content">{{ data?.conditioningProgramSupplierName }}</div>
     </div>
-    <div class="detail-item" v-if="data?.photo">
-      <div class="label">图片:</div>
-      <div class="content">
-        <a-image :width="100" :height="100" :src="data?.photo" class="service-image" />
-      </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>
+    </div>
+    <div class="detail-item" v-if="data?.status === '0' || data?.status === '1'">
+      <div class="label">启用状态:</div>
+      <div class="content">{{ data?.status === '0' ? '启用' : '停用' }}</div>
     </div>
   </div>
 </template>
@@ -108,4 +263,132 @@ onMounted(async () => {
     }
   }
 }
+.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: 100%;
+  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;
+}
 </style>

+ 4 - 7
src/service/ServiceItemsList.vue

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

+ 25 - 19
src/service/ServicePackageDetail.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, reactive } from 'vue';
+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';
@@ -19,7 +19,9 @@ let tableData = ref<any>({});
 async function getRecordDetail() {
   try {
     const res: any = await getConditioningRecordDetailMethod(props.data);
-    tableData.value = res;
+    if(res && res.length > 0){
+      tableData.value = res;
+    }
   } catch (error) {
     notification.error({ message: '获取数据失败' });
   }
@@ -89,6 +91,21 @@ function handleVoid() {
     emit('voidSubmit', tableData.value as SystemCwModel);
   });
 }
+
+const progressTextMap: Record<string, string> = {
+  '0': '待付款',
+  '1': '已作废',
+  '2': '用户取消',
+  '3': '未开始',
+  '4': '调理中',
+  '5': '已完结'
+};
+
+const progressText = computed(() => {
+  console.log(tableData.value,"tableData.value");
+  return progressTextMap[tableData.value?.progress] || '';
+});
+
 </script>
 
 <template>
@@ -96,23 +113,7 @@ function handleVoid() {
     <h2 class="title">{{ tableData?.name || tableData?.conditioningWrapName }}</h2>
     <!-- 顶部信息 -->
     <div class="header">
-      <span class="status" v-if="tableData?.progress">{{
-        tableData?.progress === '0'
-          ? '待付款'
-          : tableData?.progress === '1'
-            ? '已作废'
-            : tableData?.progress === '2'
-              ? '用户取消'
-              : tableData?.progress === '3'
-                ? '未开始'
-                : tableData?.progress === '4'
-                  ? '调理中'
-                  : tableData?.progress === '5'
-                    ? '已完结'
-                    : tableData?.progress === '6'
-                      ? '待收货'
-                      : ''
-      }}</span>
+      <span class="status" v-if="tableData?.progress">{{ progressText }}</span>
       <div>
         <!-- 调养记录详情 -->
         <div v-if="props.data.types === 'record'" style="line-height: 30px">
@@ -193,6 +194,11 @@ function handleVoid() {
         </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>
 
     <!-- 表格和作废字样包裹层 -->

+ 59 - 0
src/views/ProcessConfigDemo.vue

@@ -0,0 +1,59 @@
+<script setup lang="ts">
+import ProcessConfig from '@/components/ProcessConfig.vue';
+import { ref } from 'vue';
+
+const showModal = ref(false);
+
+function handleSubmit(data: any) {
+  console.log('提交的数据:', data);
+  showModal.value = false;
+}
+
+function handleCancel() {
+  showModal.value = false;
+}
+
+function openModal() {
+  showModal.value = true;
+}
+</script>
+
+<template>
+  <div class="demo-container">
+    <div class="demo-header">
+      <h1>流程配置演示</h1>
+      <a-button type="primary" @click="openModal">打开配置界面</a-button>
+    </div>
+
+    <a-modal
+      v-model:open="showModal"
+      title="流程配置"
+      width="800px"
+      :footer="null"
+      :destroyOnClose="true"
+    >
+      <ProcessConfig
+        @submit="handleSubmit"
+        @cancel="handleCancel"
+      />
+    </a-modal>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.demo-container {
+  padding: 20px;
+  
+  .demo-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+    
+    h1 {
+      margin: 0;
+      color: #333;
+    }
+  }
+}
+</style>