This commit is contained in:
2026-10-20 23:34:11 +08:00
parent f155976f52
commit 13e4aa56ec
321 changed files with 6756 additions and 2110 deletions
@@ -1,6 +1,7 @@
<script setup lang="ts">
import InlineHelp from '@/shared/components/InlineHelp.vue'
import '@/styles/adminDensity.css'
import '@/styles/adminWorkbench.css'
import { computed, onMounted, ref, watch } from 'vue'
import { AlertTriangle, ArrowLeft, CircleCheck, Download, FileSpreadsheet, RefreshCw, Ship, Warehouse } from '@lucide/vue'
import { ElMessage, ElMessageBox } from 'element-plus'
@@ -432,8 +433,8 @@ onMounted(() => {
</script>
<template>
<AdminPage as="div" class="price-book-detail admin-density" spacing="normal">
<DetailPageLayout class="density-detail-layout surface-panel" aria-label="船期价格表版本详情">
<AdminPage as="div" class="price-book-detail admin-density admin-workbench" spacing="normal">
<DetailPageLayout class="density-detail-layout workbench-panel is-compact is-page-surface" aria-label="船期价格表版本详情">
<template #header>
<div class="density-detail-header">
<div class="density-detail-heading">
@@ -555,37 +556,37 @@ onMounted(() => {
<style scoped>
.price-book-detail { min-width: 0; max-width: 1880px; }
.version-panel, .mapping-section, .detail-section { padding: var(--admin-space-5) 0; border-bottom: 1px solid var(--cc-border); }
.version-panel, .mapping-section, .detail-section { padding: var(--admin-space-5) 0; border-bottom: 1px solid var(--workbench-border); }
.detail-section:last-child { border-bottom: 0; }
.price-book-header-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3); }
.detail-grid .detail-grid__wide { grid-column: 1 / -1; }
.section-heading > div { min-width: 0; }
.section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--admin-space-5); margin-bottom: var(--admin-space-5); }
.section-heading h2 { display: flex; align-items: center; gap: var(--admin-space-3); margin: 0; color: var(--cc-text); font-size: var(--admin-font-section); }
.section-heading h2 svg { color: var(--cc-primary); }
.section-heading p { overflow-wrap: anywhere; margin: 5px 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption); line-height: 1.55; }
.section-heading h2 { display: flex; align-items: center; gap: var(--admin-space-3); margin: 0; color: var(--workbench-text); font-size: var(--admin-font-section); }
.section-heading h2 svg { color: var(--workbench-primary); }
.section-heading p { overflow-wrap: anywhere; margin: 5px 0 0; color: var(--workbench-muted); font-size: var(--admin-font-caption); line-height: 1.55; }
.is-danger { color: var(--el-color-danger) !important; }
.mapping-section, .detail-section { display: grid; min-width: 0; gap: var(--admin-space-5); }
.mapping-section .section-heading, .detail-section .section-heading { margin-bottom: 0; }
.mapping-groups { display: grid; gap: var(--admin-space-5); }
.mapping-group { display: grid; gap: var(--admin-space-3); }
.mapping-group h3 { margin: 0; color: var(--cc-text); font-size: var(--admin-font-body); }
.mapping-row { display: grid; grid-template-columns: minmax(220px, .8fr) minmax(320px, 1fr); align-items: center; gap: var(--admin-space-6); padding: var(--admin-space-5) var(--admin-space-5); border: 1px solid var(--cc-border); border-radius: var(--admin-radius); background: var(--cc-surface-soft, #f8fafc); }
.mapping-group h3 { margin: 0; color: var(--workbench-text); font-size: var(--admin-font-body); }
.mapping-row { display: grid; grid-template-columns: minmax(220px, .8fr) minmax(320px, 1fr); align-items: center; gap: var(--admin-space-6); padding: var(--admin-space-5) var(--admin-space-5); border: 1px solid var(--workbench-border); border-radius: var(--admin-radius); background: var(--cc-surface-soft, #f8fafc); }
.mapping-row > div { display: grid; min-width: 0; gap: var(--admin-space-1); }
.mapping-row strong { overflow-wrap: anywhere; color: var(--cc-text); font-size: var(--admin-font-body); }
.mapping-row span { color: var(--cc-muted); font-size: var(--admin-font-caption); }
.mapping-row strong { overflow-wrap: anywhere; color: var(--workbench-text); font-size: var(--admin-font-body); }
.mapping-row span { color: var(--workbench-muted); font-size: var(--admin-font-caption); }
.mapping-row :deep(.el-select) { width: 100%; }
.mapping-load-error { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5); padding: var(--admin-space-5); border: 1px solid color-mix(in srgb, var(--cc-danger) 24%, var(--cc-border)); border-radius: var(--admin-radius); }
.mapping-load-error { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5); padding: var(--admin-space-5); border: 1px solid color-mix(in srgb, var(--cc-danger) 24%, var(--workbench-border)); border-radius: var(--admin-radius); }
.mapping-load-error strong { color: var(--cc-danger); font-size: var(--admin-font-body); }
.mapping-load-error p { margin: var(--admin-space-1) 0; color: var(--cc-text); font-size: var(--admin-font-caption); }
.mapping-load-error p { margin: var(--admin-space-1) 0; color: var(--workbench-text); font-size: var(--admin-font-caption); }
.channel-tabs { width: 100%; min-width: 0; margin-bottom: 0; overflow: hidden; }
.channel-preview :deep(.el-table) { width: 100%; max-width: 100%; min-width: 0; }
.channel-meta { display: flex; flex-wrap: wrap; gap: var(--admin-space-2) var(--admin-space-5); margin: calc(-1 * var(--admin-space-1)) 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption); }
.channel-meta { display: flex; flex-wrap: wrap; gap: var(--admin-space-2) var(--admin-space-5); margin: calc(-1 * var(--admin-space-1)) 0 0; color: var(--workbench-muted); font-size: var(--admin-font-caption); }
@media (max-width: 720px) {
.detail-heading, .mapping-load-error { align-items: stretch; flex-direction: column; }
.detail-toolbar { width: 100%; }
.detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.detail-grid > div, .detail-grid > div:nth-child(5n) { border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
.detail-grid > div, .detail-grid > div:nth-child(5n) { border-right: 1px solid var(--workbench-border); border-bottom: 1px solid var(--workbench-border); }
.detail-grid > div:nth-child(2n) { border-right: 0; }
.detail-grid__wide, .detail-grid__wide + .detail-grid__wide { grid-column: span 2; }
.mapping-row { grid-template-columns: 1fr; gap: 10px; }
@@ -1,6 +1,7 @@
<script setup lang="ts">
import InlineHelp from '@/shared/components/InlineHelp.vue'
import '@/styles/adminDensity.css'
import '@/styles/adminWorkbench.css'
import { computed, onMounted, ref, watch } from 'vue'
import { FileSpreadsheet, FileUp } from '@lucide/vue'
import { ElMessage, ElMessageBox } from 'element-plus'
@@ -318,13 +319,13 @@ onMounted(() => {
</script>
<template>
<AdminPage as="div" class="price-book-page admin-density" spacing="normal">
<AdminPage as="div" class="price-book-page admin-density admin-workbench" spacing="normal">
<ListPageLayout aria-label="承运商报价版本管理">
<section aria-live="polite"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError"
<section aria-live="polite"><div class="workbench-table is-page-surface"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError"
v-model:page="page"
v-model:page-size="pageSize"
:data="pagedRows"
@@ -385,10 +386,10 @@ onMounted(() => {
<template #issues="{ row }"><div class="issue-counts"><span :class="{ 'is-blocking': (row as PlatformPriceBookVersion).blockingErrorCount }">阻断 {{ (row as PlatformPriceBookVersion).blockingErrorCount }}</span><span>警告 {{ (row as PlatformPriceBookVersion).warningCount }}</span></div></template>
<template #empty><el-empty description="暂无船期价格表版本" /></template>
<template #pagination-left>共 {{ rows.length }} 个版本 · {{ activeCount }} 个生效中 · {{ pendingCount }} 个待确认</template>
</DataTable></section>
</DataTable></div></section>
</ListPageLayout>
<AppDialog class="admin-density-dialog"
<AppDialog class="admin-density-dialog admin-workbench is-dialog"
v-model="uploadOpen"
title="上传船期价格表"
width="min(560px, calc(100vw - 28px))"
@@ -400,13 +401,13 @@ onMounted(() => {
<div class="density-dialog-sections">
<el-form class="density-form" label-position="left" label-width="90px" :disabled="uploading">
<table class="density-table density-table--editable" aria-label="上传价格表资料"><tbody>
<div class="workbench-evidence"><table class="density-table density-table--editable" aria-label="上传价格表资料"><tbody>
<tr><td><el-form-item label="报价范围"><template #label><span>报价范围<InlineHelp title="报价上传规则"><p>上传后先展示解析结果和仓库映射;确认后只替换同一报价范围的当前生效版本。客户专属报价生效后,计价时优先于平台公共报价。</p></InlineHelp></span></template><el-select v-model="uploadCustomerId" filterable :disabled="uploading" aria-label="上传报价范围"><el-option label="平台公共报价" value="0" /><el-option v-for="customer in customerOptions" :key="String(customer.id)" :label="`客户专属 · ${customer.tenantName || '未命名客户'}`" :value="String(customer.id)" /></el-select></el-form-item></td></tr>
<tr><td><el-form-item label="价格表文件">
<input ref="uploadInput" class="visually-hidden-input" type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" aria-label="选择 XLSX 船期价格表" :disabled="uploading" @change="selectUploadFile" />
<div class="upload-file-control"><el-button :disabled="uploading" @click="uploadInput?.click()"><FileSpreadsheet :size="16" />选择 XLSX 文件</el-button><span class="upload-file-name">{{ uploadFile?.name || '尚未选择文件' }}</span></div>
</el-form-item></td></tr>
</tbody></table>
</tbody></table></div>
</el-form>
<p class="density-dialog-note">{{ uploadFile ? (uploadFile.size / 1024 / 1024).toFixed(2) + ' MB' : '最大 10MB' }}</p>
<el-alert v-if="uploadFileError" :title="uploadFileError" type="error" :closable="false" show-icon />
@@ -419,11 +420,11 @@ onMounted(() => {
<style scoped>
.price-book-page { min-width: 0; }
.version-cell { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.version-cell strong { color: var(--cc-text); }
.version-cell span, .metric-cell span { color: var(--cc-muted); font-size: var(--admin-font-caption); }
.version-cell strong { color: var(--workbench-text); }
.version-cell span, .metric-cell span { color: var(--workbench-muted); font-size: var(--admin-font-caption); }
.metric-cell, .issue-counts { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--admin-space-1) var(--admin-space-3); }
.metric-cell strong { color: var(--cc-text); font-size: var(--admin-font-body); }
.issue-counts { color: var(--cc-muted); font-size: var(--admin-font-caption); }
.metric-cell strong { color: var(--workbench-text); font-size: var(--admin-font-body); }
.issue-counts { color: var(--workbench-muted); font-size: var(--admin-font-caption); }
.issue-counts .is-blocking, .is-danger { color: var(--el-color-danger); font-weight: 700; }
.visually-hidden-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; clip-path: inset(50%); }
.upload-file-control { display: flex; align-items: center; gap: var(--admin-space-5); }