fix: 李红攀:V2.0.061采购入库、销售出库添加扫码功能

This commit is contained in:
2026-05-25 10:38:03 +08:00
parent a1217e36b5
commit 2e48dcda74
4 changed files with 1227 additions and 131 deletions

View File

@@ -22,29 +22,23 @@
<view class="scan-workbench">
<!-- 当前扫码上下文提示 -->
<view class="scan-panel">
<!-- 步骤指示器库位优先 -->
<!-- 步骤指示器库位 产品 -->
<view class="scan-step-indicator">
<view class="scan-step-indicator__item" :class="{ 'active': step === 'idle', 'done': step !== 'idle' }">
<view class="scan-step-indicator__dot">1</view>
<text>扫库位</text>
</view>
<view class="scan-step-indicator__line" :class="{ 'done': step !== 'idle' }" />
<view class="scan-step-indicator__item" :class="{ 'active': step === 'location_scanned', 'done': step === 'product_scanned' }">
<view class="scan-step-indicator__item" :class="{ 'active': step === 'scanned' }">
<view class="scan-step-indicator__dot">2</view>
<text>扫产品</text>
</view>
<view class="scan-step-indicator__line" :class="{ 'done': step === 'product_scanned' }" />
<view class="scan-step-indicator__item" :class="{ 'active': step === 'product_scanned' }">
<view class="scan-step-indicator__dot">3</view>
<text>确认</text>
</view>
</view>
<!-- 当前作业提示 -->
<view class="scan-current-hint">
<text v-if="step === 'idle'">请扫描库位条码</text>
<text v-else-if="step === 'location_scanned'">请扫描产品条</text>
<text v-else-if="step === 'product_scanned'">扫码完成请确认入库</text>
<text v-else>产品已入库请继续扫</text>
</view>
<!-- 库位扫码区 -->
@@ -85,11 +79,11 @@
</view>
<!-- 产品扫码区 -->
<view class="scan-row" :class="{ 'scan-row--dim': step === 'idle' || step === 'product_scanned' }">
<view class="scan-row" :class="{ 'scan-row--dim': step === 'idle' }">
<input
v-model="productBarCode"
class="scan-field"
:class="{ 'scan-field--active': step === 'location_scanned' }"
:class="{ 'scan-field--active': step === 'scanned' }"
placeholder="扫描产品条码"
confirm-type="done"
:focus="productInputFocus"
@@ -122,26 +116,6 @@
</view>
</view>
<!-- 批次号扫产品后显示 -->
<view class="scan-row" v-if="step === 'product_scanned'">
<input
v-model="batchNo"
class="scan-field"
placeholder="批次号(可选)"
confirm-type="done"
>
</view>
<!-- 确认入库按钮 -->
<button
v-if="step === 'product_scanned'"
class="scan-confirm-btn"
:disabled="!canConfirm || scanLoading"
@tap="handleConfirm"
>
{{ scanLoading ? '处理中...' : '确认入库' }}
</button>
<ScanFeedbackBar :tone="feedbackTone" :message="feedbackMessage" />
</view>
@@ -218,7 +192,7 @@ export interface ScanRecord {
operatorName?: string
}
type Step = 'idle' | 'location_scanned' | 'product_scanned'
type Step = 'idle' | 'scanned'
/** 页面参数id=入库单ID, no=入库单号 */
const props = defineProps<{
@@ -327,11 +301,11 @@ function handleLocationBlur() {
}
}
/** 产品输入框失焦处理 - 如果当前步骤是 location_scanned自动重新聚焦 */
/** 产品输入框失焦处理 - 如果当前步骤是 scanned自动重新聚焦 */
function handleProductBlur() {
if (step.value === 'location_scanned' && !scanLoading.value) {
if (step.value === 'scanned' && !scanLoading.value) {
setTimeout(() => {
if (step.value === 'location_scanned') {
if (step.value === 'scanned') {
focusProductInput()
}
}, 100)
@@ -376,7 +350,7 @@ async function handleLocationScan() {
}
locationCode.value = code
step.value = 'location_scanned'
step.value = 'scanned'
productBarCode.value = ''
productPreview.value = null
batchNo.value = ''
@@ -431,8 +405,10 @@ async function handleProductScan() {
}
productBarCode.value = code
step.value = 'product_scanned'
step.value = 'scanned'
clearFeedback()
// 自动确认入库
nextTick(() => handleConfirm())
} finally {
scanLoading.value = false
}
@@ -878,23 +854,6 @@ function clearFeedback() {
}
}
// 确认按钮
.scan-confirm-btn {
height: 96rpx;
line-height: 96rpx;
border-radius: 16rpx;
background: #1677ff;
color: #fff;
font-size: 32rpx;
font-weight: 700;
margin-top: 8rpx;
&[disabled] {
background: #d9d9d9;
color: #fff;
}
}
// 底部栏
.scan-footer {
position: sticky;

View File

@@ -1,57 +1,554 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="销售出库详情"
:title="detail?.status === 20 ? '出库详情' : '编辑出库单'"
left-arrow
placeholder
safe-area-inset-top
fixed
@click-left="handleBack"
/>
<view class="p-24rpx">
<wd-cell-group title="出库单信息">
<wd-cell title="单号" :value="detail?.no || '-'" />
<wd-cell title="客户" :value="detail?.customerName || '-'" />
<wd-cell title="订单号" :value="detail?.orderNo || '-'" />
<wd-cell title="总数量" :value="formatCount(detail?.totalCount)" />
<wd-cell title="总金额" :value="formatPrice(detail?.totalPrice)" />
<wd-cell title="状态">
<wd-tag :type="detail?.status === 20 ? 'success' : 'warning'">
<!-- 主内容区 -->
<view class="page-content">
<!-- 顶部状态卡 -->
<view class="status-card">
<view class="status-card__left">
<view class="status-card__no">{{ detail?.no || '-' }}</view>
<view class="status-card__meta">
<text>{{ formatDate(detail?.outTime) }}</text>
<text>·</text>
<text>{{ detail?.customerName || '-' }}</text>
</view>
</view>
<view class="status-card__right">
<view
class="status-card__badge"
:class="detail?.status === 20 ? 'badge--success' : 'badge--warning'"
>
{{ detail?.status === 20 ? '已审核' : '未审核' }}
</wd-tag>
</wd-cell>
</wd-cell-group>
</view>
</view>
</view>
<!-- 基本信息 -->
<view class="section">
<view class="section__title">基本信息</view>
<view class="info-grid">
<view class="info-grid__item">
<text class="info-grid__label">客户</text>
<text class="info-grid__value">{{ detail?.customerName || '-' }}</text>
</view>
<view class="info-grid__item">
<text class="info-grid__label">销售员</text>
<text class="info-grid__value">{{ detail?.saleUserName || '-' }}</text>
</view>
<view class="info-grid__item" v-if="detail?.orderNo">
<text class="info-grid__label">关联订单</text>
<text class="info-grid__value text-[#1890ff]">{{ detail?.orderNo }}</text>
</view>
<view class="info-grid__item" v-if="detail?.deliveryNoticeNo">
<text class="info-grid__label">发货通知单</text>
<text class="info-grid__value text-[#1890ff]">{{ detail?.deliveryNoticeNo }}</text>
</view>
<view class="info-grid__item" v-if="detail?.accountName">
<text class="info-grid__label">结算账户</text>
<text class="info-grid__value">{{ detail?.accountName }}</text>
</view>
<view class="info-grid__item" v-if="detail?.remark">
<text class="info-grid__label">备注</text>
<text class="info-grid__value">{{ detail?.remark }}</text>
</view>
</view>
</view>
<!-- 产品明细 -->
<view class="section">
<view class="section__title">出库明细 {{ items.length }} </view>
<view class="item-list">
<view v-for="(item, index) in items" :key="index" class="item-card">
<view class="item-card__header">
<text class="item-card__name">{{ item.productName || '-' }}</text>
<text class="item-card__count">x{{ formatCount(item.count) }}</text>
</view>
<view class="item-card__body">
<view class="item-card__row">
<text class="item-card__label">规格</text>
<text class="item-card__value">{{ item.productSpec || '-' }}</text>
</view>
<view class="item-card__row" v-if="item.warehouseName">
<text class="item-card__label">仓库</text>
<text class="item-card__value">{{ item.warehouseName }}</text>
</view>
<view class="item-card__row" v-if="item.locationCode">
<text class="item-card__label">库位</text>
<text class="item-card__value text-[#52c41a]">{{ item.locationCode }}</text>
</view>
<view class="item-card__row" v-if="item.batchNo">
<text class="item-card__label">批次号</text>
<text class="item-card__value">{{ item.batchNo }}</text>
</view>
<view class="item-card__row">
<text class="item-card__label">单价</text>
<text class="item-card__value">¥{{ formatPrice(item.productPrice) }}</text>
</view>
<view class="item-card__row">
<text class="item-card__label">金额</text>
<text class="item-card__value text-[#f5222d] font-semibold">¥{{ formatPrice(item.totalPrice) }}</text>
</view>
</view>
</view>
<view v-if="items.length === 0" class="item-empty">暂无产品明细</view>
</view>
</view>
<!-- 金额汇总 -->
<view class="section">
<view class="section__title">金额汇总</view>
<view class="price-summary">
<view class="price-summary__row">
<text>总数量</text>
<text class="font-semibold">{{ formatCount(detail?.totalCount) }}</text>
</view>
<view class="price-summary__row">
<text>产品金额</text>
<text>¥{{ formatPrice(detail?.totalProductPrice) }}</text>
</view>
<view class="price-summary__row" v-if="detail?.discountPercent">
<text>优惠率</text>
<text>{{ detail?.discountPercent }}%</text>
</view>
<view class="price-summary__row" v-if="detail?.discountPrice">
<text>收款优惠</text>
<text class="text-[#52c41a]">-¥{{ formatPrice(detail?.discountPrice) }}</text>
</view>
<view class="price-summary__row" v-if="detail?.otherPrice">
<text>其它费用</text>
<text class="text-[#fa8c16]">+¥{{ formatPrice(detail?.otherPrice) }}</text>
</view>
<view class="price-summary__divider" />
<view class="price-summary__row price-summary__row--highlight">
<text>应收金额</text>
<text class="text-[#f5222d] font-bold text-36rpx">¥{{ formatPrice(detail?.totalPrice) }}</text>
</view>
<view class="price-summary__row">
<text>已收金额</text>
<text :class="receiptStatus === 2 ? 'text-[#52c41a]' : 'text-[#f5222d]'">
¥{{ formatPrice(detail?.receiptPrice) }}
</text>
</view>
<view class="price-summary__row" v-if="receiptStatus !== 2">
<text>未收金额</text>
<text class="text-[#f5222d] font-semibold">
¥{{ formatPrice((detail?.totalPrice || 0) - (detail?.receiptPrice || 0)) }}
</text>
</view>
<view class="price-summary__status">
<wd-tag :type="receiptStatus === 2 ? 'success' : receiptStatus === 1 ? 'warning' : 'danger'" plain>
{{ receiptStatusText }}
</wd-tag>
</view>
</view>
</view>
<!-- 操作日志 -->
<view class="section" v-if="detail?.createTime || detail?.creatorName">
<view class="section__title">操作信息</view>
<view class="info-grid">
<view class="info-grid__item" v-if="detail?.creatorName">
<text class="info-grid__label">创建人</text>
<text class="info-grid__value">{{ detail?.creatorName }}</text>
</view>
<view class="info-grid__item" v-if="detail?.createTime">
<text class="info-grid__label">创建时间</text>
<text class="info-grid__value">{{ formatDate(detail?.createTime) }}</text>
</view>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<wd-button
v-if="detail?.status !== 20"
type="primary"
size="large"
:loading="actionLoading"
@click="handleEdit"
>
编辑
</wd-button>
<wd-button
v-if="detail?.status === 10"
type="success"
size="large"
:loading="actionLoading"
@click="handleApprove"
>
审批
</wd-button>
<wd-button
v-if="detail?.status === 20"
type="warning"
size="large"
plain
:loading="actionLoading"
@click="handleReverseApprove"
>
反审批
</wd-button>
<wd-button
v-if="detail?.status !== 20"
type="error"
size="large"
plain
:loading="actionLoading"
@click="handleDelete"
>
删除
</wd-button>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { getSaleOut } from '@/api/erp/sale-out'
import { useToast } from 'wot-design-uni'
import { getSaleOut, updateSaleOutStatus, deleteSaleOut } from '@/api/erp/sale-out'
import { getNavbarHeight, navigateBackPlus } from '@/utils'
import { formatDate as formatDateValue } from '@/utils/date'
const props = defineProps<{ id?: number }>()
const toast = useToast()
const detail = ref<any>(null)
const actionLoading = ref(false)
const items = computed(() => detail.value?.items || [])
const receiptStatus = computed(() => {
const total = detail.value?.totalPrice || 0
const received = detail.value?.receiptPrice || 0
if (total === 0) return 0
if (received >= total) return 2
if (received > 0) return 1
return 0
})
const receiptStatusText = computed(() => {
const status = receiptStatus.value
if (status === 2) return '已结清'
if (status === 1) return '部分收款'
return '未收款'
})
function formatCount(count?: number) {
return count !== undefined && count !== null ? count.toFixed(2) : '-'
return count !== undefined && count !== null ? count.toFixed(2) : '0.00'
}
function formatPrice(price?: number) {
return price !== undefined && price !== null ? `¥${price.toFixed(2)}` : '-'
return price !== undefined && price !== null ? price.toFixed(2) : '0.00'
}
function formatDate(dateStr?: string | number | Date) {
return formatDateValue(dateStr) || '-'
}
function handleBack() {
navigateBackPlus()
}
onMounted(async () => {
function handleEdit() {
uni.navigateTo({ url: `/pages-erp/sale-out/form/index?id=${props.id}` })
}
async function handleApprove() {
uni.showModal({
title: '提示',
content: '确定要审批该出库单吗?',
success: async (res) => {
if (!res.confirm) return
actionLoading.value = true
try {
await updateSaleOutStatus(props.id!, 20)
toast.success('审批成功')
await refreshDetail()
} catch {
toast.error('审批失败')
} finally {
actionLoading.value = false
}
},
})
}
async function handleReverseApprove() {
uni.showModal({
title: '提示',
content: '确定要反审批该出库单吗?',
success: async (res) => {
if (!res.confirm) return
actionLoading.value = true
try {
await updateSaleOutStatus(props.id!, 10)
toast.success('反审批成功')
await refreshDetail()
} catch {
toast.error('反审批失败')
} finally {
actionLoading.value = false
}
},
})
}
async function handleDelete() {
uni.showModal({
title: '提示',
content: '确定要删除该出库单吗?',
success: async (res) => {
if (!res.confirm) return
actionLoading.value = true
try {
await deleteSaleOut([props.id!])
toast.success('删除成功')
navigateBackPlus()
} catch {
toast.error('删除失败')
actionLoading.value = false
}
},
})
}
async function refreshDetail() {
if (props.id) {
detail.value = await getSaleOut(props.id)
}
}
onMounted(async () => {
await refreshDetail()
})
</script>
<style lang="scss" scoped>
.page-content {
padding-top: calc(env(safe-area-inset-top) + 88rpx);
padding-bottom: 180rpx;
}
.status-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 32rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
&__left {
flex: 1;
min-width: 0;
}
&__no {
font-size: 36rpx;
font-weight: 700;
margin-bottom: 12rpx;
}
&__meta {
font-size: 24rpx;
opacity: 0.85;
display: flex;
align-items: center;
gap: 8rpx;
}
&__badge {
padding: 8rpx 20rpx;
border-radius: 30rpx;
font-size: 24rpx;
font-weight: 600;
flex-shrink: 0;
}
.badge--success {
background: rgba(255, 255, 255, 0.25);
color: #fff;
}
.badge--warning {
background: rgba(255, 255, 255, 0.25);
color: #fff;
}
}
.section {
margin: 24rpx;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
&__title {
padding: 24rpx 32rpx;
font-size: 28rpx;
font-weight: 600;
color: #1f2329;
border-bottom: 1rpx solid #f0f0f0;
background: #fafafa;
}
}
.info-grid {
padding: 8rpx 0;
&__item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 20rpx 32rpx;
border-bottom: 1rpx solid #f9f9f9;
&:last-child {
border-bottom: none;
}
}
&__label {
font-size: 26rpx;
color: #86909c;
flex-shrink: 0;
width: 160rpx;
}
&__value {
font-size: 26rpx;
color: #1f2329;
text-align: right;
flex: 1;
word-break: break-all;
}
}
.item-list {
padding: 0 24rpx;
margin-top: 16rpx;
}
.item-card {
border: 2rpx solid #f0f0f0;
border-radius: 12rpx;
margin-bottom: 16rpx;
overflow: hidden;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background: #f6f8fa;
border-bottom: 1rpx solid #f0f0f0;
}
&__name {
font-size: 28rpx;
font-weight: 600;
color: #1f2329;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__count {
font-size: 28rpx;
font-weight: 700;
color: #f5222d;
flex-shrink: 0;
margin-left: 16rpx;
}
&__body {
padding: 16rpx 24rpx;
}
&__row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8rpx 0;
}
&__label {
font-size: 24rpx;
color: #86909c;
flex-shrink: 0;
}
&__value {
font-size: 24rpx;
color: #1f2329;
text-align: right;
}
}
.item-empty {
text-align: center;
padding: 48rpx;
color: #86909c;
font-size: 26rpx;
}
.price-summary {
padding: 24rpx 32rpx;
&__row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10rpx 0;
font-size: 26rpx;
color: #4e5969;
&--highlight {
font-size: 28rpx;
color: #1f2329;
margin-top: 8rpx;
}
}
&__divider {
height: 1rpx;
background: #f0f0f0;
margin: 16rpx 0;
}
&__status {
display: flex;
justify-content: flex-end;
margin-top: 12rpx;
}
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
display: flex;
gap: 16rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
:deep(.wd-button) {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
}
}
</style>

View File

@@ -1,25 +1,706 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="新建销售出库"
:title="getTitle"
left-arrow
placeholder
safe-area-inset-top
fixed
@click-left="handleBack"
/>
<view class="p-24rpx">
<wd-notice-bar text="新建销售出库功能开发中,请使用其他方式创建" />
<!-- 表单区域 -->
<view>
<wd-form ref="formRef" :model="formData" :rules="formRules">
<wd-cell-group title="基本信息" border>
<wd-cell title="出库单号" title-width="180rpx" center>
<wd-input
v-model="formData.no"
placeholder="不填则自动生成"
:disabled="!!props.id"
/>
</wd-cell>
<wd-cell title="出库时间" title-width="180rpx" prop="outTime" center>
<wd-datetime-picker
v-model="formData.outTime"
type="date"
label=""
placeholder="请选择出库时间"
/>
</wd-cell>
<wd-cell title="关联订单" title-width="180rpx" is-link center @click="openOrderSelect">
<text v-if="formData.orderNo" class="text-[#1890ff]">{{ formData.orderNo }}</text>
<text v-else class="text-[#999]">点击选择销售订单</text>
</wd-cell>
<wd-cell title="发货通知单" title-width="180rpx" center>
<text>{{ formData.deliveryNoticeNo || '-' }}</text>
</wd-cell>
<wd-cell title="客户" title-width="180rpx" center>
<text>{{ getCustomerName() || '-' }}</text>
</wd-cell>
<wd-cell title="销售员" title-width="180rpx" prop="saleUserId" center>
<wd-picker
v-model="formData.saleUserId"
:columns="saleUserColumns"
label=""
placeholder="请选择销售员"
@confirm="onSaleUserConfirm"
/>
</wd-cell>
<wd-textarea
v-model="formData.remark"
label="备注"
label-width="180rpx"
placeholder="请输入备注"
:maxlength="200"
show-word-limit
clearable
/>
</wd-cell-group>
<wd-cell-group title="价格信息" border>
<wd-input
v-model="formData.discountPercent"
label="优惠率(%)"
label-width="180rpx"
type="number"
placeholder="请输入优惠率"
clearable
/>
<wd-cell title="收款优惠" title-width="180rpx" center>
<text>¥{{ formData.discountPrice || 0 }}</text>
</wd-cell>
<wd-input
v-model="formData.otherPrice"
label="其它费用"
label-width="180rpx"
type="number"
placeholder="请输入其它费用"
clearable
/>
<wd-cell title="优惠后金额" title-width="180rpx" center>
<text>¥{{ (formData.totalPrice || 0) - (formData.otherPrice || 0) }}</text>
</wd-cell>
<wd-cell title="结算账户" title-width="180rpx" center>
<wd-picker
v-model="formData.accountId"
:columns="accountColumns"
label=""
placeholder="请选择结算账户"
@confirm="onAccountConfirm"
/>
</wd-cell>
<wd-cell title="应收金额" title-width="180rpx" center>
<text class="text-[#e6a23c] font-semibold">¥{{ formData.totalPrice || 0 }}</text>
</wd-cell>
</wd-cell-group>
</wd-form>
</view>
<!-- 出库明细 -->
<view class="mx-24rpx mt-24rpx pb-180rpx">
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
出库明细{{ formData.items?.length || 0 }}
</view>
</view>
<view
v-for="(item, index) in formData.items"
:key="index"
class="mb-16rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
>
<view class="flex items-center justify-between bg-[#f8f8f8] px-24rpx py-12rpx">
<text class="text-28rpx text-[#333] font-semibold">
产品 #{{ index + 1 }} {{ item.productName || '' }}
</text>
<view class="flex gap-12rpx">
<wd-button
v-if="formData.items && formData.items.length > 1"
size="small" type="error" plain @click="handleRemoveItem(index)"
>
删除
</wd-button>
</view>
</view>
<view class="p-24rpx">
<!-- 仓库选择 -->
<view class="mb-16rpx">
<text class="mb-8rpx block text-24rpx text-[#999]">仓库</text>
<wd-picker
v-model="item.warehouseId"
:columns="warehouseColumns"
placeholder="请选择仓库"
@confirm="(e: any) => onItemWarehouseConfirm(e, index)"
/>
</view>
<!-- 库位编码 -->
<view v-if="isLocationEnabled(item.warehouseId)" class="mb-16rpx">
<text class="mb-8rpx block text-24rpx text-[#999]">库位编码</text>
<wd-input
v-model="item.locationCode"
placeholder="库房号-区域-货位号,如 1-A-01"
clearable
/>
</view>
<!-- 批次号 -->
<view class="mb-16rpx">
<text class="mb-8rpx block text-24rpx text-[#999]">批次号</text>
<wd-input
v-model="item.batchNo"
placeholder="请输入批次号"
clearable
/>
</view>
<!-- 自动填充的产品信息只读 -->
<view v-if="item.productId" class="mb-16rpx rounded-8rpx bg-[#f9f9f9] p-16rpx">
<view class="mb-8rpx flex justify-between text-24rpx">
<text class="text-[#999]">产品名称</text>
<text class="text-[#333]">{{ item.productName || '-' }}</text>
</view>
<view v-if="item.productSpec" class="mb-8rpx flex justify-between text-24rpx">
<text class="text-[#999]">规格</text>
<text class="text-[#333]">{{ item.productSpec }}</text>
</view>
<view class="mb-8rpx flex justify-between text-24rpx">
<text class="text-[#999]">单位</text>
<text class="text-[#333]">{{ item.productUnitName || '-' }}</text>
</view>
<view class="mb-8rpx flex justify-between text-24rpx">
<text class="text-[#999]">条码</text>
<text class="text-[#333]">{{ item.productBarCode || '-' }}</text>
</view>
</view>
<view class="mb-16rpx flex gap-16rpx">
<view class="flex-1">
<text class="mb-8rpx block text-24rpx text-[#999]">数量</text>
<wd-input
v-model="item.count"
type="number"
placeholder="数量"
clearable
/>
</view>
<view class="flex-1">
<text class="mb-8rpx block text-24rpx text-[#999]">单价</text>
<wd-input
v-model="item.productPrice"
type="number"
placeholder="单价"
clearable
/>
</view>
</view>
<view class="mb-16rpx flex gap-16rpx">
<view class="flex-1">
<text class="mb-8rpx block text-24rpx text-[#999]">税率(%)</text>
<wd-input
v-model="item.taxPercent"
type="number"
placeholder="税率"
clearable
/>
</view>
<view class="flex-1">
<text class="mb-8rpx block text-24rpx text-[#999]">金额</text>
<text class="block pt-16rpx text-28rpx text-[#333]">{{ calcTotalPrice(item) }}</text>
</view>
</view>
<view>
<text class="mb-8rpx block text-24rpx text-[#999]">备注</text>
<wd-input
v-model="item.remark"
placeholder="请输入备注"
clearable
/>
</view>
</view>
</view>
<view v-if="!formData.items?.length" class="py-60rpx text-center text-28rpx text-[#999]">
暂无产品请先选择关联订单
</view>
</view>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button
type="primary"
block
:loading="formLoading"
@click="handleSubmit"
>
保存
</wd-button>
</view>
<!-- 销售订单选择弹窗 -->
<wd-popup v-model="orderSelectVisible" position="right" custom-style="width: 100%; height: 100%;">
<view class="yd-page-container">
<wd-navbar
title="选择销售订单"
left-arrow
placeholder
safe-area-inset-top
fixed
@click-left="orderSelectVisible = false"
/>
<wd-search
v-model="orderQueryParams.no"
placeholder="搜索订单单号"
@search="getOrderList"
@clear="getOrderList"
/>
<view class="px-24rpx">
<view
v-for="item in orderList"
:key="item.id"
class="mb-20rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
:class="{ 'border-2 border-[#1890ff]': selectedOrderId === item.id }"
@click="handleSelectOrder(item)"
>
<view class="p-24rpx">
<view class="mb-12rpx flex items-center justify-between">
<view class="text-30rpx text-[#333] font-semibold">
{{ item.no }}
</view>
<wd-icon v-if="selectedOrderId === item.id" name="check" color="#1890ff" size="40rpx" />
</view>
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
<text class="text-[#999]">客户</text>
<text>{{ item.customerName || '-' }}</text>
</view>
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
<text class="text-[#999]">产品</text>
<text class="line-clamp-1 ml-16rpx text-right" style="max-width: 400rpx;">{{ item.productNames || '-' }}</text>
</view>
<view class="mt-12rpx flex items-center justify-around border-t border-[#f0f0f0] pt-12rpx">
<view class="text-center">
<view class="text-28rpx text-[#333] font-semibold">
{{ item.totalCount || 0 }}
</view>
<view class="text-22rpx text-[#999]">
总数量
</view>
</view>
<view class="text-center">
<view class="text-28rpx text-[#1890ff] font-semibold">
{{ item.outCount || 0 }}
</view>
<view class="text-22rpx text-[#999]">
已出库
</view>
</view>
<view class="text-center">
<view class="text-28rpx text-[#e6a23c] font-semibold">
¥{{ item.totalPrice || 0 }}
</view>
<view class="text-22rpx text-[#999]">
金额
</view>
</view>
</view>
</view>
</view>
<view v-if="orderList.length === 0" class="py-100rpx text-center">
<wd-status-tip image="content" tip="暂无可出库订单" />
</view>
<wd-loadmore
v-if="orderList.length > 0"
:state="orderLoadMoreState"
@reload="loadMoreOrders"
/>
</view>
<view class="yd-detail-footer">
<wd-button type="primary" block :disabled="!selectedOrderId" @click="confirmSelectOrder">
确认选择
</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
import type { SaleOut, SaleOutItem } from '@/api/erp/sale-out'
import type { LoadMoreState } from '@/http/types'
import type { User } from '@/api/system/user'
import { computed, onMounted, ref, watch } from 'vue'
import { useToast } from 'wot-design-uni'
import { createSaleOut, getSaleOut, updateSaleOut } from '@/api/erp/sale-out'
import { getCustomerSimpleList } from '@/api/erp/customer'
import { getSimpleUserList } from '@/api/system/user'
import { navigateBackPlus } from '@/utils'
function handleBack() {
navigateBackPlus()
/** 客户简单信息 */
interface CustomerSimple {
id: number
name: string
}
/** 账户简单信息 */
interface AccountSimple {
id: number
name: string
defaultStatus?: boolean
}
/** 仓库简单信息 */
interface WarehouseSimple {
id: number
name: string
defaultStatus?: boolean
enableLocation?: boolean
}
/** 销售订单 */
interface SaleOrder {
id?: number
no?: string
customerId?: number
customerName?: string
saleUserId?: number
accountId?: number
discountPercent?: number
remark?: string
totalCount?: number
outCount?: number
totalPrice?: number
productNames?: string
items?: any[]
}
const props = defineProps<{
id?: number | any
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑销售出库' : '新增销售出库')
const formLoading = ref(false)
const formData = ref<SaleOut>({
id: undefined,
no: undefined,
customerId: undefined,
accountId: undefined,
saleUserId: undefined,
outTime: new Date().getTime(),
remark: '',
discountPercent: 0,
discountPrice: 0,
otherPrice: 0,
totalPrice: 0,
orderId: undefined,
orderNo: undefined,
deliveryNoticeId: undefined,
deliveryNoticeNo: undefined,
items: [],
})
const formRules = {
outTime: [{ required: true, message: '出库时间不能为空' }],
}
const formRef = ref<FormInstance>()
const customerList = ref<CustomerSimple[]>([])
const accountList = ref<AccountSimple[]>([])
const warehouseList = ref<WarehouseSimple[]>([])
const userList = ref<User[]>([])
const defaultWarehouse = ref<WarehouseSimple>()
// 销售订单选择相关
const orderSelectVisible = ref(false)
const orderList = ref<SaleOrder[]>([])
const orderLoadMoreState = ref<LoadMoreState>('loading')
const orderQueryParams = ref({
pageNo: 1,
pageSize: 10,
no: undefined as string | undefined,
})
const orderTotal = ref(0)
const selectedOrderId = ref<number>()
/** 账户下拉列 */
const accountColumns = computed(() => [
accountList.value.map(a => ({ value: a.id, label: a.name })),
])
/** 仓库下拉列 */
const warehouseColumns = computed(() => [
warehouseList.value.map(w => ({ value: w.id, label: w.name })),
])
/** 销售员下拉列 */
const saleUserColumns = computed(() => [
userList.value.map(u => ({ value: u.id, label: u.nickname })),
])
/** 判断仓库是否启用库位管理 */
function isLocationEnabled(warehouseId?: number): boolean {
if (!warehouseId) return false
const warehouse = warehouseList.value.find(w => w.id === warehouseId)
return warehouse?.enableLocation === true
}
/** 获取客户名称 */
function getCustomerName() {
if (!formData.value.customerId) return ''
const customer = customerList.value.find(c => c.id === formData.value.customerId)
return customer?.name || formData.value.customerName || ''
}
/** 账户选择回调 */
function onAccountConfirm({ value }: any) {
formData.value.accountId = value?.[0]
}
/** 销售员选择回调 */
function onSaleUserConfirm({ value }: any) {
formData.value.saleUserId = value?.[0]
}
/** 行项仓库选择回调 */
function onItemWarehouseConfirm(e: any, index: number) {
if (formData.value.items && formData.value.items[index]) {
const warehouseId = e?.value?.[0] ?? e?.selectedItems?.[0]?.value ?? formData.value.items[index].warehouseId
formData.value.items[index].warehouseId = warehouseId
const warehouse = warehouseList.value.find(w => w.id === warehouseId)
if (warehouse) {
formData.value.items[index].warehouseName = warehouse.name
if (!warehouse.enableLocation) {
formData.value.items[index].locationCode = undefined
}
}
}
}
/** 计算行项金额 */
function calcTotalPrice(item: SaleOutItem) {
if (item.productPrice && item.count) {
const productTotal = Number(item.productPrice) * Number(item.count)
const taxPrice = item.taxPercent ? productTotal * Number(item.taxPercent) / 100 : 0
return `¥${(productTotal + taxPrice).toFixed(2)}`
}
return '-'
}
/** 监听表单数据变化,计算总价 */
watch(
() => formData.value,
(val) => {
if (!val || !val.items) return
val.items.forEach((item) => {
if (item.productPrice && item.count) {
item.totalProductPrice = Number(item.productPrice) * Number(item.count)
item.taxPrice = item.taxPercent ? item.totalProductPrice * Number(item.taxPercent) / 100 : 0
item.totalPrice = item.totalProductPrice + (item.taxPrice || 0)
}
})
const totalPrice = val.items.reduce((prev, curr) => prev + (curr.totalPrice || 0), 0)
const discountPrice = val.discountPercent ? totalPrice * Number(val.discountPercent) / 100 : 0
formData.value.discountPrice = Math.round(discountPrice * 100) / 100
formData.value.totalPrice = Math.round((totalPrice - discountPrice + Number(val.otherPrice || 0)) * 100) / 100
},
{ deep: true },
)
/** 加载下拉列表数据 */
async function loadDropdownData() {
try {
const { http } = await import('@/http/http')
const [customers, accounts, warehouses, users] = await Promise.all([
getCustomerSimpleList(),
http.get<AccountSimple[]>('/erp/account/simple-list'),
http.get<WarehouseSimple[]>('/erp/warehouse/simple-list'),
getSimpleUserList(),
])
customerList.value = customers || []
accountList.value = accounts || []
warehouseList.value = warehouses || []
userList.value = users || []
// 设置默认账户
const defaultAccount = accountList.value.find(a => a.defaultStatus)
if (defaultAccount && !formData.value.accountId) {
formData.value.accountId = defaultAccount.id
}
// 设置默认仓库
defaultWarehouse.value = warehouseList.value.find(w => w.defaultStatus)
} catch {
// error handled by http
}
}
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-erp/sale-out/index')
}
/** 删除出库明细行 */
function handleRemoveItem(index: number) {
uni.showModal({
title: '提示',
content: `确定要删除第 ${index + 1} 项产品吗?`,
success: (res) => {
if (res.confirm) {
formData.value.items?.splice(index, 1)
}
},
})
}
/** 打开订单选择弹窗 */
function openOrderSelect() {
orderSelectVisible.value = true
orderQueryParams.value.pageNo = 1
orderList.value = []
selectedOrderId.value = formData.value.orderId
getOrderList()
}
/** 获取可出库订单列表 */
async function getOrderList() {
orderLoadMoreState.value = 'loading'
try {
const { http } = await import('@/http/http')
const data = await http.get<{ list: SaleOrder[]; total: number }>('/erp/sale-order/out-enable-page', {
pageNo: orderQueryParams.value.pageNo,
pageSize: orderQueryParams.value.pageSize,
no: orderQueryParams.value.no,
})
if (orderQueryParams.value.pageNo === 1) {
orderList.value = data.list || []
} else {
orderList.value = [...orderList.value, ...(data.list || [])]
}
orderTotal.value = data.total || 0
orderLoadMoreState.value = orderList.value.length >= orderTotal.value ? 'finished' : 'loading'
} catch {
orderLoadMoreState.value = 'error'
}
}
/** 加载更多订单 */
function loadMoreOrders() {
if (orderLoadMoreState.value === 'finished') return
orderQueryParams.value.pageNo++
getOrderList()
}
/** 选择订单 */
function handleSelectOrder(item: SaleOrder) {
selectedOrderId.value = item.id
}
/** 确认选择订单 */
async function confirmSelectOrder() {
if (!selectedOrderId.value) return
try {
toast.loading('加载订单详情...')
const { http } = await import('@/http/http')
const orderDetail = await http.get<SaleOrder>(`/erp/sale-order/get?id=${selectedOrderId.value}`)
// 设置订单信息
formData.value.orderId = orderDetail.id
formData.value.orderNo = orderDetail.no
formData.value.deliveryNoticeId = undefined
formData.value.deliveryNoticeNo = undefined
formData.value.customerId = orderDetail.customerId
formData.value.accountId = orderDetail.accountId
formData.value.saleUserId = orderDetail.saleUserId
formData.value.discountPercent = orderDetail.discountPercent
formData.value.remark = orderDetail.remark
// 设置出库明细
if (orderDetail.items) {
formData.value.items = orderDetail.items
.filter((item: any) => (item.count || 0) - (item.outCount || 0) > 0)
.map((item: any) => ({
productId: item.productId,
productName: item.productName,
productUnitId: item.productUnitId,
productUnitName: item.productUnitName,
productPrice: item.productPrice ?? item.salePrice ?? 0,
productSpec: item.productSpec,
productBarCode: item.productBarCode,
count: (item.count || 0) - (item.outCount || 0),
totalCount: item.count,
outCount: item.outCount,
taxPercent: item.taxPercent,
warehouseId: defaultWarehouse.value?.id,
warehouseName: defaultWarehouse.value?.name,
orderItemId: item.id,
remark: item.remark,
}))
}
orderSelectVisible.value = false
} finally {
toast.close()
}
}
/** 加载详情 */
async function getDetail() {
if (!props.id) return
try {
toast.loading('加载中...')
formData.value = await getSaleOut(props.id)
formData.value.outTime = typeof formData.value.outTime === 'string'
? new Date(formData.value.outTime).getTime()
: formData.value.outTime
} finally {
toast.close()
}
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value!.validate()
if (!valid) return
// 校验出库明细
if (!formData.value.items || formData.value.items.length === 0) {
toast.warning('请至少添加一项出库产品')
return
}
for (let i = 0; i < formData.value.items.length; i++) {
const item = formData.value.items[i]
if (!item.count || Number(item.count) <= 0) {
toast.warning(`${i + 1} 项产品数量不能为空`)
return
}
}
formLoading.value = true
try {
const submitData = {
...formData.value,
items: formData.value.items?.map(item => ({
...item,
productPrice: Number(item.productPrice ?? 0),
count: Number(item.count ?? 0),
})),
}
if (props.id) {
await updateSaleOut(submitData)
toast.success('修改成功')
} else {
await createSaleOut(submitData)
toast.success('新增成功')
}
setTimeout(() => {
handleBack()
}, 500)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(async () => {
await loadDropdownData()
getDetail()
})
</script>
<style lang="scss" scoped>

View File

@@ -22,29 +22,23 @@
<view class="scan-workbench">
<!-- 当前扫码上下文提示 -->
<view class="scan-panel">
<!-- 步骤指示器库位优先 -->
<!-- 步骤指示器库位 产品 -->
<view class="scan-step-indicator">
<view class="scan-step-indicator__item" :class="{ 'active': step === 'idle', 'done': step !== 'idle' }">
<view class="scan-step-indicator__dot">1</view>
<text>扫库位</text>
</view>
<view class="scan-step-indicator__line" :class="{ 'done': step !== 'idle' }" />
<view class="scan-step-indicator__item" :class="{ 'active': step === 'location_scanned', 'done': step === 'product_scanned' }">
<view class="scan-step-indicator__item" :class="{ 'active': step === 'scanned' }">
<view class="scan-step-indicator__dot">2</view>
<text>扫产品</text>
</view>
<view class="scan-step-indicator__line" :class="{ 'done': step === 'product_scanned' }" />
<view class="scan-step-indicator__item" :class="{ 'active': step === 'product_scanned' }">
<view class="scan-step-indicator__dot">3</view>
<text>确认</text>
</view>
</view>
<!-- 当前作业提示 -->
<view class="scan-current-hint">
<text v-if="step === 'idle'">请扫描库位条码</text>
<text v-else-if="step === 'location_scanned'">请扫描产品条</text>
<text v-else-if="step === 'product_scanned'">扫码完成请确认出库</text>
<text v-else>产品已出库请继续扫</text>
</view>
<!-- 库位扫码区 -->
@@ -85,11 +79,11 @@
</view>
<!-- 产品扫码区 -->
<view class="scan-row" :class="{ 'scan-row--dim': step === 'idle' || step === 'product_scanned' }">
<view class="scan-row" :class="{ 'scan-row--dim': step === 'idle' }">
<input
v-model="productBarCode"
class="scan-field"
:class="{ 'scan-field--active': step === 'location_scanned' }"
:class="{ 'scan-field--active': step === 'scanned' }"
placeholder="扫描产品条码"
confirm-type="done"
:focus="productInputFocus"
@@ -122,26 +116,6 @@
</view>
</view>
<!-- 批次号扫产品后显示 -->
<view class="scan-row" v-if="step === 'product_scanned'">
<input
v-model="batchNo"
class="scan-field"
placeholder="批次号(可选)"
confirm-type="done"
>
</view>
<!-- 确认出库按钮 -->
<button
v-if="step === 'product_scanned'"
class="scan-confirm-btn"
:disabled="!canConfirm || scanLoading"
@tap="handleConfirm"
>
{{ scanLoading ? '处理中...' : '确认出库' }}
</button>
<ScanFeedbackBar :tone="feedbackTone" :message="feedbackMessage" />
</view>
@@ -218,7 +192,7 @@ export interface ScanRecord {
operatorName?: string
}
type Step = 'idle' | 'location_scanned' | 'product_scanned'
type Step = 'idle' | 'scanned'
/** 页面参数id=出库单ID, no=出库单号 */
const props = defineProps<{
@@ -327,11 +301,11 @@ function handleLocationBlur() {
}
}
/** 产品输入框失焦处理 - 如果当前步骤是 location_scanned自动重新聚焦 */
/** 产品输入框失焦处理 - 如果当前步骤是 scanned自动重新聚焦 */
function handleProductBlur() {
if (step.value === 'location_scanned' && !scanLoading.value) {
if (step.value === 'scanned' && !scanLoading.value) {
setTimeout(() => {
if (step.value === 'location_scanned') {
if (step.value === 'scanned') {
focusProductInput()
}
}, 100)
@@ -376,7 +350,7 @@ async function handleLocationScan() {
}
locationCode.value = code
step.value = 'location_scanned'
step.value = 'scanned'
productBarCode.value = ''
productPreview.value = null
batchNo.value = ''
@@ -431,8 +405,10 @@ async function handleProductScan() {
}
productBarCode.value = code
step.value = 'product_scanned'
step.value = 'scanned'
clearFeedback()
// 自动确认出库
nextTick(() => handleConfirm())
} finally {
scanLoading.value = false
}
@@ -877,23 +853,6 @@ function clearFeedback() {
}
}
// 确认按钮
.scan-confirm-btn {
height: 96rpx;
line-height: 96rpx;
border-radius: 16rpx;
background: #1677ff;
color: #fff;
font-size: 32rpx;
font-weight: 700;
margin-top: 8rpx;
&[disabled] {
background: #d9d9d9;
color: #fff;
}
}
// 底部栏
.scan-footer {
position: sticky;