feat: 采购到货单、库区标签打印、EBOM表格优化

- 采购到货单: 新建 arrivalnotice API,重构 Checkin 页面对齐到货通知功能
- 库区标签打印: 新增 LocationLabelPrint 组件,支持单个/批量打印(4列排版)
- EBOM: 表格表头和内容不换行,过长截断显示

Made-with: Cursor
This commit is contained in:
panchengyong
2026-03-14 14:58:17 +08:00
parent 7aee39d98e
commit dcb77279a0
6 changed files with 1041 additions and 67 deletions

View File

@@ -0,0 +1,141 @@
import request from './request'
/** 到货通知单 */
export interface ArrivalNotice {
noticeId?: number
noticeCode?: string
noticeName?: string
poCode?: string
vendorId?: number
vendorCode?: string
vendorName?: string
vendorNick?: string
arrivalDate?: string
contact?: string
tel?: string
status?: string
remark?: string
createTime?: string
updateTime?: string
}
/** 到货通知单行 */
export interface ArrivalNoticeLine {
lineId?: number
noticeId?: number
itemId?: number
itemCode?: string
itemName?: string
specification?: string
unitOfMeasure?: string
unitName?: string
quantityArrival?: number
quantityQuanlified?: number
iqcCheck?: string
iqcId?: number
iqcCode?: string
remark?: string
createTime?: string
updateTime?: string
}
/** 到货通知单查询参数 */
export interface ArrivalNoticeQuery {
noticeCode?: string
noticeName?: string
poCode?: string
vendorId?: number
vendorName?: string
arrivalDate?: string
status?: string
pageNum?: number
pageSize?: number
}
export interface ArrivalNoticeListResponse {
rows: ArrivalNotice[]
total: number
}
export interface ArrivalNoticeLineListResponse {
rows: ArrivalNoticeLine[]
total: number
}
const NOTICE_BASE = '/mes/wm/arrivalnotice'
const LINE_BASE = '/mes/wm/arrivalnoticeline'
// ============ 到货通知单 ============
/** 查询到货通知单列表 */
export function listArrivalNotice(query?: ArrivalNoticeQuery): Promise<ArrivalNoticeListResponse> {
return request.get(`${NOTICE_BASE}/list`, { params: query }).then((res: any) => ({
rows: res.rows || [],
total: res.total || 0
}))
}
/** 查询到货通知单详情 */
export function getArrivalNotice(noticeId: number): Promise<ArrivalNotice> {
return request.get(`${NOTICE_BASE}/${noticeId}`).then((res: any) => res.data)
}
/** 新增到货通知单 */
export function addArrivalNotice(data: Partial<ArrivalNotice>): Promise<{ data?: number }> {
return request.post(NOTICE_BASE, data)
}
/** 修改到货通知单 */
export function updateArrivalNotice(data: Partial<ArrivalNotice>): Promise<void> {
return request.put(NOTICE_BASE, data)
}
/** 删除到货通知单 */
export function deleteArrivalNotice(noticeId: number | number[]): Promise<void> {
const ids = Array.isArray(noticeId) ? noticeId.join(',') : String(noticeId)
return request.delete(`${NOTICE_BASE}/${ids}`)
}
/** 生成到货通知单编码 */
export function genArrivalNoticeCode(): Promise<string> {
return request.get('/system/autocode/get/ARRIVALNOTICE_CODE').then((res: any) => res.data ?? res.msg ?? '')
}
// ============ 到货通知单行 ============
/** 查询到货通知单行列表 */
export function listArrivalNoticeLine(query?: { noticeId?: number; pageNum?: number; pageSize?: number }): Promise<ArrivalNoticeLineListResponse> {
return request.get(`${LINE_BASE}/list`, { params: query }).then((res: any) => ({
rows: res.rows || [],
total: res.total || 0
}))
}
/** 查询到货通知单行详情 */
export function getArrivalNoticeLine(lineId: number): Promise<ArrivalNoticeLine> {
return request.get(`${LINE_BASE}/${lineId}`).then((res: any) => res.data)
}
/** 新增到货通知单行 */
export function addArrivalNoticeLine(data: Partial<ArrivalNoticeLine>): Promise<void> {
return request.post(LINE_BASE, data)
}
/** 修改到货通知单行 */
export function updateArrivalNoticeLine(data: Partial<ArrivalNoticeLine>): Promise<void> {
return request.put(LINE_BASE, data)
}
/** 删除到货通知单行 */
export function deleteArrivalNoticeLine(lineId: number | number[]): Promise<void> {
const ids = Array.isArray(lineId) ? lineId.join(',') : String(lineId)
return request.delete(`${LINE_BASE}/${ids}`)
}
// ============ 状态映射 ============
export const ARRIVAL_NOTICE_STATUS_MAP: Record<string, { label: string; type: string }> = {
PREPARE: { label: '开立', type: 'info' },
APPROVING: { label: '提交中', type: 'warning' },
APPROVED: { label: '待入库', type: 'success' }
}

View File

@@ -2,61 +2,63 @@
<div class="page-container">
<el-card class="search-card" shadow="never">
<el-form :model="queryParams" inline>
<el-form-item label="单编号">
<el-input v-model="queryParams.trackCode" placeholder="请输入" clearable style="width: 140px;" />
<el-form-item label="通知单编号">
<el-input v-model="queryParams.noticeCode" placeholder="请输入" clearable style="width: 140px;" />
</el-form-item>
<el-form-item label="单据编码">
<el-input v-model="queryParams.checkinCode" placeholder="请输入" clearable style="width: 140px;" />
<el-form-item label="通知单名称">
<el-input v-model="queryParams.noticeName" placeholder="请输入" clearable style="width: 140px;" />
</el-form-item>
<el-form-item label="供应商">
<el-input v-model="queryParams.supplierName" placeholder="请输入" clearable style="width: 150px;" />
<el-form-item label="采购订单编号">
<el-input v-model="queryParams.poCode" placeholder="请输入" clearable style="width: 140px;" />
</el-form-item>
<el-form-item label="日期">
<el-date-picker v-model="dateRange" type="daterange" range-separator="-" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" style="width: 220px;" />
<el-form-item label="供应商名称">
<el-input v-model="queryParams.vendorName" placeholder="请输入" clearable style="width: 150px;" />
</el-form-item>
<el-form-item label="到货日期">
<el-date-picker v-model="queryParams.arrivalDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" clearable style="width: 140px;" />
</el-form-item>
<el-form-item label="单据状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 120px;">
<el-option label="开立" value="PREPARE" />
<el-option label="提交中" value="APPROVING" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery"><el-icon><Search /></el-icon>搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card" shadow="never">
<div class="toolbar">
<el-button @click="handleDocument">单据</el-button>
<el-button @click="handleQueryAll">查询所有</el-button>
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>新增</el-button>
<el-button type="success" @click="handleExport"><el-icon><Download /></el-icon>导出</el-button>
<el-button @click="handleExport"><el-icon><Download /></el-icon>导出</el-button>
</div>
<el-table v-loading="loading" :data="tableData" stripe border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="单编号" width="130">
<template #default="{ row }">{{ row.lines?.[0]?.trackCode || '-' }}</template>
</el-table-column>
<el-table-column prop="checkinCode" label="单据编码" width="130">
<el-table-column label="通知单编号" width="150">
<template #default="{ row }">
<el-link type="primary">{{ row.checkinCode }}</el-link>
<el-link type="primary" @click="handleView(row)">{{ row.noticeCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="noticeName" label="通知单名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="poCode" label="采购订单号" width="130" show-overflow-tooltip />
<el-table-column prop="vendorName" label="供应商名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="contact" label="联系人" width="100" />
<el-table-column prop="tel" label="联系方式" width="120" />
<el-table-column prop="arrivalDate" label="到货日期" width="110" />
<el-table-column prop="status" label="单据状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '审核' ? 'success' : 'info'">{{ row.status }}</el-tag>
<el-tag :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="supplierName" label="供应商" min-width="180" show-overflow-tooltip />
<el-table-column label="物料名称" width="150">
<template #default="{ row }">{{ row.lines?.[0]?.itemName || '-' }}</template>
</el-table-column>
<el-table-column label="物料编码" width="120">
<template #default="{ row }">{{ row.lines?.[0]?.itemCode || '-' }}</template>
</el-table-column>
<el-table-column prop="totalQuantity" label="到货数量" width="100" align="right" />
<el-table-column prop="stockedQuantity" label="入库数量" width="100" align="right" />
<el-table-column prop="checkinDate" label="单据日期" width="110" />
<el-table-column label="操作" width="120" fixed="right">
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button type="primary" link>查看</el-button>
<el-popconfirm title="确定删除吗?" @confirm="handleDelete(row)">
<el-button type="primary" link @click="handleView(row)">查看</el-button>
<el-button v-if="row.status === 'PREPARE'" type="primary" link @click="handleEdit(row)">修改</el-button>
<el-popconfirm v-if="row.status === 'PREPARE'" title="确定删除吗?" @confirm="handleDelete(row)">
<template #reference>
<el-button type="danger" link>删除</el-button>
</template>
@@ -66,35 +68,238 @@
</el-table>
<div class="table-footer">
<div class="summary">
到货数量: <span class="value">{{ totalCheckinQty.toLocaleString() }}</span>
入库数量: <span class="value">{{ totalStockedQty.toLocaleString() }}</span>
</div>
<el-pagination v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :page-sizes="[10, 20, 50, 100]" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData" />
</div>
</el-card>
<!-- 新增/修改/查看弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="960px" destroy-on-close @close="handleDialogClose">
<el-form ref="formRef" :model="form" :rules="formRules" :disabled="dialogMode === 'view'" label-width="100px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="通知单编号" prop="noticeCode">
<el-input v-model="form.noticeCode" placeholder="可自动生成" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item v-if="dialogMode !== 'view' && form.status === 'PREPARE'" label-width="80">
<el-switch v-model="autoGenCode" active-text="自动生成" @change="handleAutoGenChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="通知单名称" prop="noticeName">
<el-input v-model="form.noticeName" placeholder="请输入通知单名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="采购订单编号" prop="poCode">
<el-input v-model="form.poCode" placeholder="请输入采购订单编号" />
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="供应商" prop="vendorName">
<el-input v-model="form.vendorName" readonly placeholder="请选择供应商" @click="openSupplierDialog">
<template #append>
<el-button :icon="Search" @click="openSupplierDialog" :disabled="dialogMode === 'view'" />
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="到货日期" prop="arrivalDate">
<el-date-picker v-model="form.arrivalDate" type="date" placeholder="请选择到货日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系人" prop="contact">
<el-input v-model="form.contact" placeholder="请输入联系人" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系方式" prop="tel">
<el-input v-model="form.tel" placeholder="请输入联系方式" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 物料明细区noticeId 存在时显示 -->
<el-divider v-if="form.noticeId" content-position="center">物料信息</el-divider>
<div v-if="form.noticeId" class="line-section">
<div class="line-toolbar">
<el-button v-if="dialogMode !== 'view'" type="primary" size="small" @click="handleAddLine">
<el-icon><Plus /></el-icon>新增
</el-button>
</div>
<el-table :data="lineList" stripe border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="物料编码" width="130">
<template #default="{ row, $index }">
<div v-if="dialogMode !== 'view'" class="inline-select">
<span class="mono">{{ row.itemCode || '请选择' }}</span>
<el-button type="primary" link @click="openItemDialog($index)">选择</el-button>
</div>
<span v-else>{{ row.itemCode || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="itemName" label="物料名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="specification" label="规格型号" width="120" show-overflow-tooltip />
<el-table-column prop="unitName" label="单位" width="70" align="center" />
<el-table-column label="到货数量" width="100">
<template #default="{ row }">
<el-input-number v-if="dialogMode !== 'view'" v-model="row.quantityArrival" :min="0" :precision="2" size="small" style="width: 90px" />
<span v-else>{{ row.quantityArrival ?? '-' }}</span>
</template>
</el-table-column>
<el-table-column label="是否检验" width="90" align="center">
<template #default="{ row }">
<el-select v-if="dialogMode !== 'view'" v-model="row.iqcCheck" size="small" style="width: 70px">
<el-option label="是" value="Y" />
<el-option label="否" value="N" />
</el-select>
<span v-else>{{ row.iqcCheck === 'Y' ? '' : '' }}</span>
</template>
</el-table-column>
<el-table-column prop="quantityQuanlified" label="合格数量" width="90" align="right" />
<el-table-column prop="iqcCode" label="检验单号" width="120" show-overflow-tooltip />
<el-table-column prop="remark" label="备注" min-width="100" show-overflow-tooltip />
<el-table-column v-if="dialogMode !== 'view'" label="操作" width="80" fixed="right">
<template #default="{ $index }">
<el-button type="danger" link @click="handleDeleteLine($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button v-if="dialogMode !== 'view' && form.status === 'PREPARE'" type="primary" :loading="saveLoading" @click="handleSave">保存</el-button>
<el-button v-if="dialogMode !== 'view' && form.status === 'PREPARE' && form.noticeId" type="success" :loading="saveLoading" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
<!-- 选择供应商弹窗 -->
<el-dialog v-model="showSupplierDialog" title="选择供应商" width="900px" destroy-on-close>
<div class="dialog-search">
<el-input v-model="supplierSearch.supplierName" placeholder="供应商名称" clearable style="width: 200px; margin-right: 8px" />
<el-input v-model="supplierSearch.contact1" placeholder="业务联系人" clearable style="width: 160px; margin-right: 8px" />
<el-button type="primary" @click="loadSuppliers">搜索</el-button>
<el-button @click="resetSupplierSearch">查询所有</el-button>
</div>
<el-table :data="supplierList" stripe border max-height="400" highlight-current-row>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="supplierName" label="供应商名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="tel" label="电话" width="130" />
<el-table-column prop="contact1" label="业务联系人" width="120" />
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="handleSelectSupplier(row)">选择</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 选择物料弹窗 -->
<el-dialog v-model="showItemDialog" title="选择物料" width="960px" destroy-on-close @open="handleItemDialogOpen">
<div class="item-dialog-body">
<div class="category-tree">
<el-input v-model="itemTypeFilter" placeholder="请输入分类名称" clearable size="small" style="margin-bottom: 8px" />
<el-scrollbar height="360px">
<el-tree
ref="itemTreeRef"
:data="itemTypeTree"
:props="{ label: 'label', children: 'children' }"
node-key="id"
highlight-current
default-expand-all
:filter-node-method="filterTreeNode"
@node-click="handleTreeNodeClick"
/>
</el-scrollbar>
</div>
<div class="item-table-area">
<div class="item-search-area">
<el-input v-model="itemSearch.itemCode" placeholder="物料编码" clearable style="width: 140px" @keyup.enter="loadItemTableData" />
<el-input v-model="itemSearch.itemName" placeholder="物料名称" clearable style="width: 140px" @keyup.enter="loadItemTableData" />
<el-button type="primary" @click="loadItemTableData">搜索</el-button>
<el-button @click="resetItemSearch">重置</el-button>
</div>
<div class="item-toolbar">
<el-button size="small" @click="loadAllItems">查询所有</el-button>
</div>
<el-table :data="itemTableData" v-loading="itemTableLoading" stripe border max-height="340" highlight-current-row>
<el-table-column prop="itemCode" label="物料编码" width="130" />
<el-table-column prop="itemName" label="物料名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="specification" label="型号规格" width="150" show-overflow-tooltip />
<el-table-column prop="unitName" label="主计量" width="80" align="center" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="handleSelectItem(row)">选择</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Download } from '@element-plus/icons-vue'
import { getCheckinList, deleteCheckin, type Checkin, type CheckinQuery } from '@/api/checkin'
import {
listArrivalNotice,
getArrivalNotice,
addArrivalNotice,
updateArrivalNotice,
deleteArrivalNotice,
listArrivalNoticeLine,
addArrivalNoticeLine,
updateArrivalNoticeLine,
deleteArrivalNoticeLine,
genArrivalNoticeCode,
ARRIVAL_NOTICE_STATUS_MAP,
type ArrivalNotice,
type ArrivalNoticeLine,
type ArrivalNoticeQuery
} from '@/api/arrivalnotice'
import { getPoSupplierList, type PoSupplier } from '@/api/purchaseOrder'
import { listMdItem, type MdItem } from '@/api/masterdata/item'
import { listItemType, handleTree, type ItemType } from '@/api/masterdata/itemtype'
const queryParams = reactive<CheckinQuery>({ trackCode: '', checkinCode: '', supplierName: '', pageNum: 1, pageSize: 10 })
const dateRange = ref<string[]>([])
const queryParams = reactive<ArrivalNoticeQuery>({
noticeCode: '',
noticeName: '',
poCode: '',
vendorName: '',
arrivalDate: '',
status: '',
pageNum: 1,
pageSize: 10
})
const loading = ref(false)
const tableData = ref<Checkin[]>([])
const tableData = ref<ArrivalNotice[]>([])
const total = ref(0)
const totalCheckinQty = computed(() => tableData.value.reduce((sum, r) => sum + (r.totalQuantity || 0), 0))
const totalStockedQty = computed(() => tableData.value.reduce((sum, r) => sum + (r.stockedQuantity || 0), 0))
const statusLabel = (status?: string) => ARRIVAL_NOTICE_STATUS_MAP[status || '']?.label ?? status ?? '-'
const statusTagType = (status?: string) => ARRIVAL_NOTICE_STATUS_MAP[status || '']?.type ?? 'info'
const loadData = async () => {
loading.value = true
try {
const res = await getCheckinList(queryParams)
const res = await listArrivalNotice(queryParams)
tableData.value = res.rows
total.value = res.total
} finally {
@@ -103,11 +308,383 @@ const loadData = async () => {
}
const handleQuery = () => { queryParams.pageNum = 1; loadData() }
const handleQueryAll = () => { queryParams.trackCode = ''; queryParams.checkinCode = ''; queryParams.supplierName = ''; dateRange.value = []; handleQuery() }
const handleDocument = () => ElMessage.info('单据视图切换')
const handleAdd = () => ElMessage.info('新增采购到货单')
const handleDelete = async (row: Checkin) => { await deleteCheckin(String(row.checkinId)); ElMessage.success('删除成功'); loadData() }
const handleExport = () => ElMessage.info('导出功能开发中')
const handleReset = () => {
queryParams.noticeCode = ''
queryParams.noticeName = ''
queryParams.poCode = ''
queryParams.vendorName = ''
queryParams.arrivalDate = ''
queryParams.status = ''
handleQuery()
}
// ============ 弹窗 ============
const dialogVisible = ref(false)
const dialogTitle = ref('')
const dialogMode = ref<'add' | 'edit' | 'view'>('add')
const formRef = ref()
const saveLoading = ref(false)
const autoGenCode = ref(false)
const form = reactive<Partial<ArrivalNotice>>({
noticeId: undefined,
noticeCode: '',
noticeName: '',
poCode: '',
vendorId: undefined,
vendorCode: '',
vendorName: '',
vendorNick: '',
arrivalDate: '',
contact: '',
tel: '',
status: 'PREPARE',
remark: ''
})
const formRules = {
noticeCode: [{ required: true, message: '通知单编号不能为空', trigger: 'blur' }],
noticeName: [{ required: true, message: '通知单名称不能为空', trigger: 'blur' }],
vendorName: [{ required: true, message: '请选择供应商', trigger: 'blur' }],
arrivalDate: [{ required: true, message: '请选择到货日期', trigger: 'change' }]
}
// 明细行(本地编辑用,保存时同步到后端)
const lineList = ref<ArrivalNoticeLine[]>([])
function resetForm() {
Object.assign(form, {
noticeId: undefined,
noticeCode: '',
noticeName: '',
poCode: '',
vendorId: undefined,
vendorCode: '',
vendorName: '',
vendorNick: '',
arrivalDate: '',
contact: '',
tel: '',
status: 'PREPARE',
remark: ''
})
lineList.value = []
autoGenCode.value = false
}
async function loadLines() {
if (!form.noticeId) return
const res = await listArrivalNoticeLine({ noticeId: form.noticeId, pageNum: 1, pageSize: 500 })
lineList.value = res.rows.map(r => ({ ...r, iqcCheck: r.iqcCheck || 'N' }))
}
function handleAdd() {
resetForm()
dialogTitle.value = '新增采购到货单'
dialogMode.value = 'add'
form.arrivalDate = new Date().toISOString().split('T')[0]
dialogVisible.value = true
}
async function handleEdit(row: ArrivalNotice) {
resetForm()
const data = await getArrivalNotice(row.noticeId!)
Object.assign(form, data)
await loadLines()
dialogTitle.value = '修改采购到货单'
dialogMode.value = 'edit'
dialogVisible.value = true
}
async function handleView(row: ArrivalNotice) {
resetForm()
const data = await getArrivalNotice(row.noticeId!)
Object.assign(form, data)
await loadLines()
dialogTitle.value = '查看到货通知单'
dialogMode.value = 'view'
dialogVisible.value = true
}
function handleDialogClose() {
formRef.value?.resetFields()
}
async function handleAutoGenChange(val: boolean) {
if (val) {
try {
form.noticeCode = await genArrivalNoticeCode()
} catch (e) {
ElMessage.error('生成编码失败')
autoGenCode.value = false
}
} else {
form.noticeCode = ''
}
}
async function handleSave() {
try {
await formRef.value?.validate()
} catch {
return
}
saveLoading.value = true
try {
if (form.noticeId) {
await updateArrivalNotice(form)
// 保存明细变更
await saveLines()
ElMessage.success('保存成功')
await loadLines()
} else {
await addArrivalNotice(form)
// 后端可能不返回 noticeId通过 noticeCode 查询获取
const listRes = await listArrivalNotice({ noticeCode: form.noticeCode, pageNum: 1, pageSize: 1 })
const added = listRes.rows[0]
if (added?.noticeId) {
form.noticeId = added.noticeId
await saveLines()
}
ElMessage.success('新增成功')
if (form.noticeId) await loadLines()
}
} catch (e) {
console.error('保存失败:', e)
} finally {
saveLoading.value = false
}
}
async function saveLines() {
if (!form.noticeId) return
const lines = lineList.value.filter(l => l.itemCode && (l.quantityArrival ?? 0) > 0)
for (const line of lines) {
const payload = { ...line, noticeId: form.noticeId, iqcCheck: line.iqcCheck || 'N' }
if (line.lineId) {
await updateArrivalNoticeLine(payload)
} else {
await addArrivalNoticeLine(payload)
}
}
}
async function handleSubmit() {
try {
await formRef.value?.validate()
} catch {
return
}
if (!form.noticeId) {
ElMessage.warning('请先保存表头后再提交')
return
}
const res = await listArrivalNoticeLine({ noticeId: form.noticeId, pageNum: 1, pageSize: 1 })
if (!res.rows.length) {
ElMessage.warning('请至少添加一条物料明细')
return
}
saveLoading.value = true
try {
form.status = 'APPROVING'
await updateArrivalNotice(form)
ElMessage.success('提交成功')
dialogVisible.value = false
loadData()
} catch (e) {
console.error('提交失败:', e)
} finally {
saveLoading.value = false
}
}
async function handleDelete(row: ArrivalNotice) {
try {
await ElMessageBox.confirm('确定删除该到货通知单吗?', '提示', { type: 'warning' })
await deleteArrivalNotice(row.noticeId!)
ElMessage.success('删除成功')
loadData()
} catch (e) {
if (e !== 'cancel') console.error('删除失败:', e)
}
}
function handleExport() {
ElMessage.info('导出功能开发中')
}
// ============ 明细行操作 ============
function handleAddLine() {
if (!form.noticeId) {
ElMessage.warning('请先保存表头后再添加物料')
return
}
lineList.value.push({
noticeId: form.noticeId,
itemId: undefined,
itemCode: '',
itemName: '',
specification: '',
unitOfMeasure: '',
unitName: '',
quantityArrival: 0,
quantityQuanlified: 0,
iqcCheck: 'N',
iqcCode: '',
remark: ''
})
}
function handleDeleteLine(index: number) {
const line = lineList.value[index]
if (line.lineId) {
deleteArrivalNoticeLine(line.lineId).then(() => {
lineList.value.splice(index, 1)
}).catch(() => {})
} else {
lineList.value.splice(index, 1)
}
}
// ============ 供应商选择 ============
const showSupplierDialog = ref(false)
const supplierSearch = reactive({ supplierName: '', contact1: '' })
const supplierList = ref<PoSupplier[]>([])
function openSupplierDialog() {
if (dialogMode.value === 'view') return
showSupplierDialog.value = true
loadSuppliers()
}
async function loadSuppliers() {
const res = await getPoSupplierList({
supplierName: supplierSearch.supplierName || undefined,
contact1: supplierSearch.contact1 || undefined
})
supplierList.value = res.rows
}
function resetSupplierSearch() {
supplierSearch.supplierName = ''
supplierSearch.contact1 = ''
loadSuppliers()
}
function handleSelectSupplier(row: PoSupplier) {
form.vendorId = row.supplierId
form.vendorCode = row.supplierCode
form.vendorName = row.supplierName
form.vendorNick = row.supplierNick
showSupplierDialog.value = false
}
// ============ 物料选择 ============
const showItemDialog = ref(false)
const itemTableLoading = ref(false)
const itemTableData = ref<MdItem[]>([])
const editingLineIndex = ref(-1)
const itemTreeRef = ref()
const itemTypeTree = ref<any[]>([])
const itemTypeFilter = ref('')
const itemSearch = reactive<{ itemCode: string; itemName: string; itemTypeId: number | undefined }>({
itemCode: '',
itemName: '',
itemTypeId: undefined
})
watch(itemTypeFilter, (val) => {
itemTreeRef.value?.filter(val)
})
function filterTreeNode(value: string, data: any) {
if (!value) return true
return (data.label || '').includes(value)
}
function openItemDialog(idx: number) {
editingLineIndex.value = idx
showItemDialog.value = true
}
async function handleItemDialogOpen() {
itemSearch.itemCode = ''
itemSearch.itemName = ''
itemSearch.itemTypeId = undefined
itemTypeFilter.value = ''
await Promise.all([loadItemTypeTree(), loadItemTableData()])
}
async function loadItemTypeTree() {
try {
const res = await listItemType()
const flat = (res as any).data || res || []
const list = Array.isArray(flat) ? flat : []
const tree = handleTree(list)
function toTreeNodes(items: ItemType[]): any[] {
return items.map(item => ({
id: item.itemTypeId,
label: item.itemTypeName || '',
raw: item,
children: item.children?.length ? toTreeNodes(item.children) : undefined
}))
}
itemTypeTree.value = toTreeNodes(tree)
} catch (e) {
itemTypeTree.value = []
}
}
async function loadItemTableData() {
itemTableLoading.value = true
try {
const params: any = { enableFlag: 'Y', pageNum: 1, pageSize: 100 }
if (itemSearch.itemCode) params.itemCode = itemSearch.itemCode
if (itemSearch.itemName) params.itemName = itemSearch.itemName
if (itemSearch.itemTypeId) params.itemTypeId = itemSearch.itemTypeId
const res = await listMdItem(params)
itemTableData.value = res.rows || []
} catch (e) {
itemTableData.value = []
} finally {
itemTableLoading.value = false
}
}
function handleTreeNodeClick(node: any) {
itemSearch.itemTypeId = node.id
loadItemTableData()
}
function resetItemSearch() {
itemSearch.itemCode = ''
itemSearch.itemName = ''
itemSearch.itemTypeId = undefined
itemTreeRef.value?.setCurrentKey(null)
loadItemTableData()
}
function loadAllItems() {
itemSearch.itemCode = ''
itemSearch.itemName = ''
itemSearch.itemTypeId = undefined
itemTypeFilter.value = ''
itemTreeRef.value?.setCurrentKey(null)
loadItemTableData()
}
function handleSelectItem(selected: MdItem) {
const idx = editingLineIndex.value
if (idx < 0 || !lineList.value[idx]) return
const row = lineList.value[idx]
row.itemId = selected.itemId
row.itemCode = selected.itemCode ?? ''
row.itemName = selected.itemName ?? ''
row.specification = selected.specification ?? ''
row.unitOfMeasure = selected.unitOfMeasure ?? ''
row.unitName = selected.unitName ?? selected.unitOfMeasure ?? ''
showItemDialog.value = false
}
onMounted(() => loadData())
</script>
@@ -118,6 +695,20 @@ onMounted(() => loadData())
.search-card :deep(.el-card__body) { padding-bottom: 0; }
.toolbar { margin-bottom: 16px; display: flex; gap: 8px; }
.table-footer { margin-top: 16px; display: flex; justify-content: space-between; align-items: center; }
.summary { font-size: 14px; color: #606266; }
.summary .value { color: #409eff; font-weight: bold; margin-right: 20px; }
.dialog-search { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; margin-bottom: 16px; }
.inline-select { display: flex; align-items: center; gap: 4px; }
.inline-select .mono { font-family: monospace; color: #606266; }
.line-section { margin-top: 12px; }
.line-toolbar { margin-bottom: 8px; }
.item-dialog-body { display: flex; gap: 12px; min-height: 420px; }
.category-tree {
width: 180px;
min-width: 180px;
border-right: 1px solid #e4e7ed;
padding-right: 12px;
}
.item-table-area { flex: 1; overflow: hidden; }
.item-search-area { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.item-toolbar { margin-bottom: 8px; }
</style>

View File

@@ -162,9 +162,9 @@
</div>
</div>
<el-table :data="lines" border size="small" max-height="420">
<el-table :data="lines" border size="small" max-height="420" class="ebom-table ebom-table-nowrap">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="物料编码" min-width="160">
<el-table-column label="物料编码" min-width="160" show-overflow-tooltip>
<template #default="{ row, $index }">
<div class="inline-select">
<span class="mono">{{ row.bomItemCode || '请添加物料' }}</span>
@@ -260,12 +260,12 @@
<el-button @click="queryAllProducts">查询所有</el-button>
</el-form-item>
</el-form>
<el-table :data="productList" border size="small" max-height="380" v-loading="productLoading">
<el-table-column prop="itemCode" label="物料编码" width="130" />
<el-table :data="productList" border size="small" max-height="380" v-loading="productLoading" class="ebom-table ebom-table-nowrap">
<el-table-column prop="itemCode" label="物料编码" width="130" show-overflow-tooltip />
<el-table-column prop="itemName" label="物料名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="specification" label="型号规格" width="130" show-overflow-tooltip />
<el-table-column prop="unitName" label="主计量" width="70" align="center" />
<el-table-column label="供应方式" width="80" align="center">
<el-table-column label="供应方式" width="80" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ getSupplyLabel(row.itemOrProduct) }}
</template>
@@ -323,12 +323,12 @@
<el-button @click="queryAllItems">查询所有</el-button>
</el-form-item>
</el-form>
<el-table :data="itemList" border size="small" max-height="380" v-loading="itemLoading">
<el-table-column prop="itemCode" label="物料编码" width="130" />
<el-table :data="itemList" border size="small" max-height="380" v-loading="itemLoading" class="ebom-table ebom-table-nowrap">
<el-table-column prop="itemCode" label="物料编码" width="130" show-overflow-tooltip />
<el-table-column prop="itemName" label="物料名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="specification" label="型号规格" width="130" show-overflow-tooltip />
<el-table-column prop="unitName" label="主计量" width="70" align="center" />
<el-table-column label="供应方式" width="80" align="center">
<el-table-column label="供应方式" width="80" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ getSupplyLabel(row.itemOrProduct) }}
</template>
@@ -700,10 +700,11 @@ async function loadItemTypeTree() {
}
}
/** 左侧分类树显示物料/产品名称itemTypeName参考物料管理 masterdata/item */
function toTreeNode(item: ItemType): any {
return {
id: item.itemTypeId,
label: `${item.itemTypeCode || ''}-${item.itemTypeName || ''}`.replace(/^-/, ''),
label: item.itemTypeName || item.itemTypeCode || '',
children: item.children?.map(toTreeNode) || []
}
}
@@ -732,6 +733,7 @@ function openProductDialog() {
searchProductItems()
}
/** 从物料编码选择打开:显示产品/半成品分类下的数据(含未启用) */
async function searchProductItems() {
productLoading.value = true
try {
@@ -739,7 +741,7 @@ async function searchProductItems() {
itemCode: productQuery.itemCode || undefined,
itemName: productQuery.itemName || undefined,
itemTypeId: productQuery.itemTypeId,
enableFlag: 'Y',
itemOrProduct: 'PRODUCT',
pageNum: productQuery.pageNum,
pageSize: productQuery.pageSize
})
@@ -799,6 +801,7 @@ function openItemDialog(idx: number) {
searchSubItems()
}
/** 从新增物料按钮打开:显示原材料分类下的数据 */
async function searchSubItems() {
itemLoading.value = true
try {
@@ -806,6 +809,7 @@ async function searchSubItems() {
itemCode: itemQuery.itemCode || undefined,
itemName: itemQuery.itemName || undefined,
itemTypeId: itemQuery.itemTypeId,
itemOrProduct: 'ITEM',
enableFlag: 'Y',
pageNum: itemQuery.pageNum,
pageSize: itemQuery.pageSize
@@ -959,4 +963,19 @@ onMounted(() => {
display: flex;
justify-content: flex-end;
}
/* 表格表头和内容不换行,过长截断显示 */
.ebom-table-nowrap :deep(.el-table__header th),
.ebom-table-nowrap :deep(.el-table__body td) {
white-space: nowrap;
}
.ebom-table-nowrap :deep(.el-table__body td .cell) {
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.ebom-table-nowrap :deep(.el-table__body td .el-input),
.ebom-table-nowrap :deep(.el-table__body td .el-input-number) {
max-width: 100%;
}
</style>

View File

@@ -101,9 +101,10 @@
border
style="width: 100%"
:max-height="tableHeight"
class="ebom-table ebom-table-nowrap"
>
<el-table-column type="index" label="序号" width="60" align="center" fixed="left" />
<el-table-column prop="bomCode" label="单据编码" width="140" fixed="left">
<el-table-column prop="bomCode" label="单据编码" width="140" fixed="left" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">{{ row.bomCode }}</el-link>
</template>
@@ -114,7 +115,7 @@
<el-table-column prop="version" label="版本号" width="80" align="center" />
<el-table-column prop="versionDesc" label="版本说明" width="120" show-overflow-tooltip />
<el-table-column prop="drawingNo" label="图纸号" width="100" show-overflow-tooltip />
<el-table-column prop="status" label="单据状态" width="80" align="center">
<el-table-column prop="status" label="单据状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">{{ getStatusLabel(row.status) }}</el-tag>
</template>
@@ -142,10 +143,11 @@
border
style="width: 100%"
:max-height="tableHeight"
class="ebom-table ebom-table-nowrap"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" fixed="left" />
<el-table-column prop="bomCode" label="单据编码" width="140" fixed="left">
<el-table-column prop="bomCode" label="单据编码" width="140" fixed="left" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">{{ row.bomCode }}</el-link>
</template>
@@ -155,7 +157,7 @@
{{ row.bomDate || row.createTime?.slice(0, 10) || '-' }}
</template>
</el-table-column>
<el-table-column prop="status" label="单据状态" width="80" align="center">
<el-table-column prop="status" label="单据状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">{{ getStatusLabel(row.status) }}</el-tag>
</template>
@@ -165,8 +167,8 @@
{{ getBizTypeLabel(row.businessType) }}
</template>
</el-table-column>
<el-table-column prop="deptName" label="业务部门" width="100" align="center" />
<el-table-column prop="operatorName" label="业务人员" width="100" align="center">
<el-table-column prop="deptName" label="业务部门" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="operatorName" label="业务人员" width="100" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ row.operatorName || row.createBy || '-' }}
</template>
@@ -658,4 +660,14 @@ onMounted(() => {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13px;
}
/* 表格表头和内容不换行,过长截断显示 */
.ebom-table-nowrap :deep(.el-table__header th),
.ebom-table-nowrap :deep(.el-table__body td) {
white-space: nowrap;
}
.ebom-table-nowrap :deep(.el-table__body td .cell) {
overflow: hidden;
text-overflow: ellipsis;
}
</style>