出库进行修改,确保可以进行多个样例的出库以及出库的记录展示

This commit is contained in:
dxc
2026-02-05 16:54:11 +08:00
parent 3f6ab3e607
commit c1ddb8093f
6 changed files with 608 additions and 385 deletions

View File

@ -3,14 +3,20 @@
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>扫码出库作业台</span>
<el-button type="primary" @click="toggleCamera">
{{ showCamera ? '关闭摄像头' : '打开摄像头扫码' }}
</el-button>
<span>批量扫码出库作业台</span>
<div class="header-right">
<span v-if="cartItems.length > 0" class="summary-text">
已选: <span class="num">{{ cartItems.length }}</span> |
总金额: <span class="price">¥{{ totalAmount.toFixed(2) }}</span>
</span>
<el-button type="primary" @click="toggleCamera" style="margin-left: 10px;">
{{ showCamera ? '关闭摄像头' : '打开摄像头扫码' }}
</el-button>
</div>
</div>
</template>
<div v-if="!currentItem" class="scan-area">
<div class="scan-area">
<div v-if="showCamera" id="reader" class="camera-box"></div>
<div v-if="isHttpAndNotLocal" class="http-warning">
注意当前为 HTTP 环境摄像头可能无法启动请使用 HTTPS localhost或配置浏览器安全白名单
@ -19,58 +25,76 @@
<div class="input-box">
<el-input
v-model="barcodeInput"
placeholder="请扫描条码或手动输入后回车"
placeholder="请连续扫描条码或手动输入后回车"
@keyup.enter="handleScan"
clearable
ref="barcodeRef"
class="scan-input"
>
<template #prefix>
<el-icon><Scissor /></el-icon>
</template>
<template #append>
<el-button @click="handleScan">确定</el-button>
<el-button @click="handleScan">添加商品</el-button>
</template>
</el-input>
</div>
</div>
<div v-else class="confirm-area">
<el-descriptions title="货物信息" border :column="1">
<el-descriptions-item label="名称">{{ currentItem.name }}</el-descriptions-item>
<el-descriptions-item label="规格/型号">{{ currentItem.spec_model }}</el-descriptions-item>
<el-descriptions-item label="当前库存">
<el-tag :type="currentItem.available_quantity > 0 ? 'success' : 'danger'">
{{ currentItem.stock_quantity }} (可用: {{ currentItem.available_quantity }})
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="库位">{{ currentItem.warehouse_location || '暂无' }}</el-descriptions-item>
</el-descriptions>
<div class="cart-area mt-20" v-if="cartItems.length > 0">
<el-table :data="cartItems" border stripe style="width: 100%">
<el-table-column prop="name" label="名称" show-overflow-tooltip />
<el-table-column prop="spec_model" label="规格" show-overflow-tooltip />
<el-table-column prop="sku" label="SKU" width="150" />
<el-table-column label="单价" width="120">
<template #default="{row}">
¥{{ row.price }}
</template>
</el-table-column>
<el-table-column label="库存" width="100">
<template #default="{row}">
<el-tag :type="row.available_quantity > 0 ? 'success' : 'danger'">{{ row.available_quantity }}</el-tag>
</template>
</el-table-column>
<el-table-column label="出库数量" width="160">
<template #default="{row}">
<el-input-number v-model="row.out_quantity" :min="1" :max="row.available_quantity" size="small" />
</template>
</el-table-column>
<el-table-column label="小计" width="120">
<template #default="{row}">
<span style="color: #F56C6C; font-weight: bold;">¥{{ (row.price * row.out_quantity).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{$index}">
<el-button type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
</template>
</el-table-column>
</el-table>
</div>
<el-empty v-else description="暂无扫描商品,请扫描上方条码进行添加" />
<el-form :model="form" ref="formRef" :rules="rules" label-position="top" class="mt-20">
<div v-if="cartItems.length > 0" class="confirm-area mt-20">
<el-divider content-position="left">单据信息</el-divider>
<el-form :model="form" ref="formRef" :rules="rules" label-position="top">
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="8">
<el-form-item label="出库类型" prop="outbound_type">
<el-select v-model="form.outbound_type" placeholder="请选择">
<el-select v-model="form.outbound_type" placeholder="请选择" style="width: 100%">
<el-option label="销售出库" value="SALES" />
<el-option label="内部领用" value="USE" />
<el-option label="调拨" value="TRANSFER" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出库数量" prop="quantity">
<el-input-number v-model="form.quantity" :min="1" :max="currentItem.available_quantity" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="8">
<el-form-item label="领用人/客户姓名" prop="consumer_name">
<el-input v-model="form.consumer_name" placeholder="请输入姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="操作员 (库管)" prop="operator_name">
<el-select
v-model="form.operator_name"
@ -108,8 +132,8 @@
</el-form-item>
<div class="form-actions">
<el-button @click="resetScan">取消 / 重新扫码</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">确认出库</el-button>
<el-button @click="clearAll">清空重置</el-button>
<el-button type="primary" size="large" :loading="loading" @click="submitForm">确认批量出库</el-button>
</div>
</el-form>
</div>
@ -154,16 +178,16 @@
<script setup lang="ts">
import { ref, reactive, nextTick, onUnmounted, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Html5Qrcode, Html5QrcodeSupportedFormats } from 'html5-qrcode'
import { Scissor, EditPen } from '@element-plus/icons-vue'
import { getStockByBarcode, submitOutbound, getOutboundList, type ScanResult } from '@/api/outbound'
import { Scissor, EditPen, Delete } from '@element-plus/icons-vue'
import { getStockByBarcode, submitOutbound, getOutboundList } from '@/api/outbound'
import { uploadFile } from '@/api/common/upload'
import { useUserStore } from '@/stores/user'
// --- 状态定义 ---
const barcodeInput = ref('')
const currentItem = ref<ScanResult | null>(null)
const cartItems = ref<any[]>([]) // 购物车列表
const loading = ref(false)
const showCamera = ref(false)
const html5QrCode = ref<Html5Qrcode | null>(null)
@ -188,7 +212,6 @@ const operatorOptions = ref<string[]>([])
const form = reactive({
outbound_type: 'SALES',
quantity: 1,
consumer_name: '',
operator_name: '',
remark: ''
@ -196,7 +219,6 @@ const form = reactive({
const rules = {
consumer_name: [{ required: true, message: '请输入领用人姓名', trigger: 'blur' }],
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
operator_name: [{ required: true, message: '请指定操作员', trigger: 'change' }]
}
@ -206,6 +228,11 @@ const isHttpAndNotLocal = computed(() => {
return !isHttps && !isLocal
})
// 计算总金额
const totalAmount = computed(() => {
return cartItems.value.reduce((sum, item) => sum + (item.price * item.out_quantity), 0)
})
// --- 初始化逻辑 ---
onMounted(() => {
if (userStore.username) {
@ -221,9 +248,10 @@ const loadHistoryOperators = async () => {
if (res.data && res.data.items) {
const names = new Set<string>()
if (userStore.username) names.add(userStore.username)
res.data.items.forEach((item: any) => {
if (item.operator_name) {
names.add(item.operator_name)
// 注意现在的列表结构变了operator_name 在外层
res.data.items.forEach((group: any) => {
if (group.operator_name) {
names.add(group.operator_name)
}
})
operatorOptions.value = Array.from(names)
@ -233,149 +261,139 @@ const loadHistoryOperators = async () => {
}
}
// --- 签名逻辑 ---
const openSignatureDialog = () => {
showSignatureDialog.value = true
}
const initCanvas = async () => {
await nextTick()
const canvas = nativeCanvasRef.value
const container = canvasContainerRef.value
if (canvas && container) {
// 强制设置宽高,确保在手机横屏竖屏切换后也能占满
canvas.width = container.clientWidth
canvas.height = container.clientHeight
ctx.value = canvas.getContext('2d')
if (ctx.value) {
ctx.value.lineWidth = 4
ctx.value.lineCap = 'round'
ctx.value.lineJoin = 'round'
ctx.value.strokeStyle = '#000000'
// 填充白色背景
ctx.value.fillStyle = '#ffffff'
ctx.value.fillRect(0, 0, canvas.width, canvas.height)
}
}
}
// 获取坐标(兼容鼠标和触摸)
const getPos = (e: MouseEvent | TouchEvent) => {
if (!nativeCanvasRef.value) return { x: 0, y: 0 }
const rect = nativeCanvasRef.value.getBoundingClientRect()
let clientX, clientY
if (e.type.startsWith('touch')) {
clientX = (e as TouchEvent).touches[0].clientX
clientY = (e as TouchEvent).touches[0].clientY
} else {
clientX = (e as MouseEvent).clientX
clientY = (e as MouseEvent).clientY
}
return {
x: clientX - rect.left,
y: clientY - rect.top
}
}
const startDrawing = (e: MouseEvent | TouchEvent) => {
e.preventDefault()
isDrawing.value = true
const { x, y } = getPos(e)
lastX.value = x
lastY.value = y
if(ctx.value) {
ctx.value.beginPath()
ctx.value.moveTo(x, y)
ctx.value.lineTo(x, y)
ctx.value.stroke()
}
}
const draw = (e: MouseEvent | TouchEvent) => {
e.preventDefault()
if (!isDrawing.value || !ctx.value) return
const { x, y } = getPos(e)
ctx.value.beginPath()
ctx.value.moveTo(lastX.value, lastY.value)
ctx.value.lineTo(x, y)
ctx.value.stroke()
lastX.value = x
lastY.value = y
}
const stopDrawing = () => {
isDrawing.value = false
if (ctx.value) ctx.value.beginPath()
}
const clearCanvas = () => {
if (!ctx.value || !nativeCanvasRef.value) return
ctx.value.clearRect(0, 0, nativeCanvasRef.value.width, nativeCanvasRef.value.height)
ctx.value.fillStyle = '#ffffff'
ctx.value.fillRect(0, 0, nativeCanvasRef.value.width, nativeCanvasRef.value.height)
}
const handleSignConfirm = () => {
if (!nativeCanvasRef.value) return
nativeCanvasRef.value.toBlob((blob) => {
if (blob) {
const file = new File([blob], `signature_${Date.now()}.png`, { type: 'image/png' })
signatureFile.value = file
signaturePreviewUrl.value = URL.createObjectURL(file)
showSignatureDialog.value = false
} else {
ElMessage.error('生成签名失败')
}
}, 'image/png')
}
const handleSignCancel = () => {
showSignatureDialog.value = false
}
// --- 扫码逻辑 ---
// --- 扫码逻辑 (加入购物车) ---
const handleScan = async () => {
if (!barcodeInput.value) return
const code = barcodeInput.value.trim()
if (!code) return
try {
loading.value = true
const res = await getStockByBarcode(barcodeInput.value)
// 1. 检查是否已经在购物车中
const existIndex = cartItems.value.findIndex(item => item.barcode === code || item.sku === code)
if (existIndex > -1) {
// 存在则数量+1
const item = cartItems.value[existIndex]
if (item.out_quantity < item.available_quantity) {
item.out_quantity++
ElMessage.success(`商品 ${item.name} 数量+1`)
} else {
ElMessage.warning(`库存不足 (余: ${item.available_quantity})`)
}
barcodeInput.value = ''
return
}
// 2. 新商品调用 API
const res = await getStockByBarcode(code)
if (res.data) {
const item = res.data
if (item.available_quantity <= 0) {
ElMessage.warning(`该商品库存不足或已出库!(当前库存: ${item.available_quantity})`)
barcodeInput.value = ''
return
}
currentItem.value = item
if (html5QrCode.value && html5QrCode.value.isScanning) {
await stopCamera()
} else {
// 加入购物车,默认出库 1
cartItems.value.push({
...item,
out_quantity: 1,
price: item.price || 0
})
ElMessage.success('添加成功')
}
barcodeInput.value = ''
}
} catch (error: any) {
if (error.response && error.response.status === 404) {
ElMessage.error(`未找到条码 ${barcodeInput.value} 的入库记录,请先入库!`)
ElMessage.error(`未找到条码 ${barcodeInput.value} 的入库记录`)
} else {
console.error(error)
ElMessage.error('查询出错,请重试')
}
} finally {
loading.value = false
// 聚焦回输入框
nextTick(() => { barcodeRef.value?.focus() })
}
}
const removeFromCart = (index: number) => {
cartItems.value.splice(index, 1)
}
const clearAll = () => {
ElMessageBox.confirm('确定清空所有已扫商品和填写信息吗?', '提示', {
type: 'warning'
}).then(() => {
cartItems.value = []
form.consumer_name = ''
form.remark = ''
signatureFile.value = null
signaturePreviewUrl.value = ''
barcodeInput.value = ''
})
}
// --- 提交逻辑 (批量提交) ---
const submitForm = async () => {
if (!formRef.value) return
if (cartItems.value.length === 0) {
ElMessage.warning('购物车为空,请先扫码')
return
}
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
if (!signatureFile.value) {
ElMessage.error('请进行电子签名')
return
}
try {
loading.value = true
// 1. 上传签名
const uploadRes = await uploadFile(signatureFile.value)
const signatureUrl = uploadRes.data.url
// 2. 构造 items 数据
const itemsPayload = cartItems.value.map(item => ({
stock_id: item.id,
source_table: item.source_table,
sku: item.sku,
barcode: item.barcode,
quantity: item.out_quantity,
price: item.price
}))
// 3. 提交业务单据
await submitOutbound({
items: itemsPayload,
outbound_type: form.outbound_type,
consumer_name: form.consumer_name,
operator_name: form.operator_name,
remark: form.remark,
signature_path: signatureUrl
})
ElMessage.success('批量出库成功')
// 清空
cartItems.value = []
form.consumer_name = ''
form.remark = ''
signatureFile.value = null
signaturePreviewUrl.value = ''
// 刷新操作员列表
loadHistoryOperators()
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
})
}
// --- 摄像头控制 ---
const toggleCamera = async () => {
if (showCamera.value) {
@ -435,64 +453,106 @@ const stopCamera = async () => {
}
}
// --- 提交逻辑 ---
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
if (!currentItem.value) return
if (!signatureFile.value) {
ElMessage.error('请进行电子签名')
return
}
try {
loading.value = true
const uploadRes = await uploadFile(signatureFile.value)
const signatureUrl = uploadRes.data.url
await submitOutbound({
sku: currentItem.value.sku,
source_table: currentItem.value.source_table,
stock_id: currentItem.value.id,
barcode: barcodeInput.value,
outbound_type: form.outbound_type,
quantity: form.quantity,
consumer_name: form.consumer_name,
operator_name: form.operator_name,
remark: form.remark,
signature_path: signatureUrl
})
ElMessage.success('出库成功')
resetScan()
loadHistoryOperators()
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
})
// --- 签名核心逻辑 ---
const openSignatureDialog = () => {
showSignatureDialog.value = true
}
const resetScan = () => {
currentItem.value = null
barcodeInput.value = ''
form.consumer_name = ''
form.quantity = 1
form.remark = ''
if (userStore.username) {
form.operator_name = userStore.username
const initCanvas = async () => {
// 必须等待 DOM 更新完成,确保 dialog 里的 ref 已经挂载
await nextTick()
const canvas = nativeCanvasRef.value
const container = canvasContainerRef.value
if (canvas && container) {
// 设置物理像素等于显示像素,避免模糊
canvas.width = container.clientWidth
canvas.height = container.clientHeight
ctx.value = canvas.getContext('2d')
if (ctx.value) {
ctx.value.lineWidth = 4
ctx.value.lineCap = 'round'
ctx.value.lineJoin = 'round'
ctx.value.strokeStyle = '#000000'
// 填充白色背景,防止生成透明图片
ctx.value.fillStyle = '#ffffff'
ctx.value.fillRect(0, 0, canvas.width, canvas.height)
}
} else {
console.error('Canvas 初始化失败:无法获取 DOM 元素')
}
signatureFile.value = null
signaturePreviewUrl.value = ''
nextTick(() => {
barcodeRef.value?.focus()
})
}
const getPos = (e: MouseEvent | TouchEvent) => {
if (!nativeCanvasRef.value) return { x: 0, y: 0 }
const rect = nativeCanvasRef.value.getBoundingClientRect()
let clientX, clientY
if (e.type.startsWith('touch')) {
clientX = (e as TouchEvent).touches[0].clientX
clientY = (e as TouchEvent).touches[0].clientY
} else {
clientX = (e as MouseEvent).clientX
clientY = (e as MouseEvent).clientY
}
return { x: clientX - rect.left, y: clientY - rect.top }
}
const startDrawing = (e: MouseEvent | TouchEvent) => {
// 阻止默认事件(如页面滚动)
e.preventDefault()
isDrawing.value = true
const { x, y } = getPos(e)
lastX.value = x
lastY.value = y
if(ctx.value) {
ctx.value.beginPath()
ctx.value.moveTo(x, y)
ctx.value.lineTo(x, y)
ctx.value.stroke()
}
}
const draw = (e: MouseEvent | TouchEvent) => {
e.preventDefault()
if (!isDrawing.value || !ctx.value) return
const { x, y } = getPos(e)
ctx.value.beginPath()
ctx.value.moveTo(lastX.value, lastY.value)
ctx.value.lineTo(x, y)
ctx.value.stroke()
lastX.value = x
lastY.value = y
}
const stopDrawing = () => {
isDrawing.value = false
if (ctx.value) ctx.value.beginPath()
}
const clearCanvas = () => {
if (!ctx.value || !nativeCanvasRef.value) return
ctx.value.clearRect(0, 0, nativeCanvasRef.value.width, nativeCanvasRef.value.height)
ctx.value.fillStyle = '#ffffff'
ctx.value.fillRect(0, 0, nativeCanvasRef.value.width, nativeCanvasRef.value.height)
}
const handleSignConfirm = () => {
if (!nativeCanvasRef.value) return
nativeCanvasRef.value.toBlob((blob) => {
if (blob) {
const file = new File([blob], `signature_${Date.now()}.png`, { type: 'image/png' })
signatureFile.value = file
signaturePreviewUrl.value = URL.createObjectURL(file)
showSignatureDialog.value = false
} else {
ElMessage.error('生成签名失败')
}
}, 'image/png')
}
const handleSignCancel = () => {
showSignatureDialog.value = false
}
onUnmounted(() => {
@ -504,7 +564,6 @@ onUnmounted(() => {
</script>
<style scoped>
/* 原有样式保持不变 */
.scan-area {
text-align: center;
padding: 20px;
@ -526,7 +585,7 @@ onUnmounted(() => {
font-size: 12px;
}
.confirm-area {
max-width: 600px;
max-width: 800px;
margin: 0 auto;
}
.mt-20 {
@ -537,7 +596,6 @@ onUnmounted(() => {
display: flex;
justify-content: space-between;
}
.signature-display-area {
border: 1px dashed #dcdfe6;
border-radius: 4px;
@ -549,9 +607,6 @@ onUnmounted(() => {
align-items: center;
justify-content: center;
}
.signed-image-box {
text-align: center;
}
.signed-image-box img {
max-height: 100px;
max-width: 100%;
@ -559,23 +614,32 @@ onUnmounted(() => {
margin-bottom: 5px;
border: 1px solid #eee;
}
/* 新增:头部右侧信息 */
.header-right {
display: flex;
align-items: center;
}
.summary-text {
margin-right: 15px;
font-size: 14px;
color: #606266;
}
.summary-text .num { color: #409EFF; font-weight: bold; margin: 0 4px; }
.summary-text .price { color: #F56C6C; font-weight: bold; margin: 0 4px; font-size: 16px; }
/* --- 响应式全屏弹窗样式 --- */
/* 响应式签名弹窗 */
:deep(.fullscreen-signature-dialog .el-dialog__body) {
padding: 0;
height: 100%;
overflow: hidden;
display: flex;
}
/* 默认布局(电脑/平板):左右结构 */
.signature-wrapper {
display: flex;
width: 100%;
height: 100%; /* 使用 100% 适应弹窗 body */
height: 100%;
background-color: #fff;
}
.signature-canvas-container {
flex: 1;
position: relative;
@ -584,7 +648,6 @@ onUnmounted(() => {
width: 100%;
overflow: hidden;
}
.native-canvas {
display: block;
width: 100%;
@ -592,8 +655,6 @@ onUnmounted(() => {
cursor: crosshair;
touch-action: none;
}
/* 右侧边栏(默认) */
.signature-sidebar {
width: 150px;
background: #333;
@ -607,7 +668,6 @@ onUnmounted(() => {
z-index: 10;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
}
.sidebar-title {
writing-mode: vertical-rl;
letter-spacing: 4px;
@ -615,69 +675,50 @@ onUnmounted(() => {
font-weight: bold;
opacity: 0.8;
}
.sidebar-actions {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
}
.sidebar-actions .el-button {
width: 100%;
margin-left: 0;
height: 50px;
}
.confirm-btn {
margin-top: 20px;
font-weight: bold;
}
/* --- ★ 手机端适配 (屏幕宽度小于 768px) --- */
@media screen and (max-width: 768px) {
/* 1. 改为上下布局:画布在上,按钮在下 */
.signature-wrapper {
flex-direction: column;
}
/* 2. 画布区域自动填充剩余空间 */
.signature-canvas-container {
flex: 1;
height: auto; /* 高度自适应 */
height: auto;
}
/* 3. 底部工具栏样式重写 */
.signature-sidebar {
width: 100%; /* 宽度占满 */
height: auto; /* 高度由内容撑开 */
flex-direction: row; /* 内容横向排列 */
padding: 15px; /* 增加内边距 */
width: 100%;
height: auto;
flex-direction: row;
padding: 15px;
gap: 10px;
/* 放在底部 */
order: 2;
}
/* 4. 隐藏竖排文字标题,节省空间 */
.sidebar-title {
display: none;
}
/* 5. 按钮组改为横向排列 */
.sidebar-actions {
flex-direction: row;
gap: 10px;
width: 100%;
}
/* 6. 按钮大小调整,均匀分布 */
.sidebar-actions .el-button {
flex: 1; /* 三个按钮平分宽度 */
height: 44px; /* 适合手指点击的高度 */
flex: 1;
height: 44px;
font-size: 14px;
}
/* 去掉确认按钮原本的顶部外边距 */
.confirm-btn {
margin-top: 0;
}

View File

@ -30,7 +30,33 @@
style="width: 100%; margin-top: 20px;"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
>
<el-table-column prop="outbound_no" label="出库单号" min-width="180" show-overflow-tooltip />
<el-table-column type="expand">
<template #default="props">
<div style="padding: 10px 40px; background: #fafafa;">
<h4 style="margin: 0 0 10px 0; color: #409EFF;">商品明细 (按单价降序)</h4>
<el-table :data="props.row.items" border size="small">
<el-table-column prop="sku" label="SKU" width="150" />
<el-table-column prop="name" label="名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="material_type" label="类型" width="120" show-overflow-tooltip />
<el-table-column prop="category" label="类别" width="120" show-overflow-tooltip />
<el-table-column prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip />
<el-table-column prop="quantity" label="数量" width="100" />
<el-table-column prop="unit_price" label="单价" width="120">
<template #default="{row}">¥{{ row.unit_price }}</template>
</el-table-column>
<el-table-column prop="subtotal" label="小计">
<template #default="{row}">
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column prop="outbound_no" label="出库单号" min-width="200" show-overflow-tooltip />
<el-table-column prop="outbound_time" label="出库时间" width="170" align="center">
<template #default="{ row }">
@ -44,9 +70,11 @@
</template>
</el-table-column>
<el-table-column prop="sku" label="SKU" min-width="140" show-overflow-tooltip />
<el-table-column prop="quantity" label="数量" width="90" align="center" />
<el-table-column prop="total_amount" label="总金额" width="120" align="right">
<template #default="{ row }">
<span style="color: #F56C6C; font-weight: bold; font-size: 15px;">¥{{ row.total_amount }}</span>
</template>
</el-table-column>
<el-table-column prop="consumer_name" label="领用/客户" min-width="120" show-overflow-tooltip />
@ -76,18 +104,31 @@
</template>
</el-table-column>
</el-table>
<div style="margin-top: 20px; text-align: right;">
<el-pagination
background
layout="prev, pager, next"
:total="total"
:page-size="listQuery.limit"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import { getOutboundList } from '@/api/outbound'
import { Picture } from '@element-plus/icons-vue' // 引入图标用于图片加载失败显示
import { Picture } from '@element-plus/icons-vue'
const list = ref([])
const total = ref(0)
const loading = ref(false)
const listQuery = reactive({
page: 1,
limit: 10,
keyword: '',
dateRange: []
})
@ -95,8 +136,15 @@ const listQuery = reactive({
const fetchData = async () => {
loading.value = true
try {
const res = await getOutboundList(listQuery)
const params = {
...listQuery,
start_date: listQuery.dateRange && listQuery.dateRange[0] ? listQuery.dateRange[0] : null,
end_date: listQuery.dateRange && listQuery.dateRange[1] ? listQuery.dateRange[1] : null
}
const res = await getOutboundList(params)
list.value = res.data.items || []
total.value = res.data.total || 0
} catch (e) {
console.error(e)
} finally {
@ -104,6 +152,11 @@ const fetchData = async () => {
}
}
const handlePageChange = (val: number) => {
listQuery.page = val
fetchData()
}
const formatType = (type: string) => {
const map: any = {
'SALES': '销售出库',
@ -114,7 +167,6 @@ const formatType = (type: string) => {
return map[type] || type
}
// 辅助函数:根据类型返回 Tag 颜色
const getTagType = (type: string) => {
const map: any = {
'SALES': 'success',