This commit is contained in:
zlwu committed 2026-06-10 22:59:34 +08:00
1 parent 725774ef0c
commit c3be9d1dce
176 files changed
+41925 -6342

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
<template>
<div class="page-container db-page">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">仪表盘</h2>
</div>
<div class="db-dashboard-body" v-loading="loading">
<div class="db-dashboard-stats">
<div v-for="stat in statsList" :key="stat.title" class="db-stat-card">
<p class="db-stat-title">{{ stat.title }}</p>
<p class="db-stat-value">
{{ stat.value }}<span class="db-stat-suffix">{{ stat.suffix }}</span>
</p>
</div>
</div>
<el-row :gutter="16" class="db-dashboard-charts">
<el-col :xs="24" :lg="12">
<DualBarChart title="今日入库量(按货物,t)" :data="inboundByCargo" />
</el-col>
<el-col :xs="24" :lg="12">
<DualBarChart title="今日出库量(按货物,t)" :data="outboundByCargo" />
</el-col>
</el-row>
<el-row :gutter="16" class="db-dashboard-charts">
<el-col :xs="24" :lg="12">
<DualBarChart title="今日入库量(按往来单位+货物,t)" :data="inboundByPartnerCargo" />
</el-col>
<el-col :xs="24" :lg="12">
<DualBarChart title="今日出库量(按往来单位+货物,t)" :data="outboundByPartnerCargo" />
</el-col>
</el-row>
</div>
</section>
</div>
</template>
<script setup lang="ts">
/**
* UI 模式:统计仪表盘(Fresh Mint 面板 + 图表区,无独立 P 编号)
* 参考:/#/demo/console · src/views/dashboard/console/index.vue
*/
import DualBarChart from '@/components/business/db/DualBarChart.vue'
import {
dashboardApi,
type DashboardCargoStat,
type DashboardPartnerCargoStat
} from '@/api/db/dashboard'
defineOptions({ name: 'DbDashboard' })
const loading = ref(false)
const inboundByCargo = ref<DashboardCargoStat[]>([])
const outboundByCargo = ref<DashboardCargoStat[]>([])
const inboundByPartnerCargo = ref<DashboardPartnerCargoStat[]>([])
const outboundByPartnerCargo = ref<DashboardPartnerCargoStat[]>([])
const loadData = async () => {
loading.value = true
try {
const [inCargo, outCargo, inPartner, outPartner] = await Promise.all([
dashboardApi.getInboundByCargo(),
dashboardApi.getOutboundByCargo(),
dashboardApi.getInboundByPartnerCargo(),
dashboardApi.getOutboundByPartnerCargo()
])
inboundByCargo.value = inCargo
outboundByCargo.value = outCargo
inboundByPartnerCargo.value = inPartner
outboundByPartnerCargo.value = outPartner
} catch (error) {
console.error('获取仪表盘数据失败', error)
} finally {
loading.value = false
}
}
onMounted(() => {
loadData()
})
const todayInboundVehicles = computed(() => {
const cargoSet = new Set<string>()
inboundByCargo.value.forEach((item) => {
if (item.planned > 0) cargoSet.add(item.label)
})
return cargoSet.size
})
const todayOutboundVehicles = computed(() => {
const cargoSet = new Set<string>()
outboundByCargo.value.forEach((item) => {
if (item.planned > 0) cargoSet.add(item.label)
})
return cargoSet.size
})
const todayInboundTotal = computed(() => {
const planned = inboundByCargo.value.reduce((sum, item) => sum + item.planned, 0)
const actual = inboundByCargo.value.reduce((sum, item) => sum + item.actual, 0)
return { actual, planned }
})
const todayOutboundTotal = computed(() => {
const planned = outboundByCargo.value.reduce((sum, item) => sum + item.planned, 0)
const actual = outboundByCargo.value.reduce((sum, item) => sum + item.actual, 0)
return { actual, planned }
})
const statsList = computed(() => [
{ title: '今日入库', value: todayInboundVehicles.value, suffix: '车' },
{
title: '今日入库总量',
value: `${todayInboundTotal.value.actual.toFixed(2)} / ${todayInboundTotal.value.planned.toFixed(2)}`,
suffix: '吨'
},
{ title: '今日出库', value: todayOutboundVehicles.value, suffix: '车' },
{
title: '今日出库总量',
value: `${todayOutboundTotal.value.actual.toFixed(2)} / ${todayOutboundTotal.value.planned.toFixed(2)}`,
suffix: '吨'
}
])
</script>
File diff suppressed because it is too large. Load diff
+950
View File
@@ -0,0 +1,950 @@
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">计划管理</h2>
</div>
<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">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<div
class="filter-date-shortcuts list-toolbar-date-shortcuts"
role="group"
aria-label="日期快捷筛选"
>
<el-button
size="small"
class="btn-ghost date-shortcut-btn"
:class="{ 'is-active': activeDateShortcut === 'all' }"
@click="handleDateShortcut('all')"
>
全部
</el-button>
<el-button
size="small"
class="btn-ghost date-shortcut-btn"
:class="{ 'is-active': activeDateShortcut === 'yesterday' }"
@click="handleDateShortcut('yesterday')"
>
昨日
</el-button>
<el-button
size="small"
class="btn-ghost date-shortcut-btn"
:class="{ 'is-active': activeDateShortcut === 'today' }"
@click="handleDateShortcut('today')"
>
今日
</el-button>
<el-button
size="small"
class="btn-ghost date-shortcut-btn"
:class="{ 'is-active': activeDateShortcut === 'tomorrow' }"
@click="handleDateShortcut('tomorrow')"
>
明日
</el-button>
<el-button
size="small"
class="btn-ghost date-shortcut-btn"
:class="{ 'is-active': activeDateShortcut === 'week' }"
@click="handleDateShortcut('week')"
>
本周
</el-button>
</div>
<el-input
v-model="searchKeyword"
class="db-toolbar-quick-search-input"
placeholder="往来单位 / 计划号 / 产品"
clearable
@keyup.enter="handleQuickSearch"
@clear="handleQuickSearchClear"
>
<template #suffix>
<el-icon class="db-quick-search-icon" @click="handleQuickSearch"><Search /></el-icon>
</template>
</el-input>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增计划
</el-button>
<el-button class="btn-ghost" :loading="exportLoading" @click="handleExport">
<el-icon><Download /></el-icon>
导出 Excel
</el-button>
<el-button class="btn-ghost" :loading="loading" @click="loadData">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
show-summary
:summary-method="getSummary"
@selection-change="handleSelectionChange"
>
<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
/>
<el-table-column
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
prop="customerName"
label="往来单位"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="startDate"
label="起始日期"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="endDate"
label="截止日期"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="product"
label="产品"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="specification"
label="规格"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
label="计划量(吨)"
prop="plannedQuantity"
min-width="120"
align="right"
header-align="center"
/>
<el-table-column 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
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
prop="remarks"
label="备注"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column 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
label="操作"
min-width="360"
align="center"
header-align="center"
fixed="right"
>
<template #default="{ row }">
<div class="db-op-actions">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>
查看
</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button class="btn-op-item" @click="handleGenerateInventory(row)">
<el-icon><DocumentAdd /></el-icon>
生成出入库单
</el-button>
<el-button class="btn-op-item danger" @click="handleDelete(row.id)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
v-show="total > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
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="top"
class="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" 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"
/>
</div>
</template>
<script setup lang="ts">
/**
* UI 模式 P02 · 复杂业务列表
* 参考:/#/crm/quote · src/views/crm/quote/index.vue
*/
import type { TableColumnCtx } from 'element-plus'
import {
Check,
Close,
Delete,
DocumentAdd,
Download,
Edit,
Plus,
Refresh,
RefreshLeft,
Search,
View
} from '@element-plus/icons-vue'
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
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 {
planApi,
type Plan,
type PlanCreate,
type PlanListParams,
type PlanRelatedData,
type PlanUpdate
} from '@/api/db/plans'
defineOptions({ name: 'DbPlans' })
const router = useRouter()
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(10)
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 getSummary = (param: { columns: TableColumnCtx<Plan>[]; data: Plan[] }) => {
const { columns } = param
const sums: string[] = []
columns.forEach((column, index) => {
if (column.type === 'selection') {
sums[index] = ''
return
}
if (column.type === 'index') {
sums[index] = '合计'
return
}
if (column.property === 'plannedQuantity') {
const totalPlanned = tableData.value.reduce(
(sum, row) => sum + (Number(row.plannedQuantity) || 0),
0
)
sums[index] = totalPlanned.toString()
return
}
if (column.label === '已执行量(吨)') {
const totalExecuted = tableData.value.reduce(
(sum, row) => sum + (row.executedQuantity || 0) / 1000,
0
)
sums[index] = totalExecuted.toFixed(2)
return
}
sums[index] = ''
})
return sums
}
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 = ''
currentPage.value = 1
}
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 handleView = async (record: Plan) => {
loadingRelated.value = true
try {
relatedData.value = await planApi.getRelated(record.id)
relatedModalOpen.value = true
} catch (error) {
console.error('加载关联数据失败', error)
} finally {
loadingRelated.value = false
}
}
const handleDelete = async (id: string) => {
try {
await $modal.confirm('确定要删除这个计划吗?')
await planApi.delete(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) {
await planApi.update(editingPlan.value.id, payload as PlanUpdate)
$modal.msgSuccess('更新成功')
} 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>
+289
View File
@@ -0,0 +1,289 @@
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">车牌管理</h2>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row db-list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-input
v-model="searchKeyword"
class="db-toolbar-quick-search-input"
placeholder="车牌号 / 备注"
clearable
@keyup.enter="handleQuickSearch"
@clear="handleQuickSearchClear"
>
<template #suffix>
<el-icon class="db-quick-search-icon" @click="handleQuickSearch"><Search /></el-icon>
</template>
</el-input>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增车牌号
</el-button>
<el-button class="btn-ghost" :loading="loading" @click="loadData">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" min-width="55" align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="vehicleNo"
label="车牌号"
min-width="160"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="remarks"
label="备注"
min-width="240"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
label="操作"
min-width="200"
align="center"
header-align="center"
fixed="right"
>
<template #default="{ row }">
<div class="db-op-actions">
<el-button class="btn-op-item primary" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button class="btn-op-item danger" @click="handleDelete(row.id)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
v-show="total > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="applyPagination"
@size-change="handleSizeChange"
/>
</div>
</div>
</section>
<el-dialog
v-model="modalOpen"
:title="editingVehicle ? '修改车牌号' : '新增车牌号'"
width="520px"
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="top"
class="form-grid-2"
scroll-to-error
>
<el-form-item label="车牌号" prop="vehicleNo">
<el-input v-model="formData.vehicleNo" placeholder="请输入车牌号" clearable />
</el-form-item>
<el-form-item label="备注" prop="remarks" class="form-col-span-2">
<el-input v-model="formData.remarks" type="textarea" :rows="3" 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>
</div>
</template>
<script setup lang="ts">
/**
* UI 模式 P01 · 标准列表 CRUD
* 参考:/#/crm/quote · src/views/crm/quote/index.vue
*/
import { Check, Close, Delete, Edit, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { vehicleApi, type Vehicle, type VehicleCreate } from '@/api/db/vehicles'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { $modal } from '@/utils/modal'
defineOptions({ name: 'DbVehicles' })
const loading = ref(false)
const saving = ref(false)
const allRecords = ref<Vehicle[]>([])
const tableData = ref<Vehicle[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const searchKeyword = ref('')
const selectedRows = ref<Vehicle[]>([])
const modalOpen = ref(false)
const editingVehicle = ref<Vehicle | null>(null)
const formRef = ref()
const formData = reactive<VehicleCreate>({
vehicleNo: '',
remarks: ''
})
const formRules = {
vehicleNo: [{ required: true, message: '请输入车牌号', trigger: 'blur' }]
}
const applyPagination = () => {
total.value = allRecords.value.length
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
tableData.value = allRecords.value.slice(start, end)
}
const loadData = async () => {
loading.value = true
try {
const keyword = searchKeyword.value.trim()
allRecords.value = await vehicleApi.list(keyword || undefined)
const maxPage = Math.ceil(allRecords.value.length / pageSize.value) || 1
if (currentPage.value > maxPage) {
currentPage.value = 1
}
applyPagination()
} catch (error) {
console.error('加载车牌列表失败', error)
} finally {
loading.value = false
}
}
const handleQuickSearch = () => {
currentPage.value = 1
loadData()
}
const handleQuickSearchClear = () => {
searchKeyword.value = ''
handleQuickSearch()
}
const handleSelectionChange = (selection: Vehicle[]) => {
selectedRows.value = selection
}
const handleSizeChange = (size: number) => {
pageSize.value = size
currentPage.value = 1
applyPagination()
}
const handleAdd = () => {
editingVehicle.value = null
Object.assign(formData, { vehicleNo: '', remarks: '' })
modalOpen.value = true
}
const handleEdit = (record: Vehicle) => {
editingVehicle.value = record
Object.assign(formData, {
vehicleNo: record.vehicleNo,
remarks: record.remarks || ''
})
modalOpen.value = true
}
const handleDelete = async (id: string) => {
try {
await $modal.confirm('确定要删除该车牌号吗?')
await vehicleApi.delete(id)
$modal.msgSuccess('删除成功')
loadData()
} catch {
// 用户取消或请求失败
}
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
saving.value = true
try {
const payload: VehicleCreate = {
vehicleNo: formData.vehicleNo.trim(),
remarks: formData.remarks?.trim() || null
}
if (editingVehicle.value) {
await vehicleApi.update(editingVehicle.value.id, payload)
$modal.msgSuccess('修改成功')
} else {
await vehicleApi.create(payload)
$modal.msgSuccess('新增成功')
}
modalOpen.value = false
loadData()
} catch (error) {
console.error('保存车牌失败', error)
} finally {
saving.value = false
}
})
}
const handleModalClose = () => {
modalOpen.value = false
editingVehicle.value = null
formRef.value?.resetFields()
}
useDbInstantSearch(() => searchKeyword.value, handleQuickSearch)
onMounted(() => {
loadData()
})
</script>
File diff suppressed because it is too large. Load diff