Sfoglia il codice sorgente

Merge branch 'feature/task-218' into develop

张田田 5 mesi fa
parent
commit
d0eb26d8a4

+ 3 - 2
src/components/EditEquirement.vue

@@ -6,9 +6,10 @@ 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>;
-
+(notification.config as any)({
+  zIndex: 10000, // 直接设置层级
+});
 const defaultModel = {
   deviceIds: [''],
 };

+ 3 - 1
src/components/EditMoreConfigured.vue

@@ -8,7 +8,9 @@ import type { DeviceManageModel } from '@/model/device.model';
 const flowData = ref<FlowRequestData>();
 const loading = ref(false);
 // const defaultModel: DeviceManageModel = {};
-
+(notification.config as any)({
+  zIndex: 10000, // 直接设置层级
+});
 const props = defineProps<{ data: DeviceManageModel[] }>();
 
 const emits = defineEmits<{

+ 0 - 109
src/components/EditMoreEquirement.vue

@@ -1,109 +0,0 @@
-<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() {
-  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() {
-  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>

+ 3 - 1
src/components/EditOrganization.vue

@@ -5,7 +5,9 @@ import { branchMethod } from '@/request/api/system.api';
 import { notification } from 'ant-design-vue';
 import type { EquirementModel } from '@/model/device.model';
 type FollowModel = Partial<EquirementModel>;
-
+  (notification.config as any)({
+  zIndex: 10000, // 直接设置层级
+});
 const defaultModel = {};
 
 const props = defineProps<{ data: FollowModel }>();

+ 0 - 9
src/components/EditPatient.vue

@@ -8,7 +8,6 @@ import { notification } from 'ant-design-vue';
 import { message as Message } from 'ant-design-vue/es/components';
 import RemoteSelect from '@/libs/v-select-page/RemoteSelect.vue';
 import builtinRegions from '@/tools/regions';
-import { useRequest } from 'alova/client';
 
 interface FormModel {}
 const props = defineProps<{
@@ -20,7 +19,6 @@ const emits = defineEmits<{
 
 // 编辑患者
 const form = ref({});
-// 初始化中标记:用于避免初始化数据时触发 watch 引起二次覆盖
 const initializing = ref(false);
 const submitting = ref(false);
 // 身份证校验(格式、生日、校验码)
@@ -90,7 +88,6 @@ watch(
       return;
     }
     if (!newCardno) {
-      // 仅当用户清空且表单里本就没有后端给的年龄/性别时才清空
       const hasServerAge = form.value && (form.value as any).age != null && (form.value as any).age !== '';
       const hasServerSex = form.value && (form.value as any).sex != null && (form.value as any).sex !== '';
       if (!hasServerAge && !hasServerSex) {
@@ -150,7 +147,6 @@ function normalizeRegionValue() {
   const fv: any = form.value as any;
   const raw: any = fv?.region ?? fv?.regionCode ?? fv?.districtCode ?? fv?.areaCode ?? fv?.cityCode ?? fv?.provinceCode;
   if (!raw) {
-    // 尝试从拆分的字段组装
     const pc = fv?.provinceCode ?? fv?.province ?? fv?.province_id;
     const cc = fv?.cityCode ?? fv?.city ?? fv?.city_id;
     const dc = fv?.districtCode ?? fv?.district ?? fv?.areaCode ?? fv?.area ?? fv?.district_id;
@@ -217,7 +213,6 @@ async function getFoodAllergy() {
       id: item.value,
       name: item.label,
     }));
-    // 若表单里存的是名称数组,统一转换为编码数组
     const raw = (form.value as any)?.foodAllergy;
     const currentArr: any[] = Array.isArray(raw) ? raw : typeof raw === 'string' ? raw.split(',') : raw != null ? [raw] : [];
     if (currentArr.length > 0) {
@@ -245,7 +240,6 @@ async function getHobbyFlavor() {
       id: item.value,
       name: item.label,
     }));
-    // 若表单里存的是名称数组,统一转换为编码数组
     const raw = (form.value as any)?.hobbyFlavor;
     const currentArr: any[] = Array.isArray(raw) ? raw : typeof raw === 'string' ? raw.split(',') : raw != null ? [raw] : [];
     if (currentArr.length > 0) {
@@ -274,7 +268,6 @@ async function getJob() {
       id: item.value,
       name: item.label,
     }));
-    // 若表单里存的是名称,将其规范为编码
     const current = (form.value as any)?.job;
     if (current !== undefined && current !== null && current !== '') {
       const byId = job.value.find((i) => String(i.id) === String(current));
@@ -518,8 +511,6 @@ const formEmits: VxeFormListeners<FormModel> = {
     if (Array.isArray(processedData.hobbyFlavor)) {
       processedData.hobbyFlavor = processedData.hobbyFlavor.join(',');
     }
-    console.log(processedData, '处理后的数据');
-    // 直接调用 API 方法
     submitting.value = true;
     try {
       await patientBasicInfoMethod(processedData);

+ 3 - 1
src/components/Enabled.vue

@@ -16,7 +16,9 @@ import {
   type VxeFormInstance,
 } from 'vxe-pc-ui';
 import { list2Groups } from '@/tools/data';
-
+(notification.config as any)({
+  zIndex: 10000, // 直接设置层级
+});
 type FollowModel = Partial<PlanModel>;
 
 const defaultModel = {};

+ 43 - 11
src/components/PatientHealthRecordPreview.vue

@@ -15,6 +15,8 @@ import HealthReportAnalysisWidget from '@/widgets/HealthReportAnalysisWidget.vue
 const props = defineProps<{
   patient: Partial<PatientModel>;
   report: Partial<ReportModel>;
+  source?: string;
+  sourceInsName?: string;
 }>();
 
 const emits = defineEmits<{
@@ -137,13 +139,22 @@ function openPatientTagEdit(event: MouseEvent) {
 function openPatientRecordsPreview() {
   const component = defineAsyncComponent(() => import('@/components/RecordsPatientPreview.vue'));
   const id = `modal:record-patient:preview`;
-  const onDestroy = () => { VxeUI.modal.close(id); };
+  const onDestroy = () => {
+    VxeUI.modal.close(id);
+  };
   onDestroy();
   VxeUI.modal.open({
-    id, remember: true,
-    showMaximize: true, mask: false, lockView: false, padding: false,
-    resize: true, width: Math.floor(window.innerWidth * 0.5), height: Math.floor(window.innerHeight * 0.5),
-    escClosable: true, maskClosable: true,
+    id,
+    remember: true,
+    showMaximize: true,
+    mask: false,
+    lockView: false,
+    padding: false,
+    resize: true,
+    width: Math.floor(window.innerWidth * 0.5),
+    height: Math.floor(window.innerHeight * 0.5),
+    escClosable: true,
+    maskClosable: true,
     title: `基础信息更新记录`,
     slots: {
       default() {
@@ -160,13 +171,22 @@ function openIndicatorRecordsPreview() {
   const component = defineAsyncComponent(() => import('@/components/RecordsIndicatorPreview.vue'));
   const id = `modal:record-indicator:preview`;
   console.log(patient.value, 'patient======');
-  const onDestroy = () => { VxeUI.modal.close(id); };
+  const onDestroy = () => {
+    VxeUI.modal.close(id);
+  };
   onDestroy();
   VxeUI.modal.open({
-    id, remember: true,
-    showMaximize: true, mask: false, lockView: false, padding: false,
-    resize: true, width: Math.floor(window.innerWidth * 0.5), height: Math.floor(window.innerHeight * 0.5),
-    escClosable: true, maskClosable: true,
+    id,
+    remember: true,
+    showMaximize: true,
+    mask: false,
+    lockView: false,
+    padding: false,
+    resize: true,
+    width: Math.floor(window.innerWidth * 0.5),
+    height: Math.floor(window.innerHeight * 0.5),
+    escClosable: true,
+    maskClosable: true,
     title: `指标信息更新记录`,
     slots: {
       default() {
@@ -185,6 +205,11 @@ function openIndicatorRecordsPreview() {
   <div class="p-6">
     <div class="flex">
       <section class="flex-auto">
+        <div class="row mb-5" v-if="source || sourceInsName">
+          <span class="text-lg mr-12 font-bold">来源</span>
+          <a-tag color="pink">{{ source }}</a-tag>
+          <a-tag color="red">{{ sourceInsName }}</a-tag>
+        </div>
         <header class="flex items-center">
           <div class="title">基本信息</div>
           <a-button type="link" @click="openPatientRecordsPreview">更新记录</a-button>
@@ -294,7 +319,10 @@ function openIndicatorRecordsPreview() {
             <div class="flex flex-wrap">
               <div class="text-center w-260px row" v-for="item in indicator" :key="item.id">
                 <div class="flex justify-center">
-                  <span><label>{{ item.name }}</label>{{ item.value }}{{ item.unit }}</span>
+                  <span
+                    ><label>{{ item.name }}</label
+                    >{{ item.value }}{{ item.unit }}</span
+                  >
                   <div class="inline-block ml-2 size-24px">
                     <a-button v-if="item.trend > 0" :icon="h(ArrowUpOutlined)" shape="circle" size="small" class="trend-up" />
                     <a-button v-else-if="item.trend < 0" :icon="h(ArrowDownOutlined)" shape="circle" size="small" class="trend-down" />
@@ -323,6 +351,10 @@ function openIndicatorRecordsPreview() {
 </template>
 
 <style scoped lang="scss">
+.title-text {
+  font-size: 16px;
+  color: rgba(0, 0, 0, 0.45);
+}
 section {
   color: rgba(0, 0, 0, 0.85);
 

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

@@ -60,6 +60,7 @@ export interface SystemItemModel {
   isForInfer?: string | null; // 是否为调理方案项目
   effect: string; // 功效
   itemImgFirst: string; // 操作图片
+  miniProgramCode: string; // 小程序码
   itemVideoFirst: string; // 操作视频
   attrFirst: string; // 特色
   isOffline?: string | null; // 是否线下
@@ -74,6 +75,7 @@ export interface SystemItemModel {
   types?: string[]; // 项目应用
   attrEighth: string; // 购买链接
   attrNinth: string; // 跳转类型
+  attrTen: string; // 小程序码
 }
 
 export type SystemIteQuery = Partial<SystemItemModel>;
@@ -354,8 +356,13 @@ export interface OpenConditioningSchemeModel {
 // 调理方案配置
 export interface ConditioningSchemeModel {
   id: string | number; // 调理方案配置ID
+  insId: string | number; // 机构id
+  insName?: string; // 机构名称
+  selectedCpIds: string[]; // 选中的项目ID
   orgId: string | number; // 组织id
   orgName: string; // 组织名称
+  institutionId: string; // 机构ID
+  institutionName: string; // 机构名称
   isConfig: string; // 是否配置
   configTimeStart: string; // 配置开始时间
   configTimeEnd: string; // 配置结束时间
@@ -369,7 +376,8 @@ export interface ConditioningSchemeModel {
     isShowForInfer: string; // 是否展示定制项目
     knowledgeCpShowType: string; // 智能推荐项目展示方式 1-展示 2-不展示 3-定制项目无结果时展示
     showCount: number; // 展示数量
-    
+      
   }[];
+  conditioningProgramType: string; // 调理方案类型
 }
 export type ConditioningSchemeQuery = Partial<ConditioningSchemeModel>;

+ 7 - 0
src/model/patient.model.ts

@@ -8,6 +8,10 @@ export interface PatientQuery {
   cardno?: string;
   isHaveHealthAnalysisReport?: boolean;
   tags?: string[];
+  source?: string;
+  sourceInsId?: number;
+  sourceInsName?: string;
+  pcrCount?: number;
 
   count?: number;
 }
@@ -43,6 +47,9 @@ export interface EditPatientModel {
 export interface PatientReportModel extends PatientModel {
   uid: string;
   report: ReportModel;
+  source?: string;
+  sourceInsName?: string;
+  sourceInsId?: number;
 }
 
 export interface PatientRecordModel extends PatientModel {

+ 46 - 10
src/pages/index/care/configured.vue

@@ -1,7 +1,6 @@
 <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';
@@ -50,6 +49,33 @@ const searchFormProps = reactive<VxeFormProps<ConditioningSchemeQuery>>({
           optionProps: { value: 'id', label: 'label' },
           clearable: true,
         },
+        events: {
+          change(val: any) {
+            insArr.value = [];
+            if (val.data.orgId) {
+              // 清空表单中的机构名称字段
+              if (model.value) {
+                // model.value.institutionId = '';
+                model.value.insId = '';
+              }
+              getInstitution(val.data.orgId);
+            }
+          },
+        },
+      },
+    },
+    {
+      field: 'insId',
+      title: '机构名称',
+      span: 6,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => insLoading.value),
+          options: computed(() => insArr.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
       },
     },
     {
@@ -128,14 +154,15 @@ const gridOptions = reactive<VxeGridProps<ConditioningSchemeModel>>({
   },
   columns: [
     { field: 'orgName', title: '组织名称' },
-    {
-      field: 'isHaveForInfer',
-      title: '是否定制项目',
-      slots: {
-        default: 'isHaveForInferSlot',
-      },
-    },
-    { field: 'forInferCount', title: '定制项目数' },
+    { field: 'insName', title: '机构名称' },
+    // {
+    //   field: 'isHaveForInfer',
+    //   title: '是否定制项目',
+    //   slots: {
+    //     default: 'isHaveForInferSlot',
+    //   },
+    // },
+    // { field: 'forInferCount', title: '定制项目数' },
 
     {
       field: 'isConfig',
@@ -198,7 +225,16 @@ async function getDeviceType() {
   }
   deviceTypesLoading.value = false;
 }
-
+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));
+  if (res && res.length > 0) {
+    insArr.value = res;
+  }
+  insLoading.value = false;
+}
 onMounted(() => {
   getDeviceType();
   model.value = toRaw(searchFormProps.data);

+ 3 - 1
src/pages/index/care/issueService.vue

@@ -26,7 +26,9 @@ import { VxeUI } from 'vxe-pc-ui';
 import type { HealthReportVO } from '@/model/health-report.model';
 import type { PatientTagVO } from '@/model/patient.model';
 import dayjs from 'dayjs';
-
+(notification.config as any)({
+  zIndex: 10000, // 直接设置层级
+});
 import { getPatientHealthRecordsMethod } from '@/request/api/report.api';
 
 type FollowModel = Partial<OpenConditioningSchemeModel>;

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

@@ -13,6 +13,9 @@ import type { EquirementModel, EquirementQuery } from '@/model/device.model';
 import { allTagsSearchMethod } from '@/request/api/follow.api';
 import { getDeviceRegisterMethod, deleteDeviceRegisterMethod, updateDeviceRegisterOrganizationMethod } from '@/request/api/device.api';
 import { branchMethod } from '@/request/api/system.api';
+(notification.config as any)({
+  zIndex: 10000, // 直接设置层级
+});
 // 获取设备列表
 const { data: tags, loading: tagsLoading } = useRequest(getDeviceRegisterMethod, {
   initialData: { total: 0, data: [] },

+ 96 - 6
src/pages/index/patient/history.vue

@@ -1,14 +1,14 @@
 <script setup lang="ts">
 import { h } from 'vue';
-import { notification } from 'ant-design-vue';
+import { notification, Row } from 'ant-design-vue';
 import { usePagination, useRequest } from 'alova/client';
 import { patientsHistoryMethod, patientsHistoryPullMethod, searchTagsFromSelectableMethod, carePatientMethod, patientMethod } from '@/request/api/patient.api';
 import type { PatientQuery, PatientReportModel, PatientTagVO } from '@/model';
 import { list2Groups } from '@/tools/data';
-
+import { branchMethod } from '@/request/api/system.api';
 import { VxeButton, type VxeFormListeners, type VxeFormProps, VxeUI } from 'vxe-pc-ui';
 import type { VxeGridInstance, VxeGridListeners, VxeGridProps } from 'vxe-table';
-
+import { getDictionaryMethod } from '@/request/api/dictionary.api';
 import { usePermission } from '@/core/usePermission';
 import ReportAnalysisCountEdit from '@/components/ReportAnalysisCountEdit.vue';
 import PatientEdit from '@/components/PatientEdit.vue';
@@ -17,7 +17,7 @@ import PatientTagEdit from '@/components/PatientTagEdit.vue';
 import PatientHealthRecordPreview from '@/components/PatientHealthRecordPreview.vue';
 import router from '@/router';
 import { editRoleMethod } from '@/request/api/system.api';
-
+const { data: branch, loading: branchLoading } = useRequest(branchMethod);
 const { data: selectable, loading: tagsLoading } = useRequest(searchTagsFromSelectableMethod, { initialData: [] });
 
 const model = shallowRef<PatientQuery>();
@@ -69,7 +69,52 @@ const searchFormProps = reactive<VxeFormProps<PatientQuery>>({
       },
     },
     {
+      field: 'source',
+      title: '来源渠道',
       span: 6,
+      itemRender: {
+        name: 'VxeSelect',
+        props: {
+          loading: tagsLoading,
+          placeholder: '请选择',
+          options: computed(() => sourceOptions.value),
+          optionProps: { value: 'value', label: 'label' },
+          clearable: true,
+          filterable: true,
+        },
+      },
+    },
+    {
+      field: 'sourceInsId',
+      title: '来源机构',
+      span: 6,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: { value: 'id' },
+          clearable: true,
+        },
+      },
+    },
+    {
+      field: 'pcrCount',
+      title: '调养次数',
+      span: 5,
+      itemRender: {
+        name: 'VxeNumberInput',
+        props: {
+          placeholder: '请输入',
+          controls: false,
+          precision: 0,
+          step: 1,
+        },
+      },
+    },
+    {
+      align: 'right',
+      span: 24,
       itemRender: {
         name: 'VxeButtonGroup',
         options: [
@@ -88,6 +133,32 @@ const searchFormEmits: VxeFormListeners<PatientQuery> = {
     model.value = { ...data };
   },
 };
+const sourceOptions = ref<any[]>([
+  // { label: '小程序', value: 'miniProgram' },
+  // { label: 'H5', value: 'h5' },
+  // { label: 'APP', value: 'app' },
+  // { label: '其他', value: 'other' },
+]);
+const sourceOptionsLoading = ref(false);
+// 获取来源渠道
+async function getSourceTypeOptions() {
+  sourceOptionsLoading.value = true;
+  try {
+    const res = await getDictionaryMethod('patient_source_type');
+    console.log(res, 'getSourceTypeOptions');
+    if (res?.length > 0) {
+      sourceOptions.value = res; // 直接使用返回的数据
+      sourceOptionsLoading.value = false;
+    }
+  } catch (error: any) {
+    notification.error({
+      message: error?.message ?? '获取来源渠道失败',
+    });
+    sourceOptionsLoading.value = false;
+  } finally {
+    sourceOptionsLoading.value = false;
+  }
+}
 
 const gridRef = ref<VxeGridInstance<PatientReportModel>>();
 const gridOptions = reactive<VxeGridProps<PatientReportModel>>({
@@ -118,8 +189,11 @@ const gridOptions = reactive<VxeGridProps<PatientReportModel>>({
     { field: 'phone', title: '手机号码', minWidth: 80 },
     { field: 'gender', title: '性别', minWidth: 40, formatter: 'gender' },
     { field: 'age', title: '年龄', minWidth: 40, formatter: ({ cellValue }) => (cellValue ? `${cellValue}岁` : '') },
+    { field: 'pcrCount', title: '调养次数', minWidth: 20 },
     { field: 'diagnosis', title: '诊断', minWidth: 40 },
     { field: 'tags', title: '标签' },
+    { field: 'source', title: '来源渠道', minWidth: 40 },
+    { field: 'sourceInsName', title: '来源机构', minWidth: 40 },
     { field: 'createTime', title: '创建时间' },
     {
       field: 'action',
@@ -174,16 +248,30 @@ onSuccess(({ data: { data } }) => {
   gridRef.value?.loadData(data);
 });
 
+// 获取来源机构
+// const insLoading = ref(false);
+// const insArr = ref<any[]>([]);
+// async function getInstitutionList(orgId: number) {
+//   insLoading.value = true;
+//   const res = await branchMethod(1, 0, orgId);
+//   if (res && res.length > 0) {
+//     insArr.value = res;
+//   }
+//   insLoading.value = false;
+// }
 onMounted(() => {
   model.value = toRaw(searchFormProps.data);
+  // getInstitutionList(1);
+  getSourceTypeOptions();
 });
 
-function openHistoryPreviewHandle({ report, ...patient }: PatientReportModel, index?: number) {
+function openHistoryPreviewHandle({ report, source, sourceInsName, ...patient }: PatientReportModel, index?: number) {
   const id = `drawer:report-history:preview`;
   const onDestroy = () => {
     VxeUI.drawer.close(id);
   };
   onDestroy();
+  console.log(patient, source, sourceInsName, 'patient=======');
   VxeUI.drawer.open({
     id,
     title: `健康档案`,
@@ -196,6 +284,8 @@ function openHistoryPreviewHandle({ report, ...patient }: PatientReportModel, in
         return h(PatientHealthRecordPreview, {
           patient,
           report,
+          source,
+          sourceInsName,
           onDestroy,
           onRefresh() {
             refresh(page.value);
@@ -297,7 +387,7 @@ function openPatientEditHandle() {
   VxeUI.modal.open({
     id,
     title: `新增患者`,
-    maskClosable: true,
+    maskClosable: false,
     escClosable: true,
     slots: {
       default() {

+ 17 - 4
src/request/api/care.api.ts

@@ -45,7 +45,7 @@ export function getAllSystemCpMethod() {
 }
 // 新增和编辑系统项目和新增编辑项目列表。  项目列表就是机构项目
 export function systemCpEditMethod(data: Partial<SystemItemModel>) {
-  if(data.isType === 'itemsList'){
+  if (data.isType === 'itemsList') {
     delete data.id;
   }
   if (data.addType === 'system') {
@@ -404,16 +404,29 @@ export function getConditioningSchemeMethod(page: number, size: number, query?:
   });
 }
 // 更新调理方案配置
-export function updateConditioningSchemeMethod(orgId: string | number, data: any) {
-  return request.Post(`/fdhb-pc/conditioningManage/config/updateCpConfig/${orgId}`, data, {
+export function updateConditioningSchemeMethod(insId: string | number, data: any) {
+  return request.Post(`/fdhb-pc/conditioningManage/config/updateCpConfig/${insId}`, 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}`, {
+  // return request.Post(`/fdhb-pc/conditioningManage/config/getCpConfigDetailById/${data.id || 0}/${data.orgId}`, {
+  return request.Post(`/fdhb-pc/conditioningManage/config/getCpConfigDetailById/${data.insId}`, {
     name: 'get-conditioning-scheme-detail',
     cacheFor: null,
   });
 }
+// 根据机构id获取机构调理方案配置中某种类型下可供选择的项目
+export function getConditioningSchemeItemsMethod(data: Partial<ConditioningSchemeModel>) {
+  return request.Post(
+    `/fdhb-pc/conditioningManage/config/getOptionalCps/${data.insId}`,
+    {},
+    {
+      name: 'get-conditioning-scheme-items',
+      cacheFor: null,
+      params: { conditioningProgramType: data.conditioningProgramType ?? '' },
+    }
+  );
+}

+ 6 - 12
src/request/api/device.api.ts

@@ -1,5 +1,5 @@
 import type { List } from '@/model';
-import type { EquirementModel,DeviceManageModel,DeviceReportModel } from '@/model/device.model';
+import type { EquirementModel, DeviceManageModel, DeviceReportModel } from '@/model/device.model';
 import request from '@/request/alova';
 // 设备登记分页列表
 export function getDeviceRegisterMethod(page: number, size: number, query?: Record<string, any>) {
@@ -33,15 +33,11 @@ export function getDeviceRegisterDetailMethod(data: Partial<EquirementModel>) {
 
 // 批量修改设备登记组织
 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 },
-    }
-  );
+  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 },
+  });
 }
 
 // 一体机配置分页列表
@@ -79,5 +75,3 @@ export function deviceReportMethod(page: number, size: number, query?: Record<st
     params: { pageNum: page, pageSize: size },
   });
 }
-
-

+ 125 - 59
src/service/AddItems.vue

@@ -21,7 +21,7 @@ const typeOptionsLoading = ref<boolean>(false);
 const typeOptions = ref<{ label: string; value: string }[]>([]);
 
 // 统一提升消息提示层级,避免被弹窗遮挡
-message.config({ zIndex: 5000, top: 80, maxCount: 3, duration: 2 });
+message.config({ zIndex: '10000' as any, top: 80, maxCount: 3, duration: 2 });
 
 const supplierOptions = ref<{ label: string; value: string }[]>([]);
 
@@ -108,31 +108,56 @@ const isDerivationModalOpen = ref(false);
 const rules = {
   name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
   conditioningProgramType: [{ required: true, message: '请选择方案类型', trigger: 'change' }],
-  institutionId: [{ 
-    required: true, 
-    message: '请选择机构名称', 
-    trigger: 'blur',
-    validator: (rule: any, value: any, callback: any) => {
-      // 如果推导逻辑弹窗打开,跳过验证
-      if (isDerivationModalOpen.value) {
-        callback();
-        return;
-      }
-      if (!value) {
-        callback(new Error('请选择机构名称'));
-      } else {
-        callback();
-      }
-    }
-  }],
+  institutionId: [
+    {
+      required: true,
+      message: '请选择机构名称',
+      trigger: 'blur',
+      validator: (rule: any, value: any, callback: any) => {
+        // 如果推导逻辑弹窗打开,跳过验证
+        if (isDerivationModalOpen.value) {
+          callback();
+          return;
+        }
+        if (!value) {
+          callback(new Error('请选择机构名称'));
+        } else {
+          callback();
+        }
+      },
+    },
+  ],
   isOffline: [{ required: true, message: '请选择线下项目', trigger: 'change' }],
   isDelivery: [{ required: true, message: '请选择是否配送', trigger: 'change' }],
 };
+const showMiniProgramCode = ref<boolean>(false);
+const showBuyLink = ref<boolean>(false);
+// 判断是否为小程序码类型
+const isMiniProgramCodeType = computed(() => {
+  return form.attrNinth === 'miniprogram';
+});
+watch(
+  () => form.attrNinth,
+  (newVal) => {
+    if (newVal && newVal === 'URL') {
+      // 购买链接显示不必填  小程序码隐藏
+      showMiniProgramCode.value = false;
+      showBuyLink.value = true;
+    } else if (newVal && newVal === 'miniprogram') {
+      // 跳转类型是小程序码 小程序码显示并且必上传  购买链接显示不必填
+      showMiniProgramCode.value = true;
+      showBuyLink.value = true;
+    } else {
+      // 跳转类型是其他类型 小程序码隐藏 购买链接隐藏 目前是这样处理  后期如果有其他类型再做处理
+      showMiniProgramCode.value = false;
+      showBuyLink.value = false;
+    }
+  }
+);
 const isShowOnline = ref<boolean>(false);
 const isShowDelivery = ref<boolean>(false);
 const supplierArr = ref<any[]>([]);
 
-
 // 获取所有的供应商
 async function getSupplier(params: any) {
   supplierOptions.value = [];
@@ -253,7 +278,7 @@ function doSubmit() {
     message.error('请选择方案类型');
     return;
   }
-  
+
   // 自定义验证:检查项目应用是否已选择
   if (!checkedList.value || checkedList.value.length === 0) {
     message.error('请选择项目应用');
@@ -271,7 +296,8 @@ function doSubmit() {
     // 计价规则相关字段校验
     if (form.pricingType === '0') {
       const { unitPrice, pricingUnit, convertDose, convertUnit } = form.cpFixedPricingRule || {};
-      const allFilled = (unitPrice !== '' && unitPrice !== null && unitPrice !== undefined) && pricingUnit && (convertDose !== '' && convertDose !== null && convertDose !== undefined) && convertUnit;
+      const allFilled =
+        unitPrice !== '' && unitPrice !== null && unitPrice !== undefined && pricingUnit && convertDose !== '' && convertDose !== null && convertDose !== undefined && convertUnit;
       if (!allFilled) {
         message.error('请将单价、计价单位、相当于、使用单位全部填写');
         return;
@@ -298,7 +324,7 @@ function doSubmit() {
     //   //   return;
     //   // }
     // } else
-     if (form.addType === 'system') {
+    if (form.addType === 'system') {
       form.isOffline = 'N';
       form.isDelivery = 'N';
     }
@@ -316,7 +342,8 @@ function doSubmit() {
     // 计价规则非必填时,做全填/全空校验
     if (form.pricingType === '0') {
       const { unitPrice, pricingUnit, convertDose, convertUnit } = form.cpFixedPricingRule || {};
-      const allFilled = (unitPrice !== '' && unitPrice !== null && unitPrice !== undefined) && pricingUnit && (convertDose !== '' && convertDose !== null && convertDose !== undefined) && convertUnit;
+      const allFilled =
+        unitPrice !== '' && unitPrice !== null && unitPrice !== undefined && pricingUnit && convertDose !== '' && convertDose !== null && convertDose !== undefined && convertUnit;
       const allEmpty = !unitPrice && !pricingUnit && !convertDose && !convertUnit;
       if (!(allFilled || allEmpty)) {
         message.error('单价、计价单位、相当于、使用单位要么全部填写,要么全部为空');
@@ -351,18 +378,26 @@ function doSubmit() {
     }
   }
 
-  // 购买链接与跳转类型联动校验:有购买链接则必须选择跳转类型
+  // 购买链接与跳转类型联动校验
   if (form.addType === 'itemsList') {
+   
+    // 有购买链接则必须选择跳转类型
     const hasBuyUrl = !!(form.attrEighth && String(form.attrEighth).trim());
     if (hasBuyUrl && !form.attrNinth) {
       message.error('请选择跳转类型');
       return;
     }
+     // 如果选择了小程序码类型,类型是小程序码  小程序码必填 链接可填
+     if (form.attrNinth === 'miniprogram' && miniProgramCodeList.value.length === 0) {
+      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.attrTen = miniProgramCodeList.value[0]?.response?.url || miniProgramCodeList.value[0]?.url || '';
     // 合并推导逻辑数据到表单数据中
     if (hasDerivationLogic.value && derivationData.value.cpPatientMatchRule) {
       form.cpPatientMatchRule = { ...derivationData.value.cpPatientMatchRule };
@@ -392,16 +427,16 @@ const jumpTypeOptions = ref<{ label: string; value: string }[]>([]);
 // 获取跳转类型
 async function getJumpType() {
   jumpTypeOptionsLoading.value = true;
-  try { 
-  const res = await getDictionaryMethod('fdhb_cpbuy_type');
-  if (res?.length > 0) {
-    jumpTypeOptions.value = res;
+  try {
+    const res = await getDictionaryMethod('fdhb_cpbuy_type');
+    if (res?.length > 0) {
+      jumpTypeOptions.value = res;
+    }
+  } catch (error: any) {
+    message.error(error.message);
+  } finally {
+    jumpTypeOptionsLoading.value = false;
   }
-} catch (error: any) {
-  message.error(error.message);
-} finally {
-  jumpTypeOptionsLoading.value = false;
-}
 }
 
 // 处理下拉框点击事件
@@ -412,7 +447,6 @@ function handleSelectClick() {
   }
 }
 
-
 onMounted(async () => {
   const deptId = localStorage.getItem('deptId');
   if (props.data.addType === 'system' && deptId) {
@@ -463,7 +497,17 @@ onMounted(async () => {
           },
         ]
       : [];
-
+    miniProgramCodeList.value = res.attrTen
+      ? [
+          {
+            uid: '-1',
+            name: 'image.png',
+            status: 'done',
+            url: res.attrTen,
+            thumbUrl: res.attrTen,
+          },
+        ]
+      : [];
     // 处理视频数据
     if (res.itemVideoFirst) {
       videoFileList.value = [
@@ -486,7 +530,7 @@ onMounted(async () => {
   // 获取方案类型
   getConditioningProgramType();
   // 获取跳转类型
-  getJumpType();  
+  getJumpType();
 });
 const emits = defineEmits<{
   submit: [data?: SystemItemModel];
@@ -508,6 +552,8 @@ const uploadProps = reactive({ showRemoveIcon: true });
 const fileList = ref<UploadFile[]>([]);
 // 操作图片
 const optionsList = ref<UploadFile[]>([]);
+// 小程序码
+const miniProgramCodeList = ref<UploadFile[]>([]);
 // 安全挂载弹层,避免 document 不可用或被父层遮挡
 function getSafePopupContainer(triggerNode?: HTMLElement) {
   try {
@@ -550,23 +596,27 @@ const videoFileList = ref<UploadFile[]>([]);
 // 供应商校验规则:仅在"项目应用 勾选 服务包"且"新增项目(itemsList)"时必填
 const supplierRules = computed(() => {
   const need = checkedList.value.includes('1') && form.addType === 'itemsList';
-  return need ? [{ 
-    required: true, 
-    message: '请选择供应商', 
-    trigger: 'blur',
-    validator: (rule: any, value: any, callback: any) => {
-      // 如果推导逻辑弹窗打开,跳过验证
-      if (isDerivationModalOpen.value) {
-        callback();
-        return;
-      }
-      if (!value) {
-        callback(new Error('请选择供应商'));
-      } else {
-        callback();
-      }
-    }
-  }] : [];
+  return need
+    ? [
+        {
+          required: true,
+          message: '请选择供应商',
+          trigger: 'blur',
+          validator: (rule: any, value: any, callback: any) => {
+            // 如果推导逻辑弹窗打开,跳过验证
+            if (isDerivationModalOpen.value) {
+              callback();
+              return;
+            }
+            if (!value) {
+              callback(new Error('请选择供应商'));
+            } else {
+              callback();
+            }
+          },
+        },
+      ]
+    : [];
 });
 
 // 上传前校验
@@ -766,7 +816,7 @@ const isDerivationEmpty = computed(() => {
 function handleDerivation() {
   // 设置推导逻辑弹窗打开状态
   isDerivationModalOpen.value = true;
-  
+
   VxeUI.modal.open({
     title: `推导逻辑`,
     height: 750,
@@ -795,7 +845,7 @@ function handleDerivation() {
           onSubmit: (data: any) => {
             derivationData.value = data;
             hasDerivationLogic.value = true; // 设置推导逻辑已编辑
-            
+
             // 重置状态并清除验证状态
             isDerivationModalOpen.value = false;
             nextTick(() => {
@@ -1087,14 +1137,30 @@ function handleDerivation() {
           </div>
         </div>
       </a-form-item>
-      <!-- 购买链接 -->
-      <a-form-item label="购买链接:" v-if="form.addType === 'itemsList'">
-        <a-input v-model:value="form.attrEighth" placeholder="请输入" />
-      </a-form-item>
       <!-- 跳转类型 -->
       <a-form-item label="跳转类型:" v-if="form.addType === 'itemsList'" style="width: 100%">
         <Vxe-select v-model="form.attrNinth" :options="jumpTypeOptions" placeholder="请选择" clearable transfer style="width: 100%" />
       </a-form-item>
+      <!-- 购买链接 -->
+      <a-form-item label="购买链接:" v-if="form.addType === 'itemsList' && showBuyLink">
+        <a-input v-model:value="form.attrEighth" placeholder="请输入" />
+      </a-form-item>
+      <a-form-item label="小程序码:" class="image-form-item" 
+      v-if="form.addType === 'itemsList' && showMiniProgramCode" :required="isMiniProgramCodeType">
+        <a-upload
+          :showUploadList="uploadProps"
+          v-model:file-list="miniProgramCodeList"
+          list-type="picture-card"
+          @preview="handlePreview"
+          :maxCount="1"
+          :customRequest="customUpload"
+        >
+          <div v-if="miniProgramCodeList.length < 1">
+            <PlusOutlined />
+            <div style="margin-top: 8px">上传</div>
+          </div>
+        </a-upload>
+      </a-form-item>
       <!-- 机构名称 -->
       <a-form-item label="机构名称:" v-if="form?.addType === 'itemsList'" required name="institutionId">
         <a-tree-select

+ 8 - 0
src/service/ConfirmItems.vue

@@ -13,6 +13,9 @@ import { UploadIFile } from '@/request/api/follow.api';
 import type { UploadFile } from 'ant-design-vue/es/upload/interface';
 type SystemModel = Partial<SystemItemModel>;
 const props = defineProps<{ data: SystemModel }>();
+(notification.config as any)({
+  zIndex: 10000, // 直接设置层级
+});
 const unitOptions = [
   { label: '袋', value: '袋' },
   { label: '包', value: '包' },
@@ -295,7 +298,10 @@ function handleOk() {
   if (showOffLine.value && !props.data.isOffline) {
     props.data.isOffline = 'N';
   }
+  props.data.photo = fileList.value[0]?.response?.url || fileList.value[0]?.url || '';
   confirmOrgConfirmMethod(props.data as SystemItemModel).then(() => {
+    console.log(props.data, 'props.data');
+    console.log(fileList.value, 'fileList.value111111');
     emit('submit', props.data as SystemItemModel);
     VxeUI.modal.close('confirm-item-modal');
     notification.success({
@@ -309,11 +315,13 @@ function toggleOnlineStatus() {
   props.data.isOffline = isOffline.value ? 'Y' : 'N';
 }
 onMounted(async () => {
+  console.log(props.data, 'props.data11111');
   if (props.data.id) {
     const res = await getConditioningSchemeDetailMethod(props.data);
     Object.assign(props.data, res);
     getIsonline(props.data.conditioningProgramSupplierId);
   }
+  
   fileList.value = props.data?.photo
     ? [
         {

+ 360 - 0
src/service/CustomConfiguration.vue

@@ -0,0 +1,360 @@
+<script setup lang="ts">
+import { ref, onMounted, computed } from 'vue';
+import { VxeUI } from 'vxe-pc-ui';
+import type { ConditioningSchemeModel } from '@/model/care.model';
+import { getConditioningSchemeItemsMethod } from '@/request/api/care.api';
+type ConditioningModel = Partial<ConditioningSchemeModel>;
+const props = defineProps<{ data: ConditioningModel }>();
+
+const emit = defineEmits(['submit', 'onSubmit', 'close']);
+const selectedProjectsValue = ref<any[]>([]);
+// 定义项目接口
+interface ProjectItem {
+  id: string | number;
+  name: string;
+  checked: boolean;
+}
+
+// 定义组织接口
+interface Organization {
+  id: string | number;
+  name: string;
+  projects: ProjectItem[];
+}
+// 上级项目数据
+const superiorProjects = ref<Organization[]>([]);
+
+// 本级项目数据
+const currentProjects = ref<ProjectItem[]>([]);
+
+// ===== 全选相关逻辑 =====
+// 上级项目是否全部选中
+const isAllSuperiorChecked = computed(() => {
+
+  const allProjects = superiorProjects.value.flatMap((org) => org.projects);
+  console.log(allProjects, 'allProjects111111',superiorProjects.value);
+  if (allProjects.length === 0) return false;
+  return allProjects.every((p) => p.checked);
+});
+
+// 上级项目是否部分选中(用于半选中状态)
+const isSomeSuperiorChecked = computed(() => {
+  const allProjects = superiorProjects.value.flatMap((org) => org.projects);
+  const someChecked = allProjects.some((p) => p.checked);
+  return someChecked && !isAllSuperiorChecked.value;
+});
+
+// 切换上级项目全选
+const handleSuperiorCheckAllChange = (checked: boolean) => {
+  superiorProjects.value.forEach((org) => {
+    org.projects.forEach((project) => {
+      project.checked = checked;
+    });
+  });
+};
+
+// 某机构是否全部选中
+const isOrgAllChecked = (org: Organization) => {
+  if (!org.projects || org.projects.length === 0) return false;
+  return org.projects.every((p) => p.checked);
+};
+
+// 某机构是否部分选中
+const isOrgIndeterminate = (org: Organization) => {
+  const someChecked = org.projects?.some((p) => p.checked);
+  return !!someChecked && !isOrgAllChecked(org);
+};
+
+// 切换某机构全选
+const handleOrgCheckAllChange = (org: Organization, checked: boolean) => {
+  org.projects.forEach((project) => {
+    project.checked = checked;
+  });
+};
+
+// 本级项目是否全部选中
+const isAllCurrentChecked = computed(() => {
+  if (currentProjects.value.length === 0) return false;
+  return currentProjects.value.every((p) => p.checked);
+});
+
+// 本级项目是否部分选中
+const isSomeCurrentChecked = computed(() => {
+  const someChecked = currentProjects.value.some((p) => p.checked);
+  return someChecked && !isAllCurrentChecked.value;
+});
+
+// 切换本级项目全选
+const handleCurrentCheckAllChange = (checked: boolean) => {
+  currentProjects.value.forEach((project) => {
+    project.checked = checked;
+  });
+};
+
+// 关闭弹窗(取消时使用)
+function handleClose() {
+  emit('close');
+  VxeUI.modal.close('custom-configuration-modal');
+}
+
+// 取消:仅关闭弹窗并让父组件回滚勾选状态
+function handleCancel() {
+  handleClose();
+}
+// 确认
+function handleConfirm() {
+  // 收集选中的项目数据
+  const selectedSuperiorProjects: any[] = [];
+  const selectedProjects: any[] = [];
+  superiorProjects.value.forEach((org) => {
+    org.projects.forEach((project) => {
+      if (project.checked) {
+        selectedSuperiorProjects.push({
+          projectId: project.id,
+          projectName: project.name,
+        });
+      }
+    });
+  });
+  const selectedCurrentProjects = currentProjects.value
+    .filter((p) => p.checked)
+    .map((p) => ({
+      projectId: p.id,
+      projectName: p.name,
+    }));
+  selectedProjects.push(...selectedCurrentProjects, ...selectedSuperiorProjects);
+
+  (props.data as any).selectedCpIds = selectedProjects.map((item: any) => item.projectId) as string[];
+  const result = {
+    // superiorProjects: selectedSuperiorProjects,
+    currentProjects: selectedCurrentProjects,
+    originalData: props.data,
+    selectedProjects: selectedProjects,
+  };
+  
+  // 必须选择一个项目
+  // if (selectedProjects.length <= 0 && selectedProjects.length <= 0) {
+  //   notification.error({
+  //     message: '请至少选择一个项目',
+  //   });
+  //   return;
+  // }
+  emit('submit', result);
+  emit('onSubmit', result);
+  selectedProjectsValue.value = selectedProjects;
+  // 确认时直接关闭弹窗,父组件通过 onSubmit 已经处理勾选状态
+  VxeUI.modal.close('custom-configuration-modal');
+}
+// 获取定制项目列表
+const getCustomProjectList = async () => {
+  const res = await getConditioningSchemeItemsMethod(props.data);
+  (res as any[])?.forEach((item: any) => {
+    if (item.cps && item.cps.length > 0) {
+      item.cps.forEach((cp: any) => {
+        cp.checked = false;
+      });
+    }
+  });
+  if (props.data.selectedCpIds && props.data.selectedCpIds.length > 0) {
+    props.data.selectedCpIds.forEach((id: string) => {
+      (res as any[])?.forEach((item: any) => {
+        if (item.cps && item.cps.length > 0) {
+          item.cps.forEach((cp: any) => {
+            if (cp.id === id) {
+              cp.checked = true;
+            }
+          });
+        }
+      });
+    });
+  }
+  (res as any[])?.forEach((item: any) => {
+    if (item.insId === props.data.insId) {
+      currentProjects.value = item.cps;
+    } else {
+      superiorProjects.value.push({
+        id: item.insId,
+        name: item.insName,
+        projects: item.cps,
+      });
+    }
+  });
+};
+onMounted(() => {
+  if (props.data && props.data.conditioningProgramType) {
+    getCustomProjectList();
+  }
+});
+</script>
+
+<template>
+  <div class="custom-configuration">
+    <!-- 内容区域:左右分栏 -->
+    <div class="content-wrapper">
+      <!-- 上级项目 - 左侧 -->
+      <div class="section section-left" v-if="superiorProjects.length > 0">
+        <div class="section-title section-title-with-checkbox">
+          <span>上级项目</span>
+          <a-checkbox
+            :checked="isAllSuperiorChecked"
+            :indeterminate="isSomeSuperiorChecked"
+            @change="(e: any) => handleSuperiorCheckAllChange(e.target.checked)"
+            style="margin-left: 20px;"
+          >
+            全选
+          </a-checkbox>
+        </div>
+        <div class="organizations">
+          <div v-for="org in superiorProjects" :key="org.id" class="organization-item">
+            <div class="org-header">
+              <div class="org-name">{{ org.name }}</div>
+              <a-checkbox
+                :checked="isOrgAllChecked(org)"
+                :indeterminate="isOrgIndeterminate(org)"
+                @change="(e: any) => handleOrgCheckAllChange(org, e.target.checked)"
+              >
+                全选
+              </a-checkbox>
+            </div>
+            <div class="projects-list">
+              <a-checkbox v-for="project in org.projects" :key="project.id" v-model:checked="project.checked" class="project-checkbox">
+                {{ project.name }}
+              </a-checkbox>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 本级项目 - 右侧 -->
+      <div class="section section-right" v-if="currentProjects.length > 0">
+        <div class="section-title section-title-with-checkbox">
+          <span>本级项目</span>
+          <a-checkbox
+            :checked="isAllCurrentChecked"
+            :indeterminate="isSomeCurrentChecked"
+            @change="(e: any) => handleCurrentCheckAllChange(e.target.checked)"
+            style="margin-left: 20px;"
+          >
+            全选
+          </a-checkbox>
+        </div>
+        <div class="projects-list">
+          <a-checkbox v-for="project in currentProjects" :key="project.id" v-model:checked="project.checked" class="project-checkbox">
+            {{ project.name }}
+          </a-checkbox>
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部按钮 -->
+    <div class="footer-buttons">
+      <a-button @click="handleCancel">取消</a-button>
+      <a-button type="primary" @click="handleConfirm">确认</a-button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.custom-configuration {
+  position: relative;
+  padding: 20px;
+  min-height: 500px;
+  display: flex;
+  flex-direction: column;
+}
+
+.close-btn {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+  cursor: pointer;
+  color: #999;
+  font-size: 18px;
+  width: 24px;
+  height: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: color 0.3s;
+  z-index: 10;
+
+  &:hover {
+    color: #333;
+  }
+}
+
+.content-wrapper {
+  display: flex;
+  gap: 30px;
+  flex: 1;
+  margin-bottom: 20px;
+}
+
+.section {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.section-left {
+  padding-right: 15px;
+}
+
+.section-right {
+  padding-left: 15px;
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: 800;
+  margin-bottom: 16px;
+  color: #333;
+}
+
+.organizations {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  flex: 1;
+}
+
+.organization-item {
+  .org-name {
+    font-size: 15px;
+    // font-weight: 500;
+    color: black;
+    margin-right: 20px;
+  }
+}
+
+.projects-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+}
+
+.project-checkbox {
+  :deep(.ant-checkbox-wrapper) {
+    font-size: 14px;
+    color: #333;
+  }
+}
+.org-header{
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.footer-buttons {
+  display: flex;
+  justify-content: center;
+  gap: 12px;
+  margin-top: 20px;
+  padding-top: 20px;
+  border-top: 1px solid #f0f0f0;
+
+  .ant-btn {
+    min-width: 80px;
+    height: 32px;
+  }
+}
+</style>

+ 94 - 15
src/service/EditConfigured.vue

@@ -1,13 +1,17 @@
 <script setup lang="ts">
-import { ref, watch } from 'vue';
+import { ref, watch, nextTick, h, onMounted, reactive } 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';
+import { message, notification } from 'ant-design-vue';
+import { MenuUnfoldOutlined } from '@ant-design/icons-vue';
+import CustomConfiguration from '@/service/CustomConfiguration.vue';
 type ConditioningModel = Partial<ConditioningSchemeModel>;
-
+  (notification.config as any)({
+  zIndex: 10000, // 直接设置层级
+});
 // 定义表格行数据类型
 interface TableRowData {
   isCustomize: boolean;
@@ -16,7 +20,11 @@ interface TableRowData {
   knowledgeCpShowType: string;
   showCount: number;
   isChecked: boolean;
+  selectedCpIds?: string[];
+  isShowForInfer: string;
   isNewRow?: boolean; // 用于标识是否是新添加的行
+  previousChecked?: boolean; // 上一次“确认后”的勾选状态
+  backupSelectedCpIds?: string[]; // 打开弹窗前的已选项目快照(用于取消还原)
 }
 const emits = defineEmits<{
   submit: [success: boolean, data?: Array<TableRowData>];
@@ -73,6 +81,7 @@ const addRow = () => {
     knowledgeCpShowType: '1', // 1-展示 2-不展示 3-定制项目无结果时展示
     showCount: globalItemLimit.value,
     isChecked: false,
+    isShowForInfer: 'N',
     isHaveForInfer: null,
     isCustomize: false,
     isNewRow: true, //新行
@@ -110,16 +119,16 @@ const saveData = async () => {
     return;
   }
 
-  if (!props.data.orgId) {
+  if (!props.data.insId) {
     notification.error({
-      message: '未找到组织ID',
+      message: '未找到机构ID',
     });
     return;
   }
 
   // 使用 async/await 确保正确处理成功和失败
   try {
-    await submit(props.data.orgId, formattedData);
+    await submit(props.data.insId, formattedData);
     // 成功时通知父组件
     emits('submit', true, formattedData);
   } catch (error) {
@@ -146,8 +155,7 @@ const rowConfig = reactive<VxeTablePropTypes.RowConfig>({
   drag: true,
 });
 const columnConfig = reactive<VxeTablePropTypes.ColumnConfig>({});
-const rowDragstartEvent: VxeTableEvents.RowDragstart = ({ row }) => {
-};
+const rowDragstartEvent: VxeTableEvents.RowDragstart = ({ row }) => {};
 
 const rowDragendEvent: VxeTableEvents.RowDragend = ({ newRow, oldRow }) => {
   const oldIndex = tableData.value.indexOf(oldRow as TableRowData);
@@ -159,10 +167,15 @@ const rowDragendEvent: VxeTableEvents.RowDragend = ({ newRow, oldRow }) => {
   }
 };
 const organizationName = ref('');
+const insName = ref('');
+const insId = ref<number>(0);
 onMounted(async () => {
+  console.log(props.data, 'props.onMounted');
   getConditioningProgramType();
   const res: any = await getConditioningDeviceDetailMethod(props.data);
   organizationName.value = res.orgName ?? '';
+  insName.value = res.insName ?? '';
+  insId.value = res.insId ?? 0;
   tableData.value = res.items ?? [];
   tableData.value.forEach((row) => {
     // 只有 isHaveForInfer 不是 null 的情况下才是定制项目
@@ -171,13 +184,15 @@ onMounted(async () => {
       row.isCustomize = false;
     } else if (row.isHaveForInfer === 'Y') {
       // 是定制项目且展示
-      row.isChecked = true;
       row.isCustomize = true;
+      // row.showListIcon = true; // 已勾选,显示列表图标
     } else if (row.isHaveForInfer === 'N') {
       // 是定制项目但不展示
-      row.isChecked = false;
       row.isCustomize = true;
+      // row.showListIcon = false; // 未勾选,隐藏列表图标
     }
+    // row.showListIcon =
+    row.isChecked = row.previousChecked = row.selectedCpIds && row.selectedCpIds.length > 0 ? true : false;
   });
 });
 
@@ -362,12 +377,69 @@ const handleItemLimitPaste = (params: any) => {
     return false;
   }
 };
+// 处理复选框变化
+function handleCheckboxChange(row: TableRowData) {
+  // 无论是勾选还是取消,都弹出定制项目弹窗;
+  // 真正的状态变更在弹窗“确认”时生效,“取消”会还原到打开前的项目选择
+  handleCustomProjectTrue(row);
+}
+
+const handleCustomProjectTrue = (row: any) => {
+  // 打开弹窗前先对项目选择做一份快照,供取消时还原
+  row.backupSelectedCpIds = row.selectedCpIds ? [...row.selectedCpIds] : [];
+
+  VxeUI.modal.open({
+    title: '定制项目',
+    width: 1100,
+    height: 750,
+    id: 'custom-configuration-modal',
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(CustomConfiguration, {
+          data: { ...row, insId: insId.value },
+          onSubmit: (data: any) => {
+            tableData.value.forEach((item: any) => {
+              if (item.conditioningProgramType === row.conditioningProgramType) {
+                item.selectedCpIds = data.selectedProjects.map((item: any) => item.projectId) as string[];
+              }
+            });
+            // 根据是否有选中项目决定是否展示
+            const hasSelected = row.selectedCpIds && row.selectedCpIds.length > 0;
+            row.isChecked = !!hasSelected;
+            row.isShowForInfer = hasSelected ? 'Y' : 'N';
+
+            // 确认后更新“上一次确认”的状态和快照
+            row.previousChecked = row.isChecked;
+            row.backupSelectedCpIds = row.selectedCpIds ? [...row.selectedCpIds] : [];
+            VxeUI.modal.close('custom-configuration-modal');
+          },
+          onClose: () => {
+            // 取消时还原到打开弹窗前的项目选择
+            row.selectedCpIds = row.backupSelectedCpIds ? [...row.backupSelectedCpIds] : [];
+
+            // 根据还原后的已选项目恢复展示状态(是否显示仅由是否有选中项目决定)
+            const hasSelected = row.selectedCpIds && row.selectedCpIds.length > 0;
+            row.isChecked = !!hasSelected;
+            row.isShowForInfer = hasSelected ? 'Y' : 'N';
+            VxeUI.modal.close('custom-configuration-modal');
+          },
+        
+        });
+      },
+    },
+  });
+};
 </script>
 
 <template>
   <div class="edit-configured">
     <!-- 组织名称 -->
-    <div class="organization-name">组织名称:{{ organizationName }}</div>
+    <div class="flex">
+      <div class="organization-name mr-15" v-if="organizationName">组织名称:{{ organizationName }}</div>
+      <div class="organization-name" v-if="insName">机构名称:{{ insName }}</div>
+    </div>
     <!-- 表格 -->
     <vxe-table
       border
@@ -406,9 +478,13 @@ const handleItemLimitPaste = (params: any) => {
       <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 v-if="row.isCustomize" class="custom-project-true-container">
+              <a-checkbox v-model:checked="row.isChecked" @change="handleCheckboxChange(row)"> 展示 </a-checkbox>
+              <div v-if="row.isChecked" class="flex items-center custom-project-true" @click="handleCustomProjectTrue(row)">
+                <MenuUnfoldOutlined />
+              </div>
             </div>
+
             <div v-else class="custom-project-false">
               <div class="red-x-icon">×</div>
             </div>
@@ -490,7 +566,7 @@ const handleItemLimitPaste = (params: any) => {
     <!-- 底部操作按钮 -->
     <div class="action-buttons">
       <vxe-button @click="cancelEdit">取消</vxe-button>
-      <vxe-button type="primary" status="warning" @click="saveData">保存</vxe-button>
+      <vxe-button type="primary" status="warning" @click="saveData" :loading="submitting">保存</vxe-button>
     </div>
   </div>
 </template>
@@ -517,7 +593,10 @@ const handleItemLimitPaste = (params: any) => {
   justify-content: center;
   gap: 8px;
 }
-
+.custom-project-true-container {
+  display: flex;
+  align-items: center;
+}
 .custom-project-true {
   display: flex;
   align-items: center;

+ 3 - 0
src/service/EditSystemService.vue

@@ -30,6 +30,9 @@ const fileList = ref<UploadFile[]>([]);
 const uploadProps = reactive({ showRemoveIcon: true });
 const emit = defineEmits<{ submit: [data?: SystemCwModel] }>();
 const deptId = ref<string>('');
+  (notification.config as any)({
+  zIndex: 10000, // 直接设置层级
+});
 // 获取所有的机构
 const branch = ref<any[]>([]);
 const { loading: branchLoading } = useRequest(branchMethod).onSuccess(({ data }) => {

+ 1 - 0
vite.config.ts

@@ -49,6 +49,7 @@ export default defineConfig((configEnv) => {
           VueRouterAutoImports,
         ],
         dts: `${ dts }auto-imports.d.ts`,
+
       }),
       Components({
         resolvers: [