潘的第一次 commit

This commit is contained in:
panchengyong
2026-02-27 23:50:25 +08:00
commit 10b6d0099a
117 changed files with 32547 additions and 0 deletions

View File

@@ -0,0 +1,949 @@
<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 @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,
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]
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)
}
}
// ============ 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>