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.

291 lines
17 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="'新增物料入库'" />
<!-- 扫码/选择物料 - 固定在页面最上方 -->
<view class="top-action-bar">
<view class="scan-input-row">
<input
id="material-inbound-scan-input"
class="scan-input"
v-model="scanCodeInput"
placeholder="红外扫码或输入物料码"
confirm-type="done"
@confirm="onScanInputConfirm"
/>
</view>
<view class="scan-btn" @click="handleSelectMaterial">
<text class="btn-text">选择物料</text>
</view>
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 入库信息卡片 -->
<view class="section-card">
<view class="section-header">
<view class="section-icon">
<uni-icons type="compose" size="24" color="#1f7cff"></uni-icons>
</view>
<text class="section-title">入库信息</text>
</view>
<view class="form-field">
<text class="form-label">入库时间<text class="required-star">*</text></text>
<picker mode="date" :value="inboundDate" @change="handleDateChange">
<view class="select-field">
<text :class="['select-text', inboundDate ? '' : 'placeholder']">{{ inboundDate || '请选择入库时间' }}</text>
<uni-icons type="calendar" size="18" color="#9ca3af"></uni-icons>
</view>
</picker>
</view>
<view class="form-field">
<text class="form-label">经办人<text class="required-star">*</text></text>
<view class="select-field" @click="goSelectOperator">
<text :class="['select-text', selectedOperatorName ? '' : 'placeholder']">{{ selectedOperatorName || '请选择经办人' }}</text>
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
</view>
</view>
<view class="form-field">
<text class="form-label">备注</text>
<textarea v-model="remark" class="form-textarea" placeholder="请输入备注信息" placeholder-class="placeholder-text" maxlength="500" />
</view>
<view class="form-field">
<text class="form-label">附件</text>
<view class="attachment-upload" @click="handleAddAttachment">
<uni-icons type="paperclip" size="20" color="#1f7cff"></uni-icons>
<text>选取文件</text>
</view>
<view v-if="attachmentList.length" class="attachment-list">
<view v-for="(file, idx) in attachmentList" :key="idx" class="attachment-item">
<view class="attachment-main">
<uni-icons type="paperclip" size="16" color="#64748b"></uni-icons>
<text class="attachment-name">{{ file.name }}</text>
</view>
<view class="attachment-delete" @click="removeAttachment(idx)">
<uni-icons type="closeempty" size="18" color="#ef4444"></uni-icons>
</view>
</view>
</view>
</view>
</view>
<!-- 入库清单卡片 -->
<view class="section-card">
<view class="section-header list-header">
<view class="section-title-wrap">
<view class="section-icon">
<uni-icons type="list" size="24" color="#1f7cff"></uni-icons>
</view>
<text class="section-title">入库清单({{ itemList.length }}</text>
</view>
<view class="add-product-btn" @click="handleSelectMaterial">
<uni-icons type="plusempty" size="16" color="#1f7cff"></uni-icons>
<text>添加物料</text>
</view>
</view>
<view v-if="itemList.length" class="item-list">
<view v-for="(item, idx) in itemList" :key="idx" class="item-card">
<view class="image-box">
<image
v-if="item._material && getItemImage(item._material)"
:src="getItemImage(item._material)"
class="item-image"
mode="aspectFill"
/>
<uni-icons v-else type="image" size="34" color="#cbd5e1"></uni-icons>
</view>
<view class="item-info">
<view class="item-header">
<text class="item-name">{{ textValue(item.productName) }}</text>
<view class="delete-btn" @click="removeItem(idx)">
<uni-icons type="trash" size="18" color="#ef4444"></uni-icons>
</view>
</view>
<view class="info-grid">
<view class="info-cell">
<text class="info-label">编码</text>
<text class="info-value">{{ item.productBarCode || '-' }}</text>
</view>
<view class="info-cell">
<text class="info-label">入库数量</text>
<text class="info-value">{{ item.inputCount }}{{ item.purchaseUnitName }}</text>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty-card" @click="handleSelectMaterial">
<uni-icons type="plusempty" size="30" color="#94a3b8"></uni-icons>
<text>请扫码或选择物料</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="action-bar">
<view class="action-btn back-btn" @click="handleCancel">取消</view>
<view class="action-btn submit-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 } from '@dcloudio/uni-app'
import NavBar from '@/components/common/NavBar.vue'
import { createMaterialInbound } from '@/api/mes/materialInbound'
import { getProductDetail } from '@/api/mes/sparepart'
const itemList = ref([])
const scanCodeInput = ref('')
const inboundDate = ref(formatDate(new Date()))
const selectedOperatorId = ref(null)
const selectedOperatorName = ref('')
const remark = ref('')
const attachmentList = ref([])
const focusNoKeyboardRef = ref(null)
const keywordInputSelector = '#material-inbound-scan-input input, input#material-inbound-scan-input'
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
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(material) { if (!material.images) return ''; if (Array.isArray(material.images)) return String(material.images[0] || ''); return String(material.images).split(',')[0]?.trim() || '' }
function focusKeywordNoKeyboard() { nextTick(() => { setTimeout(() => { focusNoKeyboardRef.value?.focus(keywordInputSelector) }, 80) }) }
function handleCancel() { getApp().globalData._materialInboundItems = []; uni.navigateBack() }
function handleSelectMaterial() { getApp().globalData._materialSelectFrom = 'inbound'; uni.navigateTo({ url: '/pages_function/pages/materialInbound/materialSelect?from=inbound' }) }
function onScanInputConfirm() { const code = scanCodeInput.value.trim(); if (!code) return; handleScanCode(code) }
async function handleScanCode(code) {
let materialId = null
if (code.toUpperCase().startsWith('MATERIAL-')) materialId = code.replace(/MATERIAL-/i, '')
else { const idMatch = code.match(/(\d+)$/); if (idMatch) materialId = idMatch[1] }
if (!materialId) { uni.showToast({ title: '无法识别物料码', icon: 'none' }); return }
try {
const apiRes = await getProductDetail(materialId)
const detail = apiRes?.data || apiRes
if (detail && detail.id) {
getApp().globalData._materialFromScan = true
getApp().globalData._materialBeforeConfirm = detail
uni.navigateTo({ url: '/pages_function/pages/materialInbound/materialConfirm' })
} else { uni.showToast({ title: '未找到物料: ' + materialId, icon: 'none' }) }
} catch (e) { console.error(e); uni.showToast({ title: '扫码失败', icon: 'none' }) }
}
function removeItem(idx) { itemList.value.splice(idx, 1); getApp().globalData._materialInboundItems = [...itemList.value] }
function goSelectOperator() { getApp().globalData._materialInboundUserFrom = 'materialInbound'; uni.navigateTo({ url: '/pages_function/pages/moldRepair/userSelect?field=operator&from=materialInbound' }) }
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 })); attachmentList.value.push(...files) }, () => {}, { filter: 'all', multiple: true, maximum: 9 - attachmentList.value.length })
// #endif
// #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('/'); attachmentList.value.push({ name: parts[parts.length - 1] || 'image.jpg', 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, stockUserId: String(selectedOperatorId.value), supplierId: itemList.value[0]?.supplierId, status: 0, totalCount, totalPrice: 0, remark: remark.value, items }
if (attachmentList.value.length) { submitData.attachments = attachmentList.value.map(f => f.path || f) }
uni.showLoading({ title: '提交中...', mask: true })
try {
await createMaterialInbound(submitData)
uni.hideLoading(); getApp().globalData._materialInboundItems = []
uni.showToast({ title: '入库成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1500)
} catch (e) {
uni.hideLoading(); const msg = e?.message || e?.data?.msg || e?.response?.data?.msg || '保存失败'
uni.showToast({ title: String(msg).substring(0, 50), icon: 'none' })
}
}
onReady(() => { focusKeywordNoKeyboard() })
onShow(() => {
const items = getApp().globalData?._materialInboundItems
if (Array.isArray(items)) itemList.value = [...items]
const userResult = getApp().globalData?._materialInboundUserSelectResult
if (userResult) { selectedOperatorId.value = userResult.user.id; selectedOperatorName.value = userResult.user.nickname || userResult.user.userName || userResult.user.name || ''; getApp().globalData._materialInboundUserSelectResult = null }
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f5f7fb; }
.top-action-bar { display: flex; align-items: center; gap: 16rpx; padding: 20rpx 24rpx; background: #ffffff; border-bottom: 1rpx solid #eef2f7; }
.detail-scroll { height: calc(100vh - 172rpx - 116rpx); }
.content-section { padding: 20rpx 24rpx 28rpx; }
.section-card { background: #ffffff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; border: 1rpx solid #eef2f7; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
.section-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 22rpx; padding-bottom: 18rpx; border-bottom: 1rpx solid #f1f5f9; }
.section-icon { width: 40rpx; height: 40rpx; border-radius: 10rpx; background: #eff6ff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.section-title { font-size: 32rpx; font-weight: 600; color: #1f2937; }
.section-title-wrap { display: flex; align-items: center; gap: 12rpx; min-width: 0; }
.list-header { justify-content: space-between; gap: 16rpx; }
.form-field { display: flex; flex-direction: column; gap: 12rpx; }
.form-field + .form-field { margin-top: 24rpx; }
.form-label { font-size: 26rpx; color: #4b5563; font-weight: 500; }
.required-star { color: #ef4444; font-size: 28rpx; margin-left: 4rpx; }
.select-field { display: flex; align-items: center; justify-content: space-between; padding: 0 24rpx; height: 76rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; }
.select-text { flex: 1; min-width: 0; font-size: 28rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.placeholder, .placeholder-text { color: #9ca3af; }
.form-textarea { width: 100%; min-height: 120rpx; background: #f8fafc; border-radius: 14rpx; padding: 18rpx 24rpx; font-size: 28rpx; color: #374151; box-sizing: border-box; }
.attachment-upload { height: 76rpx; border-radius: 14rpx; border: 1rpx dashed #bfdbfe; background: #eff6ff; color: #1f7cff; display: flex; align-items: center; justify-content: center; gap: 10rpx; font-size: 26rpx; font-weight: 600; }
.attachment-list { margin-top: 12rpx; display: flex; flex-direction: column; gap: 10rpx; }
.attachment-item { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 16rpx 18rpx; background: #f8fafc; border-radius: 12rpx; }
.attachment-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10rpx; }
.attachment-name { flex: 1; min-width: 0; font-size: 24rpx; color: #334155; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attachment-delete { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.add-product-btn { height: 60rpx; padding: 0 18rpx; border-radius: 999rpx; border: 1rpx solid #bfdbfe; background: #eff6ff; color: #1f7cff; font-size: 24rpx; font-weight: 600; display: flex; align-items: center; gap: 8rpx; flex-shrink: 0; }
.scan-input-row { flex: 1; }
.scan-input { width: 100%; height: 76rpx; padding: 0 20rpx; font-size: 26rpx; color: #333; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; }
.scan-btn { display: flex; align-items: center; justify-content: center; height: 76rpx; padding: 0 24rpx; border-radius: 14rpx; background: #1f4b79; white-space: nowrap; flex-shrink: 0; }
.btn-text { font-size: 26rpx; font-weight: 600; color: #fff; }
.item-list { display: flex; flex-direction: column; gap: 18rpx; }
.item-card { display: flex; gap: 18rpx; padding: 20rpx; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 18rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
.image-box { width: 128rpx; height: 128rpx; border-radius: 16rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; display: flex; align-items: center; justify-content: center; flex-shrink: 0; overflow: hidden; }
.item-image { width: 100%; height: 100%; }
.item-info { flex: 1; min-width: 0; }
.item-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 14rpx; }
.item-name { flex: 1; min-width: 0; font-size: 30rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.delete-btn { width: 48rpx; height: 48rpx; border-radius: 24rpx; background: #fef2f2; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; }
.info-cell { min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
.info-label { font-size: 22rpx; color: #9ca3af; }
.info-value { font-size: 26rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty-card { min-height: 220rpx; border: 2rpx dashed #d7dde8; border-radius: 18rpx; background: #f8fafc; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14rpx; color: #94a3b8; font-size: 27rpx; }
.action-bar { 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; }
.action-btn { flex: 1; height: 84rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
.back-btn { background: #eef2f7; color: #475569; }
.submit-btn { background: #1f4b79; color: #ffffff; }
</style>