Files
my-mom-system/erp-frontend-vue/src/views/Purchasing/Order/form.vue
panchengyong b11bf5e631 feat: 采购订单自动生成到货通知单功能
在采购订单查看页面新增"生成到货通知单"按钮,已审核的采购订单可一键生成到货通知单,
自动携带供应商信息和未到货物料明细行。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 14:26:56 +08:00

965 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="form-container">
<el-card shadow="never">
<!-- 页面标题和操作按钮 -->
<template #header>
<div class="card-header">
<span class="page-title">采购订单</span>
<div class="header-actions">
<!-- 新增模式 -->
<template v-if="mode === 'add'">
<el-button type="primary" :loading="saveLoading" @click="handleSave">保存</el-button>
<el-button @click="handleCancel">取消</el-button>
<el-button type="success" @click="handleApprove" :disabled="!form.orderId">审核</el-button>
<el-button type="warning" @click="handleUnapprove" :disabled="true">反审核</el-button>
<el-button @click="toggleHeaderCollapse">{{ headerCollapsed ? '展开' : '收起' }}</el-button>
</template>
<!-- 编辑模式 -->
<template v-else-if="mode === 'edit'">
<el-button type="primary" :loading="saveLoading" @click="handleSave">保存</el-button>
<el-button @click="handleRevert">撤回</el-button>
<el-button type="success" @click="handleApprove" :disabled="form.status !== 'PREPARE'">审核</el-button>
<el-button type="warning" @click="handleUnapprove" :disabled="form.status !== 'APPROVED'">反审核</el-button>
<el-button @click="toggleHeaderCollapse">{{ headerCollapsed ? '展开' : '收起' }}</el-button>
</template>
<!-- 查看模式 -->
<template v-else>
<el-button type="primary" @click="handleNewFromView">新增</el-button>
<el-button @click="handleSwitchToEdit">编辑</el-button>
<el-button type="success" @click="handleApprove" :disabled="form.status !== 'PREPARE'">审核</el-button>
<el-button type="warning" @click="handleUnapprove" :disabled="form.status !== 'APPROVED'">反审核</el-button>
<el-button type="info" @click="handleGenArrivalNotice" :disabled="form.status !== 'APPROVED'">生成到货通知单</el-button>
<el-button @click="handlePrint">打印</el-button>
<el-button @click="toggleHeaderCollapse">{{ headerCollapsed ? '展开' : '收起' }}</el-button>
</template>
</div>
</div>
</template>
<!-- ============ 表头表单区域可折叠 ============ -->
<el-form
v-show="!headerCollapsed"
ref="formRef"
:model="form"
:rules="formRules"
:disabled="mode === 'view'"
label-width="80px"
class="header-form"
>
<!-- 行1 -->
<el-row :gutter="20">
<el-col :span="5">
<el-form-item label="单据编码" prop="orderCode">
<el-input v-model="form.orderCode" placeholder="自动生成" disabled />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="业务类型" prop="businessType">
<el-select v-model="form.businessType" placeholder="请选择" style="width: 100%">
<el-option v-for="opt in businessTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="操作员">
<el-input v-model="form.operatorName" disabled />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="供方">
<el-input v-model="form.supplierName" placeholder="点击选择" readonly @click="openSupplierDialog">
<template #append>
<el-button :icon="Search" @click="openSupplierDialog" :disabled="mode === 'view'" />
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="备注信息">
<el-input v-model="form.remark" type="textarea" :rows="1" placeholder="请输入" />
</el-form-item>
</el-col>
</el-row>
<!-- 行2 -->
<el-row :gutter="20">
<el-col :span="5">
<el-form-item label="单据日期" prop="orderDate">
<el-date-picker v-model="form.orderDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="单据类型">
<el-input v-model="form.orderType" disabled />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="审核员">
<el-input v-model="form.approverName" placeholder="-" disabled />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="到货日期">
<el-date-picker v-model="form.deliveryDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="4" />
</el-row>
<!-- 行3 -->
<el-row :gutter="20">
<el-col :span="5">
<el-form-item label="单据状态">
<el-tag :type="statusTagType" size="default">{{ statusLabel }}</el-tag>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="采购部门">
<el-select v-model="form.deptId" placeholder="请选择" clearable style="width: 100%">
<el-option label="采购部" :value="1" />
<el-option label="生产部" :value="2" />
<el-option label="仓储部" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="审核日期">
<el-date-picker v-model="form.approveDate" type="date" placeholder="-" disabled value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="采购合同">
<el-input v-model="form.contractFile" placeholder="合同附件" disabled />
</el-form-item>
</el-col>
<el-col :span="4" />
</el-row>
<!-- 行4 -->
<el-row :gutter="20">
<el-col :span="5">
<el-form-item label="业务状态" prop="businessStatus">
<el-select v-model="form.businessStatus" placeholder="请选择" style="width: 100%">
<el-option v-for="opt in businessStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="采购人员">
<el-select v-model="form.userId" placeholder="请选择" clearable style="width: 100%">
<el-option label="张三" :value="1" />
<el-option label="李四" :value="2" />
<el-option label="王五" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="用料需求">
<el-select v-model="form.materialNeed" disabled style="width: 100%">
<el-option label="订单用料" value="订单用料" />
<el-option label="备库用料" value="备库用料" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="合同号">
<el-input v-model="form.contractNo" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="4" />
</el-row>
</el-form>
<!-- ============ 物料明细子表 ============ -->
<div class="material-section">
<div class="material-header">
<span class="section-title">物料信息</span>
<div v-if="isEditMode" class="material-actions">
<el-button type="warning" size="small" @click="showImportDialog = true">
<el-icon><Upload /></el-icon>引入
</el-button>
<el-button type="primary" size="small" @click="handleAddLine">
<el-icon><Plus /></el-icon>新增物料
</el-button>
</div>
</div>
<el-table :data="form.lines" stripe border style="width: 100%" show-summary :summary-method="getSummaries">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="trackCode" label="跟单编号" width="120">
<template #default="{ row }">
<el-input v-if="isEditMode" v-model="row.trackCode" size="small" placeholder="手工单据" />
<span v-else>{{ row.trackCode || '手工单据' }}</span>
</template>
</el-table-column>
<el-table-column prop="planCode" label="计划单号" width="120">
<template #default="{ row }">{{ row.planCode || '手工单据' }}</template>
</el-table-column>
<el-table-column label="物料编码" width="150">
<template #default="{ row, $index }">
<div v-if="isEditMode" 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="200" show-overflow-tooltip />
<el-table-column prop="specification" label="型号规格" width="150" />
<el-table-column prop="unitName" label="主计量" width="80" align="center" />
<el-table-column prop="needDate" label="需求日期" width="130">
<template #default="{ row }">
<el-date-picker v-if="isEditMode" v-model="row.needDate" type="date" placeholder="选择" value-format="YYYY-MM-DD" size="small" style="width: 100%" />
<span v-else>{{ row.needDate || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" width="100">
<template #default="{ row }">
<el-input-number v-if="isEditMode" v-model="row.quantity" :min="0" :precision="0" size="small" style="width: 90px" @change="calcLineAmount(row)" />
<span v-else>{{ row.quantity }}</span>
</template>
</el-table-column>
<el-table-column prop="unitPrice" label="单价" width="100">
<template #default="{ row }">
<el-input-number v-if="isEditMode" v-model="row.unitPrice" :min="0" :precision="2" size="small" style="width: 90px" @change="calcLineAmount(row)" />
<span v-else>{{ row.unitPrice ?? '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="100" align="right">
<template #default="{ row }">{{ row.amount != null ? Number(row.amount).toFixed(2) : '-' }}</template>
</el-table-column>
<el-table-column prop="arrivedQuantity" label="到货数量" width="90" align="right">
<template #default="{ row }">{{ row.arrivedQuantity ?? 0 }}</template>
</el-table-column>
<el-table-column prop="remark" label="采购说明" min-width="120">
<template #default="{ row }">
<el-input v-if="isEditMode" v-model="row.remark" size="small" />
<span v-else>{{ row.remark || '-' }}</span>
</template>
</el-table-column>
<el-table-column v-if="isEditMode" 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 v-if="mode !== 'add'" class="nav-links">
<el-link type="primary" :disabled="!hasPrev" @click="navigatePrev">上一条</el-link>
<el-link type="primary" :disabled="!hasNext" @click="navigateNext">下一条</el-link>
</div>
</div>
</el-card>
<!-- ============ 打印对话框 ============ -->
<PrintDialog
v-model:visible="showPrintDialog"
:config="printConfig"
/>
<!-- ============ 选择供应商弹窗 ============ -->
<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 type="success" @click="ElMessage.info('快速添加功能开发中')">快速添加</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 prop="contact1Tel" label="手机号" width="130" />
<el-table-column prop="enableFlag" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.enableFlag === 'Y' ? 'success' : 'info'" size="small">
{{ row.enableFlag === 'Y' ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<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="showImportDialog" title="采购计划单明细" width="1100px" destroy-on-close>
<div class="dialog-search">
<el-input v-model="importSearch.salesOrderCode" placeholder="跟单编号" clearable style="width: 140px; margin-right: 8px" />
<el-input v-model="importSearch.businessType" placeholder="料品大类" clearable style="width: 120px; margin-right: 8px" />
<el-input v-model="importSearch.itemCode" placeholder="物料编码" clearable style="width: 140px; margin-right: 8px" />
<el-input v-model="importSearch.itemName" placeholder="物料名称" clearable style="width: 140px; margin-right: 8px" />
<el-button type="primary" @click="loadImportData">搜索</el-button>
<el-button @click="handleSelectAllImport">全选</el-button>
</div>
<el-table ref="importTableRef" :data="importList" stripe border max-height="400" @selection-change="handleImportSelectionChange">
<el-table-column type="selection" width="50" />
<el-table-column prop="purchaseCode" label="计划单号" width="130" />
<el-table-column prop="salesUserName" label="销售员" width="80" />
<el-table-column prop="salesOrderCode" label="跟单编号" width="130" />
<el-table-column prop="deliveryDate" label="订单交期" width="110" />
<el-table-column prop="itemCode" label="物料编码" width="120" />
<el-table-column prop="itemName" label="物料名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="demandDate" label="需求日期" width="110" />
<el-table-column prop="purchaseQty" label="需求数量" width="100" align="right" />
<el-table-column prop="orderedQty" label="已采数量" width="100" align="right" />
<el-table-column label="未采数量" width="100" align="right">
<template #default="{ row }">{{ (row.purchaseQty ?? 0) - (row.orderedQty ?? 0) }}</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="showImportDialog = false">取消</el-button>
<el-button type="primary" @click="handleConfirmImport">确定</el-button>
</template>
</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>
<el-button size="small" :type="continuousSelect ? 'success' : 'default'" @click="continuousSelect = !continuousSelect">连选</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="120" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="handleSelectItem(row)">选择</el-button>
<el-button type="info" link @click="handleViewItem(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Upload, Plus } from '@element-plus/icons-vue'
import {
getPurchaseOrderDetail,
createPurchaseOrder,
updatePurchaseOrder,
approvePurchaseOrder,
unapprovePurchaseOrder,
genPurchaseOrderCode,
genArrivalNotice,
getPoSupplierList,
getLeadInList,
STATUS_MAP,
BUSINESS_TYPE_OPTIONS,
BUSINESS_STATUS_OPTIONS,
type PurchaseOrder,
type PurchaseOrderLine,
type PoSupplier,
type LeadInRow
} from '@/api/purchaseOrder'
import PrintDialog from '@/components/print/PrintDialog.vue'
import type { PrintConfig } from '@/components/print/types'
import { listMdItem, type MdItem } from '@/api/masterdata/item'
import { listItemType, handleTree, type ItemType } from '@/api/masterdata/itemtype'
const route = useRoute()
const router = useRouter()
// ============ Mode ============
const mode = computed(() => {
if (route.path.includes('/new')) return 'add'
if (route.path.includes('/edit')) return 'edit'
return 'view'
})
const isEditMode = computed(() => mode.value === 'add' || mode.value === 'edit')
// ============ Header collapse ============
const headerCollapsed = ref(false)
function toggleHeaderCollapse() { headerCollapsed.value = !headerCollapsed.value }
// ============ Form State ============
const formRef = ref()
const saveLoading = ref(false)
const businessTypeOptions = BUSINESS_TYPE_OPTIONS
const businessStatusOptions = BUSINESS_STATUS_OPTIONS
const form = reactive<Partial<PurchaseOrder>>({
orderId: undefined,
orderCode: '',
orderDate: new Date().toISOString().split('T')[0],
status: 'PREPARE',
businessType: '原材料',
businessStatus: 'NORMAL',
orderType: '采购订单',
materialNeed: '订单用料',
supplierId: undefined,
supplierCode: '',
supplierName: '',
deptId: undefined,
deptName: '',
userId: undefined,
userName: '',
deliveryDate: '',
contractNo: '',
contractFile: '',
totalQuantity: 0,
totalAmount: 0,
arrivedQuantity: 0,
remark: '',
operatorName: 'admin',
approverName: '',
approveDate: '',
lines: []
})
const formRules = {
orderDate: [{ required: true, message: '请选择单据日期', trigger: 'change' }],
businessType: [{ required: true, message: '请选择业务类型', trigger: 'change' }]
}
// ============ Status helpers ============
const statusTagType = computed(() => STATUS_MAP[form.status || 'PREPARE']?.type ?? 'info')
const statusLabel = computed(() => STATUS_MAP[form.status || 'PREPARE']?.label ?? form.status)
// ============ Load data ============
async function loadFormData() {
const id = route.params.id as string
if (id && mode.value !== 'add') {
try {
const data = await getPurchaseOrderDetail(Number(id))
Object.assign(form, data)
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载采购订单数据失败')
}
}
}
async function initNewForm() {
try {
const code = await genPurchaseOrderCode()
if (code) form.orderCode = code
} catch (error) {
console.error('生成编码失败:', error)
}
}
// ============ Line operations ============
function calcLineAmount(row: PurchaseOrderLine) {
row.amount = (row.quantity || 0) * (row.unitPrice || 0)
}
function computeTotals() {
const lines = form.lines || []
form.totalQuantity = lines.reduce((sum, l) => sum + (l.quantity || 0), 0)
form.totalAmount = lines.reduce((sum, l) => sum + (l.amount || 0), 0)
}
function getSummaries({ columns, data }: any) {
const sums: string[] = []
columns.forEach((col: any, index: number) => {
if (index === 0) { sums[index] = '合计'; return }
const prop = col.property
if (prop === 'quantity' || prop === 'amount' || prop === 'arrivedQuantity') {
const val = data.reduce((sum: number, row: any) => sum + (Number(row[prop]) || 0), 0)
sums[index] = prop === 'amount' ? val.toFixed(2) : String(val)
} else {
sums[index] = ''
}
})
return sums
}
function handleAddLine() {
form.lines = form.lines || []
form.lines.push({
lineId: 0,
orderId: 0,
orderCode: '',
lineNo: form.lines.length + 1,
trackCode: '',
planCode: '',
itemId: 0,
itemCode: '',
itemName: '',
specification: '',
unitName: '',
needDate: '',
quantity: 1,
unitPrice: 0,
amount: 0,
arrivedQuantity: 0,
remark: ''
})
}
function handleDeleteLine(index: number) {
form.lines?.splice(index, 1)
form.lines?.forEach((l, i) => { l.lineNo = i + 1 })
}
// ============ Supplier dialog ============
const showSupplierDialog = ref(false)
const supplierSearch = reactive({ supplierName: '', contact1: '' })
const supplierList = ref<PoSupplier[]>([])
function openSupplierDialog() {
if (mode.value === 'view') return
showSupplierDialog.value = true
loadSuppliers()
}
async function loadSuppliers() {
const res = await getPoSupplierList({ supplierName: supplierSearch.supplierName, contact1: supplierSearch.contact1 })
supplierList.value = res.rows
}
function resetSupplierSearch() {
supplierSearch.supplierName = ''
supplierSearch.contact1 = ''
loadSuppliers()
}
function handleSelectSupplier(row: PoSupplier) {
form.supplierId = row.supplierId
form.supplierCode = row.supplierCode
form.supplierName = row.supplierName
showSupplierDialog.value = false
}
// ============ Import dialog ============
const showImportDialog = ref(false)
const importSearch = reactive({ salesOrderCode: '', businessType: '', itemCode: '', itemName: '' })
const importList = ref<LeadInRow[]>([])
const selectedImportRows = ref<LeadInRow[]>([])
const importTableRef = ref()
async function loadImportData() {
const res = await getLeadInList({
salesOrderCode: importSearch.salesOrderCode || undefined,
itemCode: importSearch.itemCode || undefined,
itemName: importSearch.itemName || undefined,
businessType: importSearch.businessType || undefined
})
importList.value = res.rows
}
function handleImportSelectionChange(rows: LeadInRow[]) {
selectedImportRows.value = rows
}
function handleSelectAllImport() {
importTableRef.value?.toggleAllSelection()
}
function handleConfirmImport() {
if (selectedImportRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
const existingCodes = new Set((form.lines ?? []).map(l => `${l.itemCode}_${l.planCode}`))
const newLines: PurchaseOrderLine[] = selectedImportRows.value
.filter(item => !existingCodes.has(`${item.itemCode}_${item.purchaseCode}`))
.map((item, idx) => ({
lineId: 0,
orderId: 0,
orderCode: '',
lineNo: (form.lines?.length || 0) + idx + 1,
trackCode: item.salesOrderCode || '',
planCode: item.purchaseCode || '',
itemId: item.itemId,
itemCode: item.itemCode,
itemName: item.itemName,
specification: item.specification || '',
unitName: item.unitName || '',
needDate: item.demandDate || '',
quantity: (item.purchaseQty ?? 0) - (item.orderedQty ?? 0),
unitPrice: 0,
amount: 0,
arrivedQuantity: 0,
remark: ''
}))
form.lines = [...(form.lines || []), ...newLines]
form.lines.forEach((l, i) => { l.lineNo = i + 1 })
showImportDialog.value = false
ElMessage.success(`成功引入 ${newLines.length} 条物料`)
}
// ============ 选择物料弹窗(手工新增物料) ============
const showItemDialog = ref(false)
const itemTableLoading = ref(false)
const itemTableData = ref<MdItem[]>([])
const editingLineIndex = ref(-1)
const continuousSelect = ref(false)
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 tree filter text to filter tree nodes
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() {
// Reset search state each time dialog opens
itemSearch.itemCode = ''
itemSearch.itemName = ''
itemSearch.itemTypeId = undefined
itemTypeFilter.value = ''
continuousSelect.value = false
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)
// Convert to el-tree format: { id, label, children }
function toTreeNodes(items: ItemType[]): any[] {
return items.map(item => ({
id: item.itemTypeId,
label: item.itemTypeName || '',
raw: item,
children: item.children && item.children.length > 0 ? toTreeNodes(item.children) : undefined
}))
}
itemTypeTree.value = toTreeNodes(tree)
} catch (e) {
console.error('加载物料分类树失败:', 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) {
console.error('加载物料列表失败:', 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 || !form.lines) return
const row = form.lines[idx]
if (!row) return
row.itemId = selected.itemId ?? 0
row.itemCode = selected.itemCode ?? ''
row.itemName = selected.itemName ?? ''
row.specification = selected.specification ?? ''
row.unitName = selected.unitName ?? selected.unitOfMeasure ?? ''
calcLineAmount(row)
if (!continuousSelect.value) {
showItemDialog.value = false
} else {
ElMessage.success(`已选择: ${selected.itemCode} - ${selected.itemName}`)
}
}
function handleViewItem(row: MdItem) {
ElMessage.info(`物料详情: ${row.itemCode} - ${row.itemName}${row.specification || '无规格'}`)
}
// ============ Save ============
async function handleSave() {
try {
await formRef.value?.validate()
} catch {
ElMessage.warning('请完善必填信息')
return
}
if (!form.lines || form.lines.length === 0) {
ElMessage.warning('请至少添加一条物料明细')
return
}
const invalidLines = form.lines.filter(l => !l.itemCode || !l.quantity || l.quantity <= 0)
if (invalidLines.length > 0) {
ElMessage.warning('请检查物料信息物料编码不能为空数量须大于0')
return
}
computeTotals()
saveLoading.value = true
try {
if (mode.value === 'add') {
await createPurchaseOrder(form)
ElMessage.success('保存成功')
router.push('/purchasing/order')
} else {
await updatePurchaseOrder(form)
ElMessage.success('保存成功')
await loadFormData()
}
} catch (error) {
console.error('保存失败:', error)
} finally {
saveLoading.value = false
}
}
// ============ Cancel / Revert ============
function handleCancel() { router.push('/purchasing/order') }
async function handleRevert() {
try {
await ElMessageBox.confirm('确认撤回未保存的修改吗?', '提示', { type: 'warning' })
await loadFormData()
ElMessage.success('已撤回')
} catch { /* cancelled */ }
}
// ============ Approve / Unapprove ============
async function handleApprove() {
if (!form.orderId) {
ElMessage.warning('请先保存后再审核')
return
}
try {
await ElMessageBox.confirm('确认审核该采购订单吗?', '审核确认', { type: 'info' })
await approvePurchaseOrder(String(form.orderId))
ElMessage.success('审核成功')
await loadFormData()
} catch (error: any) {
if (error !== 'cancel') console.error('审核失败:', error)
}
}
async function handleUnapprove() {
if (!form.orderId) return
try {
await ElMessageBox.confirm('确认反审核该采购订单吗?', '反审核确认', { type: 'warning' })
await unapprovePurchaseOrder(String(form.orderId))
ElMessage.success('反审核成功')
router.replace(`/purchasing/order/edit/${form.orderId}`)
await loadFormData()
} catch (error: any) {
if (error !== 'cancel') console.error('反审核失败:', error)
}
}
// ============ Generate arrival notice ============
async function handleGenArrivalNotice() {
if (!form.orderId) return
try {
await ElMessageBox.confirm('确认根据当前采购订单生成到货通知单吗?', '生成到货通知单', { type: 'info' })
const noticeId = await genArrivalNotice(form.orderId)
ElMessage.success('到货通知单生成成功')
} catch (error: any) {
if (error !== 'cancel') console.error('生成到货通知单失败:', error)
}
}
// ============ View-mode actions ============
function handleNewFromView() { router.push('/purchasing/order/new') }
function handleSwitchToEdit() {
if (form.orderId) router.replace(`/purchasing/order/edit/${form.orderId}`)
}
const showPrintDialog = ref(false)
const printConfig = computed<PrintConfig>(() => ({
title: '采购订单',
qrCodeValue: form.orderCode || undefined,
headerFields: [
{ label: '单据编码', value: String(form.orderCode || '') },
{ label: '单据日期', value: String(form.orderDate || '') },
{ label: '单据状态', value: String(statusLabel.value || '') },
{ label: '业务类型', value: String(businessTypeOptions.find(o => o.value === form.businessType)?.label || form.businessType || '') },
{ label: '业务状态', value: String(businessStatusOptions.find(o => o.value === form.businessStatus)?.label || form.businessStatus || '') },
{ label: '供方', value: String(form.supplierName || '') },
{ label: '采购部门', value: String(form.deptName || '') },
{ label: '采购人员', value: String(form.userName || '') },
{ label: '到货日期', value: String(form.deliveryDate || '') },
{ label: '合同号', value: String(form.contractNo || '') },
{ label: '用料需求', value: String(form.materialNeed || '') },
{ label: '备注', value: String(form.remark || '') }
],
columns: [
{ prop: '_index', label: '序号', type: 'index', width: '50px', align: 'center' },
{ prop: 'trackCode', label: '跟单编号', width: '100px' },
{ prop: 'planCode', label: '计划单号', width: '100px' },
{ prop: 'itemCode', label: '物料编码', width: '120px' },
{ prop: 'itemName', label: '物料名称', width: '150px' },
{ prop: 'specification', label: '型号规格', width: '120px' },
{ prop: 'unitName', label: '主计量', width: '70px', align: 'center' },
{ prop: 'quantity', label: '数量', width: '80px', align: 'right' },
{ prop: 'unitPrice', label: '单价', width: '90px', align: 'right', type: 'amount' },
{ prop: 'amount', label: '金额', width: '100px', align: 'right', type: 'amount' },
{ prop: 'remark', label: '采购说明' }
],
data: form.lines || [],
summaries: [
{ label: '总数量', value: String(form.totalQuantity ?? 0) },
{ label: '总金额', value: Number(form.totalAmount ?? 0).toFixed(2) }
],
footer: {
creator: String(form.operatorName || ''),
approver: String(form.approverName || '')
}
}))
function handlePrint() { showPrintDialog.value = true }
// ============ Prev / Next navigation ============
const hasPrev = ref(false)
const hasNext = ref(false)
const siblingIds = ref<number[]>([])
const currentIdxInList = computed(() => {
const id = form.orderId
return id ? siblingIds.value.indexOf(id) : -1
})
function navigatePrev() {
const idx = currentIdxInList.value
if (idx > 0) {
const prevId = siblingIds.value[idx - 1]
router.replace(`/purchasing/order/${mode.value === 'edit' ? 'edit' : 'view'}/${prevId}`)
}
}
function navigateNext() {
const idx = currentIdxInList.value
if (idx >= 0 && idx < siblingIds.value.length - 1) {
const nextId = siblingIds.value[idx + 1]
router.replace(`/purchasing/order/${mode.value === 'edit' ? 'edit' : 'view'}/${nextId}`)
}
}
// ============ Lifecycle ============
onMounted(async () => {
if (mode.value === 'add') {
await initNewForm()
} else {
await loadFormData()
}
// Load import data for dialog when opened
nextTick(() => {
if (showImportDialog.value) loadImportData()
})
})
</script>
<style scoped>
.form-container { padding: 4px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.page-title { font-size: 16px; font-weight: 600; }
.header-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.header-form { margin-bottom: 8px; }
.material-section { margin-top: 8px; }
.material-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.section-title { font-size: 15px; font-weight: 600; color: #303133; }
.material-actions { display: flex; gap: 8px; }
.nav-links { display: flex; justify-content: flex-end; gap: 16px; margin-top: 12px; padding: 8px 0; }
.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; }
/* 选择物料弹窗 */
.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;
}
.category-tree :deep(.el-tree-node__label) { font-size: 13px; }
.category-tree :deep(.el-tree-node.is-current > .el-tree-node__content) {
background-color: #ecf5ff;
color: #409eff;
}
.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 {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
</style>