ck-chenkang 4 days ago
commit fb9c54fa0e

@ -1,13 +1,14 @@
import request from '@/utils/request'
// 备件列表(分页查询全部产品,前端按 categoryType=5 过滤备件)
// 备件列表(分页查询,后端按 categoryType=3 过滤备件)
export function getSparepartSimpleList(pageNo = 1) {
return request({
url: '/admin-api/erp/product/page',
method: 'get',
params: {
pageNo,
pageSize: 100
pageSize: 100,
categoryType: 3
}
})
}
@ -21,7 +22,7 @@ export function getSparepartDetail(id) {
})
}
// 备件库存数量
// 备件库存数量(返回数字)
export function getSparepartStockCount(productId) {
return request({
url: '/admin-api/erp/stock/get-count',
@ -30,6 +31,19 @@ export function getSparepartStockCount(productId) {
})
}
// 备件库存详情(带 warehouseId、areaId 查指定仓库库区库存,返回对象)
export function getSparepartStock(productId, warehouseId, areaId) {
return request({
url: '/admin-api/erp/stock/get',
method: 'get',
params: {
productId,
warehouseId: warehouseId || undefined,
areaId: areaId || undefined
}
})
}
// 备件库存查询(分页)
export function getSparepartInventoryPage(params) {
return request({

@ -357,9 +357,13 @@ async function handleSubmit() {
}
})
const now = new Date()
const [y, m, d] = inboundDate.value.split('-').map(Number)
const inTime = new Date(y, m - 1, d, now.getHours(), now.getMinutes(), now.getSeconds()).getTime()
const submitData = {
isCode: true,
inTime: inboundDate.value ? new Date(inboundDate.value.replace(/-/g, '/')).getTime() : Date.now(),
inTime: inTime,
stockUserId: String(selectedOperatorId.value),
supplierId: itemList.value[0]?.supplierId,
status: 0,

@ -31,7 +31,7 @@
<text class="info-value">{{ textValue(sparepart.standard || sparepart.deviceSpec) }}</text>
</view>
<view class="info-item">
<text class="info-label">当前库存</text>
<text class="info-label">当前库存</text>
<text class="info-value stock-highlight">{{ sparepart.count != null ? sparepart.count : 0 }}{{ textUnit(sparepart.unitName || sparepart.minStockUnitName || '个') }}</text>
</view>
</view>
@ -54,45 +54,6 @@
</view>
</view>
<!-- 入库数量 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">入库数量</text>
</view>
<view class="qty-input-card">
<view class="form-field">
<text class="form-label">入库数量</text>
<input
v-model="inboundQty"
class="form-input"
placeholder="请输入"
confirm-type="done"
/>
<text class="form-suffix-text">单位{{ textValue(sparepart.purchaseUnitName) }}</text>
</view>
<view class="convert-row">
<text class="convert-label-inline">折算后库存数量</text>
<text class="convert-value-inline">{{ calculatedStock }}</text>
<text class="convert-unit-inline">{{ stockUnitLabel(sparepart) }}</text>
</view>
<view class="convert-formula-inline">
{{ inboundQty || 0 }}{{ textValue(sparepart.purchaseUnitName) }} × {{ textValue(sparepart.purchaseUnitConvertQuantity) }}{{ stockUnitLabel(sparepart) }} = {{ calculatedStock }}{{ stockUnitLabel(sparepart) }}
</view>
</view>
<!-- 供应商 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">供应商</text>
</view>
<view class="select-row-card">
<view class="full-dropdown">
<text :class="{ placeholder: !defaultSupplierName }">
{{ defaultSupplierName || '未配置默认供应商' }}
</text>
</view>
</view>
<!-- 仓库/库区 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
@ -154,6 +115,45 @@
</view>
</view>
</view>
<!-- 入库数量 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">入库数量</text>
</view>
<view class="qty-input-card">
<view class="form-field">
<text class="form-label">入库数量</text>
<input
v-model="inboundQty"
class="form-input"
placeholder="请输入"
confirm-type="done"
/>
<text class="form-suffix-text">单位{{ textValue(sparepart.purchaseUnitName) }}</text>
</view>
<view class="convert-row">
<text class="convert-label-inline">折算后库存数量</text>
<text class="convert-value-inline">{{ calculatedStock }}</text>
<text class="convert-unit-inline">{{ stockUnitLabel(sparepart) }}</text>
</view>
<view class="convert-formula-inline">
{{ inboundQty || 0 }}{{ textValue(sparepart.purchaseUnitName) }} × {{ textValue(sparepart.purchaseUnitConvertQuantity) }}{{ stockUnitLabel(sparepart) }} = {{ calculatedStock }}{{ stockUnitLabel(sparepart) }}
</view>
</view>
<!-- 供应商 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">供应商</text>
</view>
<view class="select-row-card">
<view class="full-dropdown">
<text :class="{ placeholder: !defaultSupplierName }">
{{ defaultSupplierName || '未配置默认供应商' }}
</text>
</view>
</view>
</view>
<!-- 底部确认按钮 -->
@ -369,8 +369,7 @@ async function loadStockCount() {
if (!id) return
try {
const res = await getSparepartStockCount(id)
const count = res?.data ?? res
sparepart.value.count = (count != null) ? Number(count) : 0
sparepart.value.count = (res?.data ?? res) != null ? Number(res?.data ?? res) : 0
} catch (e) {
console.error('获取库存失败:', e)
sparepart.value.count = 0
@ -413,7 +412,8 @@ onHide(() => {
.info-item { display: flex; align-items: center; margin-bottom: 8rpx; }
.info-label { font-size: 26rpx; color: #6b7280; flex-shrink: 0; }
.info-name { font-size: 30rpx; font-weight: 700; color: #0f172a; }
.info-value { font-size: 26rpx; color: #374151; &.stock-highlight { color: #2563eb; font-weight: 500; } }
.info-value { font-size: 26rpx; color: #374151; }
.stock-highlight { font-size: 30rpx; font-weight: 700; color: #1f2937; }
.card-bottom { margin-top: 20rpx; padding-top: 20rpx; border-top: 1rpx solid #f0f0f0; }
.detail-row { display: flex; align-items: center; margin-bottom: 14rpx;

@ -147,7 +147,6 @@ function handleConfirm() {
// globalData
getApp().globalData._sparepartBeforeConfirm = item
const from = getApp().globalData._sparepartSelectFrom || 'inbound'
getApp().globalData._sparepartSelectFrom = null
const url = from === 'outbound'
? '/pages_function/pages/sparepartOutbound/sparepartConfirm'
: '/pages_function/pages/sparepartInbound/sparepartConfirm'

@ -25,8 +25,6 @@
<scroll-view
scroll-y
class="content-scroll"
:scroll-top="scrollTop"
@scroll="handleScroll"
@scrolltolower="loadMore"
:lower-threshold="80"
>
@ -83,10 +81,6 @@
</view>
</scroll-view>
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon"></text>
</view>
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
<uni-popup ref="warehousePickerRef" type="bottom" background-color="#fff">
@ -132,8 +126,6 @@ const pageSize = ref(10)
const total = ref(0)
const searchKeyword = ref('')
const selectedWarehouse = ref('')
const scrollTop = ref(0)
const showGoTop = ref(false)
const warehouseOptions = ref([])
@ -313,15 +305,6 @@ async function loadMore() {
await fetchList(false)
}
function handleScroll(e) {
const top = e?.detail?.scrollTop || 0
showGoTop.value = top > 600
}
function goTop() {
scrollTop.value = 0
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
@ -516,25 +499,6 @@ function formatDateTime(value) {
padding-top: 160rpx;
}
.go-top-btn {
position: fixed;
right: 32rpx;
bottom: calc(56rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: center;
width: 92rpx;
height: 92rpx;
border-radius: 46rpx;
background: rgba(31, 75, 121, 0.84);
box-shadow: 0 14rpx 30rpx rgba(24, 63, 108, 0.24);
}
.go-top-icon {
color: #ffffff;
font-size: 32rpx;
}
.picker-content {
padding: 24rpx 24rpx 36rpx;
border-radius: 28rpx 28rpx 0 0;

@ -219,8 +219,9 @@ async function handleSubmit() {
if (!outboundDate.value) { uni.showToast({ title: '请选择出库时间', icon: 'none' }); return }
if (!selectedOperatorId.value) { uni.showToast({ title: '请选择经办人', icon: 'none' }); return }
// Web 13 + stockUserId
const outTime = outboundDate.value ? new Date(outboundDate.value.replace(/-/g, '/')).getTime() : Date.now()
const now = new Date()
const [y, m, d] = outboundDate.value.split('-').map(Number)
const outTime = new Date(y, m - 1, d, now.getHours(), now.getMinutes(), now.getSeconds()).getTime()
let totalCount = 0
const items = itemList.value.map(item => {

@ -16,7 +16,7 @@
<view class="card-info-right">
<view class="info-item"><text class="info-label">备件名称</text><text class="info-name">{{ textValue(sparepart.name) }}</text></view>
<view class="info-item"><text class="info-label">规格</text><text class="info-value">{{ textValue(sparepart.standard || sparepart.deviceSpec) }}</text></view>
<view class="info-item"><text class="info-label">当前库存</text><text class="info-value stock-highlight">{{ sparepart.count != null ? sparepart.count : 0 }}{{ textUnit(sparepart.unitName || sparepart.minStockUnitName || '个') }}</text></view>
<view class="info-item"><text class="info-label">当前库存</text><text class="info-value stock-highlight">{{ sparepart.count != null ? sparepart.count : 0 }}{{ textUnit(sparepart.unitName || sparepart.minStockUnitName || '个') }}</text></view>
</view>
</view>
<view class="card-bottom">
@ -94,43 +94,6 @@
</view>
</view>
<!-- 出库数量 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view><text class="section-title">出库数量</text>
</view>
<view class="qty-input-card">
<view class="form-field">
<text class="form-label">出库数量</text>
<input v-model="outboundQty" class="form-input" placeholder="请输入" confirm-type="done" />
<text class="form-suffix-text">单位{{ textValue(sparepart.purchaseUnitName) }}</text>
</view>
<view class="convert-row">
<text class="convert-label-inline">出库后剩余库存</text>
<text :class="['convert-value-inline', { 'text-danger': stockExceeded }]">{{ remainingStock }}</text>
<text class="convert-unit-inline">{{ stockUnitLabel(sparepart) }}</text>
</view>
<view class="convert-helper">{{ currentStockText }}</view>
<view class="convert-formula-inline">{{ outboundQty || 0 }}{{ textValue(sparepart.purchaseUnitName) }} × {{ textValue(sparepart.purchaseUnitConvertQuantity) }}{{ stockUnitLabel(sparepart) }} = {{ calculatedStock }}{{ stockUnitLabel(sparepart) }}</view>
<view v-if="stockExceeded" class="stock-warning">
<text class="warning-icon"></text>
<view class="warning-text">
<text class="warning-line">当前库存不足不能出库</text>
<text class="warning-line">当前库存{{ stockCount }}{{ stockUnitLabel(sparepart) }}{{ stockPackText }}</text>
<text class="warning-line">本次出库{{ calculatedStock }}{{ stockUnitLabel(sparepart) }}{{ outboundPackText }}</text>
</view>
</view>
</view>
<!-- 供应商 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view><text class="section-title">供应商</text>
</view>
<view class="select-row-card">
<view class="full-dropdown">
<text :class="{ placeholder: !defaultSupplierName }">{{ defaultSupplierName || '未配置默认供应商' }}</text>
</view>
</view>
<!-- 仓库/库区 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view><text class="section-title">仓库/库区</text>
@ -173,6 +136,44 @@
</view>
</view>
</view>
<!-- 出库数量 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view><text class="section-title">出库数量</text>
</view>
<view class="qty-input-card">
<view class="form-field">
<text class="form-label">出库数量</text>
<input v-model="outboundQty" class="form-input" placeholder="请输入" confirm-type="done" />
<text class="form-suffix-text">单位{{ textValue(sparepart.purchaseUnitName) }}</text>
</view>
<view v-if="!hasWarehouseArea" class="warehouse-hint"></view>
<view v-if="hasWarehouseArea" class="convert-row">
<text class="convert-label-inline">出库后当前仓库/库区的剩余库存</text>
<text :class="['convert-value-inline', { 'text-danger': stockExceeded }]">{{ remainingStock }}</text>
<text class="convert-unit-inline">{{ stockUnitLabel(sparepart) }}</text>
</view>
<view v-if="hasWarehouseArea" class="convert-helper">{{ currentStockText }}</view>
<view class="convert-formula-inline">{{ outboundQty || 0 }}{{ textValue(sparepart.purchaseUnitName) }} × {{ textValue(sparepart.purchaseUnitConvertQuantity) }}{{ stockUnitLabel(sparepart) }} = {{ calculatedStock }}{{ stockUnitLabel(sparepart) }}</view>
<view v-if="stockExceeded" class="stock-warning">
<text class="warning-icon"></text>
<view class="warning-text">
<text class="warning-line">当前仓库/库区的库存不足不能出库</text>
<text class="warning-line">当前仓库/库区的库存{{ stockCount }}{{ stockUnitLabel(sparepart) }}{{ stockPackText }}</text>
<text class="warning-line">本次出库{{ calculatedStock }}{{ stockUnitLabel(sparepart) }}{{ outboundPackText }}</text>
</view>
</view>
</view>
<!-- 供应商 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view><text class="section-title">供应商</text>
</view>
<view class="select-row-card">
<view class="full-dropdown">
<text :class="{ placeholder: !defaultSupplierName }">{{ defaultSupplierName || '未配置默认供应商' }}</text>
</view>
</view>
</view>
<view class="bottom-actions">
@ -190,12 +191,14 @@ import { getDeviceLedgerList } from '@/api/mes/moldoperate'
import { getWarehouseSimpleList, getWarehouseAreaSimpleList } from '@/api/mes/moldget'
import { getEquipmentRepairListByDeviceId, getMaintenanceTicketPage } from '@/api/mes/equipment'
import { getDvRepairPage } from '@/api/mes/dvrepair'
import { getSparepartDetail, getSparepartStockCount } from '@/api/mes/sparepart'
import { getSparepartDetail, getSparepartStock, getSparepartStockCount } from '@/api/mes/sparepart'
const sparepart = ref({})
const outboundQty = ref(null)
const selectedPurpose = ref('repair')
const hasWarehouseArea = computed(() => selectedWarehouse.value && selectedArea.value)
function setPurpose(value) {
selectedPurpose.value = value
selectedDevice.value = null
@ -237,17 +240,24 @@ const sparepartImage = computed(() => {
if (Array.isArray(images)) return String(images[0] || '')
return String(images).split(',')[0]?.trim() || ''
})
const stockCount = computed(() => sparepart.value.count ?? 0)
// / 0
const warehouseAreaStockCount = ref(0)
const stockCount = computed(() => warehouseAreaStockCount.value)
const calculatedStock = computed(() => {
const qty = Number(outboundQty.value) || 0
const ratio = Number(sparepart.value.purchaseUnitConvertQuantity) || 1
return qty * ratio
const result = qty * ratio
return Number.isFinite(result) ? result : 0
})
const remainingStock = computed(() => {
return Math.max(stockCount.value - calculatedStock.value, 0)
const sc = Number.isFinite(stockCount.value) ? stockCount.value : 0
const cs = Number.isFinite(calculatedStock.value) ? calculatedStock.value : 0
return Math.max(sc - cs, 0)
})
const stockExceeded = computed(() => {
return outboundQty.value > 0 && calculatedStock.value > stockCount.value
const sc = Number.isFinite(stockCount.value) ? stockCount.value : 0
const cs = Number.isFinite(calculatedStock.value) ? calculatedStock.value : 0
return outboundQty.value > 0 && cs > sc
})
function formatStockPack(count) {
@ -263,7 +273,7 @@ function formatStockPack(count) {
const currentStockText = computed(() => {
const text = formatStockPack(stockCount.value)
return text ? `当前库存:${text}` : ''
return text ? `当前仓库/库区的库存:${text}` : ''
})
const stockPackText = computed(() => {
return formatStockPack(stockCount.value) || `${stockCount.value}${stockUnitLabel(sparepart)}`
@ -298,7 +308,7 @@ function handleConfirm() {
if (stockExceeded.value) {
uni.showModal({
title: '库存不足',
content: `当前库存:${stockCount.value}${stockUnitLabel(sparepart)}${stockPackText.value}\n本次出库${calculatedStock.value}${stockUnitLabel(sparepart)}${outboundPackText.value}`,
content: `当前仓库/库区的库存:${stockCount.value}${stockUnitLabel(sparepart)}${stockPackText.value}\n本次出库${calculatedStock.value}${stockUnitLabel(sparepart)}${outboundPackText.value}`,
showCancel: false,
confirmText: '知道了'
})
@ -449,19 +459,17 @@ async function loadRepairOrdersById(deviceId) {
}
function toggleWarehouseDropdown() { showWarehouseDropdown.value = !showWarehouseDropdown.value }
function handleSelectWarehouse(item) { selectedWarehouse.value = item; showWarehouseDropdown.value = false; selectedArea.value = null; areaOptions.value = []; loadAreas(item.value) }
function handleSelectWarehouse(item) { selectedWarehouse.value = item; showWarehouseDropdown.value = false; selectedArea.value = null; areaOptions.value = []; warehouseAreaStockCount.value = 0; loadAreas(item.value) }
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 || '') }))
const defaultWh = warehouseOptions.value.find(w => (w.label || '').includes('备件仓'))
if (defaultWh) { selectedWarehouse.value = defaultWh; loadAreas(defaultWh.value) }
} catch (e) {}
}
function toggleAreaDropdown() { if (!selectedWarehouse.value) { uni.showToast({ title: '请先选择仓库', icon: 'none' }); return }; showAreaDropdown.value = !showAreaDropdown.value }
function handleSelectArea(item) { selectedArea.value = item; showAreaDropdown.value = false }
function handleSelectArea(item) { selectedArea.value = item; showAreaDropdown.value = false; loadStockCount() }
async function loadAreas(warehouseId) {
if (!warehouseId) return
loadingAreas.value = true
@ -470,8 +478,6 @@ async function loadAreas(warehouseId) {
const res = await getWarehouseAreaSimpleList(warehouseId)
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
areaOptions.value = data.map(a => ({ value: a.id, label: a.name || a.areaName || String(a.id || '') }))
const defaultArea = areaOptions.value.find(a => (a.label || '').includes('备件库'))
if (defaultArea) selectedArea.value = defaultArea
} catch (e) {} finally { loadingAreas.value = false }
}
@ -485,7 +491,7 @@ onShow(async () => {
try {
const res = await getSparepartDetail(selectResult.id)
const detail = res?.data || res
if (detail) { sparepart.value = { ...sparepart.value, ...detail }; loadStockCount() }
if (detail) { sparepart.value = { ...sparepart.value, ...detail }; loadTotalStockCount() }
} catch (e) { console.error('获取备件详情失败:', e) }
}
}
@ -493,11 +499,32 @@ onShow(async () => {
loadWarehouses()
})
// get-count
async function loadTotalStockCount() {
const id = sparepart.value.id
if (!id) return
try {
const res = await getSparepartStockCount(id)
sparepart.value.count = (res?.data ?? res) != null ? Number(res?.data ?? res) : 0
} catch (e) { sparepart.value.count = 0 }
}
//
async function loadStockCount() {
const id = sparepart.value.id
if (!id) return
try { const res = await getSparepartStockCount(id); sparepart.value.count = (res?.data ?? res) != null ? Number(res?.data ?? res) : 0 }
catch (e) { sparepart.value.count = 0 }
const whId = selectedWarehouse.value?.value
const arId = selectedArea.value?.value
if (!whId || !arId) {
warehouseAreaStockCount.value = 0
return
}
try {
const res = await getSparepartStock(id, whId, arId)
const detail = res?.data || res
const count = (detail != null && typeof detail === 'object') ? Number(detail.count || 0) : Number(detail || 0)
warehouseAreaStockCount.value = Number.isFinite(count) ? count : 0
} catch (e) { warehouseAreaStockCount.value = 0 }
}
onHide(() => { showDeviceDropdown.value = false; showOrderDropdown.value = false })
@ -519,7 +546,8 @@ onHide(() => { showDeviceDropdown.value = false; showOrderDropdown.value = false
.info-item { display: flex; align-items: center; margin-bottom: 8rpx; }
.info-label { font-size: 26rpx; color: #6b7280; flex-shrink: 0; }
.info-name { font-size: 30rpx; font-weight: 700; color: #0f172a; }
.info-value { font-size: 26rpx; color: #374151; &.stock-highlight { color: #2563eb; font-weight: 500; } }
.info-value { font-size: 26rpx; color: #374151; }
.stock-highlight { font-size: 30rpx; font-weight: 700; color: #1f2937; }
.card-bottom { margin-top: 20rpx; padding-top: 20rpx; border-top: 1rpx solid #f0f0f0; }
.detail-row { display: flex; align-items: center; margin-bottom: 14rpx; &:last-child { margin-bottom: 0; } &.two-col { justify-content: space-between; } }
.detail-col { display: flex; align-items: center; flex: 1; }
@ -541,6 +569,7 @@ onHide(() => { showDeviceDropdown.value = false; showOrderDropdown.value = false
.convert-label-inline { font-size: 26rpx; color: #6b7280; }
.convert-value-inline { font-size: 36rpx; font-weight: 700; color: #1f2937; margin-left: auto; }
.convert-unit-inline { font-size: 24rpx; color: #64748b; margin-left: 8rpx; }
.warehouse-hint { margin-top: 16rpx; padding: 16rpx 20rpx; background: #fefce8; border: 1rpx solid #fef08a; border-radius: 10rpx; font-size: 24rpx; color: #a16207; }
.convert-formula-inline { margin-top: 10rpx; font-size: 22rpx; color: #9ca3af; }
.convert-helper { margin-top: 8rpx; font-size: 24rpx; color: #9ca3af; }
.text-danger { color: #dc2626 !important; }

Loading…
Cancel
Save