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.

405 lines
20 KiB
Vue

<template>
<view class="page-container">
<NavBar :title="t('productInbound.detailTitle')" />
<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">{{ t('productInbound.inboundInfo') }}</text>
<text :class="['status-tag', statusClass(detail.status)]">{{ statusText(detail.status) }}</text>
</view>
<view v-if="loading" class="loading-card">{{ t('productInbound.loading') }}</view>
<template v-else>
<view class="readonly-grid">
<view class="readonly-item">
<text class="readonly-label">{{ t('productInbound.documentNo') }}</text>
<text class="readonly-value">{{ textValue(detail.no) }}</text>
</view>
<view class="readonly-item">
<text class="readonly-label">{{ t('productInbound.inboundType') }}</text>
<text class="readonly-value">{{ textValue(detail.inType) }}</text>
</view>
<view class="readonly-item">
<text class="readonly-label">{{ t('productInbound.inboundTime') }}</text>
<text class="readonly-value">{{ formatDateTime(detail.inTime || detail.createTime) }}</text>
</view>
<view class="readonly-item">
<text class="readonly-label">{{ t('productInbound.operator') }}</text>
<text class="readonly-value">{{ textValue(getStockUserName(detail)) }}</text>
</view>
<view class="readonly-item">
<text class="readonly-label">{{ t('productInbound.inboundQuantity') }}</text>
<text class="readonly-value highlight">{{ textValue(detail.totalCount) }}</text>
</view>
<view class="readonly-item">
<text class="readonly-label">{{ t('productInbound.reviewer') }}</text>
<text class="readonly-value">{{ textValue(detail.auditUserName) }}</text>
</view>
</view>
<view class="form-field">
<text class="form-label">{{ t('productInbound.remark') }}</text>
<view class="readonly-box">{{ textValue(detail.remark) }}</view>
</view>
<view v-if="attachmentList.length" class="form-field">
<text class="form-label">{{ t('productInbound.attachment') }}</text>
<view class="attachment-list">
<view v-for="(file, idx) in attachmentList" :key="idx" class="attachment-item">
<uni-icons type="paperclip" size="16" color="#64748b"></uni-icons>
<text class="attachment-name">{{ textValue(getAttachmentName(file, idx)) }}</text>
</view>
</view>
</view>
</template>
</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">{{ t('productInbound.itemList') }}</text>
</view>
</view>
<view v-if="itemList.length" class="summary-strip">
<view class="summary-item">
<text class="summary-value">{{ itemList.length }}</text>
<text class="summary-label">{{ t('productInbound.product') }}</text>
</view>
<view class="summary-item">
<text class="summary-value">{{ totalPalletCount }}</text>
<text class="summary-label">{{ t('productInbound.pallet') }}</text>
</view>
<view class="summary-item">
<text class="summary-value">{{ totalInputCount }}</text>
<text class="summary-label">{{ t('productInbound.packageCount') }}</text>
</view>
<view class="summary-item">
<text class="summary-value">{{ totalPieceCount }}</text>
<text class="summary-label">{{ t('productInbound.pieceCount') }}</text>
</view>
</view>
<view v-if="itemList.length" class="item-list">
<view v-for="(item, idx) in itemList" :key="item.id || idx" class="item-card">
<view class="item-header">
<text class="item-name">{{ textValue(item.productName) }}</text>
<text class="item-code">{{ textValue(item.productBarCode || item.productCode) }}</text>
</view>
<view class="info-grid">
<view class="info-cell">
<text class="info-label">{{ t('productInbound.inboundPackageCount') }}</text>
<text class="info-value">{{ textValue(getItemInputCount(item)) }}</text>
</view>
<view class="info-cell">
<text class="info-label">{{ t('productInbound.inboundPieceCount') }}</text>
<text class="info-value">{{ textValue(getItemPieceCount(item)) }}</text>
</view>
<view class="info-cell">
<text class="info-label">{{ t('productInbound.taskOrder') }}</text>
<text class="info-value">{{ isRelatedTask(item) ? textValue(item.taskCode || item.taskName) : t('productInbound.no') }}</text>
</view>
<view class="info-cell">
<text class="info-label">{{ t('productInbound.packagingScheme') }}</text>
<text class="info-value">{{ textValue(item.packagingSchemeName) }}</text>
</view>
<view class="info-cell">
<text class="info-label">{{ t('productInbound.palletPackageQuantity') }}</text>
<text class="info-value">{{ textValue(item.palletPackageQuantity) }}</text>
</view>
<view class="info-cell">
<text class="info-label">{{ t('productInbound.packageQuantity') }}</text>
<text class="info-value">{{ textValue(item.packageQuantity) }}</text>
</view>
</view>
<view v-if="getPallets(item).length" class="pallet-list">
<view v-for="(pallet, pIndex) in getPallets(item)" :key="pallet.palletId || pallet.id || pIndex" class="pallet-card">
<view class="pallet-top">
<text class="pallet-code">{{ textValue(getPalletCode(pallet)) }}</text>
<text class="pallet-count">{{ t('productInbound.packageUnit', { count: textValue(pallet.packageCount) }) }}</text>
</view>
<view class="pallet-grid">
<view class="pallet-cell">
<text class="pallet-label">{{ t('productInbound.warehouse') }}</text>
<text class="pallet-value">{{ textValue(getWarehouseDisplay(pallet, item)) }}</text>
</view>
<view class="pallet-cell">
<text class="pallet-label">{{ t('productInbound.location') }}</text>
<text class="pallet-value">{{ textValue(getAreaDisplay(pallet, item)) }}</text>
</view>
<view class="pallet-cell">
<text class="pallet-label">{{ t('productInbound.pieceCount') }}</text>
<text class="pallet-value">{{ textValue(getPalletPieceCount(pallet, item)) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty-card">{{ loading ? t('productInbound.loading') : t('productInbound.emptyItemList') }}</view>
</view>
</view>
</scroll-view>
<view class="action-bar">
<view class="action-btn back-btn" @click="handleBack">{{ t('productInbound.back') }}</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getProductInboundDetail } from '@/api/mes/productInbound'
import { getWarehouseAreaSimpleList, getWarehouseSimpleList, getSimpleUserList } from '@/api/mes/moldget'
const { t } = useI18n()
const detailId = ref(null)
const detail = ref({})
const loading = ref(false)
const userOptions = ref([])
const warehouseOptions = ref([])
const warehouseAreaMap = ref({})
const itemList = computed(() => normalizeItems(detail.value))
const attachmentList = computed(() => normalizeAttachments(detail.value))
const totalPalletCount = computed(() => itemList.value.reduce((sum, item) => sum + getPallets(item).length, 0))
const totalInputCount = computed(() => itemList.value.reduce((sum, item) => sum + (Number(getItemInputCount(item)) || 0), 0))
const totalPieceCount = computed(() => itemList.value.reduce((sum, item) => sum + (Number(getItemPieceCount(item)) || 0), 0))
function normalizeResponse(res) {
return res && res.data !== undefined ? res.data : res
}
function normalizeItems(row) {
const list = row?.items || row?.itemList || row?.stockInItems || row?.stockInItemList || []
return Array.isArray(list) ? list : []
}
function normalizeAttachments(row) {
const raw = row?.fileUrl || row?.attachments || row?.attachmentList || row?.files
if (!raw) return []
if (Array.isArray(raw)) return raw
return String(raw).split(',').map((item) => item.trim()).filter(Boolean)
}
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [year, month, day] = value
return `${year}-${pad(month)}-${pad(day)}`
}
const raw = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value
const date = new Date(raw)
if (Number.isNaN(date.getTime())) return String(value)
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
function pad(n) {
return String(n).padStart(2, '0')
}
function statusText(s) {
const map = {
0: t('productInbound.statusPending'),
10: t('productInbound.statusAuditing'),
20: t('productInbound.statusStored'),
1: t('productInbound.statusRejected')
}
const num = Number(s)
return map[num] || textValue(s)
}
function statusClass(s) {
const num = Number(s)
if (num === 0) return 'text-primary'
if (num === 10) return 'text-warning'
if (num === 20) return 'text-success'
if (num === 1) return 'text-danger'
return ''
}
function getStockUserName(row) {
if (row?.stockUserName) return row.stockUserName
const userId = row?.stockUserId
return userOptions.value.find((item) => String(item.value) === String(userId))?.label || userId
}
function getAttachmentName(file, index) {
if (file?.name) return file.name
const text = String(file?.url || file?.path || file || '')
const name = text.split('/').pop()
return name || `${t('productInbound.attachment')}${index + 1}`
}
function isRelatedTask(item) {
const v = item?.relateTask ?? item?.relatedTask
return v === true || v === 1 || v === '1'
}
function getItemInputCount(item) {
return item?.inputCount ?? item?.packageCount ?? item?.count
}
function getItemPieceCount(item) {
return item?.count ?? ((Number(item?.inputCount) || 0) * (Number(item?.packageQuantity) || 1))
}
function getPallets(item) {
if (Array.isArray(item?.pallets)) return item.pallets
if (item?.palletId || item?.palletCode) {
return [{
palletId: item.palletId,
palletCode: item.palletCode,
packageCount: item.packageCount ?? item.inputCount,
warehouseId: item.warehouseId,
warehouseName: item.warehouseName,
areaId: item.areaId,
areaName: item.areaName,
count: item.count
}]
}
return []
}
function getPalletCode(pallet) {
return pallet?.palletCode || pallet?.code || pallet?.pallet?.code || pallet?.palletId || ''
}
function getPalletPieceCount(pallet, item) {
return pallet?.count ?? ((Number(pallet?.packageCount) || 0) * (Number(item?.packageQuantity) || 1))
}
function getWarehouseDisplay(pallet, item) {
const id = pallet?.warehouseId ?? item?.warehouseId
return pallet?.warehouseName || item?.warehouseName || warehouseOptions.value.find((w) => String(w.value) === String(id))?.label || id
}
function getAreaDisplay(pallet, item) {
const warehouseId = pallet?.warehouseId ?? item?.warehouseId
const areaId = pallet?.areaId ?? item?.areaId
const areas = warehouseAreaMap.value[String(warehouseId)] || []
return pallet?.areaName || item?.areaName || areas.find((a) => String(a.value) === String(areaId))?.label || areaId
}
async function loadUsers() {
try {
const res = await getSimpleUserList()
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
userOptions.value = data.map((u) => ({
value: u.id || u.userId,
label: u.nickname || u.userName || u.name || String(u.id || '')
}))
} catch (e) {}
}
async function loadWarehouses() {
try {
const res = await getWarehouseSimpleList()
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
warehouseOptions.value = data.map((w) => ({ value: w.id, label: w.name || String(w.id || '') }))
} catch (e) {}
}
async function loadAreasForWarehouse(warehouseId) {
if (!warehouseId || warehouseAreaMap.value[String(warehouseId)]) return
try {
const res = await getWarehouseAreaSimpleList(warehouseId)
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
warehouseAreaMap.value = {
...warehouseAreaMap.value,
[String(warehouseId)]: data.map((a) => ({ value: a.id, label: a.name || a.areaName || String(a.id || '') }))
}
} catch (e) {
warehouseAreaMap.value = { ...warehouseAreaMap.value, [String(warehouseId)]: [] }
}
}
async function loadAreasForDetail() {
const ids = new Set()
itemList.value.forEach((item) => {
if (item.warehouseId) ids.add(item.warehouseId)
getPallets(item).forEach((pallet) => {
const id = pallet.warehouseId ?? item.warehouseId
if (id) ids.add(id)
})
})
await Promise.all(Array.from(ids).map((id) => loadAreasForWarehouse(id)))
}
async function loadDetail() {
if (!detailId.value) return
loading.value = true
try {
const res = await getProductInboundDetail(detailId.value)
detail.value = normalizeResponse(res) || {}
await loadAreasForDetail()
} catch (e) {
uni.showToast({ title: t('productInbound.detailLoadFailed'), icon: 'none' })
} finally {
loading.value = false
}
}
function handleBack() {
uni.navigateBack()
}
onLoad(async (options) => {
detailId.value = options?.id || null
await Promise.all([loadUsers(), loadWarehouses()])
await loadDetail()
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f5f7fb; }
.detail-scroll { height: calc(100vh - 172rpx); }
.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; }
.status-tag { margin-left: auto; flex-shrink: 0; padding: 8rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; line-height: 1; background: #e2e8f0; color: #64748b; }
.status-tag.text-success { color: #15803d; background: #dcfce7; }
.status-tag.text-danger { color: #dc2626; background: #fee2e2; }
.status-tag.text-warning { color: #d97706; background: #fef3c7; }
.status-tag.text-primary { color: #1d4ed8; background: #dbeafe; }
.loading-card, .empty-card { min-height: 180rpx; border: 2rpx dashed #d7dde8; border-radius: 18rpx; background: #f8fafc; display: flex; align-items: center; justify-content: center; color: #94a3b8; font-size: 27rpx; }
.readonly-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 14rpx; overflow: hidden; }
.readonly-item { min-width: 0; display: flex; flex-direction: column; gap: 8rpx; padding: 18rpx 20rpx; border-right: 1rpx solid #f1f5f9; border-bottom: 1rpx solid #f1f5f9; }
.readonly-item:nth-child(2n) { border-right: 0; }
.readonly-label { font-size: 23rpx; color: #8a94a6; }
.readonly-value { font-size: 27rpx; color: #334155; line-height: 1.35; word-break: break-all; }
.readonly-value.highlight { color: #1f4b79; font-weight: 700; }
.form-field { display: flex; flex-direction: column; gap: 12rpx; margin-top: 24rpx; }
.form-label { font-size: 26rpx; color: #4b5563; font-weight: 500; }
.readonly-box { min-height: var(--app-form-control-height, 70rpx); padding: 18rpx 24rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; font-size: 28rpx; color: #374151; line-height: 1.45; }
.attachment-list { display: flex; flex-direction: column; gap: 10rpx; }
.attachment-item { display: flex; align-items: center; gap: 10rpx; padding: 16rpx 18rpx; background: #f8fafc; border-radius: 12rpx; }
.attachment-name { flex: 1; min-width: 0; font-size: 24rpx; color: #334155; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.summary-strip { display: grid; grid-template-columns: repeat(4, 1fr); background: #f8fafc; border: 1rpx solid #e8eef6; border-radius: 16rpx; overflow: hidden; margin-bottom: 18rpx; }
.summary-item { min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 6rpx; padding: 16rpx 8rpx; border-right: 1rpx solid #eef2f7; }
.summary-item:last-child { border-right: 0; }
.summary-value { max-width: 100%; font-size: 30rpx; font-weight: 700; color: #1f4b79; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.summary-label { font-size: 22rpx; color: #8a94a6; }
.item-list { display: flex; flex-direction: column; gap: 18rpx; }
.item-card { padding: 20rpx; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 18rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
.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; }
.item-code { flex-shrink: 0; max-width: 240rpx; font-size: 24rpx; color: #8a94a6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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; }
.pallet-list { margin-top: 18rpx; display: flex; flex-direction: column; gap: 12rpx; }
.pallet-card { padding: 16rpx 18rpx; background: #f8fafc; border: 1rpx solid #e8eef6; border-radius: 14rpx; }
.pallet-top { display: flex; align-items: center; justify-content: space-between; gap: 12rpx; margin-bottom: 12rpx; }
.pallet-code { flex: 1; min-width: 0; font-size: 27rpx; color: #1f2937; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pallet-count { flex-shrink: 0; font-size: 24rpx; color: #1f4b79; font-weight: 600; }
.pallet-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10rpx; }
.pallet-cell { min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
.pallet-label { font-size: 21rpx; color: #9ca3af; }
.pallet-value { font-size: 24rpx; color: #475569; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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: #1f4b79; color: #ffffff; }
</style>