Files
admin-web/src/modules/overseas-master-data/pages/FleetQuotesView.vue
T
2026-10-27 17:44:49 +08:00

56 lines
8.4 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.
<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>