- 增加合同专用 PDF 上传、鉴权预览和失败清理接口 - 支持草稿附件替换移除、并发保护和启用完整性校验 - 修复循环模板引用导致文件选择器无法打开的问题 - 补充专项测试、中文项目文档和操作日志
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>
|