Files
platforms/frontend/platform_admin/src/views/resource/ContractAttachmentField.vue
czl231 99324acc89 feat: 新增配送合同附件安全上传功能
- 增加合同专用 PDF 上传、鉴权预览和失败清理接口
- 支持草稿附件替换移除、并发保护和启用完整性校验
- 修复循环模板引用导致文件选择器无法打开的问题
- 补充专项测试、中文项目文档和操作日志
2026-08-12 21:38:49 +08:00

170 lines
4.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
功能提供单个配送合同 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>