Explorar o código

📦 传承人协议2

快乐的梦鱼 hai 4 días
pai
achega
1ee006c518

+ 1 - 0
src/components/form/Field.vue

@@ -192,6 +192,7 @@
       @click="onClear"
     />
   </Touchable>
+  <slot name="extra" />
 </template>
 
 <script setup lang="ts">

+ 71 - 23
src/components/form/Signature.vue

@@ -1,42 +1,53 @@
 <template>
   <view 
+    :id="containerId"
     class="signature-container"
     :style="containerStyle"
   >
+    <text v-if="placeholder" :style="placeholderStyle">{{ placeholder }}</text>
     <canvas
       class="signature-canvas"
       disable-scroll
       :id="id"
       :canvas-id="id"
       :style="canvasStyle"
-      @touchstart="handleTouchStart"
-      @touchmove="handleTouchMove"
-      @touchend="handleTouchEnd"
-      @touchcancel="handleTouchEnd"
-      @mousedown="handleMouseDown"
-      @mousemove="handleMouseMove"
-      @mouseup="handleMouseUp"
+      :width="canvasWidth"
+      :height="canvasHeight"
+      @touchstart.stop="handleTouchStart"
+      @touchmove.stop="handleTouchMove"
+      @touchend.stop="handleTouchEnd"
+      @touchcancel.stop="handleTouchEnd"
+      @mousedown.stop="handleMouseDown"
+      @mousemove.stop="handleMouseMove"
+      @mouseup.stop ="handleMouseUp"
     ></canvas>
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, type Ref, computed, getCurrentInstance, onBeforeUnmount } from 'vue';
+import { ref, onMounted, type Ref, computed, getCurrentInstance, onBeforeUnmount, nextTick } from 'vue';
 import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
-import { RandomUtils } from '@imengyu/imengyu-utils';
+import { RandomUtils, waitTimeOut } from '@imengyu/imengyu-utils';
 
 const id = `signatureCanvas${RandomUtils.genNonDuplicateID(10)}`;
+const containerId = `signatureContainer${RandomUtils.genNonDuplicateID(10)}`;
 
 // 定义绘图相关类型
 interface Point { x: number; y: number; }
 interface Line { points: Point[]; color: string; width: number; }
 
 export interface SignatureProps {
+  innerStyle?: object;
+  placeholderStyle?: object;
   backgroundColor?: string;
   lineColor?: string;
   lineWidth?: number;
   round?: boolean;
   border?: boolean;
+  borderStyle?: string;
+  borderWidth?: number;
+  borderColor?: string;
+  placeholder?: string;
 }
 export interface SignatureInstance {
   /** 
@@ -44,6 +55,10 @@ export interface SignatureInstance {
    */
   clear: () => void;
   /** 
+   * 设置签名图片 
+   */
+  setImage: (image: string) => void;
+  /** 
    * 导出签名为图片 
    */
   export: () => Promise<string>;
@@ -55,8 +70,10 @@ const props = withDefaults(defineProps<SignatureProps>(), {
   lineWidth: () => propGetThemeVar('SignatureLineWidth', 3),
   round: () => propGetThemeVar('SignatureRound', true),
   border: () => propGetThemeVar('SignatureBorder', true),
-  borderWidth: () => propGetThemeVar('SignatureBorderWidth', 2),
-  borderColor: () => propGetThemeVar('SignatureBorderColor', 'border.cell'),
+  borderWidth: () => propGetThemeVar('SignatureBorderWidth', 3),
+  borderStyle: () => propGetThemeVar('SignatureBorderStyle', 'dashed'),
+  borderColor: () => propGetThemeVar('SignatureBorderColor', 'border.signature'),
+  placeholder: () => propGetThemeVar('SignaturePlaceholder', '请在虚线框内签名'),
 });
 
 const theme = useTheme();
@@ -66,8 +83,14 @@ const containerStyle = computed(() => ({
   backgroundColor: theme.resolveThemeColor(props.backgroundColor),
   borderRadius: props.round ? theme.resolveThemeSize('SignatureBorderRadius', 12) : '0',
   border: props.border ? 
-    `${theme.resolveThemeSize(props.borderWidth)}px solid ${theme.resolveThemeColor(props.borderColor)}`
-    : 'none'
+    `${theme.resolveThemeSize(props.borderWidth)} ${props.borderStyle} ${theme.resolveThemeColor(props.borderColor)}`
+    : 'none',
+  ...props.innerStyle
+}));
+const placeholderStyle = computed(() => ({
+  fontSize: theme.resolveThemeSize('SignaturePlaceholderFontSize', 26),
+  color: theme.resolveThemeColor('SignaturePlaceholderColor', 'text.second'),
+  ...props.placeholderStyle
 }));
 const canvasStyle = computed(() => ({
   width: `${canvasWidth.value}px`,
@@ -85,27 +108,38 @@ let timer = 0;
 let isDrawing = false;
 let absPos = [0,0];
 let isDirty = true;
+let isCreating = false;
 
 async function initCanvas() {
+  if (isCreating)
+    return;
+  isCreating = true;
+  await waitTimeOut(100);
   // 获取容器尺寸信息
   containerRect.value = await new Promise<UniApp.NodeInfo>((resolve) => {
     uni.createSelectorQuery()
       .in(instance)
-      .select('.signature-container')
+      .select(`#${containerId}`)
       .boundingClientRect(resolve as any)
       .exec();
   });
 
   if (containerRect.value) {
-    canvasWidth.value = containerRect.value.width!;
-    canvasHeight.value = containerRect.value.height!;
+    canvasWidth.value = containerRect.value.width! - 2;
+    canvasHeight.value = containerRect.value.height! - 2;
   }
-
-  // 创建Canvas上下文
-  canvasContext.value = uni.createCanvasContext(id, instance);
-  timer = setInterval(render, 50) as any;
+  if (canvasWidth.value > 0 && canvasHeight.value > 0) {
+    if (!canvasContext.value)
+      // 创建Canvas上下文
+      canvasContext.value = uni.createCanvasContext(id, instance);
+  }
+  isCreating = false;
 }
 function render() {
+  if (canvasWidth.value <= 0 || canvasHeight.value <= 0) {
+    initCanvas();
+    return;
+  }
   if (!canvasContext.value)
     return;
   if (!isDirty)
@@ -142,7 +176,7 @@ function endDrag() {
 }
 
 function handleTouchStart(e: any) {
-  if (!canvasContext.value) return;
+  if (!canvasContext.value) return;   
   const { x, y } = e.touches[0];
   startDrag(x, y);
 }
@@ -233,15 +267,26 @@ async function exportImage(): Promise<string> {
     }, instance);
   });
 }
+function setImage(image: string) {
+  if (!canvasContext.value) return;
+  canvasContext.value.drawImage(image, 0, 0, canvasWidth.value, canvasHeight.value);
+  isDirty = true;
+}
 
-onMounted(initCanvas);
+onMounted(async () => {
+  await nextTick();
+  await initCanvas();
+  timer = setInterval(render, 100) as any;
+});
 onBeforeUnmount(() => {
   clearInterval(timer);
+  timer = 0;
 });
 
 defineExpose<SignatureInstance>({
   clear,
-  export: exportImage
+  export: exportImage,
+  setImage
 });
 </script>
 
@@ -250,6 +295,9 @@ defineExpose<SignatureInstance>({
   width: 100%;
   height: 200px;
   position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
 }
 .signature-canvas {
   width: 100%;

+ 88 - 0
src/components/form/SignatureField.vue

@@ -0,0 +1,88 @@
+<template>
+  <Popup
+    v-model:show="popupShow"
+    :closeIcon="false"
+    position="bottom"
+    size="auto"
+    closeable
+  >
+    <FlexCol :padding="16">
+      <ActionSheetTitle 
+        cancelText="取消"
+        confirmText="确定"
+        v-bind="titleProps"
+        :title="title"
+        @cancel="popupShow = false"
+        @confirm="onConfirm"
+      />
+      <Signature ref="signatureRef" :innerStyle="{
+        width: '100%',
+        height: '400rpx',
+      }" />
+    </FlexCol>
+  </Popup> 
+  <FlexCol width="100%" :gap="10">
+    <Text v-if="!value && placeholder" :text="placeholder" />
+    <Image v-else-if="value" :src="value" width="100%" mode="widthFix" />
+  </FlexCol>
+</template>
+
+<script setup lang="ts">
+import { ref, toRef } from 'vue';
+import type { SignatureInstance } from '@/components/form/Signature.vue';
+import { useFieldChildValueInjector } from './FormContext';
+import Signature from '@/components/form/Signature.vue';
+import FlexCol from '../layout/FlexCol.vue';
+import Image from '../basic/Image.vue';
+import Popup from '../dialog/Popup.vue';
+import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSheetTitle.vue';
+import Text from '../basic/Text.vue';
+
+const props = withDefaults(defineProps<{
+  modelValue?: string | null;
+  initalValue?: string | null;
+  title?: string;
+  titleProps?: ActionSheetTitleProps;
+  placeholder?: string;
+}>(), {
+  placeholder: '请签名',
+  title: '签名',
+});
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string | null): void;
+}>();
+
+const popupShow = ref(false);
+
+const {
+  value,
+  updateValue,
+} = useFieldChildValueInjector(
+  toRef(props, 'modelValue'), 
+  (v) => emit('update:modelValue', v ?? null),
+  undefined,
+  () => {
+    popupShow.value = true;
+  },
+  props.initalValue,
+);
+
+const signatureRef = ref<SignatureInstance>();
+
+function onConfirm() {
+  if (signatureRef.value) {
+    uni.showLoading();
+    signatureRef.value.export().then((res) => {
+      updateValue(res);
+      popupShow.value = false;
+    }).catch((e) => {
+      uni.showToast({
+        title: e.message,
+        icon: 'none',
+      })
+    }).finally(() => {
+      uni.hideLoading();
+    });
+  }
+}
+</script>

+ 1 - 0
src/components/theme/Theme.ts

@@ -123,6 +123,7 @@ export const DefaultTheme : ThemeConfig = {
     border: {
       input: '#dadada',
       default: '#dddddd',
+      signature: '#333',
       cell: '#efefef',
       light: '#eeeeee',
     },

+ 17 - 5
src/pages/collect/assessment/argeement-sign.vue

@@ -98,7 +98,9 @@
             <Height :height="16" />
 
             <FlexCol :gap="'md'" :inner-style="signBlockStyle">
-              <Text font-config="p" color="text.content" bold>甲方:福建省文化和旅游厅</Text>
+              <FlexRow padding="space.sm">
+                <Text font-config="p" color="text.content" bold>甲方:福建省文化和旅游厅</Text>
+              </FlexRow>
               <Field
                 label="负责人(代表人)"
                 label-position="top"
@@ -120,13 +122,22 @@
 
               <Height :height="24" />
 
-              <Text font-config="p" color="text.content" bold>乙方:{{ currentAgreement.partyB }}(签名)</Text>
+              <FlexRow padding="space.sm">
+                <Text font-config="p" color="text.content" bold>乙方:{{ currentAgreement.partyB }}(签名)</Text>
+              </FlexRow>
               <Field
                 label="乙方签名 / 说明"
                 label-position="top"
-                v-model="currentAgreement.partyBSign"
-                placeholder="签名、捺印说明或电子签说明"
-              />
+                showRightArrow
+              >
+                <Text>点击签名</Text>
+                <template #extra>
+                  <SignatureField
+                    v-model="currentAgreement.partyBSign"
+                    placeholder=""
+                  />
+                </template>
+              </Field>
               <Field
                 label="身份证号"
                 label-position="top"
@@ -182,6 +193,7 @@ import Field from '@/components/form/Field.vue';
 import Text from '@/components/basic/Text.vue';
 import AgreementPrefillInline from './components/AgreementPrefillInline.vue';
 import AgreementDateWriteBlock, { type AgreementYmdParts } from './components/AgreementDateWriteBlock.vue';
+import SignatureField from '@/components/form/SignatureField.vue';
 
 const authStore = useAuthStore();
 const currentAgreement = ref<AgreementDetail | null>(null);