Explorar el Código

添加 舌面象分析报告 页

kumu hace 6 meses
padre
commit
bac8ad4c2a

+ 1 - 1
.env/.env.development

@@ -1,3 +1,3 @@
-REQUEST_API_PROXY_URL=http://121.43.162.141:8080
+REQUEST_API_PROXY_URL=https://wx.hzliuzhi.com/manager
 
 SIX_REQUEST_BASE=/

+ 85 - 0
src/components/AnalysisComponent.vue

@@ -0,0 +1,85 @@
+<script setup lang="ts">
+import type { AnalysisModel } from '@/request/model';
+
+type Props = AnalysisModel & { title?: string };
+
+const { table, exception, result = null, cover = [], title = '分析' } = defineProps<Props>();
+const attrs = useAttrs();
+watchEffect(() => {
+  console.log(result, '12-->', attrs);
+});
+</script>
+
+<template>
+  <van-skeleton class="analysis" :row="5" :loading="result == null">
+    <slot>
+      <div class="card m-6 text-lg">
+        <div class="card__title mb-3 text-primary text-2xl font-bold">{{ title }}</div>
+        <slot name="content" :result="result" :cover="cover">
+          <div class="card__content">
+            <div
+              class="grid grid-rows-1 gap-8"
+              :class="cover?.length > 1 ? 'grid-cols-2' : 'grid-cols-1'"
+              v-if="cover?.length"
+            >
+              <img class="m-auto w-2/4 object-contain" v-for="src in cover" :key="src" :src="src" alt="分析图像" />
+            </div>
+            <table class="mt-8 mb-2 w-full table-auto border border-collapse border-primary">
+              <thead>
+                <tr>
+                  <th
+                    class="py-4 px-2 text-primary border border-primary"
+                    v-for="(value, i) in table?.columns"
+                    :key="i"
+                    v-html="value"
+                  ></th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="item in table?.data" :key="item[0]" :data-exception="item.exception">
+                  <td
+                    class="py-4 px-2 border border-primary text-center text-grey"
+                    v-for="(value, i) in item"
+                    :key="i"
+                    v-html="value"
+                  ></td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </slot>
+      </div>
+      <slot name="exception">
+        <div class="grid grid-rows-1 grid-cols-2 gap-8 m-6">
+          <div class="card text-lg" v-for="item in exception">
+            <div class="card__title mb-3 text-primary text-2xl font-bold">{{ item.title }}</div>
+            <div class="card__content">
+              <div class="flex my-6 justify-center">
+                <img v-if="item.cover" class="flex-none w-2/4 object-scale-down" :src="item.cover" alt="分析异常图像" />
+                <div class="flex-none ml-8">
+                  <div
+                    class="my-2 px-4 py-2 rounded-lg border border-primary-400 text-primary"
+                    v-for="value in item.tags"
+                    :key="value"
+                  >
+                    {{ value }}
+                  </div>
+                </div>
+              </div>
+              <div class="my-2 text-grey" v-for="description in item.descriptions" :key="description.value">
+                <label>{{ description.label }}</label>
+                <span v-html="description.value"></span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </slot>
+    </slot>
+  </van-skeleton>
+</template>
+
+<style scoped lang="scss">
+tr[data-exception='true'] td:nth-of-type(2) {
+  color: #f87171;
+}
+</style>

+ 64 - 0
src/modules/report/report-analyse.page.vue

@@ -0,0 +1,64 @@
+<script setup lang="ts">
+import { useRequest } from 'alova/client';
+import { analysisResultsMethod } from '@/request/api/analysis.api';
+import { Dialog } from '@/platform';
+import NavHomeSelect from '@/assets/images/nav-home.select.png?url';
+
+const router = useRouter();
+
+const { data, loading } = useRequest(analysisResultsMethod, {
+  initialData: {
+    tongue: {},
+    face: {},
+  },
+}).onError(async ({ error }) => {
+  await Dialog.show({
+    message: error.message,
+    theme: 'round-button',
+    showCancelButton: false,
+    confirmButtonText: '好的',
+    width: '350px',
+  });
+  await router.replace(`/camera`);
+});
+</script>
+<template>
+  <div class="report-wrapper">
+    <div class="page-header flex py-4 px-4">
+      <div class="grow shrink-0 h-full min-w-16"></div>
+      <div class="grow-[3] shrink mx-2 flex flex-col justify-center overflow-hidden">
+        <div class="font-bold text-3xl text-nowrap text-center tracking-wide overflow-ellipsis overflow-hidden">
+          舌面象分析报告
+        </div>
+      </div>
+      <div class="grow shrink-0 h-full min-w-16">
+        <router-link :to="{ path: '/screen' }" replace>
+          <img class="size-8 object-scale-down" :src="NavHomeSelect" alt="返回首页" />
+        </router-link>
+      </div>
+    </div>
+    <div class="page-content flex flex-col overflow-hidden">
+      <van-skeleton class="flex-auto" title :row="3" :loading>
+        <div class="flex-auto overflow-y-auto">
+          <div class="my-6 text-primary text-2xl text-center" v-if="data.date">报告日期:{{ data.date }}</div>
+          <AnalysisComponent title="舌象分析" v-bind="data.tongue"></AnalysisComponent>
+          <AnalysisComponent title="面象分析" v-bind="data.face"></AnalysisComponent>
+        </div>
+      </van-skeleton>
+    </div>
+  </div>
+</template>
+<style scoped lang="scss">
+.report-wrapper {
+  .panel-content {
+    padding: 0 var(--van-floating-panel-header-height) var(--van-floating-panel-header-height);
+  }
+}
+</style>
+<style lang="scss">
+.report-wrapper .card {
+  padding: 24px;
+  border-radius: 24px;
+  box-shadow: inset 0 0 80px 0 #34a76b60;
+}
+</style>

+ 20 - 59
src/modules/report/report.page.vue

@@ -12,10 +12,8 @@ const id = useRouteParams<string>('id');
 const { data, loading } = useWatcher(() => getReportMethod(id.value), [ id ], {
   initialData: {
     descriptionsTable: { column: [], data: [] },
-    tongueTable: { column: [], data: [] },
-    tongueException: [],
-    tongueAnalysis: {},
-    faceAnalysis: {},
+    tongue: {},
+    face: {},
   },
   immediate: true,
 });
@@ -64,7 +62,7 @@ async function miniProgram() {
 }
 </script>
 <template>
-  <div>
+  <div class="report-wrapper">
     <div class="page-header flex py-4 px-4">
       <div class="grow shrink-0 h-full min-w-16"></div>
       <div class="grow-[3] shrink mx-2 flex flex-col justify-center overflow-hidden">
@@ -127,56 +125,18 @@ async function miniProgram() {
               </table>
             </div>
           </div>
-          <div class="card m-6 text-lg">
-            <div class="card__title mb-3 text-primary text-2xl font-bold">舌象分析</div>
-            <div class="card__content">
-              <table class="mt-8 mb-2 w-full table-auto border border-collapse  border-primary">
-                <thead>
-                <tr>
-                  <th class="py-4 px-2 text-primary border border-primary"
-                      v-for="(value, i) in data.tongueTable.column" :key="i"
-                      v-html="value"
-                  >
-                  </th>
-                </tr>
-                </thead>
-                <tbody>
-                <tr v-for="item in data.tongueTable?.data" :key="item[0]">
-                  <td class="py-4 px-2 border border-primary text-center text-grey"
-                      v-for="(value, i) in item" :key="i"
-                      v-html="value"
-                  ></td>
-                </tr>
-                </tbody>
-              </table>
-            </div>
-          </div>
-          <div class="grid grid-rows-1 grid-cols-2 gap-8 m-6">
-            <div class="card text-lg" v-for="item in data.tongueException">
-              <div class="card__title mb-3 text-primary text-2xl font-bold">{{ item.title }}</div>
-              <div class="card__content">
-                <div class="flex my-6 justify-center">
-                  <img class="flex-none w-2/4 object-scale-down" :src="item.cover" alt="舌象">
-                  <div class="flex-none ml-8">
-                    <div class="my-2 px-4 py-2 rounded-lg border border-primary-400 text-primary"
-                         v-for="value in item.tags" :key="value"
-                    >{{ value }}
-                    </div>
-                  </div>
+          <AnalysisComponent title="舌象分析" v-bind="data.tongue" :cover="[]"></AnalysisComponent>
+          <AnalysisComponent title="面象分析" v-bind="data.face">
+            <template #content="{result, cover}">
+              <div class="card__content flex">
+                <div class="flex-auto text-grey mt-6">{{ result }}</div>
+                <div class="flex-none w-2/4 max-h-96 ml-4">
+                  <img class="size-full object-scale-down"  v-for="src in cover" :key="src" :src="src" alt="面象">
                 </div>
-                <div class="my-2 text-grey" v-for="value in item.descriptions" :key="value">{{ value }}</div>
-              </div>
-            </div>
-          </div>
-          <div class="card m-6 text-lg" v-if="data.faceAnalysis?.['结果']">
-            <div class="card__title mb-3 text-primary text-2xl font-bold">面象分析</div>
-            <div class="card__content flex">
-              <div class="flex-auto text-grey mt-6">{{ data.faceAnalysis?.[ '结果' ] }}</div>
-              <div class="flex-none w-2/4 max-h-96 ml-4">
-                <img class="size-full object-scale-down" :src="data.faceAnalysis?.['面象']" alt="面象">
               </div>
-            </div>
-          </div>
+            </template>
+            <template #exception><div><!--空占位符--></div></template>
+          </AnalysisComponent>
           <div class="card m-6 text-lg" v-if="data['中医证素']?.length">
             <div class="card__title mb-3 text-primary text-2xl font-bold">中医证素</div>
             <div class="card__content">
@@ -211,12 +171,6 @@ async function miniProgram() {
   </div>
 </template>
 <style scoped lang="scss">
-.card {
-  padding: 24px;
-  border-radius: 24px;
-  box-shadow: inset 0 0 80px 0 #34a76b60;
-}
-
 .van-button.decorate {
   font-size: 20px;
   height: 62px;
@@ -229,3 +183,10 @@ async function miniProgram() {
   color: #e3e3e3;
 }
 </style>
+<style lang="scss">
+.report-wrapper .card {
+  padding: 24px;
+  border-radius: 24px;
+  box-shadow: inset 0 0 80px 0 #34a76b60;
+}
+</style>

+ 29 - 0
src/request/api/analysis.api.ts

@@ -0,0 +1,29 @@
+import { useVisitor } from '@/stores';
+import HTTP from '@/request/alova';
+import { fromFaceAnalysisModel, fromTongueAnalysisModel } from '@/request/model';
+
+const Visitor = useVisitor();
+
+export function analysisResultsMethod(visitor = Visitor) {
+  return HTTP.Post(
+    `/fdhb-tablet/dialogueManage/dialog/${Visitor.patientId}/${Visitor.resultId}`,
+    { asyncTongueResult: false, questions: [] },
+    {
+      meta: { ignoreException: true },
+      transform(data: Record<string, any>, headers) {
+        data = data.nextQuestions?.find((item: any) => item.classify === 'tongue_result');
+        if (data) {
+          return {
+            date: data?.tonguefaceAnalysisReportDate,
+            miniProgramURL: data?.tonguefaceAnalysisReportAppletImg,
+            tongue: fromTongueAnalysisModel(data),
+            face: fromFaceAnalysisModel(data),
+
+            payLock: data?.payLock ?? true
+          };
+        }
+        throw { message: `[分析结果] 照片不符合检测要求,图片不是舌头(请拍摄带有舌头的、清晰的彩色照!)` };
+      },
+    }
+  );
+}

+ 2 - 2
src/request/api/questionnaire.api.ts

@@ -11,12 +11,12 @@ export function questionnaireMethod(data = []) {
   if ( !data?.length ) { storage = { questions: [] }; }
   return HTTP.Post(
     `/fdhb-tablet/dialogueManage/dialog/${ Visitor.patientId }/${ Visitor.resultId }`,
-    toQuestionnaireData(data, storage),
+    { ...toQuestionnaireData (data, storage), asyncTongueResult: true },
     {
       meta: { ignoreException: true },
       transform(data: Record<string, any>, headers) {
         const { storage: _storage, model } = fromQuestionnaireData(data);
-        storage = _storage;
+        storage = _storage as any;
         return model;
       },
     }

+ 104 - 0
src/request/model/analysis.model.ts

@@ -0,0 +1,104 @@
+export function fromTongueAnalysisModel(data: Record<string, any>): AnalysisModel {
+  const exception: AnalysisException[] = [];
+  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,
+    result: data?.tongueAnalysisResult ?? data?.tongue ?? '',
+    cover: Object.assign([c1, c2].filter(Boolean), {
+      ['舌上']: c1,
+      ['舌下']: c2,
+    }),
+  };
+}
+
+export function fromFaceAnalysisModel(data: Record<string, any>): AnalysisModel {
+  const exception: AnalysisException[] = [];
+  const fromTongueException = 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: [
+        fromTongueException(data?.faceColor, '面色'),
+        fromTongueException(data?.mainColor, '主色'),
+        fromTongueException(data?.shine, '光泽'),
+        fromTongueException(data?.leftBlackEye, '左黑眼圈'),
+        fromTongueException(data?.rightBlackEye, '右黑眼圈'),
+        fromTongueException(data?.lipColor, '唇色'),
+        fromTongueException(data?.eyeContact, '眼神'),
+        fromTongueException(data?.leftEyeColor, '左目色'),
+        fromTongueException(data?.rightEyeColor, '右目色'),
+        fromTongueException(data?.hecticCheek, '两颧红'),
+        fromTongueException(data?.noseFold, '鼻褶'),
+        fromTongueException(data?.cyanGlabella, '眉间/鼻柱青色'),
+        fromTongueException(data?.faceSkinDefects, '面部皮损'),
+      ],
+    },
+    exception,
+    result: data?.faceAnalysisResult ?? data?.face ?? '',
+    cover: Object.assign([c1, c2, c3].filter(Boolean), {
+      ['正面']: c1,
+      ['左面']: c2,
+      ['右面']: c3,
+    }),
+  };
+}
+
+export interface AnalysisModel {
+  table: {
+    columns: string[];
+    data: (string[] & { exception: boolean })[];
+  };
+  exception: AnalysisException[];
+  cover: string[];
+  result: string;
+}
+
+export interface AnalysisException {
+  title: string;
+  cover?: string;
+  description?: string;
+  descriptions: { label: string; value: string }[];
+  tags: string[];
+}
+
+function fromAnalysisException(exception: AnalysisException[], $title = (label: string, value: string) => value) {
+  return function (data: { actualList?: Record<string, any>[]; standardValue?: string }, label: string) {
+    let is = false;
+    const values =
+      data?.actualList?.map((item) => {
+        let title: string = item?.actualValue ?? '';
+        const suffix = item?.contrast ?? 's';
+        if (suffix !== 's') {
+          title += ` (${suffix || ''}) `;
+          is = true;
+          exception.push({
+            title: $title(label, title),
+            cover: item.splitImage,
+            descriptions: [
+              item.features ? { label: '【特征】', value: item.features } : null,
+              item.clinicalSignificance ? { label: '【临床意义】', value: item.clinicalSignificance } : null,
+            ].filter((v) => !!v),
+            tags: item.attrs ?? [],
+          });
+        }
+        return title;
+      }) ?? [];
+    return Object.assign([label, values.join('<br>'), data?.standardValue ?? ''], { exception: is });
+  };
+}

+ 1 - 0
src/request/model/index.ts

@@ -1,4 +1,5 @@
 export * from './register.model';
+export * from './analysis.model';
 export * from './questionnaire.model';
 export * from './report.model';
 export * from './scheme.model';

+ 7 - 0
src/request/model/report.model.ts

@@ -1,3 +1,5 @@
+import { fromFaceAnalysisModel, fromTongueAnalysisModel } from '@/request/model/analysis.model';
+
 export function fromReportData(data: Record<string, any>) {
   const tongueException: ReportTongueException[] = [];
   const fromTongueException = fromReportTongueExceptionData.bind(null, tongueException);
@@ -25,6 +27,11 @@ export function fromReportData(data: Record<string, any>) {
         [ '环境适应能力', data?.constitutionGroupAdaptability ],
       ],
     },
+    
+    tongue: fromTongueAnalysisModel(data),
+    face: fromFaceAnalysisModel(data),
+
+
     tongueTable: {
       column: [ '舌象维度', '检测结果', '标准值' ],
       data: [

+ 1 - 0
src/router/index.ts

@@ -9,6 +9,7 @@ const router = createRouter({
     { path: '/camera', component: () => import('@/modules/camera/camera.page.vue'), meta: { title: '拍摄' } },
     { path: '/camera/result', component: () => import('@/modules/camera/camera-result.page.vue'), meta: { title: '拍摄完成' } },
     { path: '/questionnaire', component: () => import('@/modules/questionnaire/page.vue'), meta: { title: '问卷' } },
+    { path: '/report/analysis', component: () => import('@/modules/report/report-analyse.page.vue'), meta: { title: '舌面象分析报告' } },
     { path: '/report/:id/scheme', component: () => import('@/modules/report/scheme.page.vue'), meta: { title: '调理方案' } },
     { path: '/report/:id', component: () => import('@/modules/report/report.page.vue'), meta: { title: '健康分析报告' } },
     { path: '/', redirect: '/screen' },