170 lines
4.3 KiB
Vue
170 lines
4.3 KiB
Vue
|
|
<!--
|
|||
|
|
功能:提供单个配送合同 PDF 附件的选择、拖拽、预览和移除控件。
|
|||
|
|
版本:v1.0.0
|
|||
|
|
-->
|
|||
|
|
<template>
|
|||
|
|
<div
|
|||
|
|
class="contract-file-control"
|
|||
|
|
:class="{ 'contract-file-disabled': disabled }"
|
|||
|
|
:tabindex="disabled ? -1 : 0"
|
|||
|
|
role="button"
|
|||
|
|
:aria-disabled="disabled"
|
|||
|
|
aria-label="选择合同附件 PDF"
|
|||
|
|
@click="openPicker"
|
|||
|
|
@keydown.enter.prevent="openPicker"
|
|||
|
|
@keydown.space.prevent="openPicker"
|
|||
|
|
@dragover.prevent
|
|||
|
|
@drop.prevent="selectDroppedFile"
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
ref="fileInput"
|
|||
|
|
class="contract-file-input"
|
|||
|
|
type="file"
|
|||
|
|
accept=".pdf,application/pdf"
|
|||
|
|
:disabled="disabled"
|
|||
|
|
@change="selectPickedFile"
|
|||
|
|
/>
|
|||
|
|
<div class="contract-file-main">
|
|||
|
|
<icon-upload />
|
|||
|
|
<div>
|
|||
|
|
<strong>{{ title }}</strong>
|
|||
|
|
<div class="contract-file-hint">点击选择或拖拽 PDF,最大 10 MiB</div>
|
|||
|
|
<div v-if="attachment.requiresReupload" class="contract-file-warning">
|
|||
|
|
旧附件地址不可用,请重新上传
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<a-space @click.stop @keydown.stop>
|
|||
|
|
<a-button
|
|||
|
|
v-if="attachment.available && !attachment.selectedFile && !attachment.removed"
|
|||
|
|
size="small"
|
|||
|
|
@click="emit('preview')"
|
|||
|
|
>
|
|||
|
|
<template #icon><icon-eye /></template>预览
|
|||
|
|
</a-button>
|
|||
|
|
<a-button
|
|||
|
|
v-if="attachment.selectedFile"
|
|||
|
|
size="small"
|
|||
|
|
@click="emit('clear-selection')"
|
|||
|
|
>
|
|||
|
|
取消选择
|
|||
|
|
</a-button>
|
|||
|
|
<a-button
|
|||
|
|
v-else-if="attachment.hasExisting && !attachment.removed"
|
|||
|
|
size="small"
|
|||
|
|
status="danger"
|
|||
|
|
@click="emit('remove')"
|
|||
|
|
>
|
|||
|
|
<template #icon><icon-delete /></template>移除
|
|||
|
|
</a-button>
|
|||
|
|
</a-space>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { IconDelete, IconEye, IconUpload } from '@arco-design/web-vue/es/icon';
|
|||
|
|
import { computed, ref } from 'vue';
|
|||
|
|
|
|||
|
|
export type ContractAttachmentFieldState = {
|
|||
|
|
selectedFile?: File;
|
|||
|
|
hasExisting: boolean;
|
|||
|
|
available: boolean;
|
|||
|
|
requiresReupload: boolean;
|
|||
|
|
removed: boolean;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const props = defineProps<{
|
|||
|
|
attachment: ContractAttachmentFieldState;
|
|||
|
|
disabled?: boolean;
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
select: [file: File];
|
|||
|
|
remove: [];
|
|||
|
|
'clear-selection': [];
|
|||
|
|
preview: [];
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const fileInput = ref<HTMLInputElement>();
|
|||
|
|
const title = computed(() => {
|
|||
|
|
if (props.attachment.selectedFile) return props.attachment.selectedFile.name;
|
|||
|
|
if (props.attachment.removed) return '未选择合同附件';
|
|||
|
|
if (props.attachment.hasExisting) return '合同附件.pdf';
|
|||
|
|
return '选择合同附件';
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/** 打开组件内唯一的原生文件选择器。 */
|
|||
|
|
function openPicker() {
|
|||
|
|
if (!props.disabled) fileInput.value?.click();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 读取点击选择的单个文件,并允许再次选择同名文件。 */
|
|||
|
|
function selectPickedFile(event: Event) {
|
|||
|
|
const input = event.target as HTMLInputElement;
|
|||
|
|
const file = input.files?.[0];
|
|||
|
|
if (file) emit('select', file);
|
|||
|
|
input.value = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 接收拖拽到控件上的第一个文件。 */
|
|||
|
|
function selectDroppedFile(event: DragEvent) {
|
|||
|
|
if (props.disabled) return;
|
|||
|
|
const file = event.dataTransfer?.files?.[0];
|
|||
|
|
if (file) emit('select', file);
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="less">
|
|||
|
|
.contract-file-control {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 16px;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
min-height: 88px;
|
|||
|
|
padding: 16px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
border: 1px dashed var(--color-border-3);
|
|||
|
|
border-radius: 6px;
|
|||
|
|
outline: none;
|
|||
|
|
transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
|
|||
|
|
}
|
|||
|
|
.contract-file-control:hover,
|
|||
|
|
.contract-file-control:focus-visible {
|
|||
|
|
background: var(--color-fill-1);
|
|||
|
|
border-color: rgb(var(--primary-6));
|
|||
|
|
}
|
|||
|
|
.contract-file-control:focus-visible {
|
|||
|
|
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.2);
|
|||
|
|
}
|
|||
|
|
.contract-file-disabled {
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
opacity: 0.65;
|
|||
|
|
}
|
|||
|
|
.contract-file-input {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
.contract-file-main {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
align-items: center;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
.contract-file-main strong {
|
|||
|
|
display: block;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--color-text-1);
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.contract-file-hint {
|
|||
|
|
margin-top: 4px;
|
|||
|
|
color: var(--color-text-3);
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
.contract-file-warning {
|
|||
|
|
margin-top: 4px;
|
|||
|
|
color: rgb(var(--warning-6));
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
</style>
|