ソースを参照

完成功能:辨识仪配置以及报告管理

张田田 6 ヶ月 前
コミット
4029c48e5d

+ 351 - 0
src/components/EditConfigured.vue

@@ -0,0 +1,351 @@
+<script setup lang="ts">
+import { VxeUI, type VxeFormProps, type VxeFormListeners } from 'vxe-pc-ui';
+import { useRequest } from 'alova/client';
+import { getDeviceManageDetailMethod, updateDeviceManageMethod } from '@/request/api/device.api';
+import { branchMethod } from '@/request/api/system.api';
+import type { DeviceManageModel } from '@/model/device.model';
+import { AioFlowConfig, type FlowRequestData } from '@/pages/aio/flow-config/index';
+type FollowModel = Partial<DeviceManageModel>;
+const flowData = ref<FlowRequestData>();
+const loading = ref<boolean>(false);
+
+const props = defineProps<{ data: FollowModel }>();
+
+const emits = defineEmits<{
+  submit: [data?: DeviceManageModel];
+}>();
+
+const model = ref<DeviceManageModel>({});
+
+// 获取详情
+const getDetail = async () => {
+  try {
+    const res = await getDeviceManageDetailMethod(props.data);
+    if (res && JSON.stringify(res) !== '{}') {
+      flowData.value = { ...res } as FlowRequestData;
+      console.log(flowData.value, 'flowData.value');
+      loading.value = false;
+      model.value = { ...res };
+    }
+  } catch (error: any) {
+    console.error(error, 'error');
+    // notification.error({
+    //   message: error?.message,
+    //   // description: error?.message ?? '获取一体机配置详情失败',
+    // });
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  if (props.data && props.data.id) {
+    getDetail();
+  }
+});
+
+const branch = ref<any[]>([]);
+useRequest(branchMethod(0, 1, 1)).onSuccess(({ data }) => {
+  const to = (data?: any[]): any[] => {
+    return Array.isArray(data)
+      ? data.map((item) => {
+          return {
+            ...item,
+            value: item.id,
+            key: item.id.toString(),
+            children: to(item.children),
+          };
+        })
+      : [];
+  };
+  branch.value = to(data);
+});
+
+const { loading: submitting, send: submit } = useRequest(updateDeviceManageMethod, { immediate: false }).onSuccess(() => {
+  emits('submit');
+});
+// (配置弹窗无需设备名称下拉,保留最小依赖)
+// const showDept = ref(false);
+// const insArr = ref<any[]>([]);
+// const insLoading = ref(false);
+// async function getInstitution(orgId: string | number) {
+//   insLoading.value = true;
+//   const res = await branchMethod(1, 0, Number(orgId));
+//   if (res && res.length > 0) {
+//     insArr.value = res;
+//   }
+//   insLoading.value = false;
+// }
+// watch(
+//   () => model.value?.orgId,
+//   async (newVal, oldVal) => {
+//     showDept.value = !!newVal;
+//     if (showDept.value) {
+//       // 加载机构列表,确保编辑时机构名称可显示
+//       await getInstitution(newVal ?? '');
+//     } else {
+//       insArr.value = [];
+//     }
+//     // 仅当组织实际发生切换时清空机构
+//     if (oldVal !== undefined && newVal !== oldVal) {
+//       model.value.institutionId = '';
+//     }
+//   },
+//   { immediate: true }
+// );
+// 顶部摘要展示用
+// 概要信息直接在模板中从 model 读取
+const formItems = computed(() => {
+  const baseItems: any[] = [
+    { field: 'summary', span: 24, slots: { default: 'summarySlot' } },
+    { field: 'processConfig', span: 24, slots: { default: 'processConfigSlot' } },
+    { align: 'center', span: 24, slots: { default: 'active' } },
+  ];
+
+  // 摘要展示不需要机构选择器
+
+  return baseItems;
+});
+
+const formProps = reactive<VxeFormProps>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: computed(() => model.value),
+  items: [] as any, // 临时设置为空数组,我们将在模板中使用动态items
+  rules: {},
+});
+
+// const formEmits: VxeFormListeners = {
+//   submit({ data }) {
+//     // submit(data).then(() => {
+//     //   notification.success({
+//     //     message: '操作成功',
+//     //   });
+//     //   VxeUI.modal.close('equipment-modal');
+//     // });
+//   },
+// };
+
+// 设备ID增删在本弹窗不需要
+
+// onBeforeMount(async () => {
+//   if (props.data) {
+//     model.value = { ...defaultModel, ...props.data };
+//   }
+// });
+// const mock = () => {
+//   return {
+//     tabletProcessModules: ['patient_file', 'tongueface_upload', 'tongueface_analysis', 'alcohol_upload_result', 'health_analysis?'],
+//     tabletFileFields: ['phone:required', 'cardno', 'name', 'sex', 'age', 'height', 'weight', 'is_easy_allergy'],
+//     tabletRequiredPageOperationElements: [
+//       'tongueface_upload_report_page_appletscan',
+//       'health_analysis_report_page_appletbutton',
+//       'health_analysis_report_page_appletscan',
+//       'health_analysis_scheme_page_appletbutton',
+//     ],
+//     technicalSupporter: '杭州六智科技有限公司',
+//   };
+// };
+// const request = (timeout = 1000) => {
+//   loading.value = true;
+//   // setTimeout(() => {
+//   flowData.value = mock();
+//   console.log(flowData.value, '111');
+//   loading.value = false;
+//   // }, timeout);
+// };
+// 模拟请求
+// request();
+const flowRef = useTemplateRef<InstanceType<typeof AioFlowConfig>>('flow');
+const save = async () => {
+  // 改为仅透传提交事件,由父级统一关闭弹窗,避免重复 close 导致内部状态异常
+  try {
+    await flowRef.value!.validate(/* 传入 false 不展示错误信息 */ true);
+    flowData.value!.id = model.value?.id as string;
+    // emits('submit', flowData.value as any);
+    console.log('获取修改的数据', flowData.value);
+    submit(flowData.value as any);
+  } catch (error: any) {
+    console.error('保存错误', error.message);
+  }
+};
+const reset = () => {
+  flowRef.value?.update(flowData.value as FlowRequestData);
+};
+</script>
+
+<template>
+  <div class="form-container">
+    <vxe-form
+      :title-width="formProps.titleWidth"
+      :title-align="formProps.titleAlign"
+      :title-colon="formProps.titleColon"
+      :data="formProps.data"
+      :items="formItems"
+      :rules="formProps.rules"
+      :loading="submitting"
+    >
+      <template #deviceIdTitleSlot> <span style="color: #f56c6c; font-size: 20px">*</span> 设备ID </template>
+
+      <template #summarySlot>
+        <div class="summary-container">
+          <div class="summary-item mb-3">
+            <span class="label" v-if="model && model.warrant">设备ID:</span><span class="value">{{ model.warrant }}</span>
+          </div>
+          <div class="flex">
+            <div class="summary-item mr-6" v-if="model && model.orgName">
+              <span class="label">组织名称:</span><span class="value">{{ model.orgName }}</span>
+            </div>
+            <div class="summary-item" v-if="model && model.institutionName">
+              <span class="label">机构名称:</span><span class="value">{{ model.institutionName }}</span>
+            </div>
+          </div>
+        </div>
+      </template>
+
+      <template #processConfigSlot>
+        <div class="flex-auto content-container">
+          <div class="title">流程配置</div>
+          <span class="section-divider"></span>
+          <!-- validate 方法通过后会自动更新 -->
+          <AioFlowConfig ref="flow" :loading="loading" v-model:request-data="flowData"></AioFlowConfig>
+        </div>
+      </template>
+
+      <template #active>
+        <vxe-button type="reset" content="重置" @click="reset()"></vxe-button>
+        <vxe-button type="submit" status="primary" content="保存" @click="save()"></vxe-button>
+      </template>
+    </vxe-form>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.content-container {
+  width: 100%;
+  height: 600px;
+  display: flex;
+  flex-direction: column;
+  > div:not(.title) {
+    flex: auto;
+  }
+  > .title {
+    flex: none;
+    padding: 12px;
+    font-size: 16px;
+    font-weight: 800;
+  }
+}
+.section-divider {
+  height: 1px !important;
+  background-color: #d9d9d9;
+  margin-bottom: 15px !important;
+}
+.form-container {
+  padding: 20px;
+}
+
+.device-ids-container {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: flex-start;
+
+  .device-id-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 8px;
+  }
+}
+
+.section-container {
+  width: 100%;
+  height: 800px;
+  margin-bottom: 20px;
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 10px;
+    color: #333;
+  }
+}
+
+.process-config {
+  .config-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16px;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .config-label {
+      font-weight: bold;
+      min-width: 80px;
+    }
+
+    .report-label {
+      margin-left: 40px;
+      color: #666;
+      margin-right: 10px;
+    }
+
+    .radio-group {
+      display: flex;
+      align-items: center;
+      gap: 30px;
+
+      .radio-item {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        cursor: pointer;
+
+        input[type='radio'] {
+          margin: 0;
+          cursor: pointer;
+        }
+
+        span {
+          font-size: 14px;
+        }
+      }
+
+      .badge {
+        background-color: #faad14;
+        color: white;
+        border-radius: 12px;
+        padding: 2px 8px;
+        font-size: 12px;
+        min-width: 20px;
+        text-align: center;
+      }
+    }
+  }
+}
+
+:deep(.vxe-form--item) {
+  .vxe-form--item-wrapper {
+    .vxe-form--item-content {
+      .vxe-input {
+        width: 100%;
+      }
+    }
+  }
+}
+
+.required-field {
+  :deep(.vxe-form--item-title) {
+    position: relative;
+
+    &::before {
+      content: '*';
+      color: #ff4d4f;
+      position: absolute;
+      left: -8px;
+      top: 0;
+    }
+  }
+}
+</style>

+ 5 - 260
src/components/EditEquirement.vue

@@ -26,22 +26,6 @@ watchEffect(() => {
     model.value = { ...defaultModel, ...props.data };
   }
 });
-// 确保processConfig始终存在
-// watchEffect(() => {
-//   if (!model.value.processConfig) {
-
-//     model.value.processConfig = {
-//       archiving: false,
-//       tongueDiagnosis: false,
-//       tongueReport: 'none' as const,
-//       pulseDiagnosis: false,
-//       pulseReport: 'none' as const,
-//       inquiry: false,
-//       healthReport: 'none' as const,
-//       conditioningPlan: 'none' as const
-//     };
-//   }
-// });
 // 获取详情
 const getDetail = async () => {
   const res = await getDeviceRegisterDetailMethod(props.data);
@@ -182,16 +166,8 @@ const formItems = computed(() => {
         },
       },
     },
-    // {
-    //   field: 'processConfig',
-    //   title: '',
-    //   span: 24,
-    //   slots: {
-    //     default: 'processConfigSlot',
-    //   },
-    // },
     {
-      field: 'remarks',
+      field: 'remark',
       title: '备注',
       span: 24,
       slots: {
@@ -272,6 +248,8 @@ const formEmits: VxeFormListeners = {
     if (data.id) {
       data.deviceCode = data.deviceIds[0];
     }
+    data.remark ??= '';
+    console.log(data, 'data==>');
     submit(data).then(() => {
       notification.success({
         message: '操作成功',
@@ -334,9 +312,7 @@ function normalizeDeviceTypeToId() {
       v-on="formEmits"
       :loading="submitting"
     >
-      <template #deviceIdTitleSlot>
-        <span style="color: #f56c6c;font-size: 20px;">*</span> 设备ID
-      </template>
+      <template #deviceIdTitleSlot> <span style="color: #f56c6c; font-size: 20px">*</span> 设备ID </template>
 
       <template #deviceIdSlot>
         <div class="device-ids-container">
@@ -352,237 +328,6 @@ function normalizeDeviceTypeToId() {
         </div>
       </template>
 
-      <!-- <template #processConfigSlot>
-        <div class="section-container">
-          <div class="section-title">流程配置</div>
-          <div class="section-divider"></div>
-          <div class="process-config">
-            <div class="config-item">
-              <span class="config-label">建档:</span>
-              <div class="radio-group">
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.archiving" 
-                    :value="true"
-                    :checked="model.processConfig?.archiving"
-                  />
-                  <span>有</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.archiving" 
-                    :value="false"
-                    :checked="!model.processConfig?.archiving"
-                  />
-                  <span>无</span>
-                </label>
-              </div>
-            </div>
-
-            <div class="config-item">
-              <span class="config-label">舌面诊:</span>
-              <div class="radio-group">
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.tongueDiagnosis" 
-                    :value="true"
-                    :checked="model.processConfig?.tongueDiagnosis"
-                    disabled
-                  />
-                  <span>有</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.tongueDiagnosis" 
-                    :value="false"
-                    :checked="!model.processConfig?.tongueDiagnosis"
-                  />
-                  <span>无</span>
-                </label>
-              </div>
-              <span class="report-label">舌面分析报告:</span>
-              <div class="radio-group">
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.tongueReport" 
-                    value="full"
-                    :checked="model.processConfig?.tongueReport === 'full'"
-                  />
-                  <span>完整展示</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.tongueReport" 
-                    value="scan"
-                    :checked="model.processConfig?.tongueReport === 'scan'"
-                  />
-                  <span>扫码查看</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.tongueReport" 
-                    value="none"
-                    :checked="model.processConfig?.tongueReport === 'none'"
-                  />
-                  <span>无</span>
-                </label>
-              </div>
-            </div>
-
-            <div class="config-item">
-              <span class="config-label">脉诊:</span>
-              <div class="radio-group">
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.pulseDiagnosis" 
-                    :value="true"
-                    :checked="model.processConfig?.pulseDiagnosis"
-                  />
-                  <span>有</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.pulseDiagnosis" 
-                    :value="false"
-                    :checked="!model.processConfig?.pulseDiagnosis"
-                  />
-                  <span>无</span>
-                </label>
-              </div>
-              <span class="report-label">脉象分析报告:</span>
-              <div class="radio-group">
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.pulseReport" 
-                    value="full"
-                    :checked="model.processConfig?.pulseReport === 'full'"
-                  />
-                  <span>完整展示</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.pulseReport" 
-                    value="scan"
-                    :checked="model.processConfig?.pulseReport === 'scan'"
-                  />
-                  <span>扫码查看</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.pulseReport" 
-                    value="none"
-                    :checked="model.processConfig?.pulseReport === 'none'"
-                  />
-                  <span>无</span>
-                </label>
-              </div>
-            </div>
-
-            <div class="config-item">
-              <span class="config-label">问诊:</span>
-              <div class="radio-group">
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.inquiry" 
-                    :value="true"
-                    :checked="model.processConfig?.inquiry"
-                  />
-                  <span>有</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.inquiry" 
-                    :value="false"
-                    :checked="!model.processConfig?.inquiry"
-                  />
-                  <span>无</span>
-                </label>
-              </div>
-            </div>
-
-            <div class="config-item">
-              <span class="config-label">健康分析报告:</span>
-              <div class="radio-group">
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.healthReport" 
-                    value="full"
-                    :checked="model.processConfig?.healthReport === 'full'"
-                  />
-                  <span>完整展示</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.healthReport" 
-                    value="scan"
-                    :checked="model.processConfig?.healthReport === 'scan'"
-                  />
-                  <span>扫码查看</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.healthReport" 
-                    value="none"
-                    :checked="model.processConfig?.healthReport === 'none'"
-                  />
-                  <span>无</span>
-                </label>
-              </div>
-            </div>
-
-            <div class="config-item">
-              <span class="config-label">调理方案:</span>
-              <div class="radio-group">
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.conditioningPlan" 
-                    value="full"
-                    :checked="model.processConfig?.conditioningPlan === 'full'"
-                  />
-                  <span>完整展示</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.conditioningPlan" 
-                    value="scan"
-                    :checked="model.processConfig?.conditioningPlan === 'scan'"
-                  />
-                  <span>扫码查看</span>
-                </label>
-                <label class="radio-item">
-                  <input 
-                    type="radio" 
-                    v-model="model.processConfig.conditioningPlan" 
-                    value="none"
-                    :checked="model.processConfig?.conditioningPlan === 'none'"
-                  />
-                  <span>无</span>
-                </label>
-              </div>
-            </div>
-          </div>
-        </div>
-      </template> -->
-
       <template #remarksSlot>
         <div class="section-container">
           <textarea
@@ -705,7 +450,7 @@ function normalizeDeviceTypeToId() {
 .required-field {
   :deep(.vxe-form--item-title) {
     position: relative;
-    
+
     &::before {
       content: '*';
       color: #ff4d4f;

+ 315 - 0
src/components/EditMoreConfigured.vue

@@ -0,0 +1,315 @@
+<script setup lang="ts">
+import { type VxeFormProps, type VxeFormListeners } from 'vxe-pc-ui';
+import { useRequest } from 'alova/client';
+import { batchUpdateDeviceManageMethod, getDeviceManageDetailMethod, updateDeviceManageMethod } from '@/request/api/device.api';
+import { notification } from 'ant-design-vue';
+import { AioFlowConfig, type FlowRequestData } from '@/pages/aio/flow-config/index';
+import type { DeviceManageModel } from '@/model/device.model';
+const flowData = ref<FlowRequestData>();
+const loading = ref(false);
+const defaultModel: DeviceManageModel = {};
+
+const props = defineProps<{ data: DeviceManageModel[] }>();
+
+const emits = defineEmits<{
+  submit: [data?: DeviceManageModel];
+  submitSingle: [];
+}>();
+
+const model = ref<DeviceManageModel>({ ...defaultModel });
+
+watchEffect(() => {
+  if (props.data) {
+    model.value = { ...defaultModel, ...props.data };
+  }
+});
+
+const { loading: submitting, send: submit } = useRequest(batchUpdateDeviceManageMethod, { immediate: false }).onSuccess(() => {
+  emits('submit');
+  notification.success({ message: '修改成功' });
+});
+const { loading: submittingSingle, send: submitSingle } = useRequest(updateDeviceManageMethod, { immediate: false }).onSuccess(() => {
+  emits('submitSingle');
+  notification.success({ message: '修改成功' });
+});
+// 右侧设备列表(兼容数组/对象形式)
+const tableData = computed<any[]>(() => {
+  const src = props.data as any;
+  if (Array.isArray(src)) return src;
+  if (src && Array.isArray(src.data)) return src.data;
+  return src ? [src] : [];
+});
+const formItems = computed<any[]>(() => {
+  return [
+    { field: 'processConfig', span: 24, slots: { default: 'processConfigSlot' } },
+    { field: 'actions', span: 24, slots: { default: 'active' } },
+  ];
+});
+
+const formProps = reactive<VxeFormProps>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: computed(() => model.value),
+  items: [] as any, // 临时设置为空数组,我们将在模板中使用动态items
+  rules: {},
+});
+
+// 设备ID增删在批量配置不需要
+let ids = ref<string[]>([]);
+onBeforeMount(async () => {
+  if (props.data && Array.isArray(props.data) && props.data.length > 1) {
+    props.data?.forEach((item: any) => {
+      ids.value.push(item.id);
+    });
+    model.value = { ...(Array.isArray(props.data) ? {} : props.data) };
+    flowData.value = mock();
+  } else if (props.data && props.data.length === 1) {
+    flowData.value = (await getDeviceManageDetailMethod({ id: props.data[0].id })) as FlowRequestData;
+  }
+});
+// 初始化数据
+const mock = () => {
+  return {
+    tabletProcessModules: ['patient_file', 'tongueface_upload', 'tongueface_analysis', 'health_analysis?'],
+    tabletFileFields: ['phone:required', 'cardno', 'name', 'sex', 'age', 'height', 'weight', 'is_easy_allergy'],
+    tabletRequiredPageOperationElements: [
+      'tongueface_upload_report_page_appletscan',
+      'health_analysis_report_page_appletbutton',
+      'health_analysis_report_page_appletscan',
+      'health_analysis_scheme_page_appletbutton',
+    ],
+    technicalSupporter: '杭州六智科技有限公司',
+  };
+};
+const flowRef = useTemplateRef<InstanceType<typeof AioFlowConfig>>('flow');
+const save = async () => {
+  // 改为仅透传提交事件,由父级统一关闭弹窗,避免重复 close 导致内部状态异常
+  await flowRef.value!.validate(/* 传入 false 不展示错误信息 */ true);
+  if (Array.isArray(props.data) && props.data.length > 1) {
+    await submit({ ...flowData.value, ids: ids.value });
+  } else if (Array.isArray(props.data) && props.data.length === 1) {
+    await submitSingle({ ...flowData.value, id: props.data[0].id } as DeviceManageModel);
+  }
+};
+const reset = () => {
+  flowRef.value?.update();
+};
+</script>
+
+<template>
+  <div class="two-pane">
+    <div class="left form-container">
+      <vxe-form
+        :title-width="formProps.titleWidth"
+        :title-align="formProps.titleAlign"
+        :title-colon="formProps.titleColon"
+        :data="formProps.data"
+        :items="formItems"
+        :rules="formProps.rules"
+        :loading="submitting"
+      >
+        <template #processConfigSlot>
+          <div class="flex-auto content-container">
+            <div class="title">流程配置</div>
+            <!-- validate 方法通过后会自动更新 -->
+            <AioFlowConfig ref="flow" :loading="loading" v-model:request-data="flowData"></AioFlowConfig>
+          </div>
+        </template>
+
+        <template #active>
+          <div style="text-align: center">
+            <vxe-button type="reset" content="重置" @click="reset()"></vxe-button>
+            <vxe-button type="submit" status="primary" content="保存" @click="save()"></vxe-button>
+          </div>
+        </template>
+      </vxe-form>
+    </div>
+
+    <div class="right table-container">
+      <div class="table-title">待配置设备</div>
+      <div class="table-wrapper">
+        <table class="simple-table">
+          <thead>
+            <tr>
+              <th>设备ID</th>
+              <th>组织</th>
+              <th>机构名称</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="item in tableData" :key="item.id">
+              <td>{{ item.warrant }}</td>
+              <td>{{ item.orgName }}</td>
+              <td>{{ item.institutionName }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.content-container {
+  width: 100%;
+  height: 600px;
+  display: flex;
+  flex-direction: column;
+  > div:not(.title) {
+    flex: auto;
+  }
+  > .title {
+    flex: none;
+    padding: 12px;
+    font-size: 16px;
+    font-weight: 800;
+  }
+}
+.two-pane {
+  display: flex;
+  gap: 24px;
+}
+.left {
+  flex: 1;
+}
+.right {
+  width: 40%;
+  min-width: 420px;
+}
+.table-title {
+  font-weight: 600;
+  margin-bottom: 8px;
+}
+.table-wrapper {
+  border: 1px solid #eee;
+  height: 100%;
+  overflow: auto;
+}
+.simple-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+.simple-table th,
+.simple-table td {
+  border-bottom: 1px solid #f0f0f0;
+  padding: 8px 12px;
+  text-align: left;
+}
+.form-container {
+  padding: 20px;
+}
+
+.device-ids-container {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: flex-start;
+
+  .device-id-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 8px;
+  }
+}
+.section-divider {
+  height: 1px;
+  background-color: #d9d9d9;
+  margin-bottom: 15px;
+}
+.section-container {
+  margin-bottom: 20px;
+
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 10px;
+    color: #333;
+  }
+
+  .section-divider {
+    height: 1px;
+    background-color: #d9d9d9;
+    margin-bottom: 15px;
+  }
+}
+
+.process-config {
+  .config-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16px;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .config-label {
+      font-weight: bold;
+      min-width: 80px;
+    }
+
+    .report-label {
+      margin-left: 40px;
+      color: #666;
+      margin-right: 10px;
+    }
+
+    .radio-group {
+      display: flex;
+      align-items: center;
+      gap: 30px;
+
+      .radio-item {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        cursor: pointer;
+
+        input[type='radio'] {
+          margin: 0;
+          cursor: pointer;
+        }
+
+        span {
+          font-size: 14px;
+        }
+      }
+
+      .badge {
+        background-color: #faad14;
+        color: white;
+        border-radius: 12px;
+        padding: 2px 8px;
+        font-size: 12px;
+        min-width: 20px;
+        text-align: center;
+      }
+    }
+  }
+}
+
+:deep(.vxe-form--item) {
+  .vxe-form--item-wrapper {
+    .vxe-form--item-content {
+      .vxe-input {
+        width: 100%;
+      }
+    }
+  }
+}
+
+.required-field {
+  :deep(.vxe-form--item-title) {
+    position: relative;
+
+    &::before {
+      content: '*';
+      color: #ff4d4f;
+      position: absolute;
+      left: -8px;
+      top: 0;
+    }
+  }
+}
+</style>

+ 9 - 5
src/components/Swiper.vue

@@ -9,9 +9,14 @@ import 'swiper/css/scrollbar'; // 轮播图的滚动条
 // 引入swiper核心和所需模块
 import { Autoplay, Pagination, Navigation, Scrollbar } from 'swiper';
 
+type ImageItem = { image: string; date?: string };
+
 const modules = [Autoplay, Pagination, Navigation, Scrollbar];
-const props = defineProps<{ images: string[] }>();
-const slidesPerView = computed(() => Math.min(props.images?.length, 3));
+const props = defineProps<{ images: ImageItem[] }>();
+const slidesPerView = computed(() => {
+  const count = Array.isArray(props.images) ? props.images.length : 0;
+  return Math.min(count || 1, 3);
+});
 
 </script>
 <template>
@@ -19,15 +24,14 @@ const slidesPerView = computed(() => Math.min(props.images?.length, 3));
     <swiper
       :modules="modules"
       :loop="true"
-      :slides-per-view="1"
+      :slides-per-view="slidesPerView"
       :autoplay="{ delay: 4000, disableOnInteraction: false }"
       navigation
       :pagination="{ clickable: true }"
       :scrollbar="{ draggable: true }"
-      :slidesPerView="slidesPerView"
     >
       <!-- loop可循环轮播,autoplay可自动播放 -->
-      <swiper-slide v-for="img in props.images" :key="img.date">
+      <swiper-slide v-for="img in props.images" :key="img.date || img.image">
         <div>
           <span>{{ img?.date }}</span>
           <div>

+ 178 - 0
src/components/TongueAnalysisReport.vue

@@ -0,0 +1,178 @@
+<script setup lang="ts">
+import { useWatcher } from 'alova/client';
+import { getTonguefaceAnalysisReportMethod } from '@/request/api/report.api';
+import type { DeviceReportModel } from '@/model/device.model';
+import HealthReportAnalysisWidget from '@/widgets/HealthReportAnalysisWidget.vue';
+
+const props = defineProps<{
+  report: Partial<DeviceReportModel>;
+}>();
+
+
+// 患者最后一次健康分析报告
+const { data: healthRecord } = useWatcher(() => getTonguefaceAnalysisReportMethod(props?.report?.tonguefaceAnalysisReportId?.toString()!), [() => props.report.tonguefaceAnalysisReportId], {
+  initialData: {},
+  immediate: true,
+  middleware: (_, next) => {
+    console.log(healthRecord, 'props?.report?.tonguefaceAnalysisReportId');
+    if (props?.report?.tonguefaceAnalysisReportId) next();
+  },
+});
+
+// 判断是否有数据
+const hasData = computed(() => {
+  return props?.report?.tonguefaceAnalysisReportId && 
+         healthRecord.value && 
+         Object.keys(healthRecord.value).length > 0 &&
+         healthRecord.value?.analysis;
+});
+</script>
+
+<template>
+  <div class="p-6">
+    <!-- 有数据时显示分析组件 -->
+    <template v-if="hasData">
+      <HealthReportAnalysisWidget class="row" category="tongue" :analysis="healthRecord?.analysis"></HealthReportAnalysisWidget>
+      <HealthReportAnalysisWidget class="row" category="face" :analysis="healthRecord?.analysis"></HealthReportAnalysisWidget>
+    </template>
+    
+    <!-- 无数据时显示暂无数据提示 -->
+    <template v-else>
+      <div class="no-data-container">
+        <div class="no-data-content">
+          <div class="no-data-icon">📊</div>
+          <div class="no-data-text">暂无数据</div>
+          <div class="no-data-desc">当前没有舌面分析报告数据</div>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<style scoped lang="scss">
+section {
+  color: rgba(0, 0, 0, 0.85);
+
+  > header {
+    font-size: 18px;
+    font-weight: 700;
+
+    :deep(.ant-btn-link) {
+      padding-block: 0;
+      font-size: 18px;
+      border: none;
+    }
+  }
+
+  > main {
+    margin-left: 18px * 4;
+    padding: 0 15px;
+    font-size: 16px;
+    color: rgba(0, 0, 0, 0.85);
+
+    > .row > .ant-space {
+      font-size: 16px;
+    }
+
+    .row {
+      padding: 12px 0;
+
+      span > label {
+        color: rgba(0, 0, 0, 0.45);
+      }
+
+      label::after {
+        margin-left: 2px;
+        margin-right: 8px;
+        content: ':';
+      }
+
+      > header::before {
+        $size: 10px;
+        content: '';
+        display: inline-block;
+        margin-right: 12px;
+        width: $size;
+        height: $size;
+        border: 2px solid #1d6ff6;
+        border-radius: 50%;
+      }
+
+      > main {
+        margin-left: 18px * 2;
+      }
+    }
+  }
+
+  .ant-tag {
+    margin-top: 6px;
+  }
+}
+
+.separate {
+  :deep(.ant-space-item) {
+    & + .ant-space-item::before {
+      content: ',';
+      margin-right: 2px;
+    }
+  }
+
+  span + span::before {
+    content: ',';
+    margin-right: 2px;
+  }
+}
+
+.panel-wrapper {
+  :deep(.ant-tabs-content-holder) {
+    padding-top: 12px;
+    height: calc(100vh - 60px - 24px - 32px);
+
+    .ant-tabs-content {
+      height: 100%;
+    }
+  }
+}
+
+.trend-up {
+  color: #ff4d4f;
+  border-color: #ff4d4f;
+}
+
+.trend-down {
+  color: #87d068;
+  border-color: #87d068;
+}
+
+// 暂无数据样式
+.no-data-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 300px;
+  width: 100%;
+}
+
+.no-data-content {
+  text-align: center;
+  color: #999;
+  
+  .no-data-icon {
+    font-size: 48px;
+    margin-bottom: 16px;
+    opacity: 0.6;
+  }
+  
+  .no-data-text {
+    font-size: 18px;
+    font-weight: 500;
+    margin-bottom: 8px;
+    color: #666;
+  }
+  
+  .no-data-desc {
+    font-size: 14px;
+    color: #999;
+  }
+}
+</style>

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

@@ -20,5 +20,48 @@ export interface EquirementModel {
 }
 export type EquirementQuery = Partial<EquirementModel>;
 
+export interface DeviceManageModel{
+  id?:string; //一体机id
+  partner?:string; // 合作伙伴
+  technicalSupporter?:string; // 技术支持
+  tabletProcessModules?:string[]; // 流程模块
+  tabletFileFields?:string[]; // 文件字段
+  tabletRequiredPageOperationElements?:string[]; // 必填页面操作元素
+  tabletSetsDetailResume?:{
+    isPatientFile?:boolean; // 是否建档
+    isTonguefaceUpload?:boolean; // 是否舌面诊
+    isTonguefaceUploadResult?:boolean; // 是否舌面分析报告
+    isPulseUpload?:boolean; // 是否脉诊
+    isTonguefaceAnalysis?:boolean; // 是否问诊
+  }
+  updateBy?:string;
+  orgId?:number;
+  orgName?:string;
+  institutionId?:number;
+  institutionName?:string;
+  updateTimeStart?:string;
+  updateTimeEnd?:string;
+  isHaveResume?:boolean;
+  remark?:string;
+}
+export type DeviceManageQuery = Partial<DeviceManageModel>;
 
+export interface DeviceReportModel{
+  orgName?:string;
+  orgId?:number;
+  deviceType?:string; 
+  deviceName?:string;
+  deviceCode?:string;
+  name?:string;
+  phone?:string;
+  cardno?:string;
+  createTime?:string;
+  pulseAnalysisReportId?:number;
+  tonguefaceAnalysisReportId?:number;
+  healthAnalysisReportId?:number;
+  pulseAnalysisReportUrl?:string;
+  createTimeStart?:string;
+  createTimeEnd?:string;
+}
 
+export type DeviceReportQuery = Partial<DeviceReportModel>;

+ 13 - 1
src/pages/aio/flow-config/index.vue

@@ -49,7 +49,11 @@ const parentRef = useParentElement();
 const el = computed(() => parentRef.value?.querySelector(`.v-logic-flow`));
 
 const scope = effectScope();
+// 新加的 start
 let instance!: VLogicFlowInstance;
+let isUnmounted = false;
+const hasInstance = () => !isUnmounted && !!instance && !!instance.lf;
+// end
 const init = (lf: LogicFlowInstance): void => {
   instance = VLogicFlowInit(lf, {
     register: [{ category: 'node', type: 'FlowNode', view: FlowNodeView, model: FlowNodeViewModel }],
@@ -117,6 +121,7 @@ const init = (lf: LogicFlowInstance): void => {
 };
 
 const updateLayout = (dir?: 'LR' | 'TB' | 'center') => {
+  if (!hasInstance()) return;
   if (dir === 'center') {
     instance.lf.fitView(100, 0);
     instance.lf.translateCenter();
@@ -197,6 +202,7 @@ const openNodeConfig = (node: FlowNodeProps) => {
             id: node.id,
             requestData: (node.properties?.requestData as string[]) ?? [],
             'onUpdate:requestData'(value: any) {
+              if (!hasInstance()) return;
               instance.lf.getNodeModelById(node.id!)?.setProperties({ requestData: value });
               VxeUI.modal.close(`node:config`);
             },
@@ -207,6 +213,7 @@ const openNodeConfig = (node: FlowNodeProps) => {
 };
 
 const update = (data?: FlowRequestData) => {
+  if (!hasInstance()) return;
   const { graph, group } = fromFlowRequestData(data ?? requestData.value);
   nodes.value = graph.nodes?.map((node) => node.id!) ?? [];
   nodeGroup.value = group;
@@ -216,6 +223,7 @@ const update = (data?: FlowRequestData) => {
   if (graph.nodes && graph.nodes.length > 2) updateLayout('center');
 };
 const validate = (tips = true) => {
+  if (!hasInstance()) return Promise.reject(new Error('LogicFlow 已销毁'));
   const preNodeRules = {
     [Node.ID_Report_Pulse]: Node.ID_Analysis_Pulse,
     [Node.ID_Report_TongueAndFace]: Node.ID_Analysis_TongueAndFace,
@@ -292,7 +300,11 @@ const validate = (tips = true) => {
   return promise;
 };
 
-tryOnUnmounted(scope.stop);
+tryOnUnmounted(() => {
+  // 新加的
+  isUnmounted = true;
+  try { scope.stop(); } catch {}
+});
 
 defineExpose({
   validate,

+ 2 - 2
src/pages/aio/flow-config/tool.ts

@@ -31,9 +31,9 @@ const nodeRef = {
   [ID_Register]: /* 建档页 */ 'patient_file',
   [ID_Analysis_Pulse]: /* 脉诊页 */ 'pulse_upload',
   [ID_Analysis_TongueAndFace]: /*拍照页*/ 'tongueface_upload',
-  [ID_Analysis_Health]: /* 问页 */ 'tongueface_analysis',
+  [ID_Analysis_Health]: /* 问页 */ 'tongueface_analysis',
   [ID_Report_Pulse]: /* 脉诊结果页 */ 'pulse_upload_result',
-  [ID_Report_TongueAndFace]: 'tongueface_analysis_result',
+  [ID_Report_TongueAndFace]:  /* 舌面想分析报告页 */'tongueface_analysis_result',
   [ID_Report_Health]: /* 健康报告页 */ 'health_analysis',
   [ID_Scheme_Health]: /* 调理方案页 */ 'health_analysis_scheme',
   [ID_Report_Alcohol]: /* 酒精结果页 */ 'alcohol_upload_result',

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

@@ -0,0 +1,400 @@
+<script setup lang="ts">
+import { type VxeFormListeners, type VxeFormProps, VxeUI, type VxeGridProps } 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 '@/components/EditConfigured.vue';
+import EditMoreConfigured from '@/components/EditMoreConfigured.vue';
+import { analysisRequestData, type FlowRequestData } from '@/pages/aio/flow-config/index';
+
+defineOptions({ name: 'EquipmentConfiguredPage' });
+
+import type { DeviceManageModel, DeviceManageQuery } from '@/model/device.model';
+
+// 接口数据
+import { deviceManageMethod, updateDeviceRegisterOrganizationMethod } from '@/request/api/device.api';
+import { branchMethod } from '@/request/api/system.api';
+// 获取组织树
+const { data: branch, loading: branchLoading } = useRequest(branchMethod(0, 1, 1));
+
+const model = shallowRef<DeviceManageQuery>();
+
+// 获取设备类型
+const deviceTypes = ref<{ id: string; name: string }[]>([]);
+const deviceTypesLoading = ref(false);
+
+// 日期验证
+const updateTimeStart = ref<string>('');
+const updateTimeEnd = ref<string>('');
+// 禁用结束时间的日期(早于开始时间的日期)
+function disabledEndDate(current: any) {
+  if (!updateTimeStart.value) return false;
+  return current && current < dayjs(updateTimeStart.value);
+}
+
+const searchFormProps = reactive<VxeFormProps<DeviceManageQuery>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: {},
+  items: [
+    {
+      field: 'deviceCode',
+      title: '设备ID',
+      span: 6,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'orgId',
+      title: '组织名称',
+      span: 6,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+        events: {
+          change(val: any) {
+            insArr.value = [];
+            if (val.data.orgId) {
+              // 清空表单中的机构名称字段
+              if (model.value) {
+                model.value.institutionId = '';
+              }
+              getInstitution(val.data.orgId);
+            }
+          },
+        },
+      },
+    },
+    {
+      field: 'institutionId',
+      title: '机构名称',
+      span: 6,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => insLoading.value),
+          options: computed(() => insArr.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+      },
+    },
+    {
+      field: 'isHaveResume',
+      title: '是否配置',
+      span: 6,
+      itemRender: {
+        name: 'VxeRadioGroup',
+        options: [
+          { label: '已配置', value: 'true' },
+          { label: '未配置', value: 'false' },
+        ],
+        props: {
+          strict: false,
+        },
+      },
+    },
+
+    {
+      field: 'updateBy',
+      title: '修改人',
+      span: 6,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'updateTime',
+      title: '修改时间',
+      span: 10,
+      slots: {
+        default: 'createTimes',
+      },
+    },
+    {
+      field: 'action',
+      span: 6,
+      itemRender: {
+        name: 'VxeButtonGroup',
+        options: [
+          { name: 'submits', type: 'submit', content: '查询', status: 'primary' },
+          { name: 'reset', type: 'reset', content: '清空', status: 'warning' },
+          { name: 'config', content: '配置', status: 'primary' },
+        ],
+        events: {
+          click(slotParams, { name }) {
+            if (name === 'config') {
+              importOrganization();
+            }
+          },
+        },
+      },
+    },
+  ],
+});
+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;
+}
+const searchFormEmits: VxeFormListeners<DeviceManageQuery> = {
+  // 查询设备登记
+  submit({ data }) {
+    model.value = {
+      ...data,
+      updateTimeStart: updateTimeStart.value ? dayjs(updateTimeStart.value).format('YYYY-MM-DD HH:mm:ss') : '',
+      updateTimeEnd: updateTimeEnd.value ? dayjs(updateTimeEnd.value).format('YYYY-MM-DD HH:mm:ss') : '',
+    } as any;
+  },
+  // 重置
+  reset({ data }) {
+    model.value = { ...data } as any;
+    updateTimeStart.value = '';
+    updateTimeEnd.value = '';
+  },
+};
+// 设备列表
+const gridRef = ref<DeviceManageModel>();
+const gridOptions = reactive<VxeGridProps<DeviceManageModel>>({
+  id: 'tag-list',
+  border: true,
+  showOverflow: true,
+  height: 'auto',
+  autoResize: false,
+  syncResize: true,
+  scrollY: { enabled: true, gt: 0 },
+  toolbarConfig: {
+    custom: true,
+    zoom: true,
+    slots: {
+      tools: 'toolbar-extra',
+    },
+  },
+  columnConfig: {
+    resizable: true,
+  },
+  customConfig: {
+    storage: true,
+  },
+  columns: [
+    { type: 'checkbox', width: 60, fixed: 'left', title: '', align: 'center' },
+    { field: 'orgName', title: '组织名称' },
+    { field: 'warrant', title: '设备ID' },
+    {
+      title: '流程配置',
+      align: 'center',
+      children: [
+        { field: 'isPatientFile', title: '建档', formatter: (p: any) => yesNoFormatter(p.row.tabletSetsDetailResume.isPatientFile) },
+        { field: 'isTonguefaceUpload', title: '舌面诊', formatter: (p: any) => yesNoFormatter(p.row.tabletSetsDetailResume.isTonguefaceUpload) },
+        { field: 'tonguefaceUploadResultShowType', title: '舌面分析报告', formatter: (p: any) => showTypeFormatter(p.row.tabletSetsDetailResume.tonguefaceUploadResultShowType) },
+        { field: 'puisPulseUploadlse', title: '脉诊', formatter: (p: any) => yesNoFormatter(p.row.tabletSetsDetailResume.isPulseUpload) },
+        { field: 'pulseReportShowType', title: '脉象分析报告', formatter: (p: any) => showTypeFormatter(p.row.tabletSetsDetailResume.pulseReportShowType) },
+        { field: 'isTonguefaceAnalysis', title: '问诊', formatter: (p: any) => yesNoFormatter(p.row.tabletSetsDetailResume.isTonguefaceAnalysis) },
+        { field: 'healthAnalysisReportShowType', title: '健康分析报告', formatter: (p: any) => showTypeFormatter(p.row.tabletSetsDetailResume.healthAnalysisReportShowType) },
+        { field: 'isHealthAnalysisScheme', title: '调理方案', formatter: (p: any) => yesNoFormatter(p.row.tabletSetsDetailResume.isHealthAnalysisScheme) },
+      ],
+    },
+    { field: 'updateBy', title: '修改人' },
+    { field: 'updateTime', title: '修改时间' },
+    { field: 'remark', title: '备注' },
+    {
+      field: 'action',
+      title: '操作',
+      align: 'center',
+      width: 100,
+      showOverflow: false,
+      cellRender: {
+        name: 'VxeButtonGroup',
+        props: { mode: 'text' },
+        options: [{ content: '编辑', status: 'primary', name: 'editConfigured' }],
+        events: {
+          click({ row }: any, { name }: any) {
+            if (name === 'editConfigured') {
+              editConfigured(row as any);
+            }
+          },
+        },
+      },
+    },
+  ],
+  data: [],
+});
+const gridEvents: any = {};
+
+// 分页
+const { loading, page, pageSize, total, onSuccess, refresh } = usePagination((page, size) => deviceManageMethod(page, size, model.value), {
+  initialData: { data: [], total: 0 },
+  initialPage: 1,
+  initialPageSize: 100,
+  watchingStates: [model],
+  immediate: false,
+});
+onSuccess(({ data: { data } }) => {
+  console.log(data, '获取数据');
+  gridRef.value?.loadData(data);
+});
+
+// 获取设备类型
+async function getDeviceType() {
+  deviceTypesLoading.value = true;
+  const res = await getDictionaryMethod('fdhb_device_type');
+  if (res && res.length > 0) {
+    deviceTypes.value = res.map((item: any) => ({
+      id: item.value,
+      name: item.label,
+    }));
+  }
+  deviceTypesLoading.value = false;
+}
+
+onMounted(() => {
+  getDeviceType();
+  model.value = toRaw(searchFormProps.data);
+});
+
+// 表格显示格式化
+function yesNoFormatter(value: any) {
+  if (value === true) return '有';
+  if (value === false || value == null) return '无';
+  return String(value ?? '无');
+}
+function showTypeFormatter(value: any) {
+  // 与截图文案对应:"完整展示" / "无"
+  if (value === '1') return '完整展示';
+  if (value === '2') return '扫码查看';
+  if (!value || value === '0') return '无';
+  return String(value);
+}
+
+// 编辑辨识仪配置
+function editConfigured(model?: DeviceManageModel) {
+  VxeUI.modal.open({
+    // title: model?.id ? `修改配置` : `新增配置`,
+    title: `修改配置`,
+    // height: 850,
+    // width: 900,
+    fullscreen: true,
+    escClosable: true,
+    destroyOnClose: true,
+    id: `equirement-modal`,
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(EditConfigured, {
+          data: model,
+          onSubmit() {
+            refresh(page.value);
+            VxeUI.modal.close(`equirement-modal`);
+          },
+        } as any);
+      },
+    },
+  });
+}
+
+// 批量修改
+function importOrganization() {
+  const selectedRows = (gridRef.value?.getCheckboxRecords() as any[]) || [];
+  if (selectedRows.length === 0) {
+    notification.error({ message: '请先选择设备' });
+    return;
+  }
+  VxeUI.modal.open({
+    title: '批量修改',
+    fullscreen: true,
+    escClosable: true,
+    destroyOnClose: true,
+    id: 'import-more-configured',
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(EditMoreConfigured, {
+          data: selectedRows,
+          onSubmit() {
+            refresh(page.value);
+            VxeUI.modal.close('import-more-configured');
+          },
+          // onSubmit(org: any) {
+          //   const deviceIds = selectedRows.map((item: DeviceManageModel) => item.id);
+          //   batchUpdateDeviceManageMethod({ deviceIds, orgId: org.orgId, institutionId: org.institutionId })
+          //     .then(() => {
+          //       notification.success({ message: '批量修改成功' });
+          //       refresh(page.value);
+          //       VxeUI.modal.close('import-more-configured');
+          //     })
+          //     .catch(() => {
+          //       notification.error({ message: '批量修改失败' });
+          //     });
+          // },
+        });
+      },
+    },
+  });
+}
+</script>
+<template>
+  <div class="page-container flex flex-col">
+    <header class="flex-none mt-4">
+      <vxe-form v-bind="searchFormProps" v-on="searchFormEmits">
+        <template #createTimes>
+          <div class="date-range-container">
+            <a-date-picker v-model:value="updateTimeStart" placeholder="请选择开始时间" style="flex: 1" :show-time="{ format: 'HH:mm' }" />
+            <span class="date-separator">至</span>
+            <a-date-picker v-model:value="updateTimeEnd" placeholder="请选择结束时间" style="flex: 1" :disabledDate="disabledEndDate" :show-time="{ format: 'HH:mm' }" />
+          </div>
+        </template>
+      </vxe-form>
+    </header>
+    <main class="flex-auto overflow-hidden">
+      <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents" :loading="loading">
+        <template #toolbar-extra>
+          <vxe-button style="margin-right: 12px" icon="vxe-icon-repeat" circle @click="refresh(page)"></vxe-button>
+        </template>
+      </vxe-grid>
+    </main>
+    <footer class="flex-none">
+      <vxe-pager
+        v-model:current-page="page"
+        v-model:page-size="pageSize"
+        :total="total"
+        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']"
+      />
+    </footer>
+  </div>
+</template>
+<style scoped lang="scss">
+.page-container {
+  padding: 0 24px;
+  max-height: var(--page-main-container);
+}
+.date-range-container {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  width: 100%;
+
+  .vxe-input {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .date-separator {
+    color: #666;
+    font-size: 14px;
+    font-weight: 500;
+    white-space: nowrap;
+    padding: 0 8px;
+  }
+}
+</style>

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

@@ -0,0 +1,411 @@
+<script setup lang="ts">
+import { type VxeFormListeners, type VxeFormProps, 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 TongueAnalysisReport from '@/components/TongueAnalysisReport.vue';
+import type { DeviceReportModel } from '@/model/device.model';
+defineOptions({ name: 'EquipmentConfiguredPage' });
+import { h } from 'vue';
+
+import type { EquirementQuery } from '@/model/device.model';
+// 接口数据
+import { deviceReportMethod } from '@/request/api/device.api';
+import { branchMethod } from '@/request/api/system.api';
+// 获取组织树
+const { data: branch, loading: branchLoading } = useRequest(branchMethod(0, 1, 1));
+
+const model = shallowRef<EquirementQuery>();
+// 获取设备类型
+const deviceTypes = ref<{ id: string; name: string }[]>([]);
+const deviceTypesLoading = ref(false);
+// 日期验证
+const createTimeStart = ref<string>('');
+const createTimeEnd = ref<string>('');
+// 禁用结束时间的日期(早于开始时间的日期)
+function disabledEndDate(current: any) {
+  if (!createTimeStart.value) return false;
+  return current && current < dayjs(createTimeStart.value);
+}
+
+const searchFormProps = reactive<VxeFormProps<EquirementQuery>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: {},
+  items: [
+    {
+      field: 'orgId',
+      title: '组织名称',
+      span: 6,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+        events: {
+          change(val: any) {
+            insArr.value = [];
+            if (val.data.orgId) {
+              // 清空表单中的机构名称字段
+              if (model.value) {
+                model.value.institutionId = '';
+              }
+              getInstitution(val.data.orgId);
+            }
+          },
+        },
+      },
+    },
+    {
+      field: 'deviceType',
+      title: '设备名称',
+      span: 6,
+      itemRender: {
+        name: 'VxeSelect',
+        props: {
+          placeholder: '请选择',
+          loading: deviceTypesLoading.value,
+          options: computed(() => deviceTypes.value),
+          optionProps: { value: 'id', label: 'name' },
+          optionGroupProps: { options: 'groups' },
+          clearable: true,
+          filterable: true,
+        },
+      },
+    },
+    {
+      field: 'deviceCode',
+      title: '设备ID',
+      span: 6,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+
+    {
+      field: 'phone',
+      title: '手机号码',
+      span: 6,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'cardno',
+      title: '身份证号码',
+      span: 6,
+      itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
+    },
+    {
+      field: 'createTime',
+      title: '生成时间',
+      span: 10,
+      slots: {
+        default: 'createTimes',
+      },
+    },
+    {
+      span: 6,
+      itemRender: {
+        name: 'VxeButtonGroup',
+        options: [
+          { name: 'submits', type: 'submit', content: '查询', status: 'primary' },
+          { name: 'reset', type: 'reset', content: '清空', status: 'warning' },
+        ],
+        events: {
+          click(slotParams, { name }) {},
+        },
+      },
+    },
+  ],
+});
+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;
+}
+const searchFormEmits: VxeFormListeners<EquirementQuery> = {
+  // 查询设备登记
+  submit({ data }) {
+    model.value = {
+      ...data,
+      createTimeStart: createTimeStart.value ? dayjs(createTimeStart.value).format('YYYY-MM-DD HH:mm:ss') : '',
+      createTimeEnd: createTimeEnd.value ? dayjs(createTimeEnd.value).format('YYYY-MM-DD HH:mm:ss') : '',
+    } as any;
+  },
+  // 重置
+  reset({ data }) {
+    model.value = { ...data } as any;
+    createTimeStart.value = '';
+    createTimeEnd.value = '';
+  },
+};
+// 设备列表
+const gridRef = ref<any>();
+const gridOptions = reactive<any>({
+  id: 'tag-list',
+  border: true,
+  showOverflow: true,
+  height: 'auto',
+  autoResize: false,
+  syncResize: true,
+  scrollY: { enabled: true, gt: 0 },
+  toolbarConfig: {
+    custom: true,
+    zoom: true,
+    slots: {
+      tools: 'toolbar-extra',
+    },
+  },
+  columnConfig: {
+    resizable: true,
+  },
+  customConfig: {
+    storage: true,
+  },
+  columns: [
+    { field: 'orgName', title: '组织名称' },
+    { field: 'deviceType', title: '设备名称' },
+    { field: 'deviceCode', title: '设备ID' },
+    { field: 'name', title: '用户姓名' },
+    { field: 'phone', title: '手机号' },
+    { field: 'cardno', title: '身份证号' },
+    { field: 'createTime', title: '生成时间' },
+    {
+      field: 'action',
+      title: '操作',
+      align: 'center',
+      width: 280,
+      showOverflow: false,
+      slots: {
+        default: 'actionButtons',
+      },
+    },
+  ],
+  data: [],
+});
+const gridEvents: any = {};
+
+// 分页
+const { loading, page, pageSize, total, onSuccess, refresh } = usePagination((page, size) => deviceReportMethod(page, size, model.value), {
+  initialData: { data: [], total: 0 },
+  initialPage: 1,
+  initialPageSize: 100,
+  watchingStates: [model],
+  immediate: false,
+});
+onSuccess(({ data: { data } }) => {
+  gridRef.value?.loadData(data);
+});
+
+// 获取设备类型
+async function getDeviceType() {
+  deviceTypesLoading.value = true;
+  const res = await getDictionaryMethod('fdhb_device_type');
+  if (res && res.length > 0) {
+    deviceTypes.value = res.map((item: any) => ({
+      id: item.value,
+      name: item.label,
+    }));
+  }
+  deviceTypesLoading.value = false;
+}
+
+onMounted(() => {
+  getDeviceType();
+  model.value = toRaw(searchFormProps.data);
+});
+
+// 查看健康分析报告
+function openHistoryPreviewHandle(row: DeviceReportModel) {
+  console.log(row, '健康分析报告');
+  const component = defineAsyncComponent(() => import('@/components/ReportPreview.vue'));
+  const id = `drawer:report:preview`;
+  const onDestroy = () => {
+    VxeUI.drawer.close(id);
+  };
+  onDestroy();
+  VxeUI.drawer.open({
+    id,
+    mask: true,
+    lockView: false,
+    padding: false,
+    width: window.innerWidth - 256,
+    escClosable: true,
+    maskClosable: true,
+    title: `健康分析报告`,
+    slots: {
+      default() {
+        return h(component, {
+          reportId: row.healthAnalysisReportId?.toString(),
+          onDestroy,
+        });
+      },
+    },
+  });
+}
+// 脉象报告
+function pulseAnalysisReport(row: any) {
+  console.log(row, '脉象报告');
+  const id = `drawer:report-pulse:preview`;
+  const onDestroy = () => {
+    VxeUI.drawer.close(id);
+  };
+  onDestroy();
+
+  // 构建脉象报告URL,可以根据row数据动态生成
+  // const reportUrl = `https://hybrid.reborn-tech.com/report.html#/?mid=d78511637507423fb4323ab4cc38210e&access_session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhcHBJZCI6ImhKbjVEM3JyIiwiYXBwU2VjcmV0IjoiMGYwZDQ1MDIyNmYzMTZkNjY4ZDZlYjhiY2ZiYjRhY2NhNGNjYzQ3ZiJ9.Gnk2gUdO5EsN_C8fvwj9QaWpiNGizzFAypERKPk_u7A&appId=hJn5D3rr`;
+  const reportUrl = row?.pulseAnalysisReportUrl;
+  VxeUI.drawer.open({
+    id,
+    title: `脉象报告`,
+    maskClosable: true,
+    escClosable: true,
+    padding: false,
+    width: window.innerWidth - 256,
+    slots: {
+      default() {
+        return h(
+          'div',
+          {
+            style: {
+              width: '100%',
+              height: '100%',
+              border: 'none',
+              overflow: 'hidden',
+              position: 'relative',
+            },
+          },
+          [
+            h('iframe', {
+              src: reportUrl,
+              style: {
+                width: '100%',
+                height: '100%',
+                border: 'none',
+                display: 'block',
+              },
+              allow: 'camera; microphone; geolocation; payment; usb; vr; accelerometer; gyroscope; magnetometer; ambient-light-sensor; cross-origin-isolated',
+              sandbox: 'allow-same-origin allow-scripts allow-forms allow-popups allow-presentation allow-downloads',
+              onLoad: () => {
+                console.log('脉象报告页面加载完成');
+              },
+              onError: (error: any) => {
+                console.error('脉象报告页面加载失败:', error);
+                // 可以在这里显示错误提示
+              },
+            }),
+          ]
+        );
+      },
+    },
+    onHide() {
+      VxeUI.modal.close();
+    },
+  });
+}
+// 舌面分析报告
+function tongueAndFaceAnalysisReport(row: any) {
+  console.log(row, '舌面分析报告');
+  const id = `drawer:report-tongue:preview`;
+  const onDestroy = () => {
+    VxeUI.drawer.close(id);
+  };
+  VxeUI.drawer.open({
+    id,
+    title: `舌面分析报告`,
+    maskClosable: true,
+    escClosable: true,
+    padding: false,
+    width: window.innerWidth - 256,
+    slots: {
+      default() {
+        return h(TongueAnalysisReport, {
+          report: row,
+          onDestroy,
+          onRefresh() {
+            refresh(page.value);
+          },
+        });
+      },
+    },
+    onHide() {
+      VxeUI.modal.close();
+    },
+  });
+}
+</script>
+<template>
+  <div class="page-container flex flex-col">
+    <header class="flex-none mt-4">
+      <vxe-form v-bind="searchFormProps" v-on="searchFormEmits">
+        <template #createTimes>
+          <div class="date-range-container">
+            <a-date-picker v-model:value="createTimeStart" placeholder="请选择开始时间" style="flex: 1" :show-time="{ format: 'HH:mm' }" />
+            <span class="date-separator">至</span>
+            <a-date-picker v-model:value="createTimeEnd" placeholder="请选择结束时间" style="flex: 1" :disabledDate="disabledEndDate" :show-time="{ format: 'HH:mm' }" />
+          </div>
+        </template>
+      </vxe-form>
+    </header>
+    <main class="flex-auto overflow-hidden">
+      <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents" :loading="loading">
+        <template #toolbar-extra>
+          <vxe-button style="margin-right: 12px" icon="vxe-icon-repeat" circle @click="refresh(page)"></vxe-button>
+        </template>
+
+        <!-- 动态操作按钮 -->
+        <template #actionButtons="{ row }">
+          <vxe-button-group mode="text">
+            <!-- 健康分析报告 - 根据 healthAnalysisReportId 判断 -->
+            <vxe-button v-if="row.healthAnalysisReportId" status="primary" @click="openHistoryPreviewHandle(row)"> 健康分析报告 </vxe-button>
+            <!-- 脉象报告 - 根据 pulseAnalysisReportId 判断 -->
+            <vxe-button v-if="row.pulseAnalysisReportId" status="primary" @click="pulseAnalysisReport(row)"> 脉象报告 </vxe-button>
+            <!-- 舌面分析报告 - 根据 tonguefaceAnalysisReportId 判断 -->
+            <vxe-button v-if="row.tonguefaceAnalysisReportId" status="primary" @click="tongueAndFaceAnalysisReport(row)"> 舌面分析报告 </vxe-button>
+          </vxe-button-group>
+        </template>
+      </vxe-grid>
+    </main>
+    <footer class="flex-none">
+      <vxe-pager
+        v-model:current-page="page"
+        v-model:page-size="pageSize"
+        :total="total"
+        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']"
+      />
+    </footer>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.page-container {
+  padding: 0 24px;
+  max-height: var(--page-main-container);
+}
+.date-range-container {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  width: 100%;
+
+  .vxe-input {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .date-separator {
+    color: #666;
+    font-size: 14px;
+    font-weight: 500;
+    white-space: nowrap;
+    padding: 0 8px;
+  }
+}
+</style>

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

@@ -115,6 +115,7 @@ const gridOptions = reactive<VxeGridProps<PatientReportModel>>({
   columns: [
     { type: 'seq', width: 70, fixed: 'left' },
     { field: 'name', title: '姓名', minWidth: 80 },
+    { field: 'phone', title: '手机号码', minWidth: 80 },
     { field: 'gender', title: '性别', minWidth: 40, formatter: 'gender' },
     { field: 'age', title: '年龄', minWidth: 40, formatter: ({ cellValue }) => (cellValue ? `${cellValue}岁` : '') },
     { field: 'diagnosis', title: '诊断', minWidth: 40 },

+ 11 - 16
src/request/api/account.api.ts

@@ -69,22 +69,17 @@ export function getMenusMethod(account: AccountModel) {
   return request.Get<AccountModel, any[]>(`/system/menu/getRouters`, {
     headers: { Authorization: account.token },
     transform(data) {
-      // data.push({
-      //   path: '/equipment',
-      //   meta: { title: '设备管理' },
-      //   children: [
-      //       {
-      //         path: 'registe',
-      //         meta: { title: '设备登记' },
-      //       },
-            // {
-            //   path: 'configured',
-            //   meta: { title: '调理方案配置' },
-            // },
-      //   ],
-      // });
-      // console.log(data, 'push之后的data', transformMenus(data));
+    // data[5]?.children?.push({
+    //     path: 'configured',
+    //     meta: { title: '辨识仪配置' }
+    //   },
+    //   {
+    //     path: 'reportManagement',
+    //     meta: { title: '报告管理' }
+    //   },
+    // );
+    //   console.log(data, 'push之后的data', transformMenus(data));
       return { ...account, menus: transformMenus(data) };
-    },
+    }
   });
 }

+ 36 - 1
src/request/api/device.api.ts

@@ -1,5 +1,5 @@
 import type { List } from '@/model';
-import type { EquirementModel } 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>) {
@@ -44,5 +44,40 @@ export function updateDeviceRegisterOrganizationMethod(data: any) {
   );
 }
 
+// 一体机配置分页列表
+export function deviceManageMethod(page: number, size: number, query?: Record<string, any>) {
+  return request.Post<List<DeviceManageModel>>('/fdhb-pc/deviceManage/tablet/sets/page', query ?? {}, {
+    hitSource: /plan$/, // 匹配失效源
+    params: { pageNum: page, pageSize: size },
+  });
+}
+// 根据一体机id获取一体机配置详情
+export function getDeviceManageDetailMethod(data: Partial<DeviceManageModel>) {
+  return request.Post(`/fdhb-pc/deviceManage/tablet/sets/detail/${data.id}`, {
+    name: 'get-device-manage-detail',
+    cacheFor: null,
+  });
+}
+// 修改一体机配置
+export function updateDeviceManageMethod(data: Partial<DeviceManageModel>) {
+  return request.Post(`/fdhb-pc/deviceManage/tablet/sets/update`, data, {
+    name: 'update-device-manage',
+    cacheFor: null,
+  });
+}
+// 批量修改一体机配置
+export function batchUpdateDeviceManageMethod(data: Partial<DeviceManageModel>) {
+  return request.Post(`/fdhb-pc/deviceManage/tablet/sets/batchUpdate`, data, {
+    name: 'batch-update-device-manage',
+    cacheFor: null,
+  });
+}
+// 一体机报告分页列表
+export function deviceReportMethod(page: number, size: number, query?: Record<string, any>) {
+  return request.Post<List<DeviceReportModel>>('/fdhb-pc/deviceManage/tablet/report/page', query ?? {}, {
+    hitSource: /plan$/, // 匹配失效源
+    params: { pageNum: page, pageSize: size },
+  });
+}
 
 

+ 9 - 0
src/request/api/report.api.ts

@@ -249,6 +249,15 @@ export function getPatientHealthRecordMethod(id: string) {
     },
   });
 }
+// 获取舌面分析报告 getTofRepDetailById
+export function getTonguefaceAnalysisReportMethod(id: string) {
+  return request.Get<HealthReportVO, HealthReportDTO>(`/fdhb-pc/analysisManage/getTofRepDetailById`, {
+    params: { tonguefaceAnalysisReportId: id },
+    transform(data) {
+      return fromHealthReport(data);
+    },
+  });
+}
 
 export function getPatientHealthIndicatorsMethod(patientId: string) {
   return request.Get<HealthIndicatorVO[], any[]>(`/fdhb-pc/patientQuota/getQuovalRecord`, {

+ 5 - 0
src/router/index.ts

@@ -5,6 +5,7 @@ import { createRouter, createWebHistory } from 'vue-router';
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [
+    { path: '/aio', component: () => import('@/pages/aio/FlowConfigDemo.vue') },
     { path: '/login', component: () => import('@/pages/login.vue') },
     {
       path: '/',
@@ -42,11 +43,15 @@ const router = createRouter({
             { path: 'issueService', component: () => import(`@/pages/index/care/issueService.vue`), },
             { path: 'conditioningRecord', component: () => import(`@/pages/index/care/conditioningRecord.vue`), },
             { path: 'configured', component: () => import(`@/pages/index/care/configured.vue`), },
+            
           ],
         },
         {
           path: 'equipment', children: [
             { path: 'registe', component: () => import(`@/pages/index/equipment/registe.vue`) },
+            { path: 'configured', component: () => import(`@/pages/index/equipment/configured.vue`) },
+            { path: 'reportManagement', component: () => import(`@/pages/index/equipment/reportManagement.vue`) },
+            
           ],
         },   
         {

+ 1 - 1
src/service/CareProgress.vue

@@ -286,7 +286,7 @@ const progressTextMap: Record<string, string> = {
           <vxe-column type="seq" title="序号" width="80" />
           <vxe-column field="operateTime" title="操作时间" />
           <vxe-column field="operateBy" title="操作人" />
-          <vxe-column field="feedback" title="上次治疗反馈" />
+          <vxe-column field="feedback" title="治疗备注" />
           <vxe-column field="acuPointNames" title="穴位" />
         </vxe-table>
         <div class="mt-3">

+ 3 - 3
src/widgets/AnalysisReportWidget.vue

@@ -64,10 +64,10 @@ const { data: indicator, loading: indicatorLoading } = useWatcher(
     <a-card class="card" size="small" title="舌象分析">
       <a-descriptions :column="3">
         <a-descriptions-item>
-          <a-image :width="200" :src="report.upImg" :preview="true" />
+          <a-image :width="200" :src="report.upImg" :preview="true" v-if="report.upImg" />
         </a-descriptions-item>
         <a-descriptions-item>
-          <a-image :width="200" :src="report.downImg" :preview="true" />
+          <a-image :width="200" :src="report.downImg" :preview="true" v-if="report.downImg" />
         </a-descriptions-item>
         <a-descriptions-item></a-descriptions-item>
       </a-descriptions>
@@ -140,7 +140,7 @@ const { data: indicator, loading: indicatorLoading } = useWatcher(
     <a-card class="card no-bordered" size="small" title="面象分析" v-if="report.faceAnalysisResult">
       <a-descriptions :column="3">
         <a-descriptions-item>
-          <a-image :width="200" :src="report.faceImg" :preview="true" />
+          <a-image :width="200" :src="report.faceImg" :preview="true" v-if="report.faceImg" />
         </a-descriptions-item>
         <a-descriptions-item :span="2">{{ report.faceAnalysisResult }}</a-descriptions-item>
       </a-descriptions>

+ 2 - 2
src/widgets/ReportAnalysisWidgetTongue.vue

@@ -27,10 +27,10 @@ const exceptionData = computed(() => {
   <a-card class="card" size="small" :title="props.title" :loading="props.loading">
     <a-descriptions :column="3" size="small">
       <a-descriptions-item>
-        <a-image :width="200" :src="props.dataset.upImg" :preview="true" />
+        <a-image :width="200" :src="props.dataset.upImg" :preview="true" v-if="props.dataset.upImg" />
       </a-descriptions-item>
       <a-descriptions-item>
-        <a-image :width="200" :src="props.dataset.downImg" :preview="true" />
+        <a-image :width="200" :src="props.dataset.downImg" :preview="true" v-if="props.dataset.downImg" />
       </a-descriptions-item>
       <a-descriptions-item v-if="props.dataset.tongueAnalysisResult">
         <div class="flex flex-col">

+ 3 - 3
src/widgets/ReportCardWidget.vue

@@ -210,15 +210,15 @@ defineExpose({
                 <a-descriptions-item>{{ props.dataset.sublingualVein.standardValue }}</a-descriptions-item>
               </template>
             </a-descriptions>
-            <a-image :width="200" :height="200" :src="props.dataset.upImg" :preview="true" />
-            <a-image :width="200" :height="200" :src="props.dataset.downImg" :preview="true" />
+            <a-image :width="200" :height="200" :src="props.dataset.upImg" :preview="true" v-if="props.dataset.upImg" />
+            <a-image :width="200" :height="200" :src="props.dataset.downImg" :preview="true" v-if="props.dataset.downImg" />
           </a-space>
         </a-card>
         <template v-if="props.dataset?.faceAnalysisResult">
           <a-card class="card background" size="small" title="面象分析" :loading="props.loading">
             <a-space align="start" class="w-full analysis-wrapper">
               <div>{{ props.dataset.faceAnalysisResult }}</div>
-              <a-image :width="200" :height="200" :src="props.dataset.faceImg" :preview="true" />
+              <a-image :width="200" :height="200" :src="props.dataset.faceImg" :preview="true" v-if="props.dataset.faceImg" />
             </a-space>
           </a-card>
         </template>