feat(bx-mobile): add optional apply-attachment uploader on reimburse apply

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-09-05 17:17:21 +08:00
1 parent 6adb3cee9d
commit 8d14909165
1 file changed
+113
@@ -262,6 +262,68 @@
</div>
</van-popup>
<!-- 申请附件(可选) -->
<div class="form-card">
<div class="form-header">
<h3>申请附件(可选)</h3>
</div>
<p class="form-tip">补充行程单、说明截图、合同等;与发票夹无关。</p>
<div class="uploader-slot-btn advance-attachment-upload">
<van-uploader
:after-read="onAfterReadApplyAttachment"
:before-read="beforeReadApplyAttachment"
:disabled="applyAttachment.uploading.value"
multiple
result-type="file"
:accept="attachmentAccept"
>
<button
type="button"
class="btn btn-primary btn-sm"
:class="{ 'btn-loading': applyAttachment.uploading.value }"
:disabled="applyAttachment.uploading.value"
>
{{ applyAttachment.uploading.value ? '上传中…' : '上传附件' }}
</button>
</van-uploader>
</div>
<div v-if="applyAttachment.files.value.length" class="att-grid advance-att-grid">
<div
v-for="(file, idx) in applyAttachment.files.value"
:key="file.id"
class="att-img advance-att-tile"
:class="{ 'is-uploading': file._pending }"
:title="file.fileName"
role="button"
tabindex="0"
@click="applyAttachment.open(file)"
@keydown.enter.prevent="applyAttachment.open(file)"
>
<img
v-if="applyAttachment.isImage(file.fileType) && file.previewUrl"
:src="file.previewUrl"
:alt="file.fileName"
/>
<div v-else class="att-file-icon att-doc-thumb">
<span class="att-doc-thumb__ext">{{ applyAttachment.fileExt(file.fileName) }}</span>
<span class="att-doc-thumb__name">{{ file.fileName }}</span>
</div>
<div v-if="file._pending" class="att-upload-mask" aria-label="上传中">
<van-loading size="22px" color="#fff" />
<span class="att-upload-mask__text">上传中</span>
</div>
<button
v-if="!file._pending"
type="button"
class="del"
@click.stop="applyAttachment.remove(idx)"
>
×
</button>
</div>
</div>
</div>
<!-- 报销说明 -->
<div class="form-card">
<div class="form-header">
@@ -364,10 +426,39 @@ import {
type ExpenseItem,
} from '@/hooks/useExpenseItemTree'
import { useApplicantOptions } from '@/hooks/useApplicantOptions'
import { useAttachment } from '@/hooks/useAttachment'
import { useAuthStore } from '@/stores/auth'
import { SUBMIT_APPROVE_BUTTON_COLOR } from '@/constants/dialog'
import BottomActionBar from '@/components/mobile/BottomActionBar.vue'
const MAX_ATTACHMENTS = 10
/** 与 Web BxAttachmentUpload 对齐:图片/视频/文档/压缩包 */
const attachmentAccept = [
'image/*',
'video/*',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain',
'text/csv',
'application/rtf',
'application/zip',
'application/x-zip-compressed',
'application/x-rar-compressed',
'application/x-7z-compressed',
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.txt', '.csv', '.rtf', '.wps', '.et', '.dps',
'.zip', '.rar', '.7z',
'.mp4', '.webm', '.mov', '.avi',
].join(',')
const applyAttachment = useAttachment()
const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
@@ -660,6 +751,28 @@ function removeInvoicePick(key: string) {
selectedInvoicePicks.value = selectedInvoicePicks.value.filter((p) => p.key !== key)
}
function beforeReadApplyAttachment(file: File | File[]) {
const batch = Array.isArray(file) ? file : [file]
const remain = MAX_ATTACHMENTS - applyAttachment.files.value.length
if (remain <= 0) {
showToast(`最多上传 ${MAX_ATTACHMENTS} 个附件`)
return false
}
if (batch.length > remain) {
showToast(`还可上传 ${remain} 个附件,超出部分已忽略`)
return Promise.resolve(batch.slice(0, remain))
}
return true
}
async function onAfterReadApplyAttachment(uploadFile: any) {
const files = Array.isArray(uploadFile) ? uploadFile : [uploadFile]
for (const f of files) {
if (!f?.file) continue
await applyAttachment.upload(f.file)
}
}
function lineExpenseDisplay(id: number | string): string {
if (id === '' || id == null) return ''
return formatExpenseName(id, flatList.value)