feat(repair): implement frontend API and Vue pages for repair management
This commit is contained in:
53
inventory-web/src/api/inbound/repair.ts
Normal file
53
inventory-web/src/api/inbound/repair.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 1. 获取维修单列表
|
||||||
|
export function getRepairList(params: any) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/repair/list',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 新增维修单
|
||||||
|
export function createRepair(data: any) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/repair/submit',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 更新维修单
|
||||||
|
export function updateRepair(id: number, data: any) {
|
||||||
|
return request({
|
||||||
|
url: `/inbound/repair/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 更新维修状态
|
||||||
|
export function updateRepairStatus(data: any) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/repair/update-status',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 删除维修单
|
||||||
|
export function deleteRepair(id: number) {
|
||||||
|
return request({
|
||||||
|
url: `/inbound/repair/${id}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 获取维修单详情
|
||||||
|
export function getRepairDetail(id: number) {
|
||||||
|
return request({
|
||||||
|
url: `/inbound/repair/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
447
inventory-web/src/views/stock/inbound/repair.vue
Normal file
447
inventory-web/src/views/stock/inbound/repair.vue
Normal file
@ -0,0 +1,447 @@
|
|||||||
|
<template>
|
||||||
|
<div class="repair-container">
|
||||||
|
<!-- 顶部搜索区 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
|
<el-form-item label="维修单号">
|
||||||
|
<el-input v-model="searchForm.repair_no" placeholder="请输入维修单号" clearable style="width: 180px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="SN序列号">
|
||||||
|
<el-input v-model="searchForm.serial_number" placeholder="请输入序列号" clearable style="width: 180px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="物料名称">
|
||||||
|
<el-input v-model="searchForm.material_name" placeholder="请输入物料名称" clearable style="width: 180px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="维修状态">
|
||||||
|
<el-select v-model="searchForm.repair_status" placeholder="请选择状态" clearable style="width: 150px">
|
||||||
|
<el-option label="待检测" value="待检测" />
|
||||||
|
<el-option label="维修中" value="维修中" />
|
||||||
|
<el-option label="等待配件" value="等待配件" />
|
||||||
|
<el-option label="已修复" value="已修复" />
|
||||||
|
<el-option label="报废转出" value="报废转出" />
|
||||||
|
<el-option label="已出库" value="已出库" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 操作按钮区 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button v-if="userStore.hasPermission('inbound_repair:add')" type="primary" :icon="Plus" @click="handleCreate">新增维修</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%">
|
||||||
|
<el-table-column prop="repair_no" label="维修单号" width="180" />
|
||||||
|
<el-table-column prop="material_name" label="物料名称" width="150" />
|
||||||
|
<el-table-column prop="sku" label="规格型号" width="120" />
|
||||||
|
<el-table-column prop="serial_number" label="序列号(SN)" width="150" />
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.repair_status)">{{ row.repair_status || '待检测' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="arrival_date" label="接收时间" width="120" />
|
||||||
|
<el-table-column prop="fault_description" label="故障描述" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button v-if="userStore.hasPermission('inbound_repair:edit')" type="primary" link size="small" @click="handleUpdateStatus(row)">
|
||||||
|
更新状态
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="userStore.hasPermission('inbound_repair:delete')" type="danger" link size="small" @click="handleDelete(row)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 底部分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[20, 50, 100, 200]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="fetchData"
|
||||||
|
@current-change="fetchData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增维修单弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisible" title="新增维修单" width="600px" destroy-on-close :close-on-click-modal="false">
|
||||||
|
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||||
|
<el-form-item label="物料选择" prop="base_id">
|
||||||
|
<el-select
|
||||||
|
v-model="form.base_id"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
placeholder="请输入关键词搜索物料"
|
||||||
|
:remote-method="handleSearchMaterialDebounced"
|
||||||
|
:loading="searchLoading"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="onMaterialSelected"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in materialOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
>
|
||||||
|
<span style="float: left">{{ item.name }}</span>
|
||||||
|
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.company_name }}</span>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="物料名称" prop="material_name">
|
||||||
|
<el-input v-model="form.material_name" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="序列号SN" prop="serial_number">
|
||||||
|
<el-input v-model="form.serial_number" placeholder="请输入序列号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="来源类型" prop="source_table">
|
||||||
|
<el-select v-model="form.source_table" placeholder="请选择来源类型" style="width: 100%">
|
||||||
|
<el-option label="采购入库" value="stock_buy" />
|
||||||
|
<el-option label="成品入库" value="stock_product" />
|
||||||
|
<el-option label="半成品入库" value="stock_semi" />
|
||||||
|
<el-option label="独立录入" value="independent" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="接收时间" prop="arrival_date">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.arrival_date"
|
||||||
|
type="date"
|
||||||
|
placeholder="选择接收时间"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="故障描述" prop="fault_description">
|
||||||
|
<el-input v-model="form.fault_description" type="textarea" :rows="3" placeholder="请输入客户反馈的故障描述" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="维修人" prop="repair_manager">
|
||||||
|
<el-input v-model="form.repair_manager" placeholder="请输入维修人" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="客户/来源" prop="related_contract_id">
|
||||||
|
<el-input v-model="form.related_contract_id" placeholder="请输入客户名称或来源" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="成本价" prop="cost_price">
|
||||||
|
<el-input-number v-model="form.cost_price" :precision="2" :min="0" :controls="false" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="销售价" prop="sale_price">
|
||||||
|
<el-input-number v-model="form.sale_price" :precision="2" :min="0" :controls="false" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 更新维修状态弹窗 -->
|
||||||
|
<el-dialog v-model="statusDialogVisible" title="更新维修状态" width="500px" destroy-on-close :close-on-click-modal="false">
|
||||||
|
<el-form ref="statusFormRef" :model="statusForm" :rules="statusFormRules" label-width="100px">
|
||||||
|
<el-form-item label="维修单号">
|
||||||
|
<el-input :value="statusForm.repair_no" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="当前状态">
|
||||||
|
<el-tag :type="getStatusType(statusForm.repair_status)">{{ statusForm.repair_status }}</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新状态" prop="status">
|
||||||
|
<el-select v-model="statusForm.status" placeholder="请选择新状态" style="width: 100%">
|
||||||
|
<el-option label="待检测" value="待检测" />
|
||||||
|
<el-option label="维修中" value="维修中" />
|
||||||
|
<el-option label="等待配件" value="等待配件" />
|
||||||
|
<el-option label="已修复" value="已修复" />
|
||||||
|
<el-option label="报废转出" value="报废转出" />
|
||||||
|
<el-option label="已出库" value="已出库" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="追加日志" prop="repair_log">
|
||||||
|
<el-input v-model="statusForm.repair_log" type="textarea" :rows="4" placeholder="请输入维修日志或备注" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="statusDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="statusSubmitLoading" @click="handleStatusSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, ElMessageBox, ElFormRules } from 'element-plus'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { getRepairList, createRepair, updateRepairStatus, deleteRepair } from '@/api/inbound/repair'
|
||||||
|
import { searchMaterialBase } from '@/api/inbound/buy'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
repair_no: '',
|
||||||
|
serial_number: '',
|
||||||
|
material_name: '',
|
||||||
|
repair_status: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 新增弹窗
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const form = reactive({
|
||||||
|
base_id: null as number | null,
|
||||||
|
material_name: '',
|
||||||
|
serial_number: '',
|
||||||
|
source_table: 'independent',
|
||||||
|
arrival_date: '',
|
||||||
|
fault_description: '',
|
||||||
|
repair_manager: '',
|
||||||
|
related_contract_id: '',
|
||||||
|
cost_price: undefined as number | undefined,
|
||||||
|
sale_price: undefined as number | undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// 物料搜索
|
||||||
|
const materialOptions = ref<any[]>([])
|
||||||
|
const searchLoading = ref(false)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
|
// 表单校验
|
||||||
|
const formRules: ElFormRules = [
|
||||||
|
{ required: true, message: '请选择物料', trigger: 'change', field: 'base_id' },
|
||||||
|
{ required: true, message: '请输入序列号', trigger: 'blur', field: 'serial_number' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 状态更新弹窗
|
||||||
|
const statusDialogVisible = ref(false)
|
||||||
|
const statusSubmitLoading = ref(false)
|
||||||
|
const statusFormRef = ref()
|
||||||
|
const statusForm = reactive({
|
||||||
|
id: 0,
|
||||||
|
repair_no: '',
|
||||||
|
repair_status: '',
|
||||||
|
status: '',
|
||||||
|
repair_log: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusFormRules: ElFormRules = [
|
||||||
|
{ required: true, message: '请选择新状态', trigger: 'change', field: 'status' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 状态颜色映射
|
||||||
|
const getStatusType = (status: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'待检测': 'info',
|
||||||
|
'维修中': 'warning',
|
||||||
|
'等待配件': 'warning',
|
||||||
|
'已修复': 'success',
|
||||||
|
'报废转出': 'danger',
|
||||||
|
'已出库': 'success'
|
||||||
|
}
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
const res = await getRepairList(params)
|
||||||
|
if (res.code === 200) {
|
||||||
|
tableData.value = res.data.list || []
|
||||||
|
pagination.total = res.data.total || 0
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.repair_no = ''
|
||||||
|
searchForm.serial_number = ''
|
||||||
|
searchForm.material_name = ''
|
||||||
|
searchForm.repair_status = ''
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物料搜索防抖
|
||||||
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const handleSearchMaterialDebounced = (query: string) => {
|
||||||
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
handleSearchMaterial(query)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearchMaterial = async (query: string) => {
|
||||||
|
if (!query) return
|
||||||
|
searchLoading.value = true
|
||||||
|
searchKeyword.value = query
|
||||||
|
try {
|
||||||
|
const res: any = await searchMaterialBase(query, 1)
|
||||||
|
if (res.data) {
|
||||||
|
materialOptions.value = res.data || []
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
searchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物料选中
|
||||||
|
const onMaterialSelected = (val: number) => {
|
||||||
|
const item = materialOptions.value.find(i => i.id === val)
|
||||||
|
if (item) {
|
||||||
|
form.material_name = item.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
const handleCreate = () => {
|
||||||
|
// 重置表单
|
||||||
|
form.base_id = null
|
||||||
|
form.material_name = ''
|
||||||
|
form.serial_number = ''
|
||||||
|
form.source_table = 'independent'
|
||||||
|
form.arrival_date = ''
|
||||||
|
form.fault_description = ''
|
||||||
|
form.repair_manager = ''
|
||||||
|
form.related_contract_id = ''
|
||||||
|
form.cost_price = undefined
|
||||||
|
form.sale_price = undefined
|
||||||
|
materialOptions.value = []
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交新增
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
await formRef.value.validate()
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await createRepair(form)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '新增失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const handleUpdateStatus = (row: any) => {
|
||||||
|
statusForm.id = row.id
|
||||||
|
statusForm.repair_no = row.repair_no
|
||||||
|
statusForm.repair_status = row.repair_status || '待检测'
|
||||||
|
statusForm.status = ''
|
||||||
|
statusForm.repair_log = ''
|
||||||
|
statusDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交状态更新
|
||||||
|
const handleStatusSubmit = async () => {
|
||||||
|
if (!statusFormRef.value) return
|
||||||
|
await statusFormRef.value.validate()
|
||||||
|
|
||||||
|
statusSubmitLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await updateRepairStatus({
|
||||||
|
id: statusForm.id,
|
||||||
|
status: statusForm.status,
|
||||||
|
repair_log: statusForm.repair_log
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('状态更新成功')
|
||||||
|
statusDialogVisible.value = false
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '更新失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
statusSubmitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = (row: any) => {
|
||||||
|
ElMessageBox.confirm(`确定要删除维修单 ${row.repair_no} 吗?`, '警告', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}).then(async () => {
|
||||||
|
const res = await deleteRepair(row.id)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '删除失败')
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.repair-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user