袁伟杰:V2.0.003 采购入库扫码

This commit is contained in:
yuan
2026-05-14 16:48:09 +08:00
parent 7f84bbca79
commit 7859af83b2
24 changed files with 3479 additions and 62 deletions

View File

@@ -0,0 +1,85 @@
<template>
<view class="order-context-bar">
<view class="order-context-bar__main">
<view class="order-context-bar__order">
<text class="order-context-bar__label">单号</text>
<text class="order-context-bar__value">{{ orderNo || '-' }}</text>
</view>
<view class="order-context-bar__progress">
已扫 {{ scannedCount }} / {{ targetCount }}
</view>
</view>
<view class="order-context-bar__meta">
<text>供应商{{ supplierName || '-' }}</text>
<text>仓库{{ warehouseName || '-' }}</text>
<text class="order-context-bar__exception">异常{{ exceptionCount }}</text>
</view>
</view>
</template>
<script setup lang="ts">
defineProps<{
orderNo: string
supplierName?: string
warehouseName?: string
scannedCount: number
targetCount: number
exceptionCount: number
}>()
</script>
<style lang="scss" scoped>
.order-context-bar {
padding: 20rpx 24rpx;
border-bottom: 2rpx solid #e5e6eb;
background: rgba(255, 255, 255, 0.96);
&__main,
&__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
&__order {
display: flex;
align-items: center;
gap: 12rpx;
min-width: 0;
flex: 1;
}
&__label {
flex-shrink: 0;
color: #86909c;
font-size: 24rpx;
}
&__value {
min-width: 0;
color: #1f2329;
font-size: 30rpx;
font-weight: 600;
}
&__progress {
flex-shrink: 0;
color: #1677ff;
font-size: 24rpx;
font-weight: 600;
}
&__meta {
margin-top: 12rpx;
color: #4e5969;
font-size: 22rpx;
flex-wrap: wrap;
}
&__exception {
color: #d4380d;
}
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<view class="recent-scan-list">
<view v-if="items.length === 0" class="recent-scan-list__empty">
暂无最近扫码记录
</view>
<view
v-for="(item, index) in items"
:key="buildItemKey(item)"
class="scan-card"
:class="{ 'scan-card--latest': latestIndex === index }"
>
<view class="scan-card__header">
<text class="scan-card__name">{{ item.productName || '-' }}</text>
<text class="scan-card__undo" @tap="emit('undo', index, item)">撤销</text>
</view>
<view class="scan-card__meta">
<text>条码{{ item.productBarCode || '-' }}</text>
<view class="scan-card__quantity">
<text class="scan-card__step" @tap="emit('decrease', index, item)">-</text>
<input
class="scan-card__count-input"
type="number"
:value="formatCount(item.count)"
@blur="handleCountBlur($event, index, item)"
@confirm="handleCountBlur($event, index, item)"
>
<text class="scan-card__step" @tap="emit('increase', index, item)">+</text>
</view>
</view>
<view class="scan-card__meta">
<text>批次{{ item.batchNo || '-' }}</text>
<text>剩余{{ formatCount(item.remainCount) }}</text>
</view>
<view class="scan-card__meta">
<text>库位{{ item.locationCode || '-' }}</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import type { PurchaseScanItem } from '@/utils/purchase-scan'
defineProps<{
items: PurchaseScanItem[]
latestIndex?: number
}>()
const emit = defineEmits<{
(event: 'undo', index: number, item: PurchaseScanItem): void
(event: 'increase', index: number, item: PurchaseScanItem): void
(event: 'decrease', index: number, item: PurchaseScanItem): void
(event: 'change-count', index: number, nextCount: number, item: PurchaseScanItem): void
}>()
/**
* 功能说明:生成最近扫码卡片的稳定 key复用采购扫码的聚合维度。
* 适用场景:最近扫码列表渲染时为每条记录提供唯一标识。
* @param item 当前扫码记录
* @return 组合后的字符串 key
* 注意事项:采购扫码项已按订单行、批次和库位聚合,这里直接使用相同维度生成稳定 key避免列表重排时节点误复用。
*/
function buildItemKey(item: PurchaseScanItem) {
return [
item.orderItemId || item.productId || 'unknown',
item.batchNo || 'no-batch',
item.locationCode || 'no-location',
].join('-')
}
/**
* 功能说明:格式化数量展示,避免空值直接透出到工作台。
* 适用场景:最近扫码卡片展示扫码数量和剩余数量。
* @param value 原始数量
* @return 格式化后的数量文案
* 注意事项:采购扫码数量可能来自接口或本地累加,这里统一转数值后再按整数/小数展示。
*/
function formatCount(value?: number) {
if (value === undefined || value === null) {
return '-'
}
const normalizedValue = Number(value)
return normalizedValue.toFixed(Number.isInteger(normalizedValue) ? 0 : 4)
}
/**
* 功能说明:处理扫码卡片中的手动数量录入。
* 适用场景:操作员直接输入目标数量后离焦或回车确认。
* @param event 输入框事件
* @param index 当前记录索引
* @param item 当前扫码记录
* @return 无
* 注意事项:这里只负责把输入值抛给页面层,实际数量校验与上限判断统一由页面业务逻辑处理。
*/
function handleCountBlur(event: Record<string, any>, index: number, item: PurchaseScanItem) {
const rawValue = event?.detail?.value
emit('change-count', index, Number(rawValue), item)
}
</script>
<style lang="scss" scoped>
.recent-scan-list {
&__empty {
padding: 28rpx;
border-radius: 16rpx;
background: #fff;
color: #86909c;
text-align: center;
}
}
.scan-card {
margin-bottom: 16rpx;
padding: 24rpx;
border: 2rpx solid transparent;
border-radius: 16rpx;
background: #fff;
&--latest {
border-color: #1677ff;
box-shadow: 0 8rpx 20rpx rgba(22, 119, 255, 0.12);
}
&__header,
&__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
&__header {
margin-bottom: 12rpx;
}
&__name {
color: #1f2329;
font-size: 28rpx;
font-weight: 600;
}
&__undo {
flex-shrink: 0;
color: #f53f3f;
font-size: 24rpx;
}
&__meta {
margin-top: 8rpx;
color: #4e5969;
font-size: 24rpx;
flex-wrap: wrap;
}
&__quantity {
display: flex;
align-items: center;
gap: 12rpx;
}
&__step {
width: 44rpx;
height: 44rpx;
line-height: 44rpx;
border-radius: 8rpx;
background: #f2f3f5;
color: #1f2329;
text-align: center;
font-size: 30rpx;
}
&__count-input {
width: 104rpx;
height: 44rpx;
line-height: 44rpx;
border: 2rpx solid #d9dce3;
border-radius: 8rpx;
background: #fff;
color: #1f2329;
text-align: center;
box-sizing: border-box;
}
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<view v-if="message" class="feedback-bar" :class="`feedback-bar--${tone}`">
{{ message }}
</view>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
tone?: 'success' | 'error' | 'idle'
message?: string
}>(),
{
tone: 'idle',
message: '',
},
)
</script>
<style lang="scss" scoped>
.feedback-bar {
min-height: 76rpx;
padding: 18rpx 24rpx;
border-radius: 14rpx;
font-size: 24rpx;
line-height: 40rpx;
&--idle {
background: #f2f3f5;
color: #4e5969;
}
&--success {
background: #f6ffed;
color: #389e0d;
}
&--error {
background: #fff2f0;
color: #cf1322;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
<!-- 基本信息 -->
<wd-cell-group title="基本信息" border>
<wd-cell title="采购单编号" :value="formData.no" />
<wd-cell title="扫码识别值" :value="formData.no || '-'" />
<wd-cell title="采购状态">
<view
class="rounded-8rpx px-16rpx py-4rpx text-24rpx"

View File

@@ -31,6 +31,8 @@
<view class="scan-section__title">
扫码录入
</view>
<input v-model="batchNo" class="scan-field" placeholder="请输入或扫描批次号(可选)">
<input v-model="locationCode" class="scan-field" placeholder="请输入或扫描库位码(可选)">
<ScanInput
v-model="scanCode"
placeholder="请先选择仓库,再扫描产品条码"
@@ -55,7 +57,6 @@
<script setup lang="ts">
import type { ProductByBarCode } from '@/api/erp/product'
import type { StockIn, StockInItem } from '@/api/erp/stock-in'
import type { SupplierSimple } from '@/api/erp/supplier'
import type { Warehouse } from '@/api/erp/warehouse'
import type { ItemCardValue } from '@/components/erp-scan/item-list.vue'
@@ -82,21 +83,33 @@ interface StockInScanItem extends ItemCardValue {
remark?: string
}
interface OptionalSupplierOption {
id?: number
name: string
}
const toast = useToast()
const scanCode = ref('')
const scanLoading = ref(false)
const submitting = ref(false)
const remark = ref('')
const warehouses = ref<Warehouse[]>([])
const suppliers = ref<SupplierSimple[]>([])
const suppliers = ref<OptionalSupplierOption[]>([])
const selectedWarehouseId = ref<number>()
const selectedSupplierId = ref<number>()
const items = ref<StockInScanItem[]>([])
const batchNo = ref('')
const locationCode = ref('')
const EMPTY_SUPPLIER_OPTION: OptionalSupplierOption = {
id: undefined,
name: '不选择供应商(可选)',
}
const currentWarehouse = computed(() =>
warehouses.value.find(item => item.id === selectedWarehouseId.value),
)
const currentSupplierName = computed(() => suppliers.value.find(item => item.id === selectedSupplierId.value)?.name || '请选择供应商(可选)')
const currentSupplierName = computed(() => suppliers.value.find(item => item.id === selectedSupplierId.value)?.name || EMPTY_SUPPLIER_OPTION.name)
const selectedWarehouseIndex = computed(() => Math.max(warehouses.value.findIndex(item => item.id === selectedWarehouseId.value), 0))
const selectedSupplierIndex = computed(() => {
const index = suppliers.value.findIndex(item => item.id === selectedSupplierId.value)
@@ -126,7 +139,7 @@ async function loadInitialData() {
])
warehouses.value = warehouseList
suppliers.value = supplierList
suppliers.value = [EMPTY_SUPPLIER_OPTION, ...supplierList]
selectedWarehouseId.value = warehouseList.find(item => item.defaultStatus)?.id || warehouseList[0]?.id
}
@@ -163,12 +176,22 @@ function handleSupplierChange(event: Record<string, any>) {
*/
function appendScannedProduct(product: ProductByBarCode) {
const warehouseId = selectedWarehouseId.value as number
const targetKey = buildScanMergeKey({ warehouseId, productId: product.id, count: 1 })
const currentBatchNo = batchNo.value.trim()
const currentLocationCode = locationCode.value.trim()
const targetKey = buildScanMergeKey({
warehouseId,
productId: product.id,
count: 1,
batchNo: currentBatchNo,
locationCode: currentLocationCode,
}, true)
const existedIndex = items.value.findIndex(item => buildScanMergeKey({
warehouseId: item.warehouseId,
productId: item.productId,
count: item.count,
}) === targetKey)
batchNo: item.batchNo,
locationCode: item.locationCode,
}, true) === targetKey)
if (existedIndex >= 0) {
items.value[existedIndex].count += 1
@@ -184,6 +207,8 @@ function appendScannedProduct(product: ProductByBarCode) {
productSpec: product.standard,
productUnitName: product.unitName,
productPrice: Number(product.minPrice || 0),
batchNo: currentBatchNo,
locationCode: currentLocationCode,
count: 1,
})
}
@@ -315,8 +340,10 @@ function clearItems() {
function buildSubmitItems(): StockInItem[] {
return items.value.map(item => ({
warehouseId: item.warehouseId,
locationCode: item.locationCode,
productId: item.productId,
productPrice: item.productPrice,
batchNo: item.batchNo,
count: item.count,
remark: item.remark,
}))
@@ -341,16 +368,20 @@ async function handleSubmit() {
submitting.value = true
try {
const payload: StockIn = {
supplierId: selectedSupplierId.value,
inTime: new Date().toISOString(),
remark: remark.value,
items: buildSubmitItems(),
}
if (selectedSupplierId.value !== undefined) {
payload.supplierId = selectedSupplierId.value
}
await createStockIn(payload)
toast.success('入库成功')
clearItems()
remark.value = ''
batchNo.value = ''
locationCode.value = ''
} finally {
submitting.value = false
}

View File

@@ -12,6 +12,7 @@
<!-- 基本信息 -->
<wd-cell-group title="基本信息" border>
<wd-cell title="出库单号" :value="detail.no || '-'" />
<wd-cell title="扫码识别值" :value="detail.no || '-'" />
<wd-cell title="出库时间" :value="formatDate(detail.outTime)" />
<wd-cell title="客户" :value="detail.customerName || '-'" />
<wd-cell title="审核状态">
@@ -78,6 +79,14 @@
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<wd-button
v-if="hasAccessByCodes(['erp:stock-out:query']) && detail.status !== 20"
type="primary"
plain
@click="handleTaskScan"
>
去扫码
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:stock-out:update']) && detail.status !== 20"
type="primary"
@@ -135,19 +144,25 @@ const detail = ref<StockOut>({})
/** 格式化数量 */
function formatCount(count?: number) {
if (count === undefined || count === null) return '-'
if (count === undefined || count === null) {
return '-'
}
return count.toFixed(2)
}
/** 格式化金额 */
function formatPrice(price?: number) {
if (price === undefined || price === null) return '-'
if (price === undefined || price === null) {
return '-'
}
return `¥${price.toFixed(2)}`
}
/** 格式化日期 */
function formatDate(dateStr?: string) {
if (!dateStr) return '-'
if (!dateStr) {
return '-'
}
return dateStr.substring(0, 10)
}
@@ -158,7 +173,9 @@ function handleBack() {
/** 加载详情 */
async function getDetail() {
if (!props.id) return
if (!props.id) {
return
}
try {
toast.loading('加载中...')
detail.value = await getStockOut(props.id)
@@ -172,13 +189,20 @@ function handleEdit() {
uni.navigateTo({ url: `/pages-erp/stock-out/form/index?id=${props.id}` })
}
/** 去扫码执行 */
function handleTaskScan() {
uni.navigateTo({ url: `/pages-erp/stock-out/task-scan/index?id=${props.id}` })
}
/** 审批 */
function handleApprove() {
uni.showModal({
title: '提示',
content: '确定要审批该出库单吗?',
success: async (res) => {
if (!res.confirm) return
if (!res.confirm) {
return
}
try {
await updateStockOutStatus(props.id, 20)
toast.success('审批成功')
@@ -196,7 +220,9 @@ function handleReverseApprove() {
title: '提示',
content: '确定要反审批该出库单吗?',
success: async (res) => {
if (!res.confirm) return
if (!res.confirm) {
return
}
try {
await updateStockOutStatus(props.id, 10)
toast.success('反审批成功')
@@ -214,7 +240,9 @@ function handleDelete() {
title: '提示',
content: '确定要删除该出库单吗?',
success: async (res) => {
if (!res.confirm) return
if (!res.confirm) {
return
}
try {
await deleteStockOut([props.id])
toast.success('删除成功')

View File

@@ -13,18 +13,18 @@
</view>
<!-- 快捷筛选标签 -->
<view class="flex items-center overflow-hidden rounded-12rpx bg-white mx-24rpx mb-16rpx shadow-sm">
<view class="mx-24rpx mb-16rpx flex items-center overflow-hidden rounded-12rpx bg-white shadow-sm">
<view
v-for="tab in statusTabs"
:key="tab.value"
class="flex-1 py-20rpx text-center text-26rpx relative"
class="relative flex-1 py-20rpx text-center text-26rpx"
:class="queryParams.status === tab.value ? 'text-[#1890ff] font-semibold' : 'text-[#666]'"
@click="onTabChange(tab.value)"
>
{{ tab.label }}
<view
v-if="queryParams.status === tab.value"
class="absolute bottom-0 left-1/4 w-1/2 h-4rpx rounded-2rpx bg-[#1890ff]"
class="absolute bottom-0 left-1/4 h-4rpx w-1/2 rounded-2rpx bg-[#1890ff]"
/>
</view>
</view>
@@ -58,7 +58,7 @@
<!-- 产品 -->
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
<text class="text-[#999]">产品</text>
<text class="ml-16rpx line-clamp-1 text-right" style="max-width: 400rpx;">{{ item.productNames || '-' }}</text>
<text class="line-clamp-1 ml-16rpx text-right" style="max-width: 400rpx;">{{ item.productNames || '-' }}</text>
</view>
<!-- 出库时间 -->
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
@@ -71,19 +71,33 @@
<text>{{ item.creatorName || '-' }}</text>
</view>
<!-- 数量金额区域 -->
<view class="flex items-center justify-around mt-12rpx pt-16rpx border-t border-[#f0f0f0]">
<view class="mt-12rpx flex items-center justify-around border-t border-[#f0f0f0] pt-16rpx">
<view class="text-center">
<view class="text-32rpx text-[#333] font-semibold">{{ formatCount(item.totalCount) }}</view>
<view class="text-22rpx text-[#999] mt-4rpx">数量</view>
<view class="text-32rpx text-[#333] font-semibold">
{{ formatCount(item.totalCount) }}
</view>
<view class="mt-4rpx text-22rpx text-[#999]">
数量
</view>
</view>
<view class="text-center">
<view class="text-32rpx text-[#f5222d] font-semibold">{{ formatPrice(item.totalPrice) }}</view>
<view class="text-22rpx text-[#999] mt-4rpx">金额</view>
<view class="text-32rpx text-[#f5222d] font-semibold">
{{ formatPrice(item.totalPrice) }}
</view>
<view class="mt-4rpx text-22rpx text-[#999]">
金额
</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="flex flex-wrap gap-12rpx px-24rpx pb-20rpx" @click.stop>
<wd-button
v-if="hasAccessByCodes(['erp:stock-out:query']) && item.status !== 20"
size="small" type="primary" plain @click="handleTaskScan(item)"
>
去扫码
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:stock-out:update']) && item.status !== 20"
size="small" type="primary" plain @click="handleEdit(item)"
@@ -135,11 +149,15 @@
<wd-popup v-model="searchVisible" position="top" @close="searchVisible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">出库单号</view>
<view class="yd-search-form-label">
出库单号
</view>
<wd-input v-model="searchForm.no" placeholder="请输入出库单号" clearable />
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">审核状态</view>
<view class="yd-search-form-label">
审核状态
</view>
<view class="yd-search-form-radio-group">
<view
v-for="opt in statusOptions"
@@ -153,8 +171,12 @@
</view>
</view>
<view class="yd-search-form-actions">
<wd-button class="flex-1" plain @click="handleReset">重置</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">搜索</wd-button>
<wd-button class="flex-1" plain @click="handleReset">
重置
</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">
搜索
</wd-button>
</view>
</view>
</wd-popup>
@@ -213,29 +235,34 @@ const statusOptions = [
/** 搜索条件 placeholder */
const searchPlaceholder = computed(() => {
const conditions: string[] = []
if (searchForm.no) conditions.push(`单号:${searchForm.no}`)
if (searchForm.no)
conditions.push(`单号:${searchForm.no}`)
if (searchForm.status !== undefined) {
const statusLabel = statusOptions.find(o => o.value === searchForm.status)?.label
if (statusLabel && statusLabel !== '全部') conditions.push(`状态:${statusLabel}`)
if (statusLabel && statusLabel !== '全部')
conditions.push(`状态:${statusLabel}`)
}
return conditions.length > 0 ? conditions.join(' | ') : '搜索出库单'
})
/** 格式化数量 */
function formatCount(count?: number) {
if (count === undefined || count === null) return '-'
if (count === undefined || count === null)
return '-'
return count.toFixed(2)
}
/** 格式化金额 */
function formatPrice(price?: number) {
if (price === undefined || price === null) return '-'
if (price === undefined || price === null)
return '-'
return `¥${price.toFixed(2)}`
}
/** 格式化日期 */
function formatDate(dateStr?: string) {
if (!dateStr) return '-'
if (!dateStr)
return '-'
return dateStr.substring(0, 10)
}
@@ -257,7 +284,8 @@ async function getList() {
loadMoreState.value = 'loading'
try {
const params = { ...queryParams.value }
if (searchForm.no) params.no = searchForm.no
if (searchForm.no)
params.no = searchForm.no
const data = await getStockOutPage(params)
list.value = [...list.value, ...data.list]
total.value = data.total
@@ -292,7 +320,8 @@ function handleReset() {
/** 加载更多 */
function loadMore() {
if (loadMoreState.value === 'finished') return
if (loadMoreState.value === 'finished')
return
queryParams.value.pageNo++
getList()
}
@@ -307,6 +336,11 @@ function handleEdit(item: StockOut) {
uni.navigateTo({ url: `/pages-erp/stock-out/form/index?id=${item.id}` })
}
/** 去扫码执行 */
function handleTaskScan(item: StockOut) {
uni.navigateTo({ url: `/pages-erp/stock-out/task-scan/index?id=${item.id}` })
}
/** 详情 */
function handleDetail(item: StockOut) {
uni.navigateTo({ url: `/pages-erp/stock-out/detail/index?id=${item.id}` })
@@ -318,7 +352,8 @@ function handleApprove(id: number) {
title: '提示',
content: '确定要审批该出库单吗?',
success: async (res) => {
if (!res.confirm) return
if (!res.confirm)
return
try {
await updateStockOutStatus(id, 20)
toast.success('审批成功')
@@ -336,7 +371,8 @@ function handleReverseApprove(id: number) {
title: '提示',
content: '确定要反审批该出库单吗?',
success: async (res) => {
if (!res.confirm) return
if (!res.confirm)
return
try {
await updateStockOutStatus(id, 10)
toast.success('反审批成功')
@@ -354,7 +390,8 @@ function handleDelete(id: number) {
title: '提示',
content: '确定要删除该出库单吗?',
success: async (res) => {
if (!res.confirm) return
if (!res.confirm)
return
try {
await deleteStockOut([id])
toast.success('删除成功')

View File

@@ -31,6 +31,8 @@
<view class="scan-section__title">
扫码录入
</view>
<input v-model="batchNo" class="scan-field" placeholder="请输入或扫描批次号(可选)">
<input v-model="locationCode" class="scan-field" placeholder="请输入或扫描库位码(可选)">
<ScanInput
v-model="scanCode"
placeholder="请先选择仓库,再扫描产品条码"
@@ -53,7 +55,6 @@
</template>
<script setup lang="ts">
import type { CustomerSimple } from '@/api/erp/customer'
import type { ProductByBarCode } from '@/api/erp/product'
import type { Stock } from '@/api/erp/stock'
import type { StockOut, StockOutItem } from '@/api/erp/stock-out'
@@ -84,21 +85,33 @@ interface StockOutScanItem extends ItemCardValue {
remark?: string
}
interface OptionalCustomerOption {
id?: number
name: string
}
const toast = useToast()
const scanCode = ref('')
const scanLoading = ref(false)
const submitting = ref(false)
const remark = ref('')
const warehouses = ref<Warehouse[]>([])
const customers = ref<CustomerSimple[]>([])
const customers = ref<OptionalCustomerOption[]>([])
const selectedWarehouseId = ref<number>()
const selectedCustomerId = ref<number>()
const items = ref<StockOutScanItem[]>([])
const batchNo = ref('')
const locationCode = ref('')
const EMPTY_CUSTOMER_OPTION: OptionalCustomerOption = {
id: undefined,
name: '不选择客户(可选)',
}
const currentWarehouse = computed(() =>
warehouses.value.find(item => item.id === selectedWarehouseId.value),
)
const currentCustomerName = computed(() => customers.value.find(item => item.id === selectedCustomerId.value)?.name || '请选择客户(可选)')
const currentCustomerName = computed(() => customers.value.find(item => item.id === selectedCustomerId.value)?.name || EMPTY_CUSTOMER_OPTION.name)
const selectedWarehouseIndex = computed(() => Math.max(warehouses.value.findIndex(item => item.id === selectedWarehouseId.value), 0))
const selectedCustomerIndex = computed(() => {
const index = customers.value.findIndex(item => item.id === selectedCustomerId.value)
@@ -128,7 +141,7 @@ async function loadInitialData() {
])
warehouses.value = warehouseList
customers.value = customerList
customers.value = [EMPTY_CUSTOMER_OPTION, ...customerList]
selectedWarehouseId.value = warehouseList.find(item => item.defaultStatus)?.id || warehouseList[0]?.id
}
@@ -194,12 +207,22 @@ function validateStock(stockCount: number, nextCount: number, productName: strin
*/
async function appendScannedProduct(product: ProductByBarCode) {
const warehouseId = selectedWarehouseId.value as number
const targetKey = buildScanMergeKey({ warehouseId, productId: product.id, count: 1 })
const currentBatchNo = batchNo.value.trim()
const currentLocationCode = locationCode.value.trim()
const targetKey = buildScanMergeKey({
warehouseId,
productId: product.id,
count: 1,
batchNo: currentBatchNo,
locationCode: currentLocationCode,
}, true)
const existedIndex = items.value.findIndex(item => buildScanMergeKey({
warehouseId: item.warehouseId,
productId: item.productId,
count: item.count,
}) === targetKey)
batchNo: item.batchNo,
locationCode: item.locationCode,
}, true) === targetKey)
const stockInfo = await loadStockInfo(product.id)
const stockCount = Number(stockInfo?.count || 0)
@@ -211,6 +234,7 @@ async function appendScannedProduct(product: ProductByBarCode) {
items.value[existedIndex].count = nextCount
items.value[existedIndex].stockCount = stockCount
items.value[existedIndex].remainCount = Math.max(stockCount - nextCount, 0)
return
}
@@ -228,6 +252,9 @@ async function appendScannedProduct(product: ProductByBarCode) {
productUnitName: product.unitName,
productPrice: Number(stockInfo?.unitPrice ?? product.minPrice ?? 0),
stockCount,
remainCount: Math.max(stockCount - 1, 0),
batchNo: currentBatchNo,
locationCode: currentLocationCode,
count: 1,
})
}
@@ -319,6 +346,7 @@ async function increaseCount(index: number) {
}
item.stockCount = Number(stockInfo?.count || 0)
item.remainCount = Math.max(Number(stockInfo?.count || 0) - nextCount, 0)
item.count = nextCount
}
@@ -367,8 +395,10 @@ function clearItems() {
function buildSubmitItems(): StockOutItem[] {
return items.value.map(item => ({
warehouseId: item.warehouseId,
locationCode: item.locationCode,
productId: item.productId,
productPrice: item.productPrice,
batchNo: item.batchNo,
count: item.count,
remark: item.remark,
}))
@@ -393,16 +423,20 @@ async function handleSubmit() {
submitting.value = true
try {
const payload: StockOut = {
customerId: selectedCustomerId.value,
outTime: new Date().toISOString(),
remark: remark.value,
items: buildSubmitItems(),
}
if (selectedCustomerId.value !== undefined) {
payload.customerId = selectedCustomerId.value
}
await createStockOut(payload)
toast.success('出库成功')
clearItems()
remark.value = ''
batchNo.value = ''
locationCode.value = ''
} finally {
submitting.value = false
}