Files
my-mom-system/erp-frontend-vue/src/views/Production/PurchasePlan/index.vue

764 lines
23 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="page-container">
<!-- 搜索区域 -->
<el-card class="search-card" shadow="never">
<el-form :model="queryParams" :inline="true" @submit.prevent="handleQuery">
<!-- 明细视图搜索 -->
<template v-if="viewMode === 'detail'">
<el-form-item label="跟单单号">
<el-input v-model="queryParams.salesOrderCode" placeholder="请输入" clearable style="width: 140px" />
</el-form-item>
<el-form-item label="单据编码">
<el-input v-model="queryParams.purchaseCode" placeholder="请输入" clearable style="width: 140px" />
</el-form-item>
<el-form-item label="物料编码">
<el-input v-model="queryParams.itemCode" placeholder="请输入" clearable style="width: 140px" />
</el-form-item>
<el-form-item label="物料名称">
<el-input v-model="queryParams.itemName" placeholder="请输入" clearable style="width: 140px" />
</el-form-item>
</template>
<!-- 单据视图搜索 -->
<template v-else>
<el-form-item label="单据编码">
<el-input v-model="queryParams.purchaseCode" placeholder="请输入" clearable style="width: 160px" />
</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="APPROVED" />
<el-option label="退回" value="REJECTED" />
</el-select>
</el-form-item>
<el-form-item label="业务状态">
<el-select v-model="queryParams.businessStatus" placeholder="全部" clearable style="width: 120px">
<el-option label="正常" value="NORMAL" />
<el-option label="暂停" value="PAUSE" />
<el-option label="取消" value="CANCEL" />
<el-option label="完成" value="COMPLETED" />
</el-select>
</el-form-item>
</template>
<!-- 公共搜索日期范围 -->
<el-form-item label="开始日期">
<el-date-picker
v-model="queryParams.beginDate"
type="date"
placeholder="开始日期"
value-format="YYYY-MM-DD"
style="width: 140px"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="queryParams.endDate"
type="date"
placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 140px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>搜索
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 工具栏与表格 -->
<el-card class="table-card" shadow="never">
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<!-- 视图切换按钮 -->
<el-button
v-if="viewMode === 'detail'"
type="primary"
@click="switchView('document')"
>单据</el-button>
<el-button
v-else
type="primary"
@click="switchView('detail')"
>明细</el-button>
<el-button @click="handleQueryAll">
<el-icon><Tickets /></el-icon>查询所有
</el-button>
<el-button type="success" @click="handleAdd">
<el-icon><Plus /></el-icon>新增
</el-button>
<!-- 明细视图特有按钮 -->
<el-button v-if="viewMode === 'detail'" @click="handleExport">
<el-icon><Download /></el-icon>导出
</el-button>
<!-- 单据视图特有按钮 -->
<template v-if="viewMode === 'document'">
<el-button
type="danger"
:disabled="selectedRows.length === 0"
@click="handleBatchDelete"
>
<el-icon><Delete /></el-icon>删除
</el-button>
<el-button
type="primary"
:disabled="!canBatchApprove"
@click="handleBatchApprove"
>
<el-icon><Check /></el-icon>审核
</el-button>
<el-button
:disabled="!canBatchUnapprove"
@click="handleBatchUnapprove"
>反审核
</el-button>
</template>
</div>
<!-- 明细视图 - 物料分类快捷筛选标签 -->
<div v-if="viewMode === 'detail'" class="toolbar-right">
<div class="category-tags">
<el-check-tag
v-for="tag in categoryTags"
:key="tag.itemTypeId"
:checked="queryParams.itemTypeId === tag.itemTypeId"
@change="handleCategoryTag(tag.itemTypeId)"
>{{ tag.itemTypeName }}</el-check-tag>
</div>
</div>
</div>
<!-- ============ 明细视图表格 ============ -->
<el-table
v-if="viewMode === 'detail'"
v-loading="loading"
:data="tableData"
stripe
border
style="width: 100%"
:max-height="tableHeight"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="salesOrderCode" label="跟单编号" width="140">
<template #default="{ row }">
<el-link v-if="row.salesOrderCode" type="primary">{{ row.salesOrderCode }}</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="deliveryDate" label="订单交期" width="110" align="center" />
<el-table-column prop="purchaseCode" label="单据编码" width="140">
<template #default="{ row }">
<el-link type="primary" @click="handleViewDetail(row)">{{ row.purchaseCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="status" label="单据状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">{{ getStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="itemName" label="物料名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="itemCode" label="物料编码" width="120" />
<el-table-column prop="purchaseQty" label="采购数量" width="100" align="right">
<template #default="{ row }">{{ formatNumber(row.purchaseQty) }}</template>
</el-table-column>
<el-table-column prop="orderedQty" label="已订数量" width="100" align="right">
<template #default="{ row }">{{ formatNumber(row.orderedQty ?? 0) }}</template>
</el-table-column>
<el-table-column prop="purchaseDate" label="单据日期" width="110" align="center" />
<el-table-column prop="remark" label="采购说明" width="120" show-overflow-tooltip />
</el-table>
<!-- ============ 单据视图表格 ============ -->
<el-table
v-else
v-loading="loading"
:data="tableData"
stripe
border
style="width: 100%"
:max-height="tableHeight"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="purchaseCode" label="单据编码" width="140">
<template #default="{ row }">
<el-link type="primary" @click="handleViewDetail(row)">{{ row.purchaseCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="purchaseDate" label="单据日期" width="120" align="center" sortable />
<el-table-column prop="status" label="单据状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">{{ getStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="businessType" label="业务类型" width="100" align="center">
<template #default="{ row }">{{ getBusinessTypeLabel(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="businessStatus" label="业务状态" width="90" align="center">
<template #default="{ row }">{{ getBusinessStatusLabel(row.businessStatus) }}</template>
</el-table-column>
<el-table-column prop="approveDate" label="审核日期" width="120" align="center" />
<el-table-column label="操作" width="160" align="center" fixed="right">
<template #default="{ row }">
<el-button type="success" link size="small" @click="handleViewDetail(row)">查看</el-button>
<el-button
v-if="isEditable(row.status)"
type="primary"
link
size="small"
@click="handleEdit(row)"
>编辑</el-button>
<el-button
v-if="isApproved(row.status)"
type="primary"
link
size="small"
@click="handleCopyPlan(row)"
>翻单</el-button>
<el-popconfirm
v-if="isEditable(row.status)"
title="确认删除该采购计划吗?"
@confirm="handleDeleteRow(row)"
>
<template #reference>
<el-button type="danger" link size="small">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 底部汇总和分页 -->
<div class="table-footer">
<div class="summary">
采购数量<span class="summary-value primary">{{ formatNumber(totalPurchaseQty) }}</span>
已订数量<span class="summary-value primary">{{ formatNumber(totalOrderedQty) }}</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="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 导出确认弹窗 -->
<el-dialog v-model="showExportDialog" title="导出确认" width="420px">
<p>确认导出当前查询条件下的采购计划数据吗</p>
<template #footer>
<el-button @click="showExportDialog = false">取消</el-button>
<el-button type="primary" :loading="exportLoading" @click="confirmExport">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Delete, Download, Check, Tickets } from '@element-plus/icons-vue'
import {
getPurchasePlanList,
getPurchasePlanDocList,
getPurchasePlanSummary,
deletePurchasePlan,
approvePurchasePlan,
unapprovePurchasePlan,
exportPurchasePlan,
getItemTypeList,
STATUS_MAP,
BUSINESS_STATUS_MAP,
BUSINESS_TYPE_OPTIONS,
type PurchasePlan,
type PurchasePlanQuery,
type PurchasePlanSummary
} from '@/api/purchasePlan'
const router = useRouter()
// ============ 响应式状态 ============
/** 当前视图模式:明细视图 / 单据视图 */
const viewMode = ref<'detail' | 'document'>('detail')
const loading = ref(false)
/** 本地搜索参数用户交互用loadData 时转换为 RuoYi 格式) */
const queryParams = reactive({
salesOrderCode: '',
purchaseCode: '',
itemCode: '',
itemName: '',
itemTypeId: '' as number | string,
needType: 0,
beginDate: '',
endDate: '',
status: '',
businessStatus: '',
pageNum: 1,
pageSize: 100
})
const tableData = ref<PurchasePlan[]>([])
const total = ref(0)
const selectedRows = ref<PurchasePlan[]>([])
const tableHeight = ref(500)
/** 物料分类快捷筛选标签 */
const categoryTags = ref<Array<{ itemTypeId: number; itemTypeName: string }>>([])
/** 底部汇总数据(来自后端 /summary 接口) */
const summaryData = ref<PurchasePlanSummary>({ totalPurchaseQty: 0, totalOrderedQty: 0 })
/** 导出弹窗 */
const showExportDialog = ref(false)
const exportLoading = ref(false)
// ============ 计算属性 ============
/** 底部汇总优先使用后端 summary 接口fallback 到前端计算 */
const totalPurchaseQty = computed(() =>
summaryData.value.totalPurchaseQty || tableData.value.reduce((sum, r) => sum + (r.purchaseQty || 0), 0)
)
const totalOrderedQty = computed(() =>
summaryData.value.totalOrderedQty || tableData.value.reduce((sum, r) => sum + (r.orderedQty || 0), 0)
)
const canBatchApprove = computed(() =>
selectedRows.value.length > 0 &&
selectedRows.value.every(row => isEditable(row.status))
)
const canBatchUnapprove = computed(() =>
selectedRows.value.length > 0 &&
selectedRows.value.every(row => isApproved(row.status))
)
// ============ 工具方法 ============
function getStatusType(status: string): string {
return (STATUS_MAP[status]?.type ?? 'info') as string
}
function getStatusLabel(status: string): string {
return STATUS_MAP[status]?.label ?? status ?? '开立'
}
function getBusinessTypeLabel(type: string): string {
const opt = BUSINESS_TYPE_OPTIONS.find(o => o.value === type)
return opt?.label ?? type ?? '-'
}
function getBusinessStatusLabel(status: string): string {
return BUSINESS_STATUS_MAP[status] ?? status ?? '-'
}
function isEditable(status: string): boolean {
return status === 'PREPARE' || status === '开立' || status === 'REJECTED' || status === '退回'
}
function isApproved(status: string): boolean {
return status === 'APPROVED' || status === '审核'
}
function formatNumber(val: number | undefined | null): string {
if (val === undefined || val === null) return '-'
return Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
}
// ============ 数据加载 ============
/** 将本地 queryParams 转换为后端 RuoYi 格式参数 */
function buildApiParams(): PurchasePlanQuery {
const p: PurchasePlanQuery = {
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
needType: queryParams.needType
}
if (queryParams.salesOrderCode) p.salesOrderCode = queryParams.salesOrderCode
if (queryParams.purchaseCode) p.purchaseCode = queryParams.purchaseCode
if (queryParams.itemCode) p.itemCode = queryParams.itemCode
if (queryParams.itemName) p.itemName = queryParams.itemName
if (queryParams.status) p.status = queryParams.status
if (queryParams.businessStatus) p.businessStatus = queryParams.businessStatus
if (queryParams.beginDate) p['params[beginDate]'] = queryParams.beginDate
if (queryParams.endDate) p['params[endDate]'] = queryParams.endDate
if (queryParams.itemTypeId) p['params[itemTypeId]'] = queryParams.itemTypeId
return p
}
async function loadData() {
loading.value = true
try {
const params = buildApiParams()
let res
if (viewMode.value === 'detail') {
res = await getPurchasePlanList(params)
} else {
res = await getPurchasePlanDocList(params)
}
tableData.value = res.rows
total.value = res.total
// 同时加载底部汇总
loadSummary(params)
} catch (error) {
console.error('加载数据失败:', error)
} finally {
loading.value = false
}
}
/** 加载底部汇总数据 */
async function loadSummary(params: PurchasePlanQuery) {
try {
const summary = await getPurchasePlanSummary(params)
summaryData.value = summary
} catch (error) {
console.error('加载汇总失败:', error)
}
}
async function loadCategoryTags() {
try {
categoryTags.value = await getItemTypeList()
} catch (error) {
console.error('加载物料分类失败:', error)
}
}
// ============ 搜索与筛选 ============
function handleQuery() {
queryParams.pageNum = 1
loadData()
}
function handleQueryAll() {
// 重置所有搜索条件
queryParams.salesOrderCode = ''
queryParams.purchaseCode = ''
queryParams.itemCode = ''
queryParams.itemName = ''
queryParams.itemTypeId = ''
queryParams.beginDate = ''
queryParams.endDate = ''
queryParams.status = ''
queryParams.businessStatus = ''
queryParams.pageNum = 1
loadData()
}
function handleCategoryTag(typeId: number) {
if (queryParams.itemTypeId === typeId) {
queryParams.itemTypeId = '' // 取消选中
} else {
queryParams.itemTypeId = typeId
}
handleQuery()
}
// ============ 视图切换 ============
function switchView(mode: 'detail' | 'document') {
// 保留公共搜索条件
const commonFields = {
purchaseCode: queryParams.purchaseCode,
beginDate: queryParams.beginDate,
endDate: queryParams.endDate,
pageNum: 1,
pageSize: queryParams.pageSize,
needType: queryParams.needType
}
// 清空视图特有字段
queryParams.salesOrderCode = ''
queryParams.itemCode = ''
queryParams.itemName = ''
queryParams.itemTypeId = ''
queryParams.status = ''
queryParams.businessStatus = ''
// 恢复公共字段
Object.assign(queryParams, commonFields)
viewMode.value = mode
selectedRows.value = []
loadData()
}
// ============ 选择操作 ============
function handleSelectionChange(rows: PurchasePlan[]) {
selectedRows.value = rows
}
// ============ 路由操作 ============
function handleAdd() {
router.push('/production/purchase-plan/new')
}
function handleViewDetail(row: PurchasePlan) {
router.push(`/production/purchase-plan/view/${row.purchaseId}`)
}
function handleEdit(row: PurchasePlan) {
router.push(`/production/purchase-plan/edit/${row.purchaseId}`)
}
function handleCopyPlan(row: PurchasePlan) {
// 翻单:复制采购计划生成新单据
router.push({
path: '/production/purchase-plan/new',
query: { copyFrom: String(row.purchaseId) }
})
}
// ============ 删除操作 ============
async function handleDeleteRow(row: PurchasePlan) {
try {
await deletePurchasePlan(String(row.purchaseId))
ElMessage.success('删除成功')
loadData()
} catch (error) {
console.error('删除失败:', error)
}
}
async function handleBatchDelete() {
if (selectedRows.value.length === 0) return
// 校验状态
const invalidRows = selectedRows.value.filter(row => !isEditable(row.status))
if (invalidRows.length > 0) {
ElMessage.warning('只能删除开立或退回状态的单据')
return
}
try {
await ElMessageBox.confirm(
`确认删除选中的 ${selectedRows.value.length} 条采购计划吗?`,
'删除确认',
{ type: 'warning' }
)
const ids = selectedRows.value.map(row => row.purchaseId).join(',')
await deletePurchasePlan(ids)
ElMessage.success('删除成功')
selectedRows.value = []
loadData()
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除失败:', error)
}
}
}
// ============ 审核操作 ============
async function handleBatchApprove() {
if (selectedRows.value.length === 0) return
const invalidRows = selectedRows.value.filter(row => !isEditable(row.status))
if (invalidRows.length > 0) {
ElMessage.warning('只能审核开立或退回状态的单据')
return
}
try {
await ElMessageBox.confirm(
`确认审核选中的 ${selectedRows.value.length} 条采购计划吗?`,
'审核确认',
{ type: 'info' }
)
for (const row of selectedRows.value) {
await approvePurchasePlan(row.purchaseId)
}
ElMessage.success('审核成功')
selectedRows.value = []
loadData()
} catch (error: any) {
if (error !== 'cancel') {
console.error('审核失败:', error)
}
}
}
// ============ 反审核操作 ============
async function handleBatchUnapprove() {
if (selectedRows.value.length === 0) return
const invalidRows = selectedRows.value.filter(row => !isApproved(row.status))
if (invalidRows.length > 0) {
ElMessage.warning('只能反审核已审核状态的单据')
return
}
try {
await ElMessageBox.confirm(
`确认反审核选中的 ${selectedRows.value.length} 条采购计划吗?反审核后可重新编辑。`,
'反审核确认',
{ type: 'warning' }
)
const ids = selectedRows.value.map(row => row.purchaseId).join(',')
await unapprovePurchasePlan(ids)
ElMessage.success('反审核成功')
selectedRows.value = []
loadData()
} catch (error: any) {
if (error !== 'cancel') {
console.error('反审核失败:', error)
}
}
}
// ============ 导出操作 ============
function handleExport() {
showExportDialog.value = true
}
async function confirmExport() {
exportLoading.value = true
try {
const blob = (await exportPurchasePlan(queryParams)).data
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `采购计划单_${new Date().toISOString().split('T')[0]}.xlsx`
link.click()
window.URL.revokeObjectURL(url)
showExportDialog.value = false
ElMessage.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
} finally {
exportLoading.value = false
}
}
// ============ 分页操作 ============
function handleSizeChange() {
queryParams.pageNum = 1
loadData()
}
function handleCurrentChange() {
loadData()
}
// ============ 表格高度 ============
function calcTableHeight() {
tableHeight.value = window.innerHeight - 310
}
// ============ 生命周期 ============
onMounted(() => {
loadData()
loadCategoryTags()
calcTableHeight()
window.addEventListener('resize', calcTableHeight)
})
onUnmounted(() => {
window.removeEventListener('resize', calcTableHeight)
})
</script>
<style scoped>
.page-container {
padding: 8px;
}
.search-card {
margin-bottom: 12px;
}
.search-card :deep(.el-card__body) {
padding: 12px 12px 0;
}
.search-card :deep(.el-form-item) {
margin-bottom: 12px;
margin-right: 12px;
}
.table-card :deep(.el-card__body) {
padding: 12px;
}
.toolbar {
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.toolbar-left {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.toolbar-right {
display: flex;
align-items: center;
overflow-x: auto;
}
.category-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.category-tags :deep(.el-check-tag) {
cursor: pointer;
font-size: 12px;
padding: 2px 10px;
}
.table-footer {
margin-top: 12px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.summary {
font-size: 14px;
color: #606266;
}
.summary-value {
font-weight: bold;
margin-left: 4px;
margin-right: 20px;
}
.summary-value.primary {
color: #409eff;
}
</style>