327 lines
10 KiB
Vue
327 lines
10 KiB
Vue
<template>
|
|
<div class="outbound-page">
|
|
<div class="page-header">
|
|
<h2>出库管理</h2>
|
|
<el-button type="primary" @click="showAddModal">新增出库单</el-button>
|
|
</div>
|
|
|
|
<BaseTable
|
|
:data="outboundStore.outboundList"
|
|
:loading="outboundStore.loading"
|
|
:current-page="outboundStore.page.currentPage"
|
|
:page-size="outboundStore.page.pageSize"
|
|
:total="outboundStore.page.total"
|
|
@page-change="handlePageChange"
|
|
>
|
|
<el-table-column type="index" label="ID" width="60" />
|
|
<el-table-column prop="outbound_no" label="出库单号" />
|
|
<el-table-column prop="status" label="状态">
|
|
<template #default="{ row }">
|
|
<StatusBadge :status="row.status" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="receiver" label="接收方" />
|
|
<el-table-column prop="operator" label="操作人" />
|
|
<el-table-column prop="remark" label="备注" />
|
|
<el-table-column prop="created_at" label="创建时间" width="160" class-name="pr-3">
|
|
<template #default="{ row }">
|
|
{{ formatDate(row.created_at) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="240" fixed="right" class-name="border-left">
|
|
<template #default="{ row }">
|
|
<el-button size="small" @click="showDetail(row)">详情</el-button>
|
|
<el-button v-if="authStore.isAdmin" size="small" type="primary" @click="showEditModal(row)">编辑</el-button>
|
|
<el-button v-if="authStore.isAdmin" size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</BaseTable>
|
|
|
|
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑出库单' : '新增出库单'" width="900px">
|
|
<el-form :model="form" label-width="100px">
|
|
<el-form-item label="操作人">
|
|
<el-input v-model="form.handler" placeholder="请输入操作人" />
|
|
</el-form-item>
|
|
<el-form-item label="接收方">
|
|
<el-input v-model="form.receiver" placeholder="请输入接收方" />
|
|
</el-form-item>
|
|
<el-form-item label="备注">
|
|
<el-input v-model="form.remark" type="textarea" rows="3" />
|
|
</el-form-item>
|
|
<el-form-item label="出库物料">
|
|
<div class="material-items">
|
|
<div v-for="(item, index) in form.items" :key="index" class="item-row">
|
|
<el-select
|
|
v-model="item.material_id"
|
|
placeholder="输入物料编码或名称搜索"
|
|
filterable
|
|
:filter-method="(val) => materialFilter(val, item)"
|
|
style="width: 280px"
|
|
@change="(id) => onMaterialChange(item, id)"
|
|
>
|
|
<el-option
|
|
v-for="m in getFilteredMaterials(item)"
|
|
:key="m.id"
|
|
:label="`${m.material_code} - ${m.name}`"
|
|
:value="m.id"
|
|
/>
|
|
</el-select>
|
|
<el-input-number v-model="item.quantity" :min="1" />
|
|
<span class="stock-info" :class="{ 'low-stock': isLowStock(item.material_id) }">
|
|
库存: {{ itemStocks[item.material_id] ?? '-' }}
|
|
</span>
|
|
<el-button @click="removeItem(index)" :disabled="form.items.length <= 1">删除</el-button>
|
|
</div>
|
|
<el-button @click="addItem">添加物料</el-button>
|
|
</div>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="handleSave" :loading="saveLoading">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<DeleteConfirm v-model="deleteDialogVisible" @confirm="confirmDelete" />
|
|
|
|
<el-dialog v-model="detailVisible" title="出库单详情" width="600px">
|
|
<div v-if="currentRow">
|
|
<p><strong>出库单号:</strong>{{ currentRow.outbound_no }}</p>
|
|
<p><strong>状态:</strong><StatusBadge :status="currentRow.status" /></p>
|
|
<p><strong>接收方:</strong>{{ currentRow.receiver || '-' }}</p>
|
|
<p><strong>操作人:</strong>{{ currentRow.operator }}</p>
|
|
<p><strong>备注:</strong>{{ currentRow.remark || '-' }}</p>
|
|
<p><strong>创建时间:</strong>{{ formatDate(currentRow.created_at) }}</p>
|
|
<div v-if="currentRow.items && currentRow.items.length > 0" class="mt-3">
|
|
<h5>物料明细</h5>
|
|
<el-table :data="currentRow.items" border size="small">
|
|
<el-table-column prop="material_code" label="物料编码" />
|
|
<el-table-column prop="material_name" label="物料名称" />
|
|
<el-table-column prop="quantity" label="数量" />
|
|
</el-table>
|
|
</div>
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, onMounted, computed } from 'vue'
|
|
import { useOutboundStore } from '../stores/outbound'
|
|
import { useMaterialsStore } from '../stores/materials'
|
|
import { useStockStore } from '../stores/stock'
|
|
import { useAuthStore } from '../stores/auth'
|
|
import BaseTable from '../components/business/BaseTable.vue'
|
|
import StatusBadge from '../components/business/StatusBadge.vue'
|
|
import DeleteConfirm from '../components/business/DeleteConfirm.vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { isLowStock as isQuantityLowStock } from '../utils/quantity'
|
|
|
|
const outboundStore = useOutboundStore()
|
|
const materialsStore = useMaterialsStore()
|
|
const stockStore = useStockStore()
|
|
const authStore = useAuthStore()
|
|
|
|
const dialogVisible = ref(false)
|
|
const detailVisible = ref(false)
|
|
const deleteDialogVisible = ref(false)
|
|
const saveLoading = ref(false)
|
|
const deleteId = ref(null)
|
|
const currentRow = ref(null)
|
|
const isEdit = ref(false)
|
|
const editId = ref(null)
|
|
|
|
const form = reactive({
|
|
handler: '',
|
|
receiver: '',
|
|
remark: '',
|
|
items: [{ material_id: null, materialSearchText: '', quantity: 1 }]
|
|
})
|
|
|
|
// Track stock for selected materials in form
|
|
const itemStocks = ref({})
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([
|
|
outboundStore.fetchOutboundList(),
|
|
materialsStore.fetchMaterials(),
|
|
stockStore.fetchAllStock()
|
|
])
|
|
})
|
|
|
|
function handlePageChange(page) {
|
|
outboundStore.fetchOutboundList(page)
|
|
}
|
|
|
|
function showAddModal() {
|
|
isEdit.value = false
|
|
editId.value = null
|
|
Object.assign(form, { handler: '', receiver: '', remark: '', items: [{ material_id: null, materialSearchText: '', quantity: 1 }] })
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
async function showEditModal(row) {
|
|
isEdit.value = true
|
|
editId.value = row.id
|
|
try {
|
|
const res = await outboundStore.fetchOutboundDetail(row.id)
|
|
const order = res.data || res
|
|
form.handler = order.handler || ''
|
|
form.receiver = order.receiver || ''
|
|
form.remark = order.remark || ''
|
|
form.items = order.items && order.items.length > 0
|
|
? order.items.map(item => {
|
|
const material = materialsStore.materials.find(m => m.id === item.material_id)
|
|
// Fetch stock for existing items
|
|
stockStore.fetchStockByMaterialId(item.material_id).then(stock => {
|
|
itemStocks.value[item.material_id] = stock
|
|
})
|
|
return { material_id: item.material_id, materialSearchText: material ? `${material.material_code} - ${material.name}` : '', quantity: item.quantity }
|
|
})
|
|
: [{ material_id: null, materialSearchText: '', quantity: 1 }]
|
|
dialogVisible.value = true
|
|
} catch (error) {
|
|
ElMessage.error('加载出库单详情失败')
|
|
}
|
|
}
|
|
|
|
function addItem() {
|
|
form.items.push({ material_id: null, materialSearchText: '', quantity: 1 })
|
|
}
|
|
|
|
function removeItem(index) {
|
|
form.items.splice(index, 1)
|
|
}
|
|
|
|
function materialFilter(val, item) {
|
|
item.materialSearchText = val
|
|
}
|
|
|
|
function getFilteredMaterials(item) {
|
|
const keyword = (item.materialSearchText || '').toLowerCase()
|
|
if (!keyword) return materialsStore.materials
|
|
return materialsStore.materials.filter(m =>
|
|
m.material_code.toLowerCase().includes(keyword) ||
|
|
m.name.toLowerCase().includes(keyword)
|
|
)
|
|
}
|
|
|
|
function onMaterialChange(item, id) {
|
|
const idx = form.items.indexOf(item)
|
|
if (idx !== -1) {
|
|
form.items.splice(idx, 1, { ...item })
|
|
}
|
|
if (id) {
|
|
stockStore.fetchStockByMaterialId(id).then(stock => {
|
|
itemStocks.value[id] = stock
|
|
})
|
|
}
|
|
}
|
|
|
|
function getStock(materialId) {
|
|
if (!materialId) return 0
|
|
const stock = stockStore.stockList.find(s => s.material_id === materialId)
|
|
return stock ? stock.current_stock : '-'
|
|
}
|
|
|
|
function isLowStock(materialId) {
|
|
if (!materialId) return false
|
|
const stock = stockStore.stockList.find(s => s.material_id === materialId)
|
|
if (!stock) return false
|
|
return isQuantityLowStock(stock)
|
|
}
|
|
|
|
async function handleSave() {
|
|
saveLoading.value = true
|
|
try {
|
|
const validItems = form.items.filter(item => item.material_id && item.quantity > 0)
|
|
if (validItems.length === 0) {
|
|
ElMessage.warning('请至少添加一个物料')
|
|
return
|
|
}
|
|
const data = {
|
|
handler: form.handler,
|
|
receiver: form.receiver,
|
|
remark: form.remark,
|
|
items: validItems
|
|
}
|
|
if (isEdit.value) {
|
|
await outboundStore.updateOutbound(editId.value, data)
|
|
ElMessage.success('修改成功')
|
|
} else {
|
|
await outboundStore.createOutbound(data)
|
|
ElMessage.success('保存成功')
|
|
}
|
|
dialogVisible.value = false
|
|
outboundStore.fetchOutboundList()
|
|
} finally {
|
|
saveLoading.value = false
|
|
}
|
|
}
|
|
|
|
function handleDelete(id) {
|
|
deleteId.value = id
|
|
deleteDialogVisible.value = true
|
|
}
|
|
|
|
async function confirmDelete() {
|
|
await outboundStore.deleteOutbound(deleteId.value)
|
|
ElMessage.success('删除成功')
|
|
}
|
|
|
|
async function showDetail(row) {
|
|
try {
|
|
const detail = await outboundStore.fetchOutboundDetail(row.id)
|
|
currentRow.value = detail
|
|
} catch (error) {
|
|
console.error('获取详情失败:', error)
|
|
ElMessage.error('获取详情失败')
|
|
currentRow.value = { ...row, items: [] }
|
|
}
|
|
detailVisible.value = true
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '-'
|
|
return new Date(dateStr).toLocaleString('zh-CN')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.outbound-page {
|
|
padding: 20px;
|
|
}
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.item-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
align-items: center;
|
|
}
|
|
.material-items {
|
|
width: 100%;
|
|
}
|
|
.stock-info {
|
|
padding: 0 8px;
|
|
min-width: 80px;
|
|
}
|
|
.stock-info.low-stock {
|
|
color: #f56c6c;
|
|
font-weight: bold;
|
|
}
|
|
.mt-3 {
|
|
margin-top: 16px;
|
|
}
|
|
:deep(.pr-3) {
|
|
padding-right: 12px;
|
|
}
|
|
:deep(.border-left) {
|
|
border-left: 1px solid var(--el-table-border-color);
|
|
}
|
|
</style>
|