提交
This commit is contained in:
1 parent
135dd550b6
commit
2a4767a014
76 files changed
+25160
-2306
No files matched your search
@@ -1,7 +1,7 @@
|
||||
<!-- 财务确认弹窗:附件上传 + 财务备注(对齐采集协议单附件 UI) -->
|
||||
<template>
|
||||
<div class="protocol-finance-confirm-fields">
|
||||
<el-form-item label="财务备注" class="form-col-span-2">
|
||||
<el-form-item :label="remarkLabel" class="form-col-span-2">
|
||||
<el-input
|
||||
:model-value="financeRemark"
|
||||
type="textarea"
|
||||
@@ -10,7 +10,7 @@
|
||||
@update:model-value="emit('update:financeRemark', $event)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务确认附件" class="form-col-span-2">
|
||||
<el-form-item :label="attachmentLabel" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
@@ -94,8 +94,14 @@
|
||||
financeRemark: string
|
||||
attachments: FinanceAttachmentItem[]
|
||||
uploadFolder?: string
|
||||
remarkLabel?: string
|
||||
attachmentLabel?: string
|
||||
}>(),
|
||||
{ uploadFolder: 'protocol-finance-confirm' }
|
||||
{
|
||||
uploadFolder: 'protocol-finance-confirm',
|
||||
remarkLabel: '财务备注',
|
||||
attachmentLabel: '财务确认附件'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -144,6 +150,14 @@
|
||||
emit('update:attachments', next)
|
||||
}
|
||||
|
||||
/** Mutable snapshot used to avoid race conditions when multiple files are picked at once */
|
||||
let _pendingList: FinanceAttachmentItem[] | null = null
|
||||
|
||||
function _getList(): FinanceAttachmentItem[] {
|
||||
if (_pendingList) return _pendingList
|
||||
return props.attachments
|
||||
}
|
||||
|
||||
async function handleAttachmentChange(uploadFile: { raw?: File }) {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
@@ -152,7 +166,7 @@
|
||||
ElMessage.error(`文件大小不能超过 ${maxSizeMb}MB`)
|
||||
return
|
||||
}
|
||||
const tempId = `temp_${Date.now()}`
|
||||
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
const uploading: FinanceAttachmentItem = {
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
@@ -160,29 +174,38 @@
|
||||
fileType: file.type || '',
|
||||
_uploading: true
|
||||
}
|
||||
const listWithTemp = [...props.attachments, uploading]
|
||||
// Append to the latest snapshot so concurrent calls don't overwrite each other
|
||||
const current = _getList()
|
||||
const listWithTemp = [...current, uploading]
|
||||
_pendingList = listWithTemp
|
||||
emit('update:attachments', listWithTemp)
|
||||
const idx = listWithTemp.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', props.uploadFolder)
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
const updated = [...listWithTemp]
|
||||
updated[idx] = {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
fileType: res.fileType || file.type || '',
|
||||
_uploading: false
|
||||
}
|
||||
// Read from latest snapshot (NOT the closure-captured listWithTemp) to avoid
|
||||
// overwriting sibling uploads that may have already completed while this one was in-flight
|
||||
const latest = _pendingList || props.attachments
|
||||
const updated = latest.map((a) =>
|
||||
a.id === tempId
|
||||
? {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
fileType: res.fileType || file.type || '',
|
||||
_uploading: false
|
||||
}
|
||||
: a
|
||||
)
|
||||
_pendingList = updated
|
||||
emit('update:attachments', updated)
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
emit(
|
||||
'update:attachments',
|
||||
props.attachments.filter((a) => a.id !== tempId)
|
||||
)
|
||||
const latest = _pendingList || props.attachments
|
||||
const remaining = latest.filter((a) => a.id !== tempId)
|
||||
_pendingList = remaining.length > 0 ? remaining : null
|
||||
emit('update:attachments', remaining)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -203,7 +226,7 @@
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 120px;
|
||||
width: 140px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 8px;
|
||||
@@ -236,18 +259,26 @@
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.4;
|
||||
line-height: 1;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 120px;
|
||||
width: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
&.is-uploading {
|
||||
opacity: 0.6;
|
||||
@@ -263,58 +294,84 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
|
||||
&--video {
|
||||
color: #10b981;
|
||||
background: #ede9fe;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
&--doc {
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
color: #334155;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
min-height: calc(11px * 1.4 * 2);
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
background: #f1f5f9;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: #10b981;
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -46,7 +46,7 @@
|
||||
<dd>{{ formatPaymentDueDate(detail?.installmentDueDate) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>应缴费金额</dt>
|
||||
<dt>应缴费金额(元)</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.dueAmount) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
@@ -54,7 +54,7 @@
|
||||
<dd>{{ transferTypeLabel(String(detail?.transferType || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>转账金额</dt>
|
||||
<dt>转账金额(元)</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.transferAmount) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
@@ -80,7 +80,7 @@
|
||||
<h4 v-if="refundApplyTitle" class="detail-block-title">{{ refundApplyTitle }}</h4>
|
||||
<dl class="view-kv-grid view-kv-grid--4">
|
||||
<div class="view-kv">
|
||||
<dt>退费金额</dt>
|
||||
<dt>退费金额(元)</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.refundAmount) }}</dd>
|
||||
</div>
|
||||
<div v-if="detail?.refundRemark" class="view-kv view-kv-span-2">
|
||||
|
||||
Reference in new issue
Block a user