20260805220506
This commit is contained in:
1 parent
bca59a2755
commit
74cdb7f306
5 files changed
+1351
-22
No files matched your search
@@ -0,0 +1,15 @@
|
||||
# 2026-08-05
|
||||
|
||||
## UI/UX 整体重构咨询 → 方向确认
|
||||
- 用户提出前端整体 UI/UX 重构诉求:界面太普通、白色区域过多易疲劳,AppLayout 布局与操作方式要重新设计;喜欢 shadcn/ui 与 ERPNext 风格;系统是物流/货代场景。
|
||||
- 我的结论:风格完全适合货代 ERP;但 shadcn/ui 是 React 生态,ERPNext Frappe UI 绑框架,整体迁移组件库不划算。建议保留 antdv-next 做"视觉语言 + 布局外壳"深度重构,并给出概念稿(浅灰工作区、窄侧边栏分组、Ctrl+K 命令面板、紧凑列表、状态胶囊)。
|
||||
- 用户拍板:①不重写组件库;②页签栏保留但做轻。详细决策已写入 MEMORY.md「UI/UX 重构方向」。
|
||||
- 待启动:第一步 = 设计令牌 + AppLayout 外壳重构。
|
||||
- 概念稿 v1 被否:缺应用级 Tabs、留白过多、配色不对。v2 按 ERPNext Desk 重做:顶栏白 46px + 细页签带 32px(灰底白激活)、侧边栏白底分组、内容底 #f8f9fa、主色 #2490ef、状态用 6px 圆点 + 彩色文字(非色块)、列表行高 32px、单号蓝色链接。等用户确认 v2 方向。
|
||||
- v2 仍被否:用户感觉"太素/像半成品、整体感觉不对"。结论:静态小示意图失真,改用真实尺寸可交互原型。
|
||||
- 已交付 `D:\workspace\code\fms\ui-redesign-prototype.html`(单文件、无依赖):内置 3 套主题一键切换(erpnext 灰白 / slate 深色侧栏 / zinc shadcn 风),可交互:侧边栏折叠、应用级页签打开/关闭/切换、海运订单列表筛选、点行进订单详情表单、Ctrl+K 命令面板。等用户选定主题方向后再落地到 AppLayout。
|
||||
- 用户提供了 D:\downloads\zip 参考代码(Next.js + shadcn v4 base-nova 风格,本身就是海运物流系统)。深入分析:OKLCH 纸墨色系、钢青主色 ok(0.43 0.085 245)、纯白侧边栏 accent 选中态(非主色)、dot+text 状态、44px 顶栏仅面包屑(搜索在工具栏)、密度切换按 D、侧拉详情面板、2-3px 圆角、表单分区左侧标题+右字段 max-w-760px。
|
||||
## UI/UX 重构 v1 实施完成
|
||||
- 完成 AppLayout 外壳重构,`vite build` 通过 0 error。
|
||||
- 修改文件:`main.scss`(OKLCH 令牌)、`AppSidebar.vue`(可折叠分组菜单)、`AppTabs.vue`(38px 统一顶栏+用户区)、`DefaultLayout.vue`(移除 AppToolbar)。
|
||||
- 详细设计决策已写入 MEMORY.md。原型 `ui-redesign-prototype-v3.html` 保留在项目根目录。
|
||||
@@ -0,0 +1,233 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>FMS 工作区布局 Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--nav: #1e3031;
|
||||
--nav-soft: #2a4141;
|
||||
--nav-text: #d8e4e1;
|
||||
--nav-muted: #91aaa5;
|
||||
--canvas: #eaf0ee;
|
||||
--panel: #f7faf9;
|
||||
--panel-raised: #fbfcfb;
|
||||
--line: #d4dfdc;
|
||||
--text: #20302e;
|
||||
--muted: #687875;
|
||||
--teal: #16877f;
|
||||
--teal-soft: #d7efeb;
|
||||
--amber: #c67c2b;
|
||||
--red: #c8544f;
|
||||
--shadow: 0 12px 30px rgb(27 54 51 / 8%);
|
||||
font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--canvas);
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-width: 320px; background: var(--canvas); }
|
||||
button, input, select { font: inherit; }
|
||||
button { cursor: pointer; }
|
||||
|
||||
.shell { display: flex; height: 100vh; overflow: hidden; }
|
||||
.nav { width: 236px; flex: none; display: flex; flex-direction: column; color: var(--nav-text); background: var(--nav); transition: width .2s ease; }
|
||||
.shell.is-collapsed .nav { width: 72px; }
|
||||
.brand { height: 70px; display: flex; align-items: center; gap: 11px; padding: 0 20px; border-bottom: 1px solid rgb(255 255 255 / 9%); }
|
||||
.brand-mark { width: 32px; height: 32px; display: grid; place-items: center; flex: none; border-radius: 9px; color: #17312f; background: #a8dfd6; font-size: 15px; font-weight: 800; }
|
||||
.brand-copy { overflow: hidden; white-space: nowrap; }
|
||||
.brand-copy strong { display: block; font-size: 14px; letter-spacing: .2px; }
|
||||
.brand-copy small { display: block; margin-top: 2px; color: var(--nav-muted); font-size: 11px; }
|
||||
.is-collapsed .brand { justify-content: center; padding: 0; }
|
||||
.is-collapsed .brand-copy, .is-collapsed .nav-label, .is-collapsed .nav-section { display: none; }
|
||||
.nav-scroll { flex: 1; padding: 18px 12px; overflow: auto; }
|
||||
.nav-section { margin: 0 10px 8px; color: #718c87; font-size: 11px; letter-spacing: .08em; }
|
||||
.nav-item { position: relative; display: flex; align-items: center; gap: 12px; width: 100%; height: 42px; margin: 3px 0; padding: 0 12px; border: 0; border-radius: 8px; color: var(--nav-muted); background: transparent; text-align: left; transition: .15s ease; }
|
||||
.nav-item:hover { color: var(--nav-text); background: rgb(255 255 255 / 6%); }
|
||||
.nav-item.active { color: #eafff9; background: var(--nav-soft); }
|
||||
.nav-item.active::before { position: absolute; left: -12px; width: 3px; height: 22px; border-radius: 0 3px 3px 0; background: #80d4c9; content: ""; }
|
||||
.nav-icon { width: 20px; flex: none; color: #9fc9c2; font-size: 16px; text-align: center; }
|
||||
.nav-footer { padding: 12px; border-top: 1px solid rgb(255 255 255 / 9%); }
|
||||
.profile { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: 8px; background: rgb(255 255 255 / 5%); }
|
||||
.avatar { width: 30px; height: 30px; display: grid; place-items: center; flex: none; border-radius: 50%; color: #234240; background: #c2e9e2; font-size: 12px; font-weight: 700; }
|
||||
.profile-text { overflow: hidden; white-space: nowrap; }
|
||||
.profile-text strong { display: block; color: var(--nav-text); font-size: 12px; }
|
||||
.profile-text small { display: block; margin-top: 2px; color: var(--nav-muted); font-size: 11px; }
|
||||
.collapse-btn { width: 100%; height: 34px; margin-top: 8px; border: 0; border-radius: 7px; color: var(--nav-muted); background: transparent; font-size: 12px; text-align: left; }
|
||||
.collapse-btn:hover { color: var(--nav-text); background: rgb(255 255 255 / 7%); }
|
||||
.is-collapsed .collapse-btn { text-align: center; }
|
||||
|
||||
.workspace { min-width: 0; flex: 1; display: flex; flex-direction: column; background: var(--canvas); }
|
||||
.header { height: 64px; display: flex; align-items: center; gap: 20px; flex: none; padding: 0 28px; border-bottom: 1px solid var(--line); background: rgb(247 250 249 / 92%); }
|
||||
.header-context { min-width: 180px; }
|
||||
.eyebrow { color: var(--muted); font-size: 11px; }
|
||||
.header-context strong { display: block; margin-top: 4px; font-size: 15px; }
|
||||
.search { width: min(420px, 40vw); height: 36px; display: flex; align-items: center; gap: 9px; padding: 0 12px; border: 1px solid var(--line); border-radius: 8px; color: var(--muted); background: var(--panel-raised); }
|
||||
.search input { width: 100%; border: 0; outline: 0; color: var(--text); background: transparent; font-size: 13px; }
|
||||
.search kbd { padding: 2px 5px; border: 1px solid var(--line); border-radius: 4px; color: var(--muted); background: var(--canvas); font-size: 10px; }
|
||||
.header-actions { display: flex; align-items: center; gap: 7px; margin-left: auto; }
|
||||
.icon-btn { width: 34px; height: 34px; display: grid; place-items: center; border: 0; border-radius: 7px; color: var(--muted); background: transparent; font-size: 16px; }
|
||||
.icon-btn:hover { color: var(--teal); background: var(--teal-soft); }
|
||||
.user-chip { height: 36px; display: flex; align-items: center; gap: 8px; padding: 0 8px; border: 1px solid var(--line); border-radius: 8px; color: var(--text); background: var(--panel-raised); font-size: 12px; }
|
||||
|
||||
.content { min-height: 0; flex: 1; overflow: auto; padding: 26px 30px 36px; }
|
||||
.page { width: min(1440px, 100%); margin: 0 auto; }
|
||||
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin-bottom: 22px; }
|
||||
.breadcrumb { margin-bottom: 8px; color: var(--muted); font-size: 12px; }
|
||||
h1 { margin: 0; font-size: 24px; line-height: 1.2; letter-spacing: 0; }
|
||||
.page-desc { margin: 8px 0 0; color: var(--muted); font-size: 13px; }
|
||||
.head-actions { display: flex; gap: 8px; }
|
||||
.btn { height: 36px; padding: 0 14px; border: 1px solid var(--line); border-radius: 7px; color: var(--text); background: var(--panel-raised); font-size: 13px; }
|
||||
.btn:hover { border-color: #9bc9c2; color: var(--teal); }
|
||||
.btn-primary { border-color: var(--teal); color: #fff; background: var(--teal); }
|
||||
.btn-primary:hover { color: #fff; background: #11736c; }
|
||||
|
||||
.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||
.stat { min-height: 92px; padding: 16px 18px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
|
||||
.stat-label { color: var(--muted); font-size: 12px; }
|
||||
.stat-value { margin-top: 10px; font-size: 23px; font-weight: 700; }
|
||||
.stat-meta { margin-top: 4px; color: var(--teal); font-size: 11px; }
|
||||
.stat-meta.warn { color: var(--amber); }
|
||||
|
||||
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px 14px; border: 1px solid var(--line); border-bottom: 0; border-radius: 10px 10px 0 0; background: var(--panel); }
|
||||
.filters { display: flex; flex: 1; flex-wrap: wrap; gap: 8px; }
|
||||
.field { height: 34px; min-width: 150px; padding: 0 10px; border: 1px solid var(--line); border-radius: 6px; outline: 0; color: var(--text); background: var(--panel-raised); font-size: 12px; }
|
||||
.field:focus { border-color: var(--teal); box-shadow: 0 0 0 3px rgb(22 135 127 / 12%); }
|
||||
.toolbar-tools { display: flex; gap: 6px; }
|
||||
.toolbar-tools .btn { height: 34px; padding: 0 10px; }
|
||||
|
||||
.table-wrap { overflow: auto; border: 1px solid var(--line); border-radius: 0 0 10px 10px; background: var(--panel-raised); box-shadow: var(--shadow); }
|
||||
table { width: 100%; min-width: 760px; border-collapse: collapse; font-size: 13px; }
|
||||
th { height: 42px; padding: 0 15px; color: var(--muted); background: #eaf1ef; font-size: 11px; font-weight: 600; text-align: left; white-space: nowrap; }
|
||||
td { height: 50px; padding: 0 15px; border-top: 1px solid #e4ebe9; white-space: nowrap; }
|
||||
tr:hover td { background: #f0f7f5; }
|
||||
.name-cell { display: flex; align-items: center; gap: 9px; }
|
||||
.entity { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 7px; color: var(--teal); background: var(--teal-soft); font-size: 11px; font-weight: 700; }
|
||||
.name-cell strong { display: block; font-size: 13px; }
|
||||
.name-cell small { display: block; margin-top: 2px; color: var(--muted); font-size: 11px; }
|
||||
.status { display: inline-flex; align-items: center; gap: 5px; color: #277d66; font-size: 12px; }
|
||||
.status::before { width: 6px; height: 6px; border-radius: 50%; background: currentColor; content: ""; }
|
||||
.status.warn { color: var(--amber); }
|
||||
.row-action { border: 0; color: var(--teal); background: transparent; font-size: 12px; }
|
||||
.row-action:hover { text-decoration: underline; }
|
||||
.table-foot { display: flex; align-items: center; justify-content: space-between; padding: 12px 15px; color: var(--muted); font-size: 12px; }
|
||||
.pagination { display: flex; gap: 5px; }
|
||||
.page-btn { width: 28px; height: 28px; border: 1px solid var(--line); border-radius: 5px; color: var(--muted); background: var(--panel-raised); }
|
||||
.page-btn.active { border-color: var(--teal); color: #fff; background: var(--teal); }
|
||||
|
||||
.toast { position: fixed; right: 24px; bottom: 24px; max-width: 300px; padding: 11px 14px; border: 1px solid #a8d9d1; border-radius: 8px; color: #225e58; background: #e5f5f1; box-shadow: var(--shadow); font-size: 13px; opacity: 0; transform: translateY(8px); pointer-events: none; transition: .2s ease; }
|
||||
.toast.show { opacity: 1; transform: translateY(0); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.nav { width: 72px; }
|
||||
.brand { justify-content: center; padding: 0; }
|
||||
.brand-copy, .nav-label, .nav-section, .profile-text { display: none; }
|
||||
.profile { justify-content: center; }
|
||||
.header { gap: 10px; padding: 0 16px; }
|
||||
.header-context { min-width: 100px; }
|
||||
.search { width: 34px; padding: 0 9px; }
|
||||
.search input, .search kbd { display: none; }
|
||||
.content { padding: 20px 16px 28px; }
|
||||
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.header-context { display: none; }
|
||||
.page-head { align-items: flex-start; flex-direction: column; }
|
||||
.head-actions { width: 100%; }
|
||||
.head-actions .btn { flex: 1; }
|
||||
.stats { gap: 8px; }
|
||||
.stat { padding: 13px; }
|
||||
.stat-value { font-size: 20px; }
|
||||
.toolbar { align-items: stretch; flex-direction: column; }
|
||||
.field { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell" id="shell">
|
||||
<aside class="nav">
|
||||
<div class="brand">
|
||||
<div class="brand-mark">F</div>
|
||||
<div class="brand-copy"><strong>FMS 工作台</strong><small>运营管理控制台</small></div>
|
||||
</div>
|
||||
<div class="nav-scroll">
|
||||
<div class="nav-section">工作区</div>
|
||||
<button class="nav-item active" data-nav="overview"><span class="nav-icon">⌂</span><span class="nav-label">工作台</span></button>
|
||||
<button class="nav-item" data-nav="partners"><span class="nav-icon">▦</span><span class="nav-label">合作伙伴</span></button>
|
||||
<button class="nav-item" data-nav="contacts"><span class="nav-icon">◌</span><span class="nav-label">联系人</span></button>
|
||||
<div class="nav-section" style="margin-top: 25px">系统</div>
|
||||
<button class="nav-item" data-nav="modules"><span class="nav-icon">◇</span><span class="nav-label">模块管理</span></button>
|
||||
<button class="nav-item" data-nav="users"><span class="nav-icon">◎</span><span class="nav-label">用户与权限</span></button>
|
||||
<button class="nav-item" data-nav="logs"><span class="nav-icon">≡</span><span class="nav-label">操作日志</span></button>
|
||||
</div>
|
||||
<div class="nav-footer">
|
||||
<div class="profile"><div class="avatar">林</div><div class="profile-text"><strong>林晓明</strong><small>系统管理员</small></div></div>
|
||||
<button class="collapse-btn" id="collapseBtn">‹ 收起导航</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="workspace">
|
||||
<header class="header">
|
||||
<div class="header-context"><div class="eyebrow">当前组织</div><strong>华东运营中心</strong></div>
|
||||
<label class="search"><span>⌕</span><input placeholder="搜索模块、记录或命令" /><kbd>Ctrl K</kbd></label>
|
||||
<div class="header-actions">
|
||||
<button class="icon-btn" title="通知">♧</button>
|
||||
<button class="icon-btn" title="帮助">?</button>
|
||||
<button class="user-chip"><span class="avatar">林</span><span>林晓明</span><span>⌄</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<div class="page">
|
||||
<div class="page-head">
|
||||
<div><div class="breadcrumb">工作区 / 总览</div><h1>工作台</h1><p class="page-desc">快速了解业务状态,并从最近工作继续。</p></div>
|
||||
<div class="head-actions"><button class="btn" id="refreshBtn">↻ 刷新数据</button><button class="btn btn-primary" id="createBtn">+ 新建记录</button></div>
|
||||
</div>
|
||||
|
||||
<section class="stats">
|
||||
<div class="stat"><div class="stat-label">合作伙伴总数</div><div class="stat-value">1,284</div><div class="stat-meta">↑ 8.4% 较上月</div></div>
|
||||
<div class="stat"><div class="stat-label">本月新增</div><div class="stat-value">86</div><div class="stat-meta">↑ 12 条待审核</div></div>
|
||||
<div class="stat"><div class="stat-label">待处理事项</div><div class="stat-value">23</div><div class="stat-meta warn">需要关注</div></div>
|
||||
<div class="stat"><div class="stat-label">系统可用率</div><div class="stat-value">99.98%</div><div class="stat-meta">运行正常</div></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="toolbar">
|
||||
<div class="filters"><input class="field" placeholder="搜索名称 / 编号" /><select class="field"><option>全部状态</option><option>正常</option><option>待审核</option></select><select class="field"><option>全部区域</option><option>华东</option><option>华南</option></select></div>
|
||||
<div class="toolbar-tools"><button class="btn" id="advancedBtn">更多筛选</button><button class="btn" id="columnsBtn">列设置</button></div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>合作伙伴</th><th>区域</th><th>联系人</th><th>最近更新</th><th>状态</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><div class="name-cell"><span class="entity">海</span><span><strong>海岳供应链有限公司</strong><small>PT-20240128</small></span></div></td><td>华东</td><td>周敏</td><td>今天 10:32</td><td><span class="status">正常</span></td><td><button class="row-action">查看详情</button></td></tr>
|
||||
<tr><td><div class="name-cell"><span class="entity">晨</span><span><strong>晨光科技(苏州)</strong><small>PT-20240119</small></span></div></td><td>华东</td><td>王立</td><td>昨天 16:08</td><td><span class="status">正常</span></td><td><button class="row-action">查看详情</button></td></tr>
|
||||
<tr><td><div class="name-cell"><span class="entity">南</span><span><strong>南方精密制造</strong><small>PT-20231207</small></span></div></td><td>华南</td><td>陈希</td><td>08-03 14:21</td><td><span class="status warn">待审核</span></td><td><button class="row-action">查看详情</button></td></tr>
|
||||
<tr><td><div class="name-cell"><span class="entity">远</span><span><strong>远洲物流服务中心</strong><small>PT-20231122</small></span></div></td><td>华北</td><td>李然</td><td>08-01 09:45</td><td><span class="status">正常</span></td><td><button class="row-action">查看详情</button></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-foot"><span>显示 1-4 条,共 1,284 条</span><div class="pagination"><button class="page-btn active">1</button><button class="page-btn">2</button><button class="page-btn">3</button><button class="page-btn">›</button></div></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
<div class="toast" id="toast"></div>
|
||||
<script>
|
||||
const shell = document.getElementById("shell");
|
||||
const toast = document.getElementById("toast");
|
||||
let toastTimer;
|
||||
function showToast(message) { toast.textContent = message; toast.classList.add("show"); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove("show"), 2200); }
|
||||
document.querySelectorAll(".nav-item").forEach((item) => item.addEventListener("click", () => { document.querySelectorAll(".nav-item").forEach((nav) => nav.classList.remove("active")); item.classList.add("active"); showToast(`已切换到${item.textContent.trim()}`); }));
|
||||
document.getElementById("refreshBtn").addEventListener("click", () => showToast("数据已刷新"));
|
||||
document.getElementById("createBtn").addEventListener("click", () => showToast("打开新建记录面板"));
|
||||
document.getElementById("advancedBtn").addEventListener("click", () => showToast("高级筛选可以在抽屉中展开"));
|
||||
document.getElementById("columnsBtn").addEventListener("click", () => showToast("打开列设置"));
|
||||
document.getElementById("collapseBtn").addEventListener("click", () => { shell.classList.toggle("is-collapsed"); document.getElementById("collapseBtn").innerHTML = shell.classList.contains("is-collapsed") ? "›" : "‹ 收起导航"; });
|
||||
document.querySelector(".user-chip").addEventListener("click", () => showToast("打开账户菜单"));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,28 +2,28 @@
|
||||
import zhCN from "antdv-next/locale/zh_CN";
|
||||
|
||||
const themeConfig = {
|
||||
token: {
|
||||
controlHeight: 28,
|
||||
},
|
||||
components: {
|
||||
Splitter: {
|
||||
splitBarSize: 1,
|
||||
},
|
||||
Button: {
|
||||
contentFontSize: 13,
|
||||
paddingInline: 10,
|
||||
},
|
||||
Form: {
|
||||
labelFontSize: 13,
|
||||
verticalLabelMargin: "0 0 2px",
|
||||
itemMarginBottom: 8,
|
||||
},
|
||||
Collapse: {
|
||||
headerBg: "#ffffff",
|
||||
contentBg: "#ffffff",
|
||||
borderlessContentBg: "#ffffff",
|
||||
},
|
||||
},
|
||||
// token: {
|
||||
// controlHeight: 26,
|
||||
// },
|
||||
// components: {
|
||||
// Splitter: {
|
||||
// splitBarSize: 1,
|
||||
// },
|
||||
// Button: {
|
||||
// contentFontSize: 12,
|
||||
// paddingInline: 10,
|
||||
// },
|
||||
// Form: {
|
||||
// labelFontSize: 12,
|
||||
// verticalLabelMargin: "0 0 2px",
|
||||
// itemMarginBottom: 8,
|
||||
// },
|
||||
// Collapse: {
|
||||
// headerBg: "#ffffff",
|
||||
// contentBg: "#ffffff",
|
||||
// borderlessContentBg: "#ffffff",
|
||||
// },
|
||||
// },
|
||||
};
|
||||
const timePickerLocale = {
|
||||
...zhCN.TimePicker,
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FMS 海运业务管理台 · 概念原型 v3</title>
|
||||
<style>
|
||||
:root{
|
||||
color-scheme:light;
|
||||
--background:oklch(0.981 0.003 250);
|
||||
--foreground:oklch(0.235 0.012 255);
|
||||
--card:oklch(1 0 0);
|
||||
--card-foreground:oklch(0.235 0.012 255);
|
||||
--primary:oklch(0.43 0.085 245);
|
||||
--primary-foreground:oklch(0.99 0.002 250);
|
||||
--secondary:oklch(0.965 0.004 250);
|
||||
--secondary-foreground:oklch(0.3 0.012 255);
|
||||
--muted:oklch(0.966 0.004 250);
|
||||
--muted-foreground:oklch(0.535 0.012 255);
|
||||
--accent:oklch(0.948 0.014 245);
|
||||
--accent-foreground:oklch(0.36 0.075 245);
|
||||
--destructive:oklch(0.545 0.16 25);
|
||||
--warning:oklch(0.66 0.12 68);
|
||||
--border:oklch(0.906 0.005 250);
|
||||
--border-strong:oklch(0.845 0.008 250);
|
||||
--input:oklch(0.885 0.006 250);
|
||||
--ring:oklch(0.43 0.085 245);
|
||||
--radius:0.3rem;
|
||||
--row-h:2.25rem; --cell-px:0.75rem; --block-gap:1rem;
|
||||
}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body,#app{height:100%}
|
||||
body{
|
||||
font-family:"Inter",-apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
||||
font-size:13px;color:var(--foreground);background:var(--background);
|
||||
-webkit-font-smoothing:antialiased;
|
||||
font-feature-settings:'cv05','ss01','tnum';
|
||||
}
|
||||
button,input,select{font:inherit;color:inherit;background:none;border:none;outline:none}
|
||||
button{cursor:pointer}
|
||||
input::placeholder{color:var(--muted-foreground)}
|
||||
.num{font-variant-numeric:tabular-nums;letter-spacing:-.01em}
|
||||
::-webkit-scrollbar{width:8px;height:8px}
|
||||
::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:4px}
|
||||
|
||||
/* ====== Layout ====== */
|
||||
#app{display:flex;min-height:100svh}
|
||||
|
||||
.sidebar{
|
||||
width:208px;flex-shrink:0;background:var(--card);
|
||||
border-right:1px solid var(--border);display:flex;flex-direction:column;
|
||||
position:sticky;top:0;height:100svh;overflow-y:auto;
|
||||
}
|
||||
.sidebar-logo{
|
||||
display:flex;align-items:center;gap:8px;height:44px;padding:0 12px;
|
||||
border-bottom:1px solid var(--border);flex-shrink:0;
|
||||
}
|
||||
.sidebar-logo .mark{
|
||||
width:20px;height:20px;border-radius:3px;background:var(--primary);
|
||||
color:var(--primary-foreground);display:flex;align-items:center;justify-content:center;
|
||||
font-size:11px;font-weight:600;flex-shrink:0;
|
||||
}
|
||||
.sidebar-logo .label{font-size:13px;font-weight:600;letter-spacing:-.01em;}
|
||||
.sidebar-nav{flex:1;padding:4px 8px;display:flex;flex-direction:column;gap:4px;overflow-y:auto}
|
||||
.nav-group{display:flex;flex-direction:column}
|
||||
.nav-group-header{
|
||||
display:flex;align-items:center;height:28px;padding:0 8px;margin-bottom:1px;
|
||||
font-size:12px;font-weight:500;color:var(--foreground);cursor:pointer;border-radius:5px;user-select:none;
|
||||
transition:background .1s;
|
||||
}
|
||||
.nav-group-header:hover{background:var(--muted)}
|
||||
.nav-group-header .chevron{
|
||||
width:10px;height:10px;margin-left:auto;color:var(--muted-foreground);
|
||||
transition:transform .15s;flex-shrink:0;
|
||||
}
|
||||
.nav-group.collapsed .nav-group-header .chevron{transform:rotate(-90deg)}
|
||||
.nav-group-items{overflow:hidden;max-height:600px;transition:max-height .2s ease}
|
||||
.nav-group.collapsed .nav-group-items{max-height:0}
|
||||
.nav-item{
|
||||
display:flex;align-items:center;gap:8px;width:calc(100% - 10px);height:28px;padding:0 8px;margin-left:10px;
|
||||
border-radius:5px;font-size:13px;color:oklch(0.235 0.012 255 / .8);
|
||||
transition:background .1s;white-space:nowrap;background:none;border:none;cursor:pointer;
|
||||
}
|
||||
.nav-item:hover{background:var(--muted);color:var(--foreground)}
|
||||
.nav-item.active{background:var(--accent);color:var(--accent-foreground);font-weight:500}
|
||||
.nav-item svg{width:14px;height:14px;color:var(--muted-foreground);flex-shrink:0}
|
||||
.nav-item .badge{font-size:10px;color:var(--muted-foreground);margin-left:auto}
|
||||
|
||||
.nav-group-header.has-active{background:var(--accent);color:var(--accent-foreground)}
|
||||
|
||||
.sidebar-foot{border-top:1px solid var(--border);padding:8px;flex-shrink:0}
|
||||
|
||||
.main{flex:1;min-width:0;display:flex;flex-direction:column}
|
||||
|
||||
.tabstrip{
|
||||
height:38px;flex-shrink:0;background:var(--muted);border-bottom:1px solid var(--border-strong);
|
||||
display:flex;align-items:stretch;padding:0 4px;gap:0;
|
||||
position:sticky;top:0;z-index:10;
|
||||
}
|
||||
.tabstrip-left{display:flex;align-items:stretch;flex:1;min-width:0;overflow-x:auto}
|
||||
.tab{
|
||||
display:flex;align-items:center;gap:6px;max-width:224px;flex-shrink:0;
|
||||
height:100%;padding:0 10px;font-size:12px;color:var(--muted-foreground);
|
||||
border-right:1px solid var(--border);position:relative;cursor:pointer;
|
||||
}
|
||||
.tab:hover{background:oklch(1 0 0 / .6);color:var(--foreground)}
|
||||
.tab.active{background:var(--background);color:var(--foreground);font-weight:500}
|
||||
.tab.active::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--primary)}
|
||||
.tab svg{width:14px;height:14px;color:var(--muted-foreground);flex-shrink:0}
|
||||
.tab .close{width:16px;height:16px;display:flex;align-items:center;justify-content:center;border-radius:3px;opacity:0;transition:opacity .1s;margin-left:2px;font-size:13px;color:var(--muted-foreground)}
|
||||
.tab:hover .close{opacity:1}
|
||||
.tab .close:hover{background:var(--muted);color:var(--foreground)}
|
||||
.tab-plus{width:32px;height:100%;display:flex;align-items:center;justify-content:center;color:var(--muted-foreground);cursor:pointer;flex-shrink:0;border-right:1px solid var(--border)}
|
||||
.tab-plus:hover{background:oklch(1 0 0 / .6);color:var(--foreground)}
|
||||
.tabstrip-right{display:flex;align-items:center;gap:2px;padding:0 6px;flex-shrink:0}
|
||||
.tbar-bell{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:5px;color:var(--muted-foreground);cursor:pointer;position:relative}
|
||||
.tbar-bell:hover{background:var(--muted);color:var(--foreground)}
|
||||
.tbar-bell::after{content:'';position:absolute;top:6px;right:6px;width:5px;height:5px;border-radius:50%;background:var(--destructive);border:1.5px solid var(--card)}
|
||||
.tbar-avatar{width:26px;height:26px;border-radius:5px;background:var(--primary-soft);color:var(--primary);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;cursor:pointer}
|
||||
.tbar-divider{width:1px;height:18px;background:var(--border);margin:0 4px}
|
||||
|
||||
.content{flex:1}
|
||||
|
||||
/* ====== Metrics ====== */
|
||||
.metrics{display:flex;flex-wrap:wrap;border-bottom:1px solid var(--border);background:var(--card)}
|
||||
.metrics>*+*{border-left:1px solid var(--border)}
|
||||
.metric{min-width:132px;padding:8px 12px;display:flex;flex-direction:column;gap:2px}
|
||||
.metric .l{font-size:11px;color:var(--muted-foreground)}
|
||||
.metric .v{display:flex;align-items:baseline;gap:4px}
|
||||
.metric .v .n{font-size:15px;font-weight:500;line-height:1.25}
|
||||
.metric .v .u{font-size:10px;color:var(--muted-foreground)}
|
||||
.metric .v .alert{color:var(--warning)}
|
||||
|
||||
/* ====== Toolbar ====== */
|
||||
.toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--border);background:var(--card)}
|
||||
.search-box{display:flex;align-items:center;border:1px solid var(--input);border-radius:2px;background:var(--background)}
|
||||
.search-box:focus-within{border-color:var(--ring);box-shadow:0 0 0 2px oklch(0.43 0.085 245 / .2)}
|
||||
.search-box input{height:28px;width:256px;padding:0 8px;font-size:12px;background:transparent}
|
||||
.search-box input::placeholder{color:var(--muted-foreground)}
|
||||
.chips{display:flex;gap:2px}
|
||||
.chip{height:28px;padding:0 8px;font-size:12px;border-radius:2px;color:var(--muted-foreground)}
|
||||
.chip:hover{background:var(--muted);color:var(--foreground)}
|
||||
.chip.on{background:var(--accent);color:var(--accent-foreground);font-weight:500}
|
||||
.toolbar .spacer{flex:1}
|
||||
.btn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 8px;font-size:12px;border-radius:2px;border:1px solid var(--border);color:oklch(0.235 0.012 255 / .8)}
|
||||
.btn:hover{background:var(--muted);color:var(--foreground)}
|
||||
.btn-primary{background:var(--primary);border-color:var(--primary);color:var(--primary-foreground);font-weight:500}
|
||||
.btn-primary:hover{opacity:.9;color:var(--primary-foreground)}
|
||||
.adv-panel{display:none;padding:12px;border-bottom:1px solid var(--border);background:oklch(0.966 0.004 250 / .4);grid-template-columns:repeat(4,1fr);gap:10px 16px}
|
||||
.adv-panel.open{display:grid}
|
||||
.adv-field{display:flex;flex-direction:column;gap:4px}
|
||||
.adv-field label{font-size:11px;color:var(--muted-foreground)}
|
||||
.adv-field input,.adv-field select{height:28px;border:1px solid var(--input);border-radius:2px;background:var(--background);padding:0 8px;font-size:12px}
|
||||
|
||||
/* ====== Table ====== */
|
||||
.table-wrap{overflow-x:auto}
|
||||
table{width:100%;min-width:1100px;border-collapse:collapse;font-size:12px;white-space:nowrap}
|
||||
thead th{font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.04em;color:var(--muted-foreground);text-align:left;padding:6px var(--cell-px);border-bottom:1px solid var(--border-strong);background:oklch(0.966 0.004 250 / .6)}
|
||||
thead th button{display:inline-flex;align-items:center;gap:4px;font:inherit;color:inherit;text-transform:uppercase}
|
||||
thead th button:hover{color:var(--foreground)}
|
||||
tbody td{height:var(--row-h);padding:0 var(--cell-px);border-bottom:1px solid oklch(0.906 0.005 250 / .7);color:var(--foreground)}
|
||||
tbody tr{cursor:default}
|
||||
tbody tr:hover{background:oklch(0.966 0.004 250 / .7)}
|
||||
tbody tr.sel{background:oklch(0.948 0.014 245 / .7)}
|
||||
.status{display:inline-flex;align-items:center;gap:6px;font-size:12px}
|
||||
.status::before{content:'';width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
||||
.st-draft{color:var(--muted-foreground)}.st-draft::before{background:var(--border-strong)}
|
||||
.st-booked{color:var(--foreground)}.st-booked::before{background:oklch(0.43 0.085 245 / .6)}
|
||||
.st-sailing{color:var(--primary)}.st-sailing::before{background:var(--primary)}
|
||||
.st-arrived{color:var(--foreground)}.st-arrived::before{background:oklch(0.43 0.085 245 / .4)}
|
||||
.st-hold{color:var(--warning)}.st-hold::before{background:var(--warning)}
|
||||
.st-released{color:var(--muted-foreground)}.st-released::before{background:oklch(0.235 0.012 255 / .25)}
|
||||
|
||||
.summary{display:flex;flex-wrap:wrap;align-items:center;gap:20px;padding:8px 12px;font-size:11px;color:var(--muted-foreground);border-top:1px solid var(--border);background:var(--card)}
|
||||
.summary .n{color:var(--foreground)}
|
||||
|
||||
/* ====== Detail panel ====== */
|
||||
.detail{
|
||||
position:sticky;top:38px;width:320px;flex-shrink:0;height:calc(100svh - 38px);
|
||||
background:var(--card);border-left:1px solid var(--border);overflow-y:auto;
|
||||
}
|
||||
.detail-head{display:flex;align-items:center;gap:8px;height:36px;padding:0 12px;border-bottom:1px solid var(--border)}
|
||||
.detail-head .id{font-size:12px;font-weight:500}
|
||||
.detail-head .close-btn{margin-left:auto;color:var(--muted-foreground)}
|
||||
.detail-head .close-btn:hover{color:var(--foreground)}
|
||||
.detail dl{display:flex;flex-direction:column}
|
||||
.detail dl>div{display:flex;gap:12px;padding:6px 12px;font-size:12px;border-bottom:1px solid oklch(0.906 0.005 250 / .7)}
|
||||
.detail dt{width:80px;flex-shrink:0;color:var(--muted-foreground)}
|
||||
.detail dd{flex:1;min-width:0;word-break:break-word}
|
||||
.detail-actions{padding:12px;border-top:1px solid var(--border);display:flex;flex-wrap:wrap;gap:6px;margin-top:auto}
|
||||
|
||||
/* ====== Form page ====== */
|
||||
.form .form-head{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border);background:var(--card)}
|
||||
.form .form-title{font-size:13px;font-weight:600;letter-spacing:-.01em}
|
||||
.form .form-sub{font-size:11px;color:var(--muted-foreground);margin-top:1px}
|
||||
.form-tabstrip{display:flex;gap:16px;padding:0 12px;border-bottom:1px solid var(--border);background:var(--card)}
|
||||
.form-tab{
|
||||
position:relative;padding:8px 0;font-size:12px;color:var(--muted-foreground);display:flex;align-items:center;gap:6px;cursor:pointer;
|
||||
}
|
||||
.form-tab:hover{color:var(--foreground)}
|
||||
.form-tab.on{color:var(--foreground);font-weight:500}
|
||||
.form-tab.on::after{content:'';position:absolute;bottom:-1px;left:0;right:0;height:2px;background:var(--primary)}
|
||||
.form-tab .cnt{font-size:10px;color:var(--muted-foreground)}
|
||||
.form-group{border-bottom:1px solid var(--border);padding:16px 0}
|
||||
.form-group:first-child{padding-top:12px}
|
||||
.form-group:last-child{border-bottom:none}
|
||||
.form-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--block-gap) 16px}
|
||||
.field{display:flex;flex-direction:column;gap:4px}
|
||||
.field label{font-size:11px;color:var(--muted-foreground);display:flex;align-items:baseline;gap:4px}
|
||||
.field label .req{color:var(--destructive)}
|
||||
.field label .hint{margin-left:auto;font-size:10px}
|
||||
.f-input{height:28px;border:1px solid var(--input);border-radius:2px;background:var(--background);padding:0 8px;font-size:12px;width:100%}
|
||||
.f-input:focus{border-color:var(--ring);box-shadow:0 0 0 2px oklch(0.43 0.085 245 / .2);outline:none}
|
||||
.field select.f-input{cursor:pointer}
|
||||
|
||||
.form-main{max-width:760px;margin:0 auto;padding:12px 16px 32px}
|
||||
.form-section-title{font-size:12px;font-weight:600;letter-spacing:-.01em;margin-bottom:10px}
|
||||
|
||||
/* ====== Batch bar ====== */
|
||||
.batch-bar{display:flex;align-items:center;gap:12px;padding:6px 12px;border-bottom:1px solid var(--border);background:var(--accent);font-size:12px;color:var(--accent-foreground)}
|
||||
.batch-bar .actions{display:flex;gap:6px}
|
||||
.batch-bar .actions button{height:24px;padding:0 8px;border-radius:2px;border:1px solid var(--border);background:var(--card);font-size:12px;color:var(--foreground)}
|
||||
.batch-bar .actions button:hover{background:var(--muted)}
|
||||
</style></head><body>
|
||||
<div id="app">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-logo"><div class="mark">F</div><span class="label">FMS 货代云</span></div>
|
||||
<nav class="sidebar-nav" id="sidebarNav"></nav>
|
||||
<div class="sidebar-foot" style="font-size:10px;color:var(--muted-foreground);text-align:center">FMS v3.0</div>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="tabstrip" id="tabstrip"></div>
|
||||
<div class="content" id="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ============ DATA ============ */
|
||||
const NAV=[{
|
||||
group:'业务',items:[
|
||||
{id:'sea',label:'海运订单',icon:'ship',badge:'128'},
|
||||
{id:'container',label:'集装箱池',icon:'box',badge:'38'},
|
||||
{id:'docs',label:'单证管理',icon:'file'},
|
||||
]},
|
||||
{group:'财务',items:[
|
||||
{id:'settle',label:'对账结算',icon:'ledger'},
|
||||
{id:'invoice',label:'发票管理',icon:'receipt'},
|
||||
{id:'fee',label:'费用录入',icon:'coins'},
|
||||
{id:'rate',label:'费率管理',icon:'landmark'},
|
||||
]},
|
||||
{group:'基础资料',items:[
|
||||
{id:'customer',label:'客户与委托方',icon:'users'},
|
||||
{id:'ports',label:'港口与航线',icon:'anchor'},
|
||||
{id:'booking',label:'订舱船期配置',icon:'calendar'},
|
||||
]},
|
||||
{group:'系统',items:[
|
||||
{id:'module',label:'模块管理',icon:'grid'},
|
||||
]},
|
||||
];
|
||||
const ORDERS=[
|
||||
{id:'SO-24081',bl:'COSU6258392010',customer:'华为技术有限公司',pol:'CNSHA',pod:'NLRTM',polName:'上海洋山',podName:'鹿特丹',vessel:'CSCL GLOBE',voyage:'088E',etd:'2026-08-12',eta:'2026-09-15',containers:'2×40HQ',teu:4,weight:41.8,freight:3860,currency:'USD',owner:'林岚',updated:'08-05 09:24',status:'sailing'},
|
||||
{id:'SO-24082',bl:'COSU6258392011',customer:'美的集团',pol:'CNSHK',pod:'DEHAM',polName:'深圳蛇口',podName:'汉堡',vessel:'EVER ALP',voyage:'045E',etd:'2026-08-15',eta:'2026-09-18',containers:'1×20GP',teu:1,weight:18.2,freight:1280,currency:'USD',owner:'林岚',updated:'08-05 08:10',status:'booked'},
|
||||
{id:'SO-24083',bl:'MAEU9518324',customer:'比亚迪股份',pol:'CNYTN',pod:'USLAX',polName:'盐田',podName:'洛杉矶',vessel:'OOCL EUROPE',voyage:'122W',etd:'2026-08-18',eta:'2026-09-05',containers:'4×40HQ',teu:8,weight:86.4,freight:6200,currency:'USD',owner:'陈志远',updated:'08-04 17:32',status:'booked'},
|
||||
{id:'SO-24084',bl:'',customer:'顺丰国际',pol:'CNYTN',pod:'USLAX',polName:'盐田',podName:'洛杉矶',vessel:'—',voyage:'—',etd:'2026-08-20',eta:'—',containers:'拼箱 8.6CBM',teu:0,weight:3.2,freight:0,currency:'USD',owner:'林岚',updated:'08-05 07:45',status:'draft'},
|
||||
{id:'SO-24085',bl:'OOLU7849120',customer:'格力电器',pol:'CNZUH',pod:'AEDXB',polName:'珠海',podName:'迪拜',vessel:'HMM DREAM',voyage:'034S',etd:'2026-08-21',eta:'2026-09-02',containers:'2×40GP',teu:4,weight:44.6,freight:2950,currency:'USD',owner:'王海',updated:'08-03 14:20',status:'loaded'},
|
||||
{id:'SO-24086',bl:'',customer:'中兴通讯',pol:'CNSHA',pod:'GBFEL',polName:'上海洋山',podName:'费力克斯托',vessel:'—',voyage:'—',etd:'2026-08-23',eta:'—',containers:'1×40HQ',teu:2,weight:0,freight:0,currency:'USD',owner:'林岚',updated:'08-05 10:01',status:'draft'},
|
||||
{id:'SO-24087',bl:'EVER1278490',customer:'TCL 科技',pol:'CNHUI',pod:'USLGB',polName:'惠州',podName:'长滩',vessel:'CMA CGM ZHENG HE',voyage:'156E',etd:'2026-08-25',eta:'2026-09-10',containers:'3×40HQ',teu:6,weight:62.1,freight:4800,currency:'USD',owner:'陈志远',updated:'08-04 11:55',status:'booked'},
|
||||
{id:'SO-24088',bl:'HLCU3319200',customer:'海康威视',pol:'CNHGH',pod:'BEANR',polName:'宁波',podName:'安特卫普',vessel:'AL NEFUD',voyage:'201E',etd:'2026-08-26',eta:'2026-09-20',containers:'2×20GP',teu:2,weight:36.8,freight:2100,currency:'USD',owner:'王海',updated:'08-05 06:30',status:'hold'},
|
||||
];
|
||||
const ICONS={
|
||||
ship:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M2 20a2.4 2.4 0 0 0 2 1 2.4 2.4 0 0 0 2-1 2.4 2.4 0 0 1 2-1 2.4 2.4 0 0 1 2 1 2.4 2.4 0 0 0 2 1 2.4 2.4 0 0 0 2-1 2.4 2.4 0 0 1 2-1 2.4 2.4 0 0 1 2 1 2.4 2.4 0 0 0 2 1 2.4 2.4 0 0 0 2-1"/><path d="M4 18 3 11h18l-1 7"/><path d="M12 4v7"/><path d="M8 8h8"/></svg>',
|
||||
calendar:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>',
|
||||
box:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="M3.3 7l8.7 5 8.7-5"/><path d="M12 22V12"/></svg>',
|
||||
file:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>',
|
||||
ledger:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1z"/><path d="M8 7h8M8 11h8M8 15h5"/></svg>',
|
||||
receipt:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h6"/></svg>',
|
||||
coins:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/></svg>',
|
||||
users:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
|
||||
anchor:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="5" r="3"/><path d="M12 22V8"/><path d="M5 12H2a10 10 0 0 0 20 0h-3"/></svg>',
|
||||
landmark:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 22V12l9-4 9 4v10"/><path d="M9 22V12"/><path d="M15 22V12"/><path d="M3 12h18"/></svg>',
|
||||
grid:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>',
|
||||
plus:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>',
|
||||
search:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>',
|
||||
sliders:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><circle cx="4" cy="12" r="2"/><circle cx="12" cy="10" r="2"/><circle cx="20" cy="14" r="2"/></svg>',
|
||||
download:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></svg>',
|
||||
chevronDown:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>',
|
||||
x:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>',
|
||||
arrowUp:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m18 15-6-6-6 6"/></svg>',
|
||||
arrowDown:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>',
|
||||
clipboard:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg>',
|
||||
fileText:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M16 13H8M16 17H8"/></svg>',
|
||||
};
|
||||
|
||||
/* ============ STATE ============ */
|
||||
let activeNav='sea';
|
||||
let tabs=[{key:'sea',label:'海运订单',type:'list',closable:false,icon:'ship'}];
|
||||
let activeTab='sea';
|
||||
let statusFilter='all';
|
||||
let query='';
|
||||
let sort={key:'id',dir:'desc'};
|
||||
let selectedRows=[];
|
||||
let activeRow=null;
|
||||
let advOpen=false;
|
||||
let adv={bl:'',customer:'',vessel:'',pol:'all',pod:'all',etdFrom:'',etdTo:''};
|
||||
|
||||
/* ============ RENDER ============ */
|
||||
let collapsedGroups={};
|
||||
function renderNav(){
|
||||
document.getElementById('sidebarNav').innerHTML=NAV.map((g,i)=>{
|
||||
const hasActive=g.items.some(it=>activeNav===it.id);
|
||||
if(hasActive) collapsedGroups[i]=false;
|
||||
const collapsed=collapsedGroups[i]!==false;
|
||||
return `<div class="nav-group${collapsed?' collapsed':''}">
|
||||
<div class="nav-group-header${hasActive?' has-active':''}" onclick="toggleGroup(${i})">
|
||||
<span>${g.group}</span>
|
||||
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</div>
|
||||
<div class="nav-group-items">`+
|
||||
g.items.map(it=>`<button class="nav-item${activeNav===it.id?' active':''}" onclick="navigate('${it.id}','${it.label}','${it.icon||'file'}')">${ICONS[it.icon||'file']}<span>${it.label}</span>${it.badge?`<span class="badge">${it.badge}</span>`:''}</button>`).join('')+
|
||||
`</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
function toggleGroup(i){collapsedGroups[i]=!collapsedGroups[i];renderNav();}
|
||||
|
||||
function renderTabs(){
|
||||
document.getElementById('tabstrip').innerHTML=
|
||||
`<div class="tabstrip-left">`+
|
||||
tabs.map(t=>`<div class="tab${activeTab===t.key?' active':''}" onclick="switchTab('${t.key}')">${ICONS[t.icon||'fileText']}<span>${t.label}</span>${t.closable?`<span class="close" onclick="event.stopPropagation();closeTab('${t.key}')">×</span>`:''}</div>`).join('')+
|
||||
`<button class="tab-plus" onclick="openForm()" title="新建">${ICONS.plus}</button>`+
|
||||
`</div>`+
|
||||
`<div class="tabstrip-right">
|
||||
<button class="tbar-bell" title="消息通知">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>
|
||||
</button>
|
||||
<span class="tbar-divider"></span>
|
||||
<div class="tbar-avatar" title="张明 · 操作经理">张</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function navigate(id,label,icon){
|
||||
if(!tabs.find(t=>t.key===id))tabs.push({key:id,label,type:id==='sea'?'list':'placeholder',closable:true,icon});
|
||||
switchTab(id);
|
||||
}
|
||||
|
||||
function switchTab(key){
|
||||
activeTab=key;
|
||||
const t=tabs.find(t=>t.key===key);
|
||||
activeNav=t.key;
|
||||
activeRow=null;
|
||||
selectedRows=[];
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function closeTab(key){
|
||||
const i=tabs.findIndex(t=>t.key===key);
|
||||
tabs.splice(i,1);
|
||||
if(activeTab===key)switchTab(tabs[Math.max(0,i-1)].key);
|
||||
else renderAll();
|
||||
}
|
||||
|
||||
function openForm(){if(!tabs.find(t=>t.key==='form'))tabs.push({key:'form',label:'新建订舱 SO-24089',type:'form',closable:true,icon:'fileText'});switchTab('form');}
|
||||
|
||||
function renderContent(){
|
||||
const t=tabs.find(t=>t.key===activeTab);
|
||||
const el=document.getElementById('content');
|
||||
if(!t||t.type==='placeholder'){el.innerHTML=`<div style="padding:60px;text-align:center;color:var(--muted-foreground);font-size:13px">${t?t.label:'页面'} — 原型仅演示完整列表/详情/新建流程</div>`;return}
|
||||
if(t.type==='form'){el.innerHTML=renderForm();return}
|
||||
el.innerHTML=renderList();
|
||||
}
|
||||
|
||||
function getFiltered(){
|
||||
const q=query.trim().toLowerCase();
|
||||
return ORDERS.filter(o=>{
|
||||
if(statusFilter!=='all'&&o.status!==statusFilter)return false;
|
||||
if(q&&![o.id,o.bl,o.customer,o.vessel,o.pol,o.pod,o.polName,o.podName].join(' ').toLowerCase().includes(q))return false;
|
||||
if(adv.bl&&!o.bl.toLowerCase().includes(adv.bl.toLowerCase()))return false;
|
||||
if(adv.customer&&!o.customer.toLowerCase().includes(adv.customer.toLowerCase()))return false;
|
||||
if(adv.vessel&&!o.vessel.toLowerCase().includes(adv.vessel.toLowerCase()))return false;
|
||||
if(adv.pol!=='all'&&o.pol!==adv.pol)return false;
|
||||
if(adv.pod!=='all'&&o.pod!==adv.pod)return false;
|
||||
if(adv.etdFrom&&o.etd<adv.etdFrom)return false;
|
||||
if(adv.etdTo&&o.etd>adv.etdTo)return false;
|
||||
return true;
|
||||
}).sort((a,b)=>{const av=a[sort.key],bv=b[sort.key];const r=typeof av==='number'?av-bv:String(av).localeCompare(String(bv));return sort.dir==='asc'?r:-r});
|
||||
}
|
||||
|
||||
function renderList(){
|
||||
const rows=getFiltered();
|
||||
const totalTeu=rows.reduce((s,r)=>s+r.teu,0);
|
||||
const totalWeight=rows.reduce((s,r)=>s+r.weight,0);
|
||||
const allChecked=rows.length>0&&selectedRows.length===rows.length;
|
||||
const advCount=(adv.bl?1:0)+(adv.customer?1:0)+(adv.vessel?1:0)+(adv.pol!=='all'?1:0)+(adv.pod!=='all'?1:0)+(adv.etdFrom?1:0)+(adv.etdTo?1:0);
|
||||
const statuses=['all','draft','booked','loaded','sailing','hold'];
|
||||
const labels={all:'全部',draft:'草稿',booked:'已订舱',loaded:'已装船',sailing:'航海中',hold:'异常滞留'};
|
||||
const sortLabel={id:'订舱单号',etd:'ETD',eta:'ETA',teu:'TEU',freight:'运费'};
|
||||
|
||||
return `
|
||||
<div class="metrics">${[{l:'在途箱量',v:'128',u:'TEU'},{l:'本周开船',v:'17',u:'票'},{l:'待确认订舱',v:'6',u:'票'},{l:'异常滞留',v:'1',u:'票',alert:true},{l:'应收运费',v:'412,800',u:'USD'}].map(m=>`<div class="metric"><span class="l">${m.l}</span><div class="v"><span class="n${m.alert?' alert':''}">${m.v}</span><span class="u">${m.u}</span></div></div>`).join('')}</div>
|
||||
<div class="toolbar">
|
||||
<div class="search-box">${ICONS.search}<input placeholder="搜索单号/提单号/客户/船名" value="${q.replace(/"/g,'"')}" oninput="q=this.value;renderContent()"></div>
|
||||
<button class="btn" onclick="advOpen=!advOpen;renderContent()" style="${advCount?'color:var(--primary)':''}">高级${advCount?`<span style="background:var(--primary);color:var(--primary-foreground);border-radius:50%;width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:10px;margin-left:2px">${advCount}</span>`:''}${ICONS.chevronDown}</button>
|
||||
<div class="chips">${statuses.map(s=>`<button class="chip${statusFilter===s?' on':''}" onclick="statusFilter='${s}';renderContent()">${labels[s]}</button>`).join('')}</div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn">${ICONS.sliders}列设置</button>
|
||||
<button class="btn">${ICONS.download}导出</button>
|
||||
<button class="btn btn-primary" onclick="openForm()">${ICONS.plus}新建订舱</button>
|
||||
</div>
|
||||
${advOpen?renderAdvPanel():''}
|
||||
${selectedRows.length>0?`<div class="batch-bar"><span>已选 <strong>${selectedRows.length}</strong> 条</span><div class="actions"><button>确认订舱</button><button>打印提单</button><button>批量导出</button></div><button style="margin-left:auto;background:none;color:var(--muted-foreground);font-size:12px" onclick="selectedRows=[];renderContent()">取消选择</button></div>`:''}
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr>
|
||||
<th style="width:32px"><input type="checkbox" ${allChecked?'checked':''} onclick="event.stopPropagation();selectedRows=allChecked?[]:rows.map(r=>r.id);renderContent()" style="width:12px;height:12px;accent-color:var(--primary)"></th>
|
||||
${['id','status','bl','customer','pol','vessel','etd','eta','containers','teu','freight'].map(k=>k==='id'||k==='etd'||k==='eta'||k==='teu'||k==='freight'?`<th><button onclick="sort.key==='${k}'?sort.dir=sort.dir==='asc'?'desc':'asc':(sort.key='${k}',sort.dir='asc');renderContent()">${sortLabel[k]}${sort.key===k?(sort.dir==='asc'?ICONS.arrowUp:ICONS.arrowDown):''}</button></th>`:(k==='status'?'<th>状态</th>':(k==='bl'?'<th>提单号</th>':(k==='customer'?'<th>委托客户</th>':(k==='pol'?'<th>起运→目的</th>':(k==='vessel'?'<th>船名/航次</th>':(k==='containers'?'<th>箱型箱量</th>':''))))))).join('')}
|
||||
</tr></thead>
|
||||
<tbody>${rows.map(r=>`<tr class="${activeRow===r.id?'sel':''}" onclick="activeRow='${r.id}';renderContent()">
|
||||
<td><input type="checkbox" ${selectedRows.includes(r.id)?'checked':''} onclick="event.stopPropagation();selectedRows.includes(r.id)?selectedRows=selectedRows.filter(x=>x!==r.id):selectedRows.push(r.id);renderContent()" style="width:12px;height:12px;accent-color:var(--primary)"></td>
|
||||
<td class="num" style="font-weight:500">${r.id}</td>
|
||||
<td><span class="status st-${r.status}">${labels[r.status]}</span></td>
|
||||
<td class="num" style="color:var(--muted-foreground)">${r.bl||'—'}</td>
|
||||
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis">${r.customer}</td>
|
||||
<td style="white-space:nowrap"><span class="num" style="font-size:11px;color:var(--muted-foreground)">${r.pol}</span> ${r.polName}<span style="color:var(--border-strong);margin:0 4px">→</span><span class="num" style="font-size:11px;color:var(--muted-foreground)">${r.pod}</span> ${r.podName}</td>
|
||||
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis">${r.vessel}<span class="num" style="color:var(--muted-foreground);margin-left:4px">${r.voyage}</span></td>
|
||||
<td class="num">${r.etd}</td><td class="num">${r.eta||'—'}</td><td class="num">${r.containers}</td>
|
||||
<td class="num" style="text-align:right">${r.teu}</td>
|
||||
<td class="num" style="text-align:right">${r.freight===0?'—':(r.currency==='USD'?'$':'¥')+r.freight.toLocaleString()}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table></div>
|
||||
<div class="summary">
|
||||
<span>共 <span class="n">${rows.length}</span> 条</span>
|
||||
<span>合计 TEU <span class="n">${totalTeu}</span></span>
|
||||
<span>合计毛重 <span class="n">${totalWeight.toFixed(1)}</span> 吨</span>
|
||||
<span style="margin-left:auto">数据同步于 08-05 09:20</span>
|
||||
</div>
|
||||
${activeRow?renderDetail(rows.find(r=>r.id===activeRow)):''}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAdvPanel(){
|
||||
return `<div class="adv-panel open">
|
||||
<div class="adv-field"><label>提单号</label><input placeholder="MBL/HBL" value="${adv.bl}" oninput="adv.bl=this.value;renderContent()"></div>
|
||||
<div class="adv-field"><label>客户</label><input placeholder="客户简称" value="${adv.customer}" oninput="adv.customer=this.value;renderContent()"></div>
|
||||
<div class="adv-field"><label>船名</label><input placeholder="VESSEL" value="${adv.vessel}" oninput="adv.vessel=this.value;renderContent()"></div>
|
||||
<div class="adv-field"><label>装货港 POL</label><select onchange="adv.pol=this.value;renderContent()"><option value="all">全部</option><option value="CNSHA"${adv.pol==='CNSHA'?' selected':''}>上海洋山 · CNSHA</option><option value="CNSHK"${adv.pol==='CNSHK'?' selected':''}>深圳蛇口 · CNSHK</option><option value="CNYTN"${adv.pol==='CNYTN'?' selected':''}>盐田 · CNYTN</option></select></div>
|
||||
<div class="adv-field"><label>卸货港 POD</label><select onchange="adv.pod=this.value;renderContent()"><option value="all">全部</option><option value="NLRTM"${adv.pod==='NLRTM'?' selected':''}>鹿特丹 · NLRTM</option><option value="DEHAM"${adv.pod==='DEHAM'?' selected':''}>汉堡 · DEHAM</option><option value="USLAX"${adv.pod==='USLAX'?' selected':''}>洛杉矶 · USLAX</option></select></div>
|
||||
<div class="adv-field"><label>ETD 从</label><input type="date" value="${adv.etdFrom}" oninput="adv.etdFrom=this.value;renderContent()"></div>
|
||||
<div class="adv-field"><label>ETD 至</label><input type="date" value="${adv.etdTo}" oninput="adv.etdTo=this.value;renderContent()"></div>
|
||||
<div style="grid-column:span 2;display:flex;gap:6px;align-items:end">
|
||||
<button class="btn" style="flex:1" onclick="adv={bl:'',customer:'',vessel:'',pol:'all',pod:'all',etdFrom:'',etdTo:''};renderContent()">重置</button>
|
||||
<button class="btn btn-primary" style="flex:1" onclick="advOpen=false;renderContent()">查询</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderDetail(row){
|
||||
const fields=[['订舱单号',row.id],['提单号',row.bl||'—'],['状态',`<span class="status st-${row.status}">${({draft:'草稿',booked:'已订舱',loaded:'已装船',sailing:'航海中',arrived:'已到港',released:'已放单',hold:'异常滞留'})[row.status]}</span>`],['委托客户',row.customer],['装货港',`${row.polName} / ${row.pol}`],['卸货港',`${row.podName} / ${row.pod}`],['船名航次',`${row.vessel} ${row.voyage}`],['ETD/ETA',`${row.etd} → ${row.eta||'—'}`],['箱型箱量',row.containers],['毛重',`${row.weight||'—'} 吨`],['运费',row.freight===0?'未报价':`${row.currency} ${row.freight.toLocaleString()}`],['操作员',row.owner],['最近更新',row.updated]];
|
||||
return `<div class="detail"><div class="detail-head"><span class="id">${row.id}</span><span class="status st-${row.status}"></span><button class="close-btn" onclick="activeRow=null;renderContent()">${ICONS.x}</button></div><dl>${fields.map(([k,v])=>`<div><dt>${k}</dt><dd>${v}</dd></div>`).join('')}</dl><div class="detail-actions"><button class="btn btn-primary" style="flex:1">编辑</button><button class="btn">确认订舱</button><button class="btn">打印提单</button><button class="btn">费用录入</button></div></div>`;
|
||||
}
|
||||
|
||||
function renderForm(){
|
||||
return `<div class="form">
|
||||
<div class="form-head">
|
||||
<div><div class="form-title">新建订舱单 · SO-24089</div><div class="form-sub">草稿 · 创建于 刚刚 · 操作员 林岚</div></div>
|
||||
<div style="margin-left:auto;display:flex;gap:6px">
|
||||
<button class="btn" onclick="closeTab('form');switchTab('sea')">取消</button>
|
||||
<button class="btn">存为草稿</button>
|
||||
<button class="btn btn-primary">提交订舱</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-tabstrip">
|
||||
<div class="form-tab on">基本信息<span class="cnt">16</span></div>
|
||||
<div class="form-tab" onclick="alert('原型仅演示基本信息')">装箱信息<span class="cnt">12</span></div>
|
||||
<div class="form-tab" onclick="alert('原型仅演示基本信息')">财务信息<span class="cnt">11</span></div>
|
||||
</div>
|
||||
<div class="form-main">
|
||||
<div class="form-group"><div class="form-section-title">委托方</div><div class="form-grid" style="--block-gap:1rem">${f('委托客户','','',true)}${f('客户编码','','','')}${f('业务类型','','','',true)}${f('联系人','姓名','','')}${f('联系电话','+86','','')}${f('销售/操作','林岚','','')}</div></div>
|
||||
<div class="form-group"><div class="form-section-title">航线与船期</div><div class="form-grid" style="--block-gap:1rem">${f('装货港 POL','','','',true)}${f('卸货港 POD','','','',true)}${f('交货地','内陆点/CY')}${f('航线','','','')}${f('船名','VESSEL NAME','','',true)}${f('航次','045E','','',true)}${f('ETD','','','',true)}${f('ETA','','','')}${f('截关时间','','','')}</div></div>
|
||||
<div class="form-group"><div class="form-section-title">单据要求</div><div class="form-grid" style="--block-gap:1rem">${f('放单方式','','','')}${f('报关方式','','','')}${f('提单份数','','','')}${f('备注','特殊要求、订舱备注','',false,true)}</div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
function f(l,p,d,req,col){
|
||||
return `<div class="field"${col?' style="grid-column:span 3"':''}><label>${l}${req?'<span class="req">*</span>':''}${d?`<span class="hint">${d}</span>`:''}</label>${l.includes('POL')||l.includes('POD')?`<select class="f-input"><option>${l.includes('POL')?'上海洋山 · CNSHA':'鹿特丹 · NLRTM'}</option></select>`:l==='ETD'||l==='ETA'||l==='截关时间'?`<input class="f-input num" type="date" value="${l==='ETD'?'2026-08-12':l==='ETA'?'2026-09-15':'2026-08-10'}">`:`<input class="f-input${'num'.includes(l)?' num':''}" placeholder="${p||''}" value="${l==='销售/操作'?'林岚':l==='提单份数'?'3':''}">`}</div>`;
|
||||
}
|
||||
|
||||
function renderAll(){renderNav();renderTabs();renderContent()}
|
||||
renderAll();
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,596 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-density="compact">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FMS 界面原型 · 工具感精修版</title>
|
||||
<style>
|
||||
/* ============ 设计令牌:纸感浅底 + 墨色前景 + 单一海蓝重点色 ============ */
|
||||
:root {
|
||||
--background: oklch(0.981 0.003 250);
|
||||
--foreground: oklch(0.235 0.012 255);
|
||||
--card: oklch(1 0 0);
|
||||
--primary: oklch(0.43 0.085 245);
|
||||
--primary-foreground: oklch(0.99 0.002 250);
|
||||
--secondary: oklch(0.965 0.004 250);
|
||||
--secondary-foreground: oklch(0.3 0.012 255);
|
||||
--muted: oklch(0.966 0.004 250);
|
||||
--muted-foreground: oklch(0.535 0.012 255);
|
||||
--accent: oklch(0.948 0.014 245);
|
||||
--accent-foreground: oklch(0.36 0.075 245);
|
||||
--destructive: oklch(0.545 0.16 25);
|
||||
--warning: oklch(0.66 0.12 68);
|
||||
--sidebar-bg: oklch(0.99 0 0);
|
||||
--sidebar-text: oklch(0.535 0.012 255);
|
||||
--sidebar-active-bg: oklch(0.948 0.014 245);
|
||||
--sidebar-active-text: oklch(0.36 0.075 245);
|
||||
--sidebar-group: oklch(0.6 0.01 255);
|
||||
--border: oklch(0.906 0.005 250);
|
||||
--border-strong: oklch(0.845 0.008 250);
|
||||
--input: oklch(0.885 0.006 250);
|
||||
--ring: oklch(0.43 0.085 245);
|
||||
|
||||
/* 密度令牌(默认紧凑) */
|
||||
--row-h: 1.75rem; /* 28px */
|
||||
--cell-px: 0.5rem; /* 8px */
|
||||
--block-gap: 0.625rem; /* 10px */
|
||||
--radius: 0.3rem;
|
||||
}
|
||||
[data-density="default"] { --row-h: 2.25rem; --cell-px: 0.75rem; --block-gap: 1rem; }
|
||||
[data-density="comfortable"] { --row-h: 2.625rem; --cell-px: 1rem; --block-gap: 1.25rem; }
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
font-family: "Inter", -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
font-size: 12px; color: var(--foreground); background: var(--background);
|
||||
font-feature-settings: "cv05", "ss01", "tnum"; -webkit-font-smoothing: antialiased;
|
||||
}
|
||||
button { font: inherit; cursor: pointer; background: none; border: none; color: inherit; }
|
||||
input, select { font: inherit; outline: none; border: none; background: none; color: inherit; }
|
||||
.num { font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
|
||||
kbd { font-family: inherit; font-size: 10.5px; border: 1px solid var(--border-strong); border-radius: 3px; padding: 0 4px; color: var(--muted-foreground); background: var(--card); }
|
||||
::selection { background: var(--accent); }
|
||||
svg { display: block; }
|
||||
|
||||
.app { display: flex; height: 100vh; overflow: hidden; }
|
||||
|
||||
/* ============ 侧边栏 ============ */
|
||||
.sidebar {
|
||||
width: 208px; flex-shrink: 0; background: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border); display: flex; flex-direction: column;
|
||||
transition: width .15s ease;
|
||||
}
|
||||
.sidebar.collapsed { width: 48px; }
|
||||
.sidebar-logo {
|
||||
display: flex; align-items: center; gap: 8px; height: 44px;
|
||||
padding: 0 12px; border-bottom: 1px solid var(--border);
|
||||
font-weight: 600; font-size: 13px; color: var(--foreground); white-space: nowrap;
|
||||
}
|
||||
.logo-mark {
|
||||
width: 20px; height: 20px; border-radius: 4px; background: var(--primary);
|
||||
color: var(--primary-foreground); display: flex; align-items: center; justify-content: center;
|
||||
font-size: 11px; font-weight: 600; flex-shrink: 0;
|
||||
}
|
||||
.sidebar-nav { flex: 1; overflow-y: auto; padding: 6px 8px; }
|
||||
.nav-group {
|
||||
font-size: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: .1em;
|
||||
color: var(--sidebar-group); padding: 10px 8px 4px; white-space: nowrap;
|
||||
}
|
||||
.sidebar.collapsed .nav-group { height: 1px; padding: 8px 0 0; margin: 4px 10px 0; border-top: 1px solid var(--border); overflow: hidden; color: transparent; }
|
||||
.nav-item {
|
||||
display: flex; align-items: center; gap: 8px; width: 100%; height: 28px;
|
||||
padding: 0 8px; border-radius: 4px; color: var(--sidebar-text);
|
||||
font-size: 12.5px; white-space: nowrap; transition: background .1s;
|
||||
}
|
||||
.nav-item:hover { background: var(--sidebar-active-bg); color: var(--sidebar-active-text); }
|
||||
.nav-item.active { background: var(--sidebar-active-bg); color: var(--sidebar-active-text); font-weight: 500; }
|
||||
.nav-item svg { width: 14px; height: 14px; flex-shrink: 0; opacity: .8; }
|
||||
.nav-item .badge { margin-left: auto; font-size: 10px; color: var(--sidebar-group); font-variant-numeric: tabular-nums; }
|
||||
.sidebar.collapsed .nav-item .label, .sidebar.collapsed .sidebar-logo .label { display: none; }
|
||||
.sidebar.collapsed .nav-item { justify-content: center; padding: 0; }
|
||||
.sidebar-foot { padding: 8px 12px; border-top: 1px solid var(--border); font-size: 11px; color: var(--muted-foreground); display: flex; justify-content: space-between; }
|
||||
|
||||
/* ============ 主区 ============ */
|
||||
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.topbar {
|
||||
height: 44px; flex-shrink: 0; background: var(--card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; gap: 10px; padding: 0 12px;
|
||||
}
|
||||
.breadcrumb { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted-foreground); white-space: nowrap; }
|
||||
.breadcrumb .sep { color: var(--border-strong); }
|
||||
.breadcrumb b { color: var(--foreground); font-weight: 600; }
|
||||
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
||||
.icon-btn { width: 28px; height: 28px; border-radius: 5px; display: flex; align-items: center; justify-content: center; color: var(--muted-foreground); }
|
||||
.icon-btn:hover { background: var(--muted); color: var(--foreground); }
|
||||
.avatar { width: 26px; height: 26px; border-radius: 50%; background: var(--accent); color: var(--accent-foreground); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 500; }
|
||||
|
||||
/* ============ 页签带 ============ */
|
||||
.tabstrip { height: 34px; flex-shrink: 0; background: color-mix(in oklch, var(--muted) 60%, transparent); border-bottom: 1px solid var(--border-strong); display: flex; align-items: stretch; }
|
||||
.tab {
|
||||
display: flex; align-items: center; gap: 6px; padding: 0 8px 0 10px;
|
||||
font-size: 12px; color: var(--muted-foreground); border-right: 1px solid var(--border);
|
||||
white-space: nowrap; max-width: 200px; position: relative;
|
||||
}
|
||||
.tab:hover { background: color-mix(in oklch, var(--background) 60%, transparent); color: var(--foreground); }
|
||||
.tab.active { background: var(--background); color: var(--foreground); font-weight: 500; }
|
||||
.tab.active::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--primary); }
|
||||
.tab svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--muted-foreground); }
|
||||
.tab .label { overflow: hidden; text-overflow: ellipsis; }
|
||||
.tab .close { width: 16px; height: 16px; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 12px; opacity: 0; }
|
||||
.tab:hover .close { opacity: 1; }
|
||||
.tab .close:hover { background: var(--muted); color: var(--foreground); }
|
||||
.tabstrip .count { margin-left: auto; display: flex; align-items: center; gap: 4px; padding: 0 12px; font-size: 11px; color: var(--muted-foreground); }
|
||||
|
||||
/* ============ 内容区 ============ */
|
||||
.content { flex: 1; display: flex; min-width: 0; overflow: hidden; }
|
||||
.list-col { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
|
||||
/* 指标条 */
|
||||
|
||||
/* 工具条(搜索聚合进操作组,无独立顶部搜索框) */
|
||||
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--border); background: var(--card); }
|
||||
.search-group { display: flex; align-items: center; border: 1px solid var(--input); border-radius: 4px; background: var(--background); height: 28px; }
|
||||
.search-group:focus-within { border-color: var(--ring); box-shadow: 0 0 0 2px color-mix(in oklch, var(--ring) 20%, transparent); }
|
||||
.search-group svg { width: 14px; height: 14px; margin-left: 8px; color: var(--muted-foreground); flex-shrink: 0; }
|
||||
.search-group input { width: 240px; padding: 0 8px; font-size: 12px; background: transparent; }
|
||||
.search-group input::placeholder { color: var(--muted-foreground); }
|
||||
.search-adv { display: flex; align-items: center; gap: 5px; height: 28px; padding: 0 8px; border-left: 1px solid var(--border); font-size: 12px; color: var(--muted-foreground); }
|
||||
.search-adv:hover { color: var(--foreground); }
|
||||
.search-adv.on { color: var(--primary); }
|
||||
.search-adv .count { display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 999px; background: var(--primary); color: var(--primary-foreground); font-size: 10px; font-weight: 500; }
|
||||
|
||||
.search-group { position: relative; }
|
||||
.adv-panel { position: absolute; top: calc(100% + 6px); left: 0; z-index: 40; width: 380px; padding: 12px; background: var(--card); border: 1px solid var(--border-strong); border-radius: 8px; box-shadow: 0 8px 24px -8px rgba(0,0,0,.18); }
|
||||
.adv-panel[hidden] { display: none; }
|
||||
.adv-title { font-size: 12px; font-weight: 600; color: var(--foreground); margin-bottom: 10px; }
|
||||
.adv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px; }
|
||||
.adv-block { display: flex; flex-direction: column; gap: 5px; }
|
||||
.adv-span2 { grid-column: 1 / -1; }
|
||||
.adv-label { font-size: 11px; color: var(--muted-foreground); }
|
||||
.adv-chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.achip { height: 26px; padding: 0 9px; border-radius: 4px; font-size: 12px; color: var(--muted-foreground); border: 1px solid var(--border); background: var(--background); transition: all .1s; }
|
||||
.achip:hover { background: var(--muted); color: var(--foreground); }
|
||||
.achip.on { background: var(--accent); border-color: var(--accent); color: var(--accent-foreground); font-weight: 500; }
|
||||
.adv-field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: var(--muted-foreground); }
|
||||
.adv-field input { height: 28px; padding: 0 8px; border-radius: 4px; border: 1px solid var(--input); background: var(--background); color: var(--foreground); font-size: 12px; font-family: inherit; }
|
||||
.adv-field input:focus { outline: none; border-color: var(--ring); box-shadow: 0 0 0 2px color-mix(in oklch, var(--ring) 22%, transparent); }
|
||||
.adv-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
||||
|
||||
.toolbar .spacer { flex: 1; }
|
||||
.btn { display: inline-flex; align-items: center; gap: 5px; height: 28px; padding: 0 10px; border-radius: 4px; border: 1px solid var(--border); background: var(--card); color: color-mix(in oklch, var(--foreground) 80%, transparent); font-size: 12px; transition: all .1s; }
|
||||
.btn:hover { background: var(--muted); }
|
||||
.btn svg { width: 14px; height: 14px; }
|
||||
.btn-primary { background: var(--primary); border-color: var(--primary); color: var(--primary-foreground); font-weight: 500; }
|
||||
.btn-primary:hover { filter: brightness(1.06); color: var(--primary-foreground); }
|
||||
|
||||
/* 批量操作条 */
|
||||
.bulkbar { display: none; align-items: center; gap: 12px; padding: 6px 12px; background: var(--accent); border-bottom: 1px solid var(--border); font-size: 12px; color: var(--accent-foreground); }
|
||||
.bulkbar.show { display: flex; }
|
||||
.bulkbar .acts { display: flex; gap: 6px; }
|
||||
.bulkbar .acts button { height: 24px; padding: 0 8px; border-radius: 3px; border: 1px solid var(--border); background: var(--card); font-size: 12px; }
|
||||
.bulkbar .acts button:hover { background: var(--muted); }
|
||||
|
||||
/* 表格 */
|
||||
.table-wrap { flex: 1; overflow: auto; background: var(--card); }
|
||||
table { width: 100%; border-collapse: collapse; white-space: nowrap; font-size: 12px; }
|
||||
thead th {
|
||||
text-align: left; font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .02em;
|
||||
color: var(--muted-foreground); padding: 6px var(--cell-px); background: color-mix(in oklch, var(--muted) 60%, transparent);
|
||||
border-bottom: 1px solid var(--border-strong); position: sticky; top: 0; z-index: 1;
|
||||
}
|
||||
thead th.right { text-align: right; }
|
||||
thead th.sortable { cursor: pointer; }
|
||||
thead th.sortable:hover { color: var(--foreground); }
|
||||
thead th .arrow { display: inline-flex; vertical-align: middle; margin-left: 2px; }
|
||||
thead th .arrow svg { width: 12px; height: 12px; }
|
||||
tbody td { height: var(--row-h); padding: 0 var(--cell-px); border-bottom: 1px solid color-mix(in oklch, var(--border) 70%, transparent); color: var(--foreground); font-size: 12px; }
|
||||
tbody td.right { text-align: right; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
|
||||
tbody td.num { font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
|
||||
tbody td.id { color: var(--primary); font-weight: 500; }
|
||||
tbody td.muted { color: var(--muted-foreground); }
|
||||
tbody tr { cursor: default; transition: background .08s; }
|
||||
tbody tr:hover { background: color-mix(in oklch, var(--muted) 70%, transparent); }
|
||||
tbody tr.active { background: color-mix(in oklch, var(--accent) 70%, transparent); }
|
||||
tbody tr input[type=checkbox] { width: 13px; height: 13px; accent-color: var(--primary); cursor: pointer; }
|
||||
thead input[type=checkbox] { width: 13px; height: 13px; accent-color: var(--primary); cursor: pointer; }
|
||||
|
||||
.status { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
|
||||
.status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
|
||||
.st-draft { color: var(--muted-foreground); }
|
||||
.st-booked { color: color-mix(in oklch, var(--foreground) 65%, transparent); }
|
||||
.st-loaded { color: color-mix(in oklch, var(--primary) 70%, transparent); }
|
||||
.st-sailing { color: var(--primary); }
|
||||
.st-arrived { color: color-mix(in oklch, var(--foreground) 50%, transparent); }
|
||||
.st-released { color: color-mix(in oklch, var(--foreground) 30%, transparent); }
|
||||
.st-hold { color: var(--warning); }
|
||||
|
||||
/* 汇总条 */
|
||||
.summary { display: flex; flex-wrap: wrap; align-items: center; gap: 20px; padding: 6px 12px; border-top: 1px solid var(--border); background: var(--card); font-size: 11px; color: var(--muted-foreground); }
|
||||
.summary b { color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; }
|
||||
.summary .sync { margin-left: auto; }
|
||||
|
||||
/* ============ 详情侧栏(抽屉,非 modal) ============ */
|
||||
.detail {
|
||||
width: 320px; flex-shrink: 0; border-left: 1px solid var(--border); background: var(--card);
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
.detail.show { display: flex; }
|
||||
.detail-head { display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 12px; border-bottom: 1px solid var(--border); }
|
||||
.detail-head .id { font-size: 12px; font-weight: 500; font-variant-numeric: tabular-nums; }
|
||||
.detail-head .close { margin-left: auto; width: 22px; height: 22px; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: var(--muted-foreground); }
|
||||
.detail-head .close:hover { background: var(--muted); color: var(--foreground); }
|
||||
.detail-body { flex: 1; overflow-y: auto; }
|
||||
.detail-body dl { display: flex; flex-direction: column; }
|
||||
.detail-body .row { display: flex; gap: 12px; padding: 6px 12px; font-size: 12px; border-bottom: 1px solid color-mix(in oklch, var(--border) 70%, transparent); }
|
||||
.detail-body dt { width: 80px; flex-shrink: 0; color: var(--muted-foreground); }
|
||||
.detail-body dd { flex: 1; min-width: 0; word-break: break-word; }
|
||||
.detail-foot { display: flex; flex-wrap: wrap; gap: 6px; padding: 12px; border-top: 1px solid var(--border); }
|
||||
|
||||
/* ============ 命令面板(Ctrl K) ============ */
|
||||
.palette-mask { position: fixed; inset: 0; background: rgba(15,23,42,.35); display: none; align-items: flex-start; justify-content: center; padding-top: 12vh; z-index: 50; }
|
||||
.palette-mask.open { display: flex; }
|
||||
.palette { width: 540px; background: var(--card); border-radius: 10px; box-shadow: 0 20px 50px rgba(0,0,0,.2); overflow: hidden; border: 1px solid var(--border); }
|
||||
.palette-input { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid var(--border); }
|
||||
.palette-input svg { width: 16px; height: 16px; color: var(--muted-foreground); }
|
||||
.palette-input input { flex: 1; font-size: 13px; background: transparent; }
|
||||
.palette-list { max-height: 300px; overflow-y: auto; padding: 6px; }
|
||||
.p-group { font-size: 10.5px; color: var(--muted-foreground); padding: 8px 10px 3px; text-transform: uppercase; letter-spacing: .03em; }
|
||||
.p-item { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: 6px; font-size: 12.5px; color: var(--foreground); }
|
||||
.p-item svg { width: 15px; height: 15px; color: var(--muted-foreground); }
|
||||
.p-item .right { margin-left: auto; color: var(--muted-foreground); font-size: 11px; display: flex; gap: 4px; }
|
||||
.p-item:hover, .p-item.sel { background: var(--accent); color: var(--accent-foreground); }
|
||||
.p-item:hover svg, .p-item.sel svg { color: var(--accent-foreground); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
<div class="logo-mark">F</div>
|
||||
<span class="label">FMS 管理台</span>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-group">作业</div>
|
||||
<button class="nav-item active"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg><span class="label">订舱单</span><span class="badge">12</span></button>
|
||||
<button class="nav-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg><span class="label">新建订舱</span></button>
|
||||
<button class="nav-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg><span class="label">集装箱池</span><span class="badge">38</span></button>
|
||||
<button class="nav-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M3 17h18M5 17V9l7-5 7 5v8"/></svg><span class="label">船期表</span></button>
|
||||
<div class="nav-group">主数据</div>
|
||||
<button class="nav-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="9" cy="8" r="3"/><path d="M3 20a6 6 0 0 1 12 0M16 11a3 3 0 0 0 0-6M21 20a6 6 0 0 0-4-5.6"/></svg><span class="label">客户与委托方</span></button>
|
||||
<button class="nav-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg><span class="label">港口 / 航线</span></button>
|
||||
<button class="nav-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M3 21h18M5 21V9l7-5 7 5v12M9 21v-6h6v6"/></svg><span class="label">费率与结算</span></button>
|
||||
</nav>
|
||||
<div class="sidebar-foot"><span>林岚 · 操作员</span><span class="num">v2.4</span></div>
|
||||
</aside>
|
||||
|
||||
<!-- 主区 -->
|
||||
<div class="main">
|
||||
<header class="topbar">
|
||||
<nav class="breadcrumb"><span>作业</span><span class="sep">/</span><b>订舱单</b></nav>
|
||||
<div class="topbar-right">
|
||||
<button class="icon-btn" id="collapseBtn" title="收起侧栏"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
|
||||
<button class="icon-btn" id="paletteBtn" title="命令面板 (Ctrl K)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M7 9v6M11 9v6M7 12h4M15 9v6h2"/></svg></button>
|
||||
<div class="avatar">林</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tabstrip">
|
||||
<button class="tab active"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg><span class="label">订舱单列表</span><span class="close">×</span></button>
|
||||
<button class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg><span class="label">新建订舱 SO-24082</span><span class="close">×</span></button>
|
||||
<span class="count"><span class="num">2</span> 个标签页</span>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- 列表列 -->
|
||||
<div class="list-col">
|
||||
<!-- 工具条:搜索聚进操作组,无独立顶部搜索框 -->
|
||||
<div class="toolbar">
|
||||
<div class="search-group">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input id="searchInput" placeholder="搜索单号 / 提单号 / 客户 / 船名" aria-label="搜索订舱单">
|
||||
<button class="search-adv" id="advBtn"><span>高级</span><span class="count" id="advCount" style="display:none">0</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" style="width:14px;height:14px"><path d="m6 9 6 6 6-6"/></svg></button>
|
||||
<div class="adv-panel" id="advPanel" hidden>
|
||||
<div class="adv-title">高级搜索</div>
|
||||
<div class="adv-grid">
|
||||
<div class="adv-block adv-span2">
|
||||
<span class="adv-label">状态</span>
|
||||
<div class="adv-chips" id="advStatus">
|
||||
<button class="achip on" data-s="all">全部</button>
|
||||
<button class="achip" data-s="draft">草稿</button>
|
||||
<button class="achip" data-s="booked">已订舱</button>
|
||||
<button class="achip" data-s="loaded">已装载</button>
|
||||
<button class="achip" data-s="sailing">在途</button>
|
||||
<button class="achip" data-s="hold">滞留</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="adv-field">委托客户<input id="advCustomer" placeholder="客户名称"></label>
|
||||
<label class="adv-field">船名<input id="advVessel" placeholder="船名关键字"></label>
|
||||
<label class="adv-field">起运港<input id="advPol" placeholder="如 SZX"></label>
|
||||
<label class="adv-field">目的港<input id="advPod" placeholder="如 HAM"></label>
|
||||
<label class="adv-field">ETD 起<input type="date" id="advEtdFrom"></label>
|
||||
<label class="adv-field">ETD 止<input type="date" id="advEtdTo"></label>
|
||||
<label class="adv-field">TEU ≥<input type="number" id="advTeuMin" min="0" placeholder="0"></label>
|
||||
<label class="adv-field">TEU ≤<input type="number" id="advTeuMax" min="0" placeholder="∞"></label>
|
||||
</div>
|
||||
<div class="adv-actions">
|
||||
<button class="btn" id="advReset">重置</button>
|
||||
<button class="btn btn-primary" id="advApply">应用</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 6h16M7 12h10M10 18h4"/></svg>列设置</button>
|
||||
<button class="btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M12 3v12M7 10l5 5 5-5M5 21h14"/></svg>导出</button>
|
||||
<button class="btn btn-primary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M12 5v14M5 12h14"/></svg>新建订舱</button>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作条 -->
|
||||
<div class="bulkbar" id="bulkbar">
|
||||
<span class="num" id="bulkCount">已选 0 条</span>
|
||||
<div class="acts">
|
||||
<button>确认订舱</button><button>打印提单</button><button>推送 EDI</button><button>归档</button>
|
||||
</div>
|
||||
<button id="bulkClear" style="margin-left:auto;color:var(--muted-foreground)">取消选择</button>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:28px"><input type="checkbox" id="checkAll"></th>
|
||||
<th class="sortable num" data-sort="id">订舱单号<span class="arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg></span></th>
|
||||
<th>状态</th>
|
||||
<th class="num">提单号</th>
|
||||
<th>委托客户</th>
|
||||
<th>起运 → 目的</th>
|
||||
<th>船名 / 航次</th>
|
||||
<th class="sortable num" data-sort="etd">ETD</th>
|
||||
<th class="sortable num" data-sort="eta">ETA</th>
|
||||
<th class="num">箱型箱量</th>
|
||||
<th class="sortable right" data-sort="teu">TEU</th>
|
||||
<th class="sortable right" data-sort="freight">运费</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<span>共 <b id="sumCount">0</b> 条</span>
|
||||
<span>合计 TEU <b id="sumTeu">0</b></span>
|
||||
<span>合计毛重 <b id="sumWeight">0.0</b> 吨</span>
|
||||
<span class="sync">数据同步于 08-05 09:20</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情侧栏 -->
|
||||
<aside class="detail" id="detail">
|
||||
<div class="detail-head">
|
||||
<span class="id" id="dId">—</span>
|
||||
<span class="status st-sailing" id="dStatus">在途</span>
|
||||
<button class="close" id="dClose">×</button>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<dl id="dBody"></dl>
|
||||
</div>
|
||||
<div class="detail-foot">
|
||||
<button class="btn btn-primary">编辑</button>
|
||||
<button class="btn">确认订舱</button>
|
||||
<button class="btn">打印提单</button>
|
||||
<button class="btn">费用录入</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 命令面板 -->
|
||||
<div class="palette-mask" id="palette">
|
||||
<div class="palette">
|
||||
<div class="palette-input">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input id="paletteInput" placeholder="跳转到页面、搜索记录或执行操作…" autocomplete="off">
|
||||
</div>
|
||||
<div class="palette-list" id="paletteList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ============ 数据 ============
|
||||
const PORTS = [['SHA','上海'],['NGB','宁波'],['YTN','盐田'],['SZX','蛇口'],['LAX','洛杉矶'],['NYC','纽约'],['RTM','鹿特丹'],['HAM','汉堡']];
|
||||
const STATUS = { draft:['草稿','st-draft'], booked:['已订舱','st-booked'], loaded:['已装载','st-loaded'], sailing:['在途','st-sailing'], arrived:['已到港','st-arrived'], released:['已放单','st-released'], hold:['滞留','st-hold'] };
|
||||
const raw = [
|
||||
['SO-24071','MAEU-7731','远东国际货运','SHA','LAX','EVER GIVEN','2408E','08-06','08-20','1×40HC','2','18400','USD'],
|
||||
['SO-24072','COSU-8821','中欧物流','NGB','NYC','COSCO HOPE','073W','08-07','09-11','2×20GP','2','9600','USD'],
|
||||
['SO-24073','MSCU-1190','环宇供应链','YTN','RTM','MSC TINA','FG24','08-08','09-05','1×40HC','2','21100','EUR'],
|
||||
['SO-24074','HLCU-5562','远洋通运','SZX','HAM','HMM ALGECIRAS','024E','08-09','09-08','3×40HC','3','27600','USD'],
|
||||
['SO-24075','ONEU-3344','华东外贸','SHA','LAX','ONE STORK','061E','08-10','08-24','1×20GP','1','5200','USD'],
|
||||
['SO-24076','CMA-C2009','法翔国际','NGB','NYC','CMA CGM JACQUES','238W','08-11','09-15','2×40HC','4','33800','USD'],
|
||||
['SO-24077','MAEU-9003','远东国际货运','YTN','RTM','MAERSK HANGZHOU','316W','08-12','09-09','1×40HC','2','19800','USD'],
|
||||
['SO-24078','EMCU-7712','中欧物流','SZX','HAM','EVER ACE','119E','08-13','09-12','2×20GP','2','10300','EUR'],
|
||||
['SO-24079','HLCU-4410','环宇供应链','SHA','LAX','HMM OSLO','058E','08-14','08-28','1×40HC','2','17500','USD'],
|
||||
['SO-24080','MSCU-6655','远洋通运','NGB','NYC','MSC RAYA','091W','08-15','09-19','3×40HC','3','29500','USD'],
|
||||
['SO-24081','ONEU-2288','华东外贸','YTN','RTM','ONE MAGNOLIA','204W','08-16','09-13','1×20GP','1','6100','USD'],
|
||||
['SO-24082','CMA-C1180','法翔国际','SZX','HAM','CMA CGM TROCADERO','311E','08-17','09-16','2×40HC','4','32100','USD'],
|
||||
];
|
||||
const STATUS_SEQ = ['booked','sailing','loaded','sailing','draft','booked','sailing','hold','sailing','booked','draft','sailing'];
|
||||
const SHIPMENTS = raw.map((r,i) => ({ id:r[0], bl:r[1], customer:r[2], pol:r[3], pod:r[4], vessel:r[5], voyage:r[6], etd:r[7], eta:r[8], containers:r[9], teu:r[10], freight:r[11], currency:r[12], status:STATUS_SEQ[i], owner:'林岚', weight:r[10]*11.5, updated:'08-05 09:12' }));
|
||||
</script>
|
||||
<script>
|
||||
// ============ 状态 ============
|
||||
let state = { q:'', adv:{ status:'all', customer:'', vessel:'', pol:'', pod:'', etdFrom:'', etdTo:'', teuMin:'', teuMax:'' }, sort:{key:'id', dir:'desc'}, selected:new Set(), active:null };
|
||||
|
||||
const $ = s => document.querySelector(s);
|
||||
const tbody = $('#tbody');
|
||||
|
||||
function md(v){ const [m,d]=v.split('-'); return +(m+d); }
|
||||
function filtered() {
|
||||
const q = state.q.trim().toLowerCase();
|
||||
const a = state.adv;
|
||||
const qFields = [a.customer, a.vessel, a.pol, a.pod].map(x=>x.trim().toLowerCase()).filter(Boolean);
|
||||
let list = SHIPMENTS.filter(s => {
|
||||
const okS = a.status === 'all' || s.status === a.status;
|
||||
const okQ = !q || [s.id, s.bl, s.customer, s.vessel, s.pol, s.pod].join(' ').toLowerCase().includes(q);
|
||||
const okF = qFields.every(t =>
|
||||
s.customer.toLowerCase().includes(t) || s.vessel.toLowerCase().includes(t) ||
|
||||
s.pol.toLowerCase().includes(t) || s.pod.toLowerCase().includes(t));
|
||||
const okEtd = (!a.etdFrom || md(s.etd) >= md(a.etdFrom.slice(5))) && (!a.etdTo || md(s.etd) <= md(a.etdTo.slice(5)));
|
||||
const okTeu = (a.teuMin === '' || s.teu >= +a.teuMin) && (a.teuMax === '' || s.teu <= +a.teuMax);
|
||||
return okS && okQ && okF && okEtd && okTeu;
|
||||
});
|
||||
const { key, dir } = state.sort;
|
||||
return list.sort((a,b) => {
|
||||
const av = a[key], bv = b[key];
|
||||
const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
|
||||
return dir === 'asc' ? r : -r;
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
const rows = filtered();
|
||||
tbody.innerHTML = rows.map(r => {
|
||||
const st = STATUS[r.status];
|
||||
const checked = state.selected.has(r.id);
|
||||
const act = state.active === r.id ? 'active' : '';
|
||||
return `<tr class="${act}" data-id="${r.id}">
|
||||
<td><input type="checkbox" ${checked?'checked':''} data-check="${r.id}"></td>
|
||||
<td class="id num">${r.id}</td>
|
||||
<td><span class="status ${st[1]}">${st[0]}</span></td>
|
||||
<td class="num muted">${r.bl}</td>
|
||||
<td>${r.customer}</td>
|
||||
<td><span class="num muted" style="font-size:11px">${r.pol}</span> ${PORTS.find(p=>p[0]===r.pol)?.[1]} <span style="color:var(--border-strong)">→</span> <span class="num muted" style="font-size:11px">${r.pod}</span> ${PORTS.find(p=>p[0]===r.pod)?.[1]}</td>
|
||||
<td>${r.vessel} <span class="num muted">${r.voyage}</span></td>
|
||||
<td class="num">${r.etd}</td>
|
||||
<td class="num">${r.eta}</td>
|
||||
<td class="num">${r.containers}</td>
|
||||
<td class="right">${r.teu}</td>
|
||||
<td class="right muted">${r.freight===0?'—':(r.currency==='USD'?'$':'€')+r.freight.toLocaleString()}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
const totalTeu = rows.reduce((n,r)=>n+r.teu,0);
|
||||
const totalW = rows.reduce((n,r)=>n+r.weight,0);
|
||||
$('#sumCount').textContent = rows.length;
|
||||
$('#sumTeu').textContent = totalTeu;
|
||||
$('#sumWeight').textContent = totalW.toFixed(1);
|
||||
|
||||
const bar = $('#bulkbar');
|
||||
if (state.selected.size > 0) { bar.classList.add('show'); $('#bulkCount').textContent = `已选 ${state.selected.size} 条`; }
|
||||
else bar.classList.remove('show');
|
||||
|
||||
$('#checkAll').checked = rows.length > 0 && state.selected.size === rows.length;
|
||||
}
|
||||
|
||||
// 行点击 → 详情;checkbox 不触发
|
||||
tbody.addEventListener('click', e => {
|
||||
const cb = e.target.closest('input[type=checkbox]');
|
||||
if (cb) {
|
||||
const id = cb.dataset.check;
|
||||
if (cb.checked) state.selected.add(id); else state.selected.delete(id);
|
||||
render(); return;
|
||||
}
|
||||
const tr = e.target.closest('tr');
|
||||
if (tr) { state.active = tr.dataset.id; openDetail(tr.dataset.id); render(); }
|
||||
});
|
||||
|
||||
$('#checkAll').addEventListener('change', e => {
|
||||
const rows = filtered();
|
||||
if (e.target.checked) rows.forEach(r => state.selected.add(r.id));
|
||||
else rows.forEach(r => state.selected.delete(r.id));
|
||||
render();
|
||||
});
|
||||
$('#bulkClear').addEventListener('click', () => { state.selected.clear(); render(); });
|
||||
|
||||
// 搜索
|
||||
$('#searchInput').addEventListener('input', e => { state.q = e.target.value; render(); });
|
||||
// 高级搜索
|
||||
const advPanel = $('#advPanel');
|
||||
function advActiveCount() {
|
||||
const a = state.adv;
|
||||
let n = (a.status !== 'all' ? 1 : 0) + [a.customer, a.vessel, a.pol, a.pod, a.etdFrom, a.etdTo, a.teuMin, a.teuMax].filter(v => v !== '' && v != null).length;
|
||||
return n;
|
||||
}
|
||||
function syncAdvCount() {
|
||||
const n = advActiveCount();
|
||||
const badge = $('#advCount');
|
||||
badge.textContent = n; badge.style.display = n ? 'flex' : 'none';
|
||||
}
|
||||
$('#advBtn').addEventListener('click', e => { e.stopPropagation(); advPanel.hidden = !advPanel.hidden; });
|
||||
advPanel.addEventListener('click', e => e.stopPropagation());
|
||||
document.addEventListener('click', () => { advPanel.hidden = true; });
|
||||
// 状态多选(单选切换,点“全部”重置)
|
||||
$('#advStatus').addEventListener('click', e => {
|
||||
const b = e.target.closest('.achip'); if (!b) return;
|
||||
document.querySelectorAll('#advStatus .achip').forEach(c => c.classList.toggle('on', c === b));
|
||||
state.adv.status = b.dataset.s;
|
||||
});
|
||||
// 应用
|
||||
$('#advApply').addEventListener('click', () => {
|
||||
const read = id => document.getElementById(id).value;
|
||||
state.adv = { status: state.adv.status, customer: read('advCustomer'), vessel: read('advVessel'), pol: read('advPol'), pod: read('advPod'), etdFrom: read('advEtdFrom'), etdTo: read('advEtdTo'), teuMin: read('advTeuMin'), teuMax: read('advTeuMax') };
|
||||
syncAdvCount(); advPanel.hidden = true; render();
|
||||
});
|
||||
// 重置
|
||||
$('#advReset').addEventListener('click', () => {
|
||||
['advCustomer','advVessel','advPol','advPod','advEtdFrom','advEtdTo','advTeuMin','advTeuMax'].forEach(id => document.getElementById(id).value = '');
|
||||
document.querySelectorAll('#advStatus .achip').forEach(c => c.classList.toggle('on', c.dataset.s === 'all'));
|
||||
state.adv = { status:'all', customer:'', vessel:'', pol:'', pod:'', etdFrom:'', etdTo:'', teuMin:'', teuMax:'' };
|
||||
syncAdvCount(); render();
|
||||
});
|
||||
// 排序
|
||||
document.querySelectorAll('th.sortable').forEach(th => th.addEventListener('click', () => {
|
||||
const k = th.dataset.sort;
|
||||
state.sort = state.sort.key === k ? { key:k, dir: state.sort.dir==='asc'?'desc':'asc' } : { key:k, dir:'asc' };
|
||||
document.querySelectorAll('th.sortable').forEach(t => t.querySelector('.arrow').innerHTML = '');
|
||||
th.querySelector('.arrow').innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${state.sort.dir==='asc'?'<path d="m6 15 6-6 6 6"/>':'<path d="m6 9 6 6 6-6"/>'}</svg>`;
|
||||
render();
|
||||
}));
|
||||
|
||||
// 详情抽屉
|
||||
function openDetail(id) {
|
||||
const r = SHIPMENTS.find(x => x.id === id);
|
||||
$('#dId').textContent = r.id;
|
||||
const st = STATUS[r.status];
|
||||
$('#dStatus').className = 'status ' + st[1];
|
||||
$('#dStatus').textContent = st[0];
|
||||
const fields = [['订舱单号',r.id],['提单号',r.bl],['委托客户',r.customer],['装货港',`${PORTS.find(p=>p[0]===r.pol)?.[1]} / ${r.pol}`],['卸货港',`${PORTS.find(p=>p[0]===r.pod)?.[1]} / ${r.pod}`],['船名航次',`${r.vessel} ${r.voyage}`],['ETD / ETA',`${r.etd} → ${r.eta}`],['箱型箱量',r.containers],['毛重',`${r.weight.toFixed(1)} 吨`],['运费',r.freight===0?'未报价':`${r.currency} ${r.freight.toLocaleString()}`],['操作员',r.owner],['最近更新',r.updated]];
|
||||
$('#dBody').innerHTML = fields.map(([k,v]) => `<div class="row"><dt>${k}</dt><dd>${v}</dd></div>`).join('');
|
||||
$('#detail').classList.add('show');
|
||||
}
|
||||
$('#dClose').addEventListener('click', () => { state.active = null; $('#detail').classList.remove('show'); render(); });
|
||||
|
||||
// 侧栏收起
|
||||
$('#collapseBtn').addEventListener('click', () => $('#sidebar').classList.toggle('collapsed'));
|
||||
|
||||
// 命令面板
|
||||
const palette = $('#palette');
|
||||
const CMDS = [
|
||||
['goto','订舱单列表','作业','/'],['goto','新建订舱','作业','+'],['goto','集装箱池','作业','#'],['goto','船期表','作业','#'],
|
||||
['act','确认选中订舱','批量操作','↵'],['act','导出当前列表','列表','E'],['act','列设置','列表','C'],['act','全局搜索','导航','/'],
|
||||
];
|
||||
function renderPalette(q='') {
|
||||
const list = $('#paletteList');
|
||||
const f = CMDS.filter(c => c[1].toLowerCase().includes(q.toLowerCase()));
|
||||
let html = '', cur = '';
|
||||
f.forEach((c,i) => {
|
||||
if (c[2] !== cur) { cur = c[2]; html += `<div class="p-group">${cur}</div>`; }
|
||||
html += `<div class="p-item ${i===0?'sel':''}" data-i="${i}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="9"/><path d="M12 8v8M8 12h8"/></svg><span>${c[1]}</span><span class="right"><kbd>${c[3]}</kbd></span></div>`;
|
||||
});
|
||||
list.innerHTML = html;
|
||||
}
|
||||
$('#paletteBtn').addEventListener('click', () => { palette.classList.add('open'); $('#paletteInput').value=''; renderPalette(); setTimeout(()=>$('#paletteInput').focus(),0); });
|
||||
palette.addEventListener('click', e => { if (e.target === palette) palette.classList.remove('open'); });
|
||||
$('#paletteInput').addEventListener('input', e => renderPalette(e.target.value));
|
||||
document.addEventListener('keydown', e => {
|
||||
if ((e.ctrlKey||e.metaKey) && e.key.toLowerCase()==='k') { e.preventDefault(); $('#paletteBtn').click(); }
|
||||
if (e.key==='Escape') palette.classList.remove('open');
|
||||
});
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user