Explorar el Código

Merge branch 'feature/task-152' into develop

cc12458 hace 1 mes
padre
commit
2c1ad9c2ee

+ 109 - 4
src/modules/report/scheme.page.vue

@@ -8,6 +8,11 @@ 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();
@@ -31,6 +36,57 @@ 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>
@@ -50,12 +106,19 @@ 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">
@@ -75,6 +138,27 @@ function toggle() {
         <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">
@@ -104,4 +188,25 @@ function toggle() {
     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>

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

@@ -1,32 +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;
@@ -49,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,
+  }
+}

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