feat: 新增备件出库详细页
parent
e358345cf1
commit
c82d937248
@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<NavBar :title="'备件出库详情'" />
|
||||
|
||||
<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>
|
||||
<text :class="['status-tag', statusClass(detail.status)]">{{ statusText(detail.status) }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading-card">加载中...</view>
|
||||
<template v-else>
|
||||
<view class="readonly-grid">
|
||||
<view class="readonly-item">
|
||||
<text class="readonly-label">单据号</text>
|
||||
<text class="readonly-value">{{ textValue(detail.no) }}</text>
|
||||
</view>
|
||||
<view class="readonly-item">
|
||||
<text class="readonly-label">出库类型</text>
|
||||
<text class="readonly-value">{{ textValue(detail.outType) }}</text>
|
||||
</view>
|
||||
<view class="readonly-item">
|
||||
<text class="readonly-label">出库时间</text>
|
||||
<text class="readonly-value">{{ formatDateTime(detail.outTime || detail.createTime) }}</text>
|
||||
</view>
|
||||
<view class="readonly-item">
|
||||
<text class="readonly-label">经办人</text>
|
||||
<text class="readonly-value">{{ textValue(detail.stockUserName) }}</text>
|
||||
</view>
|
||||
<view class="readonly-item">
|
||||
<text class="readonly-label">出库数量</text>
|
||||
<text class="readonly-value highlight">{{ textValue(detail.totalCount) }}{{ detail.totalCount != null ? ' 个' : '' }}</text>
|
||||
</view>
|
||||
<view class="readonly-item">
|
||||
<text class="readonly-label">审核人</text>
|
||||
<text class="readonly-value">{{ textValue(detail.auditUserName) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-field">
|
||||
<text class="form-label">备注</text>
|
||||
<view class="readonly-box">{{ textValue(detail.remark) }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="attachmentList.length" class="form-field">
|
||||
<text class="form-label">附件</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">出库清单</text>
|
||||
</view>
|
||||
<text class="count-badge">{{ itemList.length }} 项</text>
|
||||
</view>
|
||||
|
||||
<view v-if="itemList.length" class="summary-strip">
|
||||
<view class="summary-item">
|
||||
<text class="summary-value">{{ itemList.length }}</text>
|
||||
<text class="summary-label">备件</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-value">{{ totalPieceCount }} 个</text>
|
||||
<text class="summary-label">总数量</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) }}</text>
|
||||
</view>
|
||||
<view class="info-grid">
|
||||
<view class="info-cell">
|
||||
<text class="info-label">仓库</text>
|
||||
<text class="info-value">{{ textValue(item.warehouseName || warehouseMap[item.warehouseId]) }}</text>
|
||||
</view>
|
||||
<view class="info-cell">
|
||||
<text class="info-label">库区</text>
|
||||
<text class="info-value">{{ textValue(item.areaName) }}</text>
|
||||
</view>
|
||||
<view class="info-cell">
|
||||
<text class="info-label">数量</text>
|
||||
<text class="info-value">{{ textValue(item.count) }}{{ getSparepartUnitData(item).unitName && ' ' + getSparepartUnitData(item).unitName }}</text>
|
||||
</view>
|
||||
<view class="info-cell">
|
||||
<text class="info-label">换算关系</text>
|
||||
<text class="info-value">{{ textValue(getSparepartUnitData(item).packagingRule) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-card">{{ loading ? '加载中...' : '暂无出库清单' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="action-bar">
|
||||
<view class="action-btn back-btn" @click="handleBack">返回</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import NavBar from '@/components/common/NavBar.vue'
|
||||
import { getSparepartOutboundDetail } from '@/api/mes/sparepartOutbound'
|
||||
import { getSparepartDetail } from '@/api/mes/sparepart'
|
||||
import { getWarehouseSimpleList } from '@/api/mes/moldget'
|
||||
|
||||
const detailId = ref(null)
|
||||
const detail = ref({})
|
||||
const loading = ref(false)
|
||||
const sparepartMap = ref({})
|
||||
const warehouseMap = ref({})
|
||||
|
||||
const itemList = computed(() => {
|
||||
const list = detail.value?.items || detail.value?.itemList || detail.value?.stockOutItems || []
|
||||
return Array.isArray(list) ? list : []
|
||||
})
|
||||
const attachmentList = computed(() => {
|
||||
const raw = detail.value?.fileUrl || detail.value?.attachments || detail.value?.attachmentList
|
||||
if (!raw) return []
|
||||
if (Array.isArray(raw)) return raw
|
||||
return typeof raw === 'object' ? [raw] : String(raw).split(',').map(s => s.trim()).filter(Boolean)
|
||||
})
|
||||
const totalPieceCount = computed(() => itemList.value.reduce((sum, item) => sum + (Number(item.count) || 0), 0))
|
||||
|
||||
function getSparepartUnitData(item) {
|
||||
const sp = sparepartMap.value[item.productId]
|
||||
const puName = sp?.purchaseUnitName || item.purchaseUnitName || ''
|
||||
const puQty = sp?.purchaseUnitConvertQuantity || item.purchaseUnitConvertQuantity
|
||||
const unitName = sp?.unitName || sp?.productUnitName || item.unitName || item.productUnitName || '个'
|
||||
let packagingRule = sp?.packagingRule || item.packagingRule || ''
|
||||
if (!packagingRule && puName && puQty) {
|
||||
packagingRule = `1${puName}=${puQty}${unitName}`
|
||||
}
|
||||
return { packagingRule: packagingRule || '-', unitName: unitName }
|
||||
}
|
||||
|
||||
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 '-'
|
||||
const raw = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value
|
||||
const date = new Date(raw)
|
||||
if (Number.isNaN(date.getTime())) return String(value)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
function statusText(s) {
|
||||
const map = { 0: '待出库', 10: '待审核', 20: '已出库', 1: '已驳回' }
|
||||
return map[Number(s)] || textValue(s)
|
||||
}
|
||||
function statusClass(s) {
|
||||
const map = { 0: 'text-primary', 10: 'text-warning', 20: 'text-success', 1: 'text-danger' }
|
||||
return map[Number(s)] || ''
|
||||
}
|
||||
function getAttachmentName(file, index) {
|
||||
if (file?.name) return file.name
|
||||
if (file?.fileName) return file.fileName
|
||||
const text = String(file?.url || file?.fileUrl || file || '')
|
||||
const name = text.split('/').pop()
|
||||
return name || `附件${index + 1}`
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!detailId.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSparepartOutboundDetail(detailId.value)
|
||||
detail.value = res?.data !== undefined ? res.data : res
|
||||
// 加载仓库列表做名称映射
|
||||
try {
|
||||
const wRes = await getWarehouseSimpleList()
|
||||
const wData = Array.isArray(wRes) ? wRes : (Array.isArray(wRes?.data) ? wRes.data : [])
|
||||
const wMap = {}
|
||||
wData.forEach(w => { wMap[w.id] = w.name || String(w.id) })
|
||||
warehouseMap.value = wMap
|
||||
} catch (e) {}
|
||||
// 加载备件详情获取换算关系
|
||||
const items = itemList.value
|
||||
if (items.length) {
|
||||
const ids = [...new Set(items.map(i => i.productId).filter(Boolean))]
|
||||
const map = {}
|
||||
await Promise.all(ids.map(async (id) => {
|
||||
try { const r = await getSparepartDetail(id); const d = r?.data || r; if (d) map[id] = d } catch (e) {}
|
||||
}))
|
||||
sparepartMap.value = map
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载详情失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
function handleBack() { uni.navigateBack() }
|
||||
|
||||
onLoad((options) => { detailId.value = options?.id || null; 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; }
|
||||
.count-badge { flex-shrink: 0; padding: 6rpx 16rpx; border-radius: 999rpx; background: #eff6ff; color: #1f7cff; font-size: 22rpx; font-weight: 600; }
|
||||
.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: 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(2, 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; }
|
||||
.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>
|
||||
Loading…
Reference in New Issue