fix: 李红攀:V2.0.061采购入库、销售出库添加扫码功能
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 ? '已审核' : '未审核' }}
|
||||
</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>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user