56 lines
8.4 KiB
Vue
56 lines
8.4 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminWorkbench.css'
|
||
import { useExportTask } from '@/modules/downloads'
|
||
import '@/styles/adminDensity.css'
|
||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import FleetQuoteEditor from '../components/FleetQuoteEditor.vue'
|
||
import FleetQuoteImport from '../components/FleetQuoteImport.vue'
|
||
import { listFleetQuotes, setFleetQuoteStatus, deleteFleetQuote, type FleetQuote, type FleetQuoteKind, type FleetQuoteQuery } from '../api/fleetQuotes'
|
||
const submitExport = useExportTask()
|
||
const access = useAccessPreviewStore(), rows = ref<FleetQuote[]>([]), total = ref(0), page = ref(1), size = ref(20), loading = ref(false), busy = ref(false)
|
||
const kind = ref<FleetQuoteKind>('PURCHASE'), editor = ref(false), importer = ref(false), selected = ref<FleetQuote | null>(null), readonly = ref(false), error = ref<ApiErrorDetails | null>(null)
|
||
const filters = ref<QueryModel>({ keyword: '', status: '', effectiveOn: '' }); let applied: FleetQuoteQuery = {}, sequence = 0
|
||
const fields: QueryField[] = [{ key: 'keyword', label: '关键词', type: 'input', width: 300, placeholder: '发货城市 / 目的仓码 / 往来单位 / 报价号' }, { key: 'status', label: '状态', type: 'select', width: 130, options: [{ value: 'ACTIVE', label: '启用' }, { value: 'INACTIVE', label: '停用' }] }, { key: 'effectiveOn', label: '有效日期', type: 'date', width: 170, placeholder: '有效日期(选填)' }]
|
||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' }, { key: 'quoteNo', label: '报价号', minWidth: 210 },
|
||
{ key: 'origin', label: '发货城市', minWidth: 150, slotName: 'origin' }, { key: 'destination', label: '目的仓码', minWidth: 115, slotName: 'destination' }, { key: 'miles', label: '距离(mile)', width: 110, slotName: 'miles' },
|
||
{ key: 'partyName', label: '车队 / 委托客户', minWidth: 180 }, { key: 'currency', label: '币种', width: 80, slotName: 'currency' },
|
||
{ key: 'pickup', label: '整车自提 / 单', width: 140, slotName: 'pickup' }, { key: 'full', label: '整车派送 / 单', width: 140, slotName: 'full' }, { key: 'loose', label: '散板派送 / 托', width: 140, slotName: 'loose' },
|
||
{ key: 'validity', label: '有效期', minWidth: 220, slotName: 'validity' }, { key: 'status', label: '状态', width: 90, slotName: 'status' }, { key: 'updatedAt', label: '更新时间', minWidth: 190 }]
|
||
async function load() { const current = ++sequence; loading.value = true; error.value = null; try { const result = await listFleetQuotes({ ...applied, kind: kind.value, page: page.value, size: size.value }); if (current === sequence) { rows.value = result.records; total.value = result.total } } catch (e) { if (current === sequence) { rows.value = []; total.value = 0; error.value = getApiErrorDetails(e, '报价加载失败') } } finally { if (current === sequence) loading.value = false } }
|
||
function search() { applied = { keyword: String(filters.value.keyword || '') || undefined, status: String(filters.value.status || '') || undefined, effectiveOn: String(filters.value.effectiveOn || '') || undefined }; if (page.value !== 1) page.value = 1; else void load() }
|
||
function open(row: FleetQuote | null, view = false) { selected.value = row; readonly.value = view; editor.value = true }
|
||
function actions(quoteNo: unknown): RowActionItem[] { const row = rows.value.find(item => item.quoteNo === quoteNo); if (!row) return []; return [{ key: 'view', label: '详情', priority: 3 }, { key: 'edit', label: '编辑', priority: 2, visible: access.can('platform:fleet:quote:edit') }, { key: 'status', label: row.status === 'ACTIVE' ? '停用' : '启用', priority: 1, visible: access.can('platform:fleet:quote:status'), disabled: busy.value }, { key: 'delete', label: '删除', tone: 'danger', visible: access.can('platform:fleet:quote:delete'), disabled: busy.value }] }
|
||
async function action(key: string, quoteNo: unknown) {
|
||
const row = rows.value.find(item => item.quoteNo === quoteNo); if (!row) return
|
||
if (key === 'view' || key === 'edit') { open(row, key === 'view'); return }
|
||
try { await ElMessageBox.confirm(key === 'delete' ? `删除报价 ${row.quoteNo}?历史报价版本仍保留,已确认承运金额不变。` : `确认${row.status === 'ACTIVE' ? '停用' : '启用'}报价 ${row.quoteNo}?`, `${key === 'delete' ? '删除' : row.status === 'ACTIVE' ? '停用' : '启用'}车队报价 · ${row.quoteNo}`, { customClass: 'admin-density-confirm admin-workbench is-dialog', type: 'warning' }) } catch { return }
|
||
busy.value = true
|
||
try { if (key === 'delete') await deleteFleetQuote(row); else await setFleetQuoteStatus(row, row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'); ElMessage.success('报价已更新'); await load() } catch (e) { error.value = getApiErrorDetails(e, '报价操作失败') } finally { busy.value = false }
|
||
}
|
||
async function download() { busy.value = true; try { await submitExport('FLEET_QUOTE_LIST', '车队报价', { ...applied, kind: kind.value }) } catch (e) { error.value = getApiErrorDetails(e, '导出失败') } finally { busy.value = false } }
|
||
watch(kind, () => { if (page.value !== 1) page.value = 1; else void load() }); watch([page, size], load); onMounted(load); onBeforeUnmount(() => { sequence++ })
|
||
</script>
|
||
<template><AdminPage class="admin-density admin-workbench"><ListPageLayout>
|
||
<LoadErrorState v-if="error" title="报价操作失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||
<div class="workbench-table is-page-surface"><DataTable appearance="integrated" v-model:page="page" v-model:page-size="size" :data="rows" :columns="columns" :total="total" :loading="loading" row-key="quoteNo" empty-text="暂无报价">
|
||
<template #toolbar-start><div class="density-status-tabs workbench-list-tabs" role="tablist" aria-label="报价用途"><button v-for="tab in ([['PURCHASE','车队采购报价'],['SALE','外接销售报价']] as const)" :key="tab[0]" role="tab" :aria-selected="kind === tab[0]" :class="{ 'is-active': kind === tab[0] }" @click="kind = tab[0]">{{ tab[1] }}</button></div></template>
|
||
<template #filters><QueryToolbar v-model="filters" :fields="fields" ariaLabel="车队报价查询" @search="search" @reset="search" /></template>
|
||
<template #toolbar-actions><el-button v-if="access.can('platform:fleet:quote:create')" type="primary" @click="open(null)">新增报价</el-button><el-button v-if="access.can('platform:fleet:quote:import')" @click="importer = true">批量导入</el-button><el-button v-if="access.can('platform:fleet:quote:export') && access.can('platform:download:list')" :loading="busy" @click="download">导出</el-button></template>
|
||
<template #actions="{ row }"><RowActions text-actions :actions="actions(row.quoteNo)" @action="action($event,row.quoteNo)" /></template>
|
||
<template #origin="{ row }">{{ row.details.originCity }}</template><template #destination="{ row }">{{ row.details.destinationCode }}</template><template #miles="{ row }">{{ row.details.distanceMiles }}</template><template #currency="{ row }">{{ row.details.currency }}</template>
|
||
<template #pickup="{ row }">{{ row.details.selfPickupPrice ?? '—' }}</template><template #full="{ row }">{{ row.details.fullDeliveryPrice ?? '—' }}</template><template #loose="{ row }">{{ row.details.looseDeliveryPrice ?? '—' }}</template>
|
||
<template #validity="{ row }">{{ row.details.validFrom }} — {{ row.details.validTo }}</template><template #status="{ row }">{{ row.status === 'ACTIVE' ? '启用' : '停用' }}</template>
|
||
</DataTable></div>
|
||
<FleetQuoteEditor v-model="editor" :quote="selected" :kind="kind" :readonly="readonly" @saved="load" /><FleetQuoteImport v-model="importer" :kind="kind" @saved="load" />
|
||
</ListPageLayout></AdminPage></template>
|