4 次代碼提交 3dedaddfd1 ... 9308f2b79b

作者 SHA1 備註 提交日期
  kumu 9308f2b79b 添加二维码遮罩 1 年之前
  kumu bac8ad4c2a 添加 舌面象分析报告 页 1 年之前
  kumu e513f6cc07 修复导航栏 标题不居中 1 年之前
  kumu c5695b320d 获取报告添加患者id 1 年之前

+ 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=/

+ 1 - 0
src/assets/images/mini-program.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="1732606154666" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4264" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m256.717 460.186a151.962 151.962 0 0 1-87.347 65.74 83.251 83.251 0 0 1-24.474 4.096 29.082 29.082 0 0 1 0-58.163 15.667 15.667 0 0 0 6.451-1.229 91.443 91.443 0 0 0 55.91-40.96 75.264 75.264 0 0 0 11.06-39.628c0-45.978-42.496-83.866-94.31-83.866a105.267 105.267 0 0 0-51.2 13.414 81.92 81.92 0 0 0-43.725 70.452v244.224a138.445 138.445 0 0 1-72.704 120.422 159.642 159.642 0 0 1-79.77 20.48c-84.378 0-153.6-63.488-153.6-142.029a136.192 136.192 0 0 1 19.763-69.837 151.962 151.962 0 0 1 87.347-65.74 85.914 85.914 0 0 1 24.474-4.096 29.082 29.082 0 1 1 0 58.163 15.667 15.667 0 0 0-6.451 1.229 95.949 95.949 0 0 0-55.91 40.96 75.264 75.264 0 0 0-11.06 39.628c0 45.978 42.496 83.866 94.925 83.866a105.267 105.267 0 0 0 51.2-13.414 81.92 81.92 0 0 0 43.622-70.452V390.35a138.752 138.752 0 0 1 72.807-120.525 151.245 151.245 0 0 1 79.155-21.504c84.378 0 153.6 63.488 153.6 142.029a136.192 136.192 0 0 1-19.763 69.837z" fill="#00B240" p-id="4265"></path></svg>

+ 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>

+ 2 - 2
src/modules/report/NavBar.vue

@@ -27,10 +27,10 @@ watchEffect(() => {
   const path = route.fullPath;
   isScheme.value = path.endsWith('scheme');
   tabbar.value = isScheme.value ? [
-    { key: 'screen', label: '返回首页', icon: NavHome, select: NavHomeSelect },
+    // { key: 'screen', label: '返回首页', icon: NavHome, select: NavHomeSelect },
     { key: 'report', label: '健康报告', icon: NavScheme, select: NavSchemeSelect },
   ] : [
-    { key: 'screen', label: '返回首页', icon: NavHome, select: NavHomeSelect },
+    // { key: 'screen', label: '返回首页', icon: NavHome, select: NavHomeSelect },
     { key: 'scheme', label: '调理方案', icon: NavScheme, select: NavSchemeSelect },
     // { key: 'print', label: '打印', icon: NavPrint, select: NavPrintSelect },
     { key: 'mini', label: '小程序', icon: NavPrint, select: NavPrintSelect },

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

@@ -0,0 +1,113 @@
+<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, error } = useRequest(analysisResultsMethod, {
+  initialData: {
+    tongue: {},
+    face: {},
+  },
+}).onSuccess(({ data }) => {
+  if (data?.miniProgramURL) panelOpen(data.payLock ? panelProps.anchors[1] : 100);
+}).onError(async ({ error }) => {
+  await Dialog.show({
+    message: error.message,
+    theme: 'round-button',
+    showCancelButton: false,
+    confirmButtonText: '好的',
+    width: '350px',
+  });
+  await router.replace(`/camera`);
+});
+
+const panelHeight = ref(0);
+const panelProps = reactive({
+  anchors: [0, window.innerWidth],
+  contentDraggable: false,
+  lockScroll: true,
+
+  show: true,
+});
+const panelOpen = (min?: number) => {
+  if (panelProps.show && min) panelProps.anchors[0] = min;
+  panelHeight.value = panelProps.anchors[1];
+};
+
+const scrollable = computed(() => panelHeight.value < panelProps.anchors[1] || panelHeight.value === 0);
+
+</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" v-if="!error" title :row="3" :loading>
+        <div class="flex-auto" :class="{ 'overflow-y-auto': scrollable }">
+          <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 :style="{ height: panelHeight + 'px' }"><!--补偿面板打开高度--></div>
+        </div>
+      </van-skeleton>
+    </div>
+    <van-floating-panel v-model:height="panelHeight" v-bind="panelProps">
+      <Transition>
+        <div class="panel-content">
+          <img
+            class="size-full object-contain"
+            v-if="panelHeight === panelProps.anchors[1] || panelHeight > panelProps.anchors[0] * 1.5"
+            :src="data.miniProgramURL"
+            alt="小程序码"
+          />
+          <div class="flex justify-center items-center" v-else @click="panelOpen()">
+            <img class="h-10 w-10" src="@/assets/images/mini-program.svg" alt="小程序" />
+            <span class="text-lg ml-2">小程序</span>
+          </div>
+        </div>
+      </Transition>
+    </van-floating-panel>
+  </div>
+</template>
+<style scoped lang="scss">
+.report-wrapper {
+  .panel-content {
+    padding: 0 var(--van-floating-panel-header-height) var(--van-floating-panel-header-height);
+  }
+
+  .v-enter-active,
+  .v-leave-active {
+    transition: opacity 0.5s ease;
+  }
+
+  .v-enter-from,
+  .v-leave-to {
+    opacity: 0;
+  }
+}
+
+.overflow-y-auto {
+  overflow-y: auto;
+}
+</style>
+<style lang="scss">
+.report-wrapper .card {
+  padding: 24px;
+  border-radius: 24px;
+  box-shadow: inset 0 0 80px 0 #34a76b60;
+}
+</style>

+ 84 - 67
src/modules/report/report.page.vue

@@ -7,18 +7,20 @@ import { getReportMethod, updateReportMethod } from '@/request/api/report.api';
 import { useRouteParams }                      from '@vueuse/router';
 import { useRequest, useWatcher }              from 'alova/client';
 
+import NavHomeSelect from '@/assets/images/nav-home.select.png?url';
+
 
 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,
-});
+}).onSuccess(({ data }) => {
+  if (data?.miniProgramURL) panelOpen(data.payLock ? panelProps.anchors[1] : 100);
+})
 
 const { loading: uploading, send: upload } = useRequest(() => updateReportMethod(id.value, data.value), {
   immediate: false,
@@ -56,15 +58,26 @@ async function miniProgram() {
     Notify.warning(`未获取到小程序地址,请联系管理员或重试`);
     return;
   }
-  ReportPreview = defineAsyncComponent(() => import('./ReportPreview.vue'));
-  reportPreviewProps.mode = 'img';
-  reportPreviewProps.title = '微信 扫一扫';
-  reportPreviewProps.url = url;
-  reportPreviewProps.show = true;
+  panelOpen();
 }
+
+const panelHeight = ref(0);
+const panelProps = reactive({
+  anchors: [0, window.innerWidth],
+  contentDraggable: false,
+  lockScroll: true,
+
+  show: false,
+});
+const panelOpen = (min?: number) => {
+  if (panelProps.show && min) panelProps.anchors[0] = min;
+  panelHeight.value = panelProps.anchors[1];
+};
+
+const scrollable = computed(() => panelHeight.value < panelProps.anchors[1] || panelHeight.value === 0);
 </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">
@@ -72,11 +85,15 @@ async function miniProgram() {
           健康分析报告
         </div>
       </div>
-      <div class="grow shrink-0 h-full min-w-16"></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="flex-auto" :class="{ 'overflow-y-auto': scrollable }">
           <div class="my-6 text-primary text-2xl text-center">报告日期:{{ data.date }}</div>
           <div class="card m-6 text-lg">
             <div class="card__title text-primary text-3xl font-bold"></div>
@@ -127,56 +144,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">
@@ -207,16 +186,27 @@ async function miniProgram() {
       </van-skeleton>
       <NavBar class="flex-none" :uploading @print="print" @mini="miniProgram"></NavBar>
       <Component :is="ReportPreview" v-bind="reportPreviewProps" v-model:show="reportPreviewProps.show"></Component>
+
+      <van-floating-panel v-model:height="panelHeight" v-bind="panelProps">
+        <Transition>
+          <div class="panel-content">
+            <img
+              class="size-full object-contain"
+              v-if="panelHeight === panelProps.anchors[1] || panelHeight > panelProps.anchors[0] * 1.5"
+              :src="data.miniProgramURL"
+              alt="小程序码"
+            />
+            <div class="flex justify-center items-center" v-else @click="panelOpen()">
+              <img class="h-10 w-10" src="@/assets/images/mini-program.svg" alt="小程序" />
+              <span class="text-lg ml-2">小程序</span>
+            </div>
+          </div>
+        </Transition>
+      </van-floating-panel>
     </div>
   </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;
@@ -228,4 +218,31 @@ async function miniProgram() {
 .text-grey {
   color: #e3e3e3;
 }
+
+.report-wrapper {
+  .panel-content {
+    padding: 0 var(--van-floating-panel-header-height) var(--van-floating-panel-header-height);
+  }
+
+  .v-enter-active,
+  .v-leave-active {
+    transition: opacity 0.5s ease;
+  }
+
+  .v-enter-from,
+  .v-leave-to {
+    opacity: 0;
+  }
+}
+
+.overflow-y-auto {
+  overflow-y: auto;
+}
 </style>
+<style lang="scss">
+.report-wrapper .card {
+  padding: 24px;
+  border-radius: 24px;
+  box-shadow: inset 0 0 80px 0 #34a76b60;
+}
+</style>

+ 7 - 1
src/modules/report/scheme.page.vue

@@ -5,6 +5,8 @@ import { getReportSchemeMethod } from '@/request/api/report.api';
 import { useRouteParams }        from '@vueuse/router';
 import { useWatcher }            from 'alova/client';
 
+import NavHomeSelect from '@/assets/images/nav-home.select.png?url';
+
 
 const id = useRouteParams<string>('id');
 const { data, loading } = useWatcher(() => getReportSchemeMethod(id.value), [ id ], {
@@ -23,7 +25,11 @@ const { data, loading } = useWatcher(() => getReportSchemeMethod(id.value), [ id
           调理方案
         </div>
       </div>
-      <div class="grow shrink-0 h-full min-w-16"></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">
       <!--{{ data }}-->

+ 1 - 2
src/pages/register.page.vue

@@ -231,7 +231,7 @@ useWatcher(
   {
     immediate: true,
     async middleware(_, next) {
-      if ( _.method.config.params?.scanCode ) {
+      if ( scan.value ) {
         let scanToastRef: any;
         try {
           scanToastRef = Toast.loading(100, { message: '加载中' });
@@ -240,7 +240,6 @@ useWatcher(
           scanToastRef?.close?.();
         }
       }
-      console.log('12->', _);
     },
   },
 ).onSuccess(({ data }) => {

+ 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: `[分析结果] 照片不符合检测要求,图片不是舌头(请拍摄带有舌头的、清晰的彩色照!)` };
+      },
+    }
+  );
+}

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

@@ -4,19 +4,19 @@ import type { QuestionnaireStorage }                  from '../model';
 import { fromQuestionnaireData, toQuestionnaireData } from '../model';
 
 
-const visitor = useVisitor();
+const Visitor = useVisitor();
 let storage: Pick<QuestionnaireStorage, 'dialogId'> & { questions: QuestionnaireStorage['questions'][] } = { questions: [] };
 
 export function questionnaireMethod(data = []) {
   if ( !data?.length ) { storage = { questions: [] }; }
   return HTTP.Post(
-    `/fdhb-tablet/dialogueManage/dialog/${ visitor.patientId }/${ visitor.resultId }`,
-    toQuestionnaireData(data, storage),
+    `/fdhb-tablet/dialogueManage/dialog/${ Visitor.patientId }/${ Visitor.resultId }`,
+    { ...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;
       },
     }

+ 5 - 2
src/request/api/report.api.ts

@@ -1,9 +1,11 @@
 import HTTP                                  from '../alova';
 import { fromReportData, fromSchemeRequest } from '../model';
+import { useVisitor } from '@/stores';
 
+const Visitor = useVisitor();
 
 export function getReportMethod(id: string) {
-  const params = { healthAnalysisReportId: id };
+  const params = { healthAnalysisReportId: id, patientId: Visitor.patientId };
   return HTTP.Get(`/fdhb-tablet/analysisManage/getHealRepDetailById`, {
     params,
     transform(data, headers) { return fromReportData(<any> data); },
@@ -15,12 +17,13 @@ export function updateReportMethod(id: string, data: Record<string, any>) {
     healthAnalysisReportId: id,
     constitutionGroupImg: data?.constitutionGroupImg,
     factorItemRadarImg: data?.factorItemRadarImg,
+    patientId: Visitor.patientId
   };
   return HTTP.Post(`/fdhb-tablet/analysisManage/upConFacImgById`, params, {});
 }
 
 export function getReportSchemeMethod(id: string) {
-  const params = { healthAnalysisReportId: id };
+  const params = { healthAnalysisReportId: id, patientId: Visitor.patientId };
   return HTTP.Get(`/fdhb-tablet/analysisManage/getCondProgDetailById`, {
     params,
     transform(data: any, headers) {

+ 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';

+ 9 - 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: [
@@ -58,6 +65,8 @@ export function fromReportData(data: Record<string, any>) {
     factorItemRadarImg: data?.factorItemRadarImg,
     reportURL: data?.reportPdfUrl,
     miniProgramURL: data?.appletImg,
+
+    payLock: data?.payLock ?? false,
   };
 }
 

+ 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' },

+ 5 - 0
src/themes/fix.scss

@@ -0,0 +1,5 @@
+.page-header {
+  > div:not(.grow-\[3\]) {
+    flex-grow: 0;
+  }
+}

+ 1 - 0
src/themes/index.scss

@@ -3,6 +3,7 @@
 @tailwind utilities;
 
 @import "./vant";
+@import "./fix";
 
 .page {
   &-container {