Files
ai-agent-admin/web/apps/web-ele/src/components/zq-form/signature-pad/signature-pad.vue
T

307 lines
7.1 KiB
Vue

<script setup lang="ts">
import type { SignaturePadEmits, SignaturePadProps } from './types';
import { computed, ref, watch } from 'vue';
import { Eraser, PenLine, Smartphone } from '@vben/icons';
import { $t } from '@vben/locales';
import { ElButton } from 'element-plus';
import { getFileUrl } from '#/composables/useFileUrl';
import MobileSignatureDialog from './MobileSignatureDialog.vue';
import SignatureDialog from './SignatureDialog.vue';
defineOptions({
name: 'SignaturePad',
inheritAttrs: false,
});
const props = withDefaults(defineProps<SignaturePadProps>(), {
penColor: '#000000',
penWidth: 2,
backgroundColor: '#ffffff',
width: '100%',
height: 200,
disabled: false,
readonly: false,
placeholder: '',
source: 'form',
});
const emit = defineEmits<SignaturePadEmits>();
// 状态
const signatureUrl = ref<string>('');
const isHovering = ref(false);
const signatureDialogVisible = ref(false);
const mobileSignatureDialogVisible = ref(false);
// 计算属性
const isDisabled = computed(() => props.disabled || props.readonly);
const hasSignature = computed(() => !!props.modelValue && !!signatureUrl.value);
const containerStyle = computed(() => ({
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
height: `${props.height}px`,
}));
const placeholderText = computed(
() => props.placeholder || $t('form-design.signaturePad.placeholder'),
);
// 打开立即签名弹窗
function openSignatureDialog() {
if (isDisabled.value) return;
signatureDialogVisible.value = true;
}
// 打开手机签名弹窗
function openMobileSignatureDialog() {
if (isDisabled.value) return;
mobileSignatureDialogVisible.value = true;
}
// 签名完成回调
function handleSignatureComplete(fileId: string) {
emit('update:modelValue', fileId);
emit('change', fileId);
emit('signed');
signatureDialogVisible.value = false;
mobileSignatureDialogVisible.value = false;
}
// 清除签名
function clearSignature() {
if (isDisabled.value) return;
signatureUrl.value = '';
emit('update:modelValue', null);
emit('change', null);
emit('cleared');
}
// 加载已有签名
async function loadSignature(fileId: string) {
if (!fileId) {
signatureUrl.value = '';
return;
}
try {
const url = await getFileUrl(fileId);
signatureUrl.value = url;
} catch (error) {
console.error('Load signature failed:', error);
signatureUrl.value = '';
}
}
// 监听 modelValue 变化
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
loadSignature(newValue);
} else {
signatureUrl.value = '';
}
},
{ immediate: true },
);
</script>
<template>
<div class="signature-pad">
<!-- 已有签名时显示图片 -->
<div
v-if="hasSignature"
class="signature-pad__preview"
:style="containerStyle"
@mouseenter="isHovering = true"
@mouseleave="isHovering = false"
>
<img :src="signatureUrl" alt="signature" class="signature-pad__image" />
<Transition name="fade">
<div v-if="isHovering && !isDisabled" class="signature-pad__overlay">
<div class="signature-pad__actions">
<ElButton
type="primary"
size="small"
plain
:icon="PenLine"
@click="openSignatureDialog"
>
{{ $t('form-design.signaturePad.resignNow') }}
</ElButton>
<ElButton
type="danger"
size="small"
:icon="Eraser"
@click="clearSignature"
>
{{ $t('form-design.signaturePad.clear') }}
</ElButton>
</div>
</div>
</Transition>
</div>
<!-- 未签名时显示触发区域 -->
<div
v-else
class="signature-pad__trigger"
:class="{
'signature-pad__trigger--disabled': isDisabled,
'signature-pad__trigger--hover': isHovering,
}"
:style="containerStyle"
@mouseenter="isHovering = true"
@mouseleave="isHovering = false"
>
<!-- 默认状态 -->
<div v-if="!isHovering || isDisabled" class="signature-pad__placeholder">
<PenLine class="signature-pad__placeholder-icon" />
<span class="text-sm">{{ placeholderText }}</span>
</div>
<!-- Hover 状态显示操作按钮 -->
<Transition name="fade">
<div v-if="isHovering && !isDisabled" class="signature-pad__actions">
<ElButton
type="primary"
plain
size="small"
:icon="PenLine"
@click="openSignatureDialog"
>
{{ $t('form-design.signaturePad.signNow') }}
</ElButton>
<ElButton
type="default"
size="small"
:icon="Smartphone"
@click="openMobileSignatureDialog"
>
{{ $t('form-design.signaturePad.mobileSign') }}
</ElButton>
</div>
</Transition>
</div>
<!-- 立即签名弹窗 -->
<SignatureDialog
v-model:visible="signatureDialogVisible"
:pen-color="penColor"
:pen-width="penWidth"
:background-color="backgroundColor"
:source="source"
@complete="handleSignatureComplete"
/>
<!-- 手机签名弹窗 -->
<MobileSignatureDialog
v-model:visible="mobileSignatureDialogVisible"
:source="source"
@complete="handleSignatureComplete"
/>
</div>
</template>
<style scoped>
.signature-pad {
width: 100%;
}
.signature-pad__trigger {
position: relative;
border: 2px dashed var(--el-border-color);
border-radius: 8px;
background-color: var(--el-fill-color-lighter);
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
}
.signature-pad__trigger:hover {
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
.signature-pad__trigger--disabled {
cursor: not-allowed;
opacity: 0.6;
}
.signature-pad__trigger--disabled:hover {
border-color: var(--el-border-color);
background-color: var(--el-fill-color-lighter);
}
.signature-pad__placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: var(--el-text-color-placeholder);
pointer-events: none;
user-select: none;
}
.signature-pad__placeholder-icon {
width: 30px;
height: 30px;
opacity: 0.5;
}
.signature-pad__actions {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
gap: 12px;
}
.signature-pad__preview {
position: relative;
border: 1px solid var(--el-border-color);
border-radius: 8px;
background-color: var(--el-fill-color-lighter);
overflow: hidden;
}
.signature-pad__image {
width: 100%;
height: 100%;
object-fit: contain;
}
.signature-pad__overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.4);
}
/* 过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>