You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

585 lines
18 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<NavBar :title="t('sparepartInbound.createTitle')" />
<!-- 操作按钮区 -->
<view class="action-row">
<view class="scan-input-row">
<input
id="sparepart-inbound-scan-input"
class="scan-input"
v-model="scanCodeInput"
placeholder="红外扫码或输入备件码"
confirm-type="done"
@confirm="onScanInputConfirm"
/>
</view>
<view class="action-btn select-btn" @click="handleSelectSparepart">
<text class="btn-text">{{ t('sparepartInbound.selectSparepart') }}</text>
</view>
</view>
<!-- 入库信息 -->
<view class="form-section">
<!-- 入库时间 -->
<view class="section-title-bar">
<view class="section-bar-line"></view>
<text class="section-title"><text class="required">*</text>入库时间</text>
</view>
<picker mode="date" :value="inboundDate" @change="handleDateChange">
<view class="form-row-card">
<text :class="{ placeholder: !inboundDate }">{{ inboundDate || '请选择入库时间' }}</text>
<text class="form-row-arrow">▼</text>
</view>
</picker>
<!-- 经办人 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title"><text class="required">*</text>经办人</text>
</view>
<view class="form-row-card" @click="goSelectOperator">
<text :class="{ placeholder: !selectedOperatorName }">
{{ selectedOperatorName || '请选择经办人' }}
</text>
<text class="form-row-arrow">▶</text>
</view>
<!-- 备注 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">备注</text>
</view>
<view class="form-row-card remark-card">
<textarea
v-model="remark"
class="remark-textarea"
placeholder="请输入备注信息"
placeholder-class="remark-placeholder"
:maxlength="500"
auto-height
/>
</view>
<!-- 附件 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">附件</text>
</view>
<view class="attachment-area">
<view class="attachment-list">
<view v-for="(file, idx) in attachmentList" :key="idx" class="attachment-file-item">
<view class="file-icon">
<text class="file-icon-text">{{ getFileIcon(file.name) }}</text>
</view>
<view class="file-info">
<text class="file-name" :title="file.name">{{ file.name }}</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 class="attachment-add-file" @click="handleAddAttachment">
<text class="add-text">选取文件</text>
</view>
</view>
</view>
</view>
<!-- 已选备件列表 -->
<view class="sparepart-section">
<view class="section-title-bar">
<view class="section-bar-line"></view>
<text class="section-title">入库清单({{ itemList.length }}</text>
</view>
<view v-if="itemList.length" class="item-list">
<view v-for="(item, idx) in itemList" :key="idx" class="item-card">
<view class="item-delete-top" @click="removeItem(idx)"><text class="delete-icon">✕</text></view>
<view class="item-left">
<view class="item-image-wrap">
<image
v-if="item._sparepart && getItemImage(item._sparepart)"
:src="getItemImage(item._sparepart)"
class="item-image"
mode="aspectFill"
/>
<text v-else class="item-image-empty">📦</text>
</view>
<view class="item-info">
<text class="item-name">{{ item.productName }}</text>
<text class="item-spec">{{ item.productBarCode || '-' }}</text>
</view>
</view>
<view class="item-right">
<view class="item-qty-row">
<text class="item-qty-label">入库数量</text>
<text class="item-qty-value">{{ item.inputCount }}{{ item.purchaseUnitName }}</text>
</view>
</view>
</view>
</view>
<view v-else class="empty-wrap">
<text class="empty-text">请扫码或选择备件</text>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="bottom-btn cancel-btn" @click="handleCancel">取消</view>
<view class="bottom-btn confirm-btn" @click="handleSubmit">确认入库</view>
</view>
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
</view>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { onReady, onShow, onHide } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { createSparepartInbound } from '@/api/mes/sparepartInbound'
import { getSparepartDetail } from '@/api/mes/sparepart'
const { t } = useI18n()
const itemList = ref([])
const scanCodeInput = ref('') // 扫码/输入备件码
// 聚焦阻止键盘弹出
const focusNoKeyboardRef = ref(null)
const keywordInputSelector = '#sparepart-inbound-scan-input input, input#sparepart-inbound-scan-input'
function focusKeywordNoKeyboard() {
nextTick(() => {
setTimeout(() => {
focusNoKeyboardRef.value?.focus(keywordInputSelector)
}, 80)
})
}
// 红外扫码或手动输入备件码确认
function onScanInputConfirm() {
const code = scanCodeInput.value.trim()
if (!code) return
handleScanCode(code)
}
async function handleScanCode(code) {
let sparepartId = null
if (code.toUpperCase().startsWith('SPARE-')) {
sparepartId = code.replace(/SPARE-/i, '')
} else {
const idMatch = code.match(/(\d+)$/)
if (idMatch) sparepartId = idMatch[1]
}
if (!sparepartId) {
uni.showToast({ title: '无法识别备件码', icon: 'none' })
return
}
try {
const apiRes = await getSparepartDetail(sparepartId)
const detail = apiRes?.data || apiRes
if (detail && detail.id) {
getApp().globalData._sparepartFromScan = true
getApp().globalData._sparepartBeforeConfirm = detail
uni.navigateTo({
url: '/pages_function/pages/sparepartInbound/sparepartConfirm'
})
} else {
uni.showToast({ title: '未找到备件: ' + sparepartId, icon: 'none' })
}
} catch (e) {
console.error('[备件入库] 扫码查询备件失败:', e)
uni.showToast({ title: '扫码失败', icon: 'none' })
}
}
// 入库时间
const inboundDate = ref(formatDate(new Date()))
// 经办人(页面选择模式)
const selectedOperatorId = ref(null)
const selectedOperatorName = ref('')
// 备注
const remark = ref('')
// 附件
const attachmentList = ref([])
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) {
inboundDate.value = e.detail.value
}
function getItemImage(sparepart) {
const images = sparepart.images
if (!images) return ''
if (Array.isArray(images)) return String(images[0] || '')
return String(images).split(',')[0]?.trim() || ''
}
function handleCancel() {
getApp().globalData._sparepartInboundItems = []
uni.navigateBack()
}
function handleSelectSparepart() {
getApp().globalData._sparepartSelectFrom = 'inbound'
uni.navigateTo({
url: '/pages_function/pages/sparepartInbound/sparepartSelect'
})
}
function handleScan() {
uni.scanCode({
onlyFromCamera: false,
scanType: ['barCode', 'qrCode'],
success: (res) => {
const code = (res.result || '').trim()
if (!code) return
scanCodeInput.value = code
handleScanCode(code)
},
fail: () => {
uni.showToast({ title: '扫码失败', icon: 'none' })
}
})
}
function removeItem(idx) {
itemList.value.splice(idx, 1)
getApp().globalData._sparepartInboundItems = [...itemList.value]
}
// 经办人
function goSelectOperator() {
getApp().globalData._sparepartInboundUserFrom = 'inbound'
uni.navigateTo({
url: '/pages_function/pages/moldRepair/userSelect?field=operator&from=sparepartInbound'
})
}
// 附件
function getFileIcon(fileName) {
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 < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
function handleAddAttachment() {
// #ifdef APP-PLUS
plus.gallery.pick(
(res) => {
const files = (res?.files || []).map(f => ({
name: f.name || 'unknown',
size: f.size || 0,
path: f // 保留原始 file 对象
}))
attachmentList.value.push(...files)
},
(e) => {
console.log('选择文件失败:', e)
},
{ filter: 'all', multiple: true, maximum: 9 - attachmentList.value.length }
)
// #endif
// #ifndef APP-PLUS
// 非APP端暂用 chooseImage 兜底
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 })
})
}
})
// #endif
}
function removeAttachment(idx) {
attachmentList.value.splice(idx, 1)
}
async function handleSubmit() {
if (!itemList.value.length) {
uni.showToast({ title: '请先添加备件', icon: 'none' })
return
}
if (!inboundDate.value) {
uni.showToast({ title: '请选择入库时间', icon: 'none' })
return
}
if (!selectedOperatorId.value) {
uni.showToast({ title: '请选择经办人', icon: 'none' })
return
}
let totalCount = 0
const items = itemList.value.map(item => {
totalCount += item.count || 0
return {
warehouseId: item.warehouseId,
areaId: item.areaId,
productId: item.productId,
productName: item.productName,
productBarCode: item.productBarCode,
productUnitName: item.productUnitName,
purchaseUnitName: item.purchaseUnitName,
purchaseUnitConvertQuantity: item.purchaseUnitConvertQuantity,
inputCount: item.inputCount,
count: item.count
}
})
const now = new Date()
const [y, m, d] = inboundDate.value.split('-').map(Number)
const inTime = new Date(y, m - 1, d, now.getHours(), now.getMinutes(), now.getSeconds()).getTime()
const submitData = {
isCode: true,
inTime: inTime,
stockUserId: String(selectedOperatorId.value),
supplierId: itemList.value[0]?.supplierId,
status: 0,
totalCount: totalCount,
totalPrice: 0,
remark: remark.value,
items: items
}
// 附件(文件对象数组)
if (attachmentList.value.length) {
submitData.attachments = attachmentList.value.map(f => f.path || f)
}
console.log('提交数据:', JSON.stringify(submitData))
uni.showLoading({ title: '提交中...', mask: true })
try {
await createSparepartInbound(submitData)
uni.hideLoading()
getApp().globalData._sparepartInboundItems = []
uni.showToast({ title: '入库成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (e) {
uni.hideLoading()
const msg = e?.message || e?.data?.msg || e?.response?.data?.msg || t('functionCommon.saveFailed')
console.error('入库提交失败:', e)
uni.showToast({ title: String(msg).substring(0, 50), icon: 'none' })
}
}
onReady(() => {
focusKeywordNoKeyboard()
})
onShow(() => {
const items = getApp().globalData?._sparepartInboundItems
if (Array.isArray(items)) {
itemList.value = [...items]
}
// 读取用户选择页返回的经办人
const userResult = getApp().globalData?._sparepartInboundUserSelectResult
if (userResult) {
selectedOperatorId.value = userResult.user.id
selectedOperatorName.value = userResult.user.nickname || userResult.user.userName || userResult.user.name || ''
getApp().globalData._sparepartInboundUserSelectResult = null
}
})
onHide(() => {})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
}
/* 操作按钮区 */
.action-row {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
}
.scan-input-row {
flex: 1;
display: flex;
align-items: center;
height: 72rpx;
border-radius: 10rpx;
overflow: hidden;
}
.scan-input {
flex: 1;
height: 72rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
background: #fff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
height: 72rpx;
padding: 0 24rpx;
border-radius: 10rpx;
background: #1f4b79;
color: #fff;
font-size: 26rpx;
font-weight: 600;
white-space: nowrap;
.btn-icon-wrap { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; }
.btn-icon { font-size: 36rpx; color: #fff; }
.btn-text { font-size: 26rpx; font-weight: 600; color: #fff; }
}
/* 备件列表 */
.sparepart-section { padding: 0; }
.section-title-bar {
display: flex; align-items: center; gap: 10rpx;
padding: 24rpx 24rpx 18rpx;
}
.section-bar-line {
width: 6rpx; height: 32rpx; border-radius: 3rpx;
background: #2563eb; flex-shrink: 0;
}
.section-title { font-size: 30rpx; font-weight: 700; color: #1a1a1a; }
.required { color: #ef4444; margin-right: 2rpx; }
.item-list { padding: 0 24rpx; }
.item-card {
position: relative; display: flex; align-items: center; justify-content: space-between;
background: #fff; border-radius: 14rpx; padding: 20rpx; margin-bottom: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04);
}
.item-delete-top { position: absolute; top: 10rpx; right: 10rpx; width: 40rpx; height: 40rpx; border-radius: 20rpx; background: #fee2e2; display: flex; align-items: center; justify-content: center; z-index: 1; .delete-icon { font-size: 20rpx; color: #dc2626; } }
.item-left { display: flex; align-items: center; flex: 1; min-width: 0; padding-right: 36rpx; }
.item-image-wrap {
width: 80rpx; height: 80rpx; border-radius: 10rpx; overflow: hidden;
background: #f8fafc; border: 1rpx solid #f0f0f0; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.item-image { width: 100%; height: 100%; }
.item-image-empty { font-size: 36rpx; opacity: 0.3; }
.item-info { margin-left: 16rpx; flex: 1; min-width: 0; }
.item-name {
font-size: 28rpx; font-weight: 600; color: #1a1a1a; display: block;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.item-spec { font-size: 24rpx; color: #9ca3af; margin-top: 6rpx; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.item-right { display: flex; flex-direction: column; align-items: flex-end; margin-left: 16rpx; flex-shrink: 0; padding-right: 50rpx; }
.item-qty-row { display: flex; flex-direction: column; align-items: flex-end; }
.item-qty-label { font-size: 22rpx; color: #9ca3af; }
.item-qty-value { font-size: 28rpx; font-weight: 700; color: #2563eb; margin-top: 4rpx; }
.empty-wrap { padding: 160rpx 0; text-align: center; }
.empty-text { font-size: 28rpx; color: #94a3b8; }
/* 入库信息区 */
.form-section { padding-bottom: 16rpx; }
.form-row-card {
position: relative;
display: flex; align-items: center; justify-content: space-between;
background: #fff; border-radius: 16rpx; padding: 26rpx 24rpx; margin: 0 24rpx 16rpx;
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.04);
font-size: 28rpx; color: #374151;
.placeholder { color: #9ca3af; }
}
.form-row-arrow { font-size: 20rpx; color: #999; flex-shrink: 0; }
/* 备注 */
.remark-card { padding: 20rpx 24rpx; }
.remark-textarea { width: 100%; min-height: 120rpx; font-size: 27rpx; color: #374151; line-height: 1.6; box-sizing: border-box; }
.remark-placeholder { color: #bbb; }
/* 附件 */
.attachment-area { padding: 0 24rpx 16rpx; }
.attachment-list { display: flex; flex-direction: column; gap: 12rpx; }
.attachment-file-item {
display: flex; align-items: center; gap: 16rpx;
background: #fff; border-radius: 12rpx; padding: 18rpx 20rpx;
box-shadow: 0 2rpx 8rpx rgba(15, 23, 42, 0.03);
}
.file-icon {
width: 72rpx; height: 72rpx; border-radius: 10rpx;
background: #f1f5f9; display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.file-icon-text { font-size: 36rpx; }
.file-info { flex: 1; min-width: 0; }
.file-name {
font-size: 26rpx; color: #1f2937; display: block;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.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;
}
.delete-icon-sm { font-size: 22rpx; color: #dc2626; }
.attachment-add-file {
display: flex; align-items: center; justify-content: center; gap: 10rpx;
height: 88rpx; background: #fff; border: 2rpx dashed #cbd5e1;
border-radius: 12rpx;
}
.add-icon { font-size: 32rpx; }
.add-text { font-size: 26rpx; color: #6b7280; }
/* 底部操作栏 */
.bottom-actions {
position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 18rpx;
padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom));
background: #ffffff; box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06); z-index: 99;
}
.bottom-btn { flex: 1; height: 84rpx; line-height: 84rpx; text-align: center; border-radius: 16rpx; font-size: 30rpx; font-weight: 600;
&:active { opacity: 0.85; }
}
.cancel-btn { background: #eef2f7; color: #475569; }
.confirm-btn { background: #1f4b79; color: #ffffff; }
</style>