3.18
<template>
<div class="inbound-management">
<div class="header">
<h2>备件入库</h2>
<div class="actions">
<el-button type="primary" @click="showAddDialog">新增入库</el-button>
<el-button type="success" @click="batchApprove" :disabled="selectedRecords.length === 0">批量审核</el-button>
<el-button type="danger" @click="batchReject" :disabled="selectedRecords.length === 0">批量驳回</el-button>
<el-button type="info" @click="downloadTemplate">下载模板</el-button>
<el-button type="warning" @click="importData">批量导入</el-button>
</div>
</div>
<!-- 搜索区域 -->
<div class="search-area">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="备件名称">
<el-input v-model="searchForm.sparePartName" placeholder="请输入备件名称" clearable></el-input>
</el-form-item>
<el-form-item label="备件型号">
<el-input v-model="searchForm.sparePartModel" placeholder="请输入备件型号" clearable></el-input>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="待入库" value="待入库"></el-option>
<el-option label="已入库" value="已入库"></el-option>
<el-option label="已驳回" value="已驳回"></el-option>
</el-select>
</el-form-item>
<el-form-item label="库位">
<el-select v-model="searchForm.storageLocation" placeholder="请选择库位" clearable>
<el-option v-for="location in storageLocations" :key="location" :label="location" :value="location"/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchRecords">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 数据表格 -->
<el-table
:data="inboundRecords"
style="width: 100%"
@selection-change="handleSelectionChange"
v-loading="loading">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="sparePartName" label="备件名称" width="120"></el-table-column>
<el-table-column prop="sparePartModel" label="备件型号" width="120"></el-table-column>
<el-table-column prop="sparePartCategory" label="分类" width="100"></el-table-column>
<el-table-column prop="sparePartStatus" label="备件状态" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.sparePartStatus)">
{{ scope.row.sparePartStatus }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sparePartType" label="备件类型" width="100"></el-table-column>
<el-table-column prop="quantity" label="数量" width="80"></el-table-column>
<el-table-column prop="unitPrice" label="单价" width="100">
<template slot-scope="scope">
¥{{ scope.row.unitPrice }}
</template>
</el-table-column>
<el-table-column prop="invoiceType" label="票据类型" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.invoiceType === '专票' ? 'success' : 'info'">
{{ scope.row.invoiceType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="taxRate" label="税率" width="80">
<template slot-scope="scope">
{{ (scope.row.taxRate * 100).toFixed(1) }}%
</template>
</el-table-column>
<el-table-column prop="totalAmount" label="总金额" width="120">
<template slot-scope="scope">
¥{{ scope.row.totalAmount }}
</template>
</el-table-column>
<el-table-column prop="storageLocation" label="库位" width="100"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="getRecordStatusType(scope.row.status)">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="inboundPerson" label="入库人" width="100"></el-table-column>
<el-table-column prop="inboundDate" label="入库时间" width="150">
<template slot-scope="scope">
{{ formatDate(scope.row.inboundDate) }}
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template slot-scope="scope">
<el-button size="mini" @click="viewRecord(scope.row)">详情</el-button>
<el-button size="mini" type="primary" @click="editRecord(scope.row)"
v-if="scope.row.status === '待入库'">编辑</el-button>
<el-button size="mini" type="success" @click="approveRecord(scope.row)"
v-if="scope.row.status === '待入库'">审核</el-button>
<el-button size="mini" type="danger" @click="rejectRecord(scope.row)"
v-if="scope.row.status === '待入库'">驳回</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="900px">
<el-form :model="currentRecord" :rules="rules" ref="recordForm" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="备件名称" prop="sparePartName">
<el-input v-model="currentRecord.sparePartName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备件型号" prop="sparePartModel">
<el-input v-model="currentRecord.sparePartModel"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="备件分类" prop="sparePartCategory">
<el-select v-model="currentRecord.sparePartCategory" placeholder="请选择分类">
<el-option v-for="category in partCategories" :key="category" :label="category" :value="category"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备件状态" prop="sparePartStatus">
<el-select v-model="currentRecord.sparePartStatus" placeholder="请选择">
<el-option label="新好件" value="新好件"></el-option>
<el-option label="修好件" value="修好件"></el-option>
<el-option label="坏件" value="坏件"></el-option>
<el-option label="二级修" value="二级修"></el-option>
<el-option label="返厂修" value="返厂修"></el-option>
<el-option label="待调拨" value="待调拨"></el-option>
<el-option label="待报废" value="待报废"></el-option>
<el-option label="已报废" value="已报废"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="备件类型" prop="sparePartType">
<el-select v-model="currentRecord.sparePartType" placeholder="请选择">
<el-option label="正常件" value="正常件"></el-option>
<el-option label="在保件" value="在保件"></el-option>
<el-option label="遗留件" value="遗留件"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="库位" prop="storageLocation">
<el-select v-model="currentRecord.storageLocation" placeholder="请选择库位">
<el-option v-for="location in storageLocations" :key="location" :label="location" :value="location"/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="单价" prop="unitPrice">
<el-input-number v-model="currentRecord.unitPrice" :precision="2" :min="0"
@change="calculateAmounts" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="数量" prop="quantity">
<el-input-number v-model="currentRecord.quantity" :min="1"
@change="calculateAmounts" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="票据类型" prop="invoiceType">
<el-select v-model="currentRecord.invoiceType" placeholder="请选择" @change="calculateAmounts">
<el-option label="普票" value="普票"></el-option>
<el-option label="专票" value="专票"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="供应商" prop="supplier">
<el-input v-model="currentRecord.supplier"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入库人" prop="inboundPerson">
<el-input v-model="currentRecord.inboundPerson"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="SN号" prop="snNumber">
<el-input v-model="currentRecord.snNumber" placeholder="留空自动生成批量SN号"></el-input>
<div class="form-tip">系统将根据数量自动生成唯一SN号</div>
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input type="textarea" v-model="currentRecord.remarks" :rows="3"></el-input>
</el-form-item>
<!-- 税务计算显示 -->
<div v-if="currentRecord.unitPrice && currentRecord.quantity" class="tax-info">
<h4>税务信息</h4>
<el-row :gutter="20">
<el-col :span="6">
<div class="tax-item">
<label>税率:</label>
<span>{{ getTaxRate() }}%</span>
</div>
<div class="tax-item">
<label>不含税单价:</label>
<span>¥{{ currentRecord.priceExcludingTax || 0 }}</span>
</div>
</el-col>
<el-col :span="6">
<div class="tax-item">
<label>税额:</label>
<span>¥{{ currentRecord.taxAmount || 0 }}</span>
</div>
<div class="tax-item">
<label>不含税总额:</label>
<span>¥{{ currentRecord.totalExcludingTax || 0 }}</span>
</div>
</el-col>
<el-col :span="6">
<div class="tax-item">
<label>总税额:</label>
<span>¥{{ currentRecord.totalTaxAmount || 0 }}</span>
</div>
<div class="tax-item">
<label>总金额:</label>
<span class="total-amount">¥{{ currentRecord.totalAmount || 0 }}</span>
</div>
</el-col>
</el-row>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveRecord">确定</el-button>
</div>
</el-dialog>
<!-- 批量导入对话框 -->
<el-dialog title="批量导入" :visible.sync="importDialogVisible" width="500px">
<el-upload
class="upload-demo"
drag
action="/api/inbound/import"
:on-success="handleImportSuccess"
:on-error="handleImportError"
:before-upload="beforeUpload"
accept=".xlsx,.xls">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">只能上传xlsx/xls文件,且不超过10MB</div>
</el-upload>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'InboundManagement',
data() {
return {
inboundRecords: [],
selectedRecords: [],
loading: false,
dialogVisible: false,
importDialogVisible: false,
dialogTitle: '新增入库记录',
currentRecord: {},
searchForm: {
sparePartName: '',
sparePartModel: '',
status: '',
storageLocation: ''
},
currentPage: 1,
pageSize: 20,
total: 0,
storageLocations: ['A区-01', 'A区-02', 'A区-03', 'B区-01', 'B区-02', 'B区-03', 'C区-01', 'C区-02'],
partCategories: ['电气设备', '机械设备', '电子元件', '传感器', '控制器', '其他'],
rules: {
sparePartName: [{ required: true, message: '请输入备件名称', trigger: 'blur' }],
sparePartModel: [{ required: true, message: '请输入备件型号', trigger: 'blur' }],
sparePartStatus: [{ required: true, message: '请选择备件状态', trigger: 'change' }],
sparePartType: [{ required: true, message: '请选择备件类型', trigger: 'change' }],
unitPrice: [{ required: true, message: '请输入单价', trigger: 'blur' }],
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
storageLocation: [{ required: true, message: '请选择库位', trigger: 'change' }],
invoiceType: [{ required: true, message: '请选择票据类型', trigger: 'change' }],
inboundPerson: [{ required: true, message: '请输入入库人', trigger: 'blur' }]
}
}
},
mounted() {
this.loadInboundRecords()
},
methods: {
// 加载入库记录
async loadInboundRecords() {
this.loading = true
try {
const response = await axios.get('http://localhost:8081/api/inbound')
this.inboundRecords = response.data
this.total = response.data.length
} catch (error) {
this.$message.error('加载数据失败')
console.error(error)
} finally {
this.loading = false
}
},
// 搜索记录
async searchRecords() {
if (!this.searchForm.sparePartName && !this.searchForm.sparePartModel &&
!this.searchForm.status && !this.searchForm.storageLocation) {
this.loadInboundRecords()
return
}
this.loading = true
try {
const keyword = this.searchForm.sparePartName || this.searchForm.sparePartModel || ''
const response = await axios.get(`http://localhost:8081/api/inbound/search?keyword=${keyword}`)
let records = response.data
// 前端过滤
if (this.searchForm.status) {
records = records.filter(record => record.status === this.searchForm.status)
}
if (this.searchForm.storageLocation) {
records = records.filter(record => record.storageLocation === this.searchForm.storageLocation)
}
this.inboundRecords = records
this.total = records.length
} catch (error) {
this.$message.error('搜索失败')
console.error(error)
} finally {
this.loading = false
}
},
// 重置搜索
resetSearch() {
this.searchForm = {
sparePartName: '',
sparePartModel: '',
status: '',
storageLocation: ''
}
this.loadInboundRecords()
},
// 显示新增对话框
showAddDialog() {
this.dialogTitle = '新增入库记录'
this.currentRecord = {
sparePartStatus: '新好件',
sparePartType: '正常件',
invoiceType: '专票',
quantity: 1,
unitPrice: 0
}
this.dialogVisible = true
},
// 编辑记录
editRecord(record) {
this.dialogTitle = '编辑入库记录'
this.currentRecord = { ...record }
this.dialogVisible = true
},
// 查看记录详情
viewRecord(record) {
const details = `
<div style="text-align: left;">
<p><strong>备件名称:</strong> ${record.sparePartName}</p>
<p><strong>备件型号:</strong> ${record.sparePartModel}</p>
<p><strong>备件分类:</strong> ${record.sparePartCategory || '未分类'}</p>
<p><strong>备件状态:</strong> ${record.sparePartStatus}</p>
<p><strong>备件类型:</strong> ${record.sparePartType}</p>
<p><strong>数量:</strong> ${record.quantity}</p>
<p><strong>单价:</strong> ¥${record.unitPrice}</p>
<p><strong>票据类型:</strong> ${record.invoiceType}</p>
<p><strong>税率:</strong> ${(record.taxRate * 100).toFixed(1)}%</p>
<p><strong>不含税单价:</strong> ¥${record.priceExcludingTax}</p>
<p><strong>税额:</strong> ¥${record.taxAmount}</p>
<p><strong>不含税总额:</strong> ¥${record.totalExcludingTax}</p>
<p><strong>总税额:</strong> ¥${record.totalTaxAmount}</p>
<p><strong>总金额:</strong> ¥${record.totalAmount}</p>
<p><strong>库位:</strong> ${record.storageLocation}</p>
<p><strong>供应商:</strong> ${record.supplier}</p>
<p><strong>SN号:</strong> ${record.snNumber || '无'}</p>
<p><strong>入库人:</strong> ${record.inboundPerson}</p>
<p><strong>入库时间:</strong> ${this.formatDate(record.inboundDate)}</p>
<p><strong>状态:</strong> ${record.status}</p>
<p><strong>备注:</strong> ${record.remarks || '无'}</p>
</div>
`
this.$alert(details, '入库记录详情', {
dangerouslyUseHTMLString: true,
confirmButtonText: '确定'
})
},
// 保存记录
async saveRecord() {
this.$refs.recordForm.validate(async (valid) => {
if (valid) {
try {
if (this.currentRecord.id) {
await axios.put(`http://localhost:8081/api/inbound/${this.currentRecord.id}`, this.currentRecord)
this.$message.success('更新成功')
} else {
await axios.post('http://localhost:8081/api/inbound', this.currentRecord)
this.$message.success('创建成功')
}
this.dialogVisible = false
this.loadInboundRecords()
} catch (error) {
this.$message.error('保存失败')
console.error(error)
}
}
})
},
// 审核记录
async approveRecord(record) {
this.$prompt('请输入审核人姓名', '审核确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /.+/,
inputErrorMessage: '审核人姓名不能为空'
}).then(async ({ value }) => {
try {
await axios.put(`http://localhost:8081/api/inbound/${record.id}/approve?approver=${value}`)
this.$message.success('审核成功')
this.loadInboundRecords()
} catch (error) {
this.$message.error('审核失败')
console.error(error)
}
})
},
// 驳回记录
async rejectRecord(record) {
this.$prompt('请输入驳回原因', '驳回确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /.+/,
inputErrorMessage: '驳回原因不能为空'
}).then(async ({ value }) => {
try {
await axios.put(`http://localhost:8081/api/inbound/${record.id}/reject?reason=${value}`)
this.$message.success('驳回成功')
this.loadInboundRecords()
} catch (error) {
this.$message.error('驳回失败')
console.error(error)
}
})
},
// 批量审核
async batchApprove() {
if (this.selectedRecords.length === 0) {
this.$message.warning('请选择要审核的记录')
return
}
this.$prompt('请输入审核人姓名', '批量审核', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /.+/,
inputErrorMessage: '审核人姓名不能为空'
}).then(async ({ value }) => {
try {
const ids = this.selectedRecords.map(record => record.id)
await axios.put('http://localhost:8081/api/inbound/batch-approve', {
ids: ids,
approver: value
})
this.$message.success('批量审核成功')
this.loadInboundRecords()
} catch (error) {
this.$message.error('批量审核失败')
console.error(error)
}
})
},
// 批量驳回
batchReject() {
if (this.selectedRecords.length === 0) {
this.$message.warning('请选择要驳回的记录')
return
}
this.$prompt('请输入驳回原因', '批量驳回', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /.+/,
inputErrorMessage: '驳回原因不能为空'
}).then(async ({ value }) => {
try {
const ids = this.selectedRecords.map(record => record.id)
await axios.put('http://localhost:8081/api/inbound/batch-reject', {
ids: ids,
reason: value
})
this.$message.success('批量驳回成功')
this.loadInboundRecords()
} catch (error) {
this.$message.error('批量驳回失败')
console.error(error)
}
})
},
// 下载模板
downloadTemplate() {
// 创建模板数据
const templateData = [
['备件名称', '备件型号', '备件分类', '备件状态', '备件类型', '单价', '数量', '票据类型', '库位', '供应商', '入库人', '备注'],
['示例备件', 'MODEL001', '电气设备', '新好件', '正常件', '100.00', '10', '专票', 'A区-01', '供应商A', '张三', '示例备注']
]
// 这里应该调用后端API生成Excel文件
this.$message.info('模板下载功能待实现')
},
// 批量导入
importData() {
this.importDialogVisible = true
},
// 上传前验证
beforeUpload(file) {
const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.type === 'application/vnd.ms-excel'
const isLt10M = file.size / 1024 / 1024 < 10
if (!isExcel) {
this.$message.error('只能上传Excel文件!')
return false
}
if (!isLt10M) {
this.$message.error('上传文件大小不能超过10MB!')
return false
}
return true
},
// 导入成功
handleImportSuccess(response) {
this.$message.success('导入成功')
this.importDialogVisible = false
this.loadInboundRecords()
},
// 导入失败
handleImportError(error) {
this.$message.error('导入失败')
console.error(error)
},
// 计算税额
calculateAmounts() {
if (!this.currentRecord.unitPrice || !this.currentRecord.quantity || !this.currentRecord.invoiceType) {
return
}
const unitPrice = parseFloat(this.currentRecord.unitPrice)
const quantity = parseInt(this.currentRecord.quantity)
const taxRate = this.getTaxRate() / 100
// 计算不含税单价
const priceExcludingTax = unitPrice / (1 + taxRate)
this.currentRecord.priceExcludingTax = priceExcludingTax.toFixed(2)
// 计算税额
const taxAmount = unitPrice - priceExcludingTax
this.currentRecord.taxAmount = taxAmount.toFixed(2)
// 计算不含税总额
const totalExcludingTax = priceExcludingTax * quantity
this.currentRecord.totalExcludingTax = totalExcludingTax.toFixed(2)
// 计算总税额
const totalTaxAmount = taxAmount * quantity
this.currentRecord.totalTaxAmount = totalTaxAmount.toFixed(2)
// 计算总金额
const totalAmount = unitPrice * quantity
this.currentRecord.totalAmount = totalAmount.toFixed(2)
// 设置税率
this.currentRecord.taxRate = taxRate
},
// 获取税率
getTaxRate() {
if (this.currentRecord.invoiceType === '专票') {
return 13
} else if (this.currentRecord.invoiceType === '普票') {
return 3
}
return 0
},
// 选择变化
handleSelectionChange(selection) {
this.selectedRecords = selection
},
// 分页大小变化
handleSizeChange(val) {
this.pageSize = val
this.loadInboundRecords()
},
// 当前页变化
handleCurrentChange(val) {
this.currentPage = val
this.loadInboundRecords()
},
// 获取状态类型
getStatusType(status) {
const statusMap = {
'新好件': 'success',
'修好件': 'success',
'坏件': 'danger',
'二级修': 'warning',
'返厂修': 'warning',
'待调拨': 'info',
'待报废': 'danger',
'已报废': 'danger'
}
return statusMap[status] || 'info'
},
// 获取记录状态类型
getRecordStatusType(status) {
const statusMap = {
'待入库': 'warning',
'已入库': 'success',
'已驳回': 'danger'
}
return statusMap[status] || 'info'
},
// 格式化日期
formatDate(dateString) {
if (!dateString) return '未入库'
return new Date(dateString).toLocaleString('zh-CN')
}
}
}
</script>
<style scoped>
.inbound-management {
padding: 20px;
background-color: #f5f5f5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h2 {
margin: 0;
color: #303133;
}
.actions {
display: flex;
gap: 10px;
}
.search-area {
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.el-table {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.pagination {
margin-top: 20px;
text-align: right;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.tax-info {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
border: 1px solid #e9ecef;
}
.tax-info h4 {
margin: 0 0 15px 0;
color: #495057;
}
.tax-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
padding: 5px 0;
}
.tax-item label {
font-weight: bold;
color: #6c757d;
}
.tax-item span {
color: #495057;
}
.total-amount {
font-weight: bold;
color: #28a745 !important;
font-size: 16px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.dialog-footer {
text-align: right;
}
.upload-demo {
text-align: center;
}
</style>