Files
shanxia2.0/web-admin/src/views/db/plans/index.vue
T
2026-08-21 16:50:47 +08:00

1078 lines
34 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">
<section class="panel">
<div class="filter-block">
<el-form :model="filters" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="日期范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
@change="handleDateRangeChange"
/>
</el-form-item>
<el-form-item label="业务类型">
<el-select
v-model="filters.businessType"
placeholder="全部"
clearable
@change="handleSearch"
>
<el-option
v-for="item in businessTypeOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="执行状态">
<el-select
v-model="filters.executionStatus"
placeholder="全部"
clearable
@change="handleSearch"
>
<el-option
v-for="item in executionStatusOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleResetFilters">
<el-icon><RefreshLeft /></el-icon>
重置
</el-button>
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row db-list-toolbar-row">
<ListToolbarDateShortcuts
variant="pills"
:active="activeDateShortcut"
aria-label="日期快捷筛选"
@change="handleDateShortcut"
/>
<ListQuickSearchInput
v-model="searchKeyword"
placeholder="往来单位 / 计划号 / 产品"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增计划
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleToolbarEdit"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleToolbarGenerateInventory"
>
<el-icon><DocumentAdd /></el-icon>
生成出入库单
</el-button>
<el-button
class="btn-ghost"
type="danger"
:disabled="
selectedRows.length !== 1 ||
!canDeletePlanRecord(selectedRows[0]) ||
!canDeleteByCreator(selectedRows[0])
"
:title="selectedRows.length === 1 ? getDeletePlanDisabledTitle(selectedRows[0]) : ''"
@click="handleToolbarDelete"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
<el-button class="btn-ghost" :loading="exportLoading" @click="handleExport">
<el-icon><Download /></el-icon>
导出 Excel
</el-button>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="loadData"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<div ref="plansTableWrapRef" class="table-scroll bx-list-table-wrap">
<ListTable table-key="db-plans"
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
@header-dragend="() => plansStatsRowRef?.resync?.()"
>
<el-table-column type="selection" min-width="55" align="center" />
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
<el-table-column
prop="planNo"
label="计划号"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">{{ row.planNo }}</el-link>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('businessType')"
prop="businessType"
label="业务类型"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.businessType === '入库' ? 'success' : 'warning'" size="small">
{{ row.businessType }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('customerName')"
prop="customerName"
label="往来单位"
min-width="240"
align="left"
header-align="left"
class-name="col-name-wrap"
label-class-name="col-name-wrap"
/>
<el-table-column
v-if="isColumnVisible('startDate')"
prop="startDate"
label="起始日期"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('endDate')"
prop="endDate"
label="截止日期"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('product')"
prop="product"
label="产品"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('specification')"
prop="specification"
label="规格"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('plannedQuantity')"
label="计划量(吨)"
prop="plannedQuantity"
min-width="120"
align="right"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('executedQuantity')"
label="已执行量(吨)"
min-width="130"
align="right"
header-align="center"
>
<template #default="{ row }">
{{ ((row.executedQuantity || 0) / 1000).toFixed(2) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('executionStatus')"
prop="executionStatus"
label="执行状态"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="getExecutionStatusType(getCorrectExecutionStatus(row))" size="small">
{{ getCorrectExecutionStatus(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('remarks')"
prop="remarks"
label="备注"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('autoGenerate')"
label="自动生成"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-switch
v-model="row.autoGenerateEnabled"
:active-value="1"
:inactive-value="0"
active-text="开"
inactive-text="关"
@change="(val) => handleToggleAutoGenerate(row, Number(val))"
/>
<div v-if="row.autoGenerateEndMonth" class="db-auto-end-hint">
结束:{{ row.autoGenerateEndMonth }}
</div>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('createByName')"
prop="createByName"
label="创建人"
min-width="100"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.createByName || row.createBy || '—' }}
</template>
</el-table-column>
</ListTable>
<ListColumnStatsRow
ref="plansStatsRowRef"
storage-key="db-plans-column-stats"
:columns="plansStatsRowColumns"
:data="tableData"
:table-wrap-el="plansTableWrapRef"
:get-cell-value="getPlansStatCellValue"
/>
</div>
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-pagination
v-show="total > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 100, 500]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadData"
@size-change="loadData"
/>
</div>
</div>
</section>
<el-dialog
v-model="modalOpen"
:title="editingPlan ? '修改计划' : '新增计划'"
width="760px"
align-center
destroy-on-close
:close-on-click-modal="false"
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
@close="handleModalClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="right"
label-width="100px"
class="db-dialog-form form-grid-2"
scroll-to-error
>
<el-form-item label="计划号" prop="planNo">
<el-input v-model="formData.planNo" placeholder="系统自动生成" />
</el-form-item>
<el-form-item label="业务类型" prop="businessType">
<el-radio-group v-model="formData.businessType">
<el-radio value="入库">入库</el-radio>
<el-radio value="出库">出库</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="自动按月生成">
<el-switch
v-model="formData.autoGenerateEnabled"
:active-value="1"
:inactive-value="0"
active-text="开启"
inactive-text="关闭"
/>
</el-form-item>
<el-form-item label="结束月份">
<el-date-picker
v-model="formData.autoGenerateEndMonth"
type="month"
placeholder="不选表示一直生成"
style="width: 100%"
format="YYYY-MM"
value-format="YYYY-MM"
:disabled="!formData.autoGenerateEnabled"
/>
</el-form-item>
<el-form-item label="往来单位" prop="customerName">
<DbCrmCustomerSelect
v-model="formData.customerName"
:allow-create="false"
placeholder="请从客户管理中搜索选择"
/>
</el-form-item>
<el-form-item label="产品" prop="productId">
<DbCrmProductSelect
v-model:product-id="formData.productId"
v-model:product-name="formData.product"
v-model:specification="formData.specification"
placeholder="请搜索并选择 CRM 产品"
/>
</el-form-item>
<el-form-item label="规格">
<el-input
v-model="formData.specification"
readonly
placeholder="选择产品后自动带出"
/>
</el-form-item>
<el-form-item label="计划开始时间" prop="startDate">
<el-date-picker
v-model="formData.startDate"
type="date"
placeholder="选择日期"
style="width: 100%"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="计划结束时间" prop="endDate">
<el-date-picker
v-model="formData.endDate"
type="date"
placeholder="选择日期"
style="width: 100%"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="计划量(吨)" prop="plannedQuantity">
<el-input-number
v-model="formData.plannedQuantity"
:min="0"
:precision="2"
placeholder="请输入计划吨位"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注" prop="remarks" class="form-col-span-2">
<el-input
v-model="formData.remarks"
type="textarea"
:rows="2"
placeholder="填写备注信息"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleModalClose">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button type="primary" :loading="saving" @click="handleSubmit">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<DbPlanRelatedViewDialog
v-model:visible="relatedModalOpen"
:loading="loadingRelated"
:data="relatedData"
>
<template v-if="viewingRecord" #hero-actions>
<el-button class="btn-main" type="primary" @click="handleEdit(viewingRecord)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button class="btn-ghost" @click="handleGenerateInventory(viewingRecord)">
<el-icon><DocumentAdd /></el-icon>
生成出入库单
</el-button>
<el-button
class="btn-ghost"
type="danger"
:disabled="!canDeletePlanRecord(viewingRecord) || !canDeleteByCreator(viewingRecord)"
:title="viewingRecord ? getDeletePlanDisabledTitle(viewingRecord) : ''"
@click="handleViewDelete"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</DbPlanRelatedViewDialog>
</div>
</template>
<script setup lang="ts">
/**
* UI 模式 P02 · 复杂业务列表
* 参考:/#/crm/quote · src/views/crm/quote/index.vue
*/
import {
Check,
Close,
Delete,
DocumentAdd,
Download,
Edit,
Plus,
RefreshLeft,
Search,
} from '@element-plus/icons-vue'
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
import type { StatsRowColumn } from '@/utils/list-column-stats'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
import DbPlanRelatedViewDialog from '@/components/business/db/DbPlanRelatedViewDialog.vue'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { $modal } from '@/utils/modal'
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
import {
planApi,
type Plan,
type PlanCreate,
type PlanListParams,
type PlanRelatedData,
type PlanUpdate
} from '@/api/db/plans'
defineOptions({ name: 'DbPlans' })
const router = useRouter()
const listColumnDefs = [
{ prop: 'businessType', label: '业务类型' },
{ prop: 'customerName', label: '往来单位' },
{ prop: 'startDate', label: '起始日期' },
{ prop: 'endDate', label: '截止日期' },
{ prop: 'product', label: '产品' },
{ prop: 'specification', label: '规格' },
{ prop: 'plannedQuantity', label: '计划量(吨)' },
{ prop: 'executedQuantity', label: '已执行量(吨)' },
{ prop: 'executionStatus', label: '执行状态' },
{ prop: 'remarks', label: '备注' },
{ prop: 'autoGenerate', label: '自动生成' },
{ prop: 'createByName', label: '创建人' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('db-plans-list-columns', listColumnDefs)
const plansTableWrapRef = ref<HTMLElement | null>(null)
const plansStatsRowRef = ref<{ resync?: () => void } | null>(null)
const plansStatsRowColumns = computed((): StatsRowColumn[] => {
const cols: StatsRowColumn[] = [
{ key: '__selection', interactive: false },
{ key: '__index', interactive: false },
{ key: 'planNo', interactive: true, prop: 'planNo' }
]
for (const def of listColumnDefs) {
if (!isColumnVisible(def.prop)) continue
cols.push({
key: def.prop,
interactive: true,
prop: def.prop === 'autoGenerate' ? 'autoGenerateEnabled' : def.prop
})
}
return cols
})
function getPlansStatCellValue(row: Plan, prop: string): unknown {
if (prop === 'executedQuantity') {
return (row.executedQuantity || 0) / 1000
}
if (prop === 'createByName') {
return row.createByName || row.createBy || ''
}
return (row as any)?.[prop]
}
const loading = ref(false)
const exportLoading = ref(false)
const saving = ref(false)
const tableData = ref<Plan[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
const searchKeyword = ref('')
const selectedRows = ref<Plan[]>([])
const activeDateShortcut = ref<'all' | 'today' | 'tomorrow' | 'yesterday' | 'week' | ''>('')
const filters = reactive({
businessType: '' as '' | '入库' | '出库',
executionStatus: ''
})
const businessTypeOptions: Array<'入库' | '出库'> = ['入库', '出库']
const executionStatusOptions = ['待入库', '入库中', '已入库', '待出库', '出库中', '已出库']
const pad2 = (n: number) => String(n).padStart(2, '0')
const formatYmd = (d: Date) =>
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
/** 默认筛选:当前月 1 日 ~ 当前月最后一天(对齐原地磅 art-design-pro) */
const getCurrentMonthDateRange = (): [string, string] => {
const now = new Date()
return [
formatYmd(new Date(now.getFullYear(), now.getMonth(), 1)),
formatYmd(new Date(now.getFullYear(), now.getMonth() + 1, 0))
]
}
const dateRange = ref<[string, string] | []>(getCurrentMonthDateRange())
const modalOpen = ref(false)
const editingPlan = ref<Plan | null>(null)
const formRef = ref()
const formData = reactive<
PlanCreate & {
product: string
specification: string
}
>({
planNo: '',
planDate: formatYmd(new Date()),
startDate: formatYmd(new Date()),
endDate: '',
businessType: '入库',
productId: '',
product: '',
specification: '',
plannedQuantity: 0,
vehicleNo: '',
customerName: '',
remarks: '',
autoGenerateEnabled: 0,
autoGenerateEndMonth: ''
})
const formRules = {
businessType: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
productId: [{ required: true, message: '请选择 CRM 产品', trigger: 'change' }],
plannedQuantity: [{ required: true, message: '请输入计划量', trigger: 'blur' }]
}
const buildListQueryParams = (): PlanListParams => {
const params: PlanListParams = {}
const keyword = searchKeyword.value.trim()
if (keyword) params.q = keyword
if (dateRange.value?.[0] && dateRange.value?.[1]) {
params.startDateFrom = dateRange.value[0]
params.startDateTo = dateRange.value[1]
}
if (filters.businessType) params.businessType = filters.businessType
if (filters.executionStatus) params.executionStatus = filters.executionStatus
return params
}
const fetchFilteredPlans = async () => {
return planApi.list(buildListQueryParams())
}
const getCorrectExecutionStatus = (row: Plan): string => {
if (!row.executionStatus) {
return row.businessType === '入库' ? '待入库' : '待出库'
}
if (row.businessType === '入库') {
if (
row.executionStatus === '待出库' ||
row.executionStatus === '出库中' ||
row.executionStatus === '已出库'
) {
return (row.executedQuantity || 0) > 0 ? '入库中' : '待入库'
}
} else if (row.businessType === '出库') {
if (
row.executionStatus === '待入库' ||
row.executionStatus === '入库中' ||
row.executionStatus === '已入库'
) {
return (row.executedQuantity || 0) > 0 ? '出库中' : '待出库'
}
}
return row.executionStatus
}
const getExecutionStatusType = (
status: string | undefined
): 'success' | 'warning' | 'info' | 'primary' | 'danger' => {
if (!status) return 'info'
if (status === '已入库' || status === '已出库') return 'success'
if (status === '入库中' || status === '出库中') return 'info'
if (status === '待入库' || status === '待出库') return 'warning'
return 'info'
}
const loadData = async () => {
loading.value = true
try {
const records = await fetchFilteredPlans()
total.value = records.length
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
tableData.value = records.slice(start, end)
} catch (error) {
console.error('加载计划列表失败', error)
} finally {
loading.value = false
}
}
const handleSearch = () => {
currentPage.value = 1
loadData()
}
const handleQuickSearch = () => {
currentPage.value = 1
loadData()
}
const handleQuickSearchClear = () => {
searchKeyword.value = ''
handleQuickSearch()
}
const handleSelectionChange = (selection: Plan[]) => {
selectedRows.value = selection
}
const handleResetFilters = () => {
searchKeyword.value = ''
filters.businessType = ''
filters.executionStatus = ''
dateRange.value = getCurrentMonthDateRange()
activeDateShortcut.value = ''
currentPage.value = 1
loadData()
}
const handleDateShortcut = (type: 'all' | 'today' | 'tomorrow' | 'yesterday' | 'week') => {
activeDateShortcut.value = type
const today = new Date()
if (type === 'all') {
dateRange.value = []
} else if (type === 'today') {
const d = formatYmd(today)
dateRange.value = [d, d]
} else if (type === 'tomorrow') {
const t = new Date(today)
t.setDate(t.getDate() + 1)
const d = formatYmd(t)
dateRange.value = [d, d]
} else if (type === 'yesterday') {
const y = new Date(today)
y.setDate(y.getDate() - 1)
const d = formatYmd(y)
dateRange.value = [d, d]
} else if (type === 'week') {
const weekday = today.getDay() === 0 ? 7 : today.getDay()
const monday = new Date(today)
monday.setDate(today.getDate() - (weekday - 1))
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
dateRange.value = [formatYmd(monday), formatYmd(sunday)]
}
handleSearch()
}
const handleDateRangeChange = () => {
activeDateShortcut.value = ''
handleSearch()
}
const resetFormData = () => {
Object.assign(formData, {
planNo: '',
planDate: formatYmd(new Date()),
startDate: formatYmd(new Date()),
endDate: '',
businessType: '入库',
productId: '',
product: '',
specification: '',
plannedQuantity: 0,
vehicleNo: '',
customerName: '',
remarks: '',
autoGenerateEnabled: 0,
autoGenerateEndMonth: ''
})
}
const handleAdd = () => {
editingPlan.value = null
resetFormData()
modalOpen.value = true
}
const handleEdit = (record: Plan) => {
editingPlan.value = record
Object.assign(formData, {
planNo: record.planNo || '',
planDate: record.planDate,
startDate: record.startDate || '',
endDate: record.endDate || '',
businessType: record.businessType,
productId: record.productId || '',
product: record.product,
specification: record.specification || '',
plannedQuantity: record.plannedQuantity,
vehicleNo: record.vehicleNo || '',
customerName: record.customerName || '',
remarks: record.remarks || '',
autoGenerateEnabled: record.autoGenerateEnabled ?? 0,
autoGenerateEndMonth: record.autoGenerateEndMonth || ''
})
modalOpen.value = true
}
const relatedData = ref<PlanRelatedData>({
inventories: [],
weighings: []
})
const relatedModalOpen = ref(false)
const loadingRelated = ref(false)
const viewingRecord = ref<Plan | null>(null)
async function refreshRelatedViewIfOpen(editedId?: string) {
if (!relatedModalOpen.value || !viewingRecord.value?.id) return
if (editedId && viewingRecord.value.id !== editedId) return
const records = await fetchFilteredPlans()
syncViewRowFromFlatList(viewingRecord, records)
relatedData.value = await planApi.getRelated(viewingRecord.value.id)
}
const handleView = async (record: Plan) => {
viewingRecord.value = record
loadingRelated.value = true
try {
relatedData.value = await planApi.getRelated(record.id)
relatedModalOpen.value = true
} catch (error) {
console.error('加载关联数据失败', error)
} finally {
loadingRelated.value = false
}
}
function handleToolbarEdit() {
if (selectedRows.value.length !== 1) {
$modal.msgWarning('请先勾选一条计划')
return
}
handleEdit(selectedRows.value[0])
}
function handleToolbarGenerateInventory() {
if (selectedRows.value.length !== 1) {
$modal.msgWarning('请先勾选一条计划')
return
}
handleGenerateInventory(selectedRows.value[0])
}
function handleToolbarDelete() {
if (selectedRows.value.length !== 1) {
$modal.msgWarning('请先勾选一条计划')
return
}
handleDelete(selectedRows.value[0])
}
async function handleViewDelete() {
if (!viewingRecord.value) return
await handleDelete(viewingRecord.value)
relatedModalOpen.value = false
}
const executionCompletedStatuses = ['已入库', '已出库']
const canDeletePlanRecord = (row: Plan) => {
return !row.executionStatus || !executionCompletedStatuses.includes(row.executionStatus)
}
const getDeletePlanDisabledTitle = (row: Plan) => {
if (!canDeletePlanRecord(row)) return '已完成的计划不能删除'
if (!canDeleteByCreator(row)) return CREATOR_DELETE_TIP
return ''
}
const handleDelete = async (row: Plan) => {
if (!canDeletePlanRecord(row)) {
$modal.msgWarning('已完成的计划不能删除')
return
}
if (!canDeleteByCreator(row)) {
$modal.msgWarning(CREATOR_DELETE_TIP)
return
}
try {
await $modal.confirm('确定要删除这个计划吗?')
await planApi.delete(row.id)
$modal.msgSuccess('删除成功')
loadData()
} catch {
// 用户取消或请求失败
}
}
const handleGenerateInventory = (record: Plan) => {
router.push({
path: '/db/inventory',
query: {
planNo: record.planNo || '',
product: record.product || '',
productId: record.productId || '',
specification: record.specification || '',
businessType: record.businessType || '',
customerName: record.customerName || '',
purchasePlanId: record.id,
from: 'plan'
}
})
$modal.msgSuccess('已跳转到出入库页面,请填写详细信息')
}
const handleToggleAutoGenerate = async (record: Plan, value: number) => {
const normalizedValue = value === 1 ? 1 : 0
try {
await planApi.update(record.id, {
autoGenerateEnabled: normalizedValue,
autoGenerateEndMonth: record.autoGenerateEndMonth
})
$modal.msgSuccess(normalizedValue === 1 ? '已开启自动生成' : '已关闭自动生成')
loadData()
} catch (error) {
console.error('更新自动生成失败', error)
record.autoGenerateEnabled = normalizedValue === 1 ? 0 : 1
}
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
saving.value = true
try {
const payload: PlanCreate | PlanUpdate = editingPlan.value
? {
planNo: formData.planNo || undefined,
planDate: formData.planDate,
startDate: formData.startDate || undefined,
endDate: formData.endDate || undefined,
businessType: formData.businessType,
productId: formData.productId,
plannedQuantity: formData.plannedQuantity,
vehicleNo: formData.vehicleNo || undefined,
customerName: formData.customerName || undefined,
remarks: formData.remarks || undefined,
autoGenerateEnabled: formData.autoGenerateEnabled,
autoGenerateEndMonth: formData.autoGenerateEndMonth || undefined
}
: {
planNo: formData.planNo || undefined,
planDate: formData.planDate,
startDate: formData.startDate || undefined,
endDate: formData.endDate || undefined,
businessType: formData.businessType,
productId: formData.productId,
plannedQuantity: formData.plannedQuantity,
vehicleNo: formData.vehicleNo || undefined,
customerName: formData.customerName || undefined,
remarks: formData.remarks || undefined,
autoGenerateEnabled: formData.autoGenerateEnabled,
autoGenerateEndMonth: formData.autoGenerateEndMonth || undefined
}
if (editingPlan.value) {
const editedId = editingPlan.value.id
await planApi.update(editingPlan.value.id, payload as PlanUpdate)
$modal.msgSuccess('更新成功')
modalOpen.value = false
await loadData()
await refreshRelatedViewIfOpen(editedId)
} else {
await planApi.create(payload as PlanCreate)
$modal.msgSuccess('创建成功')
modalOpen.value = false
loadData()
}
} catch (error) {
console.error('保存计划失败', error)
} finally {
saving.value = false
}
})
}
const handleModalClose = () => {
modalOpen.value = false
editingPlan.value = null
formRef.value?.resetFields()
}
const handleExport = async () => {
exportLoading.value = true
try {
const records = await fetchFilteredPlans()
if (records.length === 0) {
$modal.msgWarning('没有数据可导出')
return
}
const headers = [
'计划号',
'往来单位',
'计划日期',
'起始日期',
'截止日期',
'产品',
'规格',
'车牌号',
'计划量',
'已执行量',
'执行状态',
'备注'
]
const csvContent = [
headers.join(','),
...records.map((r) =>
[
r.planNo,
r.customerName,
r.planDate,
r.startDate,
r.endDate,
r.product,
r.specification,
r.vehicleNo,
r.plannedQuantity,
r.executedQuantity || 0,
r.executionStatus,
r.remarks || ''
].join(',')
)
].join('\n')
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
const now = new Date()
const stamp = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`
a.download = `计划列表_${stamp}.csv`
a.click()
URL.revokeObjectURL(url)
$modal.msgSuccess('导出成功')
} catch (error) {
console.error('导出失败', error)
} finally {
exportLoading.value = false
}
}
useDbInstantSearch(
() => ({
q: searchKeyword.value,
dateRange: dateRange.value,
businessType: filters.businessType,
executionStatus: filters.executionStatus
}),
handleSearch
)
onMounted(() => {
if (!dateRange.value[0] || !dateRange.value[1]) {
dateRange.value = getCurrentMonthDateRange()
}
loadData()
})
</script>
<style scoped lang="scss">
.bx-list-table-wrap {
min-width: 0;
}
</style>