material_manage_system/frontend/src/views/Outbound.vue
2026-09-16 23:30:25 +08:00

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>