Parcourir la source

添加脉象分析报告详情

cc12458 il y a 3 mois
Parent
commit
453d5b73b6

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

+ 241 - 24
src/components/AnalysisPulseComponent.vue

@@ -1,58 +1,275 @@
 <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 = SimplePulseModel & { title?: string; results?: string };
+type Props = Partial<SimplePulseModel> & { title?: string; results?: string; disabled?: boolean; simple?: boolean };
 
-const { title = '分析', results, ..._report } = defineProps<Props>();
+const { title = '分析', disabled = false, simple = false, results, ..._report } = defineProps<Props>();
+const slots = defineSlots();
 
 const Visitor = useVisitor();
-const report = computed(() => unref(results) ? _report : Visitor.pulseReport)
+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')) {
+    return {
+      appId: params.get('appId')!!,
+      token: params.get('access_session')!!,
+      url: `${url}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="results == null" v-if="results !== ''">
+  <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 class="card m-6 text-lg">
+      <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" :results="results" :report="report">
+        <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">
-                <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 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">
-                <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 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="results" class="text-2xl text-center">
-              总体脉象:<span class="text-primary-400" style="letter-spacing: 4px">{{ results }}</span>
+            <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;
+    }
 
-</style>
+    &: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>

+ 5 - 1
src/modules/pulse/pulse.page.vue

@@ -136,7 +136,11 @@ tryOnUnmounted(() => {
             <div class="text-3xl text-center">恭喜您!</div>
             <div class="text-3xl text-center my-8">完成脉诊采集</div>
           </div>
-          <AnalysisPulseComponent title="" v-bind="report"></AnalysisPulseComponent>
+          <AnalysisPulseComponent title="" v-bind="report" disabled>
+            <template #exception>
+              <div><!--空占位符--></div>
+            </template>
+          </AnalysisPulseComponent>
         </template>
       </main>
       <footer class="flex flex-col justify-center items-center">

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

@@ -182,7 +182,7 @@ const scrollable = computed(() => !data.value.payLock &&
             </template>
             <template #exception><div><!--空占位符--></div></template>
           </AnalysisComponent>
-          <AnalysisPulseComponent title="脉象分析" v-bind="data.pulse"></AnalysisPulseComponent>
+          <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">

+ 39 - 1
src/request/api/pulse.api.ts

@@ -1,5 +1,5 @@
 import HTTP from '@/request/alova';
-import type { PulseAnalysisModel } from '@/request/model';
+import { fromAnalysisModel, type PulseAnalysisModel } from '@/request/model';
 
 const getSummary = (value?: string[]) => (Array.isArray(value) && value.length ? value : void 0);
 
@@ -21,7 +21,45 @@ export function putPulseMethod(patientId: string, result: PulseAnalysisModel) {
         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}#/`,
+        };
+      },
+    }
+  );
+}

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

@@ -73,6 +73,7 @@ function fromPulseAnalysisModel(data: Record<string, any>): PulseAnalysisModel {
     summaryValue: pulse?.summary,
     summaryLabel: pulse?.summary_desc,
     results: summary.join(','),
+    url: pulse?.url ?? void 0,
   };
 }
 

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

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

@@ -16,7 +16,7 @@ export const useVisitor = defineStore('visitor', () => {
   };
 
   function updatePulseReport(report: PulseAnalysisModel, patient?: string) {
-    if (patientId.value?.toString() !== patient || pulseReport.value == null) {
+    if (patientId.value?.toString() !== patient || !pulseReport.value?.results) {
       if (patient) patientId.value = patient;
       pulseReport.value = report;
     }

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