潘的第一次 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,763 @@
<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)
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>