feat(web-admin): add qc material month import template UI

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-19 12:50:37 +08:00
1 parent e4dcb43d77
commit 6957952b4c
4 files changed
+764 -1

No files matched your search

@@ -0,0 +1,83 @@
import request from '@/utils/http'
export interface QcMonthTemplateRow {
id: string
materialId?: string
materialCode: string
materialName: string
specModel?: string
unit?: string
catalogNo: string
batchNo: string
manufacturer: string
unitPrice: number
amount: number
inQty: number
expireDate: string
orderNum?: number
remark?: string
}
export interface QcMonthImportPreviewLine {
templateId?: string
materialId?: string
materialCode: string
materialName: string
specModel?: string
unit?: string
stockInNo: string
stockInDate: string
expireDate: string
catalogNo: string
batchNo: string
manufacturer: string
unitPrice: number
amount: number
qty: number
remark?: string
}
export function fetchQcMonthTemplateList() {
return request.get<QcMonthTemplateRow[]>({
url: '/web/admin/qc-material-month-templates'
})
}
export function fetchCreateQcMonthTemplate(data: Record<string, any>) {
return request.post<QcMonthTemplateRow>({
url: '/web/admin/qc-material-month-templates',
data
})
}
export function fetchUpdateQcMonthTemplate(id: string, data: Record<string, any>) {
return request.put<QcMonthTemplateRow>({
url: `/web/admin/qc-material-month-templates/${id}`,
data
})
}
export function fetchDeleteQcMonthTemplate(id: string) {
return request.delete<void>({
url: `/web/admin/qc-material-month-templates/${id}`
})
}
export function fetchPreviewQcMonthImport(stockInDate: string) {
return request.post<QcMonthImportPreviewLine[]>({
url: '/web/admin/qc-material-month-templates/preview',
data: { stockInDate }
})
}
export function fetchConfirmQcMonthImport(data: {
stockInDate: string
items: QcMonthImportPreviewLine[]
remark?: string
stockInTime?: string
}) {
return request.post<{ count: number }>({
url: '/web/admin/qc-material-month-templates/confirm',
data
})
}
@@ -0,0 +1,261 @@
<!-- 质检物料库 · 月初导入预览确认 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<el-dialog
:model-value="modelValue"
title="月初导入"
width="1080px"
class="dialog-form-layout material-batch-stock-dialog"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
@open="handleOpen"
>
<el-form label-position="top">
<div class="form-grid-2 batch-stock-move-head">
<el-form-item label="本次入库日期">
<el-date-picker
v-model="stockInDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择入库日期"
style="width: 100%"
@change="reloadPreview"
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="remark"
type="textarea"
:rows="2"
placeholder="可选,未填写行备注时将作为默认备注"
/>
</el-form-item>
</div>
</el-form>
<div class="batch-stock-move-lines-head" style="margin-bottom: 12px">
<div class="batch-stock-move-lines-head-left">
<span class="batch-stock-move-lines-title">入库明细</span>
<span class="batch-stock-move-lines-hint">
共 {{ lines.length }} 项,可改字段后确认;删除行仅影响本次
</span>
</div>
</div>
<div v-loading="loading">
<div v-if="lines.length" class="batch-in-line-list">
<div v-for="(row, index) in lines" :key="index" class="batch-in-line-card">
<div class="batch-in-line-card-head">
<span class="batch-in-line-card-index">{{ index + 1 }}</span>
<el-button link type="danger" size="small" @click="lines.splice(index, 1)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
<div class="batch-in-line-grid batch-in-line-card-body">
<el-form-item label="物料" class="batch-in-col-span-2">
<el-input
:model-value="`${row.materialName}(${row.materialCode})`"
size="small"
disabled
/>
</el-form-item>
<el-form-item label="入库编号">
<el-input v-model="row.stockInNo" size="small" placeholder="必填" />
</el-form-item>
<el-form-item label="过期日期">
<el-date-picker
v-model="row.expireDate"
type="date"
size="small"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="货号">
<el-input v-model="row.catalogNo" size="small" />
</el-form-item>
<el-form-item label="批号">
<el-input v-model="row.batchNo" size="small" />
</el-form-item>
<el-form-item label="厂家">
<el-input v-model="row.manufacturer" size="small" />
</el-form-item>
<el-form-item label="数量">
<el-input-number
v-model="row.qty"
size="small"
:min="0.0001"
:precision="4"
controls-position="right"
style="width: 100%"
@change="() => recalcAmount(row)"
/>
</el-form-item>
<el-form-item label="单价">
<el-input-number
v-model="row.unitPrice"
size="small"
:min="0"
:precision="2"
controls-position="right"
style="width: 100%"
@change="() => recalcAmount(row)"
/>
</el-form-item>
<el-form-item label="金额">
<el-input-number
v-model="row.amount"
size="small"
:min="0"
:precision="2"
controls-position="right"
style="width: 100%"
@change="() => markAmountTouched(row)"
/>
</el-form-item>
<el-form-item label="备注" class="batch-in-col-span-2">
<el-input v-model="row.remark" size="small" placeholder="可选" />
</el-form-item>
</div>
</div>
</div>
<el-empty v-else description="暂无预览明细" :image-size="56" />
</div>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleConfirm">
<el-icon><Check /></el-icon>
确认入库
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { Check, Close, Delete } from '@element-plus/icons-vue'
import {
fetchConfirmQcMonthImport,
fetchPreviewQcMonthImport,
type QcMonthImportPreviewLine
} from '@/api/qc-material-month-template'
defineOptions({ name: 'QcMaterialMonthImportDialog' })
defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
(e: 'success'): void
}>()
const loading = ref(false)
const submitting = ref(false)
const stockInDate = ref('')
const remark = ref('')
const lines = ref<QcMonthImportPreviewLine[]>([])
const amountTouchedMap = ref<Record<number, boolean>>({})
function todayStr() {
const d = new Date()
const m = `${d.getMonth() + 1}`.padStart(2, '0')
const day = `${d.getDate()}`.padStart(2, '0')
return `${d.getFullYear()}-${m}-${day}`
}
async function handleOpen() {
stockInDate.value = todayStr()
remark.value = ''
amountTouchedMap.value = {}
await reloadPreview()
}
async function reloadPreview() {
if (!stockInDate.value) {
ElMessage.warning('请选择入库日期')
return
}
loading.value = true
try {
lines.value = (await fetchPreviewQcMonthImport(stockInDate.value)) || []
amountTouchedMap.value = {}
} catch {
lines.value = []
} finally {
loading.value = false
}
}
function markAmountTouched(row: QcMonthImportPreviewLine) {
const idx = lines.value.indexOf(row)
if (idx >= 0) amountTouchedMap.value[idx] = true
}
function recalcAmount(row: QcMonthImportPreviewLine) {
const idx = lines.value.indexOf(row)
if (idx >= 0 && amountTouchedMap.value[idx]) return
const price = Number(row.unitPrice) || 0
const qty = Number(row.qty) || 0
row.amount = Number((price * qty).toFixed(2))
}
async function handleConfirm() {
if (!stockInDate.value) {
ElMessage.warning('请选择入库日期')
return
}
if (!lines.value.length) {
ElMessage.warning('请至少保留一条入库明细')
return
}
for (let i = 0; i < lines.value.length; i++) {
const line = lines.value[i]
if (!line.stockInNo?.trim()) {
ElMessage.warning(`第 ${i + 1} 行请填写入库编号`)
return
}
if (!line.expireDate) {
ElMessage.warning(`第 ${i + 1} 行请选择过期日期`)
return
}
if (!line.catalogNo?.trim() || !line.batchNo?.trim() || !line.manufacturer?.trim()) {
ElMessage.warning(`第 ${i + 1} 行货号/批号/厂家不能为空`)
return
}
if (!(Number(line.qty) > 0)) {
ElMessage.warning(`第 ${i + 1} 行数量必须大于0`)
return
}
}
submitting.value = true
try {
const res = await fetchConfirmQcMonthImport({
stockInDate: stockInDate.value,
remark: remark.value?.trim() || undefined,
items: lines.value.map((l) => ({
...l,
materialId: l.materialId || undefined,
stockInNo: l.stockInNo.trim(),
catalogNo: l.catalogNo.trim(),
batchNo: l.batchNo.trim(),
manufacturer: l.manufacturer.trim(),
qty: Number(l.qty),
unitPrice: Number(l.unitPrice),
amount: Number(l.amount)
}))
})
ElMessage.success(`月初导入成功,共 ${res?.count ?? lines.value.length} 项`)
emit('update:modelValue', false)
emit('success')
} finally {
submitting.value = false
}
}
</script>
@@ -0,0 +1,379 @@
<!-- 质检物料库 · 维护月初入库模板 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="qc-month-template-dialogs">
<el-dialog
:model-value="modelValue"
title="维护月初模板"
width="1080px"
class="dialog-form-layout material-batch-stock-dialog"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
@open="handleOpen"
>
<div class="batch-stock-move-lines-head" style="margin-bottom: 12px">
<div class="batch-stock-move-lines-head-left">
<span class="batch-stock-move-lines-title">模板明细</span>
<span class="batch-stock-move-lines-hint">共 {{ rows.length }} 项,保存后供「月初导入」使用</span>
</div>
<el-button class="btn-ghost" @click="openEdit()">
<el-icon><Plus /></el-icon>
新增一行
</el-button>
</div>
<el-table v-loading="loading" :data="rows" class="list-table" stripe max-height="420">
<el-table-column label="物料编码" prop="materialCode" min-width="120" show-overflow-tooltip />
<el-table-column label="物料名称" prop="materialName" min-width="140" show-overflow-tooltip />
<el-table-column label="货号" prop="catalogNo" min-width="100" show-overflow-tooltip />
<el-table-column label="批号" prop="batchNo" min-width="100" show-overflow-tooltip />
<el-table-column label="厂家" prop="manufacturer" min-width="120" show-overflow-tooltip />
<el-table-column label="数量" prop="inQty" min-width="80" align="right" />
<el-table-column label="过期日期" prop="expireDate" min-width="110" />
<el-table-column label="操作" fixed="right" min-width="160">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无模板,请点击「新增一行」" :image-size="64" />
</template>
</el-table>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>
关闭
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="editVisible"
:title="editForm.id ? '编辑模板行' : '新增模板行'"
width="720px"
class="dialog-form-layout"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-position="top"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="关联物资" class="form-col-span-2">
<el-select
v-model="editForm.materialId"
filterable
clearable
placeholder="可选;不选则可手填编码名称,导入时自动创建"
style="width: 100%"
:loading="materialOptionsLoading"
@change="onMaterialChange"
>
<el-option
v-for="opt in materialOptions"
:key="opt.id"
:label="`${opt.materialName}(${opt.materialCode})`"
:value="opt.id"
/>
</el-select>
</el-form-item>
<el-form-item label="物料编码" prop="materialCode">
<el-input v-model="editForm.materialCode" placeholder="必填" />
</el-form-item>
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="editForm.materialName" placeholder="必填" />
</el-form-item>
<el-form-item label="规格型号">
<el-input v-model="editForm.specModel" placeholder="可选" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="editForm.unit" placeholder="可选" />
</el-form-item>
<el-form-item label="货号" prop="catalogNo">
<el-input v-model="editForm.catalogNo" placeholder="必填" />
</el-form-item>
<el-form-item label="批号" prop="batchNo">
<el-input v-model="editForm.batchNo" placeholder="必填" />
</el-form-item>
<el-form-item label="厂家" prop="manufacturer">
<el-input v-model="editForm.manufacturer" placeholder="必填" />
</el-form-item>
<el-form-item label="过期日期" prop="expireDate">
<el-date-picker
v-model="editForm.expireDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择过期日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="单价(元)" prop="unitPrice">
<el-input-number
v-model="editForm.unitPrice"
:min="0"
:precision="2"
controls-position="right"
style="width: 100%"
@change="onPriceOrQtyChange"
/>
</el-form-item>
<el-form-item label="数量" prop="inQty">
<el-input-number
v-model="editForm.inQty"
:min="0.0001"
:precision="4"
controls-position="right"
style="width: 100%"
@change="onPriceOrQtyChange"
/>
</el-form-item>
<el-form-item label="金额(元)" prop="amount">
<el-input-number
v-model="editForm.amount"
:min="0"
:precision="2"
controls-position="right"
style="width: 100%"
@change="amountTouched = true"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="editForm.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="editVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="saving" @click="handleSave">
<el-icon><Check /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Check, Close, Delete, Edit, Plus } from '@element-plus/icons-vue'
import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
import {
fetchCreateQcMonthTemplate,
fetchDeleteQcMonthTemplate,
fetchQcMonthTemplateList,
fetchUpdateQcMonthTemplate,
type QcMonthTemplateRow
} from '@/api/qc-material-month-template'
defineOptions({ name: 'QcMaterialMonthTemplateDialog' })
defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
(e: 'saved'): void
}>()
const loading = ref(false)
const saving = ref(false)
const rows = ref<QcMonthTemplateRow[]>([])
const materialOptions = ref<QcMaterialOption[]>([])
const materialOptionsLoading = ref(false)
const editVisible = ref(false)
const editFormRef = ref<FormInstance>()
const amountTouched = ref(false)
const editForm = reactive({
id: '',
materialId: '',
materialCode: '',
materialName: '',
specModel: '',
unit: '',
catalogNo: '',
batchNo: '',
manufacturer: '',
unitPrice: 0,
amount: 0,
inQty: 1,
expireDate: '',
remark: ''
})
const editRules: FormRules = {
materialCode: [{ required: true, message: '请输入物料编码', trigger: 'blur' }],
materialName: [{ required: true, message: '请输入物料名称', trigger: 'blur' }],
catalogNo: [{ required: true, message: '请输入货号', trigger: 'blur' }],
batchNo: [{ required: true, message: '请输入批号', trigger: 'blur' }],
manufacturer: [{ required: true, message: '请输入厂家', trigger: 'blur' }],
expireDate: [{ required: true, message: '请选择过期日期', trigger: 'change' }],
unitPrice: [{ required: true, message: '请输入单价', trigger: 'blur' }],
amount: [{ required: true, message: '请输入金额', trigger: 'blur' }],
inQty: [{ required: true, message: '请输入数量', trigger: 'blur' }]
}
async function loadRows() {
loading.value = true
try {
rows.value = (await fetchQcMonthTemplateList()) || []
} catch {
rows.value = []
} finally {
loading.value = false
}
}
async function loadMaterialOptions() {
materialOptionsLoading.value = true
try {
materialOptions.value = (await fetchQcMaterialOptions()) || []
} catch {
materialOptions.value = []
} finally {
materialOptionsLoading.value = false
}
}
async function handleOpen() {
await Promise.all([loadRows(), loadMaterialOptions()])
}
function resetEditForm() {
editForm.id = ''
editForm.materialId = ''
editForm.materialCode = ''
editForm.materialName = ''
editForm.specModel = ''
editForm.unit = ''
editForm.catalogNo = ''
editForm.batchNo = ''
editForm.manufacturer = ''
editForm.unitPrice = 0
editForm.amount = 0
editForm.inQty = 1
editForm.expireDate = ''
editForm.remark = ''
amountTouched.value = false
}
function openEdit(row?: QcMonthTemplateRow) {
resetEditForm()
if (row) {
editForm.id = row.id
editForm.materialId = row.materialId || ''
editForm.materialCode = row.materialCode || ''
editForm.materialName = row.materialName || ''
editForm.specModel = row.specModel || ''
editForm.unit = row.unit || ''
editForm.catalogNo = row.catalogNo || ''
editForm.batchNo = row.batchNo || ''
editForm.manufacturer = row.manufacturer || ''
editForm.unitPrice = Number(row.unitPrice) || 0
editForm.amount = Number(row.amount) || 0
editForm.inQty = Number(row.inQty) || 1
editForm.expireDate = row.expireDate || ''
editForm.remark = row.remark || ''
amountTouched.value = true
}
editVisible.value = true
}
function onMaterialChange(id: string) {
const opt = materialOptions.value.find((o) => o.id === id)
if (!opt) return
editForm.materialId = opt.id
editForm.materialCode = opt.materialCode || ''
editForm.materialName = opt.materialName || ''
editForm.specModel = opt.specModel || ''
editForm.unit = opt.unit || ''
}
function onPriceOrQtyChange() {
if (amountTouched.value) return
const price = Number(editForm.unitPrice) || 0
const qty = Number(editForm.inQty) || 0
editForm.amount = Number((price * qty).toFixed(2))
}
async function handleSave() {
const form = editFormRef.value
if (!form) return
try {
await form.validate()
} catch {
return
}
if (!(Number(editForm.inQty) > 0)) {
ElMessage.warning('数量必须大于0')
return
}
saving.value = true
try {
const payload = {
materialId: editForm.materialId || undefined,
materialCode: editForm.materialCode.trim(),
materialName: editForm.materialName.trim(),
specModel: editForm.specModel?.trim() || undefined,
unit: editForm.unit?.trim() || undefined,
catalogNo: editForm.catalogNo.trim(),
batchNo: editForm.batchNo.trim(),
manufacturer: editForm.manufacturer.trim(),
unitPrice: Number(editForm.unitPrice),
amount: Number(editForm.amount),
inQty: Number(editForm.inQty),
expireDate: editForm.expireDate,
remark: editForm.remark?.trim() || undefined
}
if (editForm.id) {
await fetchUpdateQcMonthTemplate(editForm.id, payload)
ElMessage.success('更新成功')
} else {
await fetchCreateQcMonthTemplate(payload)
ElMessage.success('新增成功')
}
editVisible.value = false
await loadRows()
emit('saved')
} finally {
saving.value = false
}
}
async function handleDelete(row: QcMonthTemplateRow) {
try {
await ElMessageBox.confirm(`确定删除模板「${row.materialName}」吗?`, '删除确认', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消'
})
} catch {
return
}
await fetchDeleteQcMonthTemplate(row.id)
ElMessage.success('删除成功')
await loadRows()
emit('saved')
}
</script>
@@ -88,6 +88,14 @@
<el-icon><CircleClose /></el-icon>
批量废弃
</el-button>
<el-button class="btn-ghost" @click="handleMonthImport">
<el-icon><Calendar /></el-icon>
月初导入
</el-button>
<el-button class="btn-ghost" @click="monthTemplateVisible = true">
<el-icon><Setting /></el-icon>
维护月初模板
</el-button>
<ArtExcelExport
type="default"
:data="batchExportData"
@@ -1213,6 +1221,11 @@
:batch="attachmentBatch"
@saved="loadData"
/>
<QcMaterialMonthTemplateDialog v-model="monthTemplateVisible" />
<QcMaterialMonthImportDialog
v-model="monthImportVisible"
@success="handleMonthImportSuccess"
/>
</div>
<div v-show="activeTab === 'usage'" class="panel-track-pane">
@@ -1242,7 +1255,9 @@
Edit,
Delete,
CircleClose,
Paperclip
Paperclip,
Calendar,
Setting
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import AmountInput from '@/components/common/amount-input/index.vue'
@@ -1269,6 +1284,9 @@
import QcMaterialUsageTab from './QcMaterialUsageTab.vue'
import QcMaterialStockCheckTab from './QcMaterialStockCheckTab.vue'
import QcMaterialBatchAttachmentDialog from './QcMaterialBatchAttachmentDialog.vue'
import QcMaterialMonthTemplateDialog from './QcMaterialMonthTemplateDialog.vue'
import QcMaterialMonthImportDialog from './QcMaterialMonthImportDialog.vue'
import { fetchQcMonthTemplateList } from '@/api/qc-material-month-template'
import { useMenuPerm } from '@/hooks/core/useMenuPerm'
defineOptions({ name: 'QualityInspectionQcMaterial' })
@@ -1279,6 +1297,9 @@
/** 暂时隐藏批量入库入口,恢复时改为 true */
const batchStockInButtonVisible = false
const monthTemplateVisible = ref(false)
const monthImportVisible = ref(false)
const route = useRoute()
const QC_MATERIAL_TABS = [
@@ -2317,6 +2338,25 @@
batchStockInFormRef.value?.clearValidate?.()
}
async function handleMonthImport() {
try {
const list = (await fetchQcMonthTemplateList()) || []
if (!list.length) {
ElMessage.warning('请先维护月初模板')
monthTemplateVisible.value = true
return
}
monthImportVisible.value = true
} catch {
/* request 层已提示 */
}
}
async function handleMonthImportSuccess() {
monthImportVisible.value = false
await loadData()
}
async function handleBatchStockInSubmit() {
await batchStockInFormRef.value?.validate?.()
if (!batchStockInLines.value.length) {