Bläddra i källkod

修改开始操作组件

cc12458 2 månader sedan
förälder
incheckning
44fef52c51

+ 0 - 36
src/components/Start.vue

@@ -1,36 +0,0 @@
-<script setup lang="ts">
-import { registerVisitorMethod } from '@/request/api';
-import { Notify } from '@/platform';
-import { useFlowStore, useVisitor } from '@/stores';
-
-const Visitor = useVisitor();
-const Flow = useFlowStore();
-const { flow } = storeToRefs(Flow);
-const handle = async () => {
-  Visitor.$reset();
-  if (flow.value.next === '/register' || (await register())) await Flow.router.push();
-};
-
-const registering = ref(false);
-const register = async () => {
-  registering.value = true;
-  try {
-    Visitor.patientId = await registerVisitorMethod().send(true);
-  } catch (error: any) {
-    Notify.error(error.message);
-  }
-  registering.value = false;
-  return Visitor.patientId;
-};
-</script>
-
-<template>
-  <div class="van-button decorate" @click="!registering && handle()">
-    <div class="van-button__content">
-      <van-loading v-if="registering" />
-      <span v-else class="van-button__text">开始检测</span>
-    </div>
-  </div>
-</template>
-
-<style scoped lang="scss"></style>

+ 22 - 0
src/composables/start/Start.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts">
+import { useStart, startDirective } from '@/composables/start';
+
+const vStart = startDirective;
+const { loading, handle, extra } = useStart();
+</script>
+
+<template>
+  <div
+    class="van-button decorate"
+    data-start="true"
+    v-start="extra"
+    @click="!loading && handle()"
+  >
+    <div class="van-button__content">
+      <van-loading v-if="loading" />
+      <span v-else class="van-button__text">开始检测</span>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss"></style>

+ 3 - 0
src/composables/start/index.ts

@@ -0,0 +1,3 @@
+export { default as Start } from './Start.vue';
+export { startDirective } from './start.directive';
+export { useStart } from './useStart';

+ 110 - 0
src/composables/start/start.directive.ts

@@ -0,0 +1,110 @@
+import type { DirectiveBinding, ObjectDirective } from 'vue';
+
+export const START_HOLD_EVENT = 'start-hold';
+
+export interface StartDirectiveValue {
+  threshold: number;
+  fn?: () => void;
+  /** 默认派发 `start-hold`;`false` 时不派发 */
+  emit?: boolean;
+}
+
+type StartBinding = DirectiveBinding<StartDirectiveValue>;
+
+type Entry = {
+  binding: StartBinding;
+};
+
+const entries = new WeakMap<HTMLElement, Entry>();
+
+const listenOpts: AddEventListenerOptions = { capture: true };
+
+function resolveMode(modifiers: StartBinding['modifiers']): 'duration' | 'distance' {
+  if (modifiers.distance && modifiers.duration) {
+    console.warn('[v-start] Both .duration and .distance are set; using .distance.');
+    return 'distance';
+  }
+  if (modifiers.distance) return 'distance';
+  return 'duration';
+}
+
+function readValue(binding: StartBinding): StartDirectiveValue | null {
+  const v = binding.value;
+  if (v == null || typeof v !== 'object') return null;
+  if (typeof v.threshold !== 'number' || Number.isNaN(v.threshold)) return null;
+  return v;
+}
+
+function mount(el: HTMLElement, binding: StartBinding) {
+  let down: { timeStamp: number; clientX: number; clientY: number; pointerId: number } | null =
+    null;
+
+  const onPointerDown = (e: PointerEvent) => {
+    down = {
+      timeStamp: e.timeStamp,
+      clientX: e.clientX,
+      clientY: e.clientY,
+      pointerId: e.pointerId,
+    };
+  };
+
+  const onPointerUp = (e: PointerEvent) => {
+    if (!down || e.pointerId !== down.pointerId) return;
+
+    const entry = entries.get(el);
+    if (!entry) return;
+
+    const value = readValue(entry.binding);
+    const mode = resolveMode(entry.binding.modifiers);
+
+    const start = down;
+    down = null;
+
+    if (!value) return;
+
+    const delta =
+      mode === 'duration'
+        ? e.timeStamp - start.timeStamp
+        : Math.hypot(e.clientX - start.clientX, e.clientY - start.clientY);
+
+    if (delta <= value.threshold) return;
+
+    value.fn?.();
+
+    if (value.emit !== false) {
+      const detail =
+        mode === 'duration' ? { durationMs: delta } : { distancePx: delta };
+      el.dispatchEvent(
+        new CustomEvent(START_HOLD_EVENT, {
+          detail,
+          bubbles: true,
+        }),
+      );
+    }
+  };
+
+  el.addEventListener('pointerdown', onPointerDown, listenOpts);
+  el.addEventListener('pointerup', onPointerUp, listenOpts);
+
+  entries.set(el, { binding });
+
+  return () => {
+    el.removeEventListener('pointerdown', onPointerDown, listenOpts);
+    el.removeEventListener('pointerup', onPointerUp, listenOpts);
+    entries.delete(el);
+  };
+}
+
+export const startDirective: ObjectDirective<HTMLElement, StartDirectiveValue> = {
+  mounted(el, binding) {
+    (el as HTMLElement & { __startCleanup?: () => void }).__startCleanup = mount(el, binding);
+  },
+  updated(el, binding) {
+    const e = entries.get(el);
+    if (e) e.binding = binding;
+  },
+  unmounted(el) {
+    (el as HTMLElement & { __startCleanup?: () => void }).__startCleanup?.();
+    (el as HTMLElement & { __startCleanup?: () => void }).__startCleanup = undefined;
+  },
+};

+ 49 - 0
src/composables/start/useStart.ts

@@ -0,0 +1,49 @@
+import type { StartDirectiveValue } from '@/composables/start/start.directive';
+
+import { showLoadingToast } from 'vant';
+import { Notify } from '@/platform';
+import { registerVisitorMethod } from '@/request/api';
+import { useFlowStore, useVisitor } from '@/stores';
+
+const showLoading = () => {
+  const instance = showLoadingToast({
+    message: '加载中...',
+    forbidClick: true,
+    duration: 0,
+  });
+  return () => instance.close();
+}
+
+export function useStart(ui?: boolean) {
+  const Visitor = useVisitor();
+  const Flow = useFlowStore();
+  const { flow } = storeToRefs(Flow);
+
+  const extra: StartDirectiveValue = {
+    threshold: 1000 * 5,
+    fn() {},
+  };
+
+  const register = async () => {
+    loading.value = true;
+    const close = ui ? showLoading() : void 0;
+    try {
+      Visitor.patientId = await registerVisitorMethod().send(true);
+      return Visitor.patientId;
+    } catch (error: any) {
+      Notify.error(error.message);
+    } finally {
+      loading.value = false;
+      close?.()
+    }
+  };
+
+  const loading = ref(false);
+  async function handle() {
+    if (loading.value) return;
+    Visitor.$reset();
+    if (flow.value.next === '/register' || (await register())) await Flow.router.push();
+  }
+
+  return { extra, loading, handle };
+}

+ 1 - 1
src/pages/scan.page.vue

@@ -2,7 +2,7 @@
 import { useRequest } from 'alova/client';
 import { getApplicationMethod } from '@/request/api';
 
-import Start from '@/components/Start.vue';
+import { Start } from '@/composables/start';
 
 const { data } = useRequest(getApplicationMethod, { initialData: { image: { el: '', copyright: '' } } });
 const title = computed(() => data.value.image.title || import.meta.env.SIX_APP_TITLE);

+ 1 - 1
src/pages/screen.page.vue

@@ -6,7 +6,7 @@ import { useElementSize } from '@vueuse/core';
 import { useRequest }     from 'alova/client';
 import p5                 from 'p5';
 
-import Start from '@/components/Start.vue';
+import { Start } from '@/composables/start';
 
 const { data } = useRequest(getApplicationMethod, { initialData: { image: { el: '', copyright: '' } } });
 const title = computed(() => data.value.image.title || import.meta.env.SIX_APP_TITLE);