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
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>
|