style: 备件库存查询-物料库存查询搜索框优化

master
zhongwenkai 24 hours ago
parent 29e2c544ca
commit 55fbdf7ecd

@ -1920,38 +1920,54 @@ export default {
},
sparepartInventory: {
moduleName: '备件库存查询',
detailTitle: '备件详情',
searchPlaceholder: '请输入备件编码或名称',
allArea: '全部库区',
areaPlaceholder: '库区筛选',
stockInfo: '库存信息',
productName: '物料名称',
barCode: '物料编码',
warehouse: '仓库',
area: '库区',
count: '基本数量',
stockDisplay: '库存展示',
areaStockDisplay: '库区库存展示',
totalPackageCount: '总包数',
totalBaseCount: '总个数',
unit: '单位',
category: '物料小类',
standard: '规格',
packagingRule: '包装/换算规则',
recentInTime: '最近入库',
recentOutTime: '最近出库',
empty: '暂无备件库存数据'
latestInTime: '最近入库',
latestOutTime: '最近出库',
remark: '备注',
empty: '暂无备件库存数据',
noDetailId: '暂无备件库存ID'
},
materialInventory: {
moduleName: '物料库存查询',
detailTitle: '物料详情',
searchPlaceholder: '请输入物料编码或名称',
allArea: '全部库区',
areaPlaceholder: '库区筛选',
stockInfo: '库存信息',
productName: '物料名称',
barCode: '物料编码',
warehouse: '仓库',
area: '库区',
count: '基本数量',
stockDisplay: '库存展示',
areaStockDisplay: '库区库存展示',
totalPackageCount: '总包数',
totalBaseCount: '总个数',
unit: '单位',
category: '物料小类',
standard: '规格',
packagingRule: '包装/换算规则',
recentInTime: '最近入库',
recentOutTime: '最近出库',
empty: '暂无物料库存数据'
latestInTime: '最近入库',
latestOutTime: '最近出库',
remark: '备注',
empty: '暂无物料库存数据',
noDetailId: '暂无物料库存ID'
}
}

@ -1,81 +1,164 @@
<template>
<view class="page-container">
<NavBar :title="'物料详情'" />
<NavBar :title="t('materialInventory.detailTitle')" />
<view class="content-section" v-if="detail">
<view class="page-title-bar">
<view class="page-title-bar-line"></view>
<text class="page-title">物料基本信息</text>
</view>
<view class="detail-card">
<view class="info-row-top">
<view class="item-image-wrap">
<image v-if="detailImage" :src="detailImage" class="item-image" mode="aspectFill" />
<view v-else class="item-image-placeholder">📦</view>
</view>
<view class="item-info-col">
<view class="item-row"><text class="item-label">物料名称</text><text class="item-value">{{ textValue(detail.name) }}</text></view>
<view class="item-row"><text class="item-label">物料编码</text><text class="item-value">{{ textValue(detail.barCode) }}</text></view>
<view class="item-row"><text class="item-label">物料小类</text><text class="item-value">{{ textValue(detail.categoryName) }}</text></view>
<scroll-view scroll-y class="detail-scroll">
<view v-if="detail" class="content-section">
<view class="detail-card">
<view class="info-row-top">
<view class="item-image-wrap">
<image v-if="detailImage" :src="detailImage" class="item-image" mode="aspectFill" />
<view v-else class="item-image-placeholder">📦</view>
</view>
<view class="item-info-col">
<view class="item-row">
<text class="item-label">{{ t('materialInventory.productName') }}</text>
<text class="item-value">{{ textValue(detail.name || detail.productName) }}</text>
</view>
<view class="item-row">
<text class="item-label">{{ t('materialInventory.barCode') }}</text>
<text class="item-value">{{ textValue(detail.barCode || detail.productBarCode) }}</text>
</view>
<view class="item-row">
<text class="item-label">{{ t('materialInventory.category') }}</text>
<text class="item-value">{{ textValue(detail.categoryName || detail.subCategoryName) }}</text>
</view>
</view>
</view>
</view>
<view class="info-list">
<view class="info-row"><text class="info-label">仓库</text><text class="info-value">{{ textValue(warehouseName) }}</text></view>
<view class="info-row"><text class="info-label">库区</text><text class="info-value">{{ textValue(areaName) }}</text></view>
<view class="info-row"><text class="info-label">库存展示</text><text class="info-value highlight">{{ textValue(detail.stockDisplay) }}</text></view>
<view class="info-row"><text class="info-label">基本数量</text><text class="info-value highlight">{{ textValue(stockCount) }}{{ stockUnit !== '-' ? stockUnit : '' }}</text></view>
<view class="info-row"><text class="info-label">包装/换算规则</text><text class="info-value">{{ textValue(packagingRule) }}</text></view>
<view class="info-row"><text class="info-label">最近入库</text><text class="info-value">{{ formatDateTime(recentInTime) }}</text></view>
<view class="info-row"><text class="info-label">最近出库</text><text class="info-value">{{ formatDateTime(recentOutTime) }}</text></view>
<view class="section-card">
<view class="section-header">
<view class="section-icon">
<uni-icons type="info" size="22" color="#1f7cff" />
</view>
<text class="section-title">{{ t('materialInventory.stockInfo') }}</text>
</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('materialInventory.packagingRule') }}</text>
<text class="info-value">{{ textValue(detail.packagingRule) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInventory.warehouse') }}</text>
<text class="info-value">{{ textValue(detail.warehouseName) }}</text>
</view>
<view class="info-row block-row">
<text class="info-label">{{ t('materialInventory.stockDisplay') }}</text>
<view class="tag-wrap">
<text v-for="item in stockDisplayList" :key="item" class="stock-tag">{{ item }}</text>
<text v-if="!stockDisplayList.length" class="info-value">-</text>
</view>
</view>
<view class="info-row block-row">
<text class="info-label">{{ t('materialInventory.areaStockDisplay') }}</text>
<view class="tag-wrap">
<text v-for="item in areaStockDisplayList" :key="item" class="stock-tag area-tag">{{ item }}</text>
<text v-if="!areaStockDisplayList.length" class="info-value">-</text>
</view>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInventory.totalPackageCount') }}</text>
<text class="info-value highlight">{{ formatStockCount(detail.totalPackageCount) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInventory.totalBaseCount') }}</text>
<text class="info-value highlight">{{ formatStockCount(detail.totalBaseCount) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInventory.unit') }}</text>
<text class="info-value">{{ textValue(detail.unitName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInventory.latestInTime') }}</text>
<text class="info-value">{{ formatDateTime(detail.latestInTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInventory.latestOutTime') }}</text>
<text class="info-value">{{ formatDateTime(detail.latestOutTime) }}</text>
</view>
</view>
</view>
</view>
</view>
<view v-else-if="loading" class="hint">加载中...</view>
<view v-else class="hint">加载失败</view>
<view v-else-if="loading" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else class="hint">{{ t('functionCommon.loadFailed') }}</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getMaterialInventoryPage, getProductDetail } from '@/api/mes/sparepart'
const { t } = useI18n()
const detail = ref(null)
const loading = ref(true)
const stockCount = ref(0)
const stockUnit = ref('-')
const packagingRule = ref('-')
const warehouseName = ref('-')
const areaName = ref('-')
const recentInTime = ref('')
const recentOutTime = ref('')
const stockDisplayList = computed(() => formatStockDisplay(detail.value?.stockDisplay))
const areaStockDisplayList = computed(() => getAreaStockDisplayList(detail.value?.areaStocks))
const detailImage = computed(() => {
const images = detail.value?.images
const images = detail.value?.images || detail.value?.image || detail.value?.picUrl
if (!images) return ''
if (Array.isArray(images) && images.length) return String(images[0])
return String(images).split(',')[0]?.trim() || ''
})
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
function textValue(value) {
if (value === 0) return '0'
if (value == null) return '-'
const text = String(value).trim()
return text || '-'
}
function formatStockCount(value) {
if (value === undefined || value === null || value === '') return '-'
const num = Number(value)
return Number.isFinite(num) ? num.toLocaleString() : String(value)
}
function formatStockDisplay(value) {
if (!value) return []
return String(value)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
function getAreaStockDisplayList(value) {
if (!Array.isArray(value)) return []
return value
.map((item) => item?.stockDisplay?.trim())
.filter(Boolean)
}
function formatDateTime(value) {
if (!value) return '-'
const n = Number(value)
if (Number.isFinite(n) && n > 0) {
const d = new Date(n < 1e12 ? n * 1000 : n)
if (!Number.isNaN(d.getTime())) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
}
if (Array.isArray(value) && value.length >= 3) {
const [year, month, day, hour = 0, minute = 0, second = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`
}
return String(value)
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) return formatDate(date)
}
const date = new Date(text)
return Number.isNaN(date.getTime()) ? text : formatDate(date)
}
function formatDate(date) {
const pad = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
onLoad(async (options) => {
@ -85,29 +168,24 @@ onLoad(async (options) => {
const arId = options?.areaId
if (!id && !productId) {
loading.value = false
uni.showToast({ title: '缺少物料信息', icon: 'none' })
uni.showToast({ title: t('materialInventory.noDetailId'), icon: 'none' })
return
}
try {
//
const cached = getApp().globalData._materialInventoryDetail
if (cached && String(cached.productId) === String(id || productId)) {
detail.value = {
name: cached.productName,
barCode: cached.barCode,
name: cached.productName || cached.name,
barCode: cached.barCode || cached.productBarCode,
categoryName: cached.categoryName,
standard: cached.standard,
...cached
...cached,
stockDisplay: cached.stockDisplay || String(cached.count || ''),
unitName: cached.unitName || cached.purchaseUnitName,
totalPackageCount: cached.totalPackageCount ?? cached.count,
totalBaseCount: cached.totalBaseCount ?? cached.count,
latestInTime: cached.recentInTime || cached.latestInTime,
latestOutTime: cached.recentOutTime || cached.latestOutTime
}
stockCount.value = Number(cached.count ?? 0)
stockUnit.value = cached.unitName || '-'
detail.value.stockDisplay = cached.stockDisplay || String(cached.count || '')
packagingRule.value = cached.packagingRule || '-'
warehouseName.value = cached.warehouseName || '-'
areaName.value = cached.areaName || '-'
recentInTime.value = cached.recentInTime || ''
recentOutTime.value = cached.recentOutTime || ''
//
if (!cached.images) {
getProductDetail(cached.productId).then(res => {
const pd = res?.data || res
@ -119,7 +197,6 @@ onLoad(async (options) => {
return
}
// stock/page + product/get
const [pageRes, prodRes] = await Promise.all([
getMaterialInventoryPage({
pageNo: 1,
@ -136,23 +213,26 @@ onLoad(async (options) => {
const pd = prodRes?.data || prodRes
if (item) {
detail.value = {
name: item.productName,
name: item.productName || item.name,
barCode: item.barCode || item.productBarCode,
categoryName: item.categoryName || item.subCategoryName,
...item,
stockDisplay: item.stockDisplay || String(item.count || ''),
unitName: item.unitName || item.purchaseUnitName,
totalPackageCount: item.totalPackageCount ?? item.count,
totalBaseCount: item.totalBaseCount ?? item.count,
latestInTime: item.recentInTime || item.latestInTime,
latestOutTime: item.recentOutTime || item.latestOutTime,
images: pd?.images || item.images
}
stockCount.value = Number(item.count ?? 0)
stockUnit.value = item.unitName || item.purchaseUnitName || '-'
packagingRule.value = item.packagingRule || '-'
warehouseName.value = item.warehouseName || '-'
areaName.value = item.areaName || '-'
recentInTime.value = item.recentInTime || ''
recentOutTime.value = item.recentOutTime || ''
} else {
detail.value = { name: '-', barCode: '-', categoryName: '-', standard: '-', stockDisplay: '-', images: pd?.images }
detail.value = {
name: '-', barCode: '-', categoryName: '-',
stockDisplay: '-', images: pd?.images
}
}
} catch (e) {
console.error('获取物料详情失败:', e)
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
}
@ -160,129 +240,32 @@ onLoad(async (options) => {
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f3f4f6;
padding-bottom: 40rpx;
}
.content-section {
padding: 24rpx 28rpx;
}
.page-title-bar {
display: flex;
align-items: center;
gap: 10rpx;
padding: 0 0 20rpx;
}
.page-title-bar-line {
width: 6rpx;
height: 32rpx;
border-radius: 3rpx;
background: #2563eb;
flex-shrink: 0;
}
.page-title {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
}
.detail-card {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04);
}
.info-row-top {
display: flex;
align-items: center;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f3f4f6;
margin-bottom: 20rpx;
}
.item-image-wrap {
width: 130rpx;
height: 130rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f8fafc;
border: 1rpx solid #f0f0f0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.page-container { min-height: 100vh; background: #f5f7fb; }
.detail-scroll { height: calc(100vh - 120rpx); }
.content-section { padding: 20rpx 24rpx 40rpx; }
.detail-card { padding: 28rpx; margin-bottom: 20rpx; background: #ffffff; border-radius: 16rpx; box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04); }
.info-row-top { display: flex; align-items: center; }
.item-image-wrap { width: 130rpx; height: 130rpx; border-radius: 12rpx; overflow: hidden; background: #f8fafc; border: 1rpx solid #f0f0f0; flex-shrink: 0; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; }
.item-image { width: 100%; height: 100%; }
.item-image-placeholder { font-size: 48rpx; opacity: 0.2; }
.item-info-col {
flex: 1;
min-width: 0;
}
.item-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 8rpx 0;
&:last-child { padding-bottom: 0; }
}
.item-label {
font-size: 28rpx;
color: #9ca3af;
flex-shrink: 0;
}
.item-value {
font-size: 28rpx;
color: #374151;
text-align: right;
word-break: break-all;
}
.info-list {
display: flex;
flex-direction: column;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
padding: 16rpx 0;
border-bottom: 1rpx solid #f3f4f6;
&:last-child { border-bottom: 0; }
}
.info-label {
font-size: 28rpx;
color: #9ca3af;
font-weight: 600;
flex-shrink: 0;
}
.info-value {
flex: 1;
text-align: right;
font-size: 28rpx;
color: #374151;
word-break: break-all;
&.highlight {
color: #1f4b79;
font-weight: 700;
}
}
.hint {
text-align: center;
padding: 160rpx 0;
font-size: 28rpx;
color: #9ca3af;
}
.item-info-col { flex: 1; min-width: 0; }
.item-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 8rpx 0; }
.item-row:last-child { padding-bottom: 0; }
.item-label { font-size: 28rpx; color: #9ca3af; flex-shrink: 0; }
.item-value { font-size: 28rpx; color: #374151; text-align: right; word-break: break-all; }
.section-card { padding: 24rpx; margin-bottom: 20rpx; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 20rpx; 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; }
.info-list { display: flex; flex-direction: column; }
.info-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 20rpx; padding: 18rpx 0; border-bottom: 1rpx solid #f3f4f6; }
.info-row:last-child { border-bottom: 0; }
.info-row.block-row { flex-direction: column; gap: 12rpx; }
.info-label { width: 190rpx; font-size: 26rpx; color: #94a3b8; flex-shrink: 0; }
.info-value { flex: 1; text-align: right; font-size: 27rpx; color: #334155; line-height: 1.5; word-break: break-all; }
.info-value.highlight { color: #1f4b79; font-weight: 700; }
.tag-wrap { width: 100%; display: flex; flex-wrap: wrap; gap: 12rpx; justify-content: flex-end; }
.stock-tag { max-width: 100%; padding: 8rpx 16rpx; border-radius: 999rpx; color: #1d4ed8; background: #dbeafe; font-size: 24rpx; line-height: 1.35; word-break: break-all; }
.area-tag { color: #0369a1; background: #e0f2fe; }
.hint { padding: 160rpx 0; text-align: center; color: #94a3b8; font-size: 26rpx; }
</style>

@ -3,23 +3,23 @@
<NavBar :title="t('materialInventory.moduleName')" />
<view class="filter-bar">
<view class="scan-input-row">
<input
id="material-inventory-scan-input"
v-model="searchKeyword"
class="scan-input"
type="text"
placeholder="请输入物料编码或名称"
placeholder-class="scan-placeholder"
confirm-type="done"
@confirm="onScanInputConfirm"
/>
</view>
<view class="area-box" @click="openAreaPicker">
<text class="area-box-text">{{ selectedAreaLabel || t('materialInventory.allArea') }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
<view class="filter-row search-row">
<view class="keyword-wrap">
<input
id="material-inventory-scan-input"
v-model="searchKeyword"
class="keyword-input"
type="text"
placeholder="请输入物料编码或名称"
placeholder-class="scan-placeholder"
confirm-type="search"
@confirm="onScanInputConfirm"
/>
</view>
<view class="icon-filter-btn" @click="resetFilters">
<uni-icons type="refresh" size="24" color="#7b8491"></uni-icons>
</view>
</view>
<view class="reset-filter-btn" @click="resetFilters">{{ t('functionCommon.reset') }}</view>
</view>
<scroll-view
@ -32,15 +32,11 @@
<view v-for="item in list" :key="item.id" class="inventory-card" @click="openDetail(item)">
<view class="card-main">
<view class="card-top">
<text class="product-name">{{ textValue(item.barCode) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInventory.productName') }}</text>
<text class="info-value">{{ textValue(item.productName) }}</text>
<text class="product-name">{{ textValue(item.productName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInventory.area') }}</text>
<text class="info-value">{{ textValue(item.areaName) }}</text>
<text class="info-label">{{ t('materialInventory.barCode') }}</text>
<text class="info-value">{{ textValue(item.barCode) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInventory.stockDisplay') }}</text>
@ -342,77 +338,56 @@ function formatDateTime(value) {
}
.filter-bar {
padding: 18rpx 14rpx 20rpx;
background: #f3f4f6;
}
.filter-row {
display: flex;
align-items: center;
gap: 12rpx;
padding: 18rpx 24rpx 20rpx;
gap: 18rpx;
}
.scan-input-row {
flex: 1;
.keyword-wrap,
.icon-filter-btn {
height: 66rpx;
border: 1rpx solid #d9dde5;
background: #ffffff;
box-sizing: border-box;
}
.keyword-wrap {
min-width: 0;
min-height: 72rpx !important;
height: 72rpx !important;
flex: 1;
display: flex;
align-items: center;
}
.scan-input {
.keyword-input {
width: 100%;
min-height: 72rpx !important;
height: 72rpx !important;
height: 64rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
background: #fff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
box-sizing: border-box;
color: #374151;
}
.scan-placeholder {
color: #9ca3af;
}
.area-box {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
width: 180rpx;
height: 72rpx;
padding: 0 14rpx;
background: #ffffff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
box-sizing: border-box;
}
.area-box-text {
color: #374151;
font-size: 26rpx;
max-width: 110rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reset-filter-btn {
flex-shrink: 0;
.icon-filter-btn {
width: 66rpx;
flex: 0 0 66rpx;
display: flex;
align-items: center;
justify-content: center;
width: 96rpx;
height: 72rpx;
font-size: 24rpx;
color: #4b5563;
background: #ffffff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
box-sizing: border-box;
border-color: transparent;
background: transparent;
}
.content-scroll {
width: 100%;
height: calc(100vh - 120rpx);
height: calc(100vh - var(--status-bar-height) - 88rpx - 106rpx);
}
.list-wrap {

@ -1,94 +1,164 @@
<template>
<view class="page-container">
<NavBar :title="'备件详情'" />
<NavBar :title="t('sparepartInventory.detailTitle')" />
<view class="content-section" v-if="detail">
<!-- 标题 -->
<view class="page-title-bar">
<view class="page-title-bar-line"></view>
<text class="page-title">备件基本信息</text>
</view>
<view class="detail-card">
<!-- 图片 + 前三行信息 -->
<view class="info-row-top">
<view class="item-image-wrap">
<image v-if="detailImage" :src="detailImage" class="item-image" mode="aspectFill" />
<view v-else class="item-image-placeholder">📦</view>
</view>
<view class="item-info-col">
<view class="item-row"><text class="item-label">物料名称</text><text class="item-value">{{ textValue(detail.name) }}</text></view>
<view class="item-row"><text class="item-label">物料编码</text><text class="item-value">{{ textValue(detail.barCode) }}</text></view>
<view class="item-row"><text class="item-label">物料小类</text><text class="item-value">{{ textValue(detail.categoryName) }}</text></view>
<scroll-view scroll-y class="detail-scroll">
<view v-if="detail" class="content-section">
<view class="detail-card">
<view class="info-row-top">
<view class="item-image-wrap">
<image v-if="detailImage" :src="detailImage" class="item-image" mode="aspectFill" />
<view v-else class="item-image-placeholder">📦</view>
</view>
<view class="item-info-col">
<view class="item-row">
<text class="item-label">{{ t('sparepartInventory.productName') }}</text>
<text class="item-value">{{ textValue(detail.name || detail.productName) }}</text>
</view>
<view class="item-row">
<text class="item-label">{{ t('sparepartInventory.barCode') }}</text>
<text class="item-value">{{ textValue(detail.barCode || detail.productBarCode) }}</text>
</view>
<view class="item-row">
<text class="item-label">{{ t('sparepartInventory.category') }}</text>
<text class="item-value">{{ textValue(detail.categoryName || detail.subCategoryName) }}</text>
</view>
</view>
</view>
</view>
<!-- 其余信息 -->
<view class="info-list">
<view class="info-row"><text class="info-label">仓库</text><text class="info-value">{{ textValue(warehouseName) }}</text></view>
<view class="info-row"><text class="info-label">库区</text><text class="info-value">{{ textValue(areaName) }}</text></view>
<view class="info-row"><text class="info-label">库存展示</text><text class="info-value highlight">{{ textValue(detail.stockDisplay) }}</text></view>
<view class="info-row"><text class="info-label">基本数量</text><text class="info-value highlight">{{ textValue(stockCount) }}{{ stockUnit !== '-' ? stockUnit : '' }}</text></view>
<view class="info-row"><text class="info-label">包装/换算规则</text><text class="info-value">{{ textValue(packagingRule) }}</text></view>
<view class="info-row"><text class="info-label">最近入库</text><text class="info-value">{{ formatDateTime(recentInTime) }}</text></view>
<view class="info-row"><text class="info-label">最近出库</text><text class="info-value">{{ formatDateTime(recentOutTime) }}</text></view>
<view class="section-card">
<view class="section-header">
<view class="section-icon">
<uni-icons type="info" size="22" color="#1f7cff" />
</view>
<text class="section-title">{{ t('sparepartInventory.stockInfo') }}</text>
</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.packagingRule') }}</text>
<text class="info-value">{{ textValue(detail.packagingRule) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.warehouse') }}</text>
<text class="info-value">{{ textValue(detail.warehouseName) }}</text>
</view>
<view class="info-row block-row">
<text class="info-label">{{ t('sparepartInventory.stockDisplay') }}</text>
<view class="tag-wrap">
<text v-for="item in stockDisplayList" :key="item" class="stock-tag">{{ item }}</text>
<text v-if="!stockDisplayList.length" class="info-value">-</text>
</view>
</view>
<view class="info-row block-row">
<text class="info-label">{{ t('sparepartInventory.areaStockDisplay') }}</text>
<view class="tag-wrap">
<text v-for="item in areaStockDisplayList" :key="item" class="stock-tag area-tag">{{ item }}</text>
<text v-if="!areaStockDisplayList.length" class="info-value">-</text>
</view>
</view>
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.totalPackageCount') }}</text>
<text class="info-value highlight">{{ formatStockCount(detail.totalPackageCount) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.totalBaseCount') }}</text>
<text class="info-value highlight">{{ formatStockCount(detail.totalBaseCount) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.unit') }}</text>
<text class="info-value">{{ textValue(detail.unitName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.latestInTime') }}</text>
<text class="info-value">{{ formatDateTime(detail.latestInTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.latestOutTime') }}</text>
<text class="info-value">{{ formatDateTime(detail.latestOutTime) }}</text>
</view>
</view>
</view>
</view>
</view>
<view v-else-if="loading" class="hint">加载中...</view>
<view v-else class="hint">加载失败</view>
<view v-else-if="loading" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else class="hint">{{ t('functionCommon.loadFailed') }}</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getSparepartInventoryPage, getSparepartDetail } from '@/api/mes/sparepart'
const { t } = useI18n()
const detail = ref(null)
const loading = ref(true)
const stockCount = ref(0)
const stockUnit = ref('-')
const packagingRule = ref('-')
const warehouseName = ref('-')
const areaName = ref('-')
const recentInTime = ref('')
const recentOutTime = ref('')
const stockDisplayList = computed(() => formatStockDisplay(detail.value?.stockDisplay))
const areaStockDisplayList = computed(() => getAreaStockDisplayList(detail.value?.areaStocks))
const detailImage = computed(() => {
const images = detail.value?.images
const images = detail.value?.images || detail.value?.image || detail.value?.picUrl
if (!images) return ''
if (Array.isArray(images) && images.length) return String(images[0])
return String(images).split(',')[0]?.trim() || ''
})
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
function textValue(value) {
if (value === 0) return '0'
if (value == null) return '-'
const text = String(value).trim()
return text || '-'
}
function formatStockDisplay() {
// stockData stockDisplay count+unit
if (stockCount.value > 0) {
const su = stockUnit.value && stockUnit.value !== '-' ? stockUnit.value : ''
return su ? `${stockCount.value}${su}` : String(stockCount.value)
}
return '-'
function formatStockCount(value) {
if (value === undefined || value === null || value === '') return '-'
const num = Number(value)
return Number.isFinite(num) ? num.toLocaleString() : String(value)
}
function formatStockDisplay(value) {
if (!value) return []
return String(value)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
function getAreaStockDisplayList(value) {
if (!Array.isArray(value)) return []
return value
.map((item) => item?.stockDisplay?.trim())
.filter(Boolean)
}
function formatDateTime(value) {
if (!value) return '-'
//
const n = Number(value)
if (Number.isFinite(n) && n > 0) {
const d = new Date(n < 1e12 ? n * 1000 : n)
if (!Number.isNaN(d.getTime())) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
}
if (Array.isArray(value) && value.length >= 3) {
const [year, month, day, hour = 0, minute = 0, second = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`
}
return String(value)
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) return formatDate(date)
}
const date = new Date(text)
return Number.isNaN(date.getTime()) ? text : formatDate(date)
}
function formatDate(date) {
const pad = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
onLoad(async (options) => {
@ -98,30 +168,24 @@ onLoad(async (options) => {
const arId = options?.areaId
if (!id && !productId) {
loading.value = false
uni.showToast({ title: '缺少备件信息', icon: 'none' })
uni.showToast({ title: t('sparepartInventory.noDetailId'), icon: 'none' })
return
}
try {
//
const cached = getApp().globalData._sparepartInventoryDetail
if (cached && String(cached.productId) === String(id || productId)) {
// productName -> name
detail.value = {
name: cached.productName,
barCode: cached.barCode,
name: cached.productName || cached.name,
barCode: cached.barCode || cached.productBarCode,
categoryName: cached.categoryName,
standard: cached.standard,
...cached
...cached,
stockDisplay: cached.stockDisplay || String(cached.count || ''),
unitName: cached.unitName || cached.purchaseUnitName,
totalPackageCount: cached.totalPackageCount ?? cached.count,
totalBaseCount: cached.totalBaseCount ?? cached.count,
latestInTime: cached.recentInTime || cached.latestInTime,
latestOutTime: cached.recentOutTime || cached.latestOutTime
}
stockCount.value = Number(cached.count ?? 0)
stockUnit.value = cached.unitName || '-'
detail.value.stockDisplay = cached.stockDisplay || String(cached.count || '')
packagingRule.value = cached.packagingRule || '-'
warehouseName.value = cached.warehouseName || '-'
areaName.value = cached.areaName || '-'
recentInTime.value = cached.recentInTime || ''
recentOutTime.value = cached.recentOutTime || ''
// stock/page images
if (!cached.images) {
getSparepartDetail(cached.productId).then(res => {
const pd = res?.data || res
@ -133,7 +197,6 @@ onLoad(async (options) => {
return
}
// stock/page + product/get
const [pageRes, prodRes] = await Promise.all([
getSparepartInventoryPage({
pageNo: 1,
@ -150,23 +213,26 @@ onLoad(async (options) => {
const pd = prodRes?.data || prodRes
if (item) {
detail.value = {
name: item.productName,
name: item.productName || item.name,
barCode: item.barCode || item.productBarCode,
categoryName: item.categoryName || item.subCategoryName,
...item,
stockDisplay: item.stockDisplay || String(item.count || ''),
images: pd?.images || item.images // product/get
unitName: item.unitName || item.purchaseUnitName,
totalPackageCount: item.totalPackageCount ?? item.count,
totalBaseCount: item.totalBaseCount ?? item.count,
latestInTime: item.recentInTime || item.latestInTime,
latestOutTime: item.recentOutTime || item.latestOutTime,
images: pd?.images || item.images
}
stockCount.value = Number(item.count ?? 0)
stockUnit.value = item.unitName || item.purchaseUnitName || '-'
packagingRule.value = item.packagingRule || '-'
warehouseName.value = item.warehouseName || '-'
areaName.value = item.areaName || '-'
recentInTime.value = item.recentInTime || ''
recentOutTime.value = item.recentOutTime || ''
} else {
detail.value = { name: '-', barCode: '-', categoryName: '-', standard: '-', stockDisplay: '-', images: pd?.images }
detail.value = {
name: '-', barCode: '-', categoryName: '-',
stockDisplay: '-', images: pd?.images
}
}
} catch (e) {
console.error('获取备件详情失败:', e)
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
}
@ -174,132 +240,32 @@ onLoad(async (options) => {
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f3f4f6;
padding-bottom: 40rpx;
}
.content-section {
padding: 24rpx 28rpx;
}
/* 页面标题 */
.page-title-bar {
display: flex;
align-items: center;
gap: 10rpx;
padding: 0 0 20rpx;
}
.page-title-bar-line {
width: 6rpx;
height: 32rpx;
border-radius: 3rpx;
background: #2563eb;
flex-shrink: 0;
}
.page-title {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
}
/* 详情卡片 */
.detail-card {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04);
}
/* 图片 + 前三行 */
.info-row-top {
display: flex;
align-items: center;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f3f4f6;
margin-bottom: 20rpx;
}
.item-image-wrap {
width: 130rpx;
height: 130rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f8fafc;
border: 1rpx solid #f0f0f0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.page-container { min-height: 100vh; background: #f5f7fb; }
.detail-scroll { height: calc(100vh - 120rpx); }
.content-section { padding: 20rpx 24rpx 40rpx; }
.detail-card { padding: 28rpx; margin-bottom: 20rpx; background: #ffffff; border-radius: 16rpx; box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04); }
.info-row-top { display: flex; align-items: center; }
.item-image-wrap { width: 130rpx; height: 130rpx; border-radius: 12rpx; overflow: hidden; background: #f8fafc; border: 1rpx solid #f0f0f0; flex-shrink: 0; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; }
.item-image { width: 100%; height: 100%; }
.item-image-placeholder { font-size: 48rpx; opacity: 0.2; }
.item-info-col {
flex: 1;
min-width: 0;
}
.item-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 8rpx 0;
&:last-child { padding-bottom: 0; }
}
.item-label {
font-size: 28rpx;
color: #9ca3af;
flex-shrink: 0;
}
.item-value {
font-size: 28rpx;
color: #374151;
text-align: right;
word-break: break-all;
}
.info-list {
display: flex;
flex-direction: column;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
padding: 16rpx 0;
border-bottom: 1rpx solid #f3f4f6;
&:last-child { border-bottom: 0; }
}
.info-label {
font-size: 28rpx;
color: #9ca3af;
font-weight: 600;
flex-shrink: 0;
}
.info-value {
flex: 1;
text-align: right;
font-size: 28rpx;
color: #374151;
word-break: break-all;
&.highlight {
color: #1f4b79;
font-weight: 700;
}
}
.hint {
text-align: center;
padding: 160rpx 0;
font-size: 28rpx;
color: #9ca3af;
}
.item-info-col { flex: 1; min-width: 0; }
.item-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 8rpx 0; }
.item-row:last-child { padding-bottom: 0; }
.item-label { font-size: 28rpx; color: #9ca3af; flex-shrink: 0; }
.item-value { font-size: 28rpx; color: #374151; text-align: right; word-break: break-all; }
.section-card { padding: 24rpx; margin-bottom: 20rpx; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 20rpx; 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; }
.info-list { display: flex; flex-direction: column; }
.info-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 20rpx; padding: 18rpx 0; border-bottom: 1rpx solid #f3f4f6; }
.info-row:last-child { border-bottom: 0; }
.info-row.block-row { flex-direction: column; gap: 12rpx; }
.info-label { width: 190rpx; font-size: 26rpx; color: #94a3b8; flex-shrink: 0; }
.info-value { flex: 1; text-align: right; font-size: 27rpx; color: #334155; line-height: 1.5; word-break: break-all; }
.info-value.highlight { color: #1f4b79; font-weight: 700; }
.tag-wrap { width: 100%; display: flex; flex-wrap: wrap; gap: 12rpx; justify-content: flex-end; }
.stock-tag { max-width: 100%; padding: 8rpx 16rpx; border-radius: 999rpx; color: #1d4ed8; background: #dbeafe; font-size: 24rpx; line-height: 1.35; word-break: break-all; }
.area-tag { color: #0369a1; background: #e0f2fe; }
.hint { padding: 160rpx 0; text-align: center; color: #94a3b8; font-size: 26rpx; }
</style>

@ -3,23 +3,23 @@
<NavBar :title="t('sparepartInventory.moduleName')" />
<view class="filter-bar">
<view class="scan-input-row">
<input
id="sparepart-inventory-scan-input"
v-model="searchKeyword"
class="scan-input"
type="text"
placeholder="请输入备件编码或名称"
placeholder-class="scan-placeholder"
confirm-type="done"
@confirm="onScanInputConfirm"
/>
</view>
<view class="area-box" @click="openAreaPicker">
<text class="area-box-text">{{ selectedAreaLabel || t('sparepartInventory.allArea') }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
<view class="filter-row search-row">
<view class="keyword-wrap">
<input
id="sparepart-inventory-scan-input"
v-model="searchKeyword"
class="keyword-input"
type="text"
placeholder="请输入备件编码或名称"
placeholder-class="scan-placeholder"
confirm-type="search"
@confirm="onScanInputConfirm"
/>
</view>
<view class="icon-filter-btn" @click="resetFilters">
<uni-icons type="refresh" size="24" color="#7b8491"></uni-icons>
</view>
</view>
<view class="reset-filter-btn" @click="resetFilters">{{ t('functionCommon.reset') }}</view>
</view>
<scroll-view
@ -32,15 +32,11 @@
<view v-for="item in list" :key="item.id" class="inventory-card" @click="openDetail(item)">
<view class="card-main">
<view class="card-top">
<text class="product-name">{{ textValue(item.barCode) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.productName') }}</text>
<text class="info-value">{{ textValue(item.productName) }}</text>
<text class="product-name">{{ textValue(item.productName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.area') }}</text>
<text class="info-value">{{ textValue(item.areaName) }}</text>
<text class="info-label">{{ t('sparepartInventory.barCode') }}</text>
<text class="info-value">{{ textValue(item.barCode) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('sparepartInventory.stockDisplay') }}</text>
@ -348,77 +344,89 @@ function formatDateTime(value) {
}
.filter-bar {
padding: 18rpx 14rpx 20rpx;
background: #f3f4f6;
}
.filter-row {
display: flex;
align-items: center;
gap: 12rpx;
padding: 18rpx 24rpx 20rpx;
gap: 18rpx;
}
.search-row {
/* single row, no extra margin */
}
.scan-input-row {
.quick-row > picker {
min-width: 0;
flex: 1;
}
.keyword-wrap,
.status-filter,
.icon-filter-btn {
height: 66rpx;
border: 1rpx solid #d9dde5;
background: #ffffff;
box-sizing: border-box;
}
.keyword-wrap {
min-width: 0;
min-height: 72rpx !important;
height: 72rpx !important;
flex: 1;
display: flex;
align-items: center;
}
.scan-input {
.keyword-input {
width: 100%;
min-height: 72rpx !important;
height: 72rpx !important;
height: 64rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
background: #fff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
box-sizing: border-box;
color: #374151;
}
.scan-placeholder {
color: #9ca3af;
}
.area-box {
flex-shrink: 0;
.status-filter {
min-width: 0;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
width: 180rpx;
height: 72rpx;
padding: 0 14rpx;
background: #ffffff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
box-sizing: border-box;
padding: 0 18rpx 0 26rpx;
}
.area-box-text {
color: #374151;
font-size: 26rpx;
max-width: 110rpx;
.status-filter-text {
min-width: 0rpx;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 26rpx;
color: #374151;
}
.reset-filter-btn {
flex-shrink: 0;
.status-filter-text.placeholder {
color: #a8adb7;
}
.icon-filter-btn {
width: 66rpx;
flex: 0 0 66rpx;
display: flex;
align-items: center;
justify-content: center;
width: 96rpx;
height: 72rpx;
font-size: 24rpx;
color: #4b5563;
background: #ffffff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
box-sizing: border-box;
border-color: transparent;
background: transparent;
}
.content-scroll {
width: 100%;
height: calc(100vh - 120rpx);
height: calc(100vh - var(--status-bar-height) - 88rpx - 106rpx);
}
.list-wrap {

Loading…
Cancel
Save