Explorar el Código

优化健康管家下拉点击没反应

张田田 hace 8 meses
padre
commit
54c1f6596d

+ 123 - 10
src/components/EditSupplier.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { nextTick } from 'vue';
 import { VxeUI, type VxeFormProps, type VxeFormListeners } from 'vxe-pc-ui';
 import type { SupplierModel } from '@/model/care.model';
 import { useRequest } from 'alova/client';
@@ -21,7 +22,7 @@ const emits = defineEmits<{
 const model = ref<Record<string, any>>({});
 watchEffect(() => {
   const data = props.data;
-  const collaborateDepts = data?.collaborateDepts?.map((item: any) => ({value: item.deptId, label: item.deptName})) ?? [];
+  const collaborateDepts = data?.collaborateDepts?.map((item: any) => ({ value: String(item.deptId), label: item.deptName })) ?? [];
   model.value = {
     collaborateDepts
   }
@@ -30,17 +31,78 @@ watchEffect(() => {
 const branch = ref<any[]>([]);
 const { loading: branchLoading } = useRequest(branchMethod).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)
-      }
-    }) : [];
+    return Array.isArray(data)
+      ? data.map(item => ({
+          ...item,
+          value: String(item.id),
+          key: String(item.id),
+          label: item.label,
+          title: item.label,
+          children: to(item.children)
+        }))
+      : [];
   }
   branch.value = to(data);
 });
+
+// 安全挂载弹层,避免 document 不可用时报错
+function getSafePopupContainer(triggerNode?: HTMLElement) {
+  try {
+    // @ts-ignore
+    if (typeof document !== 'undefined' && document?.body) return document.body;
+  } catch (e) {}
+  return triggerNode?.parentNode as HTMLElement | undefined;
+}
+
+// 查找节点/子孙/标签
+function findNode(nodes: any[], id: string | number): any | undefined {
+  for (const n of nodes) {
+    if (String(n.value) === String(id)) return n;
+    const x = n.children && findNode(n.children, id);
+    if (x) return x;
+  }
+}
+function collectChildren(node?: any): string[] {
+  const res: string[] = [];
+  if (!node?.children) return res;
+  const dfs = (n: any) => {
+    if (!n?.children) return;
+    for (const c of n.children) {
+      res.push(String(c.value));
+      dfs(c);
+    }
+  };
+  dfs(node);
+  return res;
+}
+function findLabel(nodes: any[], id: string | number): string {
+  const stack = [...nodes];
+  while (stack.length) {
+    const n = stack.pop();
+    if (String(n.value) === String(id)) return n.label;
+    if (n.children) stack.push(...n.children);
+  }
+  return '';
+}
+
+// 收集整棵树的所有节点 key,用于一次性展开整个下拉树
+function collectAllKeys(nodes: any[]): string[] {
+  const keys: string[] = [];
+  const stack = [...nodes];
+  while (stack.length) {
+    const n = stack.pop();
+    if (!n) continue;
+    keys.push(String(n.value));
+    if (n.children) stack.push(...n.children);
+  }
+  return keys;
+}
+const expandedKeys = ref<string[]>([]);
+function onVisibleChange(visible: boolean) {
+  if (visible) {
+    expandedKeys.value = collectAllKeys(branch.value);
+  }
+}
 const { loading, send: load } = useRequest((params) => supplierMethod(1, 10, params), {
   immediate: false,
   initialData: props.data ?? defaultModel,
@@ -164,6 +226,51 @@ const formEmits: VxeFormListeners<FormModel> = {
     });
   },
 };
+
+// 统一用 change 事件集中处理(父选子,取消独立),避免在 select 阶段改值导致弹层关闭
+function onDeptChange(
+  newVal: Array<{ value: string | number; label: string }>,
+  _labels: Array<string>,
+  extra: any
+) {
+  const current = new Set(newVal?.map((n) => String(n.value)) ?? []);
+  const prev = new Set(
+    (model.value.collaborateDepts as Array<{ value: string | number }> | undefined)?.map((n) => String(n.value)) ?? []
+  );
+
+  const final = new Set(current);
+
+  // 优先使用组件提供的 triggerValue 判定是否为新增(选中)以及哪一个父节点被点击
+  const triggerValue = String(extra?.triggerValue ?? '');
+  const isChecked = !!extra?.checked; // true: 勾选, false: 取消
+
+  if (triggerValue && isChecked) {
+    // 勾选:为该节点补齐所有子孙节点
+    const node = findNode(branch.value, triggerValue);
+    if (node) {
+      for (const cid of collectChildren(node)) final.add(cid);
+    }
+  } else if (!isChecked) {
+    // 取消:独立行为,保持 current 即可(不联动移除子)
+    // 无其他处理
+  } else {
+    // 回退策略:没有 extra(某些版本或场景),使用 diff 判定新增的父节点并补齐
+    const added = [...current].filter((v) => !prev.has(v));
+    for (const v of added) {
+      const node = findNode(branch.value, v);
+      if (node) {
+        for (const cid of collectChildren(node)) final.add(cid);
+      }
+    }
+  }
+
+  const finalIds = [...final];
+  const nextList = finalIds.map((id) => ({ value: String(id), label: findLabel(branch.value, id) }));
+  // 延后设置,避免同一事件循环重绘导致“没反应”
+  nextTick(() => {
+    model.value.collaborateDepts = nextList;
+  });
+}
 function cancel() {
   VxeUI.modal.close('supplier-modal');
 }
@@ -191,9 +298,15 @@ onBeforeMount(async () => {
           :tree-data="branch"
           tree-checkable
           allow-clear
+          :labelInValue="true"
           v-model:value="model.collaborateDepts"
+          v-model:treeExpandedKeys="expandedKeys"
+          :getPopupContainer="getSafePopupContainer"
+          :dropdownMatchSelectWidth="false"
+          :dropdownStyle="{ zIndex: 4000 }"
           placeholder="请选择"
-          @select="handleSelect"
+          @change="onDeptChange"
+          @dropdownVisibleChange="onVisibleChange"
         />
       </template>
       <template #active>

+ 2 - 1
src/components/Evaluation.vue

@@ -216,7 +216,8 @@ const DealTextMap: Record<string, string> = {
           <div @click="openSwiperImage(row, column)" style="margin-top: 10px; color: #1890ff">对比</div>
         </div>
         <span v-else-if="row[column.field] == props.data.medicalTime" style="color: red">{{ row[column.field] }}(就诊日) </span>
-        <template v-else>{{ row[column.field] }}</template>
+        <!-- <template v-else>{{ row[column.field] }}</template> -->
+        <template v-else>{{ row[column.field]?.includes('(r)') ? row[column.field].replace('(r)', '') : row[column.field] }}</template>
       </template>
       <!--      已经评估-->
       <template #right v-if="props.data?.evaluateProgress === '1'">

+ 11 - 4
src/components/RecordsIndicatorPreview.vue

@@ -11,7 +11,7 @@ const props = defineProps<{
 }>();
 
 const patientId = computed(() => props.patient?.id);
-const { data: records, loading } = useWatcher(
+const { data: indicatorRecords, loading } = useWatcher(
   () => indicatorUpdateRecordsMethod(patientId.value),
   [ patientId ],
   {
@@ -21,15 +21,22 @@ const { data: records, loading } = useWatcher(
 );
 onMounted(() => {
   if (props.records) {
-    records.value = props.records;
+    indicatorRecords.value = props.records;
   }
 
 });
+watch(() => props.records, (newVal) => {
+  console.log(newVal, 'newVal----watch');
+  if (newVal) {
+    indicatorRecords.value = newVal as any[];
+  }
+  console.log(indicatorRecords.value, 'records.value222----watch');
+}, { immediate: false, deep: true });
 </script>
 <template>
   <div ref="containerRef" class="h-full">
-    <a-space direction="vertical" class="w-full" v-if="records">
-      <ReportIndicatorChartWidget style="height: 400px;" v-for="item in records" :dataset="item" :loading />
+    <a-space direction="vertical" class="w-full" v-if="indicatorRecords.length > 0">
+      <ReportIndicatorChartWidget style="height: 400px;" v-for="(item, idx) in indicatorRecords" :key="idx" :dataset="item" :loading />
     </a-space>
     <div v-else>
       <a-empty description="暂无数据" class="pt-20" />

+ 139 - 43
src/components/SearchableSelect.vue

@@ -9,6 +9,10 @@
     :filter-option="false"
     :not-found-content="notFoundContent"
     :style="{ width: width }"
+    :labelInValue="true"
+    :getPopupContainer="getSafePopupContainer"
+    :dropdownMatchSelectWidth="false"
+    :dropdownStyle="{ zIndex: 4000 }"
     @search="handleSearch"
     @change="handleChange"
     @dropdown-visible-change="handleDropdownVisibleChange"
@@ -22,11 +26,15 @@
       </div>
     </a-select-option>
 
+    <!-- 自定义输入选项放在最前面 -->
+    <a-select-option v-if="showCustomInput && customInputValue" key="custom-input" :value="customInputValue" :label="customInputValue"> + 使用: "{{ customInputValue }}" </a-select-option>
+
     <!-- 有数据时显示选项 -->
     <a-select-option
       v-for="item in allOptions"
       :key="item.id"
       :value="item.name"
+      :label="item.name"
       v-memo="[item.id, item.name]"
     >
       {{ item.name }}
@@ -44,8 +52,6 @@
         <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>
 
@@ -93,7 +99,7 @@ const emit = defineEmits<{
   search: [keyword: string];
 }>();
 
-const selectedValue = ref<string | number>('');
+const selectedValue = ref<string | number | { value: string | number; label: string }>('');
 const searchKeyword = ref('');
 const currentPage = ref(1);
 const allOptions = ref<Option[]>([]);
@@ -150,10 +156,26 @@ const showCustomInput = computed(() => {
 
 const customInputValue = computed(() => searchKeyword.value.trim());
 
+// 安全获取弹窗容器
+function getSafePopupContainer(triggerNode: HTMLElement) {
+  return document?.body || triggerNode?.parentNode || triggerNode;
+}
+
 watch(
   () => props.modelValue,
   (newValue) => {
-    selectedValue.value = newValue || '';
+    if (newValue) {
+      // 查找对应的选项来获取 label
+      const option = allOptions.value.find((item) => item.name === newValue);
+      if (option) {
+        selectedValue.value = { value: newValue, label: option.name };
+      } else {
+        // 如果是自定义值,直接使用
+        selectedValue.value = { value: newValue, label: newValue };
+      }
+    } else {
+      selectedValue.value = '';
+    }
   },
   { immediate: true }
 );
@@ -164,7 +186,15 @@ const debouncedSearch = async (keyword: string) => {
 
   searchTimer = setTimeout(async () => {
     if (!keyword.trim()) {
-      // keyword 为空时,直接请求第一页数据
+      // keyword 为空时,恢复上次保存的数据或加载初始数据
+      if (hasInitialData.value && lastViewedOptions.value.length > 0) {
+        allOptions.value = [...lastViewedOptions.value];
+        currentPage.value = lastViewedPage.value;
+        hasMore.value = lastViewedHasMore.value;
+        return;
+      }
+      
+      // 如果没有保存的数据,加载初始数据
       isLoading.value = true;
       try {
         const result = await cpMedicinesMethod(1, 10, { keyword: '' });
@@ -199,7 +229,12 @@ const debouncedSearch = async (keyword: string) => {
       lastViewedHasMore.value = hasMore.value;
     } catch (error) {
       console.error('搜索失败:', error);
-      allOptions.value = [];
+      // 搜索失败时不清空数据,保持之前的状态
+      if (hasInitialData.value && lastViewedOptions.value.length > 0) {
+        allOptions.value = [...lastViewedOptions.value];
+        currentPage.value = lastViewedPage.value;
+        hasMore.value = lastViewedHasMore.value;
+      }
     } finally {
       isLoading.value = false;
     }
@@ -208,58 +243,118 @@ const debouncedSearch = async (keyword: string) => {
 
 const handleSearch = (value: string) => {
   searchKeyword.value = value;
-  debouncedSearch(value);
+  
+  // 如果搜索框被清空,立即恢复数据
+  if (!value.trim() && hasInitialData.value && lastViewedOptions.value.length > 0) {
+    allOptions.value = [...lastViewedOptions.value];
+    currentPage.value = lastViewedPage.value;
+    hasMore.value = lastViewedHasMore.value;
+    return;
+  }
+  
+  // 使用 nextTick 确保 DOM 更新后再执行搜索
+  nextTick(() => {
+    debouncedSearch(value);
+  });
   emit('search', value);
 };
 
-const handleChange = (value: any) => {
+const handleChange = (value: any, option: any) => {
   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;
-  emit('change', value, selectedOption);
+  // 使用 labelInValue 时,value 是 { value, label } 格式
+  if (typeof value === 'object' && value !== null) {
+    selectedValue.value = value.value;
+    emit('update:modelValue', value.value);
+    emit('change', value.value, option);
+  } else {
+    selectedValue.value = value;
+    emit('update:modelValue', value);
+    const selectedOption = allOptions.value.find((option) => option.name === value) || null;
+    emit('change', value, selectedOption);
+  }
 };
 
 const handleDropdownVisibleChange = (visible: boolean) => {
-
   if (visible) {
-    // 首次展开时,如果没有数据就显示加载状态
-    if (!isInitialized.value && allOptions.value.length === 0) {
-      isLoading.value = true;
-      cpMedicinesMethod(1, 10, { keyword: searchKeyword.value })
-        .then((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;
-          }
-        })
-        .catch((error) => {
-          console.error('首次加载失败:', error);
-          allOptions.value = [];
-        })
-        .finally(() => {
-          isLoading.value = false;
-        });
+    // 展开时,如果没有数据就加载全部数据(不带keyword)
+    if (allOptions.value.length === 0) {
+      // 如果有保存的数据,先恢复
+      if (hasInitialData.value && lastViewedOptions.value.length > 0) {
+        allOptions.value = [...lastViewedOptions.value];
+        currentPage.value = lastViewedPage.value;
+        hasMore.value = lastViewedHasMore.value;
+      } else {
+        // 否则加载全部初始数据(不带keyword)
+        isLoading.value = true;
+        cpMedicinesMethod(1, 10, { keyword: '' })
+          .then((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;
+            }
+          })
+          .catch((error) => {
+            console.error('首次加载失败:', error);
+            allOptions.value = [];
+          })
+          .finally(() => {
+            isLoading.value = false;
+          });
+      }
     }
   } else {
-    // 关闭下拉框时,保存当前状态作为上一次浏览状态
+    // 关闭下拉框时,保存当前状态
     if (allOptions.value.length > 0) {
       hasInitialData.value = true;
       lastViewedOptions.value = [...allOptions.value];
       lastViewedPage.value = currentPage.value;
       lastViewedHasMore.value = hasMore.value;
     }
-    searchKeyword.value = '';
+  }
+};
+
+// 强制刷新下拉框数据的方法
+const forceRefresh = () => {
+  allOptions.value = [];
+  hasInitialData.value = false;
+  lastViewedOptions.value = [];
+  lastViewedPage.value = 1;
+  lastViewedHasMore.value = false;
+  isInitialized.value = false;
+  
+  // 重新加载数据
+  if (allOptions.value.length === 0) {
+    isLoading.value = true;
+    cpMedicinesMethod(1, 10, { keyword: '' })
+      .then((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;
+        }
+      })
+      .catch((error) => {
+        console.error('强制刷新失败:', error);
+        allOptions.value = [];
+      })
+      .finally(() => {
+        isLoading.value = false;
+      });
   }
 };
 
@@ -285,12 +380,12 @@ const loadMore = async () => {
   try {
     const nextPage = currentPage.value + 1;
 
-    const result = await cpMedicinesMethod(nextPage, 10, { keyword: searchKeyword.value });
+    // 加载更多时也不带keyword,加载全部数据
+    const result = await cpMedicinesMethod(nextPage, 10, { keyword: '' });
 
     // 批量更新数据,避免逐条渲染
     batchUpdateOptions(result.data);
 
-
     // 立即更新状态
     currentPage.value = nextPage;
     hasMore.value = allOptions.value.length < result.total;
@@ -325,6 +420,7 @@ defineExpose({
   resetOptions,
   loadMore,
   search: handleSearch,
+  forceRefresh,
 });
 </script>
 

+ 1 - 1
src/libs/v-select-page/RemoteSelect.vue

@@ -85,7 +85,7 @@ function onSelect(items: Record<string, unknown>[]) {
     <SelectPageList 
       ref="el"
       language="zh-chs"
-      :debounce="attrs.debounce ?? 300"
+      :debounce="attrs.debounce ?? 500"
       :multiple="attrs.multiple"
       :key-prop="props.keyProp"
       :label-prop="props.labelProp"

+ 2 - 1
src/model/health-report.model.ts

@@ -178,7 +178,7 @@ export function fromHealthReportAnalysis(data: Partial<HealthReportDTO>, config
     const { standardValue, actualList } = data[key]!;
     const values: HealthReportAnalysisItemVO['values'] = [];
     for (const { actualValue, splitImage: cover, ...item } of actualList) {
-      const contrast = item.contrast === 's' ? '' : (item.contrast ?? '');
+      const contrast = item.contrast === 's'  ? '' : (item.contrast  ?? '');
       const [_, features = _, significance = item.clinicalSignificance] = item.features?.match?.(
         new RegExp(`【(?:${actualValue}|正常${subcategory}|正常面色)】([^<]*)<?[\\s\\S]*?【病理意义】([^<]*)`)
       ) ?? item.features?.match?.(
@@ -187,6 +187,7 @@ export function fromHealthReportAnalysis(data: Partial<HealthReportDTO>, config
       values.push({
         cover: contrast ? cover : void 0,
         actualValue,
+        // resultValue: contrast && contrast !=='r' ? `${actualValue} (${contrast})` : actualValue,
         resultValue: contrast ? `${actualValue} (${contrast})` : actualValue,
         contrast,
         feature: features ?? '',

+ 0 - 3
src/pages/index.vue

@@ -112,9 +112,6 @@ function updateUserPassword(model: UserModel) {
     },
   });
 }
-onMounted(() => {
-  console.log(menus.value, '菜单权限');
-});
 </script>
 <template>
   <div class="page-container flex flex-col h-vh w-vw">

+ 26 - 9
src/pages/index/care/issueService.vue

@@ -549,8 +549,10 @@ function calculateCount(row: any) {
         }
       } else if (row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType === 1) {
         // 一口价
-        row.unitPrice = '-';
-        row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[1].price;
+        // row.unitPrice = '-';
+        let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[1].price;
+        row.unitPrice = unitPrice;
+        row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * row.totalMeasure;
       }
     } else {
       if (row.conditioningProgramDetail.cpDynamicPricingRule?.length > 0) {
@@ -566,8 +568,10 @@ function calculateCount(row: any) {
           }
         } else if (row.conditioningProgramDetail.cpDynamicPricingRule?.[0]?.priceType === 1) {
           // 一口价
-          row.unitPrice = '-';
-          row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[0].price;
+          // row.unitPrice = '-';
+          let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[0].price;
+          row.unitPrice = unitPrice;
+          row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * row.totalMeasure;
         }
       }
     }
@@ -847,7 +851,14 @@ function openPatientTagEdit(event: MouseEvent) {
     },
   });
 }
-
+const projectSearchRef = useTemplateRef<HTMLInputElement>('projectSearchRef');
+const projectSearchFocus = (visible: boolean) => {
+  showProjectPopover.value = visible;
+  if (visible) setTimeout(() => projectSearchRef.value?.focus?.(), 300);
+};
+watch(showProjectPopover, (v) => {
+  if (v) nextTick(() => projectSearchRef.value?.focus?.());
+});
 function openHistoryPreviewHandle() {
   const data = currentPatient.value;
   const patient = { id: data?.patientId };
@@ -988,9 +999,15 @@ function openPatientHealthRecord(row: { id: string }, showType: 'analysis' | 'sc
             <vxe-column field="conditioningProgramDetail.name" title="项目名称" width="180">
               <template #default="{ row, rowIndex }">
                 <template v-if="rowIndex === displayTableData.length - 1">
-                  <a-popover v-model:open="showProjectPopover" trigger="click" placement="bottomLeft" :overlayStyle="{ width: '350px', padding: 0 }">
+                  <a-popover
+                    v-model:open="showProjectPopover"
+                    trigger="click"
+                    placement="bottomLeft"
+                    :overlayStyle="{ width: '350px', padding: 0 }"
+                    @openChange="projectSearchFocus"
+                  >
                     <template #content>
-                      <a-input v-model:value="projectSearch" placeholder="输入项目名称搜索" style="margin: 8px; width: 90%" @input="() => nextTick()" />
+                      <a-input v-model:value="projectSearch" placeholder="输入项目名称搜索" ref="projectSearchRef" style="margin: 8px; width: 90%" @input="() => nextTick()" />
                       <vxe-table :data="filteredProjects" border size="small" style="max-height: 240px; overflow-y: auto" @cell-click="onSelectProject">
                         <vxe-column field="name" title="项目名称" />
                         <vxe-column field="conditioningProgramType" title="方案类型" />
@@ -1002,7 +1019,7 @@ function openPatientHealthRecord(row: { id: string }, showType: 'analysis' | 'sc
                         </vxe-column>
                       </vxe-table>
                     </template>
-                    <a-input v-model:value="row.name" placeholder="请搜索" style="width: 120px" @click="showProjectPopover = true" />
+                    <a-input v-model:value="row.name" placeholder="请搜索" style="width: 120px" readonly />
                   </a-popover>
                 </template>
                 <template v-else>
@@ -1190,7 +1207,7 @@ function openPatientHealthRecord(row: { id: string }, showType: 'analysis' | 'sc
     </div>
     <!-- 右侧调养记录 -->
     <div class="right-panel flex flex-col overflow-hidden">
-      <section style="flex: 0 0 auto; max-height: 270px; overflow-y: auto">
+      <section style="flex: 0 0 auto; max-height: 270px; overflow-y: auto" v-if="filteredPatients.length > 0">
         <div style="margin-top: -6px; padding-right: 8px">
           <label>标签:</label>
           <a-tag v-for="tag in patientTags" :key="tag.id" :color="tag.color">{{ tag.name }}</a-tag>

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

@@ -45,7 +45,10 @@ export function getAllSystemCpMethod() {
 }
 // 新增和编辑系统项目和新增编辑项目列表。  项目列表就是机构项目
 export function systemCpEditMethod(data: Partial<SystemItemModel>) {
-  console.log(data, 'data==>', data.addType);
+  console.log(data, 'data==>', data.addType,data.id,data.isType);
+  if(data.isType === 'itemsList'){
+    delete data.id;
+  }
   if (data.addType === 'system') {
     return data?.id
       ? request.Post(`/fdhb-pc/conditioningManage/program/updateSystemCp`, { ...data, id: data.id }, { name: 'edit-system-cp' })

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

@@ -189,7 +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==>');
+  // console.log(page, size, query, 'query==>');
   return request.Post(
     `/fdhb-pc/common/pageMedicine`,
     { page, limit: size, keyWord: query?.keyword, ...query },

+ 67 - 41
src/service/AddItems.vue

@@ -16,7 +16,7 @@ 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 props = defineProps<{ data: SystemModel; institutionId: string | number }>();
 const formRef = ref<FormInstance>();
 const typeOptionsLoading = ref<boolean>(false);
 const typeOptions = ref<{ label: string; value: string }[]>([]);
@@ -32,13 +32,6 @@ const unitOptions = [
   { 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[]>([]);
@@ -63,8 +56,8 @@ const form = reactive<SystemItemModel>({
   conditioningProgramSupplierId: '',
   institutionId: '',
   cpDynamicPricingRule: [
-    { min: 0, max: 0, priceType: 0, price: 0 },
-    { min: 0, max: 0, priceType: 0, price: 0 },
+    { min: '', max: '', priceType: '', price: '' },
+    { min: '', max: '', priceType: '', price: '' },
   ],
   pricingType: '0',
   cpFixedPricingRule: {
@@ -114,6 +107,9 @@ const isShowOnline = ref<boolean>(false);
 const isShowDelivery = ref<boolean>(false);
 const supplierArr = ref<any[]>([]);
 
+// 弹层容器:避免模板中直接引用 document 导致类型检查报错
+const getBodyContainer = () => document.body as HTMLElement;
+
 // 获取所有的供应商
 async function getSupplier(params: any) {
   supplierOptions.value = [];
@@ -138,7 +134,7 @@ function getisOffline(e: any, newOffline: any, newDelivery: any, newType: any) {
         } else {
           if (items.offlineCPTypes?.includes(newType[0])) {
             form.isOffline = 'Y';
-            
+
             return false;
           } else if (items.onlineCPTypes?.includes(newType[0])) {
             form.isOffline = 'N';
@@ -202,7 +198,6 @@ function getisOffline(e: any, newOffline: any, newDelivery: any, newType: any) {
 watch(
   [() => form.conditioningProgramType, () => form.institutionId, () => form.conditioningProgramSupplierId, () => form.isOffline, () => form.isDelivery],
   ([newType, newInstitutionId, newSupplierId, newOffline, newDelivery]) => {
-
     getSupplier({
       conditioningProgramTypes: newType ? [newType] : form.conditioningProgramType ? [form.conditioningProgramType] : [],
       collaborateDeptId: newInstitutionId ? newInstitutionId : form.institutionId ? form.institutionId : '',
@@ -360,10 +355,8 @@ async function getConditioningProgramType() {
 
 // 处理下拉框点击事件
 function handleSelectClick() {
-  console.log('下拉框被点击');
   // 如果选项为空且不在加载中,尝试重新获取数据
   if (typeOptions.value.length === 0 && !typeOptionsLoading.value) {
-    console.log('下拉框被点击,但选项为空,尝试重新获取数据');
     getConditioningProgramType();
   }
 }
@@ -440,7 +433,9 @@ onMounted(async () => {
     }
     if (props.data.isType === 'itemsList' && props.data.sourceId) {
       form.sourceId = form.id;
-      form.institutionId = '';
+      // form.institutionId = '';
+      form.institutionId = props.data.institutionId;
+      form.institutionName = props.data.institutionName;
     }
   }
   // 获取方案类型
@@ -466,6 +461,19 @@ const uploadProps = reactive({ showRemoveIcon: true });
 const fileList = ref<UploadFile[]>([]);
 // 操作图片
 const optionsList = ref<UploadFile[]>([]);
+// 安全挂载弹层,避免 document 不可用或被父层遮挡
+function getSafePopupContainer(triggerNode?: HTMLElement) {
+  try {
+    // @ts-ignore
+    if (typeof document !== 'undefined' && document?.body) return document.body;
+  } catch (e) {}
+  return (triggerNode?.parentNode as HTMLElement) || undefined;
+}
+
+// 统一用 change 处理(避免 @select 改值导致面板被打断)
+function onInstitutionChange(val: string | number | undefined) {
+  form.institutionId = val as any;
+}
 // 预览图片
 const handlePreview = async (file: UploadFile) => {
   previewImg.value = file.response?.url ?? file.thumbUrl;
@@ -492,6 +500,12 @@ const uploading = ref(false);
 const progress = ref(0);
 const videoFileList = ref<UploadFile[]>([]);
 
+// 供应商校验规则:仅在“项目应用 勾选 服务包”且“新增项目(itemsList)”时必填
+const supplierRules = computed(() => {
+  const need = checkedList.value.includes('1') && form.addType === 'itemsList';
+  return need ? [{ required: true, message: '请选择供应商', trigger: ['change', 'blur'] }] : [];
+});
+
 // 上传前校验
 function beforeVideoUpload(file: File) {
   const isValidType = accept.split(',').includes(file.type);
@@ -631,9 +645,9 @@ watch(
   (val) => {
     if (val === '0' && !form.cpFixedPricingRule) {
       form.cpFixedPricingRule = {
-        unitPrice: 0,
+        unitPrice: '',
         pricingUnit: '',
-        convertDose: 0,
+        convertDose: '',
         convertUnit: '',
       };
     } else {
@@ -642,7 +656,6 @@ watch(
   }
 );
 function bindchange(e: any) {
-  console.log(e, 'e==>');
   form.conditioningProgramSupplierId = '';
   form.isOffline = null;
 }
@@ -727,18 +740,8 @@ function handleDerivation() {
       <a-form-item label="项目名称:" name="name" required>
         <a-input v-model:value="form.name" placeholder="请输入" />
       </a-form-item>
-      <a-form-item label="方案类型:" name="conditioningProgramType" required>
-        <a-select
-          v-model:value="form.conditioningProgramType"
-          :options="typeOptions"
-          placeholder="请选择"
-          allowClear
-          showSearch
-          :filter-option="filterOption"
-          :loading="typeOptionsLoading"
-          @change="bindchange"
-          @click="handleSelectClick"
-        />
+      <a-form-item label="方案类型:" name="conditioningProgramType" required style="width: 100%">
+        <vxe-select v-model="form.conditioningProgramType" :options="typeOptions" placeholder="请选择" clearable filterable transfer @change="bindchange" style="width: 100%" />
       </a-form-item>
       <a-form-item label="项目应用:" required v-if="form.addType === 'itemsList'">
         <a-checkbox-group v-model:value="checkedList">
@@ -763,7 +766,14 @@ function handleDerivation() {
 
         <span style="margin-left: 32px">相当于</span>
         <a-input v-model:value="form.cpFixedPricingRule.convertDose" placeholder="请输入" style="width: 100px; margin-left: 8px" />
-        <a-select v-model:value="form.cpFixedPricingRule.convertUnit" style="width: 100px; margin-left: 8px" :options="unitOptions" placeholder="请选择" />
+        <vxe-select
+          v-model="form.cpFixedPricingRule.convertUnit"
+          :options="unitOptions"
+          placeholder="请选择"
+          clearable
+          transfer
+          style="width: 100px; margin-left: 8px"
+        />
 
         <span style="color: #aaa; margin-left: 8px">(使用单位)</span>
       </div>
@@ -779,14 +789,16 @@ function handleDerivation() {
               @change="() => (form.cpDynamicPricingRule[1].max = form.cpDynamicPricingRule[0].min)"
             />个时,</span
           >
-          <a-select
-            v-model:value="form.cpDynamicPricingRule[0].priceType"
+          <vxe-select
+            v-model="form.cpDynamicPricingRule[0].priceType"
             :options="[
               { label: '单价', value: 0, priceType: 0 },
               { label: '一口价', value: 1, priceType: 1 },
             ]"
-            style="width: 100px; margin: 0 4px"
             placeholder="请选择"
+            clearable
+            transfer
+            style="width: 100px; margin: 0 4px"
           />
           <span>=</span>
           <a-input v-model:value="form.cpDynamicPricingRule[0].price" style="width: 80px; margin: 0 4px" placeholder="请输入" />
@@ -797,14 +809,16 @@ function handleDerivation() {
           <span class="flex items-center"
             >当"穴位/经络/部位" &gt; <a-input placeholder="请输入" class="w-20 ml-2 mr-2" v-model:value="form.cpDynamicPricingRule[1].max" disabled />个时,</span
           >
-          <a-select
-            v-model:value="form.cpDynamicPricingRule[1].priceType"
+          <vxe-select
+            v-model="form.cpDynamicPricingRule[1].priceType"
             :options="[
               { label: '单价', value: 0, priceType: 0 },
               { label: '一口价', value: 1, priceType: 1 },
             ]"
-            style="width: 100px; margin: 0 4px"
             placeholder="请选择"
+            clearable
+            transfer
+            style="width: 100px; margin: 0 4px"
           />
           <span>=</span>
           <a-input v-model:value="form.cpDynamicPricingRule[1].price" style="width: 80px; margin: 0 4px" placeholder="请输入" />
@@ -991,19 +1005,31 @@ function handleDerivation() {
       <!-- 机构名称 -->
       <a-form-item label="机构名称:" v-if="form?.addType === 'itemsList'" required name="institutionId">
         <a-tree-select
+          :disabled="form.addType === 'itemsList' && !!form.sourceId"
           v-model:value="form.institutionId"
           style="width: 100%"
-          :dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
+          :dropdownStyle="{ maxHeight: '400px', overflow: 'auto', zIndex: 4000 }"
+          :dropdownMatchSelectWidth="false"
+          :getPopupContainer="getSafePopupContainer"
           placeholder="请选择"
           allow-clear
           tree-default-expand-all
           :tree-data="branch"
           :loading="branchLoading"
-          @select="handleSelect"
+          @change="onInstitutionChange"
         ></a-tree-select>
       </a-form-item>
-      <a-form-item label="供应商:" name="conditioningProgramSupplierId" :required="checkedList.includes('1') && form.addType === 'itemsList'">
-        <a-select v-model:value="form.conditioningProgramSupplierId" :options="supplierOptions" placeholder="请选择" allowClear @change="getConditioningProgramSupplier" />
+      <a-form-item label="供应商:" name="conditioningProgramSupplierId" :rules="supplierRules" style="width: 100%">
+        <vxe-select
+          v-model="form.conditioningProgramSupplierId"
+          :options="supplierOptions"
+          placeholder="请选择"
+          clearable
+          filterable
+          transfer
+          @change="getConditioningProgramSupplier"
+          style="width: 100%"
+        />
       </a-form-item>
       <a-form-item label="线下项目:" name="isOffline" v-if="isShowOnline" :required="checkedList.includes('1')">
         <a-checkbox-group v-model:value="onlineArr" @change="onlineChange">

+ 35 - 20
src/service/CareProgress.vue

@@ -35,9 +35,13 @@ const { data: patient } = useWatcher(() => patientMethod(props.data.patientId!),
 });
 
 const careProcessList = ref<OpenConditioningSchemeModel>();
+const showCareBox = ref<boolean>(false);
 async function getCareProgress() {
-  const res = await getConditioningProcessMethod(Number(props.data.id));
+  const res:any = await getConditioningProcessMethod(Number(props.data.id));
   careProcessList.value = res as OpenConditioningSchemeModel;
+  if(res.provinceName || res.cityName || res.areaName || res.detailAddress || res.phone){
+    showCareBox.value = true;
+  }
 }
 const isShowDelivery = ref<boolean>(false);
 // 监听 displayTableData 的变化
@@ -60,6 +64,7 @@ function openIndicatorRecordsPreview() {
     VxeUI.modal.close(id);
   };
   onDestroy();
+  console.log(patient.value, 'patient.value');
   VxeUI.modal.open({
     id,
     remember: true,
@@ -174,6 +179,7 @@ const option = ref({
 watch(
   () => careProcessList.value?.patientConditioningScores,
   (newScores) => {
+    console.log(newScores, 'newScores');
     if (newScores && newScores.length > 0) {
       option.value.xAxis.data = newScores.map((item) => item.time4);
       option.value.series[0].data = newScores.map((item) => item.score);
@@ -260,8 +266,8 @@ const progressTextMap: Record<string, string> = {
         >
       </div>
     </div>
-    <div v-if="isShowDelivery" class="delivery-info" style="margin-left: 75px">
-      <a-checkbox checked style="color: #52c41a; margin-right: 8px" />
+      <div v-if="isShowDelivery && showCareBox" class="delivery-info" style="margin-left: 75px">
+        <a-checkbox checked style="color: #52c41a; margin-right: 8px" />
       <span style="margin-right: 15px">配送</span>
       <span v-if="careProcessList?.provinceName || careProcessList?.cityName || careProcessList?.areaName || careProcessList?.detailAddress"
         >地址:{{ careProcessList?.provinceName }}{{ careProcessList?.cityName }}{{ careProcessList?.areaName }}{{ careProcessList?.detailAddress }}</span
@@ -272,7 +278,7 @@ const progressTextMap: Record<string, string> = {
     <div v-for="item in careProcessList?.items" :key="item.id" class="project-card">
       <div class="project-section" v-if="item?.patientConditioningOfflines">
         <div class="project-title">
-          <span style="font-size: 14px; font-weight: bold;color: black;">◇ {{ item?.conditioningProgramDetail?.name }}</span>
+          <span style="font-size: 14px; font-weight: bold; color: black">◇ {{ item?.conditioningProgramDetail?.name }}</span>
           <span class="stat">数量:{{ item.totalMeasure }}{{ item?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit || '次' }}</span>
           <span class="stat">还剩:{{ item?.remainCount }}{{ item?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit || '次' }}</span>
           <span class="stat">已核销:{{ item?.finishCount }}{{ item?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit || '次' }}</span>
@@ -301,7 +307,7 @@ const progressTextMap: Record<string, string> = {
       <!-- 健康评估 -->
       <div class="project-section mb-3 project-card" v-if="item?.healthAnalysisReports">
         <div class="project-title">
-          <span style="font-size: 14px; font-weight: bold;color: black;">◇ {{ item?.conditioningProgramDetail?.name }}</span>
+          <span style="font-size: 14px; font-weight: bold; color: black">◇ {{ item?.conditioningProgramDetail?.name }}</span>
           <span class="stat">数量:{{ item?.totalMeasure }} {{ item?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit || '次' }}</span>
           <span class="stat">还剩:{{ item?.remainCount }} {{ item?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit || '次' }}</span>
           <span class="stat">已核销:{{ item?.finishCount }} {{ item?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit || '次' }}</span>
@@ -324,34 +330,44 @@ const progressTextMap: Record<string, string> = {
           <div v-if="item?.remark">操作指南:{{ item?.remark }}</div>
         </div>
       </div>
-
+    </div>
+    <!-- 调养效果 -->
+    <div class="care-box" v-if="careProcessList?.patientConditioningScores && careProcessList.patientConditioningScores.length > 0">
+      <h3 style="color: black">调养效果</h3>
+      <div class="care-box-content">
+      <!-- 评分 折线图 -->
+      <div class="chart-wrapper">
+        <v-chart :option="option" style="width: 350px; height: 200px" />
+      </div>
       <!-- 健康记录 -->
       <div class="health-records-card">
-        <a-tabs class="panel-wrapper" v-model:activeKey="activePanel" v-if="item?.healthAnalysisReports && item?.healthAnalysisReports.length > 0">
+        <a-tabs class="panel-wrapper" v-model:activeKey="activePanel" v-if="careProcessList?.healthAnalysisReports && careProcessList?.healthAnalysisReports.length > 0">
           <a-tab-pane v-for="panel in panels" :key="panel.id" class="panel-pane">
             <div class="panel-title">记录</div>
-            <component :is="panel.component" :patient="patient" :healthAnalysisReports="item?.healthAnalysisReports" :type="type"></component>
+            <component :is="panel.component" :patient="patient" :healthAnalysisReports="careProcessList?.healthAnalysisReports" :type="type"></component>
           </a-tab-pane>
         </a-tabs>
       </div>
-    </div>
-    <!-- 调养效果 -->
-    <div style="margin: 20px 0 10px 0" v-if="careProcessList?.patientConditioningScores && careProcessList.patientConditioningScores.length > 0">
-      <h3 style="color: black;">调养效果</h3>
-      <!-- todo 折线图 -->
-      <div class="chart-wrapper">
-        <v-chart :option="option" style="width: 350px; height: 200px" />
-      </div>
-    </div>
+  
     <!-- 指标 -->
-    <div v-if="careProcessList?.patientQuotaGroups && careProcessList?.patientQuotaGroups.length > 0">
+    <div v-if="careProcessList?.patientQuotaGroups && careProcessList?.patientQuotaGroups.length > 0" class="mt-3">
       <b>指标</b>
-      <a-button type="link" @click="openIndicatorRecordsPreview">更新记录</a-button>
+      <a-button type="link" @click="openIndicatorRecordsPreview">查看指标记录</a-button>
     </div>
   </div>
+</div>
+  </div>
 </template>
 
 <style scoped>
+.care-box {
+  margin: 20px 0 10px 0;
+ 
+}
+.care-box-content {
+  border: 1px solid lightgray !important;
+  padding: 10px;
+}
 .panel-pane {
   height: 600px !important;
   margin-bottom: 28px;
@@ -467,6 +483,5 @@ const progressTextMap: Record<string, string> = {
 }
 .chart-wrapper {
   width: 100%;
-  border: 1px solid lightgray !important;
 }
 </style>

+ 17 - 9
src/service/ConfirmItems.vue

@@ -327,9 +327,15 @@ onMounted(async () => {
     : [];
   isOffline.value = props.data.isOffline === 'Y';
 });
-function handleSelect(value: string, node: any, extra: any) {
+// 安全获取弹窗容器
+function getSafePopupContainer(triggerNode: HTMLElement) {
+  return document?.body || triggerNode?.parentNode || triggerNode;
+}
+
+// 处理机构选择变化
+function handleInstitutionChange(value: string | number, node: any) {
   props.data.institutionId = value;
-  props.data.institutionName = node.label;
+  props.data.institutionName = node?.label || node?.title || '';
 }
 </script>
 
@@ -355,7 +361,7 @@ function handleSelect(value: string, node: any, extra: any) {
       <a-input v-model:value="data.cpFixedPricingRule!.pricingUnit" placeholder="请输入" style="width: 100px; margin-left: 8px" />
       <span style="margin-left: 20px">相当于</span>
       <a-input v-model:value="data.cpFixedPricingRule!.convertDose" placeholder="请输入" style="width: 100px; margin-left: 8px" />
-      <a-select v-model:value="data.cpFixedPricingRule!.convertUnit" style="width: 60px; margin-left: 8px" :options="unitOptions" placeholder="请选择" />
+      <vxe-select v-model="data.cpFixedPricingRule!.convertUnit" style="width: 100px; margin-left: 8px" :options="unitOptions" placeholder="请选择" clearable transfer />
       <span style="color: #aaa; margin-left: 10px">(使用单位)</span>
     </div>
     <div v-if="data.pricingType === '1'" class="per-rule">
@@ -417,17 +423,19 @@ function handleSelect(value: string, node: any, extra: any) {
       <a-tree-select
         v-model:value="data.institutionId"
         style="width: 400px"
-        :dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
+        :dropdown-style="{ maxHeight: '400px', overflow: 'auto', zIndex: 4000 }"
         placeholder="请选择"
         allow-clear
         tree-default-expand-all
         :tree-data="branch"
-        @select="handleSelect"
+        :getPopupContainer="getSafePopupContainer"
+        :dropdownMatchSelectWidth="false"
+        @change="handleInstitutionChange"
       ></a-tree-select>
     </div>
     <div class="form-row">
       <label><span class="required-star">*</span>供应商:</label>
-      <a-select v-model:value="data.conditioningProgramSupplierId" :options="supplierOptions" placeholder="请选择供应商" style="width: 400px" allow-clear class="mr-10" />
+      <vxe-select v-model="data.conditioningProgramSupplierId" :options="supplierOptions" placeholder="请选择供应商" style="width: 400px" clearable class="mr-10" />
       <a-checkbox v-model:checked="isOffline" style="margin-right: 8px" @change="toggleOnlineStatus" v-show="showOffLine"> 线下项目 </a-checkbox>
     </div>
     <div class="form-row">
@@ -536,8 +544,8 @@ html,
     font-size: 14px;
   }
   .herb-dosage {
-    width: 50px;
-    padding: 2px 6px;
+    width: 100px;
+    // padding: 2px 6px;
     font-size: 14px;
   }
   .herb-remove {
@@ -562,7 +570,7 @@ html,
   display: flex;
   justify-content: center;
   gap: 16px;
-  margin-top: 32px; 
+  margin-top: 32px;
 }
 .primary {
   background: #1890ff;

+ 35 - 33
src/service/Derivation.vue

@@ -29,7 +29,7 @@ function cancel() {
 }
 // 确定
 function confirm() {
-  if(props.data?.checkedList.includes('2')){
+  if (props.data?.checkedList.includes('2')) {
     const matchRule = formData.cpPatientMatchRule as {
       diagnoseDiseaseNames?: any[];
       willillStateNames?: any[];
@@ -51,40 +51,40 @@ function confirm() {
   emit('submit', formData);
 }
 // 欲病状态
-const desiredConditions = ref<{ id: string; name: string }[]>([]);
-const constitutionGroups = ref<{ id: string; name: string }[]>([]);
-const tabooCrowds = ref<{ id: string; name: string }[]>([]);
+const desiredConditions = ref<{ label: string; value: string }[]>([]);
+const constitutionGroups = ref<{ label: string; value: string }[]>([]);
+const tabooCrowds = ref<{ label: string; value: 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,
+      label: item.label,
+      value: item.value,
     }));
   }
 }
 // 获取性别
-const genders = ref<{ id: string; name: string }[]>([]);
+const genders = ref<{ label: string; value: 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,
+      label: item.label,
+      value: item.label,
     }));
   }
   gendersLoading.value = false;
 }
 // 获取年龄
-const ages = ref<{ id: string; name: string }[]>([]);
+const ages = ref<{ label: string; value: 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,
+      label: item.label,
+      value: item.label,
     }));
   }
 }
@@ -93,8 +93,8 @@ 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,
+      label: item.label,
+      value: item.value,
     }));
   }
 }
@@ -103,8 +103,8 @@ 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,
+      label: item.label,
+      value: item.value,
     }));
   }
 }
@@ -169,17 +169,27 @@ let multiple = ref<boolean>(true);
           <!-- 欲病状态 -->
           <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>
+            <vxe-select
+              v-model="formData.cpPatientMatchRule.willillStateNames"
+              placeholder="请选择"
+              style="width: 100%"
+              clearable
+              multiple
+              :options="desiredConditions"
+            ></vxe-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>
+            <vxe-select
+              v-model="formData.cpPatientMatchRule.constitutionGroupNames"
+              style="width: 100%"
+              placeholder="请选择"
+              clearable
+              multiple
+              :options="constitutionGroups"
+            ></vxe-select>
           </div>
 
           <!-- 证型 -->
@@ -197,19 +207,13 @@ let multiple = ref<boolean>(true);
           <!-- 性别限制 -->
           <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>
+            <vxe-select v-model="formData.cpPatientMatchRule.sex" placeholder="请选择" style="width: 100%" clearable :options="genders"></vxe-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>
+            <vxe-select v-model="formData.cpPatientMatchRule.age" placeholder="请选择" style="width: 100%" clearable :options="ages"></vxe-select>
           </div>
         </div>
       </div>
@@ -220,9 +224,7 @@ let multiple = ref<boolean>(true);
         <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>
+            <vxe-select v-model="formData.cpPatientMatchRule.tabooCrowds" style="width: 100%" placeholder="请选择" clearable multiple :options="tabooCrowds"></vxe-select>
           </div>
         </div>
       </div>

+ 85 - 20
src/service/EditSystemService.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, computed, nextTick, h, watch, onMounted } from 'vue';
+import { ref, computed, nextTick, h, watch, onMounted, useTemplateRef } from 'vue';
 import { notification } from 'ant-design-vue';
 import { getDictionaryMethod } from '@/request/api/dictionary.api';
 import { UploadIFile } from '@/request/api/follow.api';
@@ -47,6 +47,18 @@ const { loading: branchLoading } = useRequest(branchMethod).onSuccess(({ data })
   };
   branch.value = to(data);
 });
+// 递归查找机构名称(优先 label,其次 title),找不到返回空字符串
+function findInstitutionLabelById(nodes: any[], targetId: string | number | undefined): string {
+  if (!Array.isArray(nodes) || targetId === undefined || targetId === null) return '';
+  for (const node of nodes) {
+    if (node?.id === targetId || node?.value === targetId) {
+      return node?.label ?? node?.title ?? '';
+    }
+    const childResult = findInstitutionLabelById(node?.children ?? [], targetId);
+    if (childResult) return childResult;
+  }
+  return '';
+}
 const { loading: addSystemCwLoading, send: addSystemCw } = useRequest(addSystemCwMethod, {
   immediate: false,
 }).onSuccess(({ data }) => {
@@ -149,14 +161,36 @@ watch(totalPrice, (val) => {
   formData.price = val;
 });
 
-const projectSearchRef = useTemplateRef<HTMLInputElement>('projectSearchRef');
+const projectSearchRef = useTemplateRef<any>('projectSearchRef');
 const projectSearchFocus = (visible: boolean) => {
-  showProjectPopover.value = visible;
   if (visible) setTimeout(() => projectSearchRef.value?.focus?.(), 300);
 };
 
+const handleSearchInput = () => {
+  console.log('搜索输入事件触发,当前值:', projectSearch.value);
+  nextTick();
+};
+// 安全获取浮层挂载容器:优先 body,无法获取则回退到父节点
+const getPopoverContainer = (triggerNode: any) => {
+  try {
+    // @ts-ignore
+    if (typeof document !== 'undefined' && document?.body) return document.body;
+    // @ts-ignore
+    return triggerNode?.ownerDocument?.body || triggerNode?.parentNode;
+  } catch (e) {
+    return triggerNode?.parentNode;
+  }
+};
+
 const projectSearch = ref('');
 const showProjectPopover = ref(false);
+
+// 监听搜索文本变化
+watch(projectSearch, (newValue, oldValue) => {
+  console.log('搜索文本变化:', { newValue, oldValue });
+  console.log('当前项目数据:', allProjects.value);
+  console.log('过滤后的结果:', filteredProjects.value);
+});
 const allProjects = ref<
   Array<{
     name: string;
@@ -164,13 +198,24 @@ const allProjects = ref<
     effect?: string;
   }>
 >([]);
+watch(showProjectPopover, (v) => {
+  if (v) nextTick(() => projectSearchRef.value?.focus?.());
+});
+
 const filteredProjects = computed(() => {
   const searchText = projectSearch.value.toLowerCase();
+  console.log('搜索文本:', searchText);
+  console.log('所有项目数据:', allProjects.value);
+  console.log('项目数量:', allProjects.value.length);
+  
   if (allProjects.value.length > 0) {
-    return allProjects.value.filter(
+    const filtered = allProjects.value.filter(
       (p) => p.name.toLowerCase().includes(searchText) || p?.conditioningProgramType?.toLowerCase().includes(searchText) || p.effect?.toLowerCase().includes(searchText)
     );
+    console.log('过滤后的结果:', filtered);
+    return filtered;
   } else {
+    console.log('没有项目数据');
     return [];
   }
 });
@@ -336,22 +381,27 @@ function calculateCount(row: any) {
   } else if (pricingType === '1') {
     // 按穴位计价
     const frequencyType = Number(row.frequencyType) || 0;
+
     row.totalMeasure = Math.ceil((period / frequencyType) * frequency);
+    // console.log(frequencyType, 'frequencyType', frequency, period, row.totalMeasure);
+    // console.log(acCount, maxCount, 'acCount, maxCount', row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType);
     if (acCount > maxCount) {
       if (row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType === 0) {
         // 单价
         if (acCount > 0) {
           let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * acCount;
           row.unitPrice = unitPrice;
-          row.totalPrice = Math.ceil((period / frequencyType) * frequency * unitPrice);
+          row.totalPrice = Math.ceil((period / frequencyType) * frequency) * unitPrice;
         } else {
           row.unitPrice = '-';
           row.totalPrice = 0;
         }
       } else if (row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType === 1) {
         // 一口价
-        row.unitPrice = '-';
-        row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[1].price;
+        let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[1].price;
+        row.unitPrice = unitPrice;
+        // row.unitPrice = '-';
+        row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * row.totalMeasure;
       }
     } else {
       if (row.conditioningProgramDetail.cpDynamicPricingRule?.length > 0) {
@@ -360,15 +410,17 @@ function calculateCount(row: any) {
           if (acCount > 0) {
             let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * acCount;
             row.unitPrice = unitPrice;
-            row.totalPrice = Math.ceil((period / frequencyType) * frequency * unitPrice);
+            row.totalPrice = Math.ceil((period / frequencyType) * frequency) * unitPrice;
           } else {
             row.unitPrice = '-';
             row.totalPrice = 0;
           }
         } else if (row.conditioningProgramDetail.cpDynamicPricingRule?.[0]?.priceType === 1) {
           // 一口价
-          row.unitPrice = '-';
-          row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[0].price;
+          let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[0].price;
+          row.unitPrice = unitPrice;
+          // row.unitPrice = '-';
+          row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * row.totalMeasure;
         }
       }
     }
@@ -555,6 +607,12 @@ function removeTableRow(idx: number) {
 // 引入服务包
 function addInstitution() {
   deptId.value = localStorage.getItem('deptId') || '';
+  // console.log(formData.institutionId, 'formData.institutionId', branch, 'deptId.value');
+  // 若未有机构名称且已选择机构ID,则从树中递归查找名称
+  if (formData.institutionId) {
+    formData.institutionName = findInstitutionLabelById(branch.value, formData.institutionId) || '';
+  }
+  // console.log(formData.institutionName, 'formData.institutionName');
   VxeUI.modal.open({
     title: '选择引入',
     width: 1000,
@@ -569,6 +627,7 @@ function addInstitution() {
         return h(ServicePackageList, {
           data: formData,
           institutionId: formData.institutionId ? formData.institutionId : deptId.value,
+          institutionName: formData.institutionName ? formData.institutionName : '',
           onSubmit(data: SystemCwModel) {
             VxeUI.modal.close(`systemService-list-modal`);
           },
@@ -644,7 +703,7 @@ function openPopover() {
       <div class="mr-6" style="display: flex; align-items: center; margin-bottom: 0">
         <span style="white-space: nowrap; margin-right: 8px">服务包名称:</span>
         <a-input style="width: 200px" v-model:value="formData.name" />
-        <a-button type="primary" @click="addInstitution" class="ml-4" v-if="props.data?.types === 'institution'">引入</a-button>
+        <a-button type="primary" @click="addInstitution" class="ml-4" v-if="props.data?.types === 'institution'" :disabled="!formData.institutionId">引入</a-button>
       </div>
       <!-- 服务形象照 -->
       <div class="flex" v-if="props.data?.types === 'institution'" style="align-items: center; margin-bottom: 0">
@@ -765,15 +824,22 @@ function openPopover() {
         <vxe-column field="conditioningProgramDetail.name" title="项目名称" width="180">
           <template #default="{ row, rowIndex }">
             <template v-if="rowIndex === tableData.length - 1">
-              <a-popover
-                v-model="showProjectPopover"
+              <a-popover v-model:open="showProjectPopover"
                 trigger="click"
-                placement="bottomLeft"
-                :overlayStyle="{ width: '350px', padding: 0 }"
-                @openChange="projectSearchFocus($event)"
-              >
+                placement="bottomLeft" 
+               :overlayStyle="{ width: '350px', padding: 0, zIndex: 4000 }"
+               :getPopupContainer="getPopoverContainer"
+               :destroyTooltipOnHide="false"
+               :arrow="false"
+                @openChange="projectSearchFocus">
                 <template #content>
-                  <a-input ref="projectSearchRef" v-model:value="projectSearch" placeholder="输入项目名称搜索" style="margin: 8px; width: 90%" @input="() => nextTick()" />
+                  <vxe-input
+                    ref="projectSearchRef"
+                    v-model="projectSearch"
+                    placeholder="输入项目名称搜索"
+                    style="margin: 8px; width: 90%"
+                    @change="handleSearchInput"
+                  ></vxe-input>
                   <vxe-table :data="filteredProjects" border size="small" style="max-height: 240px; overflow-y: auto" @cell-click="onSelectProject">
                     <vxe-column field="name" title="项目名称" />
                     <vxe-column field="conditioningProgramType" title="方案类型" />
@@ -785,7 +851,7 @@ function openPopover() {
                     </vxe-column>
                   </vxe-table>
                 </template>
-                <a-input v-model:value="row.name" placeholder="请搜索" style="width: 120px" @focus="openPopover" readonly />
+                <vxe-input v-model="row.name" placeholder="请搜索" style="width: 120px" @click="openPopover" readonly></vxe-input>
               </a-popover>
             </template>
             <template v-else>
@@ -890,7 +956,6 @@ function openPopover() {
           </template>
         </vxe-column>
         <vxe-column field="remark" title="说明" width="180">
-         
           <template #default="{ row }">
             <!-- <div>说明star:{{ row.remark }}:说明结束</div> -->
             <a-textarea v-model:value="row.remark" style="max-width: 180px; width: 100%; height: 50px" :rows="2" show-count :maxLength="200" />

+ 8 - 2
src/service/HealthEvaluation.vue

@@ -7,7 +7,7 @@ import { systemCpEditMethod, getConditioningSchemeDetailMethod, confirmOrgConfir
 import { ref, watch } from 'vue';
 
 type FollowModel = Partial<SystemItemModel>;
-const props = defineProps<{ data: FollowModel }>();
+const props = defineProps<{ data: FollowModel; isType: string }>();
 
 const emits = defineEmits<{
   submit: [data?: SystemItemModel];
@@ -53,8 +53,14 @@ watch(pricingUnit, (newValue) => {
 });
 
 onMounted(async () => {
-  console.log(props.data, 'onMounted');
+  console.log('onMounted', props.data.isType);
+  if (props.data.isType === 'itemsList') {
+    console.log(props.data, 'itemsList111');
+    delete props.data.id;
+  }
+  console.log(props.data.id, 'props.data222');
   if (props.data.id) {
+    console.log(props.data.id, 'props.data333');
     const res: any = await getConditioningSchemeDetailMethod(props.data);
     Object.assign(props.data, res);
   }

+ 6 - 4
src/service/IntroduceProjectList.vue

@@ -12,7 +12,7 @@ const emit = defineEmits<{
 }>();
 const tableData = ref<SystemItemModel[]>([]);
 type FollowModel = Partial<SystemCwModel>;
-const props = defineProps<{ data: FollowModel; id: number | string; institutionId: number | string }>();
+const props = defineProps<{ data: FollowModel; id: number | string; institutionId: number | string; institutionName: string }>();
 // 引入已有项目
 async function handleAdd() {
   // 关闭引入服务包弹窗
@@ -48,10 +48,8 @@ onMounted(async () => {
 });
 function addProject(row: any) {
   row.sourceId = row.id;
-  // delete row.id;
   const addType = 'itemsList';
   const isType = 'itemsList';
-  console.log(row, 'row传入的参数');
   if (row?.isErasable === 'N') {
     // 健康咨询 健康评估 x显示
     VxeUI.modal.open({
@@ -67,7 +65,9 @@ function addProject(row: any) {
             data: {
               ...row,
               addType,
-
+              institutionId: props.institutionId,
+              institutionName: props.institutionName,
+              isType,
             },
             onSubmit(data?: SystemItemModel) {
               // 点击确定 刷新列表
@@ -93,6 +93,8 @@ function addProject(row: any) {
               ...row,
               addType,
               isType,
+              institutionId: props.institutionId,
+              institutionName: props.institutionName,
             },
             onSubmit(data?: SystemItemModel) {
               // 点击确定 刷新列表

+ 2 - 1
src/service/ServicePackageList.vue

@@ -6,7 +6,7 @@ import { getSystemCpListMethod, getCopyCwMethod } from '@/request/api/care.api';
 import { usePagination } from 'alova/client';
 import type { SystemCwModel } from '@/model/care.model';
 type FollowModel = Partial<SystemCwModel>;
-const props = defineProps<{ data: FollowModel; institutionId: string }>();
+const props = defineProps<{ data: FollowModel; institutionId: string,institutionName: string }>();
 const emit = defineEmits(['update:data']);
 const tableData = ref<SystemCwModel[]>([]);
 const { loading, page, pageSize, total, onSuccess, replace, refresh, remove } = usePagination((page, size) => getSystemCpListMethod(page, size), {
@@ -70,6 +70,7 @@ async function handleSelect(model?: SystemCwModel) {
           data: { ...props.data },
           id: id.value,
           institutionId: props.institutionId,
+          institutionName: props.institutionName,
           onSubmit() {
             // 提交之后刷新列表
             handleSubmit();

+ 42 - 21
src/widgets/ReportIndicatorChartWidget.vue

@@ -59,33 +59,54 @@ const option = ref({
 });
 watchEffect(() => update(props.dataset));
 
-function update(data: ReportIndicatorModel) {
-  const range = data.range.map(t => +t);
-  let min = range[ 0 ], max = range[ 1 ], source = [];
+function update(data: ReportIndicatorModel | undefined | null) {
+  if (!data || !Array.isArray((data as any).range) || !Array.isArray((data as any).records)) {
+    option.value.title.text = '';
+    option.value.yAxis.name = '' as any;
+    option.value.series = [];
+    option.value.visualMap.pieces = [] as any;
+    return;
+  }
+  const range = (data.range as any[]).map(t => +t);
+  let min = Number.isFinite(range[0]) ? range[0] : 0;
+  let max = Number.isFinite(range[1]) ? range[1] : 0;
   const records = [ ...data.records ].reverse();
-  for ( const { time, value } of records ) {
-    source.push(time.format('YYYY/MM/DD'), value);
-    min = Math.min(min, value);
-    max = Math.max(max, value);
+  const source: any[] = [];
+  for ( const { time, value } of records as any[] ) {
+    const timeStr = typeof (time as any)?.format === 'function' ? (time as any).format('YYYY/MM/DD') : String(time ?? '');
+    const numericValue = +value;
+    source.push(timeStr, numericValue);
+    if (Number.isFinite(numericValue)) {
+      min = Math.min(min, numericValue);
+      max = Math.max(max, numericValue);
+    }
+  }
+  option.value.title.text = data.name ?? '';
+  option.value.yAxis.name = (data as any).unit ?? '';
+  const editor = (data as any).editor ?? {};
+  const editorMin = Number.isFinite(+editor.min) ? +editor.min : min;
+  const editorMax = Number.isFinite(+editor.max) ? +editor.max : max;
+  option.value.yAxis.min = Math.max(editorMin, Math.floor((Number.isFinite(min) ? min : 0) * 0.75));
+  option.value.yAxis.max = Math.min(editorMax, Math.floor((Number.isFinite(max) ? max : 0) * 1.25));
+  option.value.yAxis.minInterval = editor.step ?? 1;
+  if (range.length === 2 && range.every(v => Number.isFinite(v))) {
+    option.value.visualMap.pieces = [
+      { gt: range[ 1 ], color: '#fc97af' },
+      { gte: range[ 0 ], lte: range[ 1 ], color: '#72ccff' },
+      { lt: range[ 0 ], color: '#fc97af' },
+    ] as any;
+  } else {
+    option.value.visualMap.pieces = [] as any;
   }
-  option.value.title.text = data.name;
-  option.value.yAxis.name = data.unit;
-  option.value.yAxis.min = Math.max(data.editor.min, Math.floor(min * 0.75));
-  option.value.yAxis.max = Math.min(data.editor.max, Math.floor(max * 1.25));
-  option.value.yAxis.minInterval = data.editor.step ?? 1;
-  option.value.visualMap.pieces = [
-    { gt: range[ 1 ], color: '#fc97af' },
-    { gte: range[ 0 ], lte: range[ 1 ], color: '#72ccff' },
-    { lt: range[ 0 ], color: '#fc97af' },
-  ];
   option.value.series = [
     {
       name: data.name, smooth: true, type: 'line',
-      data: records.map((record) => [ record.time.format('YYYY/MM/DD'), record.value ]),
+      data: (records as any[]).map((record) => [
+        typeof record.time?.format === 'function' ? record.time.format('YYYY/MM/DD') : String(record.time ?? ''),
+        +record.value,
+      ]),
       markLine: {
-        data: data.range.map(value => (
-          { yAxis: value }
-        )),
+        data: Array.isArray(data.range) ? (data.range as any[]).map(value => ({ yAxis: +value })) : [],
       },
     },
   ];