فهرست منبع

8. 添加 舌象分析记录

cc12458 1 سال پیش
والد
کامیت
f8db3726ec

+ 38 - 2
src/api/diagnosis.js

@@ -1,5 +1,5 @@
 import request from '@/utils/request.js'
-import {fromTongueAndFaceAnalysisModel} from "@/model/tongue-analysis,model";
+import {fromAnalysisModel} from "@/model/tongue-analysis.model";
 
 // 获取就诊病人信息
 export function getPatiiensMsg(data) {
@@ -451,7 +451,43 @@ export function getTongueAndFaceAnalysis(data) {
         params: {recordsId: data.recordsId},
     }).then(res => {
         if (res.ResultCode == 0) {
-            return res.Data = fromTongueAndFaceAnalysisModel(res.Data), res;
+            const tongue = fromAnalysisModel('tongue', res.Data);
+            const face = fromAnalysisModel('face', res.Data);
+            return res.Data = {
+                id: res.Data.recordsid,
+                time: res.Data.recordstime,
+                tongue,
+                face,
+                cover: [tongue ? tongue.cover : null, face ? face.cover : null].filter(Boolean).flatMap(item => item),
+            }, res;
+        } else {
+            throw res
+        }
+    })
+}
+
+export function getTongueAndFaceAnalysisRecords(patientId, page = 1, limit = 6) {
+    // patientId = '8d12c55e-7a29-45f3-804d-ed12b0e27660'
+    return request({
+        url: '/outpatient/electronicmedicalrecordMgr/getRecordTfs',
+        method: 'post',
+        params: {patientId, page, limit},
+    }).then(res => {
+        if (res.ResultCode == 0) {
+            return {
+                total: res.Data.TotalRecordCount,
+                data: res.Data.Items.map(item => {
+                    const tongue = fromAnalysisModel('tongue', item);
+                    const face = fromAnalysisModel('face', item);
+                    return {
+                        id: item.recordsid,
+                        time: item.recordstime,
+                        tongue,
+                        face,
+                        cover: [tongue ? tongue.cover : null, face ? face.cover : null].filter(Boolean).flatMap(item => item),
+                    }
+                }),
+            }
         } else {
             throw res
         }

+ 1 - 0
src/assets/face.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1734592524739" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8401" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M110.9 326.9c17 0 30.9-13.8 30.9-30.9v-92.6c0-34.1 27.6-61.7 61.7-61.7H296c17 0 30.9-13.8 30.9-30.9 0-17-13.8-30.9-30.9-30.9h-92.6C135.3 80 80 135.3 80 203.4V296c0 17 13.8 30.9 30.9 30.9zM296 882.3h-92.6c-34.1 0-61.7-27.6-61.7-61.7V728c0-17-13.8-30.9-30.9-30.9-17 0-30.8 13.9-30.8 30.9v92.6C80 888.7 135.3 944 203.4 944H296c17 0 30.9-13.8 30.9-30.9 0-17-13.9-30.8-30.9-30.8z m617.1-185.2c-17 0-30.9 13.8-30.9 30.9v92.6c0 34.1-27.6 61.7-61.7 61.7H728c-17 0-30.9 13.8-30.9 30.9 0 17 13.8 30.9 30.9 30.9h92.6c68.2 0 123.4-55.3 123.4-123.4V728c0-17-13.8-30.9-30.9-30.9zM820.6 80H728c-17 0-30.9 13.8-30.9 30.9 0 17 13.8 30.9 30.9 30.9h92.6c34.1 0 61.7 27.6 61.7 61.7V296c0 17 13.8 30.9 30.9 30.9 17 0 30.9-13.8 30.9-30.9v-92.6C944 135.3 888.7 80 820.6 80zM296 357.7v61.7c0 17 13.8 30.9 30.9 30.9 17 0 30.9-13.8 30.9-30.9v-61.7c0-17-13.8-30.9-30.9-30.9-17.1 0.1-30.9 13.9-30.9 30.9z m432 92.6c17 0 30.9-13.8 30.9-30.9v-61.7c0-17-13.8-30.9-30.9-30.9s-30.9 13.8-30.9 30.9v61.7c0 17.1 13.9 30.9 30.9 30.9zM481.1 604.6c51.1 0 92.6-41.5 92.6-92.6V357.7s-3.6-30.9-30.9-30.9c-24.7 0-30.9 30.9-30.9 30.9V512c0 17-13.8 30.9-30.9 30.9 0 0-30.9 4.2-30.9 30.9 0.2 22.4 31 30.8 31 30.8z m216 30.8c-18.9 0-39.8 30.9-39.8 30.9-31.1 37.4-77.4 61.7-129.9 61.7s-98.8-24.3-129.9-61.7c0 0-20.1-31.4-39.8-30.9-34.8 1-34.9 30.9-34.9 30.9 38.8 73.4 115.8 123.4 204.6 123.4s165.8-50 204.6-123.4c0 0 1-30.9-34.9-30.9z" fill="#5386F6" p-id="8402"></path></svg>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
src/assets/tongue.svg


+ 5 - 0
src/components/Propup.vue

@@ -4,6 +4,7 @@
       :visible.sync="showDialog"
       :show-close="false"
       :width="width"
+      :modal="modal"
       :close-on-click-modal="false"
       :top="distanceTop"
       :destroy-on-close="true"
@@ -72,6 +73,10 @@ export default {
       type: Boolean,
       default: true
     },
+    modal: {
+      type: Boolean,
+      default: true
+    },
     destroyOnClose: {
       type: Boolean,
       default: false

+ 0 - 61
src/model/tongue-analysis,model.js

@@ -1,61 +0,0 @@
-/**
- * 解析舌象异常数据
- * @param {{title: string; cover: string; descriptions: string[]; tags: string[];}[]} exception
- * @param {{actualList: Record<string, any>[]}} [data]
- * @return {string}
- */
-function fromReportTongueExceptionData(exception, data) {
-    if (data == null || !Array.isArray(data.actualList)) return '';
-    return data.actualList.map(item => {
-        if (!item) return '';
-        let title = item.actualValue || '';
-        const suffix = item.contrast || 's';
-        if (suffix !== 's') {
-            title += ` (${suffix || ''}) `;
-            exception.push({
-                title, cover: item.splitImage,
-                descriptions: [
-                    item.features ? `【特征】${item.features}` : '',
-                    item.clinicalSignificance ? `【临床意义】${item.clinicalSignificance}` : '',
-                ].filter(Boolean),
-                tags: item.attrs ?? [],
-            });
-        }
-        return title;
-    }).join('<br>');
-}
-
-/**
- * 解析舌面象数据
- * @param {Record<string, any>} data
- */
-export function fromTongueAndFaceAnalysisModel(data) {
-    if (data == null || typeof data !== 'object') data = {};
-    const tongueException = [];
-    const fromTongueException = fromReportTongueExceptionData.bind(null, tongueException);
-    const fromTongueValue = (data) => data && typeof data === 'object' ? data.standardValue || '' : '';
-
-    return {
-        tongueTable: {
-            column: ['舌象维度', '检测结果', '标准值'],
-            data: [
-                ['舌色', fromTongueException(data.tongueColor), fromTongueValue(data.tongueColor)],
-                ['苔色', fromTongueException(data.tongueCoatingColor), fromTongueValue(data.tongueCoatingColor)],
-                ['舌形', fromTongueException(data.tongueShape), fromTongueValue(data.tongueShape)],
-                ['苔质', fromTongueException(data.tongueCoating), fromTongueValue(data.tongueCoating)],
-                ['津液', fromTongueException(data.bodyFluid), fromTongueValue(data.bodyFluid)],
-                ['舌下', fromTongueException(data.sublingualVein), fromTongueValue(data.sublingualVein)],
-            ],
-        },
-        tongueException,
-        tongueAnalysis: {
-            ['结果']: data.tongueAnalysisResult,
-            ['舌上']: data.upImg,
-            ['舌下']: data.downImg,
-        },
-        faceAnalysis: {
-            ['结果']: data.faceAnalysisResult,
-            ['面象']: data.faceImg,
-        },
-    }
-}

+ 159 - 0
src/model/tongue-analysis.model.js

@@ -0,0 +1,159 @@
+/**
+ * @typedef {Object} AnalysisModel
+ * @property {Object} table
+ * @property {string[]} table.columns
+ * @property {Array} table.data
+ * @property {string} table.data.exception
+ * @property {boolean=} table.data.invalid
+ * @property {AnalysisException[]} exception
+ * @property {Array} exceptionGroup
+ * @property {string} exceptionGroup.key
+ * @property {AnalysisException[]} exceptionGroup.exception
+ * @property {string[]} cover
+ * @property {string} result
+ */
+
+/**
+ * @typedef {Object} AnalysisException
+ * @property {string} title
+ * @property {string=} cover
+ * @property {string=} description
+ * @property {Array} descriptions
+ * @property {string} descriptions.label
+ * @property {string} descriptions.value
+ * @property {string[]} tags
+ */
+
+/**
+ * @param {"tongue" | "face"} mode
+ * @param {Object} data
+ * @returns {AnalysisModel | null}
+ */
+export function fromAnalysisModel(mode, data) {
+  if (!data || typeof data !== 'object') return null;
+  let model;
+  switch (mode) {
+    case 'tongue':
+      model = fromTongueAnalysisModel(data);
+      break;
+    case 'face':
+      model = fromFaceAnalysisModel(data);
+      break;
+  }
+  return model;
+}
+
+/**
+ * @param {Object} data
+ * @returns {AnalysisModel}
+ */
+function fromTongueAnalysisModel(data) {
+  const exception = [];
+  const fromTongueException = fromAnalysisException(exception);
+  const c1 = data.upImg || data.tongueImgUrl;
+  const c2 = data.downImg || data.tongueBackImgUrl;
+  return {
+    table: {
+      columns: ['舌象维度', '检测结果', '标准值'],
+      data: [
+        fromTongueException(data.tongueColor, '舌色'),
+        fromTongueException(data.tongueCoatingColor, '苔色'),
+        fromTongueException(data.tongueShape, '舌形'),
+        fromTongueException(data.tongueCoating, '苔质'),
+        fromTongueException(data.bodyFluid, '津液'),
+        fromTongueException(data.sublingualVein, '舌下'),
+      ],
+    },
+    exception,
+    exceptionGroup: [],
+    result: data.tongueAnalysisResult || data.tongue || '',
+    cover: Object.assign([c1, c2].filter(Boolean), {
+      ['舌上']: c1,
+      ['舌下']: c2,
+    }),
+  };
+}
+
+/**
+ * @param {Object} data
+ * @returns {AnalysisModel}
+ */
+function fromFaceAnalysisModel(data) {
+  const exception = [];
+  const fromFaceException = fromAnalysisException(exception, (label, value) => `${label}${value}`);
+  const c1 = data.faceImg || data.faceImgUrl;
+  const c2 = data.faceLeft || data.faceLeftImgUrl;
+  const c3 = data.faceRight || data.faceRightImgUrl;
+  return {
+    table: {
+      columns: ['面象维度', '检测结果', '标准值'],
+      data: [
+        fromFaceException(data.faceColor, '面色'),
+        fromFaceException(data.mainColor, '主色'),
+        fromFaceException(data.shine, '光泽'),
+        fromFaceException(data.leftBlackEye, '左黑眼圈'),
+        fromFaceException(data.rightBlackEye, '右黑眼圈'),
+        fromFaceException(data.lipColor, '唇色'),
+        fromFaceException(data.eyeContact, '眼神'),
+        fromFaceException(data.leftEyeColor, '左目色'),
+        fromFaceException(data.rightEyeColor, '右目色'),
+        fromFaceException(data.hecticCheek, '两颧红'),
+        fromFaceException(data.noseFold, '鼻褶'),
+        fromFaceException(data.cyanGlabella, '眉间/鼻柱青色'),
+        fromFaceException(data.faceSkinDefects, '面部皮损'),
+      ],
+    },
+    exception,
+    exceptionGroup: [],
+    result: data.faceAnalysisResult || data.face || '',
+    cover: Object.assign([c1, c2, c3].filter(Boolean), {
+      ['正面']: c1,
+      ['左面']: c2,
+      ['右面']: c3,
+    }),
+  };
+}
+
+/**
+ * @param {AnalysisException[]} exception
+ * @param {Function=} $title
+ * @returns {Function}
+ */
+function fromAnalysisException(exception, $title = (label, value) => value) {
+  return function (data, label) {
+    if (!data || typeof data !== 'object') data = {actualList: [], standardValue: ''};
+
+    const rowProps = {exception: false, invalid: false};
+
+    const standard = data.standardValue || '';
+    const values = Array.isArray(data.actualList) ? data.actualList.map(item => {
+      const props = {
+        value: item.actualValue || '',
+        exception: null, invalid: false,
+      }
+      const suffix = item.contrast || 's';
+      if (props.value.endsWith('不符合检测要求')) {
+        props.invalid = rowProps.invalid = true;
+      } else if (suffix !== 's') {
+        if (suffix !== 'r') props.value += ` (${suffix || ''}) `;
+        rowProps.exception = true;
+        props.exception = {
+          title: $title(label, props.value),
+          cover: item.splitImage,
+          descriptions: [
+            item.features ? {label: '【特征】', value: item.features} : null,
+            item.clinicalSignificance ? {label: '【临床意义】', value: item.clinicalSignificance} : null,
+          ].filter((v) => !!v),
+          tags: item.attrs || [],
+        }
+        exception.push(props.exception);
+      }
+      return props;
+    }) : [];
+    return Object.assign([
+      [{value: label}],
+      values,
+      [{value: standard}],
+    ], rowProps);
+  };
+}

+ 11 - 11
src/views/diagnosis/Prescribing.vue

@@ -1078,13 +1078,13 @@
     <Popup
         :showDialog="showTongueAnalysis"
         @cancle="showTongueAnalysis=false"
-        title="分析详情"
+        title="萧山区中医健康微管家"
         :showBtns="false"
-        width="700px"
+        width="70%"
         distanceTop="5vh"
     >
-      <div slot="body">
-        <TongueAnalysis :dataset="tongueAndFaceAnalysis"></TongueAnalysis>
+      <div style="height: 100%" slot="body">
+        <TongueAnalysis v-if="showTongueAnalysis" :dataset="tongueAndFaceAnalysis"></TongueAnalysis>
       </div>
     </Popup>
 
@@ -1146,7 +1146,7 @@ import medAdress from "./components/medAddress.vue";
 import medAdressNew from "./components/medAddressNew.vue";
 import {
   getPatiensBasisM,
-  getTongueAndFaceAnalysis,
+  getTongueAndFaceAnalysisRecords,
   addRecipe,
   getRecipeShowData,
   getRecipeDataByid,
@@ -3174,12 +3174,12 @@ export default {
     async getPatientBasisTongueAndFaceAnalysis() {
       this.tongueAndFaceLoading = true;
       try {
-        let res = await getTongueAndFaceAnalysis({
-          recordsId: this.getPatiensInfo ? this.getPatiensInfo.pid : ""
-          // recordsId: '8ed63872-6d1f-4fe6-b170-4d36e677a744',
-        });
-        this.tongueAndFaceAnalysis = res.Data && (res.Data.tongueAnalysis.结果 || res.Data.faceAnalysis.结果) ? res.Data : null;
-      } catch (e) {}
+        const get = getTongueAndFaceAnalysisRecords.bind(null, this.getPatiensInfo ? this.getPatiensInfo.pid : "")
+        let result = await get(1, 3);
+        this.tongueAndFaceAnalysis = result.total > 0 ? {...result, get} : null;
+      } catch (e) {
+        this.$message.error(e.ResultInfo);
+      }
       this.tongueAndFaceLoading = false;
     },
     // 获取处方预览数据

+ 275 - 124
src/views/diagnosis/components/tongue-analysis.vue

@@ -1,80 +1,139 @@
 <template>
-  <!-- 舌象分析详情 -->
-  <div class="tongue-analysis">
-    <template v-if="dataset && (dataset.tongueAnalysis.结果 || dataset.faceAnalysis.结果)">
-      <template v-if="dataset.tongueAnalysis && dataset.tongueAnalysis.结果">
-        <el-card class="tongue-exception-wrapper" v-if="showException">
-          <div slot="header" class="card-header-container">
-            <span>异常舌象分析</span>
-            <el-button size="small" type="primary" plain style="float: right;" @click="showException=false">
-              舌象分析
-            </el-button>
-          </div>
-          <div class="card-body-container">
-            <el-card v-for="item in dataset.tongueException" :key="item.title" class="inner" size="small">
-              <div class="card__title">{{ item.title }}</div>
-              <div class="card__content">
-                <div style="display: flex;">
-                  <el-image :src="item.cover" lazy fit="scale-down" :preview-src-list="[item.cover]"></el-image>
-                  <div>
-                    <el-tag v-for="value in item.tags" :key="value" type="danger">{{ value }}</el-tag>
-                  </div>
-                </div>
-                <p class="my-2 text-grey" v-for="value in item.descriptions" :key="value">{{ value }}</p>
-              </div>
-            </el-card>
-          </div>
-        </el-card>
-        <el-card v-else>
-          <div slot="header" class="card-header-container">
-            <span>舌象分析</span>
-            <el-button
-                v-if="dataset.tongueException && dataset.tongueException.length"
-                size="small" type="danger" plain style="float: right;" @click="showException=true">
-              异常舌象分析
-            </el-button>
-          </div>
-          <div class="card-body-container">
-            <el-table v-if="dataset.tongueTable" class="result-wrapper" :data="dataset.tongueTable.data" border
-                      size="small">
-              <el-table-column
-                  v-for="(col, index) in dataset.tongueTable.column" :key="col"
-                  :prop="index + ''" :label="col" align="center"
-              >
-                <template v-slot="{row, column}">
-                  <div v-html="row[index]"></div>
-                </template>
-              </el-table-column>
-            </el-table>
-            <div v-else class="result-wrapper">{{ dataset.tongueAnalysis.结果 }}</div>
-            <div class="picture-wrapper">
-              <el-image :src="dataset.tongueAnalysis.舌上" lazy fit="scale-down"
-                        :preview-src-list="preview"></el-image>
-              <el-image :src="dataset.tongueAnalysis.舌下" lazy fit="scale-down"
-                        :preview-src-list="preview"></el-image>
+  <!-- 舌象分析记录 -->
+  <div class="tongue-analysis-wrapper" :style="cssVars">
+    <el-tabs ref="tab" type="card">
+      <el-tab-pane v-for="(pane, index) in list" :key="pane.id" :label="pane.time">
+        <div class="area" v-if="pane.tongue.result">
+          <div class="analysis-image">
+            <div>
+              <img src="../../../assets/tongue.svg" alt="">
+              <el-popover title="分析结果" :content="pane.tongue.result" width="200">
+                <span slot="reference" style="cursor: pointer;">舌象分析结果</span>
+              </el-popover>
             </div>
+            <el-image v-for="item in pane.tongue.cover" :src="item" :preview-src-list="pane.cover"></el-image>
           </div>
-        </el-card>
-      </template>
-      <el-card v-if="dataset.faceAnalysis && dataset.faceAnalysis.结果">
-        <div slot="header" class="card-header-container">
-          <span>面象分析</span>
+          <table>
+            <thead>
+            <tr>
+              <th v-for="(value, i) in pane.tongue.table.columns" :key="i" v-html="value"></th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr v-for="(item, i) in pane.tongue.table.data" :key="i"
+                :data-exception="item.exception"
+                :data-invalid="item.invalid"
+            >
+              <td v-for="(col, i) in item" :key="i">
+                <div style="margin: 2px 0;" v-for="item in col" :key="item.value">
+                  <el-popover v-if="item.exception" :title="item.exception.title"
+                              trigger="hover"
+                              popper-class="analysis-exception-popover">
+                    <div class="card__content">
+                      <div style="margin-bottom: 10px;">
+                        <img v-if="item.exception.cover" class="flex-none w-2/4 object-scale-down"
+                             :src="item.exception.cover" alt="分析异常图像"/>
+                        <el-tag style="margin-top: 2px;margin-right: 2px;" type="danger"
+                                v-for="value in item.exception.tags" :key="value">{{ value }}
+                        </el-tag>
+                      </div>
+                      <div style="margin-top: 4px;" v-for="description in item.exception.descriptions"
+                           :key="description.value">
+                        <label>{{ description.label }}</label>
+                        <span v-html="description.value"></span>
+                      </div>
+                    </div>
+                    <span slot="reference" style="cursor: pointer; color: #f87171;">{{ item.value }}</span>
+                  </el-popover>
+                  <template v-else>{{ item.value }}</template>
+                </div>
+              </td>
+            </tr>
+            </tbody>
+          </table>
         </div>
-        <div class="card-body-container">
-          <div class="result-wrapper">{{ dataset.faceAnalysis.结果 }}</div>
-          <div class="picture-wrapper">
-            <el-image :src="dataset.faceAnalysis.面象" lazy fit="scale-down" :preview-src-list="preview"></el-image>
+        <div class="area" v-if="pane.face.result">
+          <div class="analysis-image">
+            <div>
+              <img src="../../../assets/face.svg" alt="">
+              <el-popover title="分析结果" :content="pane.face.result" width="200">
+                <span slot="reference" style="cursor: pointer;">面象分析结果</span>
+              </el-popover>
+            </div>
+            <el-image v-for="item in pane.face.cover" :src="item" :preview-src-list="pane.cover"></el-image>
+            <div></div>
           </div>
+          <table>
+            <thead>
+            <tr>
+              <th v-for="(value, i) in pane.face.table.columns" :key="i" v-html="value"></th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr v-for="(item, i) in pane.face.table.data" :key="i"
+                :data-exception="item.exception"
+                :data-invalid="item.invalid"
+            >
+              <td v-for="(col, i) in item" :key="i">
+                <div style="margin: 2px 0;" v-for="item in col" :key="item.value">
+                  <el-popover v-if="item.exception" :title="item.exception.title"
+                              trigger="hover"
+                              popper-class="analysis-exception-popover">
+                    <div class="card__content">
+                      <div style="margin-bottom: 10px;">
+                        <img v-if="item.exception.cover" class="flex-none w-2/4 object-scale-down"
+                             :src="item.exception.cover" alt="分析异常图像"/>
+                        <div>
+                          <el-tag type="danger" v-for="value in item.exception.tags" :key="value">{{ value }}</el-tag>
+                        </div>
+                      </div>
+                      <div style="margin-top: 4px;" v-for="description in item.exception.descriptions"
+                           :key="description.value">
+                        <label>{{ description.label }}</label>
+                        <span v-html="description.value"></span>
+                      </div>
+                    </div>
+                    <span slot="reference" style="cursor: pointer; color: #f87171;">{{ item.value }}</span>
+                  </el-popover>
+                  <template v-else>{{ item.value }}</template>
+                </div>
+              </td>
+            </tr>
+            </tbody>
+          </table>
         </div>
-      </el-card>
-    </template>
-    <el-empty v-else-if="!loading" description="暂无数据"></el-empty>
+        <el-button style="width: 100%;" type="primary"
+                   :loading="pane.id === caseId" :disabled="!!caseId && caseId !== pane.id"
+                   @click="handle(pane.id)">查看病历详情信息
+        </el-button>
+      </el-tab-pane>
+    </el-tabs>
+    <Popup
+        :showDialog="showCase"
+        @cancle="closeCase"
+        :showBtns="false"
+        width="70%"
+        distanceTop="5vh"
+        :destroyOnClose="true"
+        title=""
+        :modal="false"
+    >
+      <div style="height: 100%" slot="body">
+        <outpatientRecord :info="caseItem"></outpatientRecord>
+      </div>
+    </Popup>
   </div>
 </template>
 <script>
-import {getTongueAndFaceAnalysis} from "@/api/diagnosis.js";
+
+import {getRecordDetail} from "@/api/diagnosis";
+import outpatientRecord from "@/components/ui/outpatientRecords.vue";
+import Popup from "@/components/Propup.vue";
+
+let _keydown_
 
 export default {
+  components: {Popup, outpatientRecord},
   props: {
     dataset: {
       type: Object,
@@ -83,97 +142,189 @@ export default {
   },
   data() {
     return {
-      showException: false
+      page: 1,
+      limit: 3,
+      list: [],
+
+      cardProps: {width: 0},
+
+      caseId: '',
+      caseItem: null,
+      showCase: false,
     };
   },
   computed: {
-    preview: function () {
-      return [
-        this.dataset.tongueAnalysis.舌上,
-        this.dataset.tongueAnalysis.舌下,
-        this.dataset.faceAnalysis.面象,
-      ].filter(Boolean);
+    cssVars() {
+      return {
+        '--w': `${this.cardProps.width}px`,
+      }
+    }
+  },
+  methods: {
+    async load(length) {
+      const {data} = await this.dataset.get(this.page, length);
+      this.list = data;
+    },
+
+    async handle(id) {
+      this.caseId = id;
+      const res = await getRecordDetail({id}).catch(() => {});
+      if (res.ResultCode == 0) {
+        this.caseItem = res.Data;
+        this.showCase = true;
+        const fn = (event) => {
+          event.stopPropagation();
+          event.preventDefault();
+          this.closeCase();
+        };
+        document.addEventListener('keydown', fn, {once: true});
+        _keydown_ = () => document.removeEventListener('keydown', fn);
+      } else {
+        this.$message.error(res.ResultInfo);
+      }
+    },
+    // 关闭就诊 详情
+    closeCase() {
+      this.showCase = false;
+      this.caseId = '';
+      this.caseItem = null;
+      if (typeof _keydown_ === 'function') _keydown_();
+    },
+    _followScroll() {
+      const nav = this.$refs.tab.$el.querySelector(`.el-tabs__nav`);
+      const content = this.$refs.tab.$el.querySelector(`.el-tabs__content`);
+
+      this.cardProps.observer = new MutationObserver(mutations => {
+        try { content.style.transform = mutations[0].target.style.transform; } catch (e) { }
+      })
+      this.cardProps.observer.observe(nav, {attributes: true});
+    }
+  },
+  beforeMount() {
+    if (this.dataset) {
+      this.list = this.dataset.data;
+      this.load(this.dataset.total || this.limit);
     }
   },
+  mounted() {
+    const rect = this.$el.parentElement.getBoundingClientRect();
+    const width = Math.max(Math.floor(rect.width / 3), 250);
+    this.$set(this.cardProps, 'width', width);
+
+    this._followScroll();
+  },
+  unmount() {
+    if (this.cardProps.observer) this.cardProps.observer.disconnect();
+  }
 };
 </script>
 <style lang="scss" scoped>
-.tongue-analysis {
-  .el-card:not(.inner) {
-    margin: 24px 0;
+.tongue-analysis-wrapper {
+  height: 100%;
+  overflow: hidden;
 
-    &:first-of-type { margin-top: 0 }
+  .area {
+    margin-bottom: 12px;
 
-    &:last-of-type { margin-bottom: 0 }
+    .analysis-image {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 8px;
 
-    &.tongue-exception-wrapper {
-      .card-body-container {
-        display: grid;
-        grid-template-rows: repeat(1, minmax(0, 1fr));
-        grid-template-columns: repeat(2, minmax(0, 1fr));
-        gap: 12px;
-      }
+      > div {
+        flex: 0 0 30%;
+        max-height: 128px;
 
-      .card__title {
-        padding: 6px 12px;
-        color: #F56C6C;
-      }
+        &:nth-of-type(1) {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: space-evenly;
 
-      .card__content {
-        padding: 6px 12px 12px;
+          img {
+            width: 50%;
+            height: 50%;
+          }
+        }
       }
 
-      .el-image {
-        width: 100px;
-        height: 100px;
-        margin-right: 12px;
-        margin-bottom: 12px;
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: scale-down;
       }
+    }
 
-      .inner ::v-deep .el-card__body {
-        padding: 0;
-      }
+    table {
+      width: 100%;
+      border-collapse: collapse;
+      text-align: center;
 
-      p {
-        line-height: 1.75;
+      th {
+        font-size: 16px;
+        padding: 10px;
+        border: 1px solid #E4E7ED;
+      }
 
-        &:not(:last-of-type) { margin-bottom: 8px; }
+      td {
+        padding: 8px;
+        border: 1px solid #E4E7ED;
+        min-height: 40px;
       }
     }
   }
 
-  .card-header-container {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    height: 100%;
+  ::v-deep {
+    .el-tabs--card {
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+
+      .el-tabs__header { flex: none; }
+
+      .el-tabs__content { flex: auto; }
+    }
   }
 
-  .card-body-container {
-    display: flex;
+  ::v-deep {
+    .el-tabs__item,
+    .el-tab-pane {
+      width: var(--w);
+    }
 
-    .result-wrapper {
-      flex: auto;
+    .el-tabs__header {
+      .el-tabs__item {
+        text-align: center;
+      }
     }
 
-    .picture-wrapper {
-      flex: none;
+    .el-tabs__content {
       display: flex;
-      flex-direction: column;
-      justify-content: space-evenly;
-      margin-left: 12px;
-      max-width: 100px;
+      width: max-content;
+      padding: 0 20px;
 
-      .el-image {
-        width: 100px;
-        height: 100px;
+      .el-tab-pane {
+        flex: none;
+        display: block !important;
+        padding: 0 12px;
+        overflow-y: auto;
+        box-sizing: border-box;
       }
     }
   }
+}
 
-  ::v-deep .el-card__header {
-    padding: 0 20px;
-    height: 56px;
-  }
+tr[data-invalid="true"] td:nth-of-type(2) {
+  color: #9ca3af;
 }
 </style>
+<style lang="scss">
+.analysis-exception-popover {
+  width: 245px;
+
+  img {
+    width: 100%;
+    object-fit: scale-down;
+  }
+}
+</style>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است