浏览代码

Merge branch 'hotfix/pulse'

cc12458 3 月之前
父节点
当前提交
74d3ed2d6c

+ 1 - 1
.prettierrc.json

@@ -3,6 +3,6 @@
   "semi": true,
   "tabWidth": 2,
   "singleQuote": true,
-  "printWidth": 120,
+  "printWidth": 180,
   "trailingComma": "es5"
 }

+ 4 - 20
@types/bridge.d.ts

@@ -1,11 +1,4 @@
-interface HandSummary {
-  chi: string;
-  cun: string;
-  guan: string;
-  summary: string[];
-}
-
-type HandKeys = 'chen' | 'chi' | 'fu' | 'hua' | 'kong' | 'ruan' | 'shi' | 'shu' | 'xi' | 'xian';
+import type { SimplePulseModel } from './pulse';
 
 interface ScanData {
   code: string;
@@ -20,20 +13,9 @@ export interface BridgeEventMap {
 export class Bridge extends EventTarget {
   public static getInstance(): Bridge;
 
-  public static pulse(userId: string): Promise<{
-    measureId: string;
-    summaryLabel: {
-      summary?: HandSummary['summary'];
-      left?: HandSummary;
-      right?: HandSummary;
-    };
-    summaryValue: Record<HandKeys, number[]>;
-    time: string;
-
+  public static pulse(userId: string): Promise<SimplePulseModel & { report: string; time: string; } & {
     appId?: string;
     userId?: string;
-    url: string;
-    report: string;
   }>;
 
   public static print(): Promise<void>;
@@ -47,6 +29,8 @@ export class Bridge extends EventTarget {
    */
   addEventListener<T extends keyof BridgeEventMap>(type: T, listener: (event: BridgeEventMap[T]) => void, options?: boolean | AddEventListenerOptions): () => void;
   removeEventListener<T extends keyof BridgeEventMap>(type: T, listener: (event: BridgeEventMap[T]) => void, options?: boolean | AddEventListenerOptions): () => void;
+
+  postMessage(...args: any[]): Promise<void>;
 }
 
 export interface globalAIO {

+ 20 - 0
@types/pulse.d.ts

@@ -0,0 +1,20 @@
+interface HandSummary {
+  chi: string;
+  cun: string;
+  guan: string;
+  summary: string[];
+}
+
+type HandKeys = 'chen' | 'chi' | 'fu' | 'hua' | 'kong' | 'ruan' | 'shi' | 'shu' | 'xi' | 'xian';
+
+export interface SimplePulseModel {
+  measureId: string;
+  summaryLabel: {
+    summary?: HandSummary['summary'];
+    left?: HandSummary;
+    right?: HandSummary;
+  };
+  summaryValue: Record<HandKeys, number[]>;
+  hand?: number;
+  url?: string;
+}

+ 1 - 0
@types/vite-env.d.ts

@@ -0,0 +1 @@
+declare const __FORBID_AUTO_PROCESS_PULSE_AGENCY__: boolean

+ 1 - 0
package.json

@@ -7,6 +7,7 @@
     "dev": "vite",
     "build": "run-p type-check \"build-aio {@}\" --",
     "build-aio": "vite build --base=/aio/",
+    "build-aio-CQ": "vite build --base=/aio/ -- --legacy-pulse-agency",
     "preview": "vite preview --base=/aio/ --mode development",
     "device": "run-s build-aio preview",
     "type-check": "vue-tsc --build --force",

+ 278 - 0
src/components/AnalysisPulseComponent.vue

@@ -0,0 +1,278 @@
+<script setup lang="ts">
+import type { SimplePulseModel } from '../../@types/pulse';
+
+import { createReusableTemplate, tryOnMounted } from '@vueuse/core';
+import { useRequest } from 'alova/client';
+import { getPulseAgentMethod } from '@/request/api/pulse.api';
+
+import { getURLSearchParamsByUrl } from '@/tools';
+
+import HandLeft from '@/assets/images/pulse-hand-left.png?url';
+import HandRight from '@/assets/images/pulse-hand-right.png?url';
+import { useVisitor } from '@/stores';
+
+type Props = Partial<SimplePulseModel> & { title?: string; results?: string; disabled?: boolean; simple?: boolean };
+
+const { title = '分析', disabled = false, simple = false, results, ..._report } = defineProps<Props>();
+const slots = defineSlots();
+
+const Visitor = useVisitor();
+const report = computed(() => (unref(results) ? Object.assign(_report, { results }) : Visitor.pulseReport));
+
+const parseUrl = (value?: string) => {
+  const [url, query = ''] = (value ?? report.value?.url)?.split('report.html') ?? [];
+  const params = getURLSearchParamsByUrl(query);
+  if (url && params.has('appId') && params.has('access_session')) {
+    const replace /* 绘制 3D 跨域 */ = () => [
+      `https://wx.hzliuzhi.com/mz/hybrid/`,
+    ].includes(url) ? 'https://hybrid.reborn-tech.com/' : url;
+    return {
+      appId: params.get('appId')!!,
+      token: params.get('access_session')!!,
+      url: `${replace()}report.html#/`,
+    };
+  }
+};
+
+const stringifyUrl = (panel: 'left' | 'right', simple = false, overall = true) => {
+  const fragment = overall ? [`overall-${panel}`] : [];
+  if (!simple) fragment.push(`${panel}-hand`);
+
+  if (!fragment.length) return '';
+
+  const value = fragment.map((value) => `${value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}=true`).join('&');
+  return `${agent.value!.url}single?appId=${agent.value!.appId}&access_session=${agent.value!.token}&mid=${report.value?.measureId}&${value}`;
+};
+
+const { data: agent, loading, send: getPulseAgent } = useRequest(() => getPulseAgentMethod(), {
+  initialData: void 0,
+  immediate: false,
+});
+
+const card = useTemplateRef('card');
+const panelConfig = reactive({
+  height: 0,
+  anchors: [0],
+});
+const offset = computed(() => panelConfig.anchors[panelConfig.anchors.length - 1] - panelConfig.height);
+const getHeightAndScrollTop = () => {
+  const el = card.value;
+  if (!el) return;
+  el.scrollIntoView({ behavior: 'instant', block: 'start' });
+  const rect = el.getBoundingClientRect();
+  const maxHeight = window.innerHeight - rect.top;
+  const height = maxHeight - rect.height;
+  panelConfig.anchors = [0, height, maxHeight];
+  panelConfig.height = height;
+};
+
+const preview = reactive({
+  clickMainPanelPreviewable: true,
+  leftUrl: void 0 as string | void,
+  rightUrl: void 0 as string | void,
+  leftPanelUrl: void 0 as string | void,
+  rightPanelUrl: void 0 as string | void,
+  showLeftPanel: false,
+  showRightPanel: false,
+});
+
+const load = async (panel: 'left' | 'right' | boolean = true, simple = false) => {
+  if (loading.value) return;
+
+  if (!agent.value) {
+    try {
+      const { promise, ...resolvers } = Promise.withResolvers<string>();
+      await window.bridge.postMessage('pulse:url:get', report.value?.measureId, resolvers);
+      agent.value = parseUrl(await promise);
+    } catch (e) {
+      const value = await getPulseAgent().catch(() => void 0);
+      if (!value) agent.value = parseUrl();
+    }
+  }
+
+  if (!agent.value) return;
+
+  if (panel === false) {
+    preview.leftUrl ??= stringifyUrl('left', simple);
+    preview.rightUrl ??= stringifyUrl('right', simple);
+  } else {
+    if (!slots.exception) {
+      preview.leftPanelUrl = void 0;
+      preview.rightPanelUrl = void 0;
+    }
+    await nextTick();
+    if (panel === true || panel === 'left') {
+      preview.leftPanelUrl ??= stringifyUrl('left', simple);
+      preview.showLeftPanel = true;
+    } else {
+      preview.showLeftPanel = false;
+    }
+
+    if (panel === true || panel === 'right') {
+      preview.rightPanelUrl ??= stringifyUrl('right', simple);
+      preview.showRightPanel = true;
+    } else {
+      preview.showRightPanel = false;
+    }
+  }
+};
+const open = async (panel?: 'left' | 'right', scroll = true) => {
+  if (disabled) return;
+
+  await load(panel);
+  if (scroll) getHeightAndScrollTop();
+  else {
+    const height = window.innerHeight * 0.8;
+    panelConfig.anchors = [0, height];
+    panelConfig.height = height;
+  }
+};
+
+tryOnMounted(() => {
+  if (!slots.exception) load(false, simple);
+  preview.clickMainPanelPreviewable = !!slots.exception;
+});
+
+const { define: PreviewSlot, reuse: ReusePreview } = createReusableTemplate<{ src: string | void }>();
+
+watchPostEffect(() => {
+  if (panelConfig.height === 0) {
+    preview.showLeftPanel = false;
+    preview.showRightPanel = false;
+  }
+});
+</script>
+
+<template>
+  <van-skeleton class="analysis" :row="5" :loading="report?.results == null" v-if="report?.results !== ''">
+    <PreviewSlot v-slot="{ src }">
+      <iframe v-if="src" :src="src" :class="{ simple: !src.includes('Hand=true') }"></iframe>
+    </PreviewSlot>
+    <slot>
+      <div ref="card" class="card m-6 text-lg" @click="panelConfig.height = 0">
+        <div v-if="title" class="card__title mb-3 text-primary text-2xl font-bold">{{ title }}</div>
+        <slot name="content" :report="report">
+          <div class="card__content">
+            <div v-if="report?.summaryLabel" class="flex justify-evenly">
+              <div v-if="report?.summaryLabel?.left" class="flex flex-row-reverse justify-center" @click.stop="preview.clickMainPanelPreviewable && open('left')">
+                <img style="width: 100px; height: 200px" :src="HandLeft" alt="左手" />
+                <div class="flex flex-col justify-evenly translate-y-2 h-40 text-xl" :class="{ highlight: preview.showLeftPanel }">
+                  <div>寸:<span style="letter-spacing: 4px">{{ report.summaryLabel.left.cun }}</span></div>
+                  <div>关:<span style="letter-spacing: 4px">{{ report.summaryLabel.left.guan }}</span></div>
+                  <div>尺:<span style="letter-spacing: 4px">{{ report.summaryLabel.left.chi }}</span>
+                  </div>
+                </div>
+              </div>
+              <div v-if="report?.summaryLabel?.right" class="flex justify-center" @click.stop="preview.clickMainPanelPreviewable && open('right')">
+                <img style="width: 100px; height: 200px" :src="HandRight" alt="右手" />
+                <div class="flex flex-col justify-evenly translate-y-2 h-40 text-xl" :class="{ highlight: preview.showRightPanel }">
+                  <div>寸:<span style="letter-spacing: 4px">{{ report.summaryLabel.right.cun }}</span></div>
+                  <div>关:<span style="letter-spacing: 4px">{{ report.summaryLabel.right.guan }}</span></div>
+                  <div>尺:<span style="letter-spacing: 4px">{{ report.summaryLabel.right.chi }}</span></div>
+                </div>
+              </div>
+            </div>
+            <p v-if="report?.results" class="text-2xl text-center" @click.stop="preview.clickMainPanelPreviewable && open()">
+              总体脉象:<span class="text-primary-400" style="letter-spacing: 4px">{{ report.results }}</span>
+            </p>
+          </div>
+        </slot>
+      </div>
+      <slot name="exception">
+        <div v-if="agent" class="grid grid-rows-1 grid-cols-1 gap-8 m-6">
+          <div class="card text-lg" v-if="report?.summaryLabel?.left" @click="panelConfig.height = 0">
+            <div class="card__title mb-3 text-primary text-2xl font-bold flex justify-between" @click.stop="simple && open('left', false)">
+              <div>左手脉象: {{ report.summaryLabel.left.summary?.join('、') }}</div>
+              <van-icon v-if="simple" class="text-xl" name="arrow" />
+            </div>
+            <div class="card__content">
+              <ReusePreview v-if="panelConfig.height < 100" :src="preview.leftUrl"></ReusePreview>
+            </div>
+          </div>
+          <div class="card text-lg" v-if="report?.summaryLabel?.right" @click="panelConfig.height = 0">
+            <div class="card__title mb-3 text-primary text-2xl font-bold flex justify-between" @click.stop="simple && open('right', false)">
+              <div>右手脉象: {{ report.summaryLabel.right.summary?.join('、') }}</div>
+              <van-icon v-if="simple" class="text-xl" name="arrow" />
+            </div>
+            <div class="card__content">
+              <ReusePreview v-if="panelConfig.height < 100" :src="preview.rightUrl"></ReusePreview>
+            </div>
+          </div>
+        </div>
+      </slot>
+    </slot>
+    <van-floating-panel class="pulse-info-panel" :content-draggable="false" :lock-scroll="true" :anchors="panelConfig.anchors" v-model:height="panelConfig.height">
+      <template #header>
+        <div class="van-floating-panel__header !justify-between">
+          <div></div>
+          <div class="van-floating-panel__header-bar"></div>
+          <van-icon class="pr-2" name="cross" @click="panelConfig.height = 0" />
+        </div>
+      </template>
+      <div class="area" :class="{ last: !preview.showRightPanel }" v-if="report?.summaryLabel?.left" v-show="preview.showLeftPanel">
+        <div class="title">左手脉象: {{ report.summaryLabel.left.summary?.join('、') }}</div>
+        <ReusePreview :src="preview.leftPanelUrl"></ReusePreview>
+      </div>
+      <div class="area" v-if="report?.summaryLabel?.right" v-show="preview.showRightPanel">
+        <div class="title">右手脉象: {{ report.summaryLabel.right.summary?.join('、') }}</div>
+        <ReusePreview :src="preview.rightPanelUrl"></ReusePreview>
+      </div>
+    </van-floating-panel>
+  </van-skeleton>
+</template>
+
+<style scoped lang="scss">
+.highlight {
+  color: rgb(52 167 107 / var(--tw-text-opacity, 1));
+}
+
+$h: 1660px;
+
+iframe {
+  width: 100%;
+  height: $h;
+  border: none;
+
+  &.simple {
+    height: 432px;
+  }
+}
+
+.pulse-info-panel {
+  --van-floating-panel-background: #fff;
+
+  .van-icon {
+    color: var(--van-floating-panel-bar-color);
+  }
+
+  .area {
+    margin: auto;
+    padding-left: 12px;
+    min-width: 320px;
+    max-width: 1200px;
+
+    .title {
+      padding-left: 9px;
+      font-family:
+        Helvetica Neue,
+        Helvetica,
+        Roboto,
+        Tahoma,
+        Arial,
+        PingFang SC,
+        sans-serif;
+      font-size: 18px;
+      font-weight: 700;
+      color: #1f2d3d;
+      line-height: 36px;
+    }
+
+    &:last-of-type,
+    &.last {
+      iframe {
+        height: calc(#{$h} + v-bind(offset) * 1px);
+      }
+    }
+  }
+}
+</style>

+ 135 - 0
src/modules/pulse/pulse-result.page.vue

@@ -0,0 +1,135 @@
+<script setup lang="ts">
+import NavHomeSelect from '@/assets/images/nav-home.select.png?url';
+import NavNextSelect from '@/assets/images/next-step.svg?url';
+import { Dialog } from '@/platform';
+
+import { useRouteQuery } from '@vueuse/router';
+import { useRequest, useWatcher } from 'alova/client';
+import { useRouter } from 'vue-router';
+import { getPulseMethod } from '@/request/api/pulse.api';
+import { processMethod3 } from '@/request/api';
+
+const id = useRouteQuery<string>('id');
+
+const next = ref<{ title: string; route: string; icon?: string; }>();
+useRequest(processMethod3, { immediate: true }).onSuccess(({ data }) => {
+  if (data.next?.route === '/screen') {
+    next.value = { title: '返回首页', route: '/screen', icon: NavHomeSelect };
+  } else if (data.next) {
+    next.value = { title: data.next.title || '获取健康调理方案', route: data.next.route, icon: NavNextSelect  };
+  }
+});
+
+const { data, loading, error } = useWatcher(() => getPulseMethod(id.value), [id], {
+  initialData: {
+    date: '',
+    pulse: {},
+  },
+  immediate: false,
+}).onError(async ({ error }) => {
+  await Dialog.show({
+    message: error.message,
+    theme: 'round-button',
+    showCancelButton: false,
+    confirmButtonText: '好的',
+    width: '350px',
+  });
+  //
+});
+
+const router = useRouter();
+
+const scrollable = computed(() => true);
+
+function replace(path: string = '/screen') {
+  return router.replace({ path });
+}
+</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>
+          <AnalysisPulseComponent title="脉象分析" v-bind="data.pulse"></AnalysisPulseComponent>
+        </div>
+      </van-skeleton>
+      <div class="flex-none flex justify-between py-2 nav-wrapper" style="background-color: #12312c" v-if="next">
+        <div class="m-auto min-w-16 text-center hover:text-primary" @click="replace(next.route)">
+          <img v-if="next.icon" :src="next.icon" :alt="next.title" />
+          <div class="mt-2">{{ next.title }}</div>
+        </div>
+        <!--        <div class="m-auto min-w-16 text-center hover:text-primary" v-if="data.scheme" @click="toggle()">
+          <img :src="NavScheme" alt="调理方案" />
+          <div class="mt-2">调理方案</div>
+        </div>
+
+        <div class="m-auto min-w-16 text-center hover:text-primary" @click="print()">
+          <van-loading v-if="uploading" color="#38ff6e" style="font-size: 24px" />
+          <img v-else :src="NavPrint" alt="打印" />
+          <div class="mt-2">打印</div>
+        </div>-->
+      </div>
+    </div>
+  </div>
+</template>
+<style scoped lang="scss">
+.van-button.decorate {
+  font-size: 20px;
+  height: 62px;
+  width: 240px;
+  background-size: 80%;
+  letter-spacing: 2px;
+}
+
+.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;
+}
+
+.nav-wrapper {
+  img {
+    margin: auto;
+    width: 36px;
+    height: 36px;
+    object-fit: scale-down;
+  }
+}
+</style>

+ 12 - 42
src/modules/pulse/pulse.page.vue

@@ -10,8 +10,6 @@ import type { Flow } from '@/request/model';
 import { useVisitor } from '@/stores';
 
 import NavHomeSelect from '@/assets/images/nav-home.select.png?url';
-import HandLeft from '@/assets/images/pulse-hand-left.png?url';
-import HandRight from '@/assets/images/pulse-hand-right.png?url';
 
 const router = useRouter();
 const Visitor = useVisitor();
@@ -79,7 +77,8 @@ const {
   data: report,
   loading: submitting,
   send: submit,
-} = useRequest((id, result) => putPulseMethod(id, result), { immediate: false }).onSuccess(() => {
+} = useRequest((id, result) => putPulseMethod(id, result), { immediate: false }).onSuccess(({ data }) => {
+  Visitor.updatePulseReport(data);
   finished.value = true;
   start();
 });
@@ -88,7 +87,10 @@ let timer: ReturnType<typeof setInterval>;
 const countdown = ref(5);
 
 function start(value?: number) {
-  if (!done.value) return;
+  if (!done.value) {
+    if (next.value?.route === '/pulse/result') replace(next.value.route);
+    return;
+  }
   countdown.value = value ?? done.value.countdown ?? 3;
   timer = setInterval(() => {
     const _countdown = countdown.value - 1;
@@ -128,50 +130,18 @@ tryOnUnmounted(() => {
     </div>
     <div class="page-content flex flex-col">
       <header></header>
-      <main class="flex flex-col justify-evenly px-8">
+      <main class="flex flex-col justify-evenly">
         <template v-if="finished && report">
           <img class="size-40 mx-auto" src="@/assets/images/tips.png" alt="" />
           <div>
             <div class="text-3xl text-center">恭喜您!</div>
             <div class="text-3xl text-center my-8">完成脉诊采集</div>
           </div>
-          <div v-if="report">
-            <div v-if="report.summaryLabel" class="flex justify-evenly">
-              <div v-if="report.summaryLabel?.left" class="flex flex-row-reverse justify-center">
-                <img style="width: 100px;height: 200px;" :src="HandLeft" alt="左手" />
-                <div class="flex flex-col justify-evenly translate-y-2 h-40 text-xl">
-                  <div>
-                    寸:<span style="letter-spacing: 4px">{{ report.summaryLabel.left.cun }}</span>
-                  </div>
-                  <div>
-                    关:<span style="letter-spacing: 4px">{{ report.summaryLabel.left.guan }}</span>
-                  </div>
-                  <div>
-                    尺:<span style="letter-spacing: 4px">{{ report.summaryLabel.left.chi }}</span>
-                  </div>
-                </div>
-              </div>
-              <div v-if="report.summaryLabel?.right">
-                <div class="flex justify-center">
-                  <img style="width: 100px;height: 200px;"  :src="HandRight" alt="右手" />
-                  <div class="flex flex-col justify-evenly translate-y-2 h-40 text-xl">
-                    <div>
-                      寸:<span style="letter-spacing: 4px">{{ report.summaryLabel.right.cun }}</span>
-                    </div>
-                    <div>
-                      关:<span style="letter-spacing: 4px">{{ report.summaryLabel.right.guan }}</span>
-                    </div>
-                    <div>
-                      尺:<span style="letter-spacing: 4px">{{ report.summaryLabel.right.chi }}</span>
-                    </div>
-                  </div>
-                </div>
-              </div>
-            </div>
-            <p v-if="report.results" class="text-2xl text-center">
-              总体脉象:<span class="text-primary-400" style="letter-spacing: 4px">{{ report.results }}</span>
-            </p>
-          </div>
+          <AnalysisPulseComponent title="" v-bind="report" disabled>
+            <template #exception>
+              <div><!--空占位符--></div>
+            </template>
+          </AnalysisPulseComponent>
         </template>
       </main>
       <footer class="flex flex-col justify-center items-center">

+ 2 - 0
src/modules/report/report.page.vue

@@ -20,6 +20,7 @@ const { data, loading } = useWatcher(() => getReportMethod(id.value), [ id ], {
     descriptionsTable: { column: [], data: [] },
     tongue: {},
     face: {},
+    pulse: {},
   },
   immediate: true,
 }).onSuccess(({ data }) => {
@@ -181,6 +182,7 @@ const scrollable = computed(() => !data.value.payLock &&
             </template>
             <template #exception><div><!--空占位符--></div></template>
           </AnalysisComponent>
+          <AnalysisPulseComponent title="脉象分析" v-bind="data.pulse" simple></AnalysisPulseComponent>
           <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">

+ 51 - 4
src/request/api/pulse.api.ts

@@ -1,18 +1,65 @@
 import HTTP from '@/request/alova';
+import { fromAnalysisModel, type PulseAnalysisModel } from '@/request/model';
 
-export type PulseResult = Awaited<ReturnType<typeof Bridge.pulse>> & { patientId: string; results: string };
 const getSummary = (value?: string[]) => (Array.isArray(value) && value.length ? value : void 0);
 
-export function putPulseMethod(patientId: string, result: PulseResult) {
+export function putPulseMethod(patientId: string, result: PulseAnalysisModel) {
   const summary =
     getSummary(result.summaryLabel?.summary) ??
     getSummary(result.summaryLabel?.left?.summary) ??
     getSummary(result.summaryLabel?.right?.summary) ??
     [];
   const data = Object.assign(result, { patientId, results: summary.join(',') });
-  return HTTP.Post<PulseResult, any>(`/fdhb-tablet/analysisManage/savePulseAnalysisReport`, data, {
+  return HTTP.Post<PulseAnalysisModel, any>(`/fdhb-tablet/analysisManage/savePulseAnalysisReport`, data, {
     transform() {
-      return data;
+      return {
+        measureId: data.measureId,
+        hand: [
+          data.summaryLabel?.left?.summary?.length,
+          data.summaryLabel?.right?.summary?.length,
+        ].filter(Boolean).length,
+        summaryValue: data.summaryValue,
+        summaryLabel: data.summaryLabel,
+        results: data.results,
+        url: __FORBID_AUTO_PROCESS_PULSE_AGENCY__ ? void 0 : data.url,
+      };
     },
   });
 }
+
+/**
+ * 暂未实现
+ * @deprecated
+ */
+export function getPulseMethod(id: string) {
+  return HTTP.Get(`/fdhb-tablet/analysisManage/getHealRepDetailById`, {
+    transform(data: Record<string, any>) {
+      return {
+        date: data?.pulseAnalysisReportDate,
+        pulse: fromAnalysisModel('pulse', data),
+      }
+    }
+  });
+}
+
+/**
+ * 暂未实现
+ * @deprecated
+ */
+export function getPulseAgentMethod() {
+  return HTTP.Post(
+    `/fdhb-tablet/warrantManage/getPageSets`,
+    {},
+    {
+      transform(data: any) {
+        if (data?.origin == null && data?.url == null) return void 0;
+        const origin = data.origin.endsWith('/report.html') ? data.origin : `${data.origin}/taiyi/report.html`;
+        return {
+          appId: data.appId ?? 'hJn5D3rr',
+          token: data.token,
+          url: `${origin}#/`,
+        };
+      },
+    }
+  );
+}

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

@@ -33,11 +33,14 @@ export function getAnalysisResultsMethod() {
 
 export function getReportMethod(id: string) {
   const Visitor = useVisitor();
-  const params = { healthAnalysisReportId: id, patientId: Visitor.patientId };
+  const params = { healthAnalysisReportId: id, patientId: Visitor.patientId?.toString() };
   return HTTP.Get(`/fdhb-tablet/analysisManage/getHealRepDetailById`, {
     params,
     async transform(data, headers) {
+      const patientId = (<any>data)?.patientId?.toString()
+      if (params.patientId !== patientId ) Visitor.$reset()
       const report = fromReportData(<any> data);
+      Visitor.updatePulseReport(report.pulse, patientId);
       const { show, force } = await miniProgramMethod();
       if ( !show ) { report.miniProgramURL = void 0; }
       report.payLock = show && force;

+ 42 - 6
src/request/model/analysis.model.ts

@@ -1,24 +1,34 @@
 import { groupBy } from '@/tools';
 
-
-export function fromAnalysisModel(mode: 'tongue' | 'face', data: Record<string, any>): AnalysisModel {
+export function fromAnalysisModel(mode: 'tongue' | 'face', data: Record<string, any>): AnalysisModel;
+export function fromAnalysisModel(mode: 'pulse', data: Record<string, any>): PulseAnalysisModel;
+export function fromAnalysisModel(
+  mode: 'tongue' | 'face' | 'pulse',
+  data: Record<string, any>
+): AnalysisModel | PulseAnalysisModel {
   let model: AnalysisModel;
-  switch ( mode ) {
+  switch (mode) {
     case 'tongue':
       model = fromTongueAnalysisModel(data);
       break;
     case 'face':
       model = fromFaceAnalysisModel(data);
       break;
+    case 'pulse':
+      return fromPulseAnalysisModel(data);
   }
 
   const group = groupBy<AnalysisException>(model.exception, (item) => item.cover ?? '');
-  model.exceptionGroup = Object.entries(group).map(([ key, exception ]) => (
-    { key, exception }
-  ));
+  model.exceptionGroup = Object.entries(group).map(([key, exception]) => ({ key, exception }));
   return model;
 }
 
+export interface PulseAnalysisModel extends Pick<Awaited<ReturnType<typeof Bridge.pulse>>, 'measureId' | 'summaryLabel' | 'summaryValue'> {
+  hand?: number;
+  results?: string;
+  url?: string;
+}
+
 export interface AnalysisModel {
   table: {
     columns: string[];
@@ -41,6 +51,32 @@ export interface AnalysisException {
   tags: string[];
 }
 
+function fromPulseAnalysisModel(data: Record<string, any>): PulseAnalysisModel {
+  const pulse = (function (value) {
+    if (typeof value === 'string')
+      try {
+        value = JSON.parse(value);
+      } catch (e) {}
+    return typeof value === 'object' ? value : {};
+  })(data.pulse);
+
+  const getSummary = (value?: string[]) => (Array.isArray(value) && value.length ? value : void 0);
+  const summary =
+    getSummary(pulse?.summary_desc?.summary) ??
+    getSummary(pulse?.summary_desc?.left?.summary) ??
+    getSummary(pulse?.summary_desc?.right?.summary) ??
+    [];
+
+  return {
+    measureId: data?.measureId,
+    hand: pulse?.hand ?? 0,
+    summaryValue: pulse?.summary,
+    summaryLabel: pulse?.summary_desc,
+    results: summary.join(','),
+    url: pulse?.url ?? void 0,
+  };
+}
+
 function fromTongueAnalysisModel(data: Record<string, any>): AnalysisModel {
   const exception: AnalysisException[] = [];
   const fromTongueException = fromAnalysisException(exception);

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

@@ -29,6 +29,7 @@ export function fromReportData(data: Record<string, any>) {
 
     tongue: fromAnalysisModel('tongue', data),
     face: fromAnalysisModel('face', data),
+    pulse: fromAnalysisModel('pulse', data),
 
     [ '中医证素' ]: data?.factorItems?.map?.((item: Record<string, any>) => {
       return { label: item?.factorItemName, value: item?.factorItemDescription, score: +item?.score };

+ 1 - 0
src/router/index.ts

@@ -7,6 +7,7 @@ const router = createRouter({
     { path: '/screen', component: () => import('@/pages/screen.page.vue'), meta: { scan: true } },
     { path: '/register', component: () => import('@/pages/register.page.vue'), meta: { title: '建档', scan: true } },
     { path: '/pulse', component: () => import('@/modules/pulse/pulse.page.vue'), meta: { title: '脉诊' } },
+    { path: '/pulse/result', component: () => import('@/modules/pulse/pulse-result.page.vue'), meta: { title: '脉象分析报告' } },
     { 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: '问卷' } },

+ 11 - 1
src/stores/visitor.store.ts

@@ -1,16 +1,26 @@
 import { defineStore } from 'pinia';
+import type { PulseAnalysisModel } from '@/request/model';
 
 
 export const useVisitor = defineStore('visitor', () => {
   const patientId = ref<string>();
   const resultId = ref<string>();
   const reportId = ref<string>();
+  const pulseReport = shallowRef<PulseAnalysisModel>();
 
   const $reset = () => {
     patientId.value = '';
     resultId.value = '';
     reportId.value = '';
+    pulseReport.value = void 0;
   };
 
-  return { patientId, resultId, reportId, $reset };
+  function updatePulseReport(report: PulseAnalysisModel, patient?: string) {
+    if (patientId.value?.toString() !== patient || !pulseReport.value?.results) {
+      if (patient) patientId.value = patient;
+      pulseReport.value = report;
+    }
+  }
+
+  return { patientId, resultId, reportId, pulseReport, $reset, updatePulseReport };
 });

+ 5 - 0
src/tools/url.tool.ts

@@ -3,6 +3,11 @@ export function getURLSearchParams(value?: string): URLSearchParams {
   return new URLSearchParams(value);
 }
 
+export function getURLSearchParamsByUrl(value: string): URLSearchParams {
+  const snippet = value.split('#');
+  return getURLSearchParams(snippet.map((param) => param.split('?')[1] || '').join('&'))
+}
+
 
 export function getClientURL(value: string, origin?: string) {
   if ( !value || /^https?:\/\//.test(value) ) return value;

+ 4 - 0
vite.config.ts

@@ -11,11 +11,15 @@ import vueDevTools               from 'vite-plugin-vue-devtools';
 
 // https://vitejs.dev/config/
 export default defineConfig((configEnv) => {
+  const argv = process.argv
   const envDir = './.env';
   const env = loadEnv(configEnv.mode, envDir, 'REQUEST_');
   return {
     envDir,
     envPrefix: 'SIX_',
+    define: {
+      __FORBID_AUTO_PROCESS_PULSE_AGENCY__: argv.includes('--legacy-pulse-agency')
+    },
     css: {
       preprocessorOptions: {
         scss: {