Test #1
@@ -0,0 +1,187 @@
|
||||
/* 柏悦物流国内 PDA · 海外版视觉对齐层
|
||||
只覆盖视觉 tokens、布局和交互状态,不参与业务逻辑。 */
|
||||
:root {
|
||||
--pda-blue: #0a66d8;
|
||||
--pda-blue-deep: #084faa;
|
||||
--pda-blue-soft: #eaf3ff;
|
||||
--pda-ink: #344054;
|
||||
--pda-muted: #667085;
|
||||
--pda-bg: #f2f4f7;
|
||||
--pda-surface: #fff;
|
||||
--pda-line: #e4e7ec;
|
||||
--pda-success: #087a62;
|
||||
--pda-success-soft: #e8f7f2;
|
||||
--pda-warning: #a75b0b;
|
||||
--pda-warning-soft: #fff3e3;
|
||||
--pda-danger: #b42318;
|
||||
--pda-danger-soft: #fff0ee;
|
||||
--pda-shadow-soft: 0 2px 8px rgba(16,24,40,.06);
|
||||
--pda-shadow: 0 8px 24px rgba(16,24,40,.08);
|
||||
--pda-focus: rgba(10,102,216,.24);
|
||||
}
|
||||
|
||||
html { background: #e7ebf0; }
|
||||
body {
|
||||
background: var(--pda-bg);
|
||||
color: var(--pda-ink);
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
button, input, select { font-family: inherit; }
|
||||
button { transition: background-color .16s ease, border-color .16s ease, box-shadow .16s ease, transform .16s ease, opacity .16s ease; }
|
||||
button:active:not(:disabled) { transform: scale(.985); }
|
||||
button:focus-visible, input:focus-visible, select:focus-visible { outline: 3px solid var(--pda-focus); outline-offset: 2px; }
|
||||
|
||||
/* 登录 / 工作台 */
|
||||
.login-page {
|
||||
justify-content: center;
|
||||
min-height: 100dvh;
|
||||
padding: 24px 18px;
|
||||
background: linear-gradient(160deg, #eaf3ff 0%, #f2f4f7 54%, #f2f4f7 100%);
|
||||
}
|
||||
.login-logo {
|
||||
width: min(430px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 28px 24px 24px;
|
||||
border-radius: 18px 18px 0 0;
|
||||
background: linear-gradient(140deg, var(--pda-blue-deep), var(--pda-blue));
|
||||
}
|
||||
.login-logo-icon { width: 54px; height: 54px; border-radius: 14px; background: rgba(255,255,255,.15); border: 1px solid rgba(255,255,255,.18); }
|
||||
.login-logo-title { font-size: 20px; letter-spacing: 0; }
|
||||
.login-status { margin-top: 20px; }
|
||||
.login-card {
|
||||
width: min(430px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--pda-line);
|
||||
border-top: 0;
|
||||
border-radius: 0 0 18px 18px;
|
||||
background: var(--pda-surface);
|
||||
box-shadow: var(--pda-shadow);
|
||||
}
|
||||
.login-card-title { font-size: 20px; color: #1d2939; }
|
||||
.login-card-desc { color: var(--pda-muted); line-height: 1.55; }
|
||||
.login-field-label { color: #475467; }
|
||||
.login-field-input { height: 50px; border-color: #d0d5dd; border-radius: 11px; background: #fff; font-size: 16px; }
|
||||
.login-field-input:focus { border-color: var(--pda-blue); box-shadow: 0 0 0 3px var(--pda-blue-soft); }
|
||||
.login-btn { height: 50px; border-radius: 11px; background: var(--pda-blue); box-shadow: 0 6px 14px rgba(10,102,216,.2); font-weight: 750; }
|
||||
.login-btn:hover { background: var(--pda-blue-deep); }
|
||||
.login-demo-tip { border-radius: 9px; background: #f8fafc; color: var(--pda-muted); }
|
||||
.login-accounts { border-color: var(--pda-line); }
|
||||
.login-quick-btn { min-height: 56px; border-color: var(--pda-line); border-radius: 10px; background: #f8fafc; }
|
||||
.login-quick-btn:hover { border-color: #b8d2f7; background: var(--pda-blue-soft); }
|
||||
|
||||
.app-shell { background: var(--pda-bg); }
|
||||
.app-topbar {
|
||||
padding: 12px 16px 18px;
|
||||
border-radius: 0;
|
||||
background: var(--pda-surface);
|
||||
color: var(--pda-ink);
|
||||
box-shadow: var(--pda-shadow-soft);
|
||||
}
|
||||
.app-topbar-row { min-height: 44px; }
|
||||
.app-brand-mark { width: 40px; height: 40px; border-radius: 11px; color: #fff; background: var(--pda-blue); box-shadow: 0 4px 10px rgba(10,102,216,.2); }
|
||||
.app-brand-title { color: #1d2939; font-size: 16px; }
|
||||
.app-brand-sub { color: var(--pda-muted); opacity: 1; }
|
||||
.profile-entry { width: 44px; height: 44px; border: 1px solid #d8e8ff; border-radius: 50%; color: var(--pda-blue); background: var(--pda-blue-soft); }
|
||||
.profile-entry:hover { background: #dcecff; }
|
||||
.welcome-card {
|
||||
align-items: flex-start;
|
||||
margin-top: 18px;
|
||||
padding: 18px;
|
||||
border-radius: 16px;
|
||||
color: #fff;
|
||||
background: linear-gradient(140deg, var(--pda-blue-deep), var(--pda-blue));
|
||||
box-shadow: 0 8px 20px rgba(10,102,216,.18);
|
||||
}
|
||||
.welcome-kicker, .welcome-meta { color: rgba(255,255,255,.82); opacity: 1; }
|
||||
.welcome-name { font-size: 23px; }
|
||||
.shift-badge { min-height: 32px; padding: 7px 10px; border: 1px solid rgba(255,255,255,.22); border-radius: 9px; background: rgba(255,255,255,.12); }
|
||||
.today-panel { gap: 8px; margin: 14px 0 20px; }
|
||||
.today-stat { min-height: 88px; padding: 13px 10px; border-color: var(--pda-line); border-radius: 13px; background: var(--pda-surface); box-shadow: none; }
|
||||
.today-stat-num { color: #1d2939; font-size: 25px; }
|
||||
.today-stat-label { color: var(--pda-muted); font-size: 12px; }
|
||||
.section-heading { margin: 22px 2px 10px; }
|
||||
.section-heading-title { color: #1d2939; font-size: 16px; }
|
||||
.section-heading-title::before { width: 4px; height: 16px; border-radius: 4px; background: var(--pda-blue); }
|
||||
.module-card { padding: 0; border: 0; background: transparent; box-shadow: none; }
|
||||
.module-grid { gap: 10px; }
|
||||
.module-item, .module-item.active-menu {
|
||||
min-height: 116px;
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--pda-line);
|
||||
border-radius: 14px;
|
||||
color: #344054;
|
||||
background: var(--pda-surface);
|
||||
box-shadow: var(--pda-shadow-soft);
|
||||
}
|
||||
.module-item:hover { border-color: #b8d2f7; background: #fbfdff; }
|
||||
.module-icon { width: 42px; height: 42px; margin: 0 auto 10px; border-radius: 12px; color: var(--pda-blue); background: var(--pda-blue-soft); box-shadow: none; font-size: 21px; }
|
||||
.module-name { color: #475467; font-size: 13px; font-weight: 700; }
|
||||
.module-badge { top: 10px; right: 10px; min-width: 22px; height: 22px; border-radius: 99px; background: var(--pda-warning); font-size: 10px; line-height: 22px; }
|
||||
|
||||
/* 个人中心 */
|
||||
.profile-cover { height: 150px; background: linear-gradient(140deg, var(--pda-blue-deep), var(--pda-blue)); }
|
||||
.profile-cover .page-back { color: #fff; background: rgba(255,255,255,.14); }
|
||||
.profile-card { margin: -58px 14px 14px; border-radius: 16px; box-shadow: var(--pda-shadow); }
|
||||
.profile-avatar { border-radius: 20px; background: var(--pda-blue-soft); color: var(--pda-blue); }
|
||||
.profile-name { color: #1d2939; }
|
||||
.profile-role, .profile-list-value { color: var(--pda-muted); }
|
||||
.profile-list { margin: 12px 14px; border-color: var(--pda-line); border-radius: 14px; box-shadow: none; }
|
||||
.profile-list-item { min-height: 60px; padding: 13px 0; color: #475467; border-color: #edf1f5; }
|
||||
.profile-list-icon { border-radius: 10px; background: var(--pda-blue-soft); }
|
||||
.profile-logout { width: calc(100% - 28px); margin: 6px 14px 24px; min-height: 48px; border-radius: 11px; color: var(--pda-danger); background: var(--pda-surface); }
|
||||
|
||||
/* 入库 / 合托公共工作面 */
|
||||
.page-topbar { height: 62px; padding: 8px 14px; border-color: var(--pda-line); background: rgba(255,255,255,.96); box-shadow: var(--pda-shadow-soft); backdrop-filter: saturate(160%) blur(16px); }
|
||||
.page-back { width: 44px; height: 44px; border-radius: 10px; color: var(--pda-blue); background: transparent; font-size: 30px; line-height: 1; }
|
||||
.page-back:hover { background: var(--pda-blue-soft); }
|
||||
.page-title { color: #1d2939; font-size: 18px; font-weight: 750; }
|
||||
.page-user { width: 44px; height: 44px; border: 1px solid #d8e8ff; border-radius: 50%; color: var(--pda-blue); background: var(--pda-blue-soft); }
|
||||
.inbound-content { padding: 18px 14px calc(28px + env(safe-area-inset-bottom)); }
|
||||
.inbound-card { margin-bottom: 10px; padding: 16px; border-color: var(--pda-line); border-radius: 14px; background: var(--pda-surface); box-shadow: none; }
|
||||
.inbound-card-title { color: #1d2939; font-size: 16px; }
|
||||
.inbound-card-sub, .inbound-grid span, .inbound-sku-row span { color: var(--pda-muted); }
|
||||
.inbound-status { padding: 6px 9px; border-radius: 99px; color: var(--pda-warning); background: var(--pda-warning-soft); }
|
||||
.inbound-status.good { color: var(--pda-success); background: var(--pda-success-soft); }
|
||||
.inbound-grid div { border-color: #edf1f5; }
|
||||
.inbound-grid strong { color: #344054; }
|
||||
.inbound-btn { min-height: 46px; border-radius: 10px; background: var(--pda-blue); }
|
||||
.inbound-btn:hover { background: var(--pda-blue-deep); }
|
||||
.inbound-btn.secondary { color: var(--pda-blue); background: var(--pda-blue-soft); }
|
||||
.inbound-scan { margin-top: 14px; }
|
||||
.inbound-scan input, .consolidation-field select, .consolidation-field input { height: 48px; border-color: #d0d5dd; border-radius: 10px; background: #fff; font-size: 16px; }
|
||||
.inbound-scan input:focus, .consolidation-field select:focus, .consolidation-field input:focus { border-color: var(--pda-blue); box-shadow: 0 0 0 3px var(--pda-blue-soft); outline: 0; }
|
||||
.inbound-scan button { width: 48px; border-radius: 10px; background: var(--pda-blue); }
|
||||
.inbound-notice { border-radius: 10px; color: var(--pda-warning); background: var(--pda-warning-soft); }
|
||||
.inbound-notice.error { color: var(--pda-danger); background: var(--pda-danger-soft); }
|
||||
.consolidation-footer { border-color: var(--pda-line); background: rgba(255,255,255,.96); box-shadow: 0 -4px 14px rgba(16,24,40,.06); }
|
||||
|
||||
/* 集装箱拣货工作面 */
|
||||
.header { padding: 12px 14px 14px; border-radius: 0 0 16px 16px; background: linear-gradient(140deg, var(--pda-blue-deep), var(--pda-blue)); box-shadow: 0 8px 20px rgba(10,102,216,.18); }
|
||||
.header-title { font-size: 18px; }
|
||||
.header-badge { padding: 6px 9px; border-radius: 99px; }
|
||||
.box-select-wrap, .scan-section, .info-cards, .progress-section, .pick-list { margin-left: 14px; margin-right: 14px; }
|
||||
.box-select-btn { min-height: 48px; border-color: #c7dbf7; border-radius: 11px; background: var(--pda-surface); color: #174d91; box-shadow: var(--pda-shadow-soft); }
|
||||
.scan-section, .info-card, .progress-section, .pick-item { border-color: var(--pda-line); border-radius: 14px; box-shadow: none; }
|
||||
.scan-section { padding: 14px; background: var(--pda-surface); }
|
||||
.scan-input { height: 48px; border-color: #d0d5dd; border-radius: 10px; background: #fff; }
|
||||
.scan-btn { width: 48px; height: 48px; border-radius: 10px; background: var(--pda-blue); }
|
||||
.progress-bar-fill { background: var(--pda-success); }
|
||||
.pick-item { padding: 15px; }
|
||||
.pick-item.active { border-left-color: var(--pda-blue); box-shadow: 0 3px 12px rgba(10,102,216,.1); }
|
||||
.pick-btn { min-height: 44px; border-radius: 9px; }
|
||||
.pick-btn-confirm { background: var(--pda-success); }
|
||||
.pick-btn-partial { color: var(--pda-blue); background: var(--pda-blue-soft); border-color: #9ebcec; }
|
||||
.submit-section { box-shadow: 0 -4px 14px rgba(16,24,40,.07); }
|
||||
.submit-btn { min-height: 50px; border-radius: 10px; background: var(--pda-blue); }
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.login-page { padding-inline: 14px; }
|
||||
.login-logo { padding-inline: 18px; }
|
||||
.login-card { padding-inline: 18px; }
|
||||
.module-item, .module-item.active-menu { min-height: 104px; padding-inline: 8px; }
|
||||
.module-name { font-size: 12px; }
|
||||
.inbound-content { padding-inline: 12px; }
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+29
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="/playwright-logo.svg" type="image/svg+xml">
|
||||
<title>Playwright Dashboard</title>
|
||||
<script type="module" crossorigin src="/assets/index-CyWAfh-p.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DhC616m4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html style='scrollbar-gutter: stable both-edges;'>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='color-scheme' content='dark light'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<title>Playwright Test Report</title>
|
||||
<script type="module" crossorigin src="./report.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./report.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'></div>
|
||||
</body>
|
||||
</html>
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="/playwright-logo.svg" type="image/svg+xml">
|
||||
<title>Playwright Inspector</title>
|
||||
<script type="module" crossorigin src="/assets/index-DYjdXIbE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-l_lX622x.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
|
||||
<link rel="manifest" href="./manifest.webmanifest">
|
||||
<title>Playwright Trace Viewer</title>
|
||||
<script type="module" crossorigin src="./index.KZ4wOW1K.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/urlMatch-L3liM589.js">
|
||||
<link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-B-dXF5JN.js">
|
||||
<link rel="stylesheet" crossorigin href="./defaultSettingsView.BLFoOugd.css">
|
||||
<link rel="stylesheet" crossorigin href="./index.B_TqY17P.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<dialog id="fallback-error">
|
||||
<p>The Playwright Trace Viewer must be loaded over the <code>http://</code> or <code>https://</code> protocols.</p>
|
||||
<p>For more information, please see the <a href="https://aka.ms/playwright/trace-viewer-file-protocol">docs</a>.</p>
|
||||
</dialog>
|
||||
<script>
|
||||
if (!/^https?:/.test(window.location.protocol)) {
|
||||
const fallbackErrorDialog = document.getElementById('fallback-error');
|
||||
const isTraceViewerInsidePlaywrightReport = window.location.protocol === 'file:' && window.location.pathname.endsWith('/trace/index.html');
|
||||
// Best-effort to show the report path in the dialog.
|
||||
if (isTraceViewerInsidePlaywrightReport) {
|
||||
const reportPath = (() => {
|
||||
const base = decodeURIComponent(window.location.pathname).replace(/\/trace\/index\.html$/, '');
|
||||
if (navigator.platform === 'Win32')
|
||||
return base.replace(/^\//, '').replace(/\//g, '\\\\');
|
||||
return base;
|
||||
})();
|
||||
const reportLink = document.createElement('div');
|
||||
const command = `npx playwright show-report "${reportPath}"`;
|
||||
reportLink.innerHTML = `You can open the report via <code>${command}</code> from your Playwright project. <button type="button">Copy Command</button>`;
|
||||
fallbackErrorDialog.insertBefore(reportLink, fallbackErrorDialog.children[1]);
|
||||
reportLink.querySelector('button').addEventListener('click', () => navigator.clipboard.writeText(command));
|
||||
}
|
||||
fallbackErrorDialog.show();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<script type="module" crossorigin src="./snapshot.B_Jk1wbt.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/urlMatch-L3liM589.js">
|
||||
|
||||
<body>
|
||||
<iframe src="about:blank" sandbox="allow-same-origin allow-scripts" style="position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;border:none;"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
|
||||
<title>Playwright Test</title>
|
||||
<script type="module" crossorigin src="./uiMode.Dzuouizj.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/urlMatch-L3liM589.js">
|
||||
<link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-B-dXF5JN.js">
|
||||
<link rel="stylesheet" crossorigin href="./defaultSettingsView.BLFoOugd.css">
|
||||
<link rel="stylesheet" crossorigin href="./uiMode.C7UW1sC9.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>柏悦物流 PDA · 仓储作业平台</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
:root { --primary:#1769e0; --primary-deep:#123b72; --primary-soft:#eaf2ff; --bg:#f3f6fa; --card:#fff; --text:#172b4d; --text-secondary:#6b788c; --border:#d9e4f2; --danger:#dc2626; --success:#0d9488; --shadow:0 4px 14px rgba(18,59,114,.08); }
|
||||
* { box-sizing:border-box; margin:0; padding:0; }
|
||||
body { min-height:100vh; max-width:480px; margin:0 auto; overflow-x:hidden; background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Noto Sans SC",sans-serif; }
|
||||
button,input { font:inherit; }
|
||||
.hidden { display:none !important; }
|
||||
.toast { position:fixed; top:70px; left:50%; z-index:400; padding:10px 20px; border-radius:8px; transform:translateX(-50%) translateY(-20px); opacity:0; color:#fff; background:#1f2937; transition:all .3s; pointer-events:none; }
|
||||
.toast.show { transform:translateX(-50%) translateY(0); opacity:1; }
|
||||
.toast.success { background:var(--success); }
|
||||
.login-page { display:flex; flex-direction:column; justify-content:center; min-height:100vh; padding:32px 24px; background:linear-gradient(160deg,#1769e0 0%,#0d47a1 100%); }
|
||||
.login-logo { margin:0 -22px; padding:54px 22px 42px; text-align:left; }.login-brand-line { display:flex; align-items:center; gap:14px; }.login-logo-icon { display:flex; align-items:center; justify-content:center; width:62px; height:62px; border-radius:16px; background:rgba(255,255,255,.15); }.login-logo-icon svg { width:31px; height:31px; }.login-logo-title { color:#fff; font-size:21px; font-weight:700; }.login-logo-sub { margin-top:5px; color:rgba(255,255,255,.72); font-size:12px; }.login-status { display:flex; align-items:center; gap:6px; margin-top:25px; color:rgba(255,255,255,.8); font-size:11px; }.login-status::before { width:7px; height:7px; border-radius:50%; background:#48d5bc; box-shadow:0 0 0 4px rgba(72,213,188,.12); content:""; }
|
||||
.login-card { padding:22px; border-radius:12px; background:#fff; box-shadow:0 8px 32px rgba(0,0,0,.18); }.login-card-title { color:#172033; font-size:18px; font-weight:750; }.login-card-desc { margin:5px 0 20px; color:#78869a; font-size:12px; }.login-field { margin-bottom:15px; }.login-field-label { margin-bottom:7px; color:#445168; font-size:13px; font-weight:600; }.login-input-wrap { position:relative; }.login-input-icon { position:absolute; top:50%; left:13px; display:flex; color:#8290a4; transform:translateY(-50%); }.login-input-icon svg,.password-toggle svg { width:18px; height:18px; }.login-field-input { width:100%; height:46px; padding:0 42px; border:1px solid #d7e0eb; border-radius:8px; outline:0; color:#243b53; background:#f9fbfd; font-size:14px; }.login-field-input:focus { border-color:var(--primary); background:#fff; box-shadow:0 0 0 3px rgba(23,105,224,.08); }.password-toggle { position:absolute; top:50%; right:10px; display:flex; align-items:center; justify-content:center; width:28px; height:28px; border:0; color:#8290a4; background:transparent; transform:translateY(-50%); cursor:pointer; }.login-error { display:none; margin-bottom:12px; padding:9px 11px; border-radius:7px; color:var(--danger); background:#fff1f1; font-size:13px; }.login-error.show { display:block; }.login-btn { width:100%; height:48px; margin-top:5px; border:0; border-radius:8px; color:#fff; background:#1769e0; box-shadow:0 6px 14px rgba(23,105,224,.2); font-size:16px; font-weight:600; cursor:pointer; }.login-demo-tip { margin-top:13px; padding:10px 12px; border-radius:7px; color:#6b788c; background:#f2f5f9; font-size:11px; text-align:center; }.login-accounts { margin-top:16px; padding-top:14px; border-top:1px solid #e3e8ef; }.login-accounts-title { margin-bottom:9px; color:#78869a; font-size:11px; text-align:left; }.login-quick-btns { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }.login-quick-btn { padding:10px; border:1px solid #dbe4ee; border-radius:8px; color:#334e68; background:#fff; text-align:left; cursor:pointer; }.login-quick-btn-name,.login-quick-btn-role { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.login-quick-btn-name { font-size:13px; font-weight:650; }.login-quick-btn-role { margin-top:3px; color:#78869a; font-size:11px; }
|
||||
.workbench-page { --wb-brand:#155eef; --wb-brand-deep:#123b72; --wb-brand-surface:#f4f8ff; --wb-brand-border:#a7c4f4; --wb-surface:#fff; --wb-border:#d9e4f2; --wb-text:#172b4d; --wb-text-secondary:#627388; --wb-danger:#e5484d; }.app-shell { min-height:100vh; padding-bottom:28px; background:var(--bg); }.app-topbar { padding:15px 16px 24px; border-radius:0 0 16px 16px; color:#fff; background:linear-gradient(140deg,var(--wb-brand-deep),var(--wb-brand)); box-shadow:0 8px 20px rgba(18,59,114,.18); }.app-topbar-row,.welcome-card,.section-heading { display:flex; align-items:center; justify-content:space-between; }.app-topbar-row { gap:12px; }.app-brand { display:flex; align-items:center; gap:10px; }.app-brand-mark { display:flex; align-items:center; justify-content:center; width:36px; height:36px; border-radius:9px; background:rgba(255,255,255,.18); font-size:18px; }.app-brand-title { font-size:17px; font-weight:700; }.app-brand-sub { margin-top:2px; font-size:11px; opacity:.72; }.profile-entry { display:flex; align-items:center; justify-content:center; width:40px; height:40px; border:1px solid rgba(255,255,255,.25); border-radius:10px; color:#fff; background:rgba(255,255,255,.16); font-size:17px; font-weight:700; cursor:pointer; }.welcome-card { align-items:flex-end; gap:12px; margin-top:18px; }.welcome-kicker { font-size:12px; opacity:.75; }.welcome-name { margin-top:3px; font-size:21px; font-weight:700; }.welcome-meta { margin-top:5px; font-size:12px; opacity:.82; }.shift-badge { flex:none; padding:7px 10px; border-radius:10px; background:rgba(255,255,255,.15); font-size:11px; }.shift-badge::before { display:inline-block; width:7px; height:7px; margin-right:5px; border-radius:50%; background:#24b886; content:""; }.workbench-content { padding:14px 12px 22px; }.today-panel { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; margin-bottom:14px; }.today-stat { padding:12px 8px; border:1px solid var(--wb-border); border-radius:12px; background:var(--wb-surface); box-shadow:var(--shadow); text-align:center; }.today-stat-num { color:var(--wb-brand-deep); font-size:20px; font-weight:750; line-height:1.1; }.today-stat-label { margin-top:5px; color:var(--wb-text-secondary); font-size:11px; }.section-heading { margin:18px 2px 8px; }.section-heading-title { display:flex; align-items:center; gap:8px; color:var(--wb-text); font-size:15px; font-weight:700; }.section-heading-title::before { width:4px; height:16px; border-radius:3px; background:var(--wb-brand); content:""; }.module-card { padding:4px 6px; border:1px solid var(--wb-border); border-radius:10px; background:var(--wb-surface); box-shadow:var(--shadow); }.module-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); }.module-item { position:relative; min-height:78px; margin:3px; padding:7px 3px; border:1px solid var(--wb-brand-border); border-radius:9px; color:#174ea6; background:var(--wb-brand-surface); box-shadow:0 4px 10px rgba(18,59,114,.1); cursor:pointer; }.module-icon { display:flex; align-items:center; justify-content:center; width:40px; height:40px; margin:0 auto 7px; border-radius:10px; color:#fff; background:var(--wb-brand); box-shadow:0 5px 12px rgba(18,59,114,.16); font-size:20px; }.module-name { font-size:12px; line-height:1.3; white-space:nowrap; }.module-badge { position:absolute; top:7px; right:5px; min-width:17px; height:17px; padding:0 4px; border-radius:9px; color:#fff; background:var(--wb-danger); font-size:9px; line-height:17px; }
|
||||
.page-back { width:38px; height:38px; border:0; border-radius:11px; color:#fff; background:rgba(255,255,255,.14); font-size:20px; cursor:pointer; }.profile-cover { height:132px; padding:16px; color:#fff; background:linear-gradient(140deg,#123b72,#1769e0); }.profile-card { position:relative; margin:-49px 12px 12px; padding:18px; border-radius:12px; background:#fff; box-shadow:0 10px 28px rgba(20,49,82,.13); text-align:center; }.profile-avatar { display:flex; align-items:center; justify-content:center; width:68px; height:68px; margin:-50px auto 10px; border:4px solid #fff; border-radius:16px; color:#155eef; background:#eaf2ff; font-size:25px; font-weight:800; }.profile-name { color:#172b4d; font-size:19px; font-weight:700; }.profile-role { margin-top:4px; color:#728196; font-size:12px; }.profile-stats { display:grid; grid-template-columns:repeat(3,1fr); margin-top:16px; padding-top:14px; border-top:1px solid #edf1f5; }.profile-stat strong { display:block; color:#163b68; font-size:17px; }.profile-stat span { color:#8a97a8; font-size:10px; }.profile-list { margin:12px; padding:2px 14px; border:1px solid #e3e8ef; border-radius:10px; background:#fff; }.profile-list-item { display:flex; align-items:center; gap:12px; padding:15px 0; border-bottom:1px solid #edf1f5; color:#334e68; font-size:13px; }.profile-list-item:last-child { border-bottom:0; }.profile-list-icon { display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; background:#eef4ff; }.profile-list-value { margin-left:auto; color:#829ab1; font-size:12px; }.profile-logout { width:calc(100% - 24px); margin:4px 12px 24px; padding:13px; border:1px solid #fecaca; border-radius:9px; color:#dc2626; background:#fff; font-size:14px; font-weight:600; cursor:pointer; }
|
||||
@media (max-width:360px) { .login-page { padding-right:16px; padding-left:16px; } .login-logo { margin-right:-16px; margin-left:-16px; padding-right:18px; padding-left:18px; } .module-name { font-size:11px; } }
|
||||
</style>
|
||||
<link rel="stylesheet" href="domestic-overseas-theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast" id="toast"></div>
|
||||
<div class="login-page" id="loginPage"><div class="login-logo"><div class="login-brand-line"><div class="login-logo-icon"><svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg></div><div><div class="login-logo-title">柏悦物流 PDA</div><div class="login-logo-sub">仓储移动作业平台</div></div></div><div class="login-status">作业系统运行正常</div></div><div class="login-card"><div class="login-card-title">账号登录</div><div class="login-card-desc">请输入作业人员账号进入工作台</div><div class="login-field"><div class="login-field-label">用户名</div><div class="login-input-wrap"><span class="login-input-icon">♙</span><input class="login-field-input" id="loginUser" placeholder="请输入用户名" autocomplete="username"></div></div><div class="login-field"><div class="login-field-label">密码</div><div class="login-input-wrap"><span class="login-input-icon">▣</span><input class="login-field-input" id="loginPass" type="password" placeholder="请输入密码" autocomplete="current-password"><button class="password-toggle" id="passwordToggle" type="button" onclick="togglePassword()" aria-label="显示密码">◉</button></div></div><div class="login-error" id="loginError">用户名或密码错误</div><button class="login-btn" type="button" onclick="handleLogin()">登录</button><div class="login-demo-tip">演示账号:W001 密码:123456</div><div class="login-accounts"><div class="login-accounts-title">快捷登录</div><div class="login-quick-btns"><button class="login-quick-btn" type="button" onclick="quickLogin('W001','张伟','拣货员')"><span class="login-quick-btn-name">W001 · 张伟</span><span class="login-quick-btn-role">拣货员 · 洛杉矶仓</span></button><button class="login-quick-btn" type="button" onclick="quickLogin('W002','李敏','拣货组长')"><span class="login-quick-btn-name">W002 · 李敏</span><span class="login-quick-btn-role">拣货组长 · 洛杉矶仓</span></button></div></div></div></div>
|
||||
<div class="workbench-page hidden" id="workbenchPage"><div class="app-shell"><div class="app-topbar"><div class="app-topbar-row"><div class="app-brand"><div class="app-brand-mark">▦</div><div><div class="app-brand-title">柏悦物流 PDA</div><div class="app-brand-sub">仓储移动作业平台</div></div></div><button class="profile-entry" id="workbenchAvatar" type="button" onclick="openProfile()" aria-label="个人中心">张</button></div><div class="welcome-card"><div><div class="welcome-kicker">欢迎回来</div><div class="welcome-name" id="workbenchUserName">张伟</div><div class="welcome-meta"><span id="workbenchRole">拣货员</span> · <span id="workbenchWarehouse">洛杉矶仓</span></div></div><div class="shift-badge">白班 · 作业中</div></div></div><div class="workbench-content"><div class="today-panel"><div class="today-stat"><div class="today-stat-num">3</div><div class="today-stat-label">待拣集装箱</div></div><div class="today-stat"><div class="today-stat-num">0</div><div class="today-stat-label">今日已完成</div></div><div class="today-stat"><div class="today-stat-num">17</div><div class="today-stat-label">待拣货品</div></div></div><div class="section-heading"><div class="section-heading-title">入库</div></div><div class="module-card"><div class="module-grid"><button class="module-item active-menu" id="receiveEntry" type="button" onclick="navigateToModule('入库/收货登记.html')"><span class="module-badge">3</span><span class="module-icon">📥</span><span class="module-name">收货登记</span></button><button class="module-item active-menu" id="palletEntry" type="button" onclick="navigateToModule('入库/入库打托.html')"><span class="module-badge">2</span><span class="module-icon">🪵</span><span class="module-name">入库打托</span></button></div></div><div class="section-heading"><div class="section-heading-title">出库</div></div><div class="module-card"><div class="module-grid" id="outboundModuleGrid"><button class="module-item active-menu" id="consolidationEntry" type="button" onclick="navigateToModule('出库/合托.html')"><span class="module-badge">1</span><span class="module-icon">🪵</span><span class="module-name">合托</span></button><button class="module-item active-menu" id="pickingEntry" type="button" onclick="navigateToModule('出库/集装箱拣货.html')"><span class="module-badge">3</span><span class="module-icon">📦</span><span class="module-name">集装箱拣货</span></button></div></div></div></div></div>
|
||||
<div class="profile-page hidden" id="profilePage"><div class="app-shell"><div class="profile-cover"><button class="page-back" type="button" onclick="backFromProfile()">‹</button></div><div class="profile-card"><div class="profile-avatar" id="profileAvatar">张</div><div class="profile-name" id="profileName">张伟</div><div class="profile-role" id="profileRole">W001 · 拣货员</div><div class="profile-stats"><div class="profile-stat"><strong>28</strong><span>今日完成</span></div><div class="profile-stat"><strong>156</strong><span>本月任务</span></div><div class="profile-stat"><strong>99.2%</strong><span>作业准确率</span></div></div></div><div class="profile-list"><div class="profile-list-item"><span class="profile-list-icon">🏢</span><span>当前仓库</span><span class="profile-list-value" id="profileWarehouse">洛杉矶仓</span></div><div class="profile-list-item"><span class="profile-list-icon">🕘</span><span>当前班次</span><span class="profile-list-value">白班 08:00–17:00</span></div><div class="profile-list-item"><span class="profile-list-icon">🔐</span><span>修改密码</span><span class="profile-list-value">›</span></div><div class="profile-list-item"><span class="profile-list-icon">⚙️</span><span>系统设置</span><span class="profile-list-value">›</span></div><div class="profile-list-item"><span class="profile-list-icon">ℹ️</span><span>版本信息</span><span class="profile-list-value">V1.0.0</span></div></div><button class="profile-logout" type="button" onclick="handleLogout()">退出登录</button></div></div>
|
||||
<script>
|
||||
const DEMO_ACCOUNTS = { W001:{pass:'123456',name:'张伟',role:'拣货员',warehouse:'洛杉矶仓'}, W002:{pass:'123456',name:'李敏',role:'拣货组长',warehouse:'洛杉矶仓'}, W003:{pass:'123456',name:'王磊',role:'仓管员',warehouse:'新泽西仓'} };
|
||||
const USER_STORAGE_KEY = 'pdaCurrentUser'; let currentUser = null; let toastTimer = null;
|
||||
function buildUserParams(user) { return new URLSearchParams({ id:user.id, name:user.name, role:user.role, warehouse:user.warehouse }); }
|
||||
function persistCurrentUser() { localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(currentUser)); }
|
||||
function navigateToModule(relativePath) { persistCurrentUser(); location.href = `${relativePath}?${buildUserParams(currentUser)}`; }
|
||||
function togglePassword() { const input = document.getElementById('loginPass'); const showing = input.type === 'text'; input.type = showing ? 'password' : 'text'; document.getElementById('passwordToggle').setAttribute('aria-label', showing ? '显示密码' : '隐藏密码'); }
|
||||
function showToast(message,type='') { const toast=document.getElementById('toast'); toast.textContent=message; toast.className=`toast show ${type}`; clearTimeout(toastTimer); toastTimer=setTimeout(()=>{toast.className='toast';},2200); }
|
||||
function showLoginError(message) { const error=document.getElementById('loginError'); error.textContent=message; error.classList.add('show'); } function hideLoginError() { document.getElementById('loginError').classList.remove('show'); }
|
||||
function updateUserUI() { const initial=currentUser.name.slice(0,1); document.getElementById('workbenchAvatar').textContent=initial; document.getElementById('workbenchUserName').textContent=currentUser.name; document.getElementById('workbenchRole').textContent=currentUser.role; document.getElementById('workbenchWarehouse').textContent=currentUser.warehouse; document.getElementById('profileAvatar').textContent=initial; document.getElementById('profileName').textContent=currentUser.name; document.getElementById('profileRole').textContent=`${currentUser.id} · ${currentUser.role}`; document.getElementById('profileWarehouse').textContent=currentUser.warehouse; }
|
||||
function showAppPage(pageId) { ['workbenchPage','profilePage'].forEach(id=>document.getElementById(id).classList.toggle('hidden',id!==pageId)); window.scrollTo(0,0); }
|
||||
function enterMainPage(showWelcome=true) { document.getElementById('loginPage').classList.add('hidden'); showAppPage('workbenchPage'); updateUserUI(); hideLoginError(); if(showWelcome) showToast(`${currentUser.name},欢迎登录`,'success'); }
|
||||
function handleLogin() { const userId=document.getElementById('loginUser').value.trim(); const password=document.getElementById('loginPass').value; if(!userId) return showLoginError('请输入用户名'); if(!password) return showLoginError('请输入密码'); const account=DEMO_ACCOUNTS[userId]; if(!account||account.pass!==password) return showLoginError('用户名或密码错误'); currentUser={id:userId,name:account.name,role:account.role,warehouse:account.warehouse}; persistCurrentUser(); enterMainPage(); }
|
||||
function quickLogin(id,name,role) { currentUser={id,name,role,warehouse:DEMO_ACCOUNTS[id].warehouse}; persistCurrentUser(); enterMainPage(); } function openProfile() { showAppPage('profilePage'); } function backFromProfile() { showAppPage('workbenchPage'); }
|
||||
function handleLogout() { currentUser=null; localStorage.removeItem(USER_STORAGE_KEY); document.getElementById('loginPage').classList.remove('hidden'); document.getElementById('workbenchPage').classList.add('hidden'); document.getElementById('profilePage').classList.add('hidden'); document.getElementById('loginUser').value=''; document.getElementById('loginPass').value=''; hideLoginError(); }
|
||||
function readUserFromParams(params) { if(!params.get('id')) return null; return Object.fromEntries(['id','name','role','warehouse'].map(key=>[key,params.get(key)||''])); } function readStoredUser() { try { const stored=JSON.parse(localStorage.getItem(USER_STORAGE_KEY)); return stored?.id ? stored : null; } catch { return null; } }
|
||||
function restoreWorkbenchSession() { const params=new URLSearchParams(location.search); if(params.get('resume')!=='1') return; currentUser=readUserFromParams(params)||readStoredUser(); if(!currentUser) return; persistCurrentUser(); enterMainPage(false); }
|
||||
document.getElementById('loginPass').addEventListener('keydown',event=>{if(event.key==='Enter')handleLogin();}); document.getElementById('loginUser').addEventListener('keydown',event=>{if(event.key==='Enter')document.getElementById('loginPass').focus();}); restoreWorkbenchSession();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,282 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>柏悦物流 PDA · 入库打托</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
:root { --primary:#1769e0; --success:#0d9488; --danger:#dc2626; --bg:#f5f7fa; --text:#1f2937; --border:#e5e7eb; --shadow:0 1px 3px rgba(0,0,0,.08); }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { min-height:100vh; max-width:480px; margin:0 auto; overflow-x:hidden; background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Noto Sans SC',sans-serif; }
|
||||
.toast { position:fixed; top:70px; left:50%; z-index:400; padding:10px 20px; border-radius:8px; background:var(--text); color:#fff; font-size:14px; opacity:0; pointer-events:none; transform:translateX(-50%) translateY(-20px); transition:all .3s; }
|
||||
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
|
||||
.toast.success { background:var(--success); }
|
||||
.toast.error { background:var(--danger); }
|
||||
.inbound-page.hidden, .profile-page.hidden { display:none; }
|
||||
.app-shell { min-height:100vh; padding-bottom:28px; background:var(--bg); }
|
||||
.page-topbar { position:sticky; top:0; z-index:90; display:flex; align-items:center; justify-content:space-between; height:60px; padding:0 12px; background:#fff; border-bottom:1px solid var(--border); box-shadow:0 2px 8px rgba(20,49,82,.05); }
|
||||
.page-back { width:38px; height:38px; border:0; border-radius:11px; background:#f1f5f9; color:#334155; font-size:20px; cursor:pointer; }
|
||||
.page-title { color:#172b4d; font-size:17px; font-weight:700; }
|
||||
.page-user { width:36px; height:36px; border:0; border-radius:12px; background:#eaf2ff; color:#155eef; font-size:13px; font-weight:700; cursor:pointer; }
|
||||
.inbound-content { padding:14px 12px 30px; }
|
||||
.inbound-card { margin-bottom:12px; padding:15px; border:1px solid #e4eaf2; border-radius:15px; background:#fff; box-shadow:var(--shadow); }
|
||||
.inbound-card-head { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; }
|
||||
.inbound-card-title { color:#243b53; font-size:15px; font-weight:700; }
|
||||
.inbound-card-sub { margin-top:4px; color:#829ab1; font-size:11px; line-height:1.5; }
|
||||
.inbound-status { flex:none; padding:5px 8px; border-radius:8px; background:#fff4e5; color:#bd6b10; font-size:10px; font-weight:700; }
|
||||
.inbound-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:9px; margin-top:13px; }
|
||||
.inbound-grid div { min-width:0; padding-top:9px; border-top:1px solid #edf1f5; }
|
||||
.inbound-grid span { display:block; color:#829ab1; font-size:10px; }
|
||||
.inbound-grid strong { display:block; margin-top:3px; overflow:hidden; color:#334e68; font-size:12px; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.inbound-actions { display:flex; gap:8px; margin-top:13px; }
|
||||
.inbound-btn { flex:1; min-height:42px; border:0; border-radius:10px; background:#1769e0; color:#fff; font-size:13px; font-weight:700; cursor:pointer; }
|
||||
.inbound-btn.secondary { background:#eaf2ff; color:#1769e0; }
|
||||
.inbound-scan { display:flex; gap:8px; margin-top:13px; }
|
||||
.inbound-scan input { flex:1; min-width:0; height:44px; padding:0 12px; border:1px solid #dfe5ee; border-radius:9px; font-size:13px; }
|
||||
.inbound-scan button { width:46px; border:0; border-radius:9px; background:#1769e0; color:#fff; font-size:19px; cursor:pointer; }
|
||||
.order-list-page .inbound-content { padding-bottom:30px; }
|
||||
.order-list-toolbar { margin-bottom:12px; }
|
||||
.order-list-summary { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
||||
.order-list-summary strong { color:#243b53; font-size:15px; }
|
||||
.order-list-summary span { color:#627d98; font-size:12px; }
|
||||
.order-list-table-wrap { overflow-x:auto; border:1px solid #e4eaf2; border-radius:12px; background:#fff; box-shadow:var(--shadow); }
|
||||
.order-list-table { width:100%; border-collapse:collapse; table-layout:fixed; font-size:11px; }
|
||||
.order-list-table th { padding:11px 7px; background:#f7f9fc; color:#627d98; font-size:10px; font-weight:700; text-align:left; white-space:nowrap; }
|
||||
.order-list-table td { padding:11px 7px; border-top:1px solid #edf1f5; color:#334e68; vertical-align:middle; overflow-wrap:anywhere; }
|
||||
.order-list-table th:nth-child(1), .order-list-table td:nth-child(1) { width:29%; }
|
||||
.order-list-table th:nth-child(2), .order-list-table td:nth-child(2) { width:29%; }
|
||||
.order-list-table th:nth-child(3), .order-list-table td:nth-child(3) { width:14%; text-align:center; }
|
||||
.order-list-table th:nth-child(4), .order-list-table td:nth-child(4) { width:28%; text-align:center; }
|
||||
.order-list-status { display:inline-flex; align-items:center; padding:4px 7px; border-radius:7px; font-size:10px; font-weight:700; white-space:nowrap; }
|
||||
.order-list-status.done { background:#e1f6f1; color:#078775; }
|
||||
.order-list-status.pending { background:#fff4e5; color:#bd6b10; }
|
||||
.order-list-message { display:none; margin-top:10px; padding:11px 12px; border-radius:9px; background:#f1f5f9; color:#627d98; font-size:12px; text-align:center; }
|
||||
.order-list-message.show { display:block; }
|
||||
.pallet-form-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; margin-top:13px; }
|
||||
.pallet-field { min-width:0; }
|
||||
.pallet-field label { display:block; margin-bottom:6px; color:#627d98; font-size:11px; }
|
||||
.pallet-field input { width:100%; height:42px; padding:0 11px; border:1px solid #dfe5ee; border-radius:9px; color:#243b53; font-size:15px; font-weight:600; outline:none; }
|
||||
.pallet-field input:focus { border-color:#1769e0; box-shadow:0 0 0 3px rgba(23,105,224,.12); }
|
||||
.pallet-count-panel { display:flex; align-items:center; justify-content:space-between; margin-top:12px; padding:12px; border-radius:9px; background:#f7f9fc; color:#627d98; font-size:12px; }
|
||||
.pallet-count-panel strong { color:#1769e0; font-size:20px; }
|
||||
.pallet-box-row { display:grid; grid-template-columns:1.2fr 1.2fr .8fr; gap:8px; padding:11px 0; border-top:1px solid #edf1f5; }
|
||||
.pallet-box-header { display:grid; grid-template-columns:1.2fr 1.2fr .8fr; gap:8px; padding:0 0 8px; color:#829ab1; font-size:10px; }
|
||||
.pallet-box-cell { min-width:0; color:#829ab1; font-size:12px; }
|
||||
.pallet-box-cell strong { display:block; overflow-wrap:anywhere; color:#829ab1; font-size:12px; font-weight:600; }
|
||||
.inbound-notice { margin:0 0 12px; padding:11px 12px; border-radius:9px; background:#e1f6f1; color:#078775; font-size:12px; }
|
||||
.inbound-notice.error { background:#fee2e2; color:#b42318; }
|
||||
.profile-cover { height:132px; padding:16px; background:linear-gradient(140deg,#123b72,#1769e0); }
|
||||
.profile-cover .page-back { background:rgba(255,255,255,.14); color:#fff; }
|
||||
.profile-card { position:relative; margin:-49px 12px 12px; padding:18px; border-radius:12px; background:#fff; box-shadow:0 10px 28px rgba(20,49,82,.13); text-align:center; }
|
||||
.profile-avatar { display:flex; align-items:center; justify-content:center; width:68px; height:68px; margin:-50px auto 10px; border:4px solid #fff; border-radius:16px; background:#eaf2ff; color:#155eef; font-size:25px; font-weight:800; }
|
||||
.profile-name { color:#172b4d; font-size:19px; font-weight:700; }
|
||||
.profile-role { margin-top:4px; color:#728196; font-size:12px; }
|
||||
.profile-stats { display:grid; grid-template-columns:repeat(3,1fr); margin-top:16px; padding-top:14px; border-top:1px solid #edf1f5; }
|
||||
.profile-stat strong { display:block; color:#163b68; font-size:17px; }
|
||||
.profile-stat span { color:#8a97a8; font-size:10px; }
|
||||
.profile-list { margin:12px; padding:2px 14px; border:1px solid #e3e8ef; border-radius:10px; background:#fff; }
|
||||
.profile-list-item { display:flex; align-items:center; gap:12px; padding:15px 0; border-bottom:1px solid #edf1f5; color:#334e68; font-size:13px; }
|
||||
.profile-list-item:last-child { border-bottom:0; }
|
||||
.profile-list-icon { display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; background:#eef4ff; }
|
||||
.profile-list-value { margin-left:auto; color:#829ab1; font-size:12px; }
|
||||
.profile-logout { width:calc(100% - 24px); margin:4px 12px 24px; padding:13px; border:1px solid #fecaca; border-radius:9px; background:#fff; color:#dc2626; font-size:14px; font-weight:600; cursor:pointer; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="../domestic-overseas-theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<div class="inbound-page order-list-page hidden" id="orderListPage">
|
||||
<div class="app-shell">
|
||||
<div class="page-topbar"><button class="page-back" type="button" onclick="returnToWorkbench()" aria-label="返回工作台">‹</button><div class="page-title">入库单列表</div><button class="page-user" id="orderListAvatar" type="button" onclick="openProfile()" aria-label="个人中心">张</button></div>
|
||||
<div class="inbound-content">
|
||||
<div class="inbound-card order-list-toolbar"><div class="inbound-card-head"><div><div class="inbound-card-title">扫描</div><div class="inbound-card-sub">扫描订单号、入库单号或 SKU 快速定位</div></div></div><div class="inbound-scan"><input id="orderListScanInput" placeholder="扫描或输入订单号/入库单号/SKU" autocomplete="off" onkeydown="if(event.key==='Enter')handleOrderListScan()"><button type="button" onclick="openPalletFromScan()" aria-label="扫描订单">▦</button></div></div>
|
||||
<div class="inbound-card order-list-summary"><div><strong>入库单列表</strong><div class="inbound-card-sub">按入库状态查看当前入库单明细</div></div><span id="orderListCount">5 条</span></div>
|
||||
<div class="order-list-table-wrap"><table class="order-list-table" id="orderListTable"><thead><tr><th>入库单号</th><th>SKU</th><th>箱数</th><th>入库状态</th></tr></thead><tbody></tbody></table></div>
|
||||
<div class="order-list-message" id="orderListMessage" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inbound-page hidden" id="palletOrdersPage">
|
||||
<div class="app-shell">
|
||||
<div class="page-topbar"><button class="page-back" type="button" onclick="returnToWorkbench()" aria-label="返回工作台">‹</button><div class="page-title">订单列表</div><button class="page-user" id="palletOrdersAvatar" type="button" onclick="openProfile()" aria-label="个人中心">张</button></div>
|
||||
<div class="inbound-content"><div class="inbound-card"><div class="inbound-card-head"><div><div class="inbound-card-title">入库订单</div><div class="inbound-card-sub">选择已收货订单开始打托</div></div><span class="inbound-status" id="palletOrderCount">2 单</span></div></div><div id="palletOrderList"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inbound-page hidden" id="palletPage">
|
||||
<div class="app-shell"><div class="page-topbar"><button class="page-back" type="button" onclick="openPalletOrders()" aria-label="返回订单列表">‹</button><div class="page-title">入库打托</div><button class="page-user" id="palletAvatar" type="button" onclick="openProfile()" aria-label="个人中心">张</button></div><div class="inbound-content" id="palletContent"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="profile-page hidden" id="profilePage">
|
||||
<div class="app-shell"><div class="profile-cover"><button class="page-back" type="button" onclick="backFromProfile()" aria-label="返回">‹</button></div><div class="profile-card"><div class="profile-avatar" id="profileAvatar">张</div><div class="profile-name" id="profileName">张伟</div><div class="profile-role" id="profileRole">W001 · 拣货员</div><div class="profile-stats"><div class="profile-stat"><strong>28</strong><span>今日完成</span></div><div class="profile-stat"><strong>156</strong><span>本月任务</span></div><div class="profile-stat"><strong>99.2%</strong><span>作业准确率</span></div></div></div><div class="profile-list"><div class="profile-list-item"><span class="profile-list-icon">🏢</span><span>当前仓库</span><span class="profile-list-value" id="profileWarehouse">洛杉矶仓</span></div><div class="profile-list-item"><span class="profile-list-icon">🕘</span><span>当前班次</span><span class="profile-list-value">白班 08:00–17:00</span></div><div class="profile-list-item"><span class="profile-list-icon">🔐</span><span>修改密码</span><span class="profile-list-value">›</span></div><div class="profile-list-item"><span class="profile-list-icon">⚙️</span><span>系统设置</span><span class="profile-list-value">›</span></div><div class="profile-list-item"><span class="profile-list-icon">ℹ️</span><span>版本信息</span><span class="profile-list-value">V1.0.0</span></div></div><button class="profile-logout" type="button" onclick="handleLogout()">退出登录</button></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function readCurrentUser() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (!params.get('id')) return null;
|
||||
return Object.fromEntries(['id','name','role','warehouse'].map(key => [key, params.get(key) || '']));
|
||||
}
|
||||
function buildUserParams(user) {
|
||||
return new URLSearchParams({ id:user.id, name:user.name, role:user.role, warehouse:user.warehouse });
|
||||
}
|
||||
function returnToWorkbench() {
|
||||
const params = buildUserParams(currentUser);
|
||||
params.set('resume', '1');
|
||||
location.href = `../pda-picking.html?${params}`;
|
||||
}
|
||||
|
||||
const currentUser = readCurrentUser() || { id:'W001', name:'张伟', role:'拣货员', warehouse:'洛杉矶仓' };
|
||||
let profileReturnPage = 'orderListPage';
|
||||
let toastTimer = null;
|
||||
let selectedPalletOrderId = null;
|
||||
let palletItems = [];
|
||||
let palletSequence = 1;
|
||||
let palletCartonCount = 0;
|
||||
let palletCartonsPerPallet = 1;
|
||||
|
||||
function showAppPage(pageId) {
|
||||
['orderListPage', 'palletOrdersPage', 'palletPage', 'profilePage'].forEach(id => document.getElementById(id).classList.toggle('hidden', id !== pageId));
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
function updateUserUI() {
|
||||
const initial = currentUser.name.slice(0, 1) || '我';
|
||||
document.getElementById('profileAvatar').textContent = initial;
|
||||
document.getElementById('profileName').textContent = currentUser.name;
|
||||
document.getElementById('profileRole').textContent = `${currentUser.id} · ${currentUser.role}`;
|
||||
document.getElementById('profileWarehouse').textContent = currentUser.warehouse;
|
||||
['orderListAvatar', 'palletOrdersAvatar', 'palletAvatar'].forEach(id => document.getElementById(id).textContent = initial);
|
||||
}
|
||||
function showToast(message, type = '') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.className = `toast show ${type}`;
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { toast.className = 'toast'; }, 2200);
|
||||
}
|
||||
function openProfile() {
|
||||
profileReturnPage = ['orderListPage', 'palletOrdersPage', 'palletPage'].find(id => !document.getElementById(id).classList.contains('hidden')) || 'orderListPage';
|
||||
showAppPage('profilePage');
|
||||
}
|
||||
function backFromProfile() { showAppPage(profileReturnPage); }
|
||||
function handleLogout() {
|
||||
try { localStorage.removeItem('pdaCurrentUser'); } catch {}
|
||||
location.href = '../pda-picking.html';
|
||||
}
|
||||
|
||||
const ORDER_LIST_ROWS = [
|
||||
{ orderNo:'SO-260901-030', inboundNo:'IN-260901-030', sku:'YQ-CASE-01', cartons:2, status:'已完成' },
|
||||
{ orderNo:'SO-260901-030', inboundNo:'IN-260901-030', sku:'YQ-CABLE-02', cartons:1, status:'未完成' },
|
||||
{ orderNo:'SO-260901-031', inboundNo:'IN-260901-031', sku:'NH-BAG-04', cartons:1, status:'未完成' },
|
||||
{ orderNo:'SO-260901-031', inboundNo:'IN-260901-031', sku:'NH-BAG-06', cartons:1, status:'未完成' },
|
||||
{ orderNo:'SO-260901-032', inboundNo:'IN-260901-032', sku:'YQ-HOLDER-03', cartons:2, status:'未完成' }
|
||||
];
|
||||
const PALLET_ORDERS = [
|
||||
{ id:'IN-260901-030', customer:'杭州积乐贸易', destination:'义乌仓', ticket:'SO-260901-030', total:6, cartons:[['BX-030-01','YQ-CASE-01','手机壳套装',120],['BX-030-02','YQ-CABLE-02','USB-C数据线',80],['BX-030-03','YQ-HOLDER-03','手机支架',60]] },
|
||||
{ id:'IN-260901-031', customer:'宁波远航电商', destination:'宁波仓', ticket:'SO-260901-031', total:4, cartons:[['BX-031-01','NH-BAG-04','旅行包',32],['BX-031-02','NH-BAG-06','收纳袋',48],['BX-031-03','NH-TAG-07','行李牌',24]] }
|
||||
];
|
||||
let orderListRows = ORDER_LIST_ROWS.slice();
|
||||
|
||||
function openOrderList() {
|
||||
showAppPage('orderListPage');
|
||||
orderListRows = ORDER_LIST_ROWS.slice();
|
||||
document.getElementById('orderListScanInput').value = '';
|
||||
renderOrderList();
|
||||
setTimeout(() => document.getElementById('orderListScanInput').focus(), 0);
|
||||
}
|
||||
function renderOrderList() {
|
||||
const tableBody = document.querySelector('#orderListTable tbody');
|
||||
const count = document.getElementById('orderListCount');
|
||||
const message = document.getElementById('orderListMessage');
|
||||
count.textContent = orderListRows.length + ' 条';
|
||||
tableBody.innerHTML = orderListRows.map(row => `<tr><td>${row.inboundNo}</td><td>${row.sku}</td><td>${row.cartons}</td><td><span class="order-list-status ${row.status === '已完成' ? 'done' : 'pending'}">${row.status}</span></td></tr>`).join('');
|
||||
message.textContent = orderListRows.length ? '' : '未找到匹配订单,请核对扫描内容';
|
||||
message.classList.toggle('show', !orderListRows.length);
|
||||
}
|
||||
function handleOrderListScan() {
|
||||
const input = document.getElementById('orderListScanInput');
|
||||
const value = input.value.trim().toLowerCase();
|
||||
orderListRows = value ? ORDER_LIST_ROWS.filter(row => [row.orderNo, row.inboundNo, row.sku].some(field => field.toLowerCase().includes(value))) : ORDER_LIST_ROWS.slice();
|
||||
renderOrderList();
|
||||
}
|
||||
function openPalletFromScan() {
|
||||
const input = document.getElementById('orderListScanInput');
|
||||
if (!input.value.trim()) { openPallet(PALLET_ORDERS[0].id); return; }
|
||||
handleOrderListScan();
|
||||
const inboundNos = [...new Set(orderListRows.map(row => row.inboundNo))];
|
||||
if (inboundNos.length !== 1) {
|
||||
const message = document.getElementById('orderListMessage');
|
||||
message.textContent = orderListRows.length ? '请扫描更精确的 SKU 或订单号后进入打托' : '未找到匹配订单,请核对扫描内容';
|
||||
message.classList.add('show');
|
||||
return;
|
||||
}
|
||||
const order = PALLET_ORDERS.find(item => item.id === inboundNos[0]);
|
||||
if (!order) {
|
||||
const message = document.getElementById('orderListMessage');
|
||||
message.textContent = '该订单暂无可打托明细';
|
||||
message.classList.add('show');
|
||||
return;
|
||||
}
|
||||
openPallet(order.id);
|
||||
}
|
||||
function openPalletOrders() {
|
||||
showAppPage('palletOrdersPage');
|
||||
renderPalletOrders();
|
||||
}
|
||||
function renderPalletOrders() {
|
||||
document.getElementById('palletOrderCount').textContent = PALLET_ORDERS.length + ' 单';
|
||||
document.getElementById('palletOrderList').innerHTML = PALLET_ORDERS.map(order => `<div class="inbound-card"><div class="inbound-card-head"><div><div class="inbound-card-title">${order.id}</div><div class="inbound-card-sub">${order.customer} · ${order.ticket}</div></div><span class="inbound-status">待打托</span></div><div class="inbound-grid"><div><span>目的仓</span><strong>${order.destination}</strong></div><div><span>计划托数</span><strong>${order.total} 托</strong></div><div><span>待打托箱数</span><strong>${order.cartons.length} 箱</strong></div><div><span>收货状态</span><strong>已收货</strong></div></div><div class="inbound-actions"><button class="inbound-btn" type="button" onclick="openPallet('${order.id}')">开始打托</button></div></div>`).join('');
|
||||
}
|
||||
function openPallet(id) {
|
||||
selectedPalletOrderId = id;
|
||||
palletItems = [];
|
||||
const order = PALLET_ORDERS.find(item => item.id === id) || PALLET_ORDERS[0];
|
||||
palletCartonCount = order.cartons.length;
|
||||
palletCartonsPerPallet = order.cartons.length || 1;
|
||||
showAppPage('palletPage');
|
||||
renderPallet();
|
||||
}
|
||||
function renderPallet() {
|
||||
const order = PALLET_ORDERS.find(order => order.id === selectedPalletOrderId) || PALLET_ORDERS[0];
|
||||
const palletCount = Math.max(1, Math.ceil((Number(palletCartonCount) || 0) / Math.max(1, Number(palletCartonsPerPallet) || 1)));
|
||||
const boxDetails = order.cartons.reduce((rows, carton) => {
|
||||
const sku = carton[1]; const quantity = carton[3]; const existing = rows.find(row => row.sku === sku && row.quantity === quantity);
|
||||
if (existing) existing.count += 1; else rows.push({ sku, quantity, count:1 });
|
||||
return rows;
|
||||
}, []);
|
||||
document.getElementById('palletContent').innerHTML = `<div class="inbound-card pallet-scan-section"><div class="inbound-scan"><input id="palletScanInput" placeholder="扫描箱号或 SKU" onkeydown="if(event.key==='Enter')handlePalletScan()"><button type="button" onclick="handlePalletScan()" aria-label="扫码">▦</button></div></div><div class="inbound-card pallet-box-section"><div class="inbound-card-head"><div><div class="inbound-card-title">箱子详情</div></div></div><div id="palletBoxDetails"><div class="pallet-box-header"><span>SKU</span><span>每箱数量</span><span>箱数</span></div>${boxDetails.map(row => `<div class="pallet-box-row"><div class="pallet-box-cell"><strong>${row.sku}</strong></div><div class="pallet-box-cell"><strong>${row.quantity}</strong></div><div class="pallet-box-cell"><strong>${row.count}</strong></div></div>`).join('')}</div></div><div class="inbound-card"><div class="inbound-card-title">打托数量</div><div class="pallet-form-grid"><div class="pallet-field"><label for="palletCartonCountInput">箱数</label><input id="palletCartonCountInput" type="number" min="1" value="${palletCartonCount}" oninput="updatePalletCount()"></div><div class="pallet-field"><label for="palletCartonsPerPalletInput">每托箱数</label><input id="palletCartonsPerPalletInput" type="number" min="1" value="${palletCartonsPerPallet}" oninput="updatePalletCount()"></div></div><div class="pallet-count-panel"><span>托数</span><strong id="palletCountValue">${palletCount}</strong></div></div><div id="palletMessage"></div><div class="inbound-actions"><button class="inbound-btn secondary" type="button" onclick="openOrderList()">返回订单列表</button><button class="inbound-btn" type="button" onclick="confirmPalletInbound()">确认入库</button></div>`;
|
||||
}
|
||||
function updatePalletCount() {
|
||||
palletCartonCount = Number(document.getElementById('palletCartonCountInput')?.value) || 0;
|
||||
palletCartonsPerPallet = Number(document.getElementById('palletCartonsPerPalletInput')?.value) || 0;
|
||||
const output = document.getElementById('palletCountValue');
|
||||
if (output) output.textContent = Math.max(0, Math.ceil(palletCartonCount / Math.max(1, palletCartonsPerPallet)));
|
||||
}
|
||||
function handlePalletScan() {
|
||||
const input = document.getElementById('palletScanInput');
|
||||
const value = (input?.value || '').trim();
|
||||
const order = PALLET_ORDERS.find(order => order.id === selectedPalletOrderId);
|
||||
if (!order || !value) { showPalletMessage('请输入或扫描箱号/SKU', true); return; }
|
||||
const found = order.cartons.find(carton => carton[0].toLowerCase() === value.toLowerCase() || carton[1].toLowerCase() === value.toLowerCase());
|
||||
if (!found) { showPalletMessage('未找到匹配货物:' + value, true); return; }
|
||||
if (palletItems.some(carton => carton[0] === found[0])) { showPalletMessage('该箱号已加入当前托盘', true); return; }
|
||||
palletItems.push(found);
|
||||
showToast('已加入托盘:' + found[0], 'success');
|
||||
renderPallet();
|
||||
}
|
||||
function showPalletMessage(message, error) { const element = document.getElementById('palletMessage'); if (element) element.innerHTML = `<div class="inbound-notice ${error ? 'error' : ''}">${message}</div>`; }
|
||||
function confirmPalletInbound() {
|
||||
if (!palletCartonCount || !palletCartonsPerPallet) { showPalletMessage('请输入有效的箱数和每托箱数', true); return; }
|
||||
palletSequence += Math.max(1, Math.ceil(palletCartonCount / palletCartonsPerPallet));
|
||||
showToast('入库确认成功', 'success');
|
||||
openOrderList();
|
||||
}
|
||||
|
||||
updateUserUI();
|
||||
openOrderList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,219 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>收货登记 · 柏悦物流 PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
:root { --bg:#f5f7fa; --card:#fff; --text:#1f2937; --text-secondary:#6b7280; --border:#e5e7eb; --success:#0d9488; --warning:#f59e0b; --danger:#dc2626; --shadow:0 1px 3px rgba(0,0,0,.08); }
|
||||
* { box-sizing:border-box; margin:0; padding:0; }
|
||||
body { max-width:480px; min-height:100vh; margin:0 auto; overflow-x:hidden; background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Noto Sans SC',sans-serif; }
|
||||
.hidden { display:none!important; }
|
||||
.app-shell { min-height:100vh; padding-bottom:28px; background:var(--bg); }
|
||||
.inbound-content { padding:14px 12px 30px; }
|
||||
.inbound-card { margin-bottom:12px; padding:15px; border:1px solid #e4eaf2; border-radius:15px; background:#fff; box-shadow:var(--shadow); }
|
||||
.inbound-card-head { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; }
|
||||
.inbound-card-title { color:#243b53; font-size:15px; font-weight:700; }
|
||||
.inbound-card-sub { margin-top:4px; color:#829ab1; font-size:11px; line-height:1.5; }
|
||||
.inbound-status { flex:none; padding:5px 8px; border-radius:8px; background:#fff4e5; color:#bd6b10; font-size:10px; font-weight:700; }
|
||||
.inbound-status.good { background:#e1f6f1; color:#078775; }
|
||||
.inbound-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:9px; margin-top:13px; }
|
||||
.inbound-grid div { min-width:0; padding-top:9px; border-top:1px solid #edf1f5; }
|
||||
.inbound-grid span { display:block; color:#829ab1; font-size:10px; }
|
||||
.inbound-grid strong { display:block; margin-top:3px; overflow:hidden; color:#334e68; font-size:12px; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.receive-pickup { display:inline-flex; align-items:center; gap:3px; color:#627d98; font-size:10px; white-space:nowrap; }
|
||||
.receive-pickup strong { color:#334e68; font-size:11px; }
|
||||
.receive-sku-field { grid-column:1/-1; }
|
||||
.receive-sku-list { display:grid; gap:5px; margin-top:6px; }
|
||||
.receive-sku-row { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:10px; min-width:0; }
|
||||
.receive-sku-row strong { margin-top:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.receive-sku-row .receive-sku-count { color:#1769e0; white-space:nowrap; }
|
||||
.inbound-actions { display:flex; gap:8px; margin-top:13px; }
|
||||
.inbound-btn { flex:1; min-height:42px; border:0; border-radius:10px; background:#1769e0; color:#fff; cursor:pointer; font-size:13px; font-weight:700; }
|
||||
.inbound-btn.secondary { background:#eaf2ff; color:#1769e0; }
|
||||
.inbound-scan { display:flex; gap:8px; margin-top:13px; }
|
||||
.inbound-scan input { flex:1; min-width:0; height:44px; padding:0 12px; border:1px solid #dfe5ee; border-radius:9px; font-size:13px; }
|
||||
.inbound-scan button { width:46px; border:0; border-radius:9px; background:#1769e0; color:#fff; cursor:pointer; font-size:19px; }
|
||||
.unload-photo-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; margin-top:12px; }
|
||||
.unload-photo-card { min-width:0; padding:10px; border:1px solid #dfe5ee; border-radius:11px; background:#fff; }
|
||||
.unload-photo-preview { display:flex; align-items:center; justify-content:center; height:70px; overflow:hidden; border-radius:9px; background:#eef0f3; color:#68758a; font-size:23px; }
|
||||
.unload-photo-preview img { width:100%; height:100%; object-fit:cover; }
|
||||
.unload-photo-name { margin-top:8px; color:#25364d; font-size:12px; font-weight:700; }
|
||||
.unload-photo-btn { display:block; width:100%; margin-top:8px; padding:10px 8px; border:0; border-radius:9px; background:#1769e0; color:#fff; cursor:pointer; font-size:12px; font-weight:700; text-align:center; }
|
||||
.unload-photo-btn input { display:none; }
|
||||
.inbound-notice { margin-top:10px; padding:10px 11px; border-radius:9px; background:#fff4e5; color:#a45c0b; font-size:11px; line-height:1.45; }
|
||||
.inbound-notice.error { background:#fee2e2; color:#b42318; }
|
||||
.page-topbar { position:sticky; top:0; z-index:90; display:flex; align-items:center; justify-content:space-between; height:60px; padding:0 12px; border-bottom:1px solid var(--border); background:#fff; box-shadow:0 2px 8px rgba(20,49,82,.05); }
|
||||
.page-back { width:38px; height:38px; border:0; border-radius:11px; background:#f1f5f9; color:#334155; cursor:pointer; font-size:20px; }
|
||||
.page-title { color:#172b4d; font-size:17px; font-weight:700; }
|
||||
.page-user { width:36px; height:36px; border:0; border-radius:12px; background:#eaf2ff; color:#155eef; cursor:pointer; font-size:13px; font-weight:700; }
|
||||
.profile-cover { height:132px; padding:16px; background:linear-gradient(140deg,#123b72,#1769e0); color:#fff; }
|
||||
.profile-cover .page-back { background:rgba(255,255,255,.14); color:#fff; }
|
||||
.profile-card { position:relative; margin:-49px 12px 12px; padding:18px; border-radius:12px; background:#fff; box-shadow:0 10px 28px rgba(20,49,82,.13); text-align:center; }
|
||||
.profile-avatar { display:flex; align-items:center; justify-content:center; width:68px; height:68px; margin:-50px auto 10px; border:4px solid #fff; border-radius:16px; background:#eaf2ff; color:#155eef; font-size:25px; font-weight:800; }
|
||||
.profile-name { color:#172b4d; font-size:19px; font-weight:700; }
|
||||
.profile-role { margin-top:4px; color:#728196; font-size:12px; }
|
||||
.profile-stats { display:grid; grid-template-columns:repeat(3,1fr); margin-top:16px; padding-top:14px; border-top:1px solid #edf1f5; }
|
||||
.profile-stat strong { display:block; color:#163b68; font-size:17px; }
|
||||
.profile-stat span { color:#8a97a8; font-size:10px; }
|
||||
.profile-list { margin:12px; padding:2px 14px; border:1px solid #e3e8ef; border-radius:10px; background:#fff; }
|
||||
.profile-list-item { display:flex; align-items:center; gap:12px; padding:15px 0; border-bottom:1px solid #edf1f5; color:#334e68; font-size:13px; }
|
||||
.profile-list-item:last-child { border-bottom:0; }
|
||||
.profile-list-icon { display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; background:#eef4ff; }
|
||||
.profile-list-value { margin-left:auto; color:#829ab1; font-size:12px; }
|
||||
.profile-logout { width:calc(100% - 24px); margin:4px 12px 24px; padding:13px; border:1px solid #fecaca; border-radius:9px; background:#fff; color:#dc2626; cursor:pointer; font-size:14px; font-weight:600; }
|
||||
.toast { position:fixed; top:70px; left:50%; z-index:400; padding:10px 20px; border-radius:8px; transform:translateX(-50%) translateY(-20px); background:var(--text); color:#fff; opacity:0; pointer-events:none; transition:all .3s; font-size:14px; }
|
||||
.toast.show { transform:translateX(-50%) translateY(0); opacity:1; }
|
||||
.toast.success { background:var(--success); } .toast.warning { background:var(--warning); } .toast.error { background:var(--danger); }
|
||||
</style>
|
||||
<link rel="stylesheet" href="../domestic-overseas-theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<div class="inbound-page hidden" id="receiveListPage">
|
||||
<div class="app-shell">
|
||||
<div class="page-topbar"><button class="page-back" onclick="returnToWorkbench()" aria-label="返回工作台">‹</button><div class="page-title">待签收列表</div><button class="page-user" id="receiveListAvatar" onclick="openProfile()" aria-label="个人中心">张</button></div>
|
||||
<div class="inbound-content">
|
||||
<div class="inbound-card"><div class="inbound-card-head"><div><div class="inbound-card-title">待签收订单</div><div class="inbound-card-sub">选择订单完成签收</div></div><span class="inbound-status" id="receivePendingCount">3 单</span></div></div>
|
||||
<div id="receiveVehicleList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inbound-page hidden" id="unloadPage">
|
||||
<div class="app-shell">
|
||||
<div class="page-topbar"><button class="page-back" onclick="openReceiveList()" aria-label="返回待签收列表">‹</button><div class="page-title">签收登记</div><button class="page-user" id="unloadAvatar" onclick="openProfile()" aria-label="个人中心">张</button></div>
|
||||
<div class="inbound-content" id="unloadContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-page hidden" id="profilePage">
|
||||
<div class="app-shell">
|
||||
<div class="profile-cover"><button class="page-back" onclick="backFromProfile()" aria-label="返回">‹</button></div>
|
||||
<div class="profile-card"><div class="profile-avatar" id="profileAvatar">张</div><div class="profile-name" id="profileName">张伟</div><div class="profile-role" id="profileRole">W001 · 拣货员</div><div class="profile-stats"><div class="profile-stat"><strong>28</strong><span>今日完成</span></div><div class="profile-stat"><strong>156</strong><span>本月任务</span></div><div class="profile-stat"><strong>99.2%</strong><span>作业准确率</span></div></div></div>
|
||||
<div class="profile-list"><div class="profile-list-item"><span class="profile-list-icon">🏢</span><span>当前仓库</span><span class="profile-list-value" id="profileWarehouse">洛杉矶仓</span></div><div class="profile-list-item"><span class="profile-list-icon">🕘</span><span>当前班次</span><span class="profile-list-value">白班 08:00–17:00</span></div><div class="profile-list-item"><span class="profile-list-icon">🔐</span><span>修改密码</span><span class="profile-list-value">›</span></div><div class="profile-list-item"><span class="profile-list-icon">⚙️</span><span>系统设置</span><span class="profile-list-value">›</span></div><div class="profile-list-item"><span class="profile-list-icon">ℹ️</span><span>版本信息</span><span class="profile-list-value">V1.0.0</span></div></div>
|
||||
<button class="profile-logout" onclick="returnToWorkbench()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function readCurrentUser() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (!params.get('id')) return null;
|
||||
return Object.fromEntries(['id','name','role','warehouse'].map(key => [key, params.get(key) || '']));
|
||||
}
|
||||
|
||||
function buildUserParams(user) {
|
||||
return new URLSearchParams({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
warehouse: user.warehouse
|
||||
});
|
||||
}
|
||||
|
||||
let currentUser = readCurrentUser() || { id:'', name:'', role:'', warehouse:'' };
|
||||
let profileReturnPage = 'receiveListPage';
|
||||
let selectedReceiveId = null;
|
||||
let receivePhotos = {};
|
||||
let toastTimer = null;
|
||||
|
||||
function returnToWorkbench() {
|
||||
const params = buildUserParams(currentUser);
|
||||
params.set('resume', '1');
|
||||
location.href = `../pda-picking.html?${params}`;
|
||||
}
|
||||
|
||||
function showAppPage(pageId) {
|
||||
['receiveListPage', 'unloadPage', 'profilePage'].forEach(id => document.getElementById(id).classList.toggle('hidden', id !== pageId));
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function updateUserUI() {
|
||||
const initial = currentUser.name.slice(0, 1) || '用';
|
||||
document.getElementById('profileName').textContent = currentUser.name;
|
||||
document.getElementById('profileRole').textContent = `${currentUser.id} · ${currentUser.role}`;
|
||||
document.getElementById('profileWarehouse').textContent = currentUser.warehouse;
|
||||
['profileAvatar', 'receiveListAvatar', 'unloadAvatar'].forEach(id => { document.getElementById(id).textContent = initial; });
|
||||
}
|
||||
|
||||
function openProfile() {
|
||||
profileReturnPage = ['receiveListPage', 'unloadPage'].find(id => !document.getElementById(id).classList.contains('hidden')) || 'receiveListPage';
|
||||
showAppPage('profilePage');
|
||||
}
|
||||
|
||||
function backFromProfile() { showAppPage(profileReturnPage); }
|
||||
|
||||
function showToast(msg, type = '') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = msg;
|
||||
toast.className = `toast show ${type}`;
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { toast.className = 'toast'; }, 2200);
|
||||
}
|
||||
|
||||
const RECEIVE_VEHICLES = [
|
||||
{ id:'RV-260901-018', container:'D2222', plate:'浙A·7K218', driver:'陈师傅', carrier:'义乌欧业物流', order:'IN-260901-018', pickupMethod:'自送', destination:'义乌仓', total:18, received:0, boxes:[['BX-018-01','电子元件套装',120],['BX-018-02','USB-C数据线',80],['BX-018-03','手机支架',60]] },
|
||||
{ id:'RV-260901-021', container:'D3333', plate:'鲁B·6M392', driver:'王师傅', carrier:'青岛远航物流', order:'IN-260901-021', pickupMethod:'自送', destination:'青岛仓', total:12, received:1, boxes:[['BX-021-01','桌面台灯',60],['BX-021-02','平板支架',40],['BX-021-03','无线鼠标',90]] },
|
||||
{ id:'RV-260901-026', container:'D4444', plate:'粤B·3Q816', driver:'李师傅', carrier:'深圳璟智供应链', order:'IN-260901-026', pickupMethod:'自送', destination:'深圳仓', total:8, received:0, boxes:[['BX-026-01','旅行包',32],['BX-026-02','收纳袋',48],['BX-026-03','行李牌',24]] }
|
||||
];
|
||||
|
||||
function openReceiveList() { showAppPage('receiveListPage'); renderReceiveList(); }
|
||||
|
||||
function renderReceiveList() {
|
||||
const pending = RECEIVE_VEHICLES.filter(vehicle => vehicle.received < vehicle.total);
|
||||
document.getElementById('receivePendingCount').textContent = `${pending.length} 单`;
|
||||
document.getElementById('receiveVehicleList').innerHTML = pending.length ? pending.map(vehicle => `
|
||||
<div class="inbound-card"><div class="inbound-card-head"><div><div class="inbound-card-title">${vehicle.order}</div><div class="inbound-card-sub"><span class="receive-pickup">提货方式:<strong>${vehicle.pickupMethod || '自送'}</strong></span> · ${vehicle.plate} · ${vehicle.driver}</div></div><span class="inbound-status">待签收</span></div><div class="inbound-grid"><div class="receive-sku-field"><span>SKU / 箱数</span><div class="receive-sku-list">${formatReceiveSkuRows(vehicle.boxes)}</div></div></div><div class="inbound-actions"><button class="inbound-btn receive-sign-button" type="button" onclick="openUnload('${vehicle.id}')">签收</button></div></div>`).join('') : '<div class="inbound-card"><div style="padding:24px;color:#829ab1;font-size:12px;text-align:center">暂无待卸货车辆</div></div>';
|
||||
}
|
||||
|
||||
function formatReceiveSkuRows(boxes) {
|
||||
const skuCounts = boxes.reduce((counts, box) => {
|
||||
counts.set(box[1], (counts.get(box[1]) || 0) + 1);
|
||||
return counts;
|
||||
}, new Map());
|
||||
return [...skuCounts].map(([sku, count]) => `<div class="receive-sku-row"><strong>${sku}</strong><strong class="receive-sku-count">${count} 箱</strong></div>`).join('');
|
||||
}
|
||||
|
||||
function openUnload(id) { selectedReceiveId = id; showAppPage('unloadPage'); renderUnload(); }
|
||||
|
||||
function renderUnload() {
|
||||
const vehicle = RECEIVE_VEHICLES.find(item => item.id === selectedReceiveId) || RECEIVE_VEHICLES[0];
|
||||
const photos = receivePhotos[vehicle.id] || {};
|
||||
const photoSteps = [{ key:'plate', label:'车牌照片', id:'licensePlatePhoto' }, { key:'waybill', label:'快递面单', id:'waybillPhoto' }, { key:'other', label:'其他照片', id:'otherPhoto' }];
|
||||
document.getElementById('unloadContent').innerHTML = `<div class="inbound-card"><div class="inbound-card-head"><div><div class="inbound-card-title">现场照片</div><div class="inbound-card-sub">照片均为选填,可按需补充</div></div><span class="inbound-status good">可提交</span></div><div class="unload-photo-grid">${photoSteps.map(step => { const photo = photos[step.key]; return `<div class="unload-photo-card"><div class="unload-photo-preview">${photo ? `<img src="${photo}" alt="${step.label}">` : '▣'}</div><div class="unload-photo-name">${step.label}</div><label class="unload-photo-btn">${photo ? '重新拍摄' : '拍照'}<input id="${step.id}" type="file" accept="image/*" capture="environment" onchange="handleUnloadPhoto('${step.key}',this)"></label></div>`; }).join('')}</div></div><div id="unloadMessage"></div><div class="inbound-actions"><button class="inbound-btn secondary" onclick="openReceiveList()">返回列表</button><button class="inbound-btn" onclick="confirmReceive()">确认签收</button></div>`;
|
||||
}
|
||||
|
||||
function handleReceiveScan() {
|
||||
const input = document.getElementById('receiveScanInput');
|
||||
const value = (input?.value || '').trim();
|
||||
const vehicle = RECEIVE_VEHICLES.find(item => item.id === selectedReceiveId);
|
||||
if (!vehicle || !value) { showUnloadMessage('请输入或扫描箱号', true); return; }
|
||||
const found = vehicle.boxes.find(box => box[0].toLowerCase() === value.toLowerCase());
|
||||
if (!found) { showUnloadMessage(`未找到匹配箱号:${value}`, true); return; }
|
||||
if (vehicle.received >= vehicle.total || vehicle.boxes.slice(0, vehicle.received).some(box => box[0] === found[0])) { showUnloadMessage('该箱号已收货', true); return; }
|
||||
vehicle.received += 1;
|
||||
showToast(`已收货:${found[0]}`, 'success');
|
||||
renderUnload();
|
||||
}
|
||||
|
||||
function handleUnloadPhoto(key, input) {
|
||||
const file = input.files && input.files[0];
|
||||
if (!file || !selectedReceiveId) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => { receivePhotos[selectedReceiveId] = receivePhotos[selectedReceiveId] || {}; receivePhotos[selectedReceiveId][key] = reader.result; renderUnload(); };
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function handlePlatePhoto(input) { handleUnloadPhoto('plate', input); }
|
||||
function showUnloadMessage(message, error) { const element = document.getElementById('unloadMessage'); if (element) element.innerHTML = `<div class="inbound-notice ${error ? 'error' : ''}">${message}</div>`; }
|
||||
function confirmReceive() { completeUnload(); }
|
||||
function completeUnload() { const vehicle = RECEIVE_VEHICLES.find(item => item.id === selectedReceiveId); if (!vehicle) return; vehicle.received = vehicle.total; showToast('收货登记已完成', 'success'); openReceiveList(); }
|
||||
|
||||
updateUserUI();
|
||||
openReceiveList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>柏悦物流 PDA · 合托</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
:root { --brand:#1769e0; --brand-soft:#eaf2ff; --success:#078775; --danger:#bd3b3b; --warning:#bd6b10; --text:#243b53; --muted:#829ab1; --border:#e4eaf2; --bg:#f3f6fa; }
|
||||
* { box-sizing:border-box; } body { max-width:480px; min-height:100vh; margin:0 auto; background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Noto Sans SC",sans-serif; } button,input,select { font:inherit; }.hidden { display:none!important; }
|
||||
.toast { position:fixed; top:70px; left:50%; z-index:300; padding:10px 20px; border-radius:8px; transform:translateX(-50%) translateY(-20px); opacity:0; color:#fff; background:#243b53; transition:all .25s; pointer-events:none; }.toast.show { transform:translateX(-50%) translateY(0); opacity:1; }.toast.success { background:var(--success); }.toast.error { background:var(--danger); }
|
||||
.app-shell { min-height:100vh; padding-bottom:82px; }.page-topbar { position:sticky; top:0; z-index:10; display:flex; align-items:center; justify-content:space-between; height:60px; padding:0 12px; border-bottom:1px solid var(--border); background:#fff; box-shadow:0 2px 8px rgba(20,49,82,.05); }.page-back,.page-user { border:0; cursor:pointer; }.page-back { width:38px; height:38px; border-radius:11px; color:#334e68; background:#f1f5f9; font-size:20px; }.page-title { font-size:17px; font-weight:700; }.page-user { width:36px; height:36px; border-radius:12px; color:#155eef; background:#eaf2ff; font-size:13px; font-weight:700; }
|
||||
.inbound-content { padding:14px 12px 30px; }.inbound-card { padding:15px; margin-bottom:12px; border:1px solid var(--border); border-radius:15px; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.08); }.inbound-card-head { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; }.inbound-card-title { color:var(--text); font-size:15px; font-weight:700; }.inbound-card-sub { margin-top:4px; color:var(--muted); font-size:11px; line-height:1.5; }.inbound-status { flex:none; padding:5px 8px; border-radius:8px; color:var(--success); background:#e1f6f1; font-size:10px; font-weight:700; }.inbound-scan { display:flex; gap:8px; margin-top:13px; }.inbound-scan input { flex:1; min-width:0; height:44px; padding:0 12px; border:1px solid #dfe5ee; border-radius:9px; font-size:13px; }.inbound-scan button { width:46px; border:0; border-radius:9px; color:#fff; background:var(--brand); font-size:19px; cursor:pointer; }
|
||||
.consolidation-message,.consolidation-selection-message { display:none; margin-top:10px; padding:10px 11px; border-radius:8px; font-size:12px; }.consolidation-message.show,.consolidation-selection-message.show { display:block; }.consolidation-message.error { color:var(--danger); background:#fff1f1; }.consolidation-message.success { color:var(--success); background:#e8f7f3; }.consolidation-selection-message.warn { color:var(--warning); background:#fff4e5; }
|
||||
.consolidation-box-info { padding-bottom:10px; }.consolidation-box { padding:12px 0; border-bottom:1px solid #edf1f5; }.consolidation-box:last-child { border-bottom:0; }.consolidation-box-head { display:flex; align-items:center; justify-content:space-between; gap:8px; }.consolidation-box-mark { color:#1769e0; font-size:13px; }.consolidation-box-meta { margin-top:4px; color:#627d98; font-size:11px; }.inbound-chip { padding:4px 7px; border:0; border-radius:7px; color:#bd6b10; background:#fff4e5; font-size:10px; cursor:pointer; }.consolidation-empty { padding:16px 0; color:var(--muted); font-size:12px; text-align:center; }.consolidation-box-details.two-box-state { padding-right:1px; padding-left:1px; }
|
||||
.consolidation-sku-heading { margin:16px 0 8px; color:#334e68; font-size:13px; font-weight:700; }.consolidation-sku-table-wrap { overflow-x:auto; border:1px solid var(--border); border-radius:10px; }.consolidation-sku-table { width:100%; border-collapse:collapse; table-layout:fixed; font-size:11px; }.consolidation-sku-table th { padding:10px 6px; color:#627d98; background:#f7f9fc; font-size:10px; text-align:left; white-space:nowrap; }.consolidation-sku-table td { padding:10px 6px; border-top:1px solid #edf1f5; color:#334e68; overflow-wrap:anywhere; }.consolidation-empty-cell { color:var(--muted); text-align:center; }
|
||||
.consolidation-form-grid { display:grid; grid-template-columns:1fr 92px; gap:10px; margin-top:14px; }.consolidation-field span { display:block; margin-bottom:6px; color:#627d98; font-size:11px; }.consolidation-field select,.consolidation-field input { width:100%; height:42px; padding:0 10px; border:1px solid #dfe5ee; border-radius:9px; outline:0; color:#243b53; background:#fff; font-size:13px; }.consolidation-field select:disabled,.consolidation-field input:disabled { color:#94a3b8; background:#f7f9fc; }.consolidation-footer { position:fixed; right:0; bottom:0; left:0; max-width:480px; margin:auto; padding:12px; border-top:1px solid var(--border); background:#fff; }.inbound-btn { width:100%; min-height:44px; border:0; border-radius:10px; color:#fff; background:var(--brand); font-size:14px; font-weight:700; cursor:pointer; }.inbound-btn:disabled { opacity:.55; cursor:not-allowed; }
|
||||
.profile-cover { height:132px; padding:16px; color:#fff; background:linear-gradient(140deg,#123b72,#1769e0); }.profile-cover .page-back { color:#fff; background:rgba(255,255,255,.14); }.profile-card { margin:-49px 12px 12px; padding:18px; border-radius:12px; background:#fff; box-shadow:0 10px 28px rgba(20,49,82,.13); text-align:center; }.profile-avatar { display:flex; align-items:center; justify-content:center; width:68px; height:68px; margin:-50px auto 10px; border:4px solid #fff; border-radius:16px; color:#155eef; background:#eaf2ff; font-size:25px; font-weight:800; }.profile-name { font-size:19px; font-weight:700; }.profile-role { margin-top:4px; color:#728196; font-size:12px; }.profile-stats { display:grid; grid-template-columns:repeat(3,1fr); margin-top:16px; padding-top:14px; border-top:1px solid #edf1f5; }.profile-stat strong { display:block; color:#163b68; font-size:17px; }.profile-stat span { color:#8a97a8; font-size:10px; }.profile-list { margin:12px; padding:2px 14px; border:1px solid #e3e8ef; border-radius:10px; background:#fff; }.profile-list-item { display:flex; gap:12px; align-items:center; padding:15px 0; border-bottom:1px solid #edf1f5; color:#334e68; font-size:13px; }.profile-list-item:last-child { border-bottom:0; }.profile-list-icon { display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; background:#eef4ff; }.profile-list-value { margin-left:auto; color:#829ab1; font-size:12px; }.profile-logout { width:calc(100% - 24px); margin:4px 12px 24px; padding:13px; border:1px solid #fecaca; border-radius:9px; color:#dc2626; background:#fff; font-size:14px; font-weight:600; cursor:pointer; }
|
||||
/* 与原合托页面一致的作业布局与两箱卡片状态。 */
|
||||
.consolidation-page .app-shell { display:flex; flex-direction:column; min-height:100vh; padding-bottom:72px; }.consolidation-page .inbound-content { flex:1; width:100%; padding-bottom:28px; }.consolidation-page .page-back,.consolidation-page .page-user { width:44px; height:44px; }.consolidation-footer { position:fixed; right:0; bottom:0; left:0; z-index:4; max-width:480px; margin:auto; padding:10px 12px max(12px,env(safe-area-inset-bottom)); border-top:1px solid #e4eaf2; background:#fff; box-shadow:0 -4px 14px rgba(23,32,51,.08); }.consolidation-footer .inbound-btn { min-height:48px; }.consolidation-page .inbound-scan input { font-size:16px; }.consolidation-page .inbound-scan input:focus,.consolidation-field select:focus,.consolidation-field input:focus { outline:2px solid rgba(23,105,224,.2); border-color:#1769e0; }.consolidation-scan-card .inbound-card-head { align-items:flex-start; }.consolidation-scan-card .inbound-status { flex:none; }.consolidation-box-info { padding-bottom:14px; }.consolidation-box { padding:12px 0; border-bottom:1px solid #e5ebf3; }.consolidation-box:first-child { padding-top:2px; }.consolidation-box:last-child { padding-bottom:4px; border-bottom:0; }.consolidation-box-head { gap:10px; }.consolidation-box-mark { color:#243b53; font-size:14px; font-weight:700; }.consolidation-box-meta { color:#627d98; font-size:12px; }.consolidation-box .inbound-chip { min-width:44px; min-height:36px; }.consolidation-sku-heading { margin-top:14px; padding-top:12px; border-top:1px solid #e5ebf3; color:#243b53; font-size:13px; font-weight:700; }.consolidation-sku-table-wrap { margin-top:8px; overflow-x:auto; border:0; }.consolidation-sku-table { table-layout:auto; font-size:12px; }.consolidation-sku-table th,.consolidation-sku-table td { padding:10px 8px; border-bottom:1px solid #edf1f5; text-align:left; white-space:nowrap; }.consolidation-sku-table th { color:#627d98; background:#f8fafc; font-weight:600; }.consolidation-sku-table td { color:#243b53; }.consolidation-sku-table td:nth-child(3),.consolidation-sku-table td:nth-child(4) { text-align:right; }.consolidation-form-card { padding-bottom:14px; }.consolidation-form-grid { grid-template-columns:minmax(0,1fr) 112px; margin-top:12px; }.consolidation-field { display:flex; flex-direction:column; gap:6px; color:#627d98; font-size:12px; font-weight:600; }.consolidation-field select,.consolidation-field input { height:44px; padding:0 11px; border-color:#cfd9e7; border-radius:8px; background:#f8fafc; font-size:14px; }.consolidation-selection-message { color:#b42318; }.consolidation-selection-message.warn { background:transparent; }.consolidation-message.success { color:#087568; background:#e8f8f4; }.consolidation-message.error { color:#b42318; background:#fff1f0; } #consolidationBoxDetails.two-box-state { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; padding:8px; border:1px solid #cfe0f7; border-radius:9px; background:#f6f9ff; } #consolidationBoxDetails.two-box-state .consolidation-box { min-width:0; padding:8px; border:1px solid #d9e5f5; border-radius:8px; background:#fff; } #consolidationBoxDetails.two-box-state .consolidation-box-head { flex-direction:column; align-items:flex-start; gap:6px; } #consolidationBoxDetails.two-box-state .consolidation-box .inbound-chip { align-self:flex-end; min-height:30px; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="../domestic-overseas-theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast" id="toast"></div>
|
||||
<div class="inbound-page consolidation-page" id="consolidationPage"><div class="app-shell"><div class="page-topbar"><button class="page-back" type="button" onclick="returnToWorkbench()" aria-label="返回工作台">‹</button><div class="page-title">合托</div><button class="page-user" id="consolidationAvatar" type="button" onclick="openProfile()" aria-label="个人中心">张</button></div><div class="inbound-content"><div class="inbound-card consolidation-scan-card"><div class="inbound-card-head"><div><div class="inbound-card-title">扫描箱唛</div><div class="inbound-card-sub">扫描箱唛加入合托,目的仓需保持一致</div></div><span class="inbound-status" id="consolidationAvailableCount">4 箱</span></div><div class="inbound-scan"><input id="consolidationScanInput" placeholder="扫描或输入箱唛" autocomplete="off"><button type="button" onclick="handleConsolidationScan()" aria-label="扫描箱唛">▦</button></div><div class="consolidation-message" id="consolidationMessage" aria-live="polite"></div></div><div class="inbound-card consolidation-box-info"><div class="inbound-card-head"><div><div class="inbound-card-title">箱子信息</div><div class="inbound-card-sub">已扫描箱唛与箱内 SKU 明细</div></div></div><div id="consolidationBoxDetails"><div class="consolidation-empty">暂无箱子,请先扫描箱唛</div></div><div class="consolidation-sku-heading">SKU 列表</div><div class="consolidation-sku-table-wrap"><table class="consolidation-sku-table" id="consolidationSkuTable"><thead><tr><th>箱唛</th><th>SKU</th><th>每箱数量</th><th>可合托箱数</th></tr></thead><tbody><tr><td colspan="4" class="consolidation-empty-cell">暂无 SKU</td></tr></tbody></table></div></div><div class="inbound-card consolidation-form-card"><div class="inbound-card-head"><div><div class="inbound-card-title">合托信息</div><div class="inbound-card-sub">选择 SKU 并输入本次合托箱数</div></div></div><div class="consolidation-form-grid"><label class="consolidation-field"><span>SKU</span><select id="consolidationSkuSelect" onchange="updateConsolidationSelection()" disabled><option value="">扫描箱唛后选择 SKU</option></select></label><label class="consolidation-field"><span>箱数</span><input id="consolidationCartonCountInput" type="number" min="1" value="1" inputmode="numeric" oninput="updateConsolidationSelection()" disabled></label></div><div class="consolidation-selection-message" id="consolidationSelectionMessage" aria-live="polite"></div></div></div><div class="consolidation-footer"><button class="inbound-btn" id="submitConsolidationButton" type="button" onclick="submitConsolidation()" disabled>合托</button></div></div></div>
|
||||
<div class="profile-page hidden" id="profilePage"><div class="app-shell"><div class="profile-cover"><button class="page-back" type="button" onclick="closeProfile()">‹</button></div><div class="profile-card"><div class="profile-avatar" id="profileAvatar">张</div><div class="profile-name" id="profileName">张伟</div><div class="profile-role" id="profileRole">W001 · 拣货员</div><div class="profile-stats"><div class="profile-stat"><strong>28</strong><span>今日完成</span></div><div class="profile-stat"><strong>156</strong><span>本月任务</span></div><div class="profile-stat"><strong>99.2%</strong><span>作业准确率</span></div></div></div><div class="profile-list"><div class="profile-list-item"><span class="profile-list-icon">🏢</span><span>当前仓库</span><span class="profile-list-value" id="profileWarehouse">洛杉矶仓</span></div><div class="profile-list-item"><span class="profile-list-icon">🕘</span><span>当前班次</span><span class="profile-list-value">白班 08:00–17:00</span></div><div class="profile-list-item"><span class="profile-list-icon">🔐</span><span>修改密码</span><span class="profile-list-value">›</span></div><div class="profile-list-item"><span class="profile-list-icon">⚙️</span><span>系统设置</span><span class="profile-list-value">›</span></div><div class="profile-list-item"><span class="profile-list-icon">ℹ️</span><span>版本信息</span><span class="profile-list-value">V1.0.0</span></div></div><button class="profile-logout" type="button" onclick="handleLogout()">退出登录</button></div></div>
|
||||
<script>
|
||||
const CONSOLIDATION_SOURCE_PALLETS = [
|
||||
{ id:'TP-YW-001', destination:'义乌仓', items:[{ boxNo:'BX-030-01', order:'SO-260901-030', product:'手机壳套装', sku:'YQ-CASE-01', qty:120 },{ boxNo:'BX-030-02', order:'SO-260901-030', product:'USB-C数据线', sku:'YQ-CABLE-02', qty:80 }] },
|
||||
{ id:'TP-YW-002', destination:'义乌仓', items:[{ boxNo:'BX-032-01', order:'SO-260901-032', product:'手机支架', sku:'YQ-HOLDER-03', qty:60 }] },
|
||||
{ id:'TP-YW-003', destination:'义乌仓', items:[{ boxNo:'BX-033-01', order:'SO-260901-033', product:'蓝牙耳机', sku:'YQ-BUDS-08', qty:48 },{ boxNo:'BX-033-02', order:'SO-260901-033', product:'充电底座', sku:'YQ-DOCK-09', qty:36 }] },
|
||||
{ id:'TP-YW-004', destination:'义乌仓', items:[{ boxNo:'BX-034-01', order:'SO-260901-034', product:'平板保护套', sku:'YQ-PAD-10', qty:50 }] },
|
||||
{ id:'TP-NB-001', destination:'宁波仓', items:[{ boxNo:'BX-031-01', order:'SO-260901-031', product:'旅行包', sku:'NH-BAG-04', qty:32 }] }
|
||||
];
|
||||
let consolidatedPallets = [{ id:'HT-260901-001', destination:'义乌仓', sourcePallets:['TP-YW-001','TP-YW-002'], cartonCount:3, pieceCount:260, items:CONSOLIDATION_SOURCE_PALLETS.slice(0,2).flatMap(pallet=>pallet.items.map(item=>({...item,sourcePallet:pallet.id}))) }];
|
||||
let currentConsolidation = null; let consolidationSelection = { sku:'', count:1 }; let currentUser = null; let toastTimer = null;
|
||||
function readCurrentUser() { const params=new URLSearchParams(location.search); if(!params.get('id')) return null; return Object.fromEntries(['id','name','role','warehouse'].map(key=>[key,params.get(key)||''])); }
|
||||
function buildUserParams(user) { return new URLSearchParams({ id:user.id, name:user.name, role:user.role, warehouse:user.warehouse }); }
|
||||
function returnToWorkbench() { const params=buildUserParams(currentUser); params.set('resume','1'); location.href=`../pda-picking.html?${params}`; }
|
||||
function handleLogout() { localStorage.removeItem('pdaCurrentUser'); location.href='../pda-picking.html'; }
|
||||
function showToast(message,type='') { const toast=document.getElementById('toast'); toast.textContent=message; toast.className=`toast show ${type}`; clearTimeout(toastTimer); toastTimer=setTimeout(()=>toast.className='toast',2200); }
|
||||
function allBoxes() { return CONSOLIDATION_SOURCE_PALLETS.flatMap(pallet=>pallet.items.map(item=>({...item,sourcePallet:pallet.id,destination:pallet.destination}))); }
|
||||
function existingOwner(boxNo) { return consolidatedPallets.find(pallet=>pallet.items.some(item=>item.boxNo===boxNo)); }
|
||||
function availableBoxes() { const selected=new Set(currentConsolidation.items.map(item=>item.boxNo)); return allBoxes().filter(item=>!selected.has(item.boxNo)&&!existingOwner(item.boxNo)); }
|
||||
function setMessage(text,type) { const message=document.getElementById('consolidationMessage'); message.textContent=text; message.className=`consolidation-message show ${type}`; }
|
||||
function resetView() { document.getElementById('consolidationScanInput').value=''; document.getElementById('consolidationMessage').className='consolidation-message'; document.getElementById('consolidationSelectionMessage').className='consolidation-selection-message'; }
|
||||
function openConsolidation() { currentConsolidation={id:'',destination:'',items:[]}; consolidationSelection={sku:'',count:1}; resetView(); renderCurrentConsolidation(); setTimeout(()=>document.getElementById('consolidationScanInput').focus(),0); }
|
||||
function handleConsolidationScan() { const input=document.getElementById('consolidationScanInput'); const boxNo=input.value.trim().toUpperCase(); if(!boxNo) return setMessage('请扫描或输入箱唛','error'); const source=allBoxes().find(item=>item.boxNo===boxNo); if(!source) return setMessage('未找到该箱唛,请核对箱唛后重试','error'); const owner=existingOwner(boxNo); if(owner) return setMessage(`该箱唛已归属合托托盘 ${owner.id},不能重复合托`,'error'); if(currentConsolidation.items.some(item=>item.boxNo===boxNo)) return setMessage('该箱唛已加入当前合托,请勿重复扫描','error'); if(currentConsolidation.destination&¤tConsolidation.destination!==source.destination) return setMessage(`目的仓不一致:当前为${currentConsolidation.destination},该箱为${source.destination}`,'error'); currentConsolidation.destination=source.destination; currentConsolidation.items.push(source); input.value=''; setMessage(`箱唛 ${source.boxNo} 已加入当前合托`,'success'); renderCurrentConsolidation(); }
|
||||
function renderCurrentConsolidation() { const items=currentConsolidation.items; const available=availableBoxes(); document.getElementById('consolidationAvailableCount').textContent=`${available.length} 箱`; const details=document.getElementById('consolidationBoxDetails'); details.className=items.length===2?'two-box-state':''; details.innerHTML=items.length?items.map((item,index)=>`<div class="consolidation-box"><div class="consolidation-box-head"><strong class="consolidation-box-mark">${item.boxNo}</strong><button class="inbound-chip warn" type="button" onclick="removeConsolidationItem(${index})">移除</button></div><div class="consolidation-box-meta">${item.product} · ${item.sourcePallet} · ${item.order}</div></div>`).join(''):'<div class="consolidation-empty">暂无箱子,请先扫描箱唛</div>'; const tbody=document.querySelector('#consolidationSkuTable tbody'); tbody.innerHTML=items.length?items.map(item=>`<tr><td>${item.boxNo}</td><td>${item.sku}</td><td>${item.qty}</td><td>${available.length}</td></tr>`).join(''):'<tr><td colspan="4" class="consolidation-empty-cell">暂无 SKU</td></tr>'; const select=document.getElementById('consolidationSkuSelect'); const input=document.getElementById('consolidationCartonCountInput'); const skus=[...new Set(items.map(item=>item.sku))]; if(!skus.includes(consolidationSelection.sku)) consolidationSelection.sku=skus[0]||''; select.innerHTML=skus.length?skus.map(sku=>`<option value="${sku}">${sku}</option>`).join(''):'<option value="">扫描箱唛后选择 SKU</option>'; select.value=consolidationSelection.sku; select.disabled=!skus.length; input.disabled=!skus.length; input.value=String(Math.max(1,Number(consolidationSelection.count)||1)); document.getElementById('submitConsolidationButton').disabled=items.length<2; }
|
||||
function updateConsolidationSelection() { const select=document.getElementById('consolidationSkuSelect'); const input=document.getElementById('consolidationCartonCountInput'); const message=document.getElementById('consolidationSelectionMessage'); if(!select||!input) return; consolidationSelection.sku=select.value; const requested=Number(input.value); const max=Math.max(1,availableBoxes().length); if(!Number.isInteger(requested)||requested<1) { message.textContent='请输入至少 1 箱'; message.className='consolidation-selection-message show'; return; } consolidationSelection.count=Math.min(requested,max); input.value=String(consolidationSelection.count); message.textContent=requested>max?`最多可输入 ${max} 箱`:''; message.className=requested>max?'consolidation-selection-message show':'consolidation-selection-message'; }
|
||||
function removeConsolidationItem(index) { if(!currentConsolidation?.items[index]) return; currentConsolidation.items.splice(index,1); if(!currentConsolidation.items.length) currentConsolidation.destination=''; setMessage('已从当前合托移除该箱','success'); renderCurrentConsolidation(); }
|
||||
function nextConsolidationId(now=new Date()) { const two=value=>String(value).padStart(2,'0'); const prefix=`HT-${String(now.getFullYear()).slice(-2)}${two(now.getMonth()+1)}${two(now.getDate())}-`; const max=consolidatedPallets.reduce((highest,pallet)=>pallet.id.startsWith(prefix)?Math.max(highest,Number.parseInt(pallet.id.slice(prefix.length),10)||0):highest,0); return `${prefix}${String(max+1).padStart(3,'0')}`; }
|
||||
function submitConsolidation() { const items=currentConsolidation?.items||[]; if(items.length<2) return setMessage('至少扫描两个箱唛后才能合托','error'); const saved={id:currentConsolidation.id||nextConsolidationId(),destination:currentConsolidation.destination,sourcePallets:[...new Set(items.map(item=>item.sourcePallet))],cartonCount:items.length,pieceCount:items.reduce((sum,item)=>sum+item.qty,0),items:items.map(item=>({...item}))}; const existingIndex=consolidatedPallets.findIndex(pallet=>pallet.id===saved.id); if(existingIndex>=0) consolidatedPallets[existingIndex]=saved; else consolidatedPallets.push(saved); currentConsolidation=null; showToast(existingIndex>=0?'合托已更新':'合托已提交','success'); setTimeout(returnToWorkbench,1200); }
|
||||
function updateUserUI() { const initial=currentUser.name.slice(0,1); document.getElementById('consolidationAvatar').textContent=initial; document.getElementById('profileAvatar').textContent=initial; document.getElementById('profileName').textContent=currentUser.name; document.getElementById('profileRole').textContent=`${currentUser.id} · ${currentUser.role}`; document.getElementById('profileWarehouse').textContent=currentUser.warehouse; }
|
||||
function openProfile() { document.getElementById('consolidationPage').classList.add('hidden'); document.getElementById('profilePage').classList.remove('hidden'); } function closeProfile() { document.getElementById('profilePage').classList.add('hidden'); document.getElementById('consolidationPage').classList.remove('hidden'); }
|
||||
document.getElementById('consolidationScanInput').addEventListener('keydown',event=>{if(event.key==='Enter')handleConsolidationScan();}); currentUser=readCurrentUser(); if(!currentUser) location.href='../pda-picking.html'; else { updateUserUI(); openConsolidation(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,535 @@
|
||||
/* style: base */
|
||||
:root{--blue:#1668dc;--blue-dark:#0f4da8;--blue-soft:#eaf2ff;--ink:#17324d;--muted:#6f8194;--neutral-0:#fff;--neutral-50:#f8fafc;--neutral-100:#f3f6fa;--neutral-200:#edf1f5;--neutral-300:#dfe7ef;--neutral-500:#6f8194;--neutral-900:#17324d;--bg:var(--neutral-100);--surface:var(--neutral-0);--line:var(--neutral-300);--green:#10856f;--green-soft:#e2f6f0;--orange:#b86612;--orange-soft:#fff1dd;--red:#c63d3d;--red-soft:#fde8e8;--shadow-soft:0 2px 10px rgba(16,24,40,.05);--shadow:0 8px 24px rgba(16,24,40,.08);--shadow-modal:0 12px 40px rgba(16,24,40,.18);--focus-ring:rgba(22,104,220,.28);--tap-highlight:rgba(22,104,220,.12);--safe-top:env(safe-area-inset-top,0px);--safe-right:env(safe-area-inset-right,0px);--safe-bottom:env(safe-area-inset-bottom,0px);--safe-left:env(safe-area-inset-left,0px);--radius:14px}
|
||||
*{box-sizing:border-box;-webkit-tap-highlight-color:var(--tap-highlight)}html,body{margin:0;min-height:100%;background:var(--bg);color:var(--ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Noto Sans SC",Arial,sans-serif}body{max-width:480px;margin:0 auto;padding:var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left)}button,input,select{font:inherit;min-height:44px}button{cursor:pointer;border:0}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--focus-ring);outline-offset:2px}.hidden{display:none!important}
|
||||
.app{min-height:100vh;background:var(--bg)}
|
||||
.topbar{position:sticky;top:0;z-index:20;background:#fff;border-bottom:1px solid var(--line);padding:12px 18px;box-shadow:var(--shadow-soft)}.topbar-row{display:flex;align-items:center;justify-content:space-between;gap:12px}.brand{display:flex;align-items:center;gap:10px}.brand-mark{display:grid;place-items:center;width:38px;height:38px;border-radius:10px;background:var(--blue);color:white;font-weight:800}.brand-title{font-size:16px;font-weight:800;letter-spacing:.01em}.brand-sub{font-size:11px;color:var(--muted);margin-top:2px}.top-actions{display:flex;align-items:center;gap:8px}.lang-pill{display:flex;gap:2px;padding:3px;background:#f1f5f9;border-radius:9px}.lang-pill button{background:transparent;color:var(--muted);padding:7px 8px;border-radius:7px;font-size:12px}.lang-pill button.active{background:#fff;color:var(--blue);font-weight:700;box-shadow:var(--shadow-soft)}.avatar{width:44px;height:44px;border-radius:50%;background:var(--blue-soft);color:var(--blue);font-weight:800}.back-btn{width:44px;height:44px;flex:0 0 44px;border-radius:9px;background:#f2f6fb;color:var(--ink);font-size:20px;line-height:1}.page-heading{display:flex;align-items:center;gap:10px;min-width:0}.page-heading h1{font-size:21px;margin:0}.page-heading p{font-size:12px;color:var(--muted);margin:4px 0 0}.content{padding:20px;max-width:980px;margin:0 auto}.content.narrow{max-width:640px}
|
||||
.login-wrap{min-height:100vh;display:grid;place-items:center;padding:22px}.login-card{width:min(430px,100%);background:#fff;border:1px solid var(--line);border-radius:20px;padding:30px 26px;box-shadow:var(--shadow)}.login-card .brand{margin-bottom:26px}.login-card h1{font-size:25px;margin:0 0 8px}.login-card .intro{color:var(--muted);font-size:13px;line-height:1.6;margin:0 0 22px}.field{display:grid;gap:7px;margin-bottom:14px}.field label{font-size:12px;font-weight:700;color:#496176}.field input,.field select{width:100%;height:46px;border:1px solid var(--line);border-radius:10px;padding:0 13px;background:#fff;color:var(--ink);outline:none}.field input:focus,.field select:focus{border-color:var(--blue);box-shadow:0 0 0 3px var(--blue-soft)}.login-error{min-height:18px;color:var(--red);font-size:12px;margin:4px 0 8px}.login-btn{width:100%;height:48px;border-radius:11px;background:var(--blue);color:#fff;font-weight:800}.demo-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:15px}.demo-row button{padding:8px 10px;border:1px solid var(--line);background:#fff;border-radius:8px;color:var(--muted);font-size:12px}.demo-tip{color:var(--muted);font-size:11px;line-height:1.5;margin-top:16px}
|
||||
.welcome{background:var(--blue);color:#fff;border-radius:16px;padding:20px;display:flex;justify-content:space-between;gap:12px;align-items:flex-start;box-shadow:0 9px 22px rgba(16,24,40,.12)}.welcome-kicker{font-size:12px;opacity:.78}.welcome-name{font-size:24px;font-weight:800;margin-top:4px}.welcome-meta{font-size:12px;opacity:.86;margin-top:8px}.shift{padding:8px 10px;border-radius:8px;background:rgba(255,255,255,.15);font-size:11px;white-space:nowrap}.metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:16px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:13px;padding:14px 12px;min-width:0}.metric-label{font-size:12px;color:var(--muted);line-height:1.35}.metric-value{font-size:26px;font-weight:800;margin-top:8px;color:var(--ink)}.metric.warn .metric-value{color:var(--orange)}.metric.good .metric-value{color:var(--green)}.metric.blue .metric-value{color:var(--blue)}.section-title{display:flex;justify-content:space-between;align-items:center;margin:24px 0 10px}.section-title h2{font-size:15px;margin:0}.section-title span{font-size:11px;color:var(--muted)}.entry-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.entry{position:relative;text-align:left;background:#fff;border:1px solid var(--line);border-radius:13px;padding:15px;min-height:110px;box-shadow:0 3px 10px rgba(16,24,40,.04)}.entry:hover{border-color:#aac7f5}.entry .entry-icon{font-size:22px}.entry h3{font-size:14px;margin:11px 0 4px}.entry p{font-size:11px;color:var(--muted);line-height:1.4;margin:0}.entry .badge{position:absolute;right:10px;top:10px;padding:4px 6px;border-radius:6px;background:var(--orange-soft);color:var(--orange);font-size:10px}.entry.disabled{opacity:.6;background:#f9fbfd}.entry.disabled:after{content:"";position:absolute;inset:0;background:transparent}.task-strip{margin-top:18px;border-top:1px solid var(--line);padding-top:16px}.task-row{display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--line);border-radius:11px;padding:12px;margin-bottom:8px}.task-dot{width:9px;height:9px;border-radius:50%;background:var(--orange);flex:0 0 auto}.task-main{flex:1;min-width:0}.task-main strong{font-size:13px}.task-main p{font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:4px 0 0}.task-row button{background:#f0f5fc;color:var(--blue);border-radius:7px;padding:8px 10px;font-size:11px}
|
||||
.toolbar{display:flex;justify-content:space-between;gap:10px;align-items:center;margin:18px 0 12px}.toolbar .subtle{font-size:12px;color:var(--muted)}.search-row{display:flex;gap:8px;margin:12px 0}.search-row input{flex:1;min-width:0;height:44px;border:1px solid var(--line);border-radius:10px;padding:0 12px;background:#fff;outline:none}.search-row button{height:44px;padding:0 14px;border-radius:10px;background:var(--blue);color:#fff;font-weight:700;font-size:12px}.list{display:grid;gap:10px}.list-card{background:#fff;border:1px solid var(--line);border-radius:13px;padding:15px}.list-top{display:flex;justify-content:space-between;gap:10px}.list-title{font-size:14px;font-weight:800}.list-meta{font-size:11px;color:var(--muted);margin-top:5px;line-height:1.45}.status{display:inline-flex;align-items:center;padding:4px 7px;border-radius:6px;font-size:10px;font-weight:700;white-space:nowrap}.status.good{color:var(--green);background:var(--green-soft)}.status.wait{color:var(--orange);background:var(--orange-soft)}.status.bad{color:var(--red);background:var(--red-soft)}.status.blue{color:var(--blue);background:var(--blue-soft)}.list-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:13px 0}.list-grid div{background:#f7f9fc;border-radius:8px;padding:8px}.list-grid span{display:block;color:var(--muted);font-size:10px}.list-grid strong{display:block;font-size:12px;margin-top:3px}.card-actions{display:flex;justify-content:flex-end;gap:8px;border-top:1px solid #edf1f5;padding-top:11px}.btn{min-height:44px;border-radius:9px;padding:0 14px;font-size:12px;font-weight:700}.btn.primary{background:var(--blue);color:#fff}.btn.secondary{background:#eef4fb;color:var(--blue)}.btn.ghost{background:#fff;border:1px solid var(--line);color:var(--muted)}.btn.danger{background:var(--red-soft);color:var(--red)}.btn:disabled{opacity:.45;cursor:not-allowed}
|
||||
.work-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px}.work-card+.work-card{margin-top:10px}.work-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}.work-head h2{font-size:16px;margin:0}.work-head p{font-size:11px;color:var(--muted);margin:5px 0 0;line-height:1.5}.progress{height:8px;background:#edf2f7;border-radius:99px;overflow:hidden;margin:14px 0 8px}.progress span{display:block;height:100%;background:var(--green);border-radius:99px}.progress-line{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}.scan-box{display:flex;gap:8px;margin:14px 0}.scan-box input{flex:1;min-width:0;height:48px;border:2px solid #cbd8e6;border-radius:10px;padding:0 13px;outline:none}.scan-box input:focus{border-color:var(--blue)}.scan-box button{width:50px;border-radius:10px;background:var(--blue);color:#fff;font-size:20px}.chips{display:flex;flex-wrap:wrap;gap:6px}.chip{font-size:10px;background:#f1f5f8;color:var(--muted);padding:6px 8px;border-radius:7px}.chip.blue{background:var(--blue-soft);color:var(--blue)}.chip.green{background:var(--green-soft);color:var(--green)}.chip.orange{background:var(--orange-soft);color:var(--orange)}.chip.red{background:var(--red-soft);color:var(--red)}.detail-list{display:grid;gap:8px;margin-top:12px}.detail-row{display:flex;justify-content:space-between;gap:12px;border-bottom:1px solid #edf1f5;padding-bottom:8px;font-size:12px}.detail-row span{color:var(--muted)}.detail-row strong{text-align:right;word-break:break-word}.scan-result{margin-top:10px;padding:10px 11px;border-radius:9px;background:var(--green-soft);color:var(--green);font-size:12px;line-height:1.45}.scan-result.error{background:var(--red-soft);color:var(--red)}.scan-result.info{background:var(--blue-soft);color:var(--blue)}.table-wrap{overflow:auto;margin-top:12px;border:1px solid var(--line);border-radius:10px}.table{width:100%;border-collapse:collapse;min-width:680px}.table th,.table td{font-size:11px;text-align:left;padding:10px;border-bottom:1px solid #edf1f5;white-space:nowrap}.table th{color:var(--muted);font-weight:700;background:#f8fafc}.table tr:last-child td{border-bottom:0}.empty{text-align:center;padding:34px 14px;color:var(--muted);font-size:12px}.bottom-bar{position:sticky;bottom:0;background:rgba(243,246,250,.96);padding:12px 0 2px;margin-top:16px}.bottom-bar .btn{width:100%;height:48px}.modal-backdrop{position:fixed;inset:0;background:rgba(16,24,40,.42);display:grid;place-items:end center;z-index:30;padding:14px}.modal{background:#fff;width:min(520px,100%);border-radius:16px;padding:20px;box-shadow:var(--shadow-modal);max-height:88vh;overflow:auto}.modal h2{font-size:17px;margin:0 0 13px}.modal p{font-size:12px;color:var(--muted);line-height:1.5}.modal-actions{display:flex;gap:8px;margin-top:18px}.modal-actions .btn{flex:1}.toast{position:fixed;left:50%;bottom:24px;z-index:50;transform:translateX(-50%);background:#17324d;color:#fff;padding:11px 14px;border-radius:9px;font-size:12px;box-shadow:var(--shadow);max-width:min(92vw,420px);text-align:center}.toast.good{background:var(--green)}.toast.bad{background:var(--red)}.toast.info{background:var(--blue-dark)}
|
||||
.profile-grid{display:grid;gap:10px}.profile-line{display:flex;justify-content:space-between;padding:13px 0;border-bottom:1px solid var(--line);font-size:13px}.profile-line span{color:var(--muted)}.profile-line strong{text-align:right}.lang-select{display:flex;gap:6px}.lang-select button{padding:7px 9px;border-radius:7px;background:#f0f4f8;color:var(--muted);font-size:11px}.lang-select button.active{background:var(--blue-soft);color:var(--blue);font-weight:700}
|
||||
@media(max-width:640px){body{max-width:100%}.content{padding:14px}.topbar{padding:10px 14px}.brand-sub{display:none}.welcome{padding:17px}.welcome-name{font-size:21px}.metrics{gap:7px}.metric{padding:11px 9px}.metric-label{font-size:10px}.metric-value{font-size:23px}.entry-grid{grid-template-columns:repeat(2,1fr)}.entry{min-height:102px;padding:13px}.entry h3{font-size:13px;margin-top:9px}.list-grid{grid-template-columns:repeat(2,1fr)}.toolbar{align-items:flex-start;flex-direction:column}.toolbar .btn{width:100%}.page-heading h1{font-size:19px}.table-wrap{margin-left:-2px;margin-right:-2px}}
|
||||
@media(min-width:641px){.content{padding-top:26px}.login-wrap{padding:40px}.modal-backdrop{align-items:center}}
|
||||
.password-wrap{position:relative}.password-wrap input{padding-right:68px}.password-wrap button{position:absolute;right:6px;top:6px;height:34px;padding:0 9px;border-radius:7px;background:#eef4fb;color:var(--blue);font-size:11px;font-weight:700}
|
||||
.photo-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:14px}.photo-step{min-height:126px;border:1px solid var(--line);border-radius:11px;background:#f8fafc;padding:12px;display:flex;flex-direction:column;justify-content:space-between}.photo-step.done{background:var(--green-soft);border-color:#a9dfd1}.photo-preview{height:52px;border-radius:8px;background:#e8eef5;display:grid;place-items:center;color:var(--muted);font-size:20px;overflow:hidden}.photo-preview img{width:100%;height:100%;object-fit:cover}.photo-step strong{font-size:12px;margin:8px 0}.photo-step label{position:relative;display:grid;place-items:center;min-height:44px;text-align:center;padding:8px;border-radius:7px;background:var(--blue);color:#fff;font-size:11px;font-weight:700;cursor:pointer}.photo-step label:focus-within{outline:3px solid var(--focus-ring);outline-offset:2px}.photo-step input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}@media(max-width:360px){.photo-grid{grid-template-columns:1fr}}
|
||||
|
||||
/* style: ios-work-surface */
|
||||
:root{
|
||||
--blue:#0a66d8;
|
||||
--blue-dark:#084faa;
|
||||
--blue-soft:#eaf3ff;
|
||||
--ink:#475467;
|
||||
--muted:#65758b;
|
||||
--bg:#f2f4f7;
|
||||
--surface:#fff;
|
||||
--line:#e4e7ec;
|
||||
--green:#087a62;
|
||||
--green-soft:#e8f7f2;
|
||||
--orange:#a75b0b;
|
||||
--orange-soft:#fff3e3;
|
||||
--red:#b42318;
|
||||
--red-soft:#fff0ee;
|
||||
--shadow-soft:0 2px 8px rgba(16,24,40,.06);
|
||||
--shadow:0 8px 24px rgba(16,24,40,.08);
|
||||
--shadow-modal:0 20px 48px rgba(16,24,40,.18);
|
||||
--radius:14px;
|
||||
}
|
||||
html{background:#e7ebf0}
|
||||
body{width:100%;max-width:520px;font-size:15px;line-height:1.45;overflow-x:hidden}
|
||||
.app{min-height:calc(100dvh - var(--safe-top) - var(--safe-bottom));background:var(--bg)}
|
||||
button,input,select{font-size:16px}
|
||||
button{transition:background-color .16s ease,transform .16s ease,opacity .16s ease}
|
||||
button:active:not(:disabled){transform:scale(.98)}
|
||||
button:disabled{opacity:.42}
|
||||
|
||||
.topbar{top:0;min-height:60px;padding:8px 16px;background:rgba(255,255,255,.96);border-color:var(--line);box-shadow:none;backdrop-filter:saturate(160%) blur(16px)}
|
||||
.topbar-row{min-height:44px;gap:10px}
|
||||
.page-heading{flex:1;gap:8px;overflow:hidden}
|
||||
.page-heading>div{min-width:0}
|
||||
.page-heading h1{font-size:19px;line-height:1.2;font-weight:750;letter-spacing:0;white-space:normal;overflow-wrap:anywhere}
|
||||
.page-heading p{font-size:12px;line-height:1.25;white-space:normal;overflow-wrap:anywhere}
|
||||
.back-btn{display:grid;place-items:center;background:transparent;color:var(--blue);font-size:31px;font-weight:400}
|
||||
.top-actions{flex:0 0 auto;gap:6px}
|
||||
.avatar{border:1px solid #d8e8ff;background:var(--blue-soft);font-size:14px}
|
||||
.lang-pill{gap:0;padding:2px;border-radius:10px;background:#eef1f5}
|
||||
.lang-pill button{min-width:44px;padding:0 7px;border-radius:8px;color:#596579;font-size:12px}
|
||||
.lang-pill button.active{color:var(--blue);background:var(--surface);box-shadow:0 1px 3px rgba(16,24,40,.09)}
|
||||
.content{width:100%;max-width:680px;padding:18px 16px calc(24px + var(--safe-bottom))}
|
||||
|
||||
.login-wrap{min-height:calc(100dvh - var(--safe-top) - var(--safe-bottom));padding:24px 18px}
|
||||
.login-card{width:min(420px,100%);padding:26px 22px;border-color:var(--line);border-radius:16px;box-shadow:var(--shadow)}
|
||||
.login-card .brand{margin-bottom:24px}
|
||||
.brand-mark{width:44px;height:44px;border-radius:12px}
|
||||
.brand-title{font-size:17px;letter-spacing:0}
|
||||
.brand-sub{font-size:12px}
|
||||
.login-card h1{font-size:25px;line-height:1.22;letter-spacing:0}
|
||||
.login-card .intro{font-size:14px;line-height:1.55;margin-bottom:20px}
|
||||
.field{gap:7px;margin-bottom:15px}
|
||||
.field label{font-size:13px;color:#475467}
|
||||
.field input,.field select{height:50px;border-color:#d0d5dd;border-radius:11px;padding-inline:14px;font-size:16px}
|
||||
.field input:focus,.field select:focus{border-color:var(--blue);box-shadow:0 0 0 3px var(--focus-ring)}
|
||||
.login-btn{height:50px;border-radius:11px;font-size:16px}
|
||||
.demo-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
|
||||
.demo-row button{min-width:0;padding:8px;border-color:var(--line);background:#f8fafc;color:#475467;font-size:12px;white-space:normal}
|
||||
.demo-tip{font-size:12px}
|
||||
|
||||
.welcome{padding:18px;border-radius:16px;background:var(--blue);box-shadow:0 7px 18px rgba(16,24,40,.12)}
|
||||
.welcome-kicker{font-size:13px}
|
||||
.welcome-name{font-size:24px;letter-spacing:0}
|
||||
.welcome-meta{font-size:12px;line-height:1.4}
|
||||
.shift{min-height:32px;padding:7px 9px;border:1px solid rgba(255,255,255,.22);border-radius:9px;background:rgba(255,255,255,.12);font-size:11px;white-space:normal;text-align:center}
|
||||
.metrics{gap:8px;margin:12px 0 20px}
|
||||
.metric{min-height:88px;padding:12px 10px;border-color:var(--line);border-radius:12px;box-shadow:none}
|
||||
.metric-label{font-size:12px;line-height:1.35;overflow-wrap:anywhere}
|
||||
.metric-value{font-size:25px;line-height:1.05;margin-top:8px}
|
||||
.section-title{margin:23px 2px 10px}
|
||||
.section-title h2{font-size:17px;line-height:1.25}
|
||||
.section-title span{font-size:12px}
|
||||
.entry-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
|
||||
.entry{min-width:0;min-height:124px;padding:14px;border-color:var(--line);border-radius:14px;box-shadow:none}
|
||||
.entry:hover{border-color:var(--line)}
|
||||
.entry:active{background:#f8fafc}
|
||||
.entry .entry-icon{display:grid;place-items:center;width:36px;height:36px;border-radius:10px;background:var(--blue-soft);color:var(--blue);font-size:19px;font-weight:700}
|
||||
.entry h3{font-size:15px;line-height:1.25;margin:12px 0 5px;overflow-wrap:anywhere}
|
||||
.entry p{font-size:12px;line-height:1.4;overflow-wrap:anywhere}
|
||||
.entry-grid .entry p{display:none}
|
||||
.inbound-entry-grid{max-width:none}
|
||||
.outbound-entry-grid{max-width:none}
|
||||
.internal-entry-grid{max-width:none}
|
||||
.section-title h2{font-size:15px;line-height:1.2;white-space:nowrap}
|
||||
.section-title{margin:16px 2px 8px}
|
||||
.page-heading h1{font-size:17px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.page-heading p{display:none}
|
||||
.entry-grid .entry{text-align:center;border-radius:10px}
|
||||
.entry-grid .entry .entry-icon{margin-inline:auto}
|
||||
.entry-grid .entry h3{font-size:12px;line-height:1.2;color:#667085;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.entry .badge{top:12px;right:12px;min-width:24px;text-align:center;border-radius:99px;font-size:11px}
|
||||
.entry.merge-entry .entry-icon{background:var(--blue-soft);color:var(--blue)}
|
||||
.task-strip{margin-top:22px;padding-top:18px;border-color:var(--line)}
|
||||
.task-row{min-width:0;padding:12px 13px;border-color:var(--line);border-radius:12px;margin-bottom:8px;box-shadow:none}
|
||||
.task-main strong{font-size:14px;overflow-wrap:anywhere}
|
||||
.task-main p{font-size:12px;white-space:normal;overflow:visible;text-overflow:clip;line-height:1.35}
|
||||
.task-row button{flex:0 0 auto;min-width:44px;min-height:44px;padding:0 12px;border-radius:10px;background:var(--blue-soft);font-size:12px;font-weight:700}
|
||||
|
||||
.toolbar{margin:4px 0 12px;min-height:32px}
|
||||
.toolbar .subtle{font-size:13px}
|
||||
.list{gap:10px}
|
||||
.list-card,.work-card{border-color:var(--line);border-radius:14px;background:var(--surface);box-shadow:none}
|
||||
.list-card{padding:15px}
|
||||
.list-top{align-items:flex-start}
|
||||
.list-title{font-size:15px;line-height:1.3;overflow-wrap:anywhere}
|
||||
.list-meta{font-size:12px;line-height:1.5;overflow-wrap:anywhere}
|
||||
.status{max-width:100%;padding:5px 8px;border-radius:99px;font-size:11px;line-height:1.25;white-space:normal;text-align:center;overflow-wrap:anywhere}
|
||||
.list-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:13px 0}
|
||||
.list-grid div{min-width:0;padding:9px;border-radius:9px;background:#f7f8fa}
|
||||
.list-grid span{font-size:11px;line-height:1.25}
|
||||
.list-grid strong{font-size:13px;line-height:1.35;overflow-wrap:anywhere}
|
||||
.card-actions{gap:8px;padding-top:12px;border-color:var(--line);flex-wrap:wrap}
|
||||
.btn{min-height:44px;border-radius:10px;padding:0 14px;font-size:13px}
|
||||
.btn.primary{background:var(--blue)}
|
||||
.btn.secondary{background:var(--blue-soft)}
|
||||
.btn.ghost{border-color:var(--line);background:#fff;color:#475467}
|
||||
|
||||
.work-card{padding:16px}
|
||||
.work-card+.work-card{margin-top:12px}
|
||||
.work-head h2{font-size:17px;line-height:1.3}
|
||||
.work-head p{font-size:12px;line-height:1.45;overflow-wrap:anywhere}
|
||||
.detail-list{gap:0;margin-top:13px}
|
||||
.detail-row{align-items:flex-start;min-height:42px;padding:10px 0;border-color:var(--line);font-size:13px;line-height:1.4}
|
||||
.detail-row:last-child{border-bottom:0}
|
||||
.detail-row span{flex:0 0 38%}
|
||||
.detail-row strong{min-width:0;overflow-wrap:anywhere}
|
||||
.scan-box{gap:8px;margin:16px 0 12px}
|
||||
.scan-box input{height:50px;border:1px solid #b9c4d1;border-radius:11px;font-size:16px}
|
||||
.scan-box button{width:50px;height:50px;border-radius:11px;background:var(--blue)}
|
||||
.scan-result{padding:12px;border-radius:10px;font-size:13px}
|
||||
.progress{height:7px;margin:16px 0 8px;background:#e9edf2}
|
||||
.progress span{background:var(--blue)}
|
||||
.progress-line{font-size:12px}
|
||||
.chip{min-height:32px;font-size:11px}
|
||||
.search-row{gap:8px;margin:8px 0 14px}
|
||||
.search-row input,.search-row button{height:48px}
|
||||
|
||||
.table-wrap{margin-top:13px;border-color:var(--line);border-radius:11px;background:#fff;-webkit-overflow-scrolling:touch}
|
||||
.table{min-width:620px}
|
||||
.table th,.table td{padding:12px 10px;border-color:var(--line);font-size:12px}
|
||||
.table th{background:#f7f8fa}
|
||||
.empty{padding:36px 16px;font-size:13px}
|
||||
.express-order-list{display:grid;gap:10px;margin-top:13px}
|
||||
.express-order-card{background:#fff;border:1px solid var(--line);border-radius:11px;padding:12px}
|
||||
.express-order-row{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;min-width:0}
|
||||
.express-order-row+.express-order-row{grid-template-columns:minmax(0,.75fr) minmax(0,1.25fr) auto 72px;align-items:end;margin-top:11px;padding-top:11px;border-top:1px solid var(--line)}
|
||||
.express-order-field{min-width:0}
|
||||
.express-order-field>span{display:block;color:var(--muted);font-size:10px;line-height:1.25;margin-bottom:3px}
|
||||
.express-order-field>strong{display:block;min-width:0;font-size:12px;line-height:1.35;overflow-wrap:anywhere;word-break:break-word}
|
||||
.express-order-action{min-width:72px}
|
||||
.express-order-action .btn{width:100%;padding-inline:10px}
|
||||
.express-scan-card .scan-box{margin-bottom:0}
|
||||
.express-quantity-field{margin-top:14px}
|
||||
.express-quantity-field input{width:100%;height:50px;border:1px solid #b9c4d1;border-radius:11px;padding:0 14px;background:#fff;color:var(--text);font:inherit;font-size:16px}
|
||||
.express-photo-row{display:flex;align-items:flex-start;gap:12px;margin-top:14px}
|
||||
.express-waybill-preview{width:92px;height:92px;border-radius:11px;background:#e9edf2;display:grid;place-items:center;overflow:hidden;color:var(--muted)}
|
||||
.express-waybill-preview img{width:100%;height:100%;object-fit:cover}
|
||||
.express-waybill-preview svg{width:26px;height:26px;stroke:currentColor;fill:none}
|
||||
.express-photo-add{position:relative;display:grid;place-items:center;width:92px;height:92px;border:1px dashed #98a2b3;border-radius:11px;background:#fff;color:var(--blue);font-size:34px;line-height:1;cursor:pointer}
|
||||
.express-photo-add input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}
|
||||
.express-photo-add:focus-within{outline:3px solid var(--focus-ring);outline-offset:2px}
|
||||
.app[data-page="express-signin-detail"]{display:flex;min-height:100dvh;flex-direction:column}
|
||||
.app[data-page="express-signin-detail"] .content{display:flex;flex:1;flex-direction:column}
|
||||
.app[data-page="express-signin-detail"] .bottom-bar{margin-top:auto}
|
||||
@media(max-width:639px){
|
||||
.express-order-row{gap:8px}
|
||||
.express-order-row+.express-order-row{grid-template-columns:minmax(0,.7fr) minmax(0,1.2fr) auto 72px;gap:7px}
|
||||
.express-order-row+.express-order-row .express-order-field>strong{font-size:11px}
|
||||
}
|
||||
.photo-grid{grid-template-columns:1fr;gap:10px}
|
||||
.photo-step{min-height:150px;padding:13px;border-color:var(--line);border-radius:12px;background:#f8fafc}
|
||||
.photo-preview{height:70px;border-radius:9px;background:#e9edf2}
|
||||
.photo-step strong{font-size:13px;line-height:1.35;overflow-wrap:anywhere}
|
||||
.photo-step label{border-radius:10px;font-size:13px}
|
||||
.bottom-bar{z-index:12;margin:18px -16px calc(-24px - var(--safe-bottom));padding:12px 16px calc(12px + var(--safe-bottom));background:rgba(242,244,247,.96);border-top:1px solid var(--line);backdrop-filter:saturate(150%) blur(14px)}
|
||||
.bottom-bar>div{align-items:stretch;flex-wrap:wrap}
|
||||
.bottom-bar .btn{height:50px;min-width:120px;font-size:14px}
|
||||
.modal-backdrop{padding:max(12px,var(--safe-top)) max(12px,var(--safe-right)) calc(12px + var(--safe-bottom)) max(12px,var(--safe-left));background:rgba(16,24,40,.46)}
|
||||
.modal{padding:20px;border-radius:16px;box-shadow:var(--shadow-modal)}
|
||||
.modal h2{font-size:18px}
|
||||
.modal p{font-size:14px}
|
||||
.toast{bottom:calc(20px + var(--safe-bottom));border-radius:11px;box-shadow:var(--shadow)}
|
||||
.password-wrap button,.lang-select button{min-width:44px;min-height:44px}
|
||||
|
||||
@media (max-width:359px){
|
||||
.topbar{padding-inline:12px}
|
||||
.top-actions{gap:4px}
|
||||
.topbar-row:has(.back-btn) .avatar{display:none}
|
||||
.lang-pill button{min-width:44px;padding-inline:5px}
|
||||
.page-heading h1{font-size:17px}
|
||||
.content{padding-inline:12px}
|
||||
.welcome{padding:15px}
|
||||
.welcome-name{font-size:21px}
|
||||
.metrics{gap:6px}
|
||||
.metric{padding:10px 7px}
|
||||
.metric-label{font-size:11px}
|
||||
.metric-value{font-size:22px}
|
||||
.entry-grid{max-width:288px;margin-inline:auto;gap:8px}
|
||||
.inbound-entry-grid,.outbound-entry-grid{max-width:none;grid-template-columns:repeat(3,minmax(0,1fr));margin-inline:0}
|
||||
.inbound-entry-grid .entry,.outbound-entry-grid .entry{min-height:92px;padding:10px 6px}
|
||||
.internal-entry-grid{max-width:none;grid-template-columns:repeat(3,minmax(0,1fr));margin-inline:0}
|
||||
.internal-entry-grid .entry{min-height:92px;padding:10px 6px}
|
||||
.entry{min-height:92px;padding:11px}
|
||||
.entry h3{font-size:14px}
|
||||
.bottom-bar{margin-inline:-12px;padding-inline:12px}
|
||||
}
|
||||
@media (min-width:360px) and (max-width:639px){
|
||||
.entry-grid{max-width:348px;margin-inline:auto;gap:8px}
|
||||
.inbound-entry-grid{max-width:none;grid-template-columns:repeat(3,minmax(0,1fr));margin-inline:0}
|
||||
.inbound-entry-grid .entry{min-height:94px;padding:11px 8px}
|
||||
.outbound-entry-grid{max-width:none;grid-template-columns:repeat(3,minmax(0,1fr));margin-inline:0}
|
||||
.outbound-entry-grid .entry{min-height:94px;padding:11px 8px}
|
||||
.internal-entry-grid{max-width:none;grid-template-columns:repeat(3,minmax(0,1fr));margin-inline:0}
|
||||
.internal-entry-grid .entry{min-height:94px;padding:11px 8px}
|
||||
.entry{min-height:94px;padding:12px}
|
||||
.entry h3{margin-top:10px}
|
||||
}
|
||||
@media (min-width:390px){
|
||||
.photo-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
}
|
||||
@media (min-width:640px){
|
||||
body{max-width:700px}
|
||||
.content{padding:24px 22px calc(28px + var(--safe-bottom))}
|
||||
.topbar{padding-inline:22px}
|
||||
.entry-grid{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.bottom-bar{margin-inline:-22px;padding-inline:22px}
|
||||
}
|
||||
@media (min-width:768px){
|
||||
body{max-width:760px}
|
||||
.content{max-width:720px}
|
||||
.list-grid{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
}
|
||||
@media (hover:hover) and (pointer:fine){
|
||||
.entry:hover,.list-card:hover{border-color:#cdd5df;background:#fcfcfd}
|
||||
.btn.primary:hover,.login-btn:hover,.scan-box button:hover{background:var(--blue-dark)}
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
*,*::before,*::after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}
|
||||
}
|
||||
|
||||
/* style: ios-icon-system */
|
||||
.entry .entry-icon{position:relative;isolation:isolate;width:40px;height:40px;border-radius:10px;font-size:0;line-height:0;box-shadow:0 2px 5px rgba(16,24,40,.07),inset 0 0 0 1px rgba(16,24,40,.025);transition:transform .16s cubic-bezier(.2,.8,.2,1),box-shadow .16s ease}
|
||||
.entry .entry-icon::after{content:"";position:absolute;z-index:-1;inset:1px;border-radius:9px;box-shadow:inset 0 1px 0 rgba(255,255,255,.82);pointer-events:none}
|
||||
.entry .entry-icon svg{width:22px;height:22px;display:block;stroke:currentColor;stroke-width:1.75;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
||||
.entry .entry-icon .icon-fill{fill:currentColor;stroke:none;opacity:.11}
|
||||
.entry:active .entry-icon{transform:scale(.94);box-shadow:0 1px 2px rgba(16,24,40,.08),inset 0 0 0 1px rgba(16,24,40,.04)}
|
||||
.entry.receive-entry .entry-icon{background:var(--orange-soft);color:var(--orange)}
|
||||
.entry.pallet-entry .entry-icon{background:var(--blue-soft);color:var(--blue)}
|
||||
.entry.pick-entry .entry-icon{background:var(--orange-soft);color:var(--orange)}
|
||||
.entry.review-entry .entry-icon{background:var(--green-soft);color:var(--green)}
|
||||
.entry.handover-entry .entry-icon{background:var(--green-soft);color:var(--green)}
|
||||
.entry.inventory-entry .entry-icon{background:#e8f5f3;color:#26766f}
|
||||
.entry.relabel-entry .entry-icon{background:#eef1ff;color:#4f5fc7}
|
||||
.entry.exception-entry .entry-icon{background:var(--red-soft);color:var(--red)}
|
||||
.entry.more-entry .entry-icon{background:#eef1f5;color:#475467}
|
||||
.outbound-entry-grid{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.back-btn svg{width:21px;height:21px;display:block;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
||||
.scan-box button{display:grid;place-items:center}
|
||||
.scan-box button svg{width:22px;height:22px;display:block;stroke:currentColor;stroke-width:1.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
||||
.photo-preview svg{width:24px;height:24px;display:block;stroke:currentColor;stroke-width:1.7;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
||||
.photo-step.done .photo-preview{color:var(--green);background:#d9f2eb}
|
||||
.photo-step.done .photo-preview svg{stroke-width:2}
|
||||
.photo-capture{width:100%;height:50px;background:var(--blue)!important;color:#fff!important;border:0!important;border-radius:10px}
|
||||
.photo-capture svg{width:20px;height:20px;display:block;flex:0 0 20px;stroke:currentColor;stroke-width:1.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
||||
.photo-capture span{display:block;white-space:nowrap;letter-spacing:0}
|
||||
.photo-add-tile:active{background:#f7f8fa!important}
|
||||
.photo-add-tile:focus-visible{outline:3px solid var(--focus-ring);outline-offset:2px}
|
||||
|
||||
/* style: ios-final-report */
|
||||
:root{
|
||||
--ios-blue:#155eef;
|
||||
--ios-blue-pressed:#004eeb;
|
||||
--ios-blue-soft:#edf4ff;
|
||||
--ios-bg:#f5f5f7;
|
||||
--ios-surface:#fff;
|
||||
--ios-surface-muted:#f9fafb;
|
||||
--ios-ink:#1d1d1f;
|
||||
--ios-secondary:#62626a;
|
||||
--ios-tertiary:#85858d;
|
||||
--ios-border:#e5e5ea;
|
||||
--ios-green:#067a57;
|
||||
--ios-green-soft:#e9f7f1;
|
||||
--ios-orange:#a84f00;
|
||||
--ios-orange-soft:#fff3e5;
|
||||
--ios-red:#b42318;
|
||||
--ios-red-soft:#fff0ee;
|
||||
--ios-indigo:#4f5fc7;
|
||||
--ios-indigo-soft:#eef0ff;
|
||||
--ios-focus:rgba(21,94,239,.28);
|
||||
--blue:var(--ios-blue);
|
||||
--blue-dark:var(--ios-blue-pressed);
|
||||
--blue-soft:var(--ios-blue-soft);
|
||||
--ink:var(--ios-ink);
|
||||
--muted:var(--ios-secondary);
|
||||
--bg:var(--ios-bg);
|
||||
--surface:var(--ios-surface);
|
||||
--line:var(--ios-border);
|
||||
--green:var(--ios-green);
|
||||
--green-soft:var(--ios-green-soft);
|
||||
--orange:var(--ios-orange);
|
||||
--orange-soft:var(--ios-orange-soft);
|
||||
--red:var(--ios-red);
|
||||
--red-soft:var(--ios-red-soft);
|
||||
--focus-ring:var(--ios-focus);
|
||||
--shadow-soft:0 2px 8px rgba(16,24,40,.05);
|
||||
--shadow:0 10px 30px rgba(16,24,40,.1);
|
||||
--shadow-modal:0 22px 56px rgba(16,24,40,.2);
|
||||
}
|
||||
html{background:#eceef1}
|
||||
body{color:var(--ios-ink);font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI","PingFang SC","Noto Sans SC",Arial,sans-serif;font-feature-settings:"tnum" 1,"kern" 1;text-rendering:optimizeLegibility}
|
||||
.app{min-height:100dvh;background:var(--ios-bg)}
|
||||
button,input,select{min-height:44px;color:inherit;letter-spacing:0}
|
||||
button{touch-action:manipulation}
|
||||
button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--ios-focus);outline-offset:2px}
|
||||
button:disabled{cursor:not-allowed;opacity:.42;transform:none!important}
|
||||
|
||||
.topbar{min-height:62px;padding:8px 16px;background:rgba(255,255,255,.94);border-bottom:1px solid rgba(60,60,67,.15);backdrop-filter:saturate(180%) blur(18px);-webkit-backdrop-filter:saturate(180%) blur(18px)}
|
||||
.topbar-row{min-height:45px}
|
||||
.page-heading{gap:6px}
|
||||
.page-heading h1{color:var(--ios-ink);font-size:17px;font-weight:700;line-height:1.2}
|
||||
.page-heading p{display:none}
|
||||
.back-btn{border-radius:10px;color:var(--ios-blue);background:transparent}
|
||||
.back-btn:active{background:var(--ios-blue-soft)}
|
||||
.lang-pill{padding:2px;border-radius:10px;background:#edeef1}
|
||||
.lang-pill button{min-width:44px;min-height:44px;padding:0 7px;border-radius:8px;color:var(--ios-secondary);font-size:12px}
|
||||
.lang-pill button.active{color:var(--ios-blue);background:#fff;box-shadow:0 1px 4px rgba(16,24,40,.1)}
|
||||
.avatar{border:1px solid #d7e5ff;background:var(--ios-blue-soft);color:var(--ios-blue);box-shadow:inset 0 1px 0 rgba(255,255,255,.8)}
|
||||
.content{padding:18px 16px calc(28px + env(safe-area-inset-bottom,0px))}
|
||||
|
||||
.login-wrap{background:var(--ios-surface);padding:20px 18px calc(20px + env(safe-area-inset-bottom,0px))}
|
||||
.login-card{border:0;border-radius:0;background:transparent;box-shadow:none}
|
||||
.brand{gap:12px}.brand-mark{background:var(--ios-blue);box-shadow:0 4px 12px rgba(16,24,40,.14)}
|
||||
.brand-title{color:var(--ios-ink);font-weight:760}.brand-sub{color:var(--ios-secondary)}
|
||||
.login-card h1{color:var(--ios-ink);font-size:26px;font-weight:760}
|
||||
.login-card .intro{color:var(--ios-secondary)}
|
||||
.field label{color:#414149;font-weight:650}
|
||||
.field input,.field select,.search-row input,.scan-box input{border:1px solid #c7c7cc;background:#fff;color:var(--ios-ink);box-shadow:inset 0 1px 0 rgba(0,0,0,.015)}
|
||||
.field input::placeholder,.search-row input::placeholder,.scan-box input::placeholder{color:#777780;opacity:1}
|
||||
.field input:focus,.field select:focus,.search-row input:focus,.scan-box input:focus{border-color:var(--ios-blue);box-shadow:0 0 0 3px var(--ios-focus)}
|
||||
.login-error{color:var(--ios-red);font-size:13px}
|
||||
.login-btn,.btn.primary,.search-row button,.scan-box button{background:var(--ios-blue);color:#fff;box-shadow:0 2px 5px rgba(16,24,40,.12)}
|
||||
.login-btn:active,.btn.primary:active,.search-row button:active,.scan-box button:active{background:var(--ios-blue-pressed)}
|
||||
.demo-row button{border:1px solid var(--ios-border);background:var(--ios-surface-muted);color:#414149}
|
||||
.demo-row button:active{background:#eeeef0}
|
||||
|
||||
.welcome{border-radius:16px;background:var(--ios-blue);box-shadow:0 10px 24px rgba(16,24,40,.13)}
|
||||
.welcome-kicker{color:rgba(255,255,255,.8)}
|
||||
.welcome-name{font-weight:780}.welcome-meta{color:rgba(255,255,255,.86)}
|
||||
.shift{border-color:rgba(255,255,255,.28);background:rgba(255,255,255,.12);color:#fff}
|
||||
.metrics{margin:12px 0 18px}
|
||||
.metric{border:1px solid var(--ios-border);background:var(--ios-surface);box-shadow:0 1px 2px rgba(16,24,40,.025)}
|
||||
.metric-label{color:var(--ios-secondary)}
|
||||
.metric-value{font-weight:760}.metric.warn .metric-value{color:var(--ios-orange)}.metric.good .metric-value{color:var(--ios-green)}.metric.blue .metric-value{color:var(--ios-blue)}
|
||||
.section-title{margin:18px 2px 8px}.section-title h2{color:#3a3a40;font-size:15px;font-weight:730}
|
||||
.entry-grid{gap:8px}
|
||||
.entry{border:1px solid var(--ios-border);border-radius:10px;background:var(--ios-surface);box-shadow:0 1px 2px rgba(16,24,40,.025);transition:background-color .16s ease,border-color .16s ease,transform .16s cubic-bezier(.2,.8,.2,1)}
|
||||
.entry:active{border-color:#d2d2d7;background:#fafafa;transform:scale(.975)}
|
||||
.entry-grid .entry h3{color:#5b5b63;font-size:12px;font-weight:680}
|
||||
.entry .entry-icon{box-shadow:0 3px 8px rgba(16,24,40,.07),inset 0 0 0 1px rgba(16,24,40,.025)}
|
||||
.entry.relabel-entry .entry-icon{background:var(--ios-indigo-soft);color:var(--ios-indigo)}
|
||||
.app[data-page="workbench"] .entry .entry-icon{background:var(--ios-blue-soft);color:var(--ios-blue)}
|
||||
|
||||
.toolbar{margin:2px 0 12px}.toolbar .subtle{color:var(--ios-secondary);font-size:13px}
|
||||
.list{gap:10px}
|
||||
.list-card,.work-card{border:1px solid var(--ios-border);border-radius:12px;background:var(--ios-surface);box-shadow:0 1px 2px rgba(16,24,40,.02)}
|
||||
.list-card{padding:15px}.work-card{padding:16px}
|
||||
.list-title,.work-head h2{color:var(--ios-ink);font-weight:720}
|
||||
.list-meta,.work-head p{color:var(--ios-secondary)}
|
||||
.list-grid div{background:var(--ios-surface-muted)}
|
||||
.list-grid span{color:var(--ios-tertiary)}
|
||||
.list-grid strong{color:#34343a;font-weight:680}
|
||||
.status{border-radius:6px;font-weight:680}
|
||||
.status.good{color:var(--ios-green);background:var(--ios-green-soft)}
|
||||
.status.wait{color:var(--ios-orange);background:var(--ios-orange-soft)}
|
||||
.status.bad{color:var(--ios-red);background:var(--ios-red-soft)}
|
||||
.status.blue{color:var(--ios-blue);background:var(--ios-blue-soft)}
|
||||
.card-actions{border-color:var(--ios-border)}
|
||||
.btn{border-radius:10px;font-weight:680}
|
||||
.btn.secondary{background:var(--ios-blue-soft);color:var(--ios-blue)}
|
||||
.btn.ghost{border:1px solid var(--ios-border);background:#fff;color:#4f4f57}
|
||||
.btn.danger{background:var(--ios-red-soft);color:var(--ios-red)}
|
||||
.btn.secondary:active,.btn.ghost:active{background:#eeeef1}
|
||||
|
||||
.detail-row{border-color:var(--ios-border)}
|
||||
.detail-row span{color:var(--ios-secondary)}
|
||||
.detail-row strong{color:#303036;font-weight:660}
|
||||
.progress{height:7px;background:#e8e8ed}.progress span{background:var(--ios-blue)}
|
||||
.progress-line{color:var(--ios-secondary)}.progress-line strong{color:#45454d}
|
||||
.scan-box button{border-radius:11px}
|
||||
.scan-result{border:1px solid transparent}
|
||||
.scan-result.info{border-color:#d7e5ff;background:var(--ios-blue-soft);color:#174ea6}
|
||||
.scan-result.good{border-color:#ccebdd;background:var(--ios-green-soft);color:var(--ios-green)}
|
||||
.scan-result.error{border-color:#f2cbc7;background:var(--ios-red-soft);color:var(--ios-red)}
|
||||
.chip{border-radius:6px;background:#f0f0f3;color:var(--ios-secondary)}
|
||||
|
||||
.photo-grid{gap:10px}.photo-step{border-color:var(--ios-border);border-radius:12px;background:var(--ios-surface-muted)}
|
||||
.photo-step.done{border-color:#ccebdd;background:var(--ios-green-soft)}
|
||||
.photo-preview{background:#eceef1}.photo-step label{background:var(--ios-blue);border-radius:10px}
|
||||
.photo-add-tile{border-color:#c7c7cc!important;border-radius:10px!important;color:#7a7a83!important}
|
||||
.bottom-bar{border-top:1px solid rgba(60,60,67,.15);background:rgba(245,245,247,.94);padding-bottom:calc(12px + env(safe-area-inset-bottom,0px));backdrop-filter:saturate(180%) blur(18px);-webkit-backdrop-filter:saturate(180%) blur(18px)}
|
||||
|
||||
.modal-backdrop{background:rgba(0,0,0,.38)}
|
||||
.modal{border-radius:16px 16px 12px 12px;background:#fff;box-shadow:var(--shadow-modal)}
|
||||
.modal h2{color:var(--ios-ink);font-weight:740}.modal p{color:var(--ios-secondary)}
|
||||
.toast{width:max-content;max-width:min(88vw,390px);border-radius:10px;background:#2c2c2e;color:#fff;box-shadow:0 8px 24px rgba(0,0,0,.2)}
|
||||
.toast.good{background:#176b52}.toast.bad{background:#9f2d24}.toast.info{background:#2c2c2e}
|
||||
.empty{color:var(--ios-secondary)}
|
||||
|
||||
.inventory-results{display:grid;gap:10px}
|
||||
.inventory-card{border:1px solid var(--ios-border);border-radius:12px;background:#fff;padding:15px;box-shadow:0 1px 2px rgba(16,24,40,.02)}
|
||||
.inventory-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding-bottom:12px;border-bottom:1px solid var(--ios-border)}
|
||||
.inventory-card-title{min-width:0;color:var(--ios-ink);font-size:15px;font-weight:720;line-height:1.35;overflow-wrap:anywhere}
|
||||
.inventory-card-customer{margin-top:4px;color:var(--ios-secondary);font-size:12px;line-height:1.4;overflow-wrap:anywhere}
|
||||
.inventory-facts{display:grid;grid-template-columns:minmax(0,1fr) minmax(88px,.55fr);gap:8px;margin-top:12px}
|
||||
.inventory-fact{min-width:0;padding:9px 10px;border-radius:9px;background:var(--ios-surface-muted)}
|
||||
.inventory-fact span{display:block;color:var(--ios-tertiary);font-size:11px;line-height:1.3}
|
||||
.inventory-fact strong{display:block;margin-top:4px;color:#34343a;font-size:13px;line-height:1.35;overflow-wrap:anywhere}
|
||||
.inventory-fact.qty strong{font-size:18px;color:var(--ios-blue)}
|
||||
.profile-line{border-color:var(--ios-border)}.profile-line span{color:var(--ios-secondary)}
|
||||
.lang-select{flex-wrap:wrap}.lang-select button{border:1px solid transparent;background:#f0f0f3;color:#4f4f57}.lang-select button.active{border-color:#d7e5ff;background:var(--ios-blue-soft);color:var(--ios-blue)}
|
||||
|
||||
@media(max-width:359px){
|
||||
.topbar{padding-inline:12px}.content{padding-inline:12px}
|
||||
.topbar-row{gap:4px}.page-heading{gap:2px}.page-heading h1{font-size:16px}
|
||||
.topbar-row:has(.back-btn) .avatar{display:none}
|
||||
.lang-pill button{min-width:42px;padding-inline:4px}
|
||||
.welcome{padding:15px}.shift{max-width:88px}
|
||||
.metric{min-height:84px;padding:10px 7px}.metric-label{font-size:11px}.metric-value{font-size:23px}
|
||||
.inbound-entry-grid .entry,.outbound-entry-grid .entry,.internal-entry-grid .entry{padding:9px 5px}
|
||||
.entry .entry-icon{width:38px;height:38px}.entry-grid .entry h3{font-size:11px}
|
||||
.list-card,.work-card{padding:14px}.card-actions .btn{flex:1 1 auto}
|
||||
.inventory-facts{grid-template-columns:1fr 82px}
|
||||
.bottom-bar{margin-inline:-12px;padding-inline:12px}
|
||||
}
|
||||
@media(max-width:480px){
|
||||
.topbar-row:has(.back-btn) .avatar{display:none}
|
||||
}
|
||||
@media(min-width:390px){
|
||||
.photo-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
}
|
||||
@media(min-width:640px){
|
||||
body{max-width:720px;box-shadow:0 0 0 1px rgba(16,24,40,.06),0 18px 60px rgba(16,24,40,.12)}
|
||||
.login-card{padding:28px;border:1px solid var(--ios-border);border-radius:16px;background:#fff;box-shadow:var(--shadow)}
|
||||
.modal-backdrop{place-items:center}.modal{max-width:440px;border-radius:16px}
|
||||
.inventory-results{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
}
|
||||
@media(hover:hover) and (pointer:fine){
|
||||
.entry:hover,.list-card:hover,.inventory-card:hover{border-color:#d1d1d6;background:#fcfcfd}
|
||||
.btn.primary:hover,.login-btn:hover,.search-row button:hover,.scan-box button:hover{background:var(--ios-blue-pressed)}
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce){
|
||||
*,*::before,*::after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}
|
||||
}
|
||||
|
||||
/* style: pallet-list-toolbar */
|
||||
.pallet-list-toolbar{display:flex!important;align-items:center;justify-content:space-between;flex-direction:row!important}
|
||||
.pallet-list-toolbar .generate-pallet-label{min-height:32px;padding:0;color:var(--blue);background:transparent;border:0}
|
||||
.pallet-list-footer>div{display:flex;gap:8px;width:100%}
|
||||
.pallet-list-footer .btn{flex:1}
|
||||
.pallet-progress-card{margin-bottom:10px}
|
||||
.pallet-list-table{min-width:0;table-layout:fixed}
|
||||
.pallet-list-table th,.pallet-list-table td{padding:8px 6px;font-size:11px;white-space:normal;overflow-wrap:anywhere;vertical-align:middle}
|
||||
.pallet-list-table tbody td:nth-child(-n+3){font-size:14px}
|
||||
.pallet-list-table th:nth-child(1),.pallet-list-table td:nth-child(1){width:35%}
|
||||
.pallet-list-table th:nth-child(2),.pallet-list-table td:nth-child(2){width:25%}
|
||||
.pallet-list-table th:nth-child(3),.pallet-list-table td:nth-child(3){width:15%;text-align:left}
|
||||
.pallet-list-table th:nth-child(4),.pallet-list-table td:nth-child(4){width:25%}
|
||||
.pallet-list-table .status{display:inline-block;max-width:100%;padding:4px 6px;font-size:10px;line-height:1.25;white-space:normal}
|
||||
.pallet-order-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:3px;padding:3px;border-radius:10px;background:#e9e9ee}
|
||||
.pallet-order-tabs button{min-width:0;min-height:40px;padding:6px 4px;border:0;border-radius:8px;background:transparent;color:#55555d;font-size:12px;font-weight:680}
|
||||
.pallet-order-tabs button.active{background:#fff;color:var(--ios-blue);box-shadow:0 1px 4px rgba(0,0,0,.08)}
|
||||
.pallet-order-tabs button:focus-visible{outline:2px solid var(--ios-blue);outline-offset:1px}
|
||||
.fba-pallet-options{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:12px}
|
||||
.fba-pallet-choice{min-width:0;min-height:44px;padding:8px;border:1px solid var(--ios-border);border-radius:9px;background:#fff;color:var(--ios-secondary);font-size:11px;font-weight:680;overflow-wrap:anywhere}
|
||||
.fba-pallet-choice.selected{border-color:#a9c8ff;background:var(--ios-blue-soft);color:var(--ios-blue)}
|
||||
.fba-pallet-choice:disabled{cursor:not-allowed;background:#f3f4f6;color:#8b8d94;opacity:.68}
|
||||
.fba-pallet-choice:focus-visible{outline:2px solid var(--ios-blue);outline-offset:1px}
|
||||
|
||||
/* style: pick-layout */
|
||||
.pick-scan-card .scan-box{margin-top:0}
|
||||
.pick-order-head{align-items:center}
|
||||
.pick-details-toggle{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:44px;padding:0 10px;border:1px solid var(--line);border-radius:10px;background:#f7f9fc;color:var(--blue);font-size:12px;font-weight:700;white-space:nowrap}
|
||||
.pick-details-toggle svg{width:16px;height:16px;display:block;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
||||
.pick-order-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:12px}
|
||||
.pick-order-summary .summary-item{min-width:0;padding:9px;border-radius:9px;background:#f7f9fc}
|
||||
.pick-order-summary .summary-item span{display:block;color:var(--muted);font-size:10px;line-height:1.3}
|
||||
.pick-order-summary .summary-item strong{display:block;margin-top:3px;font-size:12px;overflow-wrap:anywhere}
|
||||
.pick-order-details.is-collapsed .detail-list{display:none}
|
||||
@media(max-width:360px){.pick-order-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.pick-details-toggle span{display:none}}
|
||||
|
||||
/* style: packing-page */
|
||||
.entry.packing-entry .entry-icon{background:var(--ios-blue-soft);color:var(--ios-blue)}
|
||||
.packing-scan-card .scan-box{margin:0}
|
||||
.packing-customer-field{margin:0}
|
||||
.packing-customer-field input[aria-invalid="true"]{border-color:var(--ios-red);box-shadow:0 0 0 3px rgba(180,35,24,.12)}
|
||||
.packing-customer-field input[aria-invalid="true"]:focus-visible{outline:3px solid rgba(180,35,24,.22)}
|
||||
.packing-customer-error{min-height:18px;margin:5px 2px 0;color:var(--ios-red);font-size:12px;line-height:1.5}
|
||||
.packing-details-card{padding-bottom:8px}
|
||||
.packing-details-table{min-width:0;table-layout:fixed}
|
||||
.packing-details-table th,.packing-details-table td{padding:13px 10px;font-size:13px;white-space:normal;overflow-wrap:anywhere}
|
||||
.packing-details-table th{font-size:11px}
|
||||
.packing-details-table th:first-child,.packing-details-table td:first-child{width:46%}
|
||||
.packing-details-table th:nth-child(2),.packing-details-table td:nth-child(2){width:34%}
|
||||
.packing-details-table th:last-child,.packing-details-table td:last-child{width:20%}
|
||||
.packing-actions{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px}
|
||||
.packing-actions .btn{width:100%}
|
||||
@media(max-width:359px){.packing-details-table th,.packing-details-table td{padding-inline:7px;font-size:12px}}
|
||||
@media(min-width:640px){.app[data-page="packing"] .bottom-bar{margin-inline:0}}
|
||||
|
||||
/* style: base */
|
||||
.relabel-mode-tabs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px;padding:4px;background:#eef2f6;border-radius:12px;margin:0 0 14px}
|
||||
.relabel-mode-tabs button{min-height:42px;border:0;border-radius:9px;background:transparent;color:var(--muted);font-weight:750;cursor:pointer}
|
||||
.relabel-mode-tabs button.active{background:#fff;color:var(--blue);box-shadow:0 2px 7px rgba(16,24,40,.1)}
|
||||
.relabel-list-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
.relabel-label-preview{border:1px dashed #b8c8d8;border-radius:12px;background:#f8fafc;padding:18px;margin-top:14px;min-height:170px;display:grid;align-content:center;gap:10px;text-align:center}
|
||||
.relabel-label-preview .preview-kind{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
|
||||
.relabel-label-preview .preview-value{font-size:25px;font-weight:850;color:var(--ink);word-break:break-word}
|
||||
.relabel-label-preview .preview-meta{font-size:12px;color:var(--muted)}
|
||||
.relabel-print-actions{display:flex;gap:8px;margin-top:14px}.relabel-print-actions .btn{flex:1}
|
||||
@media print{body>*{display:none!important}#app{display:block!important}.topbar,.bottom-bar,.scan-box,.relabel-print-actions{display:none!important}.content{padding:0!important}.relabel-label-preview{border:0;min-height:0;margin:0;font-size:18pt}}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Consolidation · Overseas Warehouse PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
</head>
|
||||
<body data-section="consolidation">
|
||||
<div id="app" class="app"></div>
|
||||
<div id="modal-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Exception Handling · Overseas Warehouse PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
</head>
|
||||
<body data-section="exception">
|
||||
<div id="app" class="app"></div>
|
||||
<div id="modal-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Express Receiving · Overseas Warehouse PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
</head>
|
||||
<body data-section="express-signin">
|
||||
<div id="app" class="app"></div>
|
||||
<div id="modal-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Outbound Handover · Overseas Warehouse PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
</head>
|
||||
<body data-section="handover">
|
||||
<div id="app" class="app"></div>
|
||||
<div id="modal-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+5
-780
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Pick & Pack · Overseas Warehouse PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
</head>
|
||||
<body data-section="packing">
|
||||
<div id="app" class="app"></div>
|
||||
<div id="modal-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Palletize · Overseas Warehouse PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
</head>
|
||||
<body data-section="pallet">
|
||||
<div id="app" class="app"></div>
|
||||
<div id="modal-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Outbound Picking · Overseas Warehouse PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
</head>
|
||||
<body data-section="pick">
|
||||
<div id="app" class="app"></div>
|
||||
<div id="modal-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Unload & Receive · Overseas Warehouse PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
</head>
|
||||
<body data-section="receive">
|
||||
<div id="app" class="app"></div>
|
||||
<div id="modal-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Relabeling · Overseas Warehouse PDA</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
</head>
|
||||
<body data-section="relabel">
|
||||
<div id="app" class="app"></div>
|
||||
<div id="modal-root"></div>
|
||||
<div id="toast-root"></div>
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
$detailView = [regex]::Match($html, '(?s)function consolidationDetailView\(\).*?(?=const basePalletView=)').Value
|
||||
|
||||
if (-not $detailView) { throw 'Consolidation detail view is missing' }
|
||||
|
||||
if (-not $detailView.Contains("t('scanPalletLabel')")) { throw 'Consolidation scan module must use pallet-label copy' }
|
||||
if (-not $detailView.Contains('selectorClassName')) { throw 'Consolidation detail must remove the pallet label selector module' }
|
||||
$scanAt = $detailView.IndexOf('consolidation-scan-card')
|
||||
$currentAt = $detailView.IndexOf('current-pallet-card')
|
||||
if ($scanAt -lt 0 -or $currentAt -lt 0 -or $scanAt -gt $currentAt) { throw 'Consolidation scan module must remain above current pallet details' }
|
||||
|
||||
Write-Output 'PASS: consolidation detail scan-first order contract'
|
||||
@@ -0,0 +1,135 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const applicationSource = fs.readFileSync(path.join(__dirname, '..', 'assets', 'app.js'), 'utf8')
|
||||
.replace(/\s*bootstrap\(\);\s*$/, '');
|
||||
const tests = [];
|
||||
const runtimeTests = String.raw`
|
||||
render=()=>{};
|
||||
toast=()=>{};
|
||||
|
||||
function test(name,fn){tests.push({name,fn})}
|
||||
|
||||
test('inbound workbench exposes express sign-in entry',()=>{
|
||||
const markup=workbenchView();
|
||||
assert.match(markup,/快递签收/);
|
||||
assert.match(markup,/navigate\('express-signin'\)/);
|
||||
});
|
||||
|
||||
test('express sign-in view exposes purchase and return tabs with order fields',()=>{
|
||||
state.page='express-signin';
|
||||
state.expressOrderType='purchase';
|
||||
const markup=expressSignInView();
|
||||
for(const label of ['采购','退货','客户简码','SKU','箱数/件数','承运商','快递号','签收']) assert.match(markup,new RegExp(label));
|
||||
assert.match(markup,/setExpressOrderType\('return'\)/);
|
||||
});
|
||||
|
||||
test('each express order uses exactly two rows without an off-screen table',()=>{
|
||||
state.expressOrderType='purchase';
|
||||
const markup=expressSignInView();
|
||||
const cards=markup.match(/<article class="express-order-card">/g)||[];
|
||||
const rows=markup.match(/<div class="express-order-row">/g)||[];
|
||||
assert.equal(cards.length,2);
|
||||
assert.equal(rows.length,cards.length*2);
|
||||
assert.doesNotMatch(markup,/<table|express-signin-table/);
|
||||
});
|
||||
|
||||
test('signing an express order marks it received and prevents duplicate action',()=>{
|
||||
const order=expressOrders.find(item=>item.type==='purchase');
|
||||
order.signedIn=false;
|
||||
state.expressOrderType='purchase';
|
||||
openExpressSignIn(order.id);
|
||||
state.expressScan={kind:'sku',value:order.sku,quantity:1};
|
||||
state.expressWaybillPhotos[order.id]='data:image/png;base64,test';
|
||||
completeExpressSignIn();
|
||||
assert.equal(order.signedIn,true);
|
||||
assert.equal(state.page,'express-signin');
|
||||
const markup=expressSignInView();
|
||||
assert.match(markup,/已签收/);
|
||||
});
|
||||
|
||||
test('sign action opens a dedicated scan and waybill photo page',()=>{
|
||||
const order=expressOrders.find(item=>item.type==='purchase'&&!item.signedIn);
|
||||
state.expressOrderType='purchase';
|
||||
openExpressSignIn(order.id);
|
||||
assert.equal(state.page,'express-signin-detail');
|
||||
assert.equal(state.selectedExpressOrderId,order.id);
|
||||
const markup=expressSignInDetailView();
|
||||
assert.match(markup,/扫描箱唛或SKU/);
|
||||
assert.match(markup,/照片信息/);
|
||||
assert.match(markup,/面单/);
|
||||
assert.match(markup,/express-waybill-photo-input/);
|
||||
assert.match(markup,/签收完成/);
|
||||
assert.match(markup,new RegExp(order.carrier));
|
||||
assert.match(markup,new RegExp(order.trackingNo.replace(/ /g,'\\s')));
|
||||
assert.match(markup,/aria-label="扫描箱唛或SKU"/);
|
||||
});
|
||||
|
||||
test('scanning a carton mark exposes the carton count input',()=>{
|
||||
const order=expressOrders.find(item=>item.type==='purchase'&&!item.signedIn);
|
||||
openExpressSignIn(order.id);
|
||||
document.getElementById=()=>({value:order.boxMark});
|
||||
handleExpressScan();
|
||||
assert.deepEqual(state.expressScan,{kind:'carton',value:order.boxMark,quantity:order.cartons});
|
||||
const markup=expressSignInDetailView();
|
||||
assert.match(markup,/箱数/);
|
||||
assert.match(markup,/id="express-quantity"/);
|
||||
assert.doesNotMatch(markup,/>个数</);
|
||||
});
|
||||
|
||||
test('scanning an SKU exposes the piece count input',()=>{
|
||||
const order=expressOrders.find(item=>item.type==='purchase'&&!item.signedIn);
|
||||
openExpressSignIn(order.id);
|
||||
document.getElementById=()=>({value:order.sku});
|
||||
handleExpressScan();
|
||||
assert.deepEqual(state.expressScan,{kind:'sku',value:order.sku,quantity:order.pieces});
|
||||
const markup=expressSignInDetailView();
|
||||
assert.match(markup,/>个数</);
|
||||
assert.match(markup,/id="express-quantity"/);
|
||||
});
|
||||
|
||||
test('express sign-in cannot complete before a scan and waybill photo',()=>{
|
||||
const order=expressOrders.find(item=>item.type==='return');
|
||||
order.signedIn=false;
|
||||
openExpressSignIn(order.id);
|
||||
state.expressScan=null;
|
||||
delete state.expressWaybillPhotos[order.id];
|
||||
completeExpressSignIn();
|
||||
assert.equal(order.signedIn,false);
|
||||
assert.equal(state.page,'express-signin-detail');
|
||||
});
|
||||
|
||||
test('correcting an invalid quantity immediately refreshes completion readiness',()=>{
|
||||
const order=expressOrders.find(item=>item.type==='return');
|
||||
openExpressSignIn(order.id);
|
||||
state.expressScan={kind:'sku',value:order.sku,quantity:order.pieces};
|
||||
state.expressWaybillPhotos[order.id]='data:image/png;base64,test';
|
||||
const completionButton={disabled:false};
|
||||
document.getElementById=id=>id==='express-complete'?completionButton:null;
|
||||
setExpressQuantity('0');
|
||||
assert.equal(completionButton.disabled,true);
|
||||
setExpressQuantity('2');
|
||||
assert.equal(completionButton.disabled,false);
|
||||
});
|
||||
|
||||
test('express sign-in rejects fractional quantities',()=>{
|
||||
const order=expressOrders.find(item=>item.type==='return');
|
||||
order.signedIn=false;
|
||||
openExpressSignIn(order.id);
|
||||
state.expressScan={kind:'sku',value:order.sku,quantity:'1.5'};
|
||||
state.expressWaybillPhotos[order.id]='data:image/png;base64,test';
|
||||
completeExpressSignIn();
|
||||
assert.equal(order.signedIn,false);
|
||||
assert.equal(state.page,'express-signin-detail');
|
||||
});
|
||||
|
||||
for(const entry of tests){
|
||||
try{entry.fn();process.stdout.write('PASS: '+entry.name+'\n')}
|
||||
catch(error){process.stderr.write('FAIL: '+entry.name+'\n'+error.stack+'\n');globalThis.__failed=(globalThis.__failed||0)+1}
|
||||
}
|
||||
if(globalThis.__failed)throw new Error(globalThis.__failed+' runtime behavior test(s) failed');
|
||||
`;
|
||||
|
||||
const run = new Function('assert', 'tests', 'localStorage', 'document', 'window', applicationSource + '\n' + runtimeTests);
|
||||
run(assert, tests, { getItem: () => null, setItem: () => {} }, { getElementById: () => null }, { scrollTo: () => {} });
|
||||
@@ -0,0 +1,18 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
$palletView = [regex]::Match($html, '(?s)function palletView\(\).*?(?=function startPallet\()').Value
|
||||
$scanHandler = [regex]::Match($html, '(?s)function handlePalletScan\(\).*?(?=function addPalletItem)').Value
|
||||
|
||||
if (-not $html.Contains("palletScanQty:'1'")) { throw 'Pallet scan quantity state is missing' }
|
||||
$inputCartonQty = [char]0x8f93 + [char]0x5165 + [char]0x7bb1 + [char]0x6570
|
||||
if (-not $html.Contains("inputCartonQty:'$inputCartonQty'")) { throw 'FBA carton quantity label is missing' }
|
||||
if (-not $palletView.Contains("state.palletOrderType==='fba'")) { throw 'FBA scan quantity block is missing' }
|
||||
if (-not $palletView.Contains('id="pallet-scan-qty"')) { throw 'FBA carton quantity input is missing' }
|
||||
if (-not $palletView.Contains('id="pallet-scan-remaining"')) { throw 'FBA remaining carton display is missing' }
|
||||
if (-not $html.Contains('function updatePalletScanQty(value)')) { throw 'FBA carton quantity handler is missing' }
|
||||
if (-not $scanHandler.Contains('if(!value){return}')) { throw 'Empty pallet scan still reports a required-value error' }
|
||||
if (-not $scanHandler.Contains('requestedQty')) { throw 'Pallet scan does not read the requested carton quantity' }
|
||||
if (-not $scanHandler.Contains('.slice(0,requestedQty)')) { throw 'Pallet scan does not add the requested carton quantity' }
|
||||
if (-not $html.Contains("state.palletOrderType!=='overseas'")) { throw 'Quantity modal guard is missing' }
|
||||
|
||||
Write-Output 'PASS: FBA pallet scan quantity contract'
|
||||
@@ -1,5 +1,5 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\index.html')
|
||||
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
|
||||
$required = @(
|
||||
'totalWeightLb:203.0',
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$htmlPath = Join-Path $PSScriptRoot '..\assets\app.css'
|
||||
$html = Get-Content -Raw -LiteralPath $htmlPath
|
||||
$rule = [regex]::Match(
|
||||
$html,
|
||||
'(?s)\.app\[data-page="workbench"\]\s+\.entry\s+\.entry-icon\s*\{(?<body>[^}]*)\}'
|
||||
)
|
||||
|
||||
if (-not $rule.Success) {
|
||||
throw 'Homepage entry icons do not have a shared workbench-scoped color rule.'
|
||||
}
|
||||
|
||||
$body = $rule.Groups['body'].Value
|
||||
if ($body -notmatch 'background\s*:\s*var\(--ios-blue-soft\)') {
|
||||
throw 'Homepage entry icon backgrounds are not uniformly light blue.'
|
||||
}
|
||||
if ($body -notmatch 'color\s*:\s*var\(--ios-blue\)') {
|
||||
throw 'Homepage entry icon foregrounds are not uniformly blue.'
|
||||
}
|
||||
|
||||
Write-Output 'PASS: homepage entry icons use the shared blue treatment'
|
||||
@@ -0,0 +1,15 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
|
||||
$workbenchView = [regex]::Match($html, '(?s)function workbenchView\(\).*?(?=function openPlan)').Value
|
||||
if (-not $workbenchView) { throw 'Workbench view is missing' }
|
||||
if ($workbenchView.Contains("navigate('inventory')")) { throw 'Home still exposes the inventory entry' }
|
||||
if ($workbenchView.Contains('more-entry')) { throw 'Home still exposes the More operations entry' }
|
||||
|
||||
$emptyInternalSection = '<div class="section-title"><h2>${t(''internal'')}</h2><span>${t(''inventory'')}</span></div><div class="entry-grid"></div><div class="task-strip">'
|
||||
if (-not $workbenchView.Contains($emptyInternalSection)) { throw 'Home does not retain the empty Warehouse section' }
|
||||
|
||||
if (-not $html.Contains('function inventoryView()')) { throw 'Inventory page implementation was removed' }
|
||||
if (-not $html.Contains("state.page==='inventory'?inventoryView()")) { throw 'Inventory page render route was removed' }
|
||||
|
||||
Write-Output 'PASS: home internal entries hidden while empty section and inventory page remain'
|
||||
@@ -0,0 +1,71 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const applicationSource = fs.readFileSync(path.join(__dirname, '..', 'assets', 'app.js'), 'utf8')
|
||||
.replace(/\s*bootstrap\(\);\s*$/, '');
|
||||
const runtimeTests = String.raw`
|
||||
render=()=>{};
|
||||
let lastToast=null;
|
||||
toast=(message,type)=>{lastToast={message,type}};
|
||||
|
||||
const entries=internalEntriesMarkup();
|
||||
assert.ok(entries.indexOf("navigate('packing')")>=0, 'packing entry is missing');
|
||||
assert.ok(entries.indexOf("navigate('packing')")<entries.indexOf("navigate('relabel')"), 'packing entry must appear before relabeling');
|
||||
|
||||
state.page='packing';
|
||||
state.packingMessage={type:'good',text:'READY'};
|
||||
const view=packingView();
|
||||
assert.match(view,/id="packing-scan"/, 'packing scan input is missing');
|
||||
assert.match(view,/id="packing-customer-code"[^>]*required/, 'required customer short-code input is missing');
|
||||
assert.match(view,/packing-scan-card[\s\S]*?<\/section><section class="work-card packing-customer-card"[\s\S]*?id="packing-customer-code"/, 'scan and customer information must render as separate modules');
|
||||
const customerModule=view.match(/<section class="work-card packing-customer-card">([\s\S]*?)<\/section>/)?.[1]||'';
|
||||
assert.doesNotMatch(customerModule,/<h2>/, 'customer module must not show a duplicate customer information heading');
|
||||
assert.equal((customerModule.match(/<label\b/g)||[]).length,1, 'customer module must show one field label');
|
||||
assert.match(customerModule,new RegExp('<label[^>]*>'+t('customerCode')+' \\*</label>'), 'customer module must label the field as customer code');
|
||||
assert.match(view,/role="status" aria-live="polite"/, 'successful scan feedback must be announced');
|
||||
assert.match(view,/packing-details-table/, 'carton details table is missing');
|
||||
assert.match(view,/YQ-CASE-01[\s\S]*YQ-CABLE-02[\s\S]*YQ-HOLDER-03/, 'carton detail rows are missing');
|
||||
assert.match(view,/confirmPackingInbound\(\)/, 'direct inbound action is missing');
|
||||
assert.doesNotMatch(view,/packing-pallet-quantity|pallet-quantity-card/, 'pallet quantity module must stay hidden');
|
||||
|
||||
state.packingMessage=null;
|
||||
state.packingCustomerCode='CUS-01';
|
||||
document.__input='YQ-CABLE-02';
|
||||
handlePackingScan();
|
||||
assert.equal(state.packingMessage.type,'good');
|
||||
assert.match(state.packingMessage.text,/YQ-CABLE-02/);
|
||||
assert.equal(document.__focused,'packing-scan', 'packing scan input must regain focus after a scan');
|
||||
assert.match(packingView(),/value="CUS-01"/, 'customer short code must survive scan re-renders');
|
||||
|
||||
state.page='packing';
|
||||
state.packingInboundDone=false;
|
||||
state.packingCustomerCode=' ';
|
||||
confirmPackingInbound();
|
||||
assert.equal(state.packingInboundDone,false, 'empty customer short code must block inbound');
|
||||
assert.equal(state.page,'packing', 'validation failure must stay on the packing page');
|
||||
assert.equal(state.packingCustomerError,t('customerCodeRequired'));
|
||||
assert.equal(document.__focused,'packing-customer-code', 'invalid customer short code must receive focus');
|
||||
|
||||
state.packingCustomerCode=' CUS-01 ';
|
||||
confirmPackingInbound();
|
||||
assert.equal(state.packingInboundDone,true, 'confirmation must complete direct inbound');
|
||||
assert.equal(state.page,'workbench', 'confirmation must return to the order list');
|
||||
assert.equal(state.packingCustomerCode,'', 'successful inbound must clear the customer short code');
|
||||
assert.deepEqual(lastToast,{message:t('packingInboundDone'),type:'good'});
|
||||
`;
|
||||
|
||||
const documentStub = {
|
||||
__input: '',
|
||||
__focused: '',
|
||||
getElementById(id) {
|
||||
if (id === 'packing-scan') return { value: this.__input, focus: () => { this.__focused = id; } };
|
||||
if (id === 'packing-customer-code') return { value: '', focus: () => { this.__focused = id; } };
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const run = new Function('assert', 'localStorage', 'document', 'window', applicationSource + '\n' + runtimeTests);
|
||||
run(assert, { getItem: () => null, setItem: () => {} }, documentStub, { scrollTo: () => {}, requestAnimationFrame: callback => callback() });
|
||||
|
||||
process.stdout.write('PASS: packing entry, hidden pallet quantity, scan, and direct inbound behavior\n');
|
||||
@@ -0,0 +1,12 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
$palletView = [regex]::Match($html, '(?s)function palletView\(\).*?(?=function startPallet\()').Value
|
||||
|
||||
if (-not $palletView.Contains('<span>${t(''destinationWarehouseShort'')}</span>')) {
|
||||
throw 'Inbound pallet detail still uses the generic destination label'
|
||||
}
|
||||
if ($palletView.Contains('<span>${t(''destination'')}</span>')) {
|
||||
throw 'Inbound pallet detail still renders the generic destination label'
|
||||
}
|
||||
|
||||
Write-Output 'PASS: pallet detail destination label contract'
|
||||
@@ -0,0 +1,11 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
$palletView = [regex]::Match($html, '(?s)function palletView\(\).*?(?=function startPallet\()').Value
|
||||
|
||||
$scanAt = $palletView.IndexOf('${scanCard}')
|
||||
$cargoAt = $palletView.IndexOf('${cargoInfo}')
|
||||
if ($scanAt -lt 0) { throw 'Pallet detail scan module is missing' }
|
||||
if ($cargoAt -lt 0) { throw 'Pallet detail cargo information module is missing' }
|
||||
if ($scanAt -gt $cargoAt) { throw 'Pallet detail scan module must appear before cargo information' }
|
||||
|
||||
Write-Output 'PASS: pallet detail scan module order contract'
|
||||
@@ -0,0 +1,13 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
$plansView = [regex]::Match($html, '(?s)function plansPalletView\(\).*?(?=function receivePhotoView)').Value
|
||||
$css = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.css')
|
||||
|
||||
if (-not $plansView.Contains('<th>${t(''destinationWarehouseShort'')}</th>')) {
|
||||
throw 'Inbound pallet list still uses the generic destination label'
|
||||
}
|
||||
if (-not $css.Contains('.pallet-list-table tbody td:nth-child(-n+3){font-size:14px}')) {
|
||||
throw 'Inbound pallet list primary content is not enlarged to 14px'
|
||||
}
|
||||
|
||||
Write-Output 'PASS: pallet list typography and destination label contract'
|
||||
@@ -2,11 +2,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
const match = html.match(/<script>([\s\S]*?)<\/script>/);
|
||||
assert.ok(match, 'inline application script is missing');
|
||||
|
||||
const applicationSource = match[1].replace(/\s*render\(\);\s*$/, '');
|
||||
const applicationSource = fs.readFileSync(path.join(__dirname, '..', 'assets', 'app.js'), 'utf8')
|
||||
.replace(/\s*bootstrap\(\);\s*$/, '');
|
||||
const tests = [];
|
||||
const runtimeTests = String.raw`
|
||||
render=()=>{};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
|
||||
$buttonAt = $html.LastIndexOf('class="photo-add-tile photo-add-action"')
|
||||
if ($buttonAt -lt 0) { throw 'Pallet photo add action class is missing' }
|
||||
$buttonMarkup = $html.Substring($buttonAt, [Math]::Min(360, $html.Length - $buttonAt))
|
||||
if (-not $buttonMarkup.Contains('margin:0;')) { throw 'Pallet photo add action is not left aligned' }
|
||||
$detailView = [regex]::Match($html, '(?s)function consolidationDetailView\(\).*?(?=const basePalletView=)').Value
|
||||
if (-not $detailView.Contains('const emptyPhotoMarkup')) { throw 'Consolidation empty photo state still reserves a separate empty row' }
|
||||
|
||||
Write-Output 'PASS: pallet photo button position contract'
|
||||
@@ -0,0 +1,8 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$css = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.css')
|
||||
|
||||
if (-not $css.Contains('.pallet-progress-card{margin-bottom:10px}')) {
|
||||
throw 'Pallet progress card bottom spacing does not match the 10px top spacing'
|
||||
}
|
||||
|
||||
Write-Output 'PASS: pallet progress spacing contract'
|
||||
@@ -1,5 +1,5 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\index.html')
|
||||
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
|
||||
$required = @(
|
||||
'id="pallet-top-scan"',
|
||||
@@ -42,10 +42,10 @@ if (-not $scanHandler.Contains("if(!value){state.palletTopMessage=null;state.pal
|
||||
if (-not $plansView.Contains('<table class="table pallet-list-table">')) { throw 'Pallet list is not rendered as a table' }
|
||||
if ($plansView.Contains("t('viewPalletDetails')") -or $plansView.Contains("t('palletizeAction')")) { throw 'Pallet list still exposes a detail/action button' }
|
||||
if ($plansView.Contains('${esc(pallet.plan.id)}') -or $plansView.Contains('${esc(pallet.container)}')) { throw 'Pallet list still exposes order/container information' }
|
||||
$css = [regex]::Match($html, '(?s)<style id="pallet-list-toolbar">.*?</style>').Value
|
||||
$css = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.css')
|
||||
if (-not $css.Contains('.pallet-list-table{min-width:0')) { throw 'Pallet table still requires horizontal scrolling' }
|
||||
if (-not $css.Contains('.pallet-list-table th,.pallet-list-table td{padding:8px 6px')) { throw 'Pallet table spacing is not compact' }
|
||||
if (-not $css.Contains('.pallet-list-table tbody td:nth-child(-n+3){font-size:12px}')) { throw 'Primary pallet columns are not enlarged' }
|
||||
if (-not $css.Contains('.pallet-list-table tbody td:nth-child(-n+3){font-size:14px}')) { throw 'Primary pallet columns are not enlarged' }
|
||||
if (-not $css.Contains('.pallet-list-table th:nth-child(1),.pallet-list-table td:nth-child(1){width:35%}')) { throw 'Pallet label column width is incorrect' }
|
||||
if (-not $css.Contains('.pallet-list-table th:nth-child(2),.pallet-list-table td:nth-child(2){width:25%}')) { throw 'Destination column width is incorrect' }
|
||||
if (-not $css.Contains('.pallet-list-table th:nth-child(3),.pallet-list-table td:nth-child(3){width:15%;text-align:left}')) { throw 'Carton count column width is incorrect' }
|
||||
@@ -54,9 +54,10 @@ if ($html.Contains("if(state.page==='plans'||state.page==='relabel')")) { throw
|
||||
|
||||
$palletView = [regex]::Match($html, '(?s)function palletView\(\).*?(?=function startPallet\()').Value
|
||||
$tabsAt = $palletView.IndexOf('${palletOrderTabs()}')
|
||||
$scanAt = $palletView.IndexOf('${scanCard}')
|
||||
$progressAt = $palletView.IndexOf('pallet-progress-card')
|
||||
$cargoAt = $palletView.IndexOf('${cargoInfo}')
|
||||
if ($tabsAt -lt 0 -or $progressAt -lt $tabsAt -or $cargoAt -lt $progressAt) { throw 'Pallet detail module order is incorrect' }
|
||||
if ($tabsAt -lt 0 -or $scanAt -lt $tabsAt -or $progressAt -lt $scanAt -or $cargoAt -lt $progressAt) { throw 'Pallet detail module order is incorrect' }
|
||||
if (-not $palletView.Contains("const showDetails=state.palletOrderType==='overseas'")) { throw 'Detail visibility is not type-driven' }
|
||||
if (-not $palletView.Contains("const scanHint=state.palletOrderType==='dropship'?t('scanPalletLabel'):t('scanBoxSku')")) { throw 'Scan hint is not type-driven' }
|
||||
if (-not $palletView.Contains("t('cargoInfo')")) { throw 'Pallet detail title is not cargo information' }
|
||||
@@ -79,7 +80,7 @@ $dropshipContracts = @(
|
||||
"state.palletOrderType==='dropship'",
|
||||
'state.dropshipPalletLabel=pallet.id',
|
||||
"state.palletOrderType==='dropship'&&!state.dropshipPalletLabel",
|
||||
"state.palletOrderType!=='dropship'"
|
||||
"state.palletOrderType==='overseas'"
|
||||
)
|
||||
$missingDropship = $dropshipContracts | Where-Object { -not $html.Contains($_) }
|
||||
if ($missingDropship) { throw "Missing dropship scan behavior: $($missingDropship -join ', ')" }
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
$pickView = [regex]::Match($html, '(?s)pickView=function\(\).*?(?=function togglePickDetails)').Value
|
||||
|
||||
if (-not $pickView) { throw 'Could not locate pickView source' }
|
||||
|
||||
$scanAt = $pickView.IndexOf('class="scan-box"')
|
||||
$progressAt = $pickView.IndexOf('class="progress"')
|
||||
$detailsAt = $pickView.IndexOf('pick-order-details')
|
||||
if ($scanAt -lt 0 -or $progressAt -lt 0 -or $detailsAt -lt 0) {
|
||||
throw 'Pick page is missing the scan, progress, or order details sections'
|
||||
}
|
||||
if ($scanAt -gt $progressAt -or $progressAt -gt $detailsAt) {
|
||||
throw 'Pick page order must be scan, progress, then order details'
|
||||
}
|
||||
|
||||
if (-not $html.Contains('state.pickDetailsCollapsed=false')) {
|
||||
throw 'Pick detail collapse state is not initialized'
|
||||
}
|
||||
if (-not $html.Contains('function togglePickDetails()')) {
|
||||
throw 'Pick detail collapse action is missing'
|
||||
}
|
||||
if (-not $html.Contains('pick-order-summary hidden')) {
|
||||
throw 'Pick order summary should be hidden'
|
||||
}
|
||||
if (-not $html.Contains('basePickViewForOrderDisplay') -or -not $html.Contains('t(''outboundOrderNo'')')) {
|
||||
throw 'Pick details heading should show the outbound order number label'
|
||||
}
|
||||
if (-not $html.Contains('const orderTitleMarkup=`<h2>${t(''outboundOrderNo'')}</h2>`')) {
|
||||
throw 'Pick details title should be replaced with the outbound order number'
|
||||
}
|
||||
$hasLabeledDestination = $html.Contains('const labeledDestinationSub=')
|
||||
$hasDestinationLabel = $html.Contains("t('destinationWarehouseShort')")
|
||||
$hasDestinationValue = $html.Contains('${esc(task.destination)}</p>')
|
||||
if (-not ($hasLabeledDestination -and $hasDestinationLabel -and $hasDestinationValue)) {
|
||||
throw 'Pick details subtitle should label the destination warehouse'
|
||||
}
|
||||
if (-not $html.Contains("replace(redundantDestinationRow,'')")) {
|
||||
throw 'Expanded pick details should not repeat the destination warehouse row'
|
||||
}
|
||||
if (-not $html.Contains('linkedInboundCode')) {
|
||||
throw 'Linked inbound code label is missing'
|
||||
}
|
||||
if (-not $html.Contains("replace(hiddenInboundCodeRow,'')")) {
|
||||
throw 'Standalone inbound code row should be hidden'
|
||||
}
|
||||
if (-not $html.Contains('const linkedInboundCodeRow=') -or -not $html.Contains('esc(task.inboundCode)')) {
|
||||
throw 'Linked inbound code row should display the task inbound code'
|
||||
}
|
||||
|
||||
$requiredSummaryLabels = @('outboundOrderNo', 'status', 'customer', 'destinationWarehouseShort')
|
||||
$missing = $requiredSummaryLabels | Where-Object { -not $pickView.Contains("t('$_')") }
|
||||
if ($missing) { throw "Missing collapsed summary labels: $($missing -join ', ')" }
|
||||
|
||||
Write-Output 'PASS: pick layout and collapse contract'
|
||||
@@ -0,0 +1,19 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
|
||||
$detailView = [regex]::Match($html, '(?s)function relabelDetailView\(mode\).*?(?=function relabelBoxView)').Value
|
||||
if (-not $detailView) { throw 'Relabel detail view function not found' }
|
||||
|
||||
if (-not $detailView.Contains("t('relabelProgress')")) {
|
||||
throw 'Relabel detail view does not render relabel progress'
|
||||
}
|
||||
|
||||
if (-not $detailView.Contains('progress-line')) {
|
||||
throw 'Relabel progress should use the standard progress line'
|
||||
}
|
||||
|
||||
if (-not $detailView.Contains('relabelBoxTitle')) {
|
||||
throw 'Relabel detail view no longer distinguishes carton-mark mode'
|
||||
}
|
||||
|
||||
Write-Output 'PASS: carton-mark relabel detail exposes progress'
|
||||
@@ -0,0 +1,24 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const applicationSource = fs.readFileSync(path.join(__dirname, '..', 'assets', 'app.js'), 'utf8')
|
||||
.replace(/\s*bootstrap\(\);\s*$/, '');
|
||||
const runtimeTests = String.raw`
|
||||
render=()=>{};
|
||||
|
||||
for (const mode of ['box', 'sku']) {
|
||||
state.relabelMode=mode;
|
||||
const view=relabelView();
|
||||
assert.doesNotMatch(view,/class="list-title"/, mode+' relabel cards must not show a repeated mode title');
|
||||
assert.match(view,/class="list-grid relabel-list-grid"/, mode+' relabel cards must keep their field grid');
|
||||
assert.match(view,/openRelabelWork/, mode+' relabel cards must keep the start-work action');
|
||||
}
|
||||
`;
|
||||
|
||||
const documentStub = { getElementById: () => null };
|
||||
const windowStub = { scrollTo: () => {}, requestAnimationFrame: callback => callback() };
|
||||
const run = new Function('assert', 'localStorage', 'document', 'window', applicationSource + '\n' + runtimeTests);
|
||||
run(assert, { getItem: () => null, setItem: () => {} }, documentStub, windowStub);
|
||||
|
||||
process.stdout.write('PASS: relabel cards hide repeated mode titles\n');
|
||||
@@ -0,0 +1,17 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
|
||||
$detailView = [regex]::Match($html, '(?s)function relabelDetailView\(mode\).*?(?=function relabelBoxView)').Value
|
||||
if (-not $detailView) { throw 'Relabel detail view function not found' }
|
||||
|
||||
if (-not $detailView.Contains('relabel-scan-card')) {
|
||||
throw 'Relabel detail view should expose a dedicated scan card after hiding the summary card'
|
||||
}
|
||||
|
||||
if ($detailView.Contains('p.customer') -or
|
||||
$detailView.Contains('p.container') -or
|
||||
$detailView.Contains('p.inboundCode')) {
|
||||
throw 'Relabel detail view still renders the hidden summary card content'
|
||||
}
|
||||
|
||||
Write-Output 'PASS: relabel detail summary is hidden for carton-mark and SKU pages'
|
||||
@@ -0,0 +1,32 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$htmlPath = Join-Path $PSScriptRoot '..\assets\app.js'
|
||||
$html = [System.IO.File]::ReadAllText($htmlPath, [System.Text.Encoding]::UTF8)
|
||||
|
||||
$orderNoLabel = -join [char[]](0x6362,0x6807,0x5355,0x53F7)
|
||||
$boxCountLabel = -join [char[]](0x7BB1,0x6570)
|
||||
$unitsPerBoxLabel = -join [char[]](0x6BCF,0x7BB1,0x4E2A,0x6570)
|
||||
$requirementLabel = -join [char[]](0x8D34,0x6807,0x8981,0x6C42)
|
||||
$requiredLabels = @(
|
||||
"relabelOrderNo:'$orderNoLabel'",
|
||||
"relabelBoxCount:'$boxCountLabel'",
|
||||
"relabelUnitsPerBox:'$unitsPerBoxLabel'",
|
||||
"relabelRequirement:'$requirementLabel'"
|
||||
)
|
||||
|
||||
$missingLabels = $requiredLabels | Where-Object { -not $html.Contains($_) }
|
||||
if ($missingLabels) { throw "Missing relabel list labels: $($missingLabels -join ', ')" }
|
||||
|
||||
$start = $html.LastIndexOf('function relabelView()')
|
||||
$end = $html.IndexOf('function relabelDetailView', $start)
|
||||
if ($start -lt 0 -or $end -lt 0) { throw 'Relabel list view function not found' }
|
||||
$render = $html.Substring($start, $end - $start)
|
||||
|
||||
foreach ($token in @('relabelOrderNo','relabelBoxCount','relabelUnitsPerBox','relabelRequirement',"t('status')",'openRelabelWork')) {
|
||||
if (-not $render.Contains($token)) { throw "Relabel list is missing $token" }
|
||||
}
|
||||
|
||||
if ($render.Contains("t('inboundCode')") -or $render.Contains("t('palletCount')") -or $render.Contains("t('relabelStatus')")) {
|
||||
throw 'Relabel list still renders an obsolete field'
|
||||
}
|
||||
|
||||
Write-Output 'PASS: relabel list columns match carton-mark and SKU-label requirements'
|
||||
@@ -1,5 +1,5 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\index.html')
|
||||
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
|
||||
$required = @(
|
||||
'selectedRelabelId:null',
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const applicationSource = fs.readFileSync(path.join(__dirname, '..', 'assets', 'app.js'), 'utf8')
|
||||
.replace(/\s*bootstrap\(\);\s*$/, '');
|
||||
const runtimeTests = String.raw`
|
||||
render=()=>{};
|
||||
state.selectedRelabelId='INB-260819-018';
|
||||
state.page='relabel-sku';
|
||||
state.relabelDoneOrders={};
|
||||
state.relabelScans.sku={};
|
||||
let view=relabelDetailView('sku');
|
||||
assert.match(view,/class="progress-line"/,'SKU relabel detail should show progress');
|
||||
assert.match(view,/0 \/ 0/,'SKU relabel detail should start without a scanned carton');
|
||||
|
||||
state.relabelScans.sku['INB-260819-018']={value:'YQ-CASE-01',itemId:'C-001'};
|
||||
view=relabelDetailView('sku');
|
||||
assert.match(view,/0 \/ 120/,'SKU relabel detail should show scanned SKU carton quantity');
|
||||
|
||||
state.relabelDoneOrders['sku:INB-260819-018']=true;
|
||||
view=relabelDetailView('sku');
|
||||
assert.match(view,/120 \/ 120/,'Completed SKU relabel detail should show full quantity progress');
|
||||
`;
|
||||
|
||||
const documentStub = { getElementById: () => null };
|
||||
const windowStub = { scrollTo: () => {}, requestAnimationFrame: callback => callback() };
|
||||
const run = new Function('assert', 'localStorage', 'document', 'window', applicationSource + '\n' + runtimeTests);
|
||||
run(assert, { getItem: () => null, setItem: () => {} }, documentStub, windowStub);
|
||||
|
||||
process.stdout.write('PASS: SKU relabel detail exposes quantity progress\n');
|
||||
@@ -0,0 +1,40 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$htmlPath = Join-Path $PSScriptRoot '..\assets\app.js'
|
||||
$html = [System.IO.File]::ReadAllText($htmlPath, [System.Text.Encoding]::UTF8)
|
||||
|
||||
$required = @(
|
||||
'relabelMode:',
|
||||
'relabel-box',
|
||||
'relabel-sku',
|
||||
'setRelabelMode(mode)',
|
||||
'贴箱唛',
|
||||
'贴SKU标',
|
||||
'开始作业',
|
||||
'function relabelBoxView()',
|
||||
'function relabelSkuView()',
|
||||
'window.print()',
|
||||
'handleRelabelScan()'
|
||||
)
|
||||
|
||||
$missing = $required | Where-Object { -not $html.Contains($_) }
|
||||
if ($missing) { throw "Missing relabel workflow contract: $($missing -join ', ')" }
|
||||
|
||||
$renderStart = $html.LastIndexOf('function relabelView()')
|
||||
$renderEnd = $html.IndexOf('function relabelDetailView', $renderStart)
|
||||
if ($renderStart -lt 0 -or $renderEnd -lt 0) { throw 'Current relabel list view not found' }
|
||||
$render = $html.Substring($renderStart, $renderEnd - $renderStart)
|
||||
if (-not $render.Contains('setRelabelMode(')) { throw 'Relabel list does not expose mode tabs' }
|
||||
if (-not $render.Contains("t('relabelStart')")) { throw 'Relabel list does not use the start-work action' }
|
||||
|
||||
$detailView = [regex]::Match($html, '(?s)function relabelDetailView\(mode\).*?(?=function relabelBoxView)').Value
|
||||
foreach ($token in @('relabel-scan','relabel-box-preview','relabel-sku-preview','printRelabelLabel')) {
|
||||
if (-not $detailView.Contains($token)) { throw "Relabel detail page is missing $token" }
|
||||
}
|
||||
|
||||
$boxView = [regex]::Match($html, 'function relabelBoxView\(\)\{[^}]+\}').Value
|
||||
if (-not $boxView.Contains("relabelDetailView('box')")) { throw 'Carton-mark page does not use the shared detail view' }
|
||||
|
||||
$skuView = [regex]::Match($html, 'function relabelSkuView\(\)\{[^}]+\}').Value
|
||||
if (-not $skuView.Contains("relabelDetailView('sku')")) { throw 'SKU-label page does not use the shared detail view' }
|
||||
|
||||
Write-Output 'PASS: relabel workflow source contract'
|
||||
@@ -0,0 +1,36 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$pages = [ordered]@{
|
||||
'index.html' = 'home'
|
||||
'receive.html' = 'receive'
|
||||
'pallet.html' = 'pallet'
|
||||
'express-signin.html' = 'express-signin'
|
||||
'consolidation.html' = 'consolidation'
|
||||
'pick.html' = 'pick'
|
||||
'handover.html' = 'handover'
|
||||
'packing.html' = 'packing'
|
||||
'relabel.html' = 'relabel'
|
||||
'exception.html' = 'exception'
|
||||
}
|
||||
|
||||
foreach ($entry in $pages.GetEnumerator()) {
|
||||
$path = Join-Path $root $entry.Key
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
throw "Missing section page: $($entry.Key)"
|
||||
}
|
||||
|
||||
$html = Get-Content -Raw -Encoding UTF8 -LiteralPath $path
|
||||
if ($html -notmatch '<!doctype html>') { throw "$($entry.Key) is not an HTML document" }
|
||||
if ($html -notmatch "<body data-section=`"$([regex]::Escape($entry.Value))`">") {
|
||||
throw "$($entry.Key) does not declare section $($entry.Value)"
|
||||
}
|
||||
if ($html -notmatch '<link rel="stylesheet" href="assets/app\.css">') {
|
||||
throw "$($entry.Key) does not load the shared stylesheet"
|
||||
}
|
||||
if ($html -notmatch '<script src="assets/app\.js"></script>') {
|
||||
throw "$($entry.Key) does not load the shared application script"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "PASS: $($pages.Count) section pages load the shared application assets"
|
||||
@@ -0,0 +1,21 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const applicationSource = fs.readFileSync(path.join(__dirname, '..', 'assets', 'app.js'), 'utf8')
|
||||
.replace(/\s*bootstrap\(\);\s*$/, '');
|
||||
|
||||
const routingTest = String.raw`
|
||||
render=()=>{};
|
||||
toast=()=>{};
|
||||
quickLogin('W001');
|
||||
assert.equal(state.page,'receive-list');
|
||||
assert.equal(state.user.id,'W001');
|
||||
process.stdout.write('PASS: signing in from a section page opens that section\n');
|
||||
`;
|
||||
|
||||
const storage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
|
||||
const document = { body: { dataset: { section: 'receive' } }, getElementById: () => null };
|
||||
const window = { location: { pathname: '/receive.html', href: '' }, scrollTo: () => {} };
|
||||
const run = new Function('assert', 'localStorage', 'document', 'window', applicationSource + '\n' + routingTest);
|
||||
run(assert, storage, document, window);
|
||||
@@ -0,0 +1,26 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$html = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $PSScriptRoot '..\assets\app.js')
|
||||
|
||||
$listView = [regex]::Match($html, '(?s)function receiveListView\(\).*?(?=function plansView)').Value
|
||||
$photoView = [regex]::Match($html, '(?s)function receivePhotoView\(\).*?(?=function handleUnloadPhoto)').Value
|
||||
$completeUnload = [regex]::Match($html, '(?s)function completeUnload\(\).*?(?=function receiveView)').Value
|
||||
|
||||
$zhPalletBoxes = [string]::Concat([char]0x6258,[char]0x6570,'/',[char]0x7bb1,[char]0x6570)
|
||||
$zhOther = [char]0x5176 + [char]0x4ed6
|
||||
$requiredLabels = @(
|
||||
"Object.assign(EXTRA_LABELS.zh,{receivePalletBoxes:'$zhPalletBoxes',other:'$zhOther'})",
|
||||
"Object.assign(EXTRA_LABELS.en,{receivePalletBoxes:'Pallets/Cartons',other:'Other'})",
|
||||
"Object.assign(EXTRA_LABELS.es,{receivePalletBoxes:'Pallets/Cajas',other:'Otro'})"
|
||||
)
|
||||
$labelMissing = $requiredLabels | Where-Object { -not $html.Contains($_) }
|
||||
if ($labelMissing) { throw "Missing unload-specific labels: $($labelMissing -join ', ')" }
|
||||
|
||||
if (-not $listView.Contains("t('receivePalletBoxes')")) { throw 'Unload list does not use the combined count label' }
|
||||
if (-not $photoView.Contains("t('receivePalletBoxes')")) { throw 'Unload detail does not use the combined count label' }
|
||||
if (-not $html.Contains("const UNLOAD_PHOTO_STEPS=['licensePlate','containerSeal','doorOpen','other'];")) { throw 'Unload display steps do not include other photo' }
|
||||
if (-not $html.Contains("const UNLOAD_REQUIRED_PHOTO_STEPS=['licensePlate','containerSeal','doorOpen'];")) { throw 'Required unload photo steps are missing' }
|
||||
if (-not $photoView.Contains("UNLOAD_REQUIRED_PHOTO_STEPS.includes(key)?' *':''")) { throw 'Required photo marker is missing' }
|
||||
if (-not $photoView.Contains('UNLOAD_REQUIRED_PHOTO_STEPS.every(key=>photos[key])')) { throw 'Detail submit state does not use required photos only' }
|
||||
if (-not $completeUnload.Contains('UNLOAD_REQUIRED_PHOTO_STEPS.some(key=>!photos[key])')) { throw 'Completion validation still checks optional photos' }
|
||||
|
||||
Write-Output 'PASS: unload receiving photo required contract'
|
||||
+5620
-2436
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
const page = fs.readFileSync('货柜管理/订舱列表.html', 'utf8');
|
||||
|
||||
test('订舱列表查看详情进入只读订舱详情页并按订舱编号加载', () => {
|
||||
assert.match(page, /id="page-booking-view"/);
|
||||
assert.match(page, /onclick="openBookingView\(this\)"/);
|
||||
assert.match(page, /function openBookingView\(el\)/);
|
||||
assert.match(page, /data-booking-view-id/);
|
||||
});
|
||||
|
||||
test('订舱详情页提供三类页签及货物信息完整字段', () => {
|
||||
const start = page.indexOf('id="page-booking-view"');
|
||||
const end = page.indexOf('<!-- ====================', start + 1);
|
||||
const detail = page.slice(start, end === -1 ? page.length : end);
|
||||
|
||||
assert.match(detail, />订舱信息<\/button>/);
|
||||
assert.match(detail, />货物信息<\/button>/);
|
||||
assert.match(detail, />补充信息<\/button>/);
|
||||
for (const field of ['序号', '唛头', '提单件数', '包装类型', 'HS编码', '英文品名/货物描述', '毛重', '体积', '件数(大写)']) {
|
||||
assert.match(detail, new RegExp(`<th[^>]*>${field}<\\/th>`));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
const page = fs.readFileSync('财务管理/客户账户.html', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const marker = `function ${name}`;
|
||||
const start = page.lastIndexOf(marker);
|
||||
assert.notEqual(start, -1, `${name} should be defined in the customer account page`);
|
||||
const nextFunction = page.indexOf('\nfunction ', start + marker.length);
|
||||
const end = nextFunction === -1 ? page.length : nextFunction;
|
||||
return eval(`(${page.slice(start, end).trim()})`);
|
||||
}
|
||||
|
||||
function extractCreditHelpers() {
|
||||
const start = page.lastIndexOf('function normalizeCustomerCreditStatus');
|
||||
const end = page.indexOf('\nfunction calculateCustomerDeduction', start);
|
||||
assert.notEqual(start, -1, 'credit helpers should be defined in the customer account page');
|
||||
assert.notEqual(end, -1, 'credit deduction should follow credit helpers');
|
||||
return eval(`(()=>{${page.slice(start, end)};return {normalizeCustomerCreditStatus,customerCreditAvailable};})()`);
|
||||
}
|
||||
|
||||
test('expired credit is automatically disabled after the validity date', () => {
|
||||
const { normalizeCustomerCreditStatus: normalize } = extractCreditHelpers();
|
||||
const account = { creditStatus: '启用', creditExpireDate: '2026-09-07' };
|
||||
|
||||
normalize(account, '2026-09-08');
|
||||
|
||||
assert.equal(account.creditStatus, '停用');
|
||||
});
|
||||
|
||||
test('credit remains enabled on its validity date and is available when enabled', () => {
|
||||
const { normalizeCustomerCreditStatus: normalize, customerCreditAvailable: available } = extractCreditHelpers();
|
||||
const account = { creditStatus: '启用', creditExpireDate: '2026-09-08', creditLimit: 2000, creditUsed: 500 };
|
||||
|
||||
normalize(account, '2026-09-08');
|
||||
|
||||
assert.equal(account.creditStatus, '启用');
|
||||
assert.equal(available(account, '2026-09-08'), 1500);
|
||||
});
|
||||
|
||||
test('stopped credit cannot be used even when amount remains', () => {
|
||||
const { customerCreditAvailable: available } = extractCreditHelpers();
|
||||
const account = { creditStatus: '停用', creditExpireDate: '2026-12-31', creditLimit: 2000, creditUsed: 0 };
|
||||
|
||||
assert.equal(available(account, '2026-09-08'), 0);
|
||||
});
|
||||
|
||||
test('customer account UI exposes credit validity and status fields', () => {
|
||||
assert.match(page, /id="ca-credit-expire-date"/);
|
||||
assert.match(page, /id="ca-credit-status"/);
|
||||
assert.match(page, /<option value="启用">启用<\/option>/);
|
||||
assert.match(page, /<option value="停用">停用<\/option>/);
|
||||
assert.match(page, /<th>授信有效期<\/th>/);
|
||||
assert.match(page, /<th>授信状态<\/th>/);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const page = fs.readFileSync('财务管理/客户应收应付.html', 'utf8');
|
||||
|
||||
assert.match(
|
||||
page,
|
||||
/#page-customer-arap \.arap-table\{width:1810px;min-width:1810px;/,
|
||||
'客户应收应付表格总宽应与运行时序号列保持一致'
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/<colgroup><col style="width:32px"\/><col style="width:48px"\/><col style="width:190px"\/>/,
|
||||
'客户应收应付列表首列、序号列和操作列应为 32px、48px 和 190px'
|
||||
);
|
||||
|
||||
console.log('customer-arap column widths are compact');
|
||||
@@ -0,0 +1,109 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
const pagePaths = [
|
||||
'海外仓/尾程订单.html',
|
||||
'index.html',
|
||||
'通用/船期价格表.html',
|
||||
];
|
||||
|
||||
function extractLastMileHeaderLabels(page) {
|
||||
const tbodyStart = page.indexOf('<tbody id="tbody-last-mile-orders">');
|
||||
assert.notEqual(tbodyStart, -1, 'last-mile order table body should exist');
|
||||
const tableStart = page.lastIndexOf('<table', tbodyStart);
|
||||
assert.notEqual(tableStart, -1, 'last-mile order table should exist');
|
||||
const table = page.slice(tableStart, tbodyStart);
|
||||
return [...table.matchAll(/<th[^>]*>([\s\S]*?)<\/th>/g)].map(match =>
|
||||
match[1].replace(/<[^>]+>/g, '').trim(),
|
||||
);
|
||||
}
|
||||
|
||||
function extractFunction(page, name) {
|
||||
const marker = `function ${name}`;
|
||||
const start = page.indexOf(marker);
|
||||
assert.notEqual(start, -1, `${name} should be defined`);
|
||||
const end = page.indexOf('\nfunction ', start + marker.length);
|
||||
return page.slice(start, end === -1 ? page.length : end).trim();
|
||||
}
|
||||
|
||||
function renderRepresentativeRow(page) {
|
||||
const tbody = {
|
||||
innerHTML: '',
|
||||
closest: () => ({ classList: { toggle() {} } }),
|
||||
};
|
||||
const total = { textContent: '' };
|
||||
const document = {
|
||||
getElementById(id) {
|
||||
if (id === 'tbody-last-mile-orders') return tbody;
|
||||
if (id === 'lmo-total') return total;
|
||||
return null;
|
||||
},
|
||||
};
|
||||
const dependencies = {
|
||||
document,
|
||||
lastMileOrderData: [],
|
||||
currentLastMileTab: '全部',
|
||||
selectedLastMileOrderNos: new Set(),
|
||||
isLastMileInboundLocked: () => false,
|
||||
isLastMileOrderDetained: () => false,
|
||||
getLastMileRelabelOrder: () => null,
|
||||
renderLastMileWarningTags: () => 'WARNING-MARKER',
|
||||
findContainerByNo: () => null,
|
||||
renderContainerStatusTag: () => 'CONTAINER-STATUS',
|
||||
getContainerAbnormalState: () => null,
|
||||
renderAbnormalTag: () => 'ABNORMAL-STATUS',
|
||||
isLastMileConsolidated: () => false,
|
||||
lastMileStatusClass: () => 'tag-gray',
|
||||
getLastMileDisplayStatus: () => 'CURRENT-STATUS',
|
||||
renderLastMileDirectActions: () => 'OPERATIONS',
|
||||
updateLastMileSelectionSummary() {},
|
||||
lmoEsc: value => String(value ?? ''),
|
||||
};
|
||||
const names = Object.keys(dependencies);
|
||||
const render = Function(...names, `return (${extractFunction(page, 'renderLastMileOrders')})`)(
|
||||
...names.map(name => dependencies[name]),
|
||||
);
|
||||
|
||||
render([{
|
||||
no: 'ORDER-NO',
|
||||
serviceType: 'ORDER-TYPE',
|
||||
rejectionTime: 'REJECTION-TIME',
|
||||
inboundCode: 'INBOUND-CODE',
|
||||
owner: 'OWNER-NAME',
|
||||
warehouse: 'WAREHOUSE',
|
||||
transferWarehouse: 'TRANSFER-WAREHOUSE',
|
||||
fbaWarehouse: 'FBA-WAREHOUSE',
|
||||
containerNo: 'CONTAINER-NO',
|
||||
pallets: 1,
|
||||
boxes: 2,
|
||||
volume: 3,
|
||||
pieces: 4,
|
||||
volumeWeight: 5,
|
||||
portEta: 'PORT-ETA',
|
||||
actualArrivalTime: 'ACTUAL-ARRIVAL',
|
||||
eta: 'WAREHOUSE-ETA',
|
||||
noteSelf: '',
|
||||
noteCustomer: '',
|
||||
noteWarehouse: '',
|
||||
}]);
|
||||
|
||||
return [...tbody.innerHTML.matchAll(/<td[^>]*>([\s\S]*?)<\/td>/g)].map(match => match[1]);
|
||||
}
|
||||
|
||||
for (const pagePath of pagePaths) {
|
||||
test(`${pagePath} 尾程订单列表按业务优先级排列列`, () => {
|
||||
const page = fs.readFileSync(pagePath, 'utf8');
|
||||
const labels = extractLastMileHeaderLabels(page);
|
||||
const cells = renderRepresentativeRow(page);
|
||||
|
||||
assert.deepEqual(labels.slice(0, 6), ['', '操作', '预警', '尾程单号', '订单类型', '货主']);
|
||||
assert.equal(labels[labels.indexOf('预计到仓') + 1], '拒收时间');
|
||||
|
||||
assert.match(cells[2], /WARNING-MARKER/);
|
||||
assert.match(cells[3], /ORDER-NO/);
|
||||
assert.match(cells[4], /ORDER-TYPE/);
|
||||
assert.match(cells[5], /OWNER-NAME/);
|
||||
assert.match(cells[cells.findIndex(cell => cell.includes('WAREHOUSE-ETA')) + 1], /REJECTION-TIME/);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
const page = fs.readFileSync('海外仓/排车列表.html', 'utf8');
|
||||
|
||||
function extractFunction(name, dependencies = {}) {
|
||||
const marker = `function ${name}`;
|
||||
const start = page.indexOf(marker);
|
||||
assert.notEqual(start, -1, `${name} should be defined`);
|
||||
const nextFunction = page.indexOf('\nfunction ', start + marker.length);
|
||||
const end = nextFunction === -1 ? page.length : nextFunction;
|
||||
const names = Object.keys(dependencies);
|
||||
const values = Object.values(dependencies);
|
||||
return Function(...names, `return (${page.slice(start, end).trim()})`)(...values);
|
||||
}
|
||||
|
||||
function createElement(id) {
|
||||
const classes = new Set();
|
||||
return {
|
||||
id,
|
||||
hidden: false,
|
||||
tabIndex: -1,
|
||||
attributes: {},
|
||||
classList: {
|
||||
toggle(name, enabled) {
|
||||
if (enabled) classes.add(name);
|
||||
else classes.delete(name);
|
||||
},
|
||||
contains(name) {
|
||||
return classes.has(name);
|
||||
},
|
||||
},
|
||||
setAttribute(name, value) {
|
||||
this.attributes[name] = value;
|
||||
},
|
||||
focus() {
|
||||
this.focused = true;
|
||||
},
|
||||
scrollIntoView() {
|
||||
this.scrolledIntoView = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('排车详情以车辆信息、关联订单和关联单据三个标签页组织内容', () => {
|
||||
const start = page.indexOf('id="page-outbound-vehicle-detail"');
|
||||
const end = page.indexOf('<!-- ====================', start + 1);
|
||||
const detail = page.slice(start, end === -1 ? page.length : end);
|
||||
|
||||
assert.match(detail, />车辆信息<\/button>[\s\S]*?>关联订单<\/button>[\s\S]*?>关联单据<\/button>/);
|
||||
assert.match(detail, /role="tablist"/);
|
||||
assert.equal((detail.match(/role="tabpanel"/g) || []).length, 3);
|
||||
assert.match(detail, /<th>排车单号<\/th><th>提货方式<\/th><th>关联派送单号<\/th><th>关联出库单号<\/th>/);
|
||||
});
|
||||
|
||||
test('排车详情标签切换只显示选中的内容并同步无障碍状态', () => {
|
||||
const activateTab = extractFunction('activateOutboundVehicleDetailTab');
|
||||
const keys = ['vehicle', 'orders', 'documents'];
|
||||
const nodes = new Map();
|
||||
keys.forEach(key => {
|
||||
nodes.set(`ovd-tab-${key}`, createElement(`ovd-tab-${key}`));
|
||||
nodes.set(`ovd-panel-${key}`, createElement(`ovd-panel-${key}`));
|
||||
});
|
||||
const previousDocument = global.document;
|
||||
global.document = { getElementById: id => nodes.get(id) || null };
|
||||
|
||||
try {
|
||||
activateTab('documents', true);
|
||||
|
||||
assert.equal(nodes.get('ovd-tab-documents').attributes['aria-selected'], 'true');
|
||||
assert.equal(nodes.get('ovd-tab-documents').tabIndex, 0);
|
||||
assert.equal(nodes.get('ovd-tab-documents').focused, true);
|
||||
assert.equal(nodes.get('ovd-tab-documents').scrolledIntoView, true);
|
||||
assert.equal(nodes.get('ovd-panel-documents').hidden, false);
|
||||
assert.equal(nodes.get('ovd-panel-vehicle').hidden, true);
|
||||
assert.equal(nodes.get('ovd-panel-orders').hidden, true);
|
||||
} finally {
|
||||
global.document = previousDocument;
|
||||
}
|
||||
});
|
||||
|
||||
test('排车详情标签支持左右方向键循环切换', () => {
|
||||
let activated = null;
|
||||
const handleKeydown = extractFunction('handleOutboundVehicleDetailTabKeydown', {
|
||||
activateOutboundVehicleDetailTab(key, focusTab) {
|
||||
activated = { key, focusTab };
|
||||
},
|
||||
});
|
||||
let prevented = false;
|
||||
|
||||
handleKeydown({
|
||||
key: 'ArrowRight',
|
||||
currentTarget: { id: 'ovd-tab-documents' },
|
||||
preventDefault() {
|
||||
prevented = true;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(prevented, true);
|
||||
assert.deepEqual(activated, { key: 'vehicle', focusTab: true });
|
||||
});
|
||||
|
||||
test('关联单据按每个出库单生成一行并带出排车与转派送信息', () => {
|
||||
const getDocuments = extractFunction('getOutboundVehicleDocuments');
|
||||
const documents = getDocuments({
|
||||
vehicleNo: 'PC20260908001',
|
||||
deliveryNo: 'PS20260908002',
|
||||
delivery: { pickupMethod: '一提多卸' },
|
||||
orders: [{ orderNo: 'CK20260908003' }, { orderNo: 'CK20260908004' }],
|
||||
});
|
||||
|
||||
assert.deepEqual(documents, [
|
||||
{
|
||||
vehicleNo: 'PC20260908001',
|
||||
pickupMethod: '一提多卸',
|
||||
dispatchNo: 'PS20260908002',
|
||||
outboundNo: 'CK20260908003',
|
||||
},
|
||||
{
|
||||
vehicleNo: 'PC20260908001',
|
||||
pickupMethod: '一提多卸',
|
||||
dispatchNo: 'PS20260908002',
|
||||
outboundNo: 'CK20260908004',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('未生成转派送单时关联单据保留出库关系并显示占位值', () => {
|
||||
const getDocuments = extractFunction('getOutboundVehicleDocuments');
|
||||
|
||||
assert.deepEqual(getDocuments({
|
||||
vehicleNo: 'PC20260908001',
|
||||
orders: [{ orderNo: 'CK20260908003' }],
|
||||
}), [{
|
||||
vehicleNo: 'PC20260908001',
|
||||
pickupMethod: '—',
|
||||
dispatchNo: '—',
|
||||
outboundNo: 'CK20260908003',
|
||||
}]);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
const page = fs.readFileSync('海外仓/入库单.html', 'utf8');
|
||||
|
||||
test('海外仓入库列表在入库单号后展示客户并绑定记录字段', () => {
|
||||
const start = page.indexOf('id="page-fba-inbound-order"');
|
||||
const end = page.indexOf('<!-- ==================== FBA RETURN - 换标单', start);
|
||||
const section = page.slice(start, end);
|
||||
|
||||
assert.match(
|
||||
section,
|
||||
/<th[^>]*>入库单号<\/th>\s*<th[^>]*>客户<\/th>\s*<th[^>]*>入仓码<\/th>/,
|
||||
'客户列应紧跟在入库单号之后'
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/<td[^>]*>\$\{lmoEsc\(d\.customer\|\|d\.owner\|\|'—'\)\}<\/td>/,
|
||||
'入库列表行应展示客户字段'
|
||||
);
|
||||
assert.match(page, /fbaInboundOrderData\.forEach\(record=>\{if\(!record\.customer\)record\.customer=record\.owner\}\)/, '入库记录应补齐客户字段');
|
||||
});
|
||||
|
||||
test('编辑入库单可维护客户字段', () => {
|
||||
assert.match(page, /id="fbio-edit-customer"/);
|
||||
assert.match(page, /originalOpenFbioEditModal\(inboundNo\)/);
|
||||
assert.match(page, /record\.customer=customer/);
|
||||
});
|
||||
|
||||
console.log('overseas inbound customer column checks are defined');
|
||||
@@ -0,0 +1,138 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
const pages = [
|
||||
['入库单', fs.readFileSync('海外仓/入库单.html', 'utf8')],
|
||||
['出库单', fs.readFileSync('海外仓/出库单.html', 'utf8')],
|
||||
];
|
||||
|
||||
function extractFunction(page, name) {
|
||||
const marker = `function ${name}`;
|
||||
const start = page.indexOf(marker);
|
||||
assert.notEqual(start, -1, `${name} should be defined`);
|
||||
const nextFunction = page.indexOf('\nfunction ', start + marker.length);
|
||||
const end = nextFunction === -1 ? page.length : nextFunction;
|
||||
return Function('lmoEsc', 'fbaDetailDisplay', `return (${page.slice(start, end).trim()})`)(
|
||||
value => String(value).replace(/[&<>"']/g, character => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
})[character]),
|
||||
value => value === null || value === undefined || value === '' ? '—' : String(value),
|
||||
);
|
||||
}
|
||||
|
||||
function createElement(id) {
|
||||
const classes = new Set();
|
||||
return {
|
||||
id,
|
||||
hidden: false,
|
||||
tabIndex: -1,
|
||||
attributes: {},
|
||||
classList: {
|
||||
toggle(name, enabled) {
|
||||
if (enabled) classes.add(name);
|
||||
else classes.delete(name);
|
||||
},
|
||||
contains(name) {
|
||||
return classes.has(name);
|
||||
},
|
||||
},
|
||||
setAttribute(name, value) {
|
||||
this.attributes[name] = value;
|
||||
},
|
||||
focus() {
|
||||
this.focused = true;
|
||||
},
|
||||
scrollIntoView() {
|
||||
this.scrolledIntoView = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
for (const [name, page] of pages) {
|
||||
test(`${name}详情在应收费用后提供操作记录页签与面板`, () => {
|
||||
const detailMarker = name === '入库单' ? 'page-fba-inbound-order-detail' : 'page-fba-outbound-order-detail';
|
||||
const detailStart = page.indexOf(`id="${detailMarker}"`);
|
||||
const detailEnd = page.indexOf('<!-- ====================', detailStart + detailMarker.length);
|
||||
const detail = page.slice(detailStart, detailEnd);
|
||||
|
||||
assert.match(detail, />应收费用<\/button>[\s\S]*?>操作记录<\/button>/);
|
||||
assert.match(detail, /<th>操作时间<\/th><th>操作内容<\/th><th>操作人<\/th><th>操作结果<\/th><th>备注<\/th>/);
|
||||
});
|
||||
|
||||
test(`${name}操作记录渲染真实记录、结果标签和空状态`, () => {
|
||||
const render = extractFunction(page, 'renderFbaOperationRecords');
|
||||
const body = { innerHTML: '' };
|
||||
const previousDocument = global.document;
|
||||
global.document = { getElementById: id => id === 'operation-body' ? body : null };
|
||||
|
||||
try {
|
||||
render('operation-body', [{
|
||||
time: '2026-09-08 10:30',
|
||||
action: '审核应收费用',
|
||||
operator: '张经理',
|
||||
result: '成功',
|
||||
remark: '费用审核通过',
|
||||
}]);
|
||||
|
||||
assert.match(body.innerHTML, /2026-09-08 10:30/);
|
||||
assert.match(body.innerHTML, /审核应收费用/);
|
||||
assert.match(body.innerHTML, /张经理/);
|
||||
assert.match(body.innerHTML, /tag-green[^>]*>成功/);
|
||||
assert.match(body.innerHTML, /费用审核通过/);
|
||||
|
||||
render('operation-body', []);
|
||||
assert.match(body.innerHTML, /colspan="5"[^>]*>暂无操作记录/);
|
||||
} finally {
|
||||
global.document = previousDocument;
|
||||
}
|
||||
});
|
||||
|
||||
test(`${name}详情切换操作记录时激活面板并将页签带入可视区域`, () => {
|
||||
const inboundSwitch = extractFunction(page, 'switchFbaInboundDetailTab');
|
||||
const outboundSwitch = extractFunction(page, 'activateFbaOutboundDetailTab');
|
||||
const inboundKeys = ['basic', 'pallet', 'appointment', 'fee', 'operations'];
|
||||
const outboundKeys = ['outbound', 'handover', 'pallet', 'fees', 'operations'];
|
||||
const inboundTabs = inboundKeys.map(key => createElement(`fbd-inbound-tab-${key}`));
|
||||
const inboundPanels = inboundKeys.map(key => createElement(`fbd-inbound-panel-${key}`));
|
||||
const outboundNodes = new Map();
|
||||
outboundKeys.forEach(key => {
|
||||
outboundNodes.set(`fbd-outbound-tab-${key}`, createElement(`fbd-outbound-tab-${key}`));
|
||||
outboundNodes.set(`fbd-outbound-panel-${key}`, createElement(`fbd-outbound-panel-${key}`));
|
||||
});
|
||||
const inboundPage = {
|
||||
querySelectorAll(selector) {
|
||||
return selector === '.fbd-inbound-tab' ? inboundTabs : inboundPanels;
|
||||
},
|
||||
};
|
||||
const previousDocument = global.document;
|
||||
global.document = {
|
||||
getElementById(id) {
|
||||
if (id === 'page-fba-inbound-order-detail') return inboundPage;
|
||||
return inboundTabs.find(tab => tab.id === id) || outboundNodes.get(id) || null;
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
inboundSwitch('operations');
|
||||
outboundSwitch('operations', false);
|
||||
|
||||
const inboundTab = inboundTabs.at(-1);
|
||||
const inboundPanel = inboundPanels.at(-1);
|
||||
const outboundTab = outboundNodes.get('fbd-outbound-tab-operations');
|
||||
const outboundPanel = outboundNodes.get('fbd-outbound-panel-operations');
|
||||
assert.equal(inboundTab.attributes['aria-selected'], 'true');
|
||||
assert.equal(inboundPanel.hidden, false);
|
||||
assert.equal(inboundTab.scrolledIntoView, true);
|
||||
assert.equal(outboundTab.attributes['aria-selected'], 'true');
|
||||
assert.equal(outboundPanel.hidden, false);
|
||||
assert.equal(outboundTab.scrolledIntoView, true);
|
||||
} finally {
|
||||
global.document = previousDocument;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
const page = fs.readFileSync('海外仓/退货单.html', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const start = page.indexOf(`function ${name}`);
|
||||
assert.notEqual(start, -1, `${name} should be defined in the return order page`);
|
||||
const nextFunction = page.indexOf('\nfunction ', start + 1);
|
||||
const dataDeclaration = page.indexOf('\nconst fbaReturnOrderData', start + 1);
|
||||
const endCandidates = [nextFunction, dataDeclaration].filter(index => index !== -1);
|
||||
const end = endCandidates.length ? Math.min(...endCandidates) : page.length;
|
||||
return eval(`(${page.slice(start, end).trim()})`);
|
||||
}
|
||||
|
||||
function extractReturnNumberHelpers() {
|
||||
const start = page.indexOf('function fbaReturnDateStamp');
|
||||
const end = page.indexOf('\nconst fbaReturnOrderData', start);
|
||||
assert.notEqual(start, -1, 'return number helpers should be defined in the return order page');
|
||||
assert.notEqual(end, -1, 'return order data should follow return number helpers');
|
||||
return eval(`(()=>{${page.slice(start, end)};return {formatFbaReturnOrderNo,nextFbaReturnOrderNo};})()`);
|
||||
}
|
||||
|
||||
test('return order number follows BYTH date, fixed 01 segment, and four digit sequence', () => {
|
||||
const { formatFbaReturnOrderNo: formatNo } = extractReturnNumberHelpers();
|
||||
const result = formatNo(new Date(2026, 7, 12), 1);
|
||||
assert.equal(result, 'BYTH260812010001');
|
||||
});
|
||||
|
||||
test('return order number sequence increments per date without reusing existing numbers', () => {
|
||||
const { nextFbaReturnOrderNo: nextNo } = extractReturnNumberHelpers();
|
||||
const existing = [
|
||||
{ returnNo: 'BYTH260812010001' },
|
||||
{ returnNo: 'BYTH260812010003' },
|
||||
{ returnNo: 'BYTH260811010099' },
|
||||
];
|
||||
assert.equal(nextNo(new Date(2026, 7, 12), existing), 'BYTH260812010004');
|
||||
});
|
||||
|
||||
test('return list columns include customer and inbound code, hide last-mile order, and separate statuses', () => {
|
||||
const header = page.match(/<table class="fbrt-table"[\s\S]*?<thead><tr>([\s\S]*?)<\/tr><\/thead>/)?.[1] || '';
|
||||
assert.match(header, /<th>客户<\/th>/);
|
||||
assert.match(header, /<th>入仓码<\/th>/);
|
||||
assert.match(header, /<th>换标单号<\/th><th>换标状态<\/th><th>是否签收<\/th>/);
|
||||
assert.doesNotMatch(header, /<th>尾程订单号<\/th>/);
|
||||
});
|
||||
|
||||
test('relabel status and sign status use independent option sets', () => {
|
||||
const relabelStatusOptions = [...page.matchAll(/id="fbrt-edit-relabel-status"[^>]*>[\s\S]*?<\/select>/g)]
|
||||
.map(match => match[0])
|
||||
.join('');
|
||||
const signStatusOptions = [...page.matchAll(/id="fbrt-edit-sign-status"[^>]*>[\s\S]*?<\/select>/g)]
|
||||
.map(match => match[0])
|
||||
.join('');
|
||||
assert.match(relabelStatusOptions, /待处理|换标中|已完成/);
|
||||
assert.doesNotMatch(relabelStatusOptions, /部分签收|全部签收/);
|
||||
assert.match(signStatusOptions, /部分签收/);
|
||||
assert.match(signStatusOptions, /全部签收/);
|
||||
assert.doesNotMatch(signStatusOptions, /换标中|已完成|待处理/);
|
||||
assert.match(page, /relabelStatus:'换标中'/);
|
||||
assert.match(page, /signStatus:'部分签收'/);
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user