1078 lines
34 KiB
Vue
1078 lines
34 KiB
Vue
<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>
|