优化
This commit is contained in:
@@ -44,6 +44,7 @@ export function createPaginationItems(page: number, pageCount: number): Paginati
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots, watch } from 'vue'
|
||||
import { ChevronLeft, ChevronRight } from '@lucide/vue'
|
||||
import { splitTableActions, runTableAction, type TableAction } from './tableActions'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
@@ -54,6 +55,7 @@ const props = withDefaults(defineProps<{
|
||||
emptyText?: string
|
||||
total?: number
|
||||
pageSizes?: number[]
|
||||
rowActions?: (row: object) => TableAction[]
|
||||
}>(), {
|
||||
loading: false,
|
||||
emptyText: '暂无数据',
|
||||
@@ -141,6 +143,13 @@ function setPage(value: number) {
|
||||
|
||||
<template>
|
||||
<div class="data-table">
|
||||
<header v-if="$slots.toolbar || $slots['toolbar-actions']" class="data-table__toolbar">
|
||||
<div class="data-table__toolbar-heading"><slot name="toolbar" /></div>
|
||||
<div class="data-table__toolbar-actions"><slot name="toolbar-actions" /></div>
|
||||
</header>
|
||||
<slot name="before-table" />
|
||||
<slot v-if="$slots.error" name="error" />
|
||||
<template v-else>
|
||||
<el-table v-bind="$attrs" v-loading="loading" :data="data" :empty-text="emptyText" :aria-busy="loading">
|
||||
<el-table-column
|
||||
v-for="(column, index) in columns"
|
||||
@@ -152,7 +161,26 @@ function setPage(value: number) {
|
||||
<template v-if="hasSlot(column.headerSlotName)" #header="scope">
|
||||
<slot :name="column.headerSlotName" v-bind="scope" />
|
||||
</template>
|
||||
<template v-if="hasSlot(column.slotName)" #default="scope">
|
||||
<template v-if="column.slotName === 'actions' && rowActions" #default="{ row }">
|
||||
<div class="data-table__row-actions">
|
||||
<el-button v-for="action in splitTableActions(rowActions(row)).visible" :key="action.key"
|
||||
:disabled="action.disabled" :loading="action.loading" @click="runTableAction(action)">
|
||||
<component :is="action.icon" v-if="action.icon" :size="14" />{{ action.label }}
|
||||
</el-button>
|
||||
<el-dropdown v-if="splitTableActions(rowActions(row)).overflow.length" trigger="click">
|
||||
<el-button class="data-table__more" aria-label="更多操作" title="更多操作">…</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="action in splitTableActions(rowActions(row)).overflow" :key="action.key"
|
||||
:disabled="action.disabled || action.loading" @click="runTableAction(action)">
|
||||
{{ action.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="hasSlot(column.slotName)" #default="scope">
|
||||
<slot :name="column.slotName" v-bind="scope" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -192,15 +220,29 @@ function setPage(value: number) {
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.data-table { min-width: 0; }
|
||||
.data-table__toolbar { display: flex; min-height: 48px; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--client-gap); padding: var(--client-toolbar-padding); border-bottom: 1px solid var(--client-border); }
|
||||
.data-table__toolbar-heading, .data-table__toolbar-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--client-gap); }
|
||||
.data-table__toolbar-heading { color: var(--client-muted); font-size: 12px; }
|
||||
.data-table__toolbar-heading :slotted(strong) { color: var(--client-text); font-size: 14px; }
|
||||
.data-table__toolbar-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.data-table__row-actions { display: flex; align-items: center; gap: var(--client-gap); white-space: nowrap; }
|
||||
.data-table__row-actions :deep(.el-button) { height: var(--client-row-action-height); padding: 0 8px; margin: 0 !important; }
|
||||
.data-table__more { min-width: 28px; }
|
||||
:global(.client-ui .data-table .el-table) { color: var(--client-text); font-size: var(--client-font-size); --el-table-border-color: var(--client-border); --el-table-header-bg-color: var(--client-header-bg); --el-table-row-hover-bg-color: var(--client-hover-bg); }
|
||||
:global(.client-ui .data-table th.el-table__cell) { height: var(--client-table-header-height); padding: 0; color: var(--client-muted); font-weight: 600; }
|
||||
:global(.client-ui .data-table td.el-table__cell) { height: var(--client-table-row-height); padding: 0; }
|
||||
:global(.client-ui .data-table .el-table .cell) { padding: 0 12px; line-height: 20px; }
|
||||
:global(.client-ui .data-table .el-table__row:nth-child(even)) { --el-table-tr-bg-color: #fcfcfd; }
|
||||
.data-table :deep(.data-table__single-line-cell .cell) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.data-table :deep(.data-table__system-cell .cell) { white-space: nowrap; }
|
||||
.data-table :deep(.data-table__system-cell .el-button + .el-button), .data-table :deep(.data-table__system-cell .el-button + .el-dropdown) { margin-left: 8px; }
|
||||
.data-table__pagination { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 18px; border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; }
|
||||
.data-table__pagination { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 16px; padding: var(--client-pagination-padding); border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; }
|
||||
.data-table__pagination-left { display: flex; min-width: 0; flex: 1; align-items: center; gap: 8px; }
|
||||
.data-table__pagination-controls, .data-table__page-size, .data-table__page-buttons { display: flex; align-items: center; gap: 8px; }
|
||||
.data-table__pagination-controls { min-width: 0; flex-wrap: wrap; justify-content: flex-end; }
|
||||
|
||||
@@ -23,12 +23,14 @@ export interface QueryField {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
layout?: 'inline' | 'grid'
|
||||
ariaLabel: string
|
||||
fields: QueryField[]
|
||||
searchText?: string
|
||||
resetText?: string
|
||||
searchOnReset?: boolean
|
||||
}>(), {
|
||||
layout: 'inline',
|
||||
searchText: '查询',
|
||||
resetText: '重置',
|
||||
searchOnReset: true,
|
||||
@@ -77,6 +79,7 @@ defineExpose({ search, reset })
|
||||
<section
|
||||
v-bind="$attrs"
|
||||
class="query-toolbar"
|
||||
:class="{ 'query-toolbar--grid': layout === 'grid' }"
|
||||
:aria-label="ariaLabel"
|
||||
>
|
||||
<div class="query-toolbar__filters">
|
||||
@@ -133,5 +136,11 @@ defineExpose({ search, reset })
|
||||
.query-toolbar__filters :deep(.el-input), .query-toolbar__filters :deep(.el-select), .query-toolbar__filters :deep(.el-date-editor) { width: 260px; max-width: 100%; min-width: 0; }
|
||||
.query-toolbar__actions { display: flex; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-start; gap: 8px; }
|
||||
.query-toolbar__actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.query-toolbar--grid { display: block; padding: var(--client-query-padding); border: 1px solid var(--client-border); border-radius: var(--client-radius); }
|
||||
.query-toolbar--grid .query-toolbar__filters { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px 16px; }
|
||||
.query-toolbar--grid .query-toolbar__field { min-width: 0; }
|
||||
.query-toolbar--grid .query-toolbar__field :deep(.el-input), .query-toolbar--grid .query-toolbar__field :deep(.el-select), .query-toolbar--grid .query-toolbar__field :deep(.el-date-editor) { flex: 1; width: 100%; min-width: 0; }
|
||||
.query-toolbar--grid .query-toolbar__actions { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--client-border); }
|
||||
@media (max-width: 1200px) { .query-toolbar--grid .query-toolbar__filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 720px) { .query-toolbar { align-items: stretch; flex-direction: column; } .query-toolbar__filters { width: 100%; flex-direction: column; } .query-toolbar__field { display: grid; gap: 6px; } .query-toolbar__filters :deep(.el-input), .query-toolbar__filters :deep(.el-select), .query-toolbar__filters :deep(.el-date-editor) { width: 100%; } .query-toolbar__actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); width: 100%; } .query-toolbar__actions :deep(.el-button) { min-height: 40px; margin-left: 0; } }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
export interface TableAction {
|
||||
key: string
|
||||
label: string
|
||||
icon?: Component
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
run: () => unknown
|
||||
}
|
||||
|
||||
// Callers supply only permitted actions; disabled actions retain their place.
|
||||
export function splitTableActions(actions: readonly TableAction[]) {
|
||||
return actions.length > 3
|
||||
? { visible: actions.slice(0, 2), overflow: actions.slice(2) }
|
||||
: { visible: actions, overflow: [] }
|
||||
}
|
||||
|
||||
export function runTableAction(action: TableAction) {
|
||||
if (!action.disabled && !action.loading) return action.run()
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { AlertTriangle, Box, ChevronDown, ChevronUp, Download, Eye, FilePlus2, F
|
||||
import { FIRST_LEG_ORDER_STATUSES, type FirstLegOrderStatus } from '@chongchong/shared'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { ApiError } from '@/api/http'
|
||||
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
|
||||
import InboundBillingPanel from '@/modules/first-leg/components/InboundBillingPanel.vue'
|
||||
@@ -110,7 +111,7 @@ const batchImportResult = ref<MerchantBatchImportResponse | null>(null)
|
||||
const batchImportError = ref('')
|
||||
const fixedOrderColumns: DataTableColumn[] = [
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true },
|
||||
{ key: 'actions', label: '操作', width: 216, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' },
|
||||
{ key: 'actions', label: '操作', width: 284, fixed: 'left', align: 'left', headerAlign: 'left', slotName: 'actions' },
|
||||
{ key: 'order', label: '订单号', width: 190, fixed: 'left', align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'order' },
|
||||
]
|
||||
const selectableOrderColumns: DataTableColumn[] = [
|
||||
@@ -1858,14 +1859,14 @@ async function searchOrders(model: QueryModel) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="first-leg-page" :class="{ 'is-create-route': isCreatePage }">
|
||||
<section class="first-leg-page" :class="{ 'is-create-route': isCreatePage, 'client-ui': !isCreatePage }">
|
||||
<template v-if="!isCreatePage">
|
||||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" />
|
||||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" layout="grid" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" />
|
||||
|
||||
<div class="client-card order-table-card">
|
||||
<div class="order-table-toolbar">
|
||||
<div><strong>订单列表</strong><span>共 {{ filteredOrders.length }} 条</span></div>
|
||||
<div class="order-table-toolbar-actions">
|
||||
<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="visibleOrderColumns" :key="orderTableVersion" :loading="store.loading" :total="filteredOrders.length" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单" class="prototype-order-table" :row-actions="getOrderRowActions" @selection-change="handleOrderSelectionChange">
|
||||
<template #toolbar><strong>订单列表</strong><span>共 {{ filteredOrders.length }} 条</span><span v-if="selectedOrderCount">已选 {{ selectedOrderCount }} 条</span></template>
|
||||
<template #toolbar-actions>
|
||||
<el-popover placement="bottom-end" :width="320" trigger="click" popper-class="order-column-popper">
|
||||
<template #reference>
|
||||
<el-button class="column-settings-trigger" aria-label="设置订单列表显示列" title="设置显示列"><SlidersHorizontal :size="18" /></el-button>
|
||||
@@ -1897,8 +1898,9 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-button v-if="canSubmitOrder" :loading="batchSubmitting" :disabled="selectedDraftOrders.length === 0 || companySubmissionBlocked" @click="submitSelectedDraftOrders"><Send :size="17" />批量提交草稿</el-button>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" @click="openBatchImport"><Upload :size="17" />批量导入订单</el-button>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template #before-table>
|
||||
<div v-if="companySubmissionBlocked && !supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility is-pending" role="status">
|
||||
<AlertTriangle :size="20" />
|
||||
<div><strong>{{ canCreateOrder ? '企业资料待审核,当前仅可保存订单草稿' : '企业资料待审核,当前订单为只读' }}</strong><span>{{ canCreateOrder ? '平台审核通过后即可提交;您仍可创建、编辑和保存草稿。' : '您当前拥有只读权限,可以继续查看订单进展。' }}</span></div>
|
||||
@@ -1909,21 +1911,15 @@ async function searchOrders(model: QueryModel) {
|
||||
<div><strong>部分订单配套数据加载失败</strong><span v-for="message in supportingDataErrors" :key="message">{{ message }}</span></div>
|
||||
<el-button :loading="supportingDataLoading || store.loading" @click="loadPageData">重新加载</el-button>
|
||||
</div>
|
||||
<div v-if="store.loadError" class="order-load-state" role="alert">
|
||||
|
||||
</template>
|
||||
<template v-if="store.loadError" #error>
|
||||
<div class="order-load-state" role="alert">
|
||||
<AlertTriangle :size="24" />
|
||||
<div><strong>订单数据加载失败</strong><span>{{ store.loadError }}</span></div>
|
||||
<el-button :loading="store.loading" @click="loadPageData">重新加载</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="desktop-order-table">
|
||||
<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="visibleOrderColumns" :key="orderTableVersion" :loading="store.loading" :total="filteredOrders.length" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单" class="prototype-order-table" @selection-change="handleOrderSelectionChange">
|
||||
<template #actions="{ row }">
|
||||
<div class="order-row-actions">
|
||||
<el-button link type="primary" @click="openDetail(row as FirstLegOrder)"><Eye :size="15" />详情</el-button>
|
||||
<el-button v-if="canConfirmInboundFee && (row as FirstLegOrder).businessSummary.customerFeeConfirmationRequired" link type="primary" @click="openFeeConfirmation(row as FirstLegOrder)">确认计费重</el-button>
|
||||
<el-button v-if="canEditOrder && canClientEditFirstLegOrder((row as FirstLegOrder).status)" link type="primary" :loading="openingEditor" @click="openEdit(row as FirstLegOrder)"><Pencil :size="15" />编辑</el-button>
|
||||
<el-button v-if="eligibleCustomsDetail(row as FirstLegOrder)" link type="primary" @click="openCustomsDocuments(row as FirstLegOrder)"><FileText :size="15" />报关资料</el-button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template #order="{ row }">
|
||||
<button class="order-number-link table-code" type="button" :title="(row as FirstLegOrder).orderNo || '—'" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '—' }}</button>
|
||||
@@ -1973,8 +1969,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</el-empty>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
<div v-loading="store.loading" class="mobile-order-surface">
|
||||
<div v-if="!store.loadError" v-loading="store.loading" class="mobile-order-surface">
|
||||
<div v-if="filteredOrders.length" class="mobile-order-list">
|
||||
<article v-for="order in filteredOrders" :key="order.id" class="mobile-order-card">
|
||||
<header><button type="button" @click="openDetail(order)">{{ order.orderNo || '—' }}</button><el-tag :type="getFirstLegStatusTone(order.status)" effect="light">{{ getFirstLegStatusLabel(order.status) }}</el-tag></header>
|
||||
@@ -1997,7 +1992,6 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><Plus :size="16" />{{ pageDataLoading ? '加载收货仓中' : (availableWarehouses.length ? '创建第一笔订单' : '暂无可用收货仓') }}</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/* Client content standard. Sidebar and brand navigation are intentionally outside this scope. */
|
||||
:root {
|
||||
--client-action: #1777ff;
|
||||
--client-action-hover: #0958d9;
|
||||
--client-text: #344054;
|
||||
--client-muted: #667085;
|
||||
--client-border: #e4e7ec;
|
||||
--client-surface: #fff;
|
||||
--client-header-bg: #f8fafc;
|
||||
--client-hover-bg: #f5f8ff;
|
||||
--client-success: #067647;
|
||||
--client-success-bg: #ecfdf3;
|
||||
--client-warning: #b54708;
|
||||
--client-warning-bg: #fffaeb;
|
||||
--client-danger: #b42318;
|
||||
--client-danger-bg: #fef3f2;
|
||||
--client-info: #475467;
|
||||
--client-info-bg: #f2f4f7;
|
||||
--client-progress: #175cd3;
|
||||
--client-progress-bg: #eff8ff;
|
||||
--client-font-size: 13px;
|
||||
--client-control-height: 32px;
|
||||
--client-row-action-height: 28px;
|
||||
--client-table-header-height: 44px;
|
||||
--client-table-row-height: 48px;
|
||||
--client-toolbar-padding: 8px 16px;
|
||||
--client-pagination-padding: 12px 16px;
|
||||
--client-query-padding: 16px;
|
||||
--client-gap: 8px;
|
||||
--client-section-gap: 16px;
|
||||
--client-radius: 6px;
|
||||
}
|
||||
.client-ui { color: var(--client-text); font-size: var(--client-font-size); }
|
||||
.client-ui .el-button {
|
||||
--el-button-text-color: var(--client-action);
|
||||
--el-button-border-color: var(--client-border);
|
||||
--el-button-hover-text-color: var(--client-action-hover);
|
||||
--el-button-hover-border-color: #b2ccff;
|
||||
--el-button-hover-bg-color: var(--client-hover-bg);
|
||||
height: var(--client-control-height); border-radius: var(--client-radius); font-size: var(--client-font-size);
|
||||
}
|
||||
.client-ui .el-button--primary { --el-button-bg-color: var(--client-action); --el-button-border-color: var(--client-action); --el-button-text-color: #fff; --el-button-hover-bg-color: var(--client-action-hover); --el-button-hover-border-color: var(--client-action-hover); --el-button-hover-text-color: #fff; --el-button-active-bg-color: var(--client-action-hover); --el-button-active-border-color: var(--client-action-hover); }
|
||||
.client-ui .el-button--success { --el-button-text-color: var(--client-success); --el-button-bg-color: var(--client-success-bg); --el-button-border-color: #abefc6; --el-button-hover-text-color: var(--client-success); --el-button-hover-bg-color: #dcfae6; }
|
||||
.client-ui .el-button--warning { --el-button-text-color: var(--client-warning); --el-button-bg-color: var(--client-warning-bg); --el-button-border-color: #fedf89; --el-button-hover-text-color: var(--client-warning); --el-button-hover-bg-color: #fef0c7; }
|
||||
.client-ui .el-button--danger { --el-button-text-color: var(--client-danger); --el-button-bg-color: var(--client-danger-bg); --el-button-border-color: #fecdca; --el-button-hover-text-color: var(--client-danger); --el-button-hover-bg-color: #fee4e2; }
|
||||
.client-ui .el-button.is-link, .client-ui .el-button.is-text { --el-button-text-color: var(--client-action); --el-button-hover-text-color: var(--client-action-hover); }
|
||||
.client-ui .el-button.is-disabled { color: #98a2b3; background: #f9fafb; border-color: var(--client-border); }
|
||||
.client-ui .el-button > span { gap: 4px; }
|
||||
.client-ui .el-input__wrapper, .client-ui .el-select__wrapper { min-height: var(--client-control-height); font-size: var(--client-font-size); }
|
||||
.client-ui .el-button:focus-visible { outline: 2px solid var(--client-action); outline-offset: 2px; }
|
||||
.client-status { display: inline-flex; align-items: center; gap: 6px; min-height: 26px; padding: 3px 10px; border-radius: 999px; color: var(--client-info); background: var(--client-info-bg); font-size: 12px; font-weight: 600; line-height: 20px; white-space: nowrap; }
|
||||
.client-status::before { content: ''; flex: 0 0 6px; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||
.client-status[data-tone="primary"] { color: var(--client-progress); background: var(--client-progress-bg); }
|
||||
.client-status[data-tone="success"] { color: var(--client-success); background: var(--client-success-bg); }
|
||||
.client-status[data-tone="warning"] { color: var(--client-warning); background: var(--client-warning-bg); }
|
||||
.client-status[data-tone="danger"] { color: var(--client-danger); background: var(--client-danger-bg); }
|
||||
@@ -1,4 +1,5 @@
|
||||
@import './foundation.css';
|
||||
@import './client-ui.css';
|
||||
@import './shell.css';
|
||||
@import './workspace-tabs.css';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user