feat(repair): decouple material base, sync global sku sequence and add scan/print features
This commit is contained in:
@ -37,18 +37,23 @@
|
||||
<!-- 数据表格 -->
|
||||
<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="sku" label="全局SKU(系统条码)" width="140" />
|
||||
<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 prop="customer_name" label="客户名称" width="120" />
|
||||
<el-table-column prop="customer_location" label="所在地" width="150" show-overflow-tooltip />
|
||||
<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">
|
||||
<el-table-column prop="fault_description" label="故障描述" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="userStore.hasPermission('inbound_repair:edit')" type="warning" link size="small" @click="handlePrint(row)">
|
||||
<el-icon><Printer /></el-icon> 打印
|
||||
</el-button>
|
||||
<el-button v-if="userStore.hasPermission('inbound_repair:edit')" type="primary" link size="small" @click="handleUpdateStatus(row)">
|
||||
更新状态
|
||||
</el-button>
|
||||
@ -73,70 +78,74 @@
|
||||
</div>
|
||||
|
||||
<!-- 新增维修单弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" title="新增维修单" width="600px" destroy-on-close :close-on-click-modal="false">
|
||||
<el-dialog v-model="dialogVisible" title="新增维修单" width="650px" 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-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="物料名称" prop="material_name">
|
||||
<el-input v-model="form.material_name" placeholder="请输入物料名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="序列号SN" prop="serial_number">
|
||||
<el-input v-model="form.serial_number" placeholder="请输入或扫描序列号">
|
||||
<template #append>
|
||||
<el-button :icon="Camera" @click="openScanner" title="智能扫码" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户名称" prop="customer_name">
|
||||
<el-input v-model="form.customer_name" placeholder="请输入客户名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所在地" prop="customer_location">
|
||||
<el-input v-model="form.customer_location" placeholder="请输入客户所在地" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<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-col :span="8">
|
||||
<el-form-item label="维修人" prop="repair_manager">
|
||||
<el-input v-model="form.repair_manager" placeholder="请输入维修人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<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-col :span="8">
|
||||
<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>
|
||||
@ -177,16 +186,40 @@
|
||||
<el-button type="primary" :loading="statusSubmitLoading" @click="handleStatusSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 智能扫码弹窗 -->
|
||||
<SmartScannerDialog v-model="scannerDialogVisible" @confirm="handleScannerConfirm" />
|
||||
|
||||
<!-- 打印预览弹窗 -->
|
||||
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
|
||||
<div v-loading="printLoading" class="preview-box">
|
||||
<img v-if="previewUrl" :src="previewUrl" alt="打印预览" style="width: 100%" />
|
||||
</div>
|
||||
<p>打印机 IP: 192.168.9.205</p>
|
||||
<div style="margin: 15px 0;">
|
||||
<span style="font-weight: bold; color: #303133;">打印份数:</span>
|
||||
<el-input-number v-model="printCopies" :min="1" :max="100" size="default" style="width: 120px; margin-left: 10px;" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="printVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="printing" @click="confirmPrint">
|
||||
<el-icon><Printer /></el-icon>确认打印
|
||||
</el-button>
|
||||
</div>
|
||||
</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 { Plus, Search, Refresh, Printer, Camera } 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'
|
||||
import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||
import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
@ -212,26 +245,21 @@ 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: '',
|
||||
customer_name: '',
|
||||
customer_location: '',
|
||||
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: 'material_name' },
|
||||
{ required: true, message: '请输入序列号', trigger: 'blur', field: 'serial_number' }
|
||||
]
|
||||
|
||||
@ -251,6 +279,26 @@ const statusFormRules: ElFormRules = [
|
||||
{ required: true, message: '请选择新状态', trigger: 'change', field: 'status' }
|
||||
]
|
||||
|
||||
// 智能扫码
|
||||
const scannerDialogVisible = ref(false)
|
||||
|
||||
const openScanner = () => {
|
||||
scannerDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleScannerConfirm = (result: string) => {
|
||||
form.serial_number = result
|
||||
scannerDialogVisible.value = false
|
||||
}
|
||||
|
||||
// 打印相关
|
||||
const printVisible = ref(false)
|
||||
const printLoading = ref(false)
|
||||
const printing = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const printCopies = ref(1)
|
||||
const currentPrintData = ref<any>({})
|
||||
|
||||
// 状态颜色映射
|
||||
const getStatusType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
@ -298,51 +346,19 @@ const handleReset = () => {
|
||||
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.customer_name = ''
|
||||
form.customer_location = ''
|
||||
form.repair_manager = ''
|
||||
form.related_contract_id = ''
|
||||
form.cost_price = undefined
|
||||
form.sale_price = undefined
|
||||
materialOptions.value = []
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@ -417,6 +433,43 @@ const handleDelete = (row: any) => {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 打印标签
|
||||
const handlePrint = async (row: any) => {
|
||||
printVisible.value = true
|
||||
printLoading.value = true
|
||||
printCopies.value = 1
|
||||
|
||||
currentPrintData.value = {
|
||||
sku: row.sku,
|
||||
material_name: row.material_name,
|
||||
serial_number: row.serial_number,
|
||||
repair_no: row.repair_no
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await getLabelPreview(currentPrintData.value)
|
||||
previewUrl.value = res.data
|
||||
} catch (e) {
|
||||
ElMessage.error('预览失败')
|
||||
} finally {
|
||||
printLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 确认打印
|
||||
const confirmPrint = async () => {
|
||||
printing.value = true
|
||||
try {
|
||||
await executePrint({ ...currentPrintData.value, copies: printCopies.value })
|
||||
ElMessage.success(`打印指令已发送 (x${printCopies.value})`)
|
||||
printVisible.value = false
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.msg || '打印失败')
|
||||
} finally {
|
||||
printing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
@ -444,4 +497,19 @@ onMounted(() => {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.preview-box {
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f7fa;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user