feat: 备件出入库附件选择优化

master
zhongwenkai 4 days ago
parent 35065a0027
commit 2eeffb1fda

@ -67,22 +67,23 @@
<text class="section-title">附件</text> <text class="section-title">附件</text>
</view> </view>
<view class="attachment-area"> <view class="attachment-area">
<view class="attachment-list"> <view v-if="attachmentFileName" class="attachment-file-item">
<view v-for="(file, idx) in attachmentList" :key="idx" class="attachment-file-item"> <view class="file-icon">
<view class="file-icon"> <text class="file-icon-text">{{ getFileIcon(attachmentFileName) }}</text>
<text class="file-icon-text">{{ getFileIcon(file.name) }}</text> </view>
</view> <view class="file-info">
<view class="file-info"> <text class="file-name" :title="attachmentFileName">{{ attachmentFileName }}</text>
<text class="file-name" :title="file.name">{{ file.name }}</text> <text v-if="attachmentFileSize" class="file-size">{{ formatFileSize(attachmentFileSize) }}</text>
<text class="file-size">{{ formatFileSize(file.size) }}</text>
</view>
<view class="file-delete" @click="removeAttachment(idx)">
<text class="delete-icon-sm"></text>
</view>
</view> </view>
<view class="attachment-add-file" @click="handleAddAttachment"> <view v-if="uploadLoading" class="file-uploading">
<text class="add-text">选取文件</text> <text class="uploading-text">上传中...</text>
</view> </view>
<view v-else class="file-delete" @click="removeAttachment">
<text class="delete-icon-sm"></text>
</view>
</view>
<view class="attachment-add-file" @click="handleAddAttachment" v-if="!attachmentFileName">
<text class="add-text">选取文件</text>
</view> </view>
</view> </view>
</view> </view>
@ -143,6 +144,8 @@ import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue' import NavBar from '@/components/common/NavBar.vue'
import { createSparepartInbound } from '@/api/mes/sparepartInbound' import { createSparepartInbound } from '@/api/mes/sparepartInbound'
import { getSparepartDetail } from '@/api/mes/sparepart' import { getSparepartDetail } from '@/api/mes/sparepart'
import { getBaseUrl } from '@/utils/request'
import { getToken } from '@/utils/auth'
const { t } = useI18n() const { t } = useI18n()
@ -208,8 +211,11 @@ const selectedOperatorName = ref('')
// //
const remark = ref('') const remark = ref('')
// // web limit=1
const attachmentList = ref([]) const fileUrl = ref('') // JSON: {"fileName":"xxx.pdf","fileUrl":"https://..."}
const attachmentFileName = ref('') //
const attachmentFileSize = ref(0) //
const uploadLoading = ref(false) //
function formatDate(date) { function formatDate(date) {
const y = date.getFullYear() const y = date.getFullYear()
@ -288,42 +294,130 @@ function formatFileSize(bytes) {
return (bytes / (1024 * 1024)).toFixed(1) + ' MB' return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
} }
const ALLOWED_EXTS = ['png', 'jpg', 'jpeg', 'doc', 'xls', 'ppt', 'txt', 'pdf']
const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB
function handleAddAttachment() { function handleAddAttachment() {
// #ifdef APP-PLUS // #ifdef APP-PLUS
// APP plus.gallery
plus.gallery.pick( plus.gallery.pick(
(res) => { (res) => {
const files = (res?.files || []).map(f => ({ const file = res?.files?.[0]
name: f.name || 'unknown', if (!file) return
size: f.size || 0, const ext = (file.name || '').split('.').pop()?.toLowerCase()
path: f // file if (!ALLOWED_EXTS.includes(ext)) {
})) uni.showToast({ title: '不支持的文件类型:.' + (ext || '未知'), icon: 'none' })
attachmentList.value.push(...files) return
}, }
(e) => { if (file.size > MAX_FILE_SIZE) {
console.log('选择文件失败:', e) uni.showToast({ title: '文件大小不能超过100MB', icon: 'none' })
return
}
uploadFile(file)
}, },
{ filter: 'all', multiple: true, maximum: 9 - attachmentList.value.length } (e) => { console.log('选择文件失败:', e) },
{ filter: 'all', multiple: false }
) )
// #endif // #endif
// #ifndef APP-PLUS // #ifndef APP-PLUS
// APP chooseImage // APP uni.chooseFile
uni.chooseImage({ uni.chooseFile({
count: 9 - attachmentList.value.length, count: 1,
sizeType: ['compressed'], type: 'all',
sourceType: ['album', 'camera'],
success: (res) => { success: (res) => {
res.tempFilePaths.forEach(path => { const tempFile = res.tempFiles?.[0]
const parts = path.split('/') if (!tempFile) return
const name = parts[parts.length - 1] || 'image.jpg' const ext = (tempFile.name || '').split('.').pop()?.toLowerCase()
attachmentList.value.push({ name, size: 0, path }) if (!ALLOWED_EXTS.includes(ext)) {
uni.showToast({ title: '不支持的文件类型:.' + (ext || '未知'), icon: 'none' })
return
}
if (tempFile.size > MAX_FILE_SIZE) {
uni.showToast({ title: '文件大小不能超过100MB', icon: 'none' })
return
}
uploadFile(tempFile)
},
fail: () => {
// chooseFile chooseImage
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (imgRes) => {
const path = imgRes.tempFilePaths?.[0]
if (!path) return
const parts = path.split('/')
const name = parts[parts.length - 1] || 'image.jpg'
uploadFile({ path, name, size: 0 })
}
}) })
} }
}) })
// #endif // #endif
} }
function removeAttachment(idx) { async function uploadFile(file) {
attachmentList.value.splice(idx, 1) uploadLoading.value = true
attachmentFileName.value = file.name || 'unknown'
attachmentFileSize.value = file.size || 0
// form-data
const formData = {
file: file.path // uni.uploadFile files
}
try {
const res = await new Promise((resolve, reject) => {
uni.uploadFile({
url: getBaseUrl() + '/admin-api/infra/file/upload',
filePath: file.path,
name: 'file',
header: {
'Authorization': 'Bearer ' + (getToken() || ''),
'tenantId': '1'
},
success: (uploadRes) => {
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data)
resolve(data)
} catch (e) {
reject(new Error('解析上传结果失败'))
}
} else {
reject(new Error('上传失败,状态码:' + uploadRes.statusCode))
}
},
fail: (err) => {
reject(err)
}
})
})
if (res.code === 0 && res.data) {
const { fileName, fileUrl: url } = res.data
fileUrl.value = JSON.stringify({ fileName: fileName || file.name, fileUrl: url })
attachmentFileName.value = fileName || file.name
uni.showToast({ title: '上传成功', icon: 'success' })
} else {
throw new Error(res.msg || '上传失败')
}
} catch (e) {
attachmentFileName.value = ''
attachmentFileSize.value = 0
fileUrl.value = ''
const msg = e?.message || '上传失败'
uni.showToast({ title: String(msg).substring(0, 50), icon: 'none' })
} finally {
uploadLoading.value = false
}
}
function removeAttachment() {
attachmentFileName.value = ''
attachmentFileSize.value = 0
fileUrl.value = ''
} }
async function handleSubmit() { async function handleSubmit() {
@ -373,9 +467,9 @@ async function handleSubmit() {
items: items items: items
} }
// // web JSON
if (attachmentList.value.length) { if (fileUrl.value) {
submitData.attachments = attachmentList.value.map(f => f.path || f) submitData.fileUrl = fileUrl.value
} }
console.log('提交数据:', JSON.stringify(submitData)) console.log('提交数据:', JSON.stringify(submitData))
@ -561,6 +655,10 @@ onHide(() => {})
flex-shrink: 0; flex-shrink: 0;
} }
.delete-icon-sm { font-size: 22rpx; color: #dc2626; } .delete-icon-sm { font-size: 22rpx; color: #dc2626; }
.file-uploading {
flex-shrink: 0;
.uploading-text { font-size: 22rpx; color: #2563eb; }
}
.attachment-add-file { .attachment-add-file {
display: flex; align-items: center; justify-content: center; gap: 10rpx; display: flex; align-items: center; justify-content: center; gap: 10rpx;

@ -58,19 +58,24 @@
<text class="section-title">附件</text> <text class="section-title">附件</text>
</view> </view>
<view class="attachment-area"> <view class="attachment-area">
<view class="attachment-list"> <view v-if="attachmentFileName" class="attachment-file-item">
<view v-for="(file, idx) in attachmentList" :key="idx" class="attachment-file-item"> <view class="file-icon">
<view class="file-icon"><text class="file-icon-text">{{ getFileIcon(file.name) }}</text></view> <text class="file-icon-text">{{ getFileIcon(attachmentFileName) }}</text>
<view class="file-info"> </view>
<text class="file-name">{{ file.name }}</text> <view class="file-info">
<text class="file-size">{{ formatFileSize(file.size) }}</text> <text class="file-name" :title="attachmentFileName">{{ attachmentFileName }}</text>
</view> <text v-if="attachmentFileSize" class="file-size">{{ formatFileSize(attachmentFileSize) }}</text>
<view class="file-delete" @click="removeAttachment(idx)"><text class="delete-icon-sm"></text></view> </view>
<view v-if="uploadLoading" class="file-uploading">
<text class="uploading-text">上传中...</text>
</view> </view>
<view class="attachment-add-file" @click="handleAddAttachment"> <view v-else class="file-delete" @click="removeAttachment">
<text class="add-text">选取文件</text> <text class="delete-icon-sm"></text>
</view> </view>
</view> </view>
<view class="attachment-add-file" @click="handleAddAttachment" v-if="!attachmentFileName">
<text class="add-text">选取文件</text>
</view>
</view> </view>
</view> </view>
@ -121,6 +126,8 @@ import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue' import NavBar from '@/components/common/NavBar.vue'
import { createSparepartOutbound } from '@/api/mes/sparepartOutbound' import { createSparepartOutbound } from '@/api/mes/sparepartOutbound'
import { getSparepartDetail } from '@/api/mes/sparepart' import { getSparepartDetail } from '@/api/mes/sparepart'
import { getBaseUrl } from '@/utils/request'
import { getToken } from '@/utils/auth'
const { t } = useI18n() const { t } = useI18n()
@ -129,7 +136,11 @@ const outboundDate = ref(formatDate(new Date()))
const selectedOperatorId = ref(null) const selectedOperatorId = ref(null)
const selectedOperatorName = ref('') const selectedOperatorName = ref('')
const remark = ref('') const remark = ref('')
const attachmentList = ref([]) //
const fileUrl = ref('')
const attachmentFileName = ref('')
const attachmentFileSize = ref(0)
const uploadLoading = ref(false)
function formatDate(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}` } function formatDate(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}` }
function handleDateChange(e) { outboundDate.value = e.detail.value } function handleDateChange(e) { outboundDate.value = e.detail.value }
@ -202,17 +213,101 @@ function goSelectOperator() {
} }
// //
function getFileIcon(fileName) { const ext = (fileName || '').split('.').pop()?.toLowerCase(); const m = { pdf: '📄', doc: '📝', docx: '📝', xls: '📊', xlsx: '📊', ppt: '📽', pptx: '📽', txt: '📃', zip: '📦', rar: '📦', jpg: '🖼', jpeg: '🖼', png: '🖼', gif: '🖼' }; return m[ext] || '📎' } function getFileIcon(fileName) {
function formatFileSize(bytes) { if (!bytes) return '0 B'; if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(1) + ' MB' } const ext = (fileName || '').split('.').pop()?.toLowerCase()
const iconMap = { pdf: '📄', doc: '📝', docx: '📝', xls: '📊', xlsx: '📊', ppt: '📽', pptx: '📽', txt: '📃', zip: '📦', rar: '📦', jpg: '🖼', jpeg: '🖼', png: '🖼', gif: '🖼', webp: '🖼' }
return iconMap[ext] || '📎'
}
function formatFileSize(bytes) {
if (!bytes) return '0 B'
if (bytes < 1024) return bytes + ' B'
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / 1048576).toFixed(1) + ' MB'
}
const ALLOWED_EXTS = ['png', 'jpg', 'jpeg', 'doc', 'xls', 'ppt', 'txt', 'pdf']
const MAX_FILE_SIZE = 100 * 1024 * 1024
function handleAddAttachment() { function handleAddAttachment() {
// #ifdef APP-PLUS // #ifdef APP-PLUS
plus.gallery.pick((res) => { const files = (res?.files || []).map(f => ({ name: f.name || 'unknown', size: f.size || 0, path: f })); attachmentList.value.push(...files) }, (e) => {}, { filter: 'all', multiple: true, maximum: 9 - attachmentList.value.length }) plus.gallery.pick(
(res) => {
const file = res?.files?.[0]
if (!file) return
const ext = (file.name || '').split('.').pop()?.toLowerCase()
if (!ALLOWED_EXTS.includes(ext)) { uni.showToast({ title: '不支持的文件类型:.' + (ext || '未知'), icon: 'none' }); return }
if (file.size > MAX_FILE_SIZE) { uni.showToast({ title: '文件大小不能超过100MB', icon: 'none' }); return }
uploadFile(file)
},
(e) => { console.log('选择文件失败:', e) },
{ filter: 'all', multiple: false }
)
// #endif // #endif
// #ifndef APP-PLUS // #ifndef APP-PLUS
uni.chooseImage({ count: 9 - attachmentList.value.length, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (res) => { res.tempFilePaths.forEach(path => { const parts = path.split('/'); const name = parts[parts.length - 1] || 'image.jpg'; attachmentList.value.push({ name, size: 0, path }) }) } }) uni.chooseFile({
count: 1, type: 'all',
success: (res) => {
const tempFile = res.tempFiles?.[0]
if (!tempFile) return
const ext = (tempFile.name || '').split('.').pop()?.toLowerCase()
if (!ALLOWED_EXTS.includes(ext)) { uni.showToast({ title: '不支持的文件类型:.' + (ext || '未知'), icon: 'none' }); return }
if (tempFile.size > MAX_FILE_SIZE) { uni.showToast({ title: '文件大小不能超过100MB', icon: 'none' }); return }
uploadFile(tempFile)
},
fail: () => {
uni.chooseImage({
count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'],
success: (imgRes) => {
const path = imgRes.tempFilePaths?.[0]
if (!path) return
const parts = path.split('/')
uploadFile({ path, name: parts[parts.length - 1] || 'image.jpg', size: 0 })
}
})
}
})
// #endif // #endif
} }
function removeAttachment(idx) { attachmentList.value.splice(idx, 1) }
async function uploadFile(file) {
uploadLoading.value = true
attachmentFileName.value = file.name || 'unknown'
attachmentFileSize.value = file.size || 0
try {
const res = await new Promise((resolve, reject) => {
uni.uploadFile({
url: getBaseUrl() + '/admin-api/infra/file/upload',
filePath: file.path,
name: 'file',
header: { 'Authorization': 'Bearer ' + (getToken() || ''), 'tenantId': '1' },
success: (uploadRes) => {
if (uploadRes.statusCode === 200) {
try { resolve(JSON.parse(uploadRes.data)) }
catch (e) { reject(new Error('解析上传结果失败')) }
} else { reject(new Error('上传失败,状态码:' + uploadRes.statusCode)) }
},
fail: (err) => { reject(err) }
})
})
if (res.code === 0 && res.data) {
const { fileName, fileUrl: url } = res.data
fileUrl.value = JSON.stringify({ fileName: fileName || file.name, fileUrl: url })
attachmentFileName.value = fileName || file.name
uni.showToast({ title: '上传成功', icon: 'success' })
} else {
throw new Error(res.msg || '上传失败')
}
} catch (e) {
attachmentFileName.value = ''; attachmentFileSize.value = 0; fileUrl.value = ''
uni.showToast({ title: String(e?.message || '上传失败').substring(0, 50), icon: 'none' })
} finally {
uploadLoading.value = false
}
}
function removeAttachment() {
attachmentFileName.value = ''; attachmentFileSize.value = 0; fileUrl.value = ''
}
async function handleSubmit() { async function handleSubmit() {
if (!itemList.value.length) { uni.showToast({ title: '请先添加备件', icon: 'none' }); return } if (!itemList.value.length) { uni.showToast({ title: '请先添加备件', icon: 'none' }); return }
@ -250,7 +345,7 @@ async function handleSubmit() {
items: items items: items
} }
if (attachmentList.value.length) { submitData.fileUrl = String(attachmentList.value[0]?.path || '') } if (fileUrl.value) { submitData.fileUrl = fileUrl.value }
console.log('=== 出库提交 ===') console.log('=== 出库提交 ===')
console.log(JSON.stringify(submitData)) console.log(JSON.stringify(submitData))
@ -312,6 +407,7 @@ onHide(() => {})
.file-size { font-size: 22rpx; color: #9ca3af; margin-top: 4rpx; display: block; } .file-size { font-size: 22rpx; color: #9ca3af; margin-top: 4rpx; display: block; }
.file-delete { width: 48rpx; height: 48rpx; border-radius: 24rpx; background: #fee2e2; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .file-delete { width: 48rpx; height: 48rpx; border-radius: 24rpx; background: #fee2e2; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.delete-icon-sm { font-size: 22rpx; color: #dc2626; } .delete-icon-sm { font-size: 22rpx; color: #dc2626; }
.file-uploading { flex-shrink: 0; .uploading-text { font-size: 22rpx; color: #2563eb; } }
.attachment-add-file { display: flex; align-items: center; justify-content: center; height: 88rpx; background: #fff; border: 2rpx dashed #cbd5e1; border-radius: 12rpx; } .attachment-add-file { display: flex; align-items: center; justify-content: center; height: 88rpx; background: #fff; border: 2rpx dashed #cbd5e1; border-radius: 12rpx; }
.add-text { font-size: 26rpx; color: #6b7280; } .add-text { font-size: 26rpx; color: #6b7280; }

Loading…
Cancel
Save