Bladeren bron

Merge branch 'release/2.1.1'

cc12458 3 weken geleden
bovenliggende
commit
e64183ab88

+ 80 - 0
src/components/MiniProgram.vue

@@ -0,0 +1,80 @@
+<script setup lang="ts">
+import NavMiniProgram from '@/assets/images/mini-program.svg?url';
+import { Notify } from '@/platform';
+
+const props = defineProps<{ url?: string; closeable?: boolean; forcedShow?: boolean }>();
+
+const panelHeight = ref(0);
+const panelProps = reactive({
+  anchors: [0, window.innerWidth],
+  contentDraggable: false,
+  lockScroll: true,
+});
+const showOverlay = computed(() => panelHeight.value >= panelProps.anchors[0] && panelHeight.value > 0)
+
+watchEffect(() => {
+  panelProps.anchors[0] = props.closeable ? 0 : 100;
+});
+
+const handle = () => {
+  if (!props.url) {
+    Notify.warning(`未获取到小程序地址,请联系管理员或重试`);
+    return;
+  }
+  panelOpen();
+};
+
+const panelOpen = (min?: number) => {
+  if (min) panelProps.anchors[0] = min;
+  panelHeight.value = panelProps.anchors[1];
+};
+
+defineExpose({
+  open: handle,
+});
+</script>
+
+<template>
+  <div class="nav m-auto min-w-16 text-center hover:text-primary" v-if="url || props.forcedShow" @click="handle()">
+    <img class="nav-img" :src="NavMiniProgram" alt="小程序" />
+    <div class="mt-2">小程序</div>
+  </div>
+  <van-overlay :show="showOverlay" class="panel-mask"></van-overlay>
+  <van-floating-panel v-model:height="panelHeight" v-bind="panelProps">
+    <template #header>
+      <div class="van-floating-panel__header !justify-between">
+        <div></div>
+        <div class="van-floating-panel__header-bar"></div>
+        <div>
+          <van-icon v-if="props.closeable" name="cross" @click.stop="panelHeight = panelProps.anchors[0]" />
+        </div>
+      </div>
+    </template>
+    <Transition>
+      <div class="panel-content">
+        <img class="size-full object-contain" v-if="panelHeight === panelProps.anchors[1] || panelHeight > panelProps.anchors[0] * 1.5" :src="url" 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>
+</template>
+
+<style scoped lang="scss">
+.nav-img {
+  margin: auto;
+  width: 36px;
+  height: 36px;
+  object-fit: scale-down;
+}
+
+.panel-mask {
+  --van-overlay-background: transparent;
+}
+
+.panel-content {
+  padding: 0 var(--van-floating-panel-header-height) var(--van-floating-panel-header-height);
+}
+</style>

+ 1 - 0
src/computable/useRouteNext.ts

@@ -54,6 +54,7 @@ export function getRoutePath(flow?: Flow) {
   let route = flow?.route;
   if (!route) throw { message: `[路由] 页面未找到` };
   if (route === '/report') route += `/${useVisitor().reportId}`;
+  if (route === '/scheme') route += `/${useVisitor().reportId}`;
 
   return route;
 }

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

@@ -55,6 +55,8 @@ function onPlay(event?: Event) {
     <div class="flex-none mx-2" v-for="item in media" :key="item.title">
       <div v-if="item.url" class="relative h-32 rounded-lg	overflow-hidden" @click="handle(item)">
         <img class="size-full object-scale-down" v-if="item.poster" :src="item.poster" :alt="item.title" />
+        <video class="size-full object-scale-down" v-else-if="item.type === 'video'" :src="item.url" preload="metadata"></video>
+        <div class="size-full object-scale-down w-64 bg-gray-600" v-else></div>
         <van-icon class="play" v-if="item.type === 'video'" name="play-circle-o" />
       </div>
       <template v-if="item.title">

+ 11 - 58
src/modules/report/report.page.vue

@@ -13,6 +13,9 @@ import { useRouteParams }         from '@vueuse/router';
 import { useRequest, useWatcher } from 'alova/client';
 import { useRouter }              from 'vue-router';
 
+import MiniProgram from '@/components/MiniProgram.vue';
+const miniProgramRef = useTemplateRef<InstanceType<typeof MiniProgram>>('mini-program');
+
 const hidePulseExceptionTemplate = computed(() => platformIsAIO())
 
 const id = useRouteParams<string>('id');
@@ -25,7 +28,9 @@ const { data, loading } = useWatcher(() => getReportMethod(id.value), [ id ], {
   },
   immediate: true,
 }).onSuccess(({ data }) => {
-  if ( data?.miniProgramURL && data.payLock ) panelOpen(100);
+  if ( data?.miniProgramURL && data.payLock ) {
+    nextTick(() => miniProgramRef.value?.open());
+  }
 })
 
 const { loading: uploading, send: upload } = useRequest(() => updateReportMethod(id.value, data.value), {
@@ -72,36 +77,12 @@ async function print() {
   }
 }
 
-async function miniProgram() {
-  let url = data.value.miniProgramURL;
-  if ( !url ) {
-    Notify.warning(`未获取到小程序地址,请联系管理员或重试`);
-    return;
-  }
-  panelOpen();
-}
-
 const router = useRouter();
 
 function toggle() {
   const path = `${ router.currentRoute.value.fullPath }/scheme`.replace(/\/{2,}/g, '/');
   router.replace({ path });
 }
-
-const panelHeight = ref(0);
-const panelProps = reactive({
-  anchors: [0, window.innerWidth],
-  contentDraggable: false,
-  lockScroll: true,
-});
-const panelOpen = (min?: number) => {
-  if ( min ) panelProps.anchors[ 0 ] = min;
-  panelHeight.value = panelProps.anchors[1];
-};
-
-const scrollable = computed(() => !data.value.payLock &&
-                                  panelHeight.value < panelProps.anchors[ 1 ] || panelHeight.value === 0,
-);
 </script>
 <template>
   <div class="report-wrapper">
@@ -120,7 +101,7 @@ const scrollable = computed(() => !data.value.payLock &&
     </div>
     <div class="page-content flex flex-col overflow-hidden">
       <van-skeleton class="flex-auto" title :row="3" :loading>
-        <div class="flex-auto" :class="{ 'overflow-y-auto': scrollable }">
+        <div class="flex-auto overflow-y-auto">
           <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>
@@ -216,45 +197,17 @@ const scrollable = computed(() => !data.value.payLock &&
       </van-skeleton>
       <div class="flex-none flex justify-between py-2 nav-wrapper" style="background-color: #12312c;">
         <div class="m-auto min-w-16 text-center hover:text-primary" v-if="data.scheme" @click="toggle()">
-          <img :src="NavScheme" alt="调理方案">
+          <img class="nav-img" :src="NavScheme" alt="调理方案">
           <div class="mt-2">调理方案</div>
         </div>
-        <div class="m-auto min-w-16 text-center hover:text-primary" v-if="data.miniProgramURL" @click="miniProgram()">
-          <img :src="NavMiniProgram" alt="小程序">
-          <div class="mt-2">小程序</div>
-        </div>
+        <mini-program ref="mini-program" :url="data.miniProgramURL" :closeable="!data.payLock"></mini-program>
         <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="打印">
+          <img class="nav-img" v-else :src="NavPrint" alt="打印">
           <div class="mt-2">打印</div>
         </div>
       </div>
       <Component :is="ReportPreview" v-bind="reportPreviewProps" v-model:show="reportPreviewProps.show"></Component>
-      <van-floating-panel v-model:height="panelHeight" v-bind="panelProps">
-        <template #header>
-          <div class="van-floating-panel__header !justify-between">
-            <div></div>
-            <div class="van-floating-panel__header-bar"></div>
-            <div>
-              <van-icon v-if="!data.payLock" name="cross" @click.stop="panelHeight = panelProps.anchors[0];" />
-            </div>
-          </div>
-        </template>
-        <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>
@@ -299,7 +252,7 @@ const scrollable = computed(() => !data.value.payLock &&
 }
 
 .nav-wrapper {
-  img {
+  .nav-img {
     margin: auto;
     width: 36px;
     height: 36px;

+ 133 - 9
src/modules/report/scheme.page.vue

@@ -7,21 +7,86 @@ import { useRouteParams }        from '@vueuse/router';
 import { useWatcher }            from 'alova/client';
 import { useRouter } from 'vue-router';
 
+import MiniProgram from '@/components/MiniProgram.vue';
 
+import type { SchemeGoodsProps } from '@/request/model';
+import { createReusableTemplate } from '@vueuse/core';
+import { Toast } from '@/platform';
+
+const miniProgramRef = useTemplateRef<InstanceType<typeof MiniProgram>>('mini-program');
+
+const route = useRoute();
 const id = useRouteParams<string>('id');
-const { data, loading } = useWatcher(() => getReportSchemeMethod(id.value), [ id ], {
+const { data, loading } = useWatcher(() => getReportSchemeMethod(id.value, !route.meta.toggle), [id], {
   initialData: {
     children: [],
   },
   immediate: true,
+}).onSuccess(({ data }) => {
+  if ( data?.miniProgramURL && data.payLock ) {
+    nextTick(() => miniProgramRef.value?.open());
+  }
 });
 
 const router = useRouter();
 
+const toggleable = computed(() => route.meta.toggle ?? true);
+
 function toggle() {
   const path = router.currentRoute.value.fullPath.replace('/scheme', '');
   router.replace({ path });
 }
+
+const { define: PreviewLinkSlot, reuse: ReusePreviewLink } = createReusableTemplate<{ src: string | void; complete?: Function; error?: Function }>();
+const panelConfig = reactive({
+  fullHeight: 0,
+  height: 0,
+  anchors: [0],
+  goods: void 0 as unknown as SchemeGoodsProps,
+  onError() {},
+  onComplete(event?: Event) {},
+});
+const container = useTemplateRef('container');
+const getHeightAndScrollTop = (value?: HTMLElement | string) => {
+  const el = typeof value === 'string' ? container.value?.querySelector<HTMLDivElement>(`#${value}`) : value;
+  if (!el) return true;
+  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;
+  panelConfig.fullHeight = maxHeight;
+};
+async function openGoodsPanel(goods: SchemeGoodsProps, event?: Event | string) {
+  if (panelConfig.goods === goods) {
+    panelConfig.height = panelConfig.anchors[panelConfig.anchors.length - 1];
+    return;
+  }
+
+  if (goods.type === 'link') {
+    const toast = Toast.loading(500);
+    panelConfig.goods = goods;
+    panelConfig.onComplete = () => {
+      toast.close();
+      panelConfig.height = panelConfig.anchors[panelConfig.anchors.length - 1];
+    };
+    panelConfig.onError = () => {
+      Toast.error(`链接加载错误`)
+      panelConfig.height = 0;
+    };
+  } else {
+    Toast.warning(`暂不支持该操作 (${goods.type})`);
+    return;
+  }
+
+  if (getHeightAndScrollTop(typeof event === 'string' ? event : (event?.target as HTMLElement))) {
+    const height = window.innerHeight * 0.8;
+    panelConfig.anchors = [0, height];
+    panelConfig.height = height;
+    panelConfig.fullHeight = window.innerHeight;
+  }
+}
 </script>
 <template>
   <div>
@@ -41,15 +106,22 @@ function toggle() {
     <div class="page-content flex flex-col overflow-hidden">
       <!--{{ data }}-->
       <van-skeleton class="flex-auto" title :row="3" :loading>
-        <div class="flex-auto px-6 overflow-y-auto">
-          <div class="card my-6 text-lg" v-for="item in data.children" :key="item.id">
+        <div class="flex-auto px-6 overflow-y-auto" ref="container">
+          <div class="card my-6 text-lg" :id="item.id" v-for="item in data.children" :key="item.id">
             <div class="card__title mb-3 text-primary text-2xl font-bold">{{ item.title }}</div>
             <div class="card__content">
-              <div class="my-4" v-for="card in item.children" :key="card.id">
-                <div class="text-xl text-center text-primary">{{ card.title }}</div>
+              <div class="my-4" :id="'T_' + card.id" v-for="card in item.children" :key="card.id">
+                <div class="relative" :class="{ 'has-link': card.goods }">
+                  <div class="text-xl text-center text-primary" v-if="card.title">{{ card.title }}</div>
+                  <van-button
+                    class="!absolute top-0 right-0" v-if="card.goods"
+                    type="primary" icon="cart-o" size="small" plain
+                    @click="openGoodsPanel(card.goods, 'T_' + card.id)"
+                  >{{ card.goods.label }}</van-button>
+                </div>
                 <SchemeMedia :media="card.media"></SchemeMedia>
                 <div v-if="card.description">{{ card.description }}</div>
-                <div v-for="(item,index) in card.descriptions ">
+                <div v-for="(item, index) in card.descriptions">
                   <span class="text-primary">【{{ item.title }}】</span>
                   <span v-html="item.description"></span>
                 </div>
@@ -58,13 +130,35 @@ function toggle() {
           </div>
         </div>
       </van-skeleton>
-      <div class="flex-none flex justify-between py-2 nav-wrapper" style="background-color: #12312c;">
-        <div class="m-auto min-w-16 text-center hover:text-primary" @click="toggle()">
-          <img :src="NavScheme" alt="健康报告">
+      <div class="flex-none flex justify-between py-2 nav-wrapper" style="background-color: #12312c">
+        <div v-if="toggleable" class="m-auto min-w-16 text-center hover:text-primary" @click="toggle()">
+          <img class="nav-img" :src="NavScheme" alt="健康报告" />
           <div class="mt-2">健康报告</div>
         </div>
+        <mini-program ref="mini-program" :url="data.miniProgramURL" :closeable="!data.payLock"></mini-program>
       </div>
     </div>
+
+    <PreviewLinkSlot v-slot="{ src, complete, error }">
+      <iframe v-if="src" :src="src" @load="complete!" @error="error!"></iframe>
+    </PreviewLinkSlot>
+    <van-floating-panel
+      ref="panel-wrapper-ref"
+      :class="{ full: panelConfig.height === panelConfig.fullHeight }"
+      :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>
+      <ReusePreviewLink v-if="panelConfig.goods?.type === 'link'" :src="panelConfig.goods?.value" :complete="panelConfig.onComplete"></ReusePreviewLink>
+    </van-floating-panel>
   </div>
 </template>
 <style scoped lang="scss">
@@ -85,4 +179,34 @@ function toggle() {
 .text-grey {
   color: #e3e3e3;
 }
+
+.nav-wrapper {
+  .nav-img {
+    margin: auto;
+    width: 36px;
+    height: 36px;
+    object-fit: scale-down;
+  }
+}
+
+.full {
+  --van-floating-panel-border-radius: 0;
+}
+
+.has-link {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 40px;
+
+  > div + .van-button {
+    top: 4px !important;
+  }
+}
+
+iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+}
 </style>

+ 22 - 10
src/request/api/report.api.ts

@@ -23,7 +23,7 @@ export function getAnalysisResultsMethod() {
           if (!tongue.result && !face.result) {
             message = data.content;
           } else {
-            const { show, force } = await miniProgramMethod();
+            const { show, force } = await miniProgramMethod('report');
             return {
               id, date, miniProgramURL: show ? miniProgramURL : void 0,
               tongue, face,
@@ -47,7 +47,7 @@ export function getReportMethod(id: string) {
       if (params.patientId !== patientId ) Visitor.$reset()
       const report = fromReportData(<any> data);
       Visitor.updatePulseReport(report.pulse, patientId);
-      const { show, force } = await miniProgramMethod();
+      const { show, force } = await miniProgramMethod('report');
       if ( !show ) { report.miniProgramURL = void 0; }
       report.payLock = show && force;
       return report;
@@ -80,27 +80,39 @@ export function updateReportMethod(id: string, data: Record<string, any>) {
   return HTTP.Post(`/fdhb-tablet/analysisManage/upConFacImgById`, params, {});
 }
 
-export function getReportSchemeMethod(id: string) {
+export function getReportSchemeMethod(id: string, standalone?: boolean) {
   const Visitor = useVisitor();
-  const params = { healthAnalysisReportId: id, patientId: Visitor.patientId };
+  const params = { healthAnalysisReportId: id, patientId: Visitor.patientId, standalone };
   return HTTP.Get(`/fdhb-tablet/analysisManage/getCondProgDetailById`, {
     params,
-    transform(data: any, headers) {
-      return fromSchemeRequest(data);
+    async transform(data: any, headers) {
+      const scheme = fromSchemeRequest(data);
+      const { show, force } = await miniProgramMethod('scheme');
+      if ( !show ) { scheme.miniProgramURL = void 0; }
+      scheme.payLock = show && force;
+      return scheme;
     },
   });
 }
 
 
-export function miniProgramMethod() {
+export function miniProgramMethod(type: 'report' | 'scheme' = 'report') {
   return HTTP.Post<{ show: boolean; force: boolean; }, { tabletRequiredPageOperationElements: string[] }>(
     `/fdhb-tablet/warrantManage/getPageSets`, void 0, {
       cacheFor, name: `variate:mini_program`,
-      params: { k: 'mini_program' },
+      params: { k: `mini_program_${type}` },
       transform(data, headers) {
+        const cfg = Array.isArray(data.tabletRequiredPageOperationElements) ? data.tabletRequiredPageOperationElements : [];
+        /**
+         *  - health_analysis_report_page_appletbutton
+         *  - health_analysis_report_page_appletscan
+         *
+         *  - health_analysis_scheme_page_appletbutton
+         *  - health_analysis_scheme_page_appletscan
+         */
         return {
-          show: data?.tabletRequiredPageOperationElements?.includes('health_analysis_report_page_appletbutton'),
-          force: data?.tabletRequiredPageOperationElements?.includes('health_analysis_report_page_appletscan'),
+          show: cfg.includes(`health_analysis_${type}_page_appletbutton`),
+          force: cfg.includes(`health_analysis_${type}_page_appletscan`),
         };
       },
     });

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

@@ -135,12 +135,14 @@ function fromFaceAnalysisModel(data: Record<string, any>): AnalysisModel {
         fromFaceException(data?.faceColor, '面色'),
         fromFaceException(data?.mainColor, '主色'),
         fromFaceException(data?.shine, '光泽'),
-        fromFaceException(data?.leftBlackEye, '左黑眼圈'),
-        fromFaceException(data?.rightBlackEye, '右黑眼圈'),
+        fromFaceException(data?.blackEye, '黑眼圈'),
+        // fromFaceException(data?.leftBlackEye, '左黑眼圈'),
+        // fromFaceException(data?.rightBlackEye, '右黑眼圈'),
         fromFaceException(data?.lipColor, '唇色'),
         fromFaceException(data?.eyeContact, '眼神'),
-        fromFaceException(data?.leftEyeColor, '左目色'),
-        fromFaceException(data?.rightEyeColor, '右目色'),
+        fromFaceException(data?.eyeColor, '目色'),
+        // fromFaceException(data?.leftEyeColor, '左目色'),
+        // fromFaceException(data?.rightEyeColor, '右目色'),
         fromFaceException(data?.hecticCheek, '两颧红'),
         fromFaceException(data?.noseFold, '鼻褶'),
         fromFaceException(data?.cyanGlabella, '眉间/鼻柱青色'),

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

@@ -7,6 +7,7 @@ const Routes = {
   'tongueface_analysis': /* 问卷页 */ '/questionnaire',
   'tongueface_analysis_result': /* 舌面象分析报告页 */ '/report/analysis',
   'health_analysis': /* 健康报告页 */ '/report',
+  'health_analysis_scheme': /* 调理方案页 */ '/scheme',
   'pulse_upload': /* 脉诊页 */ '/pulse',
   'pulse_upload_result': /* 脉诊结果页 */ '/pulse/result',
   'alcohol_upload_result': /* 酒精结果页 */ '/alcohol/result',

+ 39 - 1
src/request/model/scheme.model.ts

@@ -1,30 +1,54 @@
+import { randomUUID } from '@/tools';
+
 type Data<T extends any = any> = Record<string, T>
 
 export function fromSchemeRequest(data: Data) {
   return {
     children: data?.types?.map((item: Data) => {
       return {
+        id: data?.id ?? randomUUID(),
         title: item?.type || '',
         children: item?.groups?.map((item: Data) => {
           return {
             ...fragment(item),
             descriptions: item?.attrs?.map(fragment) ?? [],
             media: fromSchemeMedia(item.items ?? []),
+            goods: item?.buyUrl ? fromSchemeGoods(item) : void 0,
           };
         }) ?? [],
       };
     }) ?? [],
-  };
+    miniProgramURL: data?.appletImg,
+    payLock: data?.payLock,
+  } as { children: SchemeProps[], miniProgramURL?: string; payLock?: boolean };
 }
 
 
 function fragment(data: Data<string>) {
   return {
+    id: data?.id ?? randomUUID(),
     title: data?.name || '',
     description: data?.description,
   };
 }
 
+export interface SchemeProps {
+  id: string;
+  title: string;
+  children: {
+    id: string;
+    title: string;
+    description?: string;
+    descriptions: {
+      id: string;
+      title: string;
+      description?: string;
+    }[];
+    goods?: SchemeGoodsProps;
+    media: SchemeMediaProps[];
+  }[];
+}
+
 export interface SchemeMediaProps {
   title: string;
   description: string;
@@ -47,3 +71,17 @@ function fromSchemeMedia(data: Data[]) {
   }
   return media;
 }
+
+export interface SchemeGoodsProps {
+  type: 'link' | string;
+  value: string;
+  label?: string;
+}
+
+function fromSchemeGoods(data: Data): SchemeGoodsProps {
+  return {
+    type: { URL: 'link' }[<string>data.buyType] ?? data.buyType,
+    label: data.buyName ?? '去购买',
+    value: data.buyUrl,
+  }
+}

+ 2 - 1
src/router/index.ts

@@ -13,8 +13,9 @@ const router = createRouter({
     { path: '/questionnaire', component: () => import('@/modules/questionnaire/page.vue'), meta: { title: '问卷' } },
     { path: '/alcohol/result', component: () => import('@/modules/alcohol/alcohol.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/scheme', component: () => import('@/modules/report/scheme.page.vue'), meta: { title: '调理方案', toggle: true } },
     { path: '/report/:id', component: () => import('@/modules/report/report.page.vue'), meta: { title: '健康分析报告' } },
+    { path: '/scheme/:id', component: () => import('@/modules/report/scheme.page.vue'), meta: { title: '调理方案', toggle: false, } },
     { path: '/', redirect: '/screen' },
   ],
 });

+ 1 - 0
src/tools/index.ts

@@ -1,3 +1,4 @@
 export * from './url.tool';
 export * from './string.tool';
 export * from './polyfills';
+export * from './uuid.tool';

+ 28 - 0
src/tools/uuid.tool.ts

@@ -0,0 +1,28 @@
+interface RandomUUID {
+  (): `${string}-${string}-${string}-${string}-${string}`;
+}
+
+let randomUUID: RandomUUID = () => {
+  try {
+    if (typeof crypto?.randomUUID === 'function') randomUUID = crypto.randomUUID.bind(crypto);
+    else {
+      randomUUID = () => {
+        const temp_url = URL.createObjectURL(new Blob());
+        const uuid = temp_url.toString();
+        return (URL.revokeObjectURL(temp_url), uuid.split('/').pop()) as ReturnType<typeof randomUUID>;
+      };
+    }
+    return randomUUID();
+  } catch (e) {
+    randomUUID = () => {
+      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+        const r = (Math.random() * 16) | 0;
+        const v = c === 'x' ? r : (r & 0x3) | 0x8;
+        return v.toString(16);
+      }) as ReturnType<typeof randomUUID>;
+    };
+  }
+  return randomUUID();
+};
+
+export { randomUUID };