优化
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180" viewBox="0 0 180 180" fill="none">
|
||||||
|
<rect width="180" height="180" rx="16" fill="#F8FAFC"/>
|
||||||
|
<rect x="12" y="12" width="156" height="156" rx="12" stroke="#CBD5E1" stroke-width="2"/>
|
||||||
|
<g fill="#1565C0">
|
||||||
|
<path d="M28 28h38v38H28V28Zm9 9v20h20V37H37ZM114 28h38v38h-38V28Zm9 9v20h20V37h-20ZM28 114h38v38H28v-38Zm9 9v20h20v-20H37Z"/>
|
||||||
|
<rect x="82" y="28" width="10" height="10" rx="2"/>
|
||||||
|
<rect x="82" y="46" width="18" height="10" rx="2"/>
|
||||||
|
<rect x="74" y="70" width="10" height="10" rx="2"/>
|
||||||
|
<rect x="92" y="70" width="18" height="10" rx="2"/>
|
||||||
|
<rect x="118" y="78" width="10" height="18" rx="2"/>
|
||||||
|
<rect x="136" y="82" width="16" height="10" rx="2"/>
|
||||||
|
<rect x="74" y="100" width="18" height="10" rx="2"/>
|
||||||
|
<rect x="100" y="92" width="10" height="18" rx="2"/>
|
||||||
|
<rect x="82" y="118" width="10" height="10" rx="2"/>
|
||||||
|
<rect x="100" y="118" width="28" height="10" rx="2"/>
|
||||||
|
<rect x="136" y="110" width="16" height="18" rx="2"/>
|
||||||
|
<rect x="74" y="136" width="18" height="16" rx="2"/>
|
||||||
|
<rect x="108" y="138" width="10" height="14" rx="2"/>
|
||||||
|
<rect x="128" y="136" width="24" height="16" rx="2"/>
|
||||||
|
</g>
|
||||||
|
<rect x="46" y="74" width="88" height="32" rx="16" fill="#0F172A" fill-opacity=".88"/>
|
||||||
|
<path d="M60 90h60" stroke="#FFF" stroke-width="3" stroke-linecap="round" opacity=".72"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
+198
-5
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>柏悦物流 CHONGCHONG GLOBAL | 跨境物流一站式解决方案</title>
|
<title>柏悦物流 CHONGCHONG GLOBAL | 跨境物流一站式解决方案</title>
|
||||||
<meta name="description" content="柏悦物流 - 专业货代服务,提供海外采购、美本土卡车、美本土散板、海运专线、一件代发等跨境物流解决方案。">
|
<meta name="description" content="柏悦物流 - 专业货代服务,提供海外采购、美本土卡车、美本土散板、海运专线、一件代发等跨境物流解决方案。">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%231565c0'/%3E%3Cpath d='M18 14h19c9 0 14 4 14 11 0 5-3 8-8 9 6 1 9 4 9 10 0 8-6 12-16 12H18V14zm10 9v8h8c4 0 6-1 6-4s-2-4-6-4h-8zm0 16v9h9c4 0 6-2 6-5s-2-4-7-4h-8z' fill='white'/%3E%3C/svg%3E">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
@@ -508,6 +509,54 @@
|
|||||||
.dash-sidebar.open { transform: translateX(0); }
|
.dash-sidebar.open { transform: translateX(0); }
|
||||||
.dash-menu-btn { display: inline-flex; }
|
.dash-menu-btn { display: inline-flex; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 官网内认证流程 ===== */
|
||||||
|
body.auth-modal-open { overflow: hidden; }
|
||||||
|
.auth-modal { position: fixed; inset: 0; z-index: 80; display: grid; place-items: center; padding: 24px; }
|
||||||
|
.auth-modal[hidden] { display: none; }
|
||||||
|
.auth-modal-backdrop { position: absolute; inset: 0; border: 0; background: rgba(5, 18, 34, .76); cursor: default; }
|
||||||
|
.auth-dialog { position: relative; width: min(1040px, 100%); max-height: calc(100vh - 48px); overflow: hidden; border-radius: 16px; background: #fff; box-shadow: 0 28px 80px rgba(4, 18, 34, .34); }
|
||||||
|
.auth-close { position: absolute; top: 16px; right: 16px; z-index: 3; display: inline-grid; width: 40px; height: 40px; place-items: center; border: 1px solid #d9e2ec; border-radius: 10px; background: #fff; color: #334155; transition: background-color .2s ease, border-color .2s ease, color .2s ease; }
|
||||||
|
.auth-close:hover { border-color: #1565c0; background: #f0f7ff; color: #0d47a1; }
|
||||||
|
.auth-close:focus-visible, .auth-dialog button:focus-visible, .auth-dialog input:focus-visible { outline: 3px solid rgba(21, 101, 192, .28); outline-offset: 2px; }
|
||||||
|
.auth-layout { display: grid; grid-template-columns: minmax(320px, .82fr) minmax(0, 1.18fr); min-height: 620px; }
|
||||||
|
.auth-story { position: relative; display: flex; min-width: 0; flex-direction: column; justify-content: space-between; overflow: hidden; padding: 48px 42px; background: #0b2d55 url('assets/core-ocean.jpg') center/cover no-repeat; color: #fff; }
|
||||||
|
.auth-modal[data-mode="register"] .auth-story { background-image: url('assets/core-fulfillment.jpg'); }
|
||||||
|
.auth-story::before { content: ''; position: absolute; inset: 0; background: linear-gradient(155deg, rgba(7, 33, 64, .9), rgba(13, 71, 161, .78)); }
|
||||||
|
.auth-story > * { position: relative; z-index: 1; }
|
||||||
|
.auth-story-logo { width: 161px; height: 60px; object-fit: contain; object-position: left center; margin-bottom: 56px; }
|
||||||
|
.auth-story h2 { max-width: 12ch; margin: 0 0 14px; font-size: clamp(30px, 3vw, 42px); line-height: 1.15; letter-spacing: -.02em; }
|
||||||
|
.auth-story p { max-width: 34ch; color: rgba(255, 255, 255, .82); line-height: 1.75; }
|
||||||
|
.auth-benefits { display: grid; gap: 14px; margin-top: 36px; }
|
||||||
|
.auth-benefit { display: flex; align-items: center; gap: 10px; color: rgba(255, 255, 255, .94); font-size: 14px; }
|
||||||
|
.auth-benefit i { color: #65d46e; }
|
||||||
|
.auth-form-side { min-width: 0; overflow-y: auto; padding: 58px 64px 48px; }
|
||||||
|
.auth-panel { max-width: 470px; margin: 0 auto; }
|
||||||
|
.auth-panel[hidden] { display: none; }
|
||||||
|
.auth-panel h2 { margin: 0 0 8px; color: #0f172a; font-size: 30px; font-weight: 800; letter-spacing: -.02em; }
|
||||||
|
.auth-switch-copy { margin-bottom: 28px; color: #64748b; }
|
||||||
|
.auth-link { color: #1565c0; font-weight: 700; }
|
||||||
|
.auth-link:hover { color: #0d47a1; text-decoration: underline; }
|
||||||
|
.auth-form { display: grid; gap: 18px; }
|
||||||
|
.auth-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
||||||
|
.auth-field label { display: block; margin-bottom: 7px; color: #334155; font-size: 14px; font-weight: 600; }
|
||||||
|
.auth-input-wrap { position: relative; }
|
||||||
|
.auth-input-wrap > i { position: absolute; top: 50%; left: 14px; color: #64748b; transform: translateY(-50%); pointer-events: none; }
|
||||||
|
.auth-input-wrap .input-field { padding-left: 42px; }
|
||||||
|
.auth-input-wrap .toggle-password { position: absolute; top: 50%; right: 10px; display: grid; width: 36px; height: 36px; place-items: center; color: #64748b; transform: translateY(-50%); }
|
||||||
|
.auth-dialog .input-field { min-height: 48px; border: 1px solid #cbd5e1; border-radius: 10px; background: #fff; color: #0f172a; font-size: 16px; }
|
||||||
|
.auth-dialog .input-field::placeholder { color: #64748b; }
|
||||||
|
.auth-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; color: #64748b; font-size: 14px; }
|
||||||
|
.auth-check { display: flex; min-width: 0; align-items: flex-start; gap: 9px; color: #64748b; font-size: 14px; line-height: 1.55; }
|
||||||
|
.auth-check input { flex: 0 0 auto; width: 16px; height: 16px; margin-top: 2px; accent-color: #1565c0; }
|
||||||
|
.auth-submit { min-height: 50px; border-radius: 10px; }
|
||||||
|
.auth-divider { display: flex; align-items: center; gap: 14px; margin: 24px 0 18px; color: #64748b; font-size: 13px; }
|
||||||
|
.auth-divider::before, .auth-divider::after { content: ''; height: 1px; flex: 1; background: #e2e8f0; }
|
||||||
|
.auth-socials { display: flex; justify-content: center; gap: 12px; }
|
||||||
|
.auth-socials button { display: grid; width: 44px; height: 44px; place-items: center; border: 1px solid #dbe4ee; border-radius: 12px; background: #fff; }
|
||||||
|
.auth-socials button:hover { border-color: #9fb7ca; background: #f6f9fb; }
|
||||||
|
@media (max-width: 900px) { .auth-layout { grid-template-columns: minmax(0, 1fr); min-height: 0; } .auth-story { display: none; } .auth-form-side { padding: 54px 34px 36px; } }
|
||||||
|
@media (max-width: 520px) { .auth-modal { align-items: end; padding: 0; } .auth-dialog { width: 100%; max-height: 94vh; border-radius: 16px 16px 0 0; } .auth-form-side { padding: 54px 20px 28px; } .auth-panel h2 { font-size: 26px; } .auth-field-grid { grid-template-columns: minmax(0, 1fr); } .auth-row { align-items: flex-start; flex-direction: column; gap: 10px; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-gray-800 antialiased">
|
<body class="text-gray-800 antialiased">
|
||||||
@@ -540,8 +589,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 未登录:登录注册按钮 -->
|
<!-- 未登录:登录注册按钮 -->
|
||||||
<div id="navAuthOut" class="hidden lg:flex items-center space-x-4">
|
<div id="navAuthOut" class="hidden lg:flex items-center space-x-4">
|
||||||
<button onclick="window.location.href='login.html'" class="nav-link text-white/90 hover:text-white font-medium px-4 py-2 transition-colors" data-i18n="nav_login">登录</button>
|
<button onclick="openAuthModal('login')" class="nav-link text-white/90 hover:text-white font-medium px-4 py-2 transition-colors" data-i18n="nav_login">登录</button>
|
||||||
<button onclick="window.location.href='register.html'" class="bg-white text-brand-blue hover:bg-brand-green hover:text-white font-semibold px-6 py-2.5 rounded-full transition-all duration-300 transform hover:scale-105" data-i18n="nav_register">
|
<button onclick="openAuthModal('register')" class="bg-white text-brand-blue hover:bg-brand-green hover:text-white font-semibold px-6 py-2.5 rounded-full transition-all duration-300 transform hover:scale-105" data-i18n="nav_register">
|
||||||
免费注册
|
免费注册
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -578,8 +627,8 @@
|
|||||||
<a href="#about" onclick="navigateTo('about'); closeMobileMenu();" class="text-gray-700 hover:text-brand-blue font-medium py-2 border-b border-gray-100" data-i18n="nav_about">关于</a>
|
<a href="#about" onclick="navigateTo('about'); closeMobileMenu();" class="text-gray-700 hover:text-brand-blue font-medium py-2 border-b border-gray-100" data-i18n="nav_about">关于</a>
|
||||||
<a href="#contact" onclick="navigateTo('contact'); closeMobileMenu();" class="text-gray-700 hover:text-brand-blue font-medium py-2 border-b border-gray-100" data-i18n="nav_contact">联系</a>
|
<a href="#contact" onclick="navigateTo('contact'); closeMobileMenu();" class="text-gray-700 hover:text-brand-blue font-medium py-2 border-b border-gray-100" data-i18n="nav_contact">联系</a>
|
||||||
<div id="navAuthOutMobile">
|
<div id="navAuthOutMobile">
|
||||||
<button onclick="window.location.href='login.html'; closeMobileMenu();" class="text-left text-gray-700 hover:text-brand-blue font-medium py-2 border-b border-gray-100" data-i18n="nav_login">登录</button>
|
<button onclick="closeMobileMenu(); openAuthModal('login');" class="text-left text-gray-700 hover:text-brand-blue font-medium py-2 border-b border-gray-100" data-i18n="nav_login">登录</button>
|
||||||
<button onclick="window.location.href='register.html'; closeMobileMenu();" class="btn-primary text-white font-semibold py-3 rounded-xl mt-4" data-i18n="nav_register">免费注册</button>
|
<button onclick="closeMobileMenu(); openAuthModal('register');" class="btn-primary text-white font-semibold py-3 rounded-xl mt-4" data-i18n="nav_register">免费注册</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="navAuthInMobile" class="hidden">
|
<div id="navAuthInMobile" class="hidden">
|
||||||
<button onclick="window.location.href='workbench.html'; closeMobileMenu();" class="text-left text-gray-700 hover:text-brand-blue font-medium py-2 border-b border-gray-100" data-i18n="nav_dashboard">工作台</button>
|
<button onclick="window.location.href='workbench.html'; closeMobileMenu();" class="text-left text-gray-700 hover:text-brand-blue font-medium py-2 border-b border-gray-100" data-i18n="nav_dashboard">工作台</button>
|
||||||
@@ -1177,7 +1226,7 @@
|
|||||||
<h2 class="text-4xl lg:text-5xl font-bold text-white mb-6" data-i18n="cta_title">开启您的出海之旅</h2>
|
<h2 class="text-4xl lg:text-5xl font-bold text-white mb-6" data-i18n="cta_title">开启您的出海之旅</h2>
|
||||||
<p class="text-xl text-white/80 mb-10" data-i18n="cta_desc">立即注册,获取专属物流方案与报价,让柏悦物流成为您的跨境物流伙伴。</p>
|
<p class="text-xl text-white/80 mb-10" data-i18n="cta_desc">立即注册,获取专属物流方案与报价,让柏悦物流成为您的跨境物流伙伴。</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<button id="ctaPrimary" onclick="window.location.href='register.html'" class="btn-accent text-white font-bold px-10 py-4 rounded-full text-lg" data-i18n="cta_btn1">
|
<button id="ctaPrimary" onclick="openAuthModal('register')" class="btn-accent text-white font-bold px-10 py-4 rounded-full text-lg" data-i18n="cta_btn1">
|
||||||
免费注册账号
|
免费注册账号
|
||||||
</button>
|
</button>
|
||||||
<button onclick="showToast(t('toast_contact'))" class="bg-white/10 backdrop-blur-sm border-2 border-white/30 text-white hover:bg-white/20 font-bold px-10 py-4 rounded-full text-lg transition-all" data-i18n="cta_btn2">
|
<button onclick="showToast(t('toast_contact'))" class="bg-white/10 backdrop-blur-sm border-2 border-white/30 text-white hover:bg-white/20 font-bold px-10 py-4 rounded-full text-lg transition-all" data-i18n="cta_btn2">
|
||||||
@@ -1271,6 +1320,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- 官网内登录 / 注册 -->
|
||||||
|
<div id="authModal" class="auth-modal" data-mode="login" role="dialog" aria-modal="true" aria-labelledby="authDialogTitle" aria-hidden="true" hidden>
|
||||||
|
<button type="button" class="auth-modal-backdrop" aria-label="关闭登录注册窗口" onclick="closeAuthModal()"></button>
|
||||||
|
<div class="auth-dialog" role="document">
|
||||||
|
<button type="button" class="auth-close" aria-label="关闭" onclick="closeAuthModal()"><i class="fas fa-xmark" aria-hidden="true"></i></button>
|
||||||
|
<div class="auth-layout">
|
||||||
|
<aside class="auth-story" aria-hidden="true">
|
||||||
|
<div>
|
||||||
|
<img src="assets/logo.png" alt="" class="auth-story-logo">
|
||||||
|
<div id="authLoginStory">
|
||||||
|
<h2 data-i18n="login_welcome">欢迎回来</h2>
|
||||||
|
<p data-i18n="login_desc">登录柏悦物流平台,实时追踪货物轨迹,管理订单与库存。</p>
|
||||||
|
</div>
|
||||||
|
<div id="authRegisterStory" hidden>
|
||||||
|
<h2 data-i18n="reg_welcome">加入我们</h2>
|
||||||
|
<p data-i18n="reg_welcome_desc">注册柏悦物流企业账号,开启高效跨境物流体验。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="authLoginBenefits" class="auth-benefits">
|
||||||
|
<div class="auth-benefit"><i class="fas fa-circle-check"></i><span data-i18n="login_feat1">全程可视化追踪</span></div>
|
||||||
|
<div class="auth-benefit"><i class="fas fa-circle-check"></i><span data-i18n="login_feat2">智能订单管理</span></div>
|
||||||
|
<div class="auth-benefit"><i class="fas fa-circle-check"></i><span data-i18n="login_feat3">在线询价下单</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="authRegisterBenefits" class="auth-benefits" hidden>
|
||||||
|
<div class="auth-benefit"><i class="fas fa-circle-check"></i><span data-i18n="reg_feat1">新用户专属优惠</span></div>
|
||||||
|
<div class="auth-benefit"><i class="fas fa-circle-check"></i><span data-i18n="reg_feat2">免费获取物流方案</span></div>
|
||||||
|
<div class="auth-benefit"><i class="fas fa-circle-check"></i><span data-i18n="reg_feat3">专属客户经理对接</span></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="auth-form-side">
|
||||||
|
<section id="loginPanel" class="auth-panel">
|
||||||
|
<h2 id="authDialogTitle" data-i18n="login_title">账号登录</h2>
|
||||||
|
<p class="auth-switch-copy"><span data-i18n="login_no_account">还没有账号?</span> <button type="button" class="auth-link" onclick="switchAuthMode('register')" data-i18n="login_register_link">立即注册</button></p>
|
||||||
|
<form id="loginForm" class="auth-form">
|
||||||
|
<div class="auth-field">
|
||||||
|
<label for="loginAccount" data-i18n="login_account_label">邮箱 / 手机号</label>
|
||||||
|
<div class="auth-input-wrap"><i class="fas fa-user"></i><input id="loginAccount" class="input-field w-full px-4" type="text" name="account" required autocomplete="username" placeholder="请输入邮箱或手机号" data-i18n-ph="login_account_ph"></div>
|
||||||
|
</div>
|
||||||
|
<div class="auth-field">
|
||||||
|
<label for="loginPassword" data-i18n="login_password_label">密码</label>
|
||||||
|
<div class="auth-input-wrap"><i class="fas fa-lock"></i><input id="loginPassword" class="input-field w-full px-4 pr-12" type="password" name="password" required autocomplete="current-password" placeholder="请输入密码" data-i18n-ph="login_password_ph"><button type="button" class="toggle-password" aria-label="显示或隐藏密码"><i class="fas fa-eye"></i></button></div>
|
||||||
|
</div>
|
||||||
|
<div class="auth-row">
|
||||||
|
<label class="auth-check"><input type="checkbox" name="remember"><span data-i18n="login_remember">记住我</span></label>
|
||||||
|
<button type="button" class="auth-link" onclick="showToast(t('toast_resetpw'))" data-i18n="login_forgot">忘记密码?</button>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="auth-submit btn-primary text-white font-bold" data-i18n="login_submit">登 录</button>
|
||||||
|
</form>
|
||||||
|
<div class="auth-divider" data-i18n="login_other">其他方式登录</div>
|
||||||
|
<div class="auth-socials">
|
||||||
|
<button type="button" aria-label="微信登录" onclick="showToast('微信登录即将上线')"><i class="fab fa-weixin text-green-600 text-xl"></i></button>
|
||||||
|
<button type="button" aria-label="企业微信登录" onclick="showToast('企业微信登录即将上线')"><i class="fas fa-briefcase text-blue-600"></i></button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="registerPanel" class="auth-panel" hidden>
|
||||||
|
<h2 id="registerDialogTitle" data-i18n="reg_title">企业注册</h2>
|
||||||
|
<p class="auth-switch-copy"><span data-i18n="reg_have_account">已有账号?</span> <button type="button" class="auth-link" onclick="switchAuthMode('login')" data-i18n="reg_login_link">立即登录</button></p>
|
||||||
|
<form id="registerForm" class="auth-form">
|
||||||
|
<div class="auth-field-grid">
|
||||||
|
<div class="auth-field"><label for="registerCompany" data-i18n="reg_company_label">企业名称</label><input id="registerCompany" class="input-field w-full px-4" type="text" name="company" required autocomplete="organization" placeholder="请输入企业名称" data-i18n-ph="reg_company_ph"></div>
|
||||||
|
<div class="auth-field"><label for="registerContact" data-i18n="reg_contact_label">联系人</label><input id="registerContact" class="input-field w-full px-4" type="text" name="contact" required autocomplete="name" placeholder="请输入联系人姓名" data-i18n-ph="reg_contact_ph"></div>
|
||||||
|
</div>
|
||||||
|
<div class="auth-field"><label for="registerEmail" data-i18n="reg_email_label">邮箱</label><div class="auth-input-wrap"><i class="fas fa-envelope"></i><input id="registerEmail" class="input-field w-full px-4" type="email" name="email" required autocomplete="email" placeholder="请输入企业邮箱" data-i18n-ph="reg_email_ph"></div></div>
|
||||||
|
<div class="auth-field"><label for="registerPhone" data-i18n="reg_phone_label">手机号</label><div class="auth-input-wrap"><i class="fas fa-phone"></i><input id="registerPhone" class="input-field w-full px-4" type="tel" name="phone" required autocomplete="tel" placeholder="请输入手机号" data-i18n-ph="reg_phone_ph"></div></div>
|
||||||
|
<div class="auth-field"><label for="registerPassword" data-i18n="reg_password_label">密码</label><div class="auth-input-wrap"><i class="fas fa-lock"></i><input id="registerPassword" class="input-field w-full px-4 pr-12" type="password" name="password" required minlength="8" maxlength="20" autocomplete="new-password" placeholder="请设置8-20位密码" data-i18n-ph="reg_password_ph"><button type="button" class="toggle-password" aria-label="显示或隐藏密码"><i class="fas fa-eye"></i></button></div></div>
|
||||||
|
<label class="auth-check" for="registerAgree"><input id="registerAgree" type="checkbox" required><span><span data-i18n="reg_agree">我已阅读并同意</span> <button type="button" class="auth-link" onclick="showToast(t('toast_tos'))" data-i18n="reg_agree_tos">《用户服务协议》</button> 和 <button type="button" class="auth-link" onclick="showToast(t('toast_pp'))" data-i18n="reg_agree_pp">《隐私政策》</button></span></label>
|
||||||
|
<button type="submit" class="auth-submit btn-accent text-white font-bold" data-i18n="reg_submit">立即注册</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Toast 提示 -->
|
<!-- Toast 提示 -->
|
||||||
<div id="toast" class="fixed top-24 left-1/2 -translate-x-1/2 z-[60] opacity-0 pointer-events-none transition-all duration-300 transform -translate-y-4">
|
<div id="toast" class="fixed top-24 left-1/2 -translate-x-1/2 z-[60] opacity-0 pointer-events-none transition-all duration-300 transform -translate-y-4">
|
||||||
<div class="bg-gray-900 text-white px-6 py-3 rounded-full shadow-2xl flex items-center space-x-2">
|
<div class="bg-gray-900 text-white px-6 py-3 rounded-full shadow-2xl flex items-center space-x-2">
|
||||||
@@ -1555,6 +1680,74 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let authReturnFocus = null;
|
||||||
|
|
||||||
|
function switchAuthMode(mode) {
|
||||||
|
const normalizedMode = mode === 'register' ? 'register' : 'login';
|
||||||
|
const modal = document.getElementById('authModal');
|
||||||
|
const isRegister = normalizedMode === 'register';
|
||||||
|
modal.dataset.mode = normalizedMode;
|
||||||
|
document.getElementById('loginPanel').hidden = isRegister;
|
||||||
|
document.getElementById('registerPanel').hidden = !isRegister;
|
||||||
|
document.getElementById('authLoginStory').hidden = isRegister;
|
||||||
|
document.getElementById('authRegisterStory').hidden = !isRegister;
|
||||||
|
document.getElementById('authLoginBenefits').hidden = isRegister;
|
||||||
|
document.getElementById('authRegisterBenefits').hidden = !isRegister;
|
||||||
|
modal.setAttribute('aria-labelledby', isRegister ? 'registerDialogTitle' : 'authDialogTitle');
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const firstField = document.querySelector(isRegister ? '#registerForm input' : '#loginForm input');
|
||||||
|
if (firstField) firstField.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAuthModal(mode) {
|
||||||
|
const modal = document.getElementById('authModal');
|
||||||
|
authReturnFocus = document.activeElement;
|
||||||
|
modal.hidden = false;
|
||||||
|
modal.setAttribute('aria-hidden', 'false');
|
||||||
|
document.body.classList.add('auth-modal-open');
|
||||||
|
switchAuthMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAuthModal() {
|
||||||
|
const modal = document.getElementById('authModal');
|
||||||
|
if (modal.hidden) return;
|
||||||
|
modal.hidden = true;
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.classList.remove('auth-modal-open');
|
||||||
|
if (authReturnFocus && typeof authReturnFocus.focus === 'function') authReturnFocus.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('authModal').addEventListener('keydown', function(event) {
|
||||||
|
if (event.key === 'Escape') { closeAuthModal(); return; }
|
||||||
|
if (event.key !== 'Tab') return;
|
||||||
|
const focusable = [...this.querySelectorAll('button:not([hidden]), input:not([hidden])')]
|
||||||
|
.filter(element => !element.closest('[hidden]') && !element.disabled && element.offsetParent !== null);
|
||||||
|
if (!focusable.length) return;
|
||||||
|
const first = focusable[0];
|
||||||
|
const last = focusable[focusable.length - 1];
|
||||||
|
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
||||||
|
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
isLoggedIn = true;
|
||||||
|
updateAuthUI();
|
||||||
|
closeAuthModal();
|
||||||
|
showToast(t('toast_login_ok'));
|
||||||
|
setTimeout(() => { window.location.href = 'workbench.html'; }, 700);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('registerForm').addEventListener('submit', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
isLoggedIn = true;
|
||||||
|
updateAuthUI();
|
||||||
|
closeAuthModal();
|
||||||
|
showToast(t('toast_register_ok'));
|
||||||
|
setTimeout(() => { window.location.href = 'workbench.html'; }, 700);
|
||||||
|
});
|
||||||
|
|
||||||
// 退出登录
|
// 退出登录
|
||||||
function doLogout() {
|
function doLogout() {
|
||||||
isLoggedIn = false;
|
isLoggedIn = false;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(html, /<th>地址名称<\/th>/);
|
||||||
|
assert.match(html, /<label for="profile-address-name"><span class="required">\*<\/span>地址名称<\/label>/);
|
||||||
|
assert.match(html, /id="pickup-address-select"/);
|
||||||
|
assert.match(html, /提货时间[\s\S]*?选择地址/);
|
||||||
|
assert.match(html, /class="pickup-time-address-row"[\s\S]*?提货时间[\s\S]*?选择地址/);
|
||||||
|
|
||||||
|
const normalizeProfileAddress = vm.runInNewContext(`(${extractFunction('normalizeProfileAddress')})`);
|
||||||
|
const legacy = normalizeProfileAddress({id:'legacy-1', privateWarehouse:'旧地址', warehouseCode:'WH-1', country:'美国'});
|
||||||
|
assert.equal(legacy.addressName, '旧地址');
|
||||||
|
|
||||||
|
const select = { innerHTML: '' };
|
||||||
|
const renderPickupAddressOptions = vm.runInNewContext(`(${extractFunction('renderPickupAddressOptions')})`, {
|
||||||
|
document: { getElementById: id => id === 'pickup-address-select' ? select : null },
|
||||||
|
loadProfileAddresses: () => [
|
||||||
|
{id:'enabled-1', addressName:'洛杉矶仓', enabled:true},
|
||||||
|
{id:'disabled-1', addressName:'停用地址', enabled:false}
|
||||||
|
],
|
||||||
|
escapeProfileAddressHtml: value => String(value == null ? '' : value)
|
||||||
|
});
|
||||||
|
renderPickupAddressOptions();
|
||||||
|
assert.match(select.innerHTML, /value="enabled-1"[^>]*>洛杉矶仓<\/option>/);
|
||||||
|
assert.doesNotMatch(select.innerHTML, /停用地址/);
|
||||||
|
|
||||||
|
const controls = {
|
||||||
|
'pickup-address-select': { value: '' },
|
||||||
|
'pickup-contact': { value: '' },
|
||||||
|
'pickup-phone': { value: '' },
|
||||||
|
'pickup-address-detail': { value: '' }
|
||||||
|
};
|
||||||
|
const applyPickupAddress = vm.runInNewContext(`(${extractFunction('applyPickupAddress')})`, {
|
||||||
|
document: { getElementById: id => controls[id] || null },
|
||||||
|
formatPickupAddress: address => address.detailAddress || [address.stateProvince, address.city, address.country, address.postalCode, address.warehouseCode].filter(Boolean).join(','),
|
||||||
|
pickupAutoFilledValues: null,
|
||||||
|
loadProfileAddresses: () => [{
|
||||||
|
id:'address-1', addressName:'纽约仓', contact:'李雷', phone:'13800000000',
|
||||||
|
warehouseCode:'WH-NY', detailAddress:'123 Main St', city:'纽约', stateProvince:'纽约州', country:'美国', postalCode:'10001', enabled:true
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
applyPickupAddress('address-1');
|
||||||
|
assert.equal(controls['pickup-contact'].value, '李雷');
|
||||||
|
assert.equal(controls['pickup-phone'].value, '13800000000');
|
||||||
|
assert.equal(controls['pickup-address-detail'].value, '123 Main St');
|
||||||
|
applyPickupAddress('');
|
||||||
|
assert.equal(controls['pickup-contact'].value, '');
|
||||||
|
assert.equal(controls['pickup-phone'].value, '');
|
||||||
|
assert.equal(controls['pickup-address-detail'].value, '');
|
||||||
|
|
||||||
|
console.log('address selection: ok');
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const detail = html.match(/<div class="page-content" id="page-cargo-disposal-detail">([\s\S]*?)<!-- ========== PAGE: 退货换标/)?.[1] || '';
|
||||||
|
|
||||||
|
assert.ok(detail, '货物废弃详情页 should exist');
|
||||||
|
assert.doesNotMatch(detail, /business-detail-tabs-shell/, '货物废弃详情不应再使用标签页容器');
|
||||||
|
assert.doesNotMatch(detail, /business-detail-panel|cargo-disposal-panel-|cargo-disposal-tab-/);
|
||||||
|
|
||||||
|
assert.match(detail, /货物废弃单号:<strong id="cargo-disposal-detail-order-no">-<\/strong>/);
|
||||||
|
assert.match(detail, /class="page-header-actions cargo-disposal-detail-header"/);
|
||||||
|
assert.match(detail, /class="page-action-group cargo-disposal-detail-actions"/);
|
||||||
|
assert.match(detail, /class="labeling-detail-status" id="cargo-disposal-detail-status"/);
|
||||||
|
assert.match(detail, /class="fas fa-arrow-left"/);
|
||||||
|
|
||||||
|
assert.match(detail, /class="labeling-detail-grid"/);
|
||||||
|
assert.equal((detail.match(/class="card labeling-detail-card"/g) || []).length, 4);
|
||||||
|
for (const title of ['单据信息', '货物明细', '费用明细', '关联单据']) {
|
||||||
|
assert.match(detail, new RegExp(`class="info-section-title">${title}<\\/div>`));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(detail, /<dl id="cargo-disposal-detail-document"><\/dl>/);
|
||||||
|
assert.match(detail, /<tbody id="cargo-disposal-detail-items"><\/tbody>/);
|
||||||
|
assert.match(detail, /<dl id="cargo-disposal-detail-fees"><\/dl>/);
|
||||||
|
assert.match(detail, /<dl id="cargo-disposal-detail-relations">/);
|
||||||
|
assert.doesNotMatch(detail, /cargo-disposal-(?:summary-grid|relation-grid|relation-item)/);
|
||||||
|
|
||||||
|
console.log('cargo disposal detail layout requirements: ok');
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const listPage = html.match(/<div class="page-content" id="page-cargo-disposal-list">([\s\S]*?)<!-- ========== PAGE: 新增货物废弃/)?.[1] || '';
|
||||||
|
assert.match(listPage, /<th>操作<\/th>/, 'view and edit should share an operation column');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editModal = html.match(/<div class="modal-overlay" id="cargo-disposal-edit-modal"([\s\S]*?)<!-- ========== MODAL:/)?.[1] || '';
|
||||||
|
assert.ok(editModal, 'cargo disposal edit modal should exist');
|
||||||
|
assert.match(editModal, /id="cargo-disposal-edit-inbound-search"/);
|
||||||
|
assert.match(editModal, /id="cargo-disposal-edit-inbound-cards"/);
|
||||||
|
assert.match(editModal, /id="cargo-disposal-edit-fee"/);
|
||||||
|
assert.match(editModal, /id="cargo-disposal-edit-error"/);
|
||||||
|
assert.match(editModal, /onsubmit="submitCargoDisposalEdit\(event\)"/);
|
||||||
|
|
||||||
|
const tbody = { innerHTML: '' };
|
||||||
|
const controls = {
|
||||||
|
'cargo-disposal-list-body': tbody,
|
||||||
|
'cargo-disposal-total': { textContent: '' },
|
||||||
|
'cargo-disposal-page-number': { textContent: '' },
|
||||||
|
'cargo-disposal-prev': { disabled: false },
|
||||||
|
'cargo-disposal-next': { disabled: false }
|
||||||
|
};
|
||||||
|
const renderCargoDisposalList = vm.runInNewContext(
|
||||||
|
`(${extractFunction('renderCargoDisposalList')})`,
|
||||||
|
{
|
||||||
|
document: { getElementById: id => controls[id] || null },
|
||||||
|
cargoDisposalFilteredOrders: null,
|
||||||
|
loadCargoDisposalOrders: () => [{
|
||||||
|
id: 'order-1', orderNo: 'FH001', inboundNo: 'RK001', disposalBoxes: 2,
|
||||||
|
warehouse: '美西洛杉矶仓', feeAmount: 10, outboundOrderNo: 'CK001',
|
||||||
|
billingNo: 'JF001', status: '待处理', createdAt: '2026-09-04 09:00'
|
||||||
|
}],
|
||||||
|
CARGO_DISPOSAL_PAGE_SIZE: 10,
|
||||||
|
cargoDisposalCurrentPage: 1,
|
||||||
|
escapeCargoDisposalHtml: value => String(value == null ? '' : value)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
renderCargoDisposalList();
|
||||||
|
assert.match(tbody.innerHTML, />查看<\/button>[\s\S]*?>编辑<\/button>/, 'each row should show edit after view');
|
||||||
|
assert.match(tbody.innerHTML, /openCargoDisposalEdit\(this\.dataset\.cargoDisposalId\)/);
|
||||||
|
|
||||||
|
const mergeCargoDisposalOrder = vm.runInNewContext(`(${extractFunction('mergeCargoDisposalOrder')})`);
|
||||||
|
const original = {
|
||||||
|
id: 'order-1', orderNo: 'FH001', tailOrderNo: 'LC001', outboundOrderNo: 'CK001',
|
||||||
|
billingNo: 'JF001', status: '处理中', createdAt: '2026-09-01 08:00', currency: 'USD',
|
||||||
|
inboundNo: 'OLD', warehouse: '旧仓', disposalBoxes: 2, feeAmount: 10,
|
||||||
|
items: [{ sku: 'OLD-SKU', name: '旧商品', availableBoxes: 4, disposalBoxes: 2 }]
|
||||||
|
};
|
||||||
|
const updated = mergeCargoDisposalOrder(original, {
|
||||||
|
inbound: {
|
||||||
|
inboundNo: 'RK009', warehouse: '美东新泽西仓',
|
||||||
|
items: [
|
||||||
|
{ id: 'one', sku: 'SKU-1', name: '商品一', availableBoxes: 8 },
|
||||||
|
{ id: 'two', sku: 'SKU-2', name: '商品二', availableBoxes: 5 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ id: 'one', disposalBoxes: 3 },
|
||||||
|
{ id: 'two', disposalBoxes: 1 }
|
||||||
|
]
|
||||||
|
}, 5);
|
||||||
|
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(updated)), {
|
||||||
|
id: 'order-1', orderNo: 'FH001', tailOrderNo: 'LC001', outboundOrderNo: 'CK001',
|
||||||
|
billingNo: 'JF001', status: '处理中', createdAt: '2026-09-01 08:00', currency: 'USD',
|
||||||
|
inboundNo: 'RK009', warehouse: '美东新泽西仓', disposalBoxes: 4, feeAmount: 20,
|
||||||
|
items: [
|
||||||
|
{ sku: 'SKU-1', name: '商品一', availableBoxes: 8, disposalBoxes: 3 },
|
||||||
|
{ sku: 'SKU-2', name: '商品二', availableBoxes: 5, disposalBoxes: 1 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('cargo disposal edit: ok');
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const listPage = html.match(/<div class="page-content" id="page-cargo-disposal-list">([\s\S]*?)<!-- ========== PAGE: 新增货物废弃/)?.[1] || '';
|
||||||
|
|
||||||
|
function textContent(markup) {
|
||||||
|
return markup.replace(/<[^>]+>/g, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = Array.from(listPage.matchAll(/<th(?:\s[^>]*)?>([\s\S]*?)<\/th>/g), match => textContent(match[1]));
|
||||||
|
const orderNoIndex = headers.indexOf('货物废弃单号');
|
||||||
|
assert.ok(orderNoIndex >= 0, '货物废弃单号列应存在');
|
||||||
|
assert.equal(headers[orderNoIndex + 1], '入仓码', '入仓码应紧跟货物废弃单号');
|
||||||
|
assert.equal(headers.includes('计费单号'), false, '列表不应显示计费单号');
|
||||||
|
assert.equal(headers.includes('关联出库单'), true, '列表应显示关联出库单');
|
||||||
|
assert.equal(headers.includes('废弃出库单号'), false, '列表不应再显示废弃出库单号');
|
||||||
|
|
||||||
|
const detailPage = html.match(/<div class="page-content" id="page-cargo-disposal-detail">([\s\S]*?)<!-- ========== PAGE:/)?.[1] || '';
|
||||||
|
assert.match(detailPage, /<dt>关联出库单<\/dt>/, '详情页应显示关联出库单');
|
||||||
|
assert.doesNotMatch(detailPage, /<dt>废弃出库单号<\/dt>/, '详情页不应再显示废弃出库单号');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbody = { innerHTML: '' };
|
||||||
|
const controls = {
|
||||||
|
'cargo-disposal-list-body': tbody,
|
||||||
|
'cargo-disposal-total': { textContent: '' },
|
||||||
|
'cargo-disposal-page-number': { textContent: '' },
|
||||||
|
'cargo-disposal-prev': { disabled: false },
|
||||||
|
'cargo-disposal-next': { disabled: false }
|
||||||
|
};
|
||||||
|
const renderCargoDisposalList = vm.runInNewContext(
|
||||||
|
`(${extractFunction('renderCargoDisposalList')})`,
|
||||||
|
{
|
||||||
|
document: { getElementById: id => controls[id] || null },
|
||||||
|
cargoDisposalFilteredOrders: null,
|
||||||
|
loadCargoDisposalOrders: () => [{
|
||||||
|
id: 'order-1', orderNo: 'FH001', inboundNo: 'RK001', disposalBoxes: 2,
|
||||||
|
warehouse: '美西洛杉矶仓', feeAmount: 10, outboundOrderNo: 'CK001',
|
||||||
|
billingNo: 'JF001', status: '待处理', createdAt: '2026-09-04 09:00'
|
||||||
|
}],
|
||||||
|
CARGO_DISPOSAL_PAGE_SIZE: 10,
|
||||||
|
cargoDisposalCurrentPage: 1,
|
||||||
|
escapeCargoDisposalHtml: value => String(value == null ? '' : value)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
renderCargoDisposalList();
|
||||||
|
const orderNoCellStart = tbody.innerHTML.indexOf('<td>FH001</td>');
|
||||||
|
assert.ok(orderNoCellStart >= 0, '行中应输出货物废弃单号');
|
||||||
|
assert.equal(tbody.innerHTML.slice(orderNoCellStart).indexOf('<td>RK001</td>'), '<td>RK001</td>'.length, '行中入仓码应紧跟货物废弃单号');
|
||||||
|
assert.doesNotMatch(tbody.innerHTML, /JF001/, '行中不应输出计费单号');
|
||||||
|
|
||||||
|
console.log('cargo disposal list columns: ok');
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const pageOrder = html.match(/<div class="page-content active" id="page-order">([\s\S]*?)<\/div>\n\n<!-- ========== PAGE 2:/)?.[1] || '';
|
||||||
|
|
||||||
|
assert.ok(pageOrder, 'page-order markup should exist');
|
||||||
|
assert.match(pageOrder, /onclick="setPickupTab\(this,'truck'\)"[^>]*>自送货<\/button>/);
|
||||||
|
assert.doesNotMatch(pageOrder, />自送货-快递<\/button>|>自送货-卡车<\/button>/);
|
||||||
|
assert.match(pageOrder, /<div class="form-label">快递单号<\/div>[\s\S]*?请输入快递单号/);
|
||||||
|
assert.match(pageOrder, /class="pickup-driver-row"[\s\S]*?司机姓名[\s\S]*?司机电话/);
|
||||||
|
assert.match(html, /\.pickup-driver-row \.form-group\s*\{\s*grid-template-columns:\s*96px minmax\(0, 1fr\);/);
|
||||||
|
assert.match(pageOrder, /onclick="setDestTab\(this,'fulfillment'\)"[^>]*>一件代发<\/button>/);
|
||||||
|
const fulfillmentPanel = pageOrder.match(/<div class="dest-content" id="dest-fulfillment">([\s\S]*?)<\/div>\s*<\/div>\s*<\/section>/)?.[1] || '';
|
||||||
|
assert.ok(fulfillmentPanel, 'fulfillment destination panel should exist');
|
||||||
|
assert.match(fulfillmentPanel, /LX入库单号/);
|
||||||
|
assert.match(fulfillmentPanel, /name="lxInboundNo"/);
|
||||||
|
assert.match(fulfillmentPanel, /<span class="required">\*<\/span>LX入库单号/);
|
||||||
|
assert.doesNotMatch(fulfillmentPanel, /一件代发仓|收件人姓名|收件人电话|收件地址/);
|
||||||
|
const taxSelect = pageOrder.match(/<select name="tax"[^>]*>/)?.[0] || '';
|
||||||
|
assert.ok(taxSelect, 'tax selector should exist');
|
||||||
|
assert.match(taxSelect, /\brequired\b/);
|
||||||
|
const fullContainerSelect = pageOrder.match(/<select name="fullContainer"[^>]*>[\s\S]*?<\/select>/)?.[0] || '';
|
||||||
|
assert.ok(fullContainerSelect, 'full container selector should exist');
|
||||||
|
assert.match(fullContainerSelect, /<option value="">请选择<\/option>/);
|
||||||
|
assert.match(fullContainerSelect, /<option value="是">是<\/option>/);
|
||||||
|
assert.match(fullContainerSelect, /<option value="否">否<\/option>/);
|
||||||
|
assert.doesNotMatch(fullContainerSelect.match(/^<select[^>]*>/)?.[0] || '', /\brequired\b/);
|
||||||
|
const inspectionSelect = pageOrder.match(/<select name="inspection"[^>]*>[\s\S]*?<\/select>/)?.[0] || '';
|
||||||
|
assert.ok(inspectionSelect, 'inspection selector should exist');
|
||||||
|
assert.match(inspectionSelect, /<option value="">请选择<\/option>/);
|
||||||
|
assert.match(inspectionSelect, /<option value="是">是<\/option>/);
|
||||||
|
assert.match(inspectionSelect, /<option value="否">否<\/option>/);
|
||||||
|
assert.doesNotMatch(inspectionSelect.match(/^<select[^>]*>/)?.[0] || '', /\brequired\b/);
|
||||||
|
assert.match(pageOrder, /<div class="form-label"><span class="required">\*<\/span>买单\/退税<\/div>/);
|
||||||
|
assert.doesNotMatch(pageOrder, /<div class="form-label"><span class="required">\*<\/span>是否整柜<\/div>|<div class="form-label"><span class="required">\*<\/span>是否商检<\/div>/);
|
||||||
|
|
||||||
|
console.log('create-order requirements: ok');
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inboundPage = html.match(/<div class="page-content" id="page-inbound">([\s\S]*?)<!-- ========== PAGE: 头程订单详情/)?.[1] || '';
|
||||||
|
assert.ok(inboundPage, 'inbound order list should exist');
|
||||||
|
assert.match(inboundPage, /onclick="openInboundBatchOrder\(\)"[^>]*>[\s\S]*?批量下单<\/button>/);
|
||||||
|
assert.doesNotMatch(inboundPage, /导出订单尾程信息/);
|
||||||
|
assert.match(inboundPage, /class="filter-item inbound-destination-filter"[\s\S]*?id="inbound-filter-destination-type"[\s\S]*?onclick="filterInboundOrders\(\)"[\s\S]*?>查询<\/button>/);
|
||||||
|
|
||||||
|
const modalStart = html.indexOf('<div class="topup-overlay hidden" id="inbound-batch-order-modal"');
|
||||||
|
const modalEnd = html.indexOf('<div class="sea-edit-overlay"', modalStart);
|
||||||
|
const modal = modalStart >= 0 && modalEnd > modalStart ? html.slice(modalStart, modalEnd) : '';
|
||||||
|
assert.ok(modal, 'batch order modal should exist');
|
||||||
|
assert.match(modal, /下载模板/);
|
||||||
|
assert.match(modal, /id="inbound-batch-order-file"[^>]*accept="\.xlsx,\.xls"/);
|
||||||
|
assert.match(modal, /onclick="submitInboundBatchOrder\(\)"[^>]*>提交订单<\/button>/);
|
||||||
|
|
||||||
|
const getTemplateSheets = vm.runInNewContext(`(${extractFunction('getInboundBatchTemplateSheets')})`);
|
||||||
|
const sheets = getTemplateSheets();
|
||||||
|
assert.deepEqual(Array.from(sheets, sheet => sheet.name), ['订单信息', '商品信息', '箱子信息']);
|
||||||
|
assert.deepEqual(Array.from(sheets, sheet => sheet.headers[0]), ['批次编号*', '批次编号*', '批次编号*']);
|
||||||
|
|
||||||
|
for (const sheet of sheets) {
|
||||||
|
const headers = Array.from(sheet.headers);
|
||||||
|
const firstOptional = headers.findIndex(header => !header.endsWith('*'));
|
||||||
|
assert.notEqual(firstOptional, -1, `${sheet.name} should include optional fields`);
|
||||||
|
assert.ok(headers.slice(0, firstOptional).every(header => header.endsWith('*')), `${sheet.name} required fields should come first`);
|
||||||
|
assert.ok(headers.slice(firstOptional).every(header => !header.endsWith('*')), `${sheet.name} optional fields should follow required fields`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(sheets[0].headers.includes('取货方式*'));
|
||||||
|
assert.ok(sheets[0].headers.includes('目的仓类型*'));
|
||||||
|
assert.ok(sheets[0].headers.includes('收货仓*'));
|
||||||
|
assert.ok(sheets[0].headers.includes('渠道*'));
|
||||||
|
assert.ok(sheets[0].headers.includes('买单/退税*'));
|
||||||
|
assert.ok(sheets[0].headers.includes('LX入库单号*'));
|
||||||
|
assert.ok(!sheets[0].headers.includes('一件代发仓*'));
|
||||||
|
assert.ok(sheets[0].headers.includes('订单备注'));
|
||||||
|
assert.ok(sheets[1].headers.includes('SKU NO.*'));
|
||||||
|
assert.ok(sheets[1].headers.includes('中文品名'));
|
||||||
|
assert.ok(sheets[2].headers.includes('关联 SKU*'));
|
||||||
|
assert.ok(sheets[2].headers.includes('箱数*'));
|
||||||
|
assert.ok(sheets[2].headers.includes('总体积(m³)'));
|
||||||
|
|
||||||
|
const validateWorkbookData = vm.runInNewContext(`(${extractFunction('validateInboundBatchWorkbookData')})`, {
|
||||||
|
getInboundBatchTemplateSheets: getTemplateSheets
|
||||||
|
});
|
||||||
|
const valid = validateWorkbookData({
|
||||||
|
订单信息: [sheets[0].headers, sheets[0].headers.map(header => header.endsWith('*') ? '示例值' : '')],
|
||||||
|
商品信息: [sheets[1].headers, sheets[1].headers.map(header => header.endsWith('*') ? '示例值' : '')],
|
||||||
|
箱子信息: [sheets[2].headers, sheets[2].headers.map(header => header.endsWith('*') ? '1' : '')]
|
||||||
|
});
|
||||||
|
assert.equal(valid.ok, true);
|
||||||
|
assert.deepEqual(Array.from(valid.counts), [1, 1, 1]);
|
||||||
|
|
||||||
|
const invalid = validateWorkbookData({
|
||||||
|
订单信息: [sheets[0].headers, sheets[0].headers.map(header => header === '订单备注' ? '测试备注' : '')],
|
||||||
|
商品信息: [sheets[1].headers],
|
||||||
|
箱子信息: [sheets[2].headers]
|
||||||
|
});
|
||||||
|
assert.equal(invalid.ok, false);
|
||||||
|
assert.match(invalid.message, /订单信息.*第 2 行.*批次编号/);
|
||||||
|
|
||||||
|
function makeRow(headers, values) {
|
||||||
|
return headers.map(header => values[header] || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const conditionalInvalid = validateWorkbookData({
|
||||||
|
订单信息: [sheets[0].headers, makeRow(sheets[0].headers, {
|
||||||
|
'批次编号*':'B001', '取货方式*':'我司提货(有取货费)', '目的仓类型*':'FBA仓', '收货仓*':'义乌仓', '渠道*':'美线快线', '买单/退税*':'买单', 'FBA仓编码*':'ONT8'
|
||||||
|
})],
|
||||||
|
商品信息: [sheets[1].headers, makeRow(sheets[1].headers, {'批次编号*':'B001', 'SKU NO.*':'SKU-01'})],
|
||||||
|
箱子信息: [sheets[2].headers, makeRow(sheets[2].headers, {'批次编号*':'B001', '关联 SKU*':'SKU-01', '箱数*':'2'})]
|
||||||
|
});
|
||||||
|
assert.equal(conditionalInvalid.ok, false);
|
||||||
|
assert.match(conditionalInvalid.message, /提货时间/);
|
||||||
|
|
||||||
|
const fulfillmentInvalid = validateWorkbookData({
|
||||||
|
订单信息: [sheets[0].headers, makeRow(sheets[0].headers, {
|
||||||
|
'批次编号*':'B002', '取货方式*':'自送货', '目的仓类型*':'一件代发', '收货仓*':'义乌仓', '渠道*':'美线快线', '买单/退税*':'买单', '预计到达时间*':'2026-09-10 10:00'
|
||||||
|
})],
|
||||||
|
商品信息: [sheets[1].headers, makeRow(sheets[1].headers, {'批次编号*':'B002', 'SKU NO.*':'SKU-02'})],
|
||||||
|
箱子信息: [sheets[2].headers, makeRow(sheets[2].headers, {'批次编号*':'B002', '关联 SKU*':'SKU-02', '箱数*':'1'})]
|
||||||
|
});
|
||||||
|
assert.equal(fulfillmentInvalid.ok, false);
|
||||||
|
assert.match(fulfillmentInvalid.message, /LX入库单号/);
|
||||||
|
|
||||||
|
const createOrders = vm.runInNewContext(`(${extractFunction('createInboundOrdersFromBatchData')})`);
|
||||||
|
const orders = createOrders({
|
||||||
|
订单信息: [sheets[0].headers, makeRow(sheets[0].headers, {
|
||||||
|
'批次编号*':'B001', '取货方式*':'自送货', '目的仓类型*':'FBA仓', '收货仓*':'义乌仓', '渠道*':'美线快线', '买单/退税*':'买单', '预计到达时间*':'2026-09-10 10:00', 'FBA仓编码*':'ONT8', '订单备注':'测试订单'
|
||||||
|
})],
|
||||||
|
商品信息: [sheets[1].headers, makeRow(sheets[1].headers, {'批次编号*':'B001', 'SKU NO.*':'SKU-01'})],
|
||||||
|
箱子信息: [sheets[2].headers, makeRow(sheets[2].headers, {'批次编号*':'B001', '关联 SKU*':'SKU-01', '箱数*':'2', '总毛重(kg)':'20', '总净重(kg)':'18', '总体积(m³)':'0.2'})]
|
||||||
|
}, new Date('2026-09-04T12:30:00'));
|
||||||
|
assert.equal(orders.length, 1);
|
||||||
|
assert.equal(orders[0].destinationWarehouse, 'ONT8');
|
||||||
|
assert.equal(orders[0].skus, 'SKU-01');
|
||||||
|
assert.equal(orders[0].boxes, 2);
|
||||||
|
assert.equal(orders[0].grossWeight, '20 kg');
|
||||||
|
assert.equal(orders[0].volume, '0.2 m³');
|
||||||
|
assert.equal(orders[0].status, '待处理');
|
||||||
|
|
||||||
|
const fulfillmentOrders = createOrders({
|
||||||
|
订单信息: [sheets[0].headers, makeRow(sheets[0].headers, {
|
||||||
|
'批次编号*':'B003', '取货方式*':'自送货', '目的仓类型*':'一件代发', '收货仓*':'深圳仓', '渠道*':'美线快线', '买单/退税*':'买单', '预计到达时间*':'2026-09-10 10:00', 'LX入库单号*':'LX20260904001'
|
||||||
|
})],
|
||||||
|
商品信息: [sheets[1].headers, makeRow(sheets[1].headers, {'批次编号*':'B003', 'SKU NO.*':'SKU-03'})],
|
||||||
|
箱子信息: [sheets[2].headers, makeRow(sheets[2].headers, {'批次编号*':'B003', '关联 SKU*':'SKU-03', '箱数*':'1'})]
|
||||||
|
}, new Date('2026-09-04T12:30:00'));
|
||||||
|
assert.equal(fulfillmentOrders[0].destinationWarehouse, 'LX20260904001');
|
||||||
|
assert.equal(fulfillmentOrders[0].lxInboundNo, 'LX20260904001');
|
||||||
|
|
||||||
|
console.log('inbound batch order requirements: ok');
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractConstValue(name) {
|
||||||
|
const declaration = `const ${name} = `;
|
||||||
|
const start = html.indexOf(declaration);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const valueStart = start + declaration.length;
|
||||||
|
const end = html.indexOf(';', valueStart);
|
||||||
|
assert.notEqual(end, -1, `${name} should have a value`);
|
||||||
|
return html.slice(valueStart, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function textContent(markup) {
|
||||||
|
return markup.replace(/<[^>]+>/g, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(html, /(?=[^>]*data-tab="container")(?=[^>]*aria-controls="inbound-panel-container")[^>]*>货柜节点<\/button>/);
|
||||||
|
assert.match(html, /id="inbound-panel-container"[\s\S]*id="inbound-detail-container-nodes"/);
|
||||||
|
|
||||||
|
const body = { innerHTML: '' };
|
||||||
|
const context = {
|
||||||
|
document: { getElementById: id => id === 'inbound-detail-container-nodes' ? body : null },
|
||||||
|
escapeInboundHtml: value => String(value == null ? '' : value),
|
||||||
|
};
|
||||||
|
const render = vm.runInNewContext(`(${extractFunction('renderInboundContainerNodes')})`, context);
|
||||||
|
render([]);
|
||||||
|
const defaultNodeNames = Array.from(body.innerHTML.matchAll(/<div class="inbound-container-node-name">([\s\S]*?)<\/div>/g), match => textContent(match[1]));
|
||||||
|
assert.equal(defaultNodeNames[6], '等待提柜');
|
||||||
|
|
||||||
|
const inboundOrders = vm.runInNewContext(`(${extractConstValue('INBOUND_ORDER_SEED')})`);
|
||||||
|
render(inboundOrders[0].containerNodes);
|
||||||
|
const seededNodeNames = Array.from(body.innerHTML.matchAll(/<div class="inbound-container-node-name">([\s\S]*?)<\/div>/g), match => textContent(match[1]));
|
||||||
|
assert.equal(seededNodeNames[6], '等待提柜');
|
||||||
|
|
||||||
|
render([
|
||||||
|
{ name: '国内报关完成', status: 'done', time: '2026-07-22 11:13' },
|
||||||
|
{ name: '实际开船', status: 'current', time: '-' },
|
||||||
|
{ name: '实际到港', status: 'pending', time: '-' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal((body.innerHTML.match(/class="inbound-container-node is-/g) || []).length, 3);
|
||||||
|
assert.match(body.innerHTML, /国内报关完成/);
|
||||||
|
assert.match(body.innerHTML, /实际开船/);
|
||||||
|
assert.match(body.innerHTML, /实际到港/);
|
||||||
|
assert.match(body.innerHTML, /inbound-container-node is-done/);
|
||||||
|
assert.match(body.innerHTML, /inbound-container-node is-current/);
|
||||||
|
assert.match(body.innerHTML, /inbound-container-node is-pending/);
|
||||||
|
assert.equal(textContent(body.innerHTML).includes('2026-07-22 11:13'), true);
|
||||||
|
|
||||||
|
console.log('inbound container nodes requirements: ok');
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = { innerHTML: '' };
|
||||||
|
const context = {
|
||||||
|
document: { getElementById: id => id === 'inbound-detail-fees' ? target : null },
|
||||||
|
escapeInboundHtml: value => String(value == null ? '' : value),
|
||||||
|
};
|
||||||
|
const render = vm.runInNewContext(`(${extractFunction('renderInboundFees')})`, context);
|
||||||
|
|
||||||
|
render({ unitPrice:'$0.85/kg', billableVolume:'1.26 m³', billableWeight:'186.4 kg', totalFee:'$158.44', paymentStatus:'已付款' });
|
||||||
|
const labels = Array.from(target.innerHTML.matchAll(/<span>([^<]+)<\/span>/g), match => match[1]);
|
||||||
|
assert.deepEqual(labels, ['计费单价', '计费体积', '计费重量', '费用总计', '费用状态']);
|
||||||
|
assert.match(target.innerHTML, /<span>费用总计<\/span><strong>\$158\.44<\/strong>/);
|
||||||
|
|
||||||
|
render({});
|
||||||
|
assert.match(target.innerHTML, /<span>费用总计<\/span><strong>—<\/strong>/);
|
||||||
|
|
||||||
|
console.log('inbound fee info requirements: ok');
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(html, /inbound-order-info-columns/);
|
||||||
|
const renderFunctionSource = html.slice(html.indexOf('function renderInboundOrderInfo('), html.indexOf('function handleInboundCustomsUpload('));
|
||||||
|
for (const label of ['入仓码', '开船时间', '到港时间', '提货方式', '提货时间', '提货联系人电话', '提货地址', '提货邮编', '提货联系人邮箱', '预计到货时间', '货物到仓时间', '订单类型', '渠道', '集装箱号', '目的仓类型', '目的仓', '邮编', '联系人', '联系人电话', '是否整柜', '商检验', '买单/退税', '总箱数', '总毛重(T.K.GW)', '总净重(T.K.NW)', '总体积(m³)', '计费毛重', '计费体积(m³)', '预估重量', '创建时间', '备注']) {
|
||||||
|
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
assert.match(renderFunctionSource, new RegExp(`['\"]${escapedLabel}['\"]`), `missing order info label: ${label}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = {
|
||||||
|
orderNo: 'RK-1', inboundNo: 'RK-1', channel: '美线快线', destinationWarehouse: 'ONT8', boxes: 4,
|
||||||
|
grossWeight: '79 kg', volume: '0.384 m³', billableWeight: '79 kg', billableVolume: '0.384 m³',
|
||||||
|
createdAt: '2026-09-02 10:47:09', remark: '-'
|
||||||
|
};
|
||||||
|
const target = { innerHTML: '' };
|
||||||
|
const context = {
|
||||||
|
document: { getElementById: id => id === 'inbound-detail-order-info' ? target : null },
|
||||||
|
escapeInboundHtml: value => String(value == null ? '' : value),
|
||||||
|
inboundDetailField: (label, value) => `<div class="inbound-detail-field"><dt>${label}</dt><dd>${value || '-'}</dd></div>`,
|
||||||
|
};
|
||||||
|
const render = vm.runInNewContext(`(${extractFunction('renderInboundOrderInfo')})`, context);
|
||||||
|
render(order);
|
||||||
|
|
||||||
|
assert.equal((target.innerHTML.match(/class="inbound-order-info-column/g) || []).length, 3);
|
||||||
|
assert.match(target.innerHTML, /<dt>入仓码<\/dt><dd>RK-1<\/dd>/);
|
||||||
|
assert.match(target.innerHTML, /<dt>订单类型<\/dt><dd>头程订单<\/dd>/);
|
||||||
|
assert.match(target.innerHTML, /<dt>渠道<\/dt><dd>美线快线<\/dd>/);
|
||||||
|
assert.match(target.innerHTML, /<dt>总箱数<\/dt><dd>4<\/dd>/);
|
||||||
|
assert.match(target.innerHTML, /<dt>创建时间<\/dt><dd>2026-09-02 10:47:09<\/dd>/);
|
||||||
|
assert.doesNotMatch(target.innerHTML, /<dt>客户<\/dt>/);
|
||||||
|
assert.doesNotMatch(renderFunctionSource, /['"]客户['"]/);
|
||||||
|
assert.doesNotMatch(renderFunctionSource, /['"]公司['"]/);
|
||||||
|
|
||||||
|
console.log('inbound order info requirements: ok');
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function textContent(markup) {
|
||||||
|
return markup.replace(/<[^>]+>/g, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const inboundPage = html.match(/<div class="page-content" id="page-inbound">([\s\S]*?)<!-- ========== PAGE: 头程订单详情/)?.[1] || '';
|
||||||
|
assert.ok(inboundPage, 'inbound order list should exist');
|
||||||
|
|
||||||
|
const headers = Array.from(inboundPage.matchAll(/<th(?:\s[^>]*)?>([\s\S]*?)<\/th>/g), match => textContent(match[1]));
|
||||||
|
assert.deepEqual(headers.slice(0, 5), ['序号', '', '操作', '入仓码', '订单状态']);
|
||||||
|
|
||||||
|
const body = { innerHTML: '' };
|
||||||
|
const total = { textContent: '' };
|
||||||
|
const order = {
|
||||||
|
id: 'order-1', orderNo: 'RK-1', inboundNo: 'RK-1', channel: '美线快线', destinationWarehouse: 'ONT8',
|
||||||
|
boxes: 1, carrier: '顺丰', expressNo: 'SF-1', skus: 'SKU-1', grossWeight: '10 kg', netWeight: '9 kg',
|
||||||
|
volume: '0.1 m3', quantity: 10, unitPrice: '$1/kg', billableVolume: '0.1 m3', billableWeight: '10 kg',
|
||||||
|
remark: '-', shipmentNo: '-', trackingNo: '-', receivedAt: '-', businessDate: '2026-09-02',
|
||||||
|
status: '待确认', measurement: { rows: [] }, measurementConfirmed: false
|
||||||
|
};
|
||||||
|
const renderContext = {
|
||||||
|
document: { getElementById: id => ({ 'inbound-table-body': body, 'inbound-total': total })[id] || null },
|
||||||
|
inboundFilteredOrders: null,
|
||||||
|
INBOUND_ORDER_SEED: [order],
|
||||||
|
escapeInboundHtml: value => String(value == null ? '' : value),
|
||||||
|
inboundStatusClass: () => 'status-pending'
|
||||||
|
};
|
||||||
|
const renderInboundOrders = vm.runInNewContext(`(${extractFunction('renderInboundOrders')})`, renderContext);
|
||||||
|
renderInboundOrders();
|
||||||
|
|
||||||
|
const rowCells = Array.from(body.innerHTML.matchAll(/<td(?:\s[^>]*)?>([\s\S]*?)<\/td>/g), match => match[1]);
|
||||||
|
assert.equal(textContent(rowCells[0]), '1');
|
||||||
|
assert.match(rowCells[2], />查看<\/button>/);
|
||||||
|
assert.match(rowCells[2], /class="inbound-action"[^>]*>确认计费重<\/button>/);
|
||||||
|
assert.doesNotMatch(rowCells[2], /inbound-action-confirm/);
|
||||||
|
assert.match(rowCells[2], /onclick="showInboundOrderDetail\('order-1','measure'\)"[^>]*>确认计费重<\/button>/);
|
||||||
|
assert.equal(textContent(rowCells[3]), 'RK-1');
|
||||||
|
assert.equal(textContent(rowCells[4]), '待确认');
|
||||||
|
|
||||||
|
assert.match(inboundPage, />待确认<\/option>/);
|
||||||
|
assert.doesNotMatch(inboundPage, /待客户确认/);
|
||||||
|
assert.match(html, /<b>待发送数据<\/b><small id="inbound-node-shipping-data">/);
|
||||||
|
assert.doesNotMatch(html, /<b>待发运数据<\/b><small id="inbound-node-shipping-data">/);
|
||||||
|
|
||||||
|
const measurementModal = html.match(/<div class="topup-overlay hidden" id="inbound-measurement-modal"[^>]*>([\s\S]*?)<\/div>\s*<\/div>/)?.[1] || '';
|
||||||
|
assert.ok(measurementModal, 'measurement confirmation modal should exist');
|
||||||
|
assert.match(measurementModal, /<h3 id="inbound-measurement-title">确认<\/h3>/);
|
||||||
|
assert.match(measurementModal, /请确认是否接受后台测量结果?确认后将按测量结果计费,且不可修改。/);
|
||||||
|
assert.doesNotMatch(measurementModal, /inbound-modal-measure-summary|inbound-modal-measure-rows|箱规长\(cm\)/);
|
||||||
|
assert.match(measurementModal, />确认<\/button>/);
|
||||||
|
|
||||||
|
const selectedTabs = [];
|
||||||
|
const visitedPages = [];
|
||||||
|
const detailContext = {
|
||||||
|
inboundOrderById: () => order,
|
||||||
|
switchPage: page => visitedPages.push(page),
|
||||||
|
switchInboundDetailTab: tab => selectedTabs.push(tab),
|
||||||
|
renderInboundMeasureSummary() {},
|
||||||
|
renderInboundMeasureRows() {},
|
||||||
|
renderInboundCustomsDocs() {},
|
||||||
|
inboundDetailField: () => '',
|
||||||
|
escapeInboundHtml: value => String(value == null ? '' : value),
|
||||||
|
inboundDetailOrderId: null,
|
||||||
|
document: {
|
||||||
|
getElementById: id => id === 'inbound-detail-confirm-btn'
|
||||||
|
? { textContent: '', innerHTML: '', hidden: false, disabled: false }
|
||||||
|
: { textContent: '', innerHTML: '', hidden: false }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const showInboundOrderDetail = vm.runInNewContext(`(${extractFunction('showInboundOrderDetail')})`, detailContext);
|
||||||
|
showInboundOrderDetail('order-1', 'measure');
|
||||||
|
assert.deepEqual(visitedPages, ['inbound-detail']);
|
||||||
|
assert.deepEqual(selectedTabs, ['measure']);
|
||||||
|
|
||||||
|
order.measurementConfirmed = true;
|
||||||
|
const confirmedButton = { textContent: '', innerHTML: '', hidden: false, disabled: false };
|
||||||
|
const confirmedDetailContext = {
|
||||||
|
...detailContext,
|
||||||
|
inboundOrderById: () => order,
|
||||||
|
document: {
|
||||||
|
getElementById: id => id === 'inbound-detail-confirm-btn' ? confirmedButton : { textContent: '', innerHTML: '', hidden: false }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const showConfirmedInboundOrderDetail = vm.runInNewContext(`(${extractFunction('showInboundOrderDetail')})`, confirmedDetailContext);
|
||||||
|
showConfirmedInboundOrderDetail('order-1', 'measure');
|
||||||
|
assert.equal(confirmedButton.disabled, true);
|
||||||
|
assert.equal(confirmedButton.hidden, false);
|
||||||
|
assert.equal(confirmedButton.textContent, '已确认');
|
||||||
|
|
||||||
|
console.log('inbound order list requirements: ok');
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const productPanel = html.match(/id="inbound-panel-product"[\s\S]*?<\/section><\/div>/)?.[0] || '';
|
||||||
|
assert.ok(productPanel, 'product panel should exist');
|
||||||
|
const headers = Array.from(productPanel.matchAll(/<th>([^<]+)<\/th>/g), match => match[1]);
|
||||||
|
assert.deepEqual(headers, ['SKU NO.', '货件编号', '跟踪编号', 'UPC NO.', '中文品名', '图片', '英文品名', '产品规格', '每箱数量', '每箱重量', '每箱体积', '箱数', '材质', '用途', '型号', '品牌', '报关 HS CODE', '清关 HS CODE', '清关申报单价', '清关申报金额', '备注']);
|
||||||
|
|
||||||
|
const body = { innerHTML: '' };
|
||||||
|
const count = { textContent: '' };
|
||||||
|
const context = {
|
||||||
|
document: { getElementById: id => ({ 'inbound-detail-product-rows': body, 'inbound-detail-product-count': count })[id] || null },
|
||||||
|
escapeInboundHtml: value => String(value == null ? '' : value),
|
||||||
|
};
|
||||||
|
const render = vm.runInNewContext(`(${extractFunction('renderInboundProductItems')})`, context);
|
||||||
|
|
||||||
|
render({ productItems: [{ skuNo:'SKU-001', shipmentNo:'FBA-1', chineseName:'陀螺', englishName:'Fidget spinner', boxes:3, declaredAmount:'$42.00' }] });
|
||||||
|
assert.equal((body.innerHTML.match(/<tr>/g) || []).length, 1);
|
||||||
|
assert.match(body.innerHTML, /SKU-001/);
|
||||||
|
assert.match(body.innerHTML, /Fidget spinner/);
|
||||||
|
assert.match(body.innerHTML, /\$42\.00/);
|
||||||
|
assert.match(body.innerHTML, />—<\/td>/);
|
||||||
|
assert.equal(count.textContent, '共 1 条');
|
||||||
|
|
||||||
|
render({ skus:'SKU-A, SKU-B' });
|
||||||
|
assert.equal((body.innerHTML.match(/<tr>/g) || []).length, 2);
|
||||||
|
assert.match(body.innerHTML, /SKU-A/);
|
||||||
|
assert.match(body.innerHTML, /SKU-B/);
|
||||||
|
assert.equal(count.textContent, '共 2 条');
|
||||||
|
|
||||||
|
console.log('inbound product info requirements: ok');
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPage = html.match(/<div class="page-content" id="page-labeling">([\s\S]*?)<!-- ========== MODAL: 编辑贴标申请/)?.[1] || '';
|
||||||
|
const editModal = html.match(/<div class="modal-overlay" id="labeling-edit-modal"([\s\S]*?)<!-- ========== PAGE: 仓储转运单列表/)?.[1] || '';
|
||||||
|
assert.ok(editModal, 'labeling edit modal should exist');
|
||||||
|
|
||||||
|
const fieldNames = source => Array.from(source.matchAll(/\bname="([^"]+)"/g), match => match[1]).sort();
|
||||||
|
assert.deepEqual(fieldNames(editModal), fieldNames(createPage), 'edit modal should expose the same fields as create order');
|
||||||
|
assert.match(editModal, /onsubmit="submitLabelingEdit\(event\)"/);
|
||||||
|
|
||||||
|
const tbody = { innerHTML: '' };
|
||||||
|
const controls = {
|
||||||
|
'labeling-list-body': tbody,
|
||||||
|
'labeling-total': { textContent: '' }
|
||||||
|
};
|
||||||
|
const renderLabelingList = vm.runInNewContext(
|
||||||
|
`(${extractFunction('renderLabelingList')})`,
|
||||||
|
{
|
||||||
|
document: { getElementById: id => controls[id] || null },
|
||||||
|
labelingFilteredOrders: null,
|
||||||
|
loadLabelingOrders: () => [{
|
||||||
|
id: 'order-1', orderNo: 'TB001', inboundNo: 'RK001', warehouse: '美西洛杉矶仓',
|
||||||
|
labelRequirement: '换箱唛', skuCount: 1, totalBoxes: 2, status: '待处理', createdAt: '2026-09-04 09:00'
|
||||||
|
}],
|
||||||
|
escapeLabelingHtml: value => String(value == null ? '' : value),
|
||||||
|
labelingStatusClass: () => 'status-pending'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
renderLabelingList();
|
||||||
|
assert.match(tbody.innerHTML, />查看<\/button>[\s\S]*?>编辑<\/button>/, 'each row should show edit after view');
|
||||||
|
assert.match(tbody.innerHTML, /openLabelingEdit\(this\.dataset\.labelingId\)/);
|
||||||
|
|
||||||
|
const mergeLabelingOrder = vm.runInNewContext(`(${extractFunction('mergeLabelingOrder')})`);
|
||||||
|
const original = {
|
||||||
|
id: 'order-1', orderNo: 'TB001', status: '处理中', createdAt: '2026-09-01 08:00', updatedAt: '2026-09-01 08:00',
|
||||||
|
inboundNo: 'OLD', warehouse: '旧仓', deadline: '2026-09-05T10:00', contact: '旧联系人', phone: '100', remark: '旧备注',
|
||||||
|
labelRequirement: '换箱唛', skuCount: 1, totalBoxes: 2, items: [{ sku: 'OLD-SKU', name: '旧商品', boxes: 2, requirement: '换箱唛', fileName: 'old.pdf', relationFileName: 'old.xlsx' }],
|
||||||
|
fees: { opening: '-', picking: '-', relabel: '-', carton: '-', total: '-' }
|
||||||
|
};
|
||||||
|
const updated = mergeLabelingOrder(original, {
|
||||||
|
inboundNo: 'RK009', warehouse: '美东新泽西仓', deadline: '2026-09-10T10:30', contact: '新联系人', phone: '200', remark: '新备注',
|
||||||
|
items: [{ sku: 'SKU-1', name: '商品一', boxes: 3, requirement: '换SKU标', fileName: '', relationFileName: '' }]
|
||||||
|
});
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(updated)), {
|
||||||
|
id: 'order-1', orderNo: 'TB001', status: '处理中', createdAt: '2026-09-01 08:00', updatedAt: updated.updatedAt,
|
||||||
|
inboundNo: 'RK009', warehouse: '美东新泽西仓', deadline: '2026-09-10T10:30', contact: '新联系人', phone: '200', remark: '新备注',
|
||||||
|
labelRequirement: '换SKU标', skuCount: 1, totalBoxes: 3, items: [{ sku: 'SKU-1', name: '商品一', boxes: 3, requirement: '换SKU标', fileName: 'old.pdf', relationFileName: 'old.xlsx' }],
|
||||||
|
fees: { opening: '-', picking: '-', relabel: '-', carton: '-', total: '-' }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('labeling edit: ok');
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLabelingRelationTemplateRows = vm.runInNewContext(
|
||||||
|
`(${extractFunction('getLabelingRelationTemplateRows')})`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(getLabelingRelationTemplateRows())),
|
||||||
|
[['箱唛', '需贴箱唛', '需贴SKU标', '备注']],
|
||||||
|
'the downloaded relation template should expose the required columns in order'
|
||||||
|
);
|
||||||
|
|
||||||
|
const createPage = html.match(/<div class="page-content" id="page-labeling">([\s\S]*?)<!-- ========== MODAL: 编辑贴标申请/)?.[1] || '';
|
||||||
|
assert.match(
|
||||||
|
createPage,
|
||||||
|
/onclick="downloadLabelingRelationTemplate\(\)"[\s\S]*?onclick="addLabelingRow\(\)"/,
|
||||||
|
'the relation template download should appear before Add row'
|
||||||
|
);
|
||||||
|
|
||||||
|
let writtenWorkbook = null;
|
||||||
|
let writtenFileName = '';
|
||||||
|
const downloadLabelingRelationTemplate = vm.runInNewContext(
|
||||||
|
`(${extractFunction('downloadLabelingRelationTemplate')})`,
|
||||||
|
{
|
||||||
|
getLabelingRelationTemplateRows,
|
||||||
|
showToast: () => {},
|
||||||
|
XLSX: {
|
||||||
|
utils: {
|
||||||
|
aoa_to_sheet: rows => ({ rows }),
|
||||||
|
book_new: () => ({ sheets: [] }),
|
||||||
|
book_append_sheet: (workbook, worksheet, name) => workbook.sheets.push({ worksheet, name })
|
||||||
|
},
|
||||||
|
writeFile: (workbook, fileName) => {
|
||||||
|
writtenWorkbook = workbook;
|
||||||
|
writtenFileName = fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
downloadLabelingRelationTemplate();
|
||||||
|
assert.equal(writtenFileName, '贴标关联模板.xlsx');
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(writtenWorkbook.sheets)),
|
||||||
|
[{ worksheet: { rows: [['箱唛', '需贴箱唛', '需贴SKU标', '备注']], '!cols': [{ wch: 24 }, { wch: 14 }, { wch: 14 }, { wch: 32 }] }, name: '关联明细' }]
|
||||||
|
);
|
||||||
|
|
||||||
|
const generatedRow = {
|
||||||
|
innerHTML: '',
|
||||||
|
querySelector: selector => selector === '.labeling-requirement' ? { value: '' } : null
|
||||||
|
};
|
||||||
|
const createLabelingRow = vm.runInNewContext(
|
||||||
|
`(${extractFunction('createLabelingRow')})`,
|
||||||
|
{
|
||||||
|
document: { createElement: () => generatedRow },
|
||||||
|
escapeLabelingHtml: value => String(value == null ? '' : value)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
createLabelingRow({ relationFileName: '关联表.xlsx', itemRemark: '优先处理 A 箱' });
|
||||||
|
assert.match(generatedRow.innerHTML, /class="[^"]*labeling-relation-file[^"]*"[\s\S]*?关联表\.xlsx/);
|
||||||
|
assert.match(generatedRow.innerHTML, /class="form-input labeling-item-remark"[\s\S]*?value="优先处理 A 箱"/);
|
||||||
|
|
||||||
|
const fileControl = (selectedName, existingName) => ({
|
||||||
|
files: selectedName ? [{ name: selectedName }] : [],
|
||||||
|
dataset: { existingFile: existingName || '' }
|
||||||
|
});
|
||||||
|
const controls = {
|
||||||
|
'.labeling-sku': { value: ' SKU-100 ' },
|
||||||
|
'.labeling-name': { value: ' 商品一 ' },
|
||||||
|
'.labeling-boxes': { value: '3' },
|
||||||
|
'.labeling-transfer-boxes': { value: '2' },
|
||||||
|
'.labeling-shipment-no': { value: ' FBA-1 ' },
|
||||||
|
'.labeling-tracking-no': { value: ' TRACK-1 ' },
|
||||||
|
'.labeling-new-sku': { value: ' NEW-100 ' },
|
||||||
|
'.labeling-requirement': { value: '换SKU标' },
|
||||||
|
'.labeling-label-file input[type="file"]': fileControl('', '贴标文件.pdf'),
|
||||||
|
'.labeling-relation-file input[type="file"]': fileControl('新关联表.xlsx', '旧关联表.xlsx'),
|
||||||
|
'.labeling-item-remark': { value: ' 易碎品 ' }
|
||||||
|
};
|
||||||
|
const collectLabelingRows = vm.runInNewContext(`(${extractFunction('collectLabelingRows')})`);
|
||||||
|
const collectedRows = collectLabelingRows({
|
||||||
|
querySelectorAll: () => [{ querySelector: selector => controls[selector] || null }]
|
||||||
|
});
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(collectedRows)), [{
|
||||||
|
sku: 'SKU-100',
|
||||||
|
name: '商品一',
|
||||||
|
boxes: 3,
|
||||||
|
transferBoxes: 2,
|
||||||
|
shipmentNo: 'FBA-1',
|
||||||
|
trackingNo: 'TRACK-1',
|
||||||
|
newSku: 'NEW-100',
|
||||||
|
requirement: '换SKU标',
|
||||||
|
fileName: '贴标文件.pdf',
|
||||||
|
relationFileName: '新关联表.xlsx',
|
||||||
|
itemRemark: '易碎品'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
let savedOrders = null;
|
||||||
|
const dataRow = { querySelector: selector => controls[selector] || null };
|
||||||
|
const labelingTbody = {
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [dataRow]
|
||||||
|
};
|
||||||
|
const labelingForm = {
|
||||||
|
checkValidity: () => true,
|
||||||
|
elements: {
|
||||||
|
inboundNo: { value: 'RK001' },
|
||||||
|
deadline: { value: '2026-09-10T10:00' },
|
||||||
|
contact: { value: '联系人' },
|
||||||
|
phone: { value: '13800000000' },
|
||||||
|
remark: { value: '订单备注' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const submitLabeling = vm.runInNewContext(
|
||||||
|
`(${extractFunction('submitLabeling')})`,
|
||||||
|
{
|
||||||
|
document: { getElementById: id => id === 'labeling-form' ? labelingForm : labelingTbody },
|
||||||
|
getLabelingInboundByNo: () => ({ warehouse: '美西洛杉矶仓' }),
|
||||||
|
loadLabelingOrders: () => [],
|
||||||
|
collectLabelingRows,
|
||||||
|
saveLabelingOrders: orders => { savedOrders = orders; return true; },
|
||||||
|
renderLabelingList: () => {},
|
||||||
|
openLabelingList: () => {},
|
||||||
|
showToast: () => {},
|
||||||
|
labelingFilteredOrders: null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
submitLabeling({ preventDefault: () => {} });
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(savedOrders[0].items)),
|
||||||
|
JSON.parse(JSON.stringify(collectedRows)),
|
||||||
|
'creating an order should persist all per-row labeling data'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('labeling relation fields: ok');
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function textContent(markup) {
|
||||||
|
return markup.replace(/<[^>]+>/g, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstHeaders(pageMarkup) {
|
||||||
|
return Array.from(pageMarkup.matchAll(/<th(?:\s[^>]*)?>([\s\S]*?)<\/th>/g), match => textContent(match[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function page(id, endMarker) {
|
||||||
|
return html.match(new RegExp(`<div class="page-content" id="${id}">([\\s\\S]*?)${endMarker}`))?.[1] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(firstHeaders(page('page-inbound', '<!-- ========== PAGE: 头程订单详情'))[0], '序号');
|
||||||
|
assert.equal(firstHeaders(page('page-delivery', '<!-- ========== PAGE: 异常订单列表'))[0], '序号');
|
||||||
|
assert.equal(firstHeaders(page('page-exception-orders', '<!-- ========== PAGE: 异常订单详情'))[0], '序号');
|
||||||
|
assert.equal(firstHeaders(page('page-labeling-list', '<!-- ========== PAGE: 贴标申请'))[0], '序号');
|
||||||
|
assert.equal(firstHeaders(page('page-storage-transfer-list', '<!-- ========== PAGE: 仓储转运'))[0], '序号');
|
||||||
|
assert.equal(firstHeaders(page('page-cargo-disposal-list', '<!-- ========== PAGE: 新增货物废弃'))[0], '序号');
|
||||||
|
assert.equal(firstHeaders(page('page-return-relabel-list', '<div class="page-content" id="page-return-relabel-create">'))[0], '序号');
|
||||||
|
|
||||||
|
for (const id of ['sea-procurement-orders-body', 'sea-truck-orders-body', 'sea-pallet-orders-body']) {
|
||||||
|
const table = html.match(new RegExp(`<tbody id="${id}">`))?.[0] || '';
|
||||||
|
assert.ok(table, `${id} should exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstCell(markup) {
|
||||||
|
return textContent(markup.match(/<td(?:\s[^>]*)?>([\s\S]*?)<\/td>/)?.[1] || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const seaBody = { innerHTML: '' };
|
||||||
|
const seaContext = {
|
||||||
|
document: {
|
||||||
|
getElementById: id => ({
|
||||||
|
'sea-procurement-orders-body': seaBody,
|
||||||
|
'sea-procurement-total': { textContent: '' },
|
||||||
|
'sea-procurement-page-size': { value: '' },
|
||||||
|
'sea-procurement-page-number': { textContent: '' },
|
||||||
|
'sea-procurement-prev': { disabled: false },
|
||||||
|
'sea-procurement-next': { disabled: false }
|
||||||
|
})[id] || null
|
||||||
|
},
|
||||||
|
seaBusinessSlug: () => 'procurement',
|
||||||
|
seaOrders: { Procurement: [{ id: 'one', orderNo: 'PO-1', fields: { '采购品类': '箱子' }, status: '处理中' }] },
|
||||||
|
seaPageState: { Procurement: { orderNo: '', customer: '', status: '' } },
|
||||||
|
seaPaginationState: { Procurement: { currentPage: 1, pageSize: 10 } },
|
||||||
|
paginateSeaOrders: (orders, currentPage, pageSize) => ({ total: orders.length, pages: 1, currentPage, pageSize, rows: orders }),
|
||||||
|
seaOrderCells: () => ['PO-1', '箱子', '-', '-', '-', '处理中'],
|
||||||
|
escapeSeaOrderHtml: value => String(value == null ? '' : value),
|
||||||
|
seaBusinessName: () => '采购'
|
||||||
|
};
|
||||||
|
const renderSeaOrders = vm.runInNewContext(`(${extractFunction('renderSeaOrders')})`, seaContext);
|
||||||
|
renderSeaOrders('Procurement');
|
||||||
|
assert.equal(firstCell(seaBody.innerHTML), '1');
|
||||||
|
|
||||||
|
const storageBody = { innerHTML: '' };
|
||||||
|
const storageControls = {
|
||||||
|
'storage-transfer-list-body': storageBody,
|
||||||
|
'storage-transfer-total': { textContent: '' },
|
||||||
|
'storage-transfer-page-number': { textContent: '' },
|
||||||
|
'storage-transfer-prev': { disabled: false },
|
||||||
|
'storage-transfer-next': { disabled: false }
|
||||||
|
};
|
||||||
|
const renderStorageTransferList = vm.runInNewContext(`(${extractFunction('renderStorageTransferList')})`, {
|
||||||
|
document: { getElementById: id => storageControls[id] || null },
|
||||||
|
storageTransferFilteredOrders: null,
|
||||||
|
loadStorageTransferOrders: () => [{ id: 'one', orderNo: 'ZY-1', inboundNo: 'RK-1', boxCount: 1, warehouse: '仓', fbaCode: 'ONT8', shipmentId: 'FBA-1', deliveryMethod: '卡车', relabel: 'no', tailOrderNo: 'LC-1', status: '待处理', createdAt: '2026-09-01' }],
|
||||||
|
STORAGE_TRANSFER_PAGE_SIZE: 10,
|
||||||
|
storageTransferCurrentPage: 1,
|
||||||
|
escapeStorageTransferHtml: value => String(value == null ? '' : value)
|
||||||
|
});
|
||||||
|
renderStorageTransferList();
|
||||||
|
assert.equal(firstCell(storageBody.innerHTML), '1');
|
||||||
|
|
||||||
|
console.log('order list index requirements: ok');
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const detail = html.match(/<section class="sea-business-detail-page" id="sea-pallet-detail"[\s\S]*?<div class="sea-business-form-panel" data-order-panel="Pallet"[^>]*>/)?.[0] || '';
|
||||||
|
|
||||||
|
assert.ok(detail, '美本土散板详情视图 should exist');
|
||||||
|
assert.match(detail, /class="pallet-detail-breadcrumb"/);
|
||||||
|
assert.match(detail, /class="pallet-detail-header"/);
|
||||||
|
assert.match(detail, /data-detail-content="status"/);
|
||||||
|
assert.match(detail, /class="pallet-detail-grid"/);
|
||||||
|
for (const card of ['transport', 'contact', 'cargo', 'timing']) {
|
||||||
|
assert.match(detail, new RegExp(`data-detail-card="${card}"`));
|
||||||
|
}
|
||||||
|
assert.doesNotMatch(detail, /business-detail-tabs-shell/);
|
||||||
|
assert.match(detail, /<dt>运输方式<\/dt>/);
|
||||||
|
assert.match(detail, /<dt>是否打盘<\/dt>/);
|
||||||
|
assert.match(detail, /<dt>总重量<\/dt>/);
|
||||||
|
assert.match(detail, /<dt>预算总价<\/dt>/);
|
||||||
|
|
||||||
|
console.log('pallet detail layout requirements: ok');
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
const createPage = html.match(/<div class="page-content" id="page-return-relabel-create">([\s\S]*?)<\/div>\s*<!-- ========== MODAL: 编辑退货换标单/)?.[1] || '';
|
||||||
|
const editModal = html.match(/<div class="modal-overlay" id="return-relabel-edit-modal"([\s\S]*?)<!-- ========== MODAL:/)?.[1] || '';
|
||||||
|
const detailScript = html.match(/function openReturnRelabelDetail\(orderId\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
const submitScript = html.match(/function submitReturnRelabelOrder\(event\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
const editSubmitScript = html.match(/function submitReturnRelabelEdit\(event\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
const associatedUiScript = html.match(/function ensureReturnRelabelAssociatedFields\(\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
|
||||||
|
assert.ok(createPage, '新建退货换标页 should exist');
|
||||||
|
assert.ok(editModal, '退货换标编辑弹框 should exist');
|
||||||
|
|
||||||
|
assert.match(editModal, /关联文件模板/, '编辑换标设置应包含关联文件模板入口');
|
||||||
|
assert.match(editModal, /name="relationFile"/, '编辑换标设置应包含上传关联文件');
|
||||||
|
assert.match(editModal, /name="relationRemark"/, '编辑换标设置应包含关联文件备注');
|
||||||
|
assert.match(associatedUiScript, /关联文件模板/, '新建换标设置应包含关联文件模板入口');
|
||||||
|
assert.match(associatedUiScript, /name="relationFile"/, '新建换标设置应包含上传关联文件');
|
||||||
|
assert.match(associatedUiScript, /name="relationRemark"/, '新建换标设置应包含关联文件备注');
|
||||||
|
|
||||||
|
assert.match(createPage, /贴标要求[\s\S]*上传箱唛[\s\S]*上传 SKU 标签/);
|
||||||
|
assert.match(associatedUiScript, /关联文件模板[\s\S]*上传关联文件[\s\S]*备注/);
|
||||||
|
assert.match(editModal, /贴标要求[\s\S]*关联文件模板[\s\S]*上传箱唛[\s\S]*上传 SKU 标签[\s\S]*上传关联文件[\s\S]*备注/);
|
||||||
|
assert.match(associatedUiScript, /downloadReturnRelabelRelationTemplate\(\)/);
|
||||||
|
assert.match(editModal, /downloadReturnRelabelRelationTemplate\(\)/);
|
||||||
|
assert.match(html, /submitReturnRelabelOrder=function[\s\S]*relationFile/);
|
||||||
|
assert.match(html, /submitReturnRelabelEdit=function[\s\S]*relationFile/);
|
||||||
|
assert.match(detailScript, /return-relabel-detail-relation-file/);
|
||||||
|
assert.match(detailScript, /return-relabel-detail-relation-remark/);
|
||||||
|
|
||||||
|
console.log('return relabel associated fields: ok');
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
html,
|
||||||
|
/\.client-shell #page-return-relabel-create \.return-relabel-form-grid\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*repeat\(3,\s*minmax\(0,\s*1fr\)\)/,
|
||||||
|
'新建退货换标页应恢复三列表单布局'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
html,
|
||||||
|
/\.client-shell #page-return-relabel-create :is\(\.return-relabel-input,\s*\.return-relabel-select,\s*\.return-relabel-textarea\)\s*\{[^}]*width:\s*100%;[^}]*border:\s*1px solid #dbe3ef/,
|
||||||
|
'新建退货换标页应恢复表单控件样式'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
html,
|
||||||
|
/\.client-shell #page-return-relabel-create \.return-relabel-settings-panel\s*\{[^}]*border:\s*1px solid #dce6ee;[^}]*background:\s*#fbfdfe/,
|
||||||
|
'新建退货换标页应恢复换标设置面板样式'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
html,
|
||||||
|
/@media\s*\(max-width:\s*760px\)[\s\S]*?\.client-shell #page-return-relabel-create \.return-relabel-form-grid[\s\S]*?grid-template-columns:\s*1fr/,
|
||||||
|
'新建退货换标页在窄屏应恢复单列布局'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('return relabel create styles: ok');
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const list = html.match(/<div class="page-content" id="page-return-relabel-list">([\s\S]*?)<div class="page-content" id="page-return-relabel-create">/)?.[1] || '';
|
||||||
|
const detail = html.match(/<div class="page-content" id="page-return-relabel-detail">([\s\S]*?)<!-- ========== PAGE: 旧退货入库/)?.[1] || '';
|
||||||
|
const detailScript = html.match(/function openReturnRelabelDetail\(orderId\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
|
||||||
|
function extractDivByClass(source, className) {
|
||||||
|
const opening = `<div class="${className}">`;
|
||||||
|
const start = source.indexOf(opening);
|
||||||
|
assert.notEqual(start, -1, `${className} should exist`);
|
||||||
|
const tags = /<\/?div\b[^>]*>/g;
|
||||||
|
tags.lastIndex = start;
|
||||||
|
let depth = 0;
|
||||||
|
let match;
|
||||||
|
while ((match = tags.exec(source))) {
|
||||||
|
depth += match[0].startsWith('</') ? -1 : 1;
|
||||||
|
if (depth === 0) return source.slice(start, tags.lastIndex);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${className}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(list, '退货换标列表页 should exist');
|
||||||
|
assert.ok(detail, '退货换标详情页 should exist');
|
||||||
|
assert.match(list, /class="storage-transfer-filter-grid"/);
|
||||||
|
assert.match(list, /class="storage-transfer-filter-item"/);
|
||||||
|
assert.match(list, /class="storage-transfer-filter-label"/);
|
||||||
|
assert.match(list, /class="storage-transfer-input"/);
|
||||||
|
assert.match(list, /class="storage-transfer-select"/);
|
||||||
|
assert.match(list, /class="storage-transfer-date-range"/);
|
||||||
|
assert.match(list, /class="storage-transfer-filter-actions"/);
|
||||||
|
const filterGrid = extractDivByClass(list, 'storage-transfer-filter-grid');
|
||||||
|
const createdEndIndex = filterGrid.indexOf('id="return-relabel-filter-created-end"');
|
||||||
|
const actionsIndex = filterGrid.indexOf('<div class="storage-transfer-filter-actions">');
|
||||||
|
assert.ok(createdEndIndex >= 0, '创建时间结束日期应存在');
|
||||||
|
assert.ok(actionsIndex > createdEndIndex, '查询/重置按钮应紧跟创建时间筛选项,并位于筛选网格内');
|
||||||
|
assert.doesNotMatch(list, /return-relabel-filter-(?:card|grid|actions)/);
|
||||||
|
assert.doesNotMatch(list, /return-relabel-(?:input|select|list-card|pagination)/);
|
||||||
|
assert.match(list, /class="card"><div class="table-wrap-simple"><table class="data-table return-relabel-table"/);
|
||||||
|
assert.match(list, /class="pagination-bar"/);
|
||||||
|
assert.doesNotMatch(list, /storage-transfer-list-card|storage-transfer-pagination/);
|
||||||
|
assert.doesNotMatch(detail, /business-detail-tabs-shell/, '退货换标详情不应再使用标签页容器');
|
||||||
|
assert.match(detail, /class="labeling-detail-grid return-relabel-detail-grid"/);
|
||||||
|
assert.match(detail, /class="card labeling-detail-card"/);
|
||||||
|
assert.match(detail, /class="card labeling-detail-card return-relabel-detail-order-card"/);
|
||||||
|
assert.match(detail, /class="return-relabel-detail-order-grid" id="return-relabel-detail-summary"/);
|
||||||
|
assert.match(detail, /class="info-section-title">订单信息<\/div>/);
|
||||||
|
assert.match(detail, /class="info-section-title">商品信息<\/div>/);
|
||||||
|
assert.match(detail, /class="info-section-title">换标数据<\/div>/);
|
||||||
|
assert.match(detail, /id="return-relabel-detail-order-no"/);
|
||||||
|
assert.match(detail, /class="labeling-detail-status"[^>]*id="return-relabel-detail-status"/);
|
||||||
|
assert.match(detailScript, /return-relabel-detail-order-no/);
|
||||||
|
assert.match(detailScript, /labeling-detail-field/);
|
||||||
|
assert.match(detailScript, /return-relabel-detail-status/);
|
||||||
|
assert.match(detail, /id="return-relabel-detail-relabel-data"[\s\S]*return-relabel-detail-relabel-enabled/);
|
||||||
|
assert.match(detail, /id="return-relabel-detail-relabel-data"[\s\S]*return-relabel-detail-label-case/);
|
||||||
|
assert.match(detail, /id="return-relabel-detail-relabel-data"[\s\S]*return-relabel-detail-carton-label/);
|
||||||
|
assert.match(detail, /id="return-relabel-detail-relabel-data"[\s\S]*return-relabel-detail-sku-label/);
|
||||||
|
assert.doesNotMatch(detail, /return-relabel-detail-tail-order-no/);
|
||||||
|
assert.doesNotMatch(detail, /return-relabel-detail-tail-status/);
|
||||||
|
assert.doesNotMatch(detailScript, /\['是否换标'/);
|
||||||
|
assert.doesNotMatch(detailScript, /\['贴标要求'/);
|
||||||
|
assert.doesNotMatch(detailScript, /\['上传箱唛'/);
|
||||||
|
assert.doesNotMatch(detailScript, /\['上传 SKU 标签'/);
|
||||||
|
assert.doesNotMatch(detail, /storage-transfer-section|storage-transfer-summary-grid|storage-transfer-relation-grid/);
|
||||||
|
assert.doesNotMatch(detail, /return-relabel-(?:section|summary-grid|relation-grid|table-wrap)/);
|
||||||
|
assert.doesNotMatch(detail, /后台关联单据/);
|
||||||
|
|
||||||
|
assert.match(html, /\.return-relabel-detail-order-card\s*\{[^}]*grid-column:\s*1\s*\/\s*-1/);
|
||||||
|
assert.match(html, /\.return-relabel-detail-order-grid\s*\{[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\)/);
|
||||||
|
assert.match(html, /@media\s*\(max-width:\s*760px\)[\s\S]*\.return-relabel-detail-order-grid\s*\{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)/);
|
||||||
|
|
||||||
|
console.log('return relabel detail layout requirements: ok');
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editModal = html.match(/<div class="modal-overlay" id="return-relabel-edit-modal"([\s\S]*?)<!-- ========== MODAL:/)?.[1] || '';
|
||||||
|
assert.ok(editModal, '退货换标编辑弹框 should exist');
|
||||||
|
assert.match(editModal, /onsubmit="submitReturnRelabelEdit\(event\)"/);
|
||||||
|
for (const field of ['sourceFba', 'reason', 'carrier', 'trackingNo', 'returnRemark', 'relabel', 'labelCase', 'cartonLabel', 'skuLabel', 'destinationFba', 'address', 'expectedArrival', 'tailRemark']) {
|
||||||
|
assert.match(editModal, new RegExp(`name="${field}"`), `edit modal should include ${field}`);
|
||||||
|
}
|
||||||
|
assert.match(editModal, /id="return-relabel-edit-sku-body"/);
|
||||||
|
assert.match(editModal, /id="return-relabel-edit-error"/);
|
||||||
|
assert.match(editModal, /当前文件/);
|
||||||
|
|
||||||
|
const tbody = { innerHTML: '' };
|
||||||
|
const controls = {
|
||||||
|
'return-relabel-list-body': tbody,
|
||||||
|
'return-relabel-total': { textContent: '' },
|
||||||
|
'return-relabel-page-number': { textContent: '' },
|
||||||
|
'return-relabel-prev': { disabled: false },
|
||||||
|
'return-relabel-next': { disabled: false }
|
||||||
|
};
|
||||||
|
const renderReturnRelabelList = vm.runInNewContext(
|
||||||
|
`(${extractFunction('renderReturnRelabelList')})`,
|
||||||
|
{
|
||||||
|
document: { getElementById: id => controls[id] || null },
|
||||||
|
returnRelabelFilteredOrders: null,
|
||||||
|
loadReturnRelabelOrders: () => [{
|
||||||
|
id: 'order-1', orderNo: 'THHB001', sourceFba: 'ONT8', destinationFba: 'LAX9',
|
||||||
|
relabel: true, labelCase: 'Case1', backendTailOrderNo: 'LC001',
|
||||||
|
backendRelabelOrderNo: 'HB001', relabelStatus: '待换标', tailStatus: '待入库',
|
||||||
|
createdAt: '2026-09-04 09:00', skus: [{ sku: 'SKU-1', quantity: 2 }]
|
||||||
|
}],
|
||||||
|
RETURN_RELABEL_PAGE_SIZE: 10,
|
||||||
|
returnRelabelCurrentPage: 1,
|
||||||
|
returnRelabelLabelText: () => '换箱唛',
|
||||||
|
escapeReturnRelabelHtml: value => String(value == null ? '' : value)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
renderReturnRelabelList();
|
||||||
|
assert.match(tbody.innerHTML, />查看<\/button>[\s\S]*?>编辑<\/button>/, '编辑 should follow 查看');
|
||||||
|
assert.match(tbody.innerHTML, /openReturnRelabelEdit\(this\.dataset\.returnRelabelEditId\)/);
|
||||||
|
|
||||||
|
const mergeReturnRelabelOrder = vm.runInNewContext(`(${extractFunction('mergeReturnRelabelOrder')})`);
|
||||||
|
const original = {
|
||||||
|
id: 'order-1', orderNo: 'THHB001', backendTailOrderNo: 'LC001', backendRelabelOrderNo: 'HB001',
|
||||||
|
relabelStatus: '换标中', tailStatus: '待入库', createdAt: '2026-09-01 08:00',
|
||||||
|
sourceFba: 'ONT8', carrier: 'UPS', cartonLabel: 'old-carton.pdf', skuLabel: ''
|
||||||
|
};
|
||||||
|
const updated = mergeReturnRelabelOrder(original, {
|
||||||
|
sourceFba: 'ABE8', destinationFba: 'ONT8', reason: '客户退货', carrier: 'FedEx',
|
||||||
|
trackingNo: 'NEW-TRACK', returnRemark: '已更新', relabel: true, labelCase: 'Case3',
|
||||||
|
cartonLabel: 'old-carton.pdf', skuLabel: 'new-sku.pdf', address: 'New address',
|
||||||
|
expectedArrival: '2026-09-10T10:30', tailRemark: 'new remark',
|
||||||
|
skus: [{ sku: 'SKU-2', name: '新品', fnsku: 'FN-2', newSku: 'NEW-2', quantity: 5 }]
|
||||||
|
});
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(updated)), {
|
||||||
|
id: 'order-1', orderNo: 'THHB001', backendTailOrderNo: 'LC001', backendRelabelOrderNo: 'HB001',
|
||||||
|
relabelStatus: '换标中', tailStatus: '待入库', createdAt: '2026-09-01 08:00',
|
||||||
|
sourceFba: 'ABE8', destinationFba: 'ONT8', reason: '客户退货', carrier: 'FedEx',
|
||||||
|
trackingNo: 'NEW-TRACK', returnRemark: '已更新', relabel: true, labelCase: 'Case3',
|
||||||
|
cartonLabel: 'old-carton.pdf', skuLabel: 'new-sku.pdf', address: 'New address',
|
||||||
|
expectedArrival: '2026-09-10T10:30', tailRemark: 'new remark',
|
||||||
|
skus: [{ sku: 'SKU-2', name: '新品', fnsku: 'FN-2', newSku: 'NEW-2', quantity: 5 }]
|
||||||
|
});
|
||||||
|
|
||||||
|
const saved = [];
|
||||||
|
const storageContext = {
|
||||||
|
localStorage: {
|
||||||
|
getItem: () => JSON.stringify([{ id: 'return-relabel-seed-1', orderNo: 'EDITED', skus: [{ sku: 'SKU', name: '商品', fnsku: 'FN', quantity: 1 }] }]),
|
||||||
|
setItem: (_key, value) => saved.push(JSON.parse(value))
|
||||||
|
},
|
||||||
|
RETURN_RELABEL_KEY: 'key',
|
||||||
|
RETURN_RELABEL_SEED: [{ id: 'return-relabel-seed-1', orderNo: 'SEED', skus: [{ sku: 'SKU', name: '商品', fnsku: 'FN', quantity: 1 }] }],
|
||||||
|
showToast() {}
|
||||||
|
};
|
||||||
|
const loadReturnRelabelOrders = vm.runInNewContext(`(${extractFunction('loadReturnRelabelOrders')})`, storageContext);
|
||||||
|
const saveReturnRelabelOrders = vm.runInNewContext(`(${extractFunction('saveReturnRelabelOrders')})`, storageContext);
|
||||||
|
assert.equal(loadReturnRelabelOrders()[0].orderNo, 'EDITED', 'saved seed override should win');
|
||||||
|
assert.equal(saveReturnRelabelOrders([{ id: 'return-relabel-seed-1', orderNo: 'EDITED', skus: [] }]), true);
|
||||||
|
assert.equal(saved[0][0].id, 'return-relabel-seed-1', 'seed edits should be persisted');
|
||||||
|
|
||||||
|
console.log('return relabel edit: ok');
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const createPage = html.match(/<div class="page-content" id="page-return-relabel-create">([\s\S]*?)<\/div>\s*<!-- ========== MODAL: 编辑退货换标单/)?.[1] || '';
|
||||||
|
const editModal = html.match(/<div class="modal-overlay" id="return-relabel-edit-modal"([\s\S]*?)<!-- ========== MODAL:/)?.[1] || '';
|
||||||
|
const submitScript = html.match(/function submitReturnRelabelOrder\(event\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
const editSubmitScript = html.match(/function submitReturnRelabelEdit\(event\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
const detailScript = html.match(/function openReturnRelabelDetail\(orderId\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
const validateScript = html.match(/function validateReturnRelabelForm\(\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
const editValidateScript = html.match(/function validateReturnRelabelEditForm\(\)\{([\s\S]*?)\n \}/)?.[1] || '';
|
||||||
|
|
||||||
|
assert.ok(createPage, '新建退货换标页 should exist');
|
||||||
|
assert.ok(editModal, '编辑退货换标弹框 should exist');
|
||||||
|
|
||||||
|
const inboundIndex = createPage.indexOf('name="inboundCode"');
|
||||||
|
const sourceIndex = createPage.indexOf('name="sourceFba"');
|
||||||
|
const reasonIndex = createPage.indexOf('name="reason"');
|
||||||
|
const receivingIndex = createPage.indexOf('name="receivingWarehouse"');
|
||||||
|
assert.ok(inboundIndex >= 0, '新建页应包含入仓码');
|
||||||
|
assert.ok(createPage.match(/name="inboundCode"[^>]*required/), '入仓码应必填');
|
||||||
|
assert.ok(sourceIndex >= 0 && inboundIndex < sourceIndex, '入仓码应在来源 FBA 仓前');
|
||||||
|
assert.ok(receivingIndex >= 0, '新建页应包含收货仓');
|
||||||
|
assert.ok(createPage.match(/name="receivingWarehouse"[^>]*required/), '收货仓应必填');
|
||||||
|
assert.ok(reasonIndex >= 0 && receivingIndex > reasonIndex, '收货仓应在退货原因后');
|
||||||
|
assert.match(createPage, /<option>洛杉矶仓<\/option>[\s\S]*<option>纽约仓<\/option>/);
|
||||||
|
|
||||||
|
for (const field of ['inboundCode', 'receivingWarehouse']) {
|
||||||
|
assert.match(editModal, new RegExp(`name="${field}"`), `编辑弹框应包含 ${field}`);
|
||||||
|
}
|
||||||
|
assert.match(submitScript, /inboundCode:form\.elements\.inboundCode\.value\.trim\(\)/);
|
||||||
|
assert.match(submitScript, /receivingWarehouse:form\.elements\.receivingWarehouse\.value/);
|
||||||
|
assert.match(editSubmitScript, /inboundCode:form\.elements\.inboundCode\.value\.trim\(\)/);
|
||||||
|
assert.match(editSubmitScript, /receivingWarehouse:form\.elements\.receivingWarehouse\.value/);
|
||||||
|
assert.match(validateScript, /inboundCode/);
|
||||||
|
assert.match(validateScript, /receivingWarehouse/);
|
||||||
|
assert.match(editValidateScript, /inboundCode/);
|
||||||
|
assert.match(editValidateScript, /receivingWarehouse/);
|
||||||
|
assert.match(detailScript, /入仓码/);
|
||||||
|
assert.match(detailScript, /收货仓/);
|
||||||
|
|
||||||
|
console.log('return relabel inbound receiving: ok');
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const list = html.match(/<div class="page-content" id="page-return-relabel-list">([\s\S]*?)<div class="page-content" id="page-return-relabel-create">/)?.[1] || '';
|
||||||
|
const tableHead = list.match(/<table class="data-table return-relabel-table">[\s\S]*?<thead><tr>([\s\S]*?)<\/tr><\/thead>/)?.[1] || '';
|
||||||
|
const headers = [...tableHead.matchAll(/<th[^>]*>([\s\S]*?)<\/th>/g)].map(([, label]) => label.replace(/<[^>]+>/g, '').trim());
|
||||||
|
const renderScript = html.match(/function renderReturnRelabelList\(\)\{([\s\S]*?)\n function ensureReturnRelabelAssociatedFields/)?.[1] || '';
|
||||||
|
|
||||||
|
assert.deepEqual(headers, [
|
||||||
|
'序号', '退货换标单号', '入仓码', '来源FBA仓', '目的FBA仓', 'SKU汇总',
|
||||||
|
'退件总数', '是否换标', '贴标要求', '换标单号', '换标状态', '创建时间', '操作'
|
||||||
|
], '退货换标列表列顺序应包含入仓码并隐藏尾程字段');
|
||||||
|
assert.equal((list.match(/<th>尾程订单号<\/th>/g) || []).length, 0, '列表不应显示尾程订单号列');
|
||||||
|
assert.equal((list.match(/<th>尾程订单状态<\/th>/g) || []).length, 0, '列表不应显示尾程订单状态列');
|
||||||
|
assert.match(renderScript, /escapeReturnRelabelHtml\(o\.inboundCode\|\|'—'\)/, '列表行应渲染入仓码');
|
||||||
|
assert.doesNotMatch(renderScript, /backendTailOrderNo/, '列表行不应渲染尾程订单号');
|
||||||
|
assert.doesNotMatch(renderScript, /o\.tailStatus/, '列表行不应渲染尾程订单状态');
|
||||||
|
assert.match(renderScript, /<td colspan="13">暂无退货换标订单<\/td>/, '空列表列数应与新表头一致');
|
||||||
|
|
||||||
|
console.log('return relabel list columns: ok');
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const createPage = html.match(/<div class="page-content" id="page-return-relabel-create">([\s\S]*?)<div class="page-content" id="page-return-relabel-detail">/)?.[1] || '';
|
||||||
|
assert.ok(createPage, '新建退货换标页 should exist');
|
||||||
|
assert.match(createPage, /name="inboundCode"[^>]*required/, '入仓码应必填');
|
||||||
|
assert.match(createPage, /<label><span[^>]*color:\s*#ff4d4f[^>]*>\*<\/span>入仓码<\/label>/, '入仓码前应显示红色星号');
|
||||||
|
assert.doesNotMatch(createPage, /<th>承运商<span[^>]*>\*<\/span><\/th>/, '承运商不应显示必填标记');
|
||||||
|
assert.doesNotMatch(createPage, /<th>运单号<span[^>]*>\*<\/span><\/th>/, '运单号不应显示必填标记');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function control(value, extra = {}) {
|
||||||
|
return { value, ...extra };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cargoRow({ carrier = '', trackingNo = '' } = {}) {
|
||||||
|
const fields = {
|
||||||
|
'.return-relabel-carrier': control(carrier),
|
||||||
|
'.return-relabel-tracking-no': control(trackingNo),
|
||||||
|
'.return-relabel-sku': control('SKU-1'),
|
||||||
|
'.return-relabel-name': control('商品一'),
|
||||||
|
'.return-relabel-fnsku': control('FN-1'),
|
||||||
|
'.return-relabel-quantity': control('1')
|
||||||
|
};
|
||||||
|
return { querySelector: selector => fields[selector] || null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = {
|
||||||
|
checkValidity: () => true,
|
||||||
|
reportValidity() {},
|
||||||
|
elements: {
|
||||||
|
inboundCode: control('IN-001'),
|
||||||
|
receivingWarehouse: control('洛杉矶仓'),
|
||||||
|
address: control('123 Main St'),
|
||||||
|
relabel: control('', { checked: false }),
|
||||||
|
labelCase: control('Case1'),
|
||||||
|
cartonLabel: control('', { files: [] }),
|
||||||
|
skuLabel: control('', { files: [] })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const error = { textContent: '' };
|
||||||
|
const rows = [cargoRow()];
|
||||||
|
const context = {
|
||||||
|
document: {
|
||||||
|
getElementById: id => ({
|
||||||
|
'return-relabel-form': form,
|
||||||
|
'return-relabel-error': error
|
||||||
|
}[id] || null),
|
||||||
|
querySelectorAll: selector => selector === '#return-relabel-sku-body tr' ? rows : []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const validateReturnRelabelForm = vm.runInNewContext(
|
||||||
|
`(${extractFunction('validateReturnRelabelForm')})`,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(validateReturnRelabelForm(), true, '承运商和运单号为空时仍应允许提交');
|
||||||
|
form.elements.inboundCode.value = '';
|
||||||
|
assert.equal(validateReturnRelabelForm(), false, '入仓码为空时应阻止提交');
|
||||||
|
assert.equal(error.textContent, '请完整填写退货和尾程信息');
|
||||||
|
|
||||||
|
console.log('return relabel required fields: ok');
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression caught: logistics fields drift back to order-level controls or
|
||||||
|
// appear after the SKU instead of being the first two cargo columns.
|
||||||
|
const createPage = html.match(/<div class="page-content" id="page-return-relabel-create">([\s\S]*?)<div class="page-content" id="page-return-relabel-detail">/)?.[1] || '';
|
||||||
|
const returnInfo = createPage.match(/<div class="return-relabel-section-title">退货信息<\/div>([\s\S]*?)<div class="card return-relabel-section">/)?.[1] || '';
|
||||||
|
const cargoHeader = createPage.match(/id="return-relabel-sku-table">\s*<thead><tr>([\s\S]*?)<\/tr><\/thead>/)?.[1] || '';
|
||||||
|
assert.ok(returnInfo, '退货信息区域 should exist');
|
||||||
|
assert.doesNotMatch(returnInfo, /name="(?:carrier|trackingNo)"/, '物流字段不应再作为整单输入');
|
||||||
|
assert.match(
|
||||||
|
cargoHeader,
|
||||||
|
/^<th>承运商[\s\S]*?<th>运单号[\s\S]*?<th>商品SKU/,
|
||||||
|
'承运商、运单号应为商品 SKU 前两列'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Carrier/tracking controls remain available on each SKU row but are optional.
|
||||||
|
const appendedRows = [];
|
||||||
|
const rowContext = {
|
||||||
|
document: {
|
||||||
|
getElementById: id => id === 'return-relabel-sku-body' ? { appendChild: row => appendedRows.push(row) } : null,
|
||||||
|
createElement: () => ({ innerHTML: '' })
|
||||||
|
},
|
||||||
|
escapeReturnRelabelHtml: value => String(value == null ? '' : value)
|
||||||
|
};
|
||||||
|
const addReturnRelabelSkuRow = vm.runInNewContext(
|
||||||
|
`(${extractFunction('addReturnRelabelSkuRow')})`,
|
||||||
|
rowContext
|
||||||
|
);
|
||||||
|
addReturnRelabelSkuRow({ carrier: 'UPS', trackingNo: '1Z-001', sku: 'SKU-1' });
|
||||||
|
assert.match(
|
||||||
|
appendedRows[0].innerHTML,
|
||||||
|
/^<td><input[^>]*return-relabel-carrier[^>]*value="UPS"[^>]*><\/td><td><input[^>]*return-relabel-tracking-no[^>]*value="1Z-001"[^>]*><\/td><td><input[^>]*return-relabel-sku/,
|
||||||
|
'每条 SKU 行都应先生成可选的承运商和运单号输入框'
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(appendedRows[0].innerHTML, /return-relabel-carrier[^>]*required/, '承运商不应设置 required');
|
||||||
|
assert.doesNotMatch(appendedRows[0].innerHTML, /return-relabel-tracking-no[^>]*required/, '运单号不应设置 required');
|
||||||
|
|
||||||
|
function control(value, extra = {}) {
|
||||||
|
return { value, ...extra };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cargoRow(values) {
|
||||||
|
const fields = {
|
||||||
|
'.return-relabel-carrier': control(values.carrier),
|
||||||
|
'.return-relabel-tracking-no': control(values.trackingNo),
|
||||||
|
'.return-relabel-sku': control(values.sku),
|
||||||
|
'.return-relabel-name': control(values.name),
|
||||||
|
'.return-relabel-fnsku': control(values.fnsku),
|
||||||
|
'.return-relabel-new-sku': control(values.newSku || ''),
|
||||||
|
'.return-relabel-quantity': control(String(values.quantity))
|
||||||
|
};
|
||||||
|
return { querySelector: selector => fields[selector] || null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
cargoRow({ carrier: 'UPS', trackingNo: '1Z-001', sku: 'SKU-1', name: '商品一', fnsku: 'FN-1', quantity: 2 }),
|
||||||
|
cargoRow({ carrier: 'FedEx', trackingNo: 'FX-002', sku: 'SKU-2', name: '商品二', fnsku: 'FN-2', quantity: 3 })
|
||||||
|
];
|
||||||
|
const error = { textContent: '' };
|
||||||
|
const submitButton = { disabled: false };
|
||||||
|
const form = {
|
||||||
|
checkValidity: () => true,
|
||||||
|
reportValidity() {},
|
||||||
|
elements: {
|
||||||
|
inboundCode: control('IN-001'),
|
||||||
|
sourceFba: control('ONT8'),
|
||||||
|
reason: control('客户退货'),
|
||||||
|
receivingWarehouse: control('洛杉矶仓'),
|
||||||
|
destinationFba: control('LAX9'),
|
||||||
|
relabel: control('', { checked: false }),
|
||||||
|
labelCase: control('Case1'),
|
||||||
|
cartonLabel: control('', { files: [] }),
|
||||||
|
skuLabel: control('', { files: [] }),
|
||||||
|
address: control('123 Main St'),
|
||||||
|
expectedArrival: control('2026-09-10T10:00'),
|
||||||
|
returnRemark: control(''),
|
||||||
|
tailRemark: control('')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const saved = [];
|
||||||
|
const runtime = {
|
||||||
|
document: {
|
||||||
|
getElementById: id => ({
|
||||||
|
'return-relabel-form': form,
|
||||||
|
'return-relabel-error': error,
|
||||||
|
'return-relabel-submit': submitButton
|
||||||
|
}[id] || null),
|
||||||
|
querySelectorAll: selector => selector === '#return-relabel-sku-body tr' ? rows : []
|
||||||
|
},
|
||||||
|
loadReturnRelabelOrders: () => [],
|
||||||
|
saveReturnRelabelOrders: orders => { saved.push(orders); return true; },
|
||||||
|
renderReturnRelabelList() {},
|
||||||
|
showToast() {},
|
||||||
|
openReturnRelabelList() {},
|
||||||
|
returnRelabelFilteredOrders: null,
|
||||||
|
returnRelabelSubmitting: false
|
||||||
|
};
|
||||||
|
vm.runInNewContext(
|
||||||
|
`${extractFunction('validateReturnRelabelForm')}
|
||||||
|
${extractFunction('summarizeReturnRelabelSkuField')}
|
||||||
|
${extractFunction('submitReturnRelabelOrder')}`,
|
||||||
|
runtime
|
||||||
|
);
|
||||||
|
|
||||||
|
// Optional logistics fields must not block a later SKU from submitting.
|
||||||
|
rows[1].querySelector('.return-relabel-tracking-no').value = '';
|
||||||
|
assert.equal(runtime.validateReturnRelabelForm(), true);
|
||||||
|
assert.equal(error.textContent, '');
|
||||||
|
rows[1].querySelector('.return-relabel-tracking-no').value = 'FX-002';
|
||||||
|
|
||||||
|
// Regression caught: only the first row's logistics data is persisted.
|
||||||
|
assert.equal(runtime.submitReturnRelabelOrder({ preventDefault() {} }), false);
|
||||||
|
const order = saved[0][0];
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(order.skus)), [
|
||||||
|
{ carrier: 'UPS', trackingNo: '1Z-001', sku: 'SKU-1', name: '商品一', fnsku: 'FN-1', newSku: '', quantity: 2 },
|
||||||
|
{ carrier: 'FedEx', trackingNo: 'FX-002', sku: 'SKU-2', name: '商品二', fnsku: 'FN-2', newSku: '', quantity: 3 }
|
||||||
|
]);
|
||||||
|
assert.equal(order.carrier, 'UPS、FedEx', '整单兼容字段应汇总去重后的承运商');
|
||||||
|
assert.equal(order.trackingNo, '1Z-001、FX-002', '整单兼容字段应汇总去重后的运单号');
|
||||||
|
|
||||||
|
console.log('return relabel SKU logistics: ok');
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function detailActions(id, actionClass) {
|
||||||
|
const detail = html.match(new RegExp(`<section class="sea-business-detail-page" id="${id}"[\\s\\S]*?<div class="${actionClass}">([\\s\\S]*?)</div>`))?.[1] || '';
|
||||||
|
assert.ok(detail, `${id} detail actions should exist`);
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, actionClass] of [
|
||||||
|
['sea-truck-detail', 'truck-detail-actions'],
|
||||||
|
['sea-pallet-detail', 'pallet-detail-actions']
|
||||||
|
]) {
|
||||||
|
const actions = detailActions(id, actionClass);
|
||||||
|
assert.doesNotMatch(actions, /data-detail-content="status"/);
|
||||||
|
assert.doesNotMatch(actions, /openSeaOrderEditor/);
|
||||||
|
assert.match(actions, /返回列表/);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('sea business detail actions requirements: ok');
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPage = html.match(/<div class="page-content" id="page-storage-transfer-create">([\s\S]*?)<!-- ========== PAGE: 仓储转运单详情/)?.[1] || '';
|
||||||
|
const editModal = html.match(/<div class="modal-overlay" id="storage-transfer-edit-modal"([\s\S]*?)<!-- ========== MODAL:/)?.[1] || '';
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
createPage,
|
||||||
|
/换标要求[\s\S]*关联文件模板[\s\S]*上传箱唛[\s\S]*上传 SKU 标签[\s\S]*上传关联文件[\s\S]*备注/,
|
||||||
|
'create order should place the relation template and associated fields inside relabel settings'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
editModal,
|
||||||
|
/换标要求[\s\S]*关联文件模板[\s\S]*上传箱唛[\s\S]*上传 SKU 标签[\s\S]*上传关联文件[\s\S]*备注/,
|
||||||
|
'edit order should expose the same associated fields as create order'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
createPage,
|
||||||
|
/class="storage-transfer-upload-card" id="storage-transfer-relation-upload-card"/,
|
||||||
|
'create relation upload should use the same background card as carton upload'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
editModal,
|
||||||
|
/class="storage-transfer-upload-card" id="storage-transfer-edit-relation-upload-card"/,
|
||||||
|
'edit relation upload should use the same background card as carton upload'
|
||||||
|
);
|
||||||
|
|
||||||
|
const getStorageTransferRelationTemplateRows = vm.runInNewContext(
|
||||||
|
`(${extractFunction('getStorageTransferRelationTemplateRows')})`
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(getStorageTransferRelationTemplateRows())),
|
||||||
|
[['箱唛', '需贴箱唛', '需贴SKU标', '备注']]
|
||||||
|
);
|
||||||
|
|
||||||
|
let writtenWorkbook = null;
|
||||||
|
let writtenFileName = '';
|
||||||
|
const downloadStorageTransferRelationTemplate = vm.runInNewContext(
|
||||||
|
`(${extractFunction('downloadStorageTransferRelationTemplate')})`,
|
||||||
|
{
|
||||||
|
getStorageTransferRelationTemplateRows,
|
||||||
|
showToast: () => {},
|
||||||
|
XLSX: {
|
||||||
|
utils: {
|
||||||
|
aoa_to_sheet: rows => ({ rows }),
|
||||||
|
book_new: () => ({ sheets: [] }),
|
||||||
|
book_append_sheet: (workbook, worksheet, name) => workbook.sheets.push({ worksheet, name })
|
||||||
|
},
|
||||||
|
writeFile: (workbook, fileName) => {
|
||||||
|
writtenWorkbook = workbook;
|
||||||
|
writtenFileName = fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
downloadStorageTransferRelationTemplate();
|
||||||
|
assert.equal(writtenFileName, '仓储转运关联模板.xlsx');
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(writtenWorkbook.sheets)),
|
||||||
|
[{ worksheet: { rows: [['箱唛', '需贴箱唛', '需贴SKU标', '备注']], '!cols': [{ wch: 24 }, { wch: 14 }, { wch: 14 }, { wch: 32 }] }, name: '关联明细' }]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileControl = name => ({ files: name ? [{ name }] : [] });
|
||||||
|
const form = {
|
||||||
|
elements: {
|
||||||
|
relabel: { checked: true },
|
||||||
|
relabelRequirement: { value: 'Case3' },
|
||||||
|
relabelCartonLabel: fileControl('carton.pdf'),
|
||||||
|
relabelSkuLabel: fileControl('sku.pdf'),
|
||||||
|
relabelRelationFile: fileControl('关联明细.xlsx'),
|
||||||
|
relabelRemark: { value: ' 优先处理 A 箱 ' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getStorageTransferRelabelConfig = vm.runInNewContext(`(${extractFunction('getStorageTransferRelabelConfig')})`);
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(getStorageTransferRelabelConfig(form))), {
|
||||||
|
relabel: 'yes',
|
||||||
|
relabelRequirement: 'Case3',
|
||||||
|
relabelCartonLabel: 'carton.pdf',
|
||||||
|
relabelSkuLabel: 'sku.pdf',
|
||||||
|
relabelRelationFile: '关联明细.xlsx',
|
||||||
|
relabelRemark: '优先处理 A 箱'
|
||||||
|
});
|
||||||
|
|
||||||
|
form.elements.relabel.checked = false;
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(getStorageTransferRelabelConfig(form))), {
|
||||||
|
relabel: 'no',
|
||||||
|
relabelRequirement: '',
|
||||||
|
relabelCartonLabel: '',
|
||||||
|
relabelSkuLabel: '',
|
||||||
|
relabelRelationFile: '',
|
||||||
|
relabelRemark: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('storage transfer associated fields: ok');
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPage = html.match(/<div class="page-content" id="page-storage-transfer-create">([\s\S]*?)<!-- ========== PAGE: 仓储转运单详情/)?.[1] || '';
|
||||||
|
const editModal = html.match(/<div class="modal-overlay" id="storage-transfer-edit-modal"([\s\S]*?)<!-- ========== MODAL:/)?.[1] || '';
|
||||||
|
assert.ok(editModal, 'storage transfer edit modal should exist');
|
||||||
|
|
||||||
|
const fieldNames = source => Array.from(source.matchAll(/\bname="([^"]+)"/g), match => match[1]).sort();
|
||||||
|
assert.deepEqual(fieldNames(editModal), fieldNames(createPage), 'edit modal should expose the same fields as create order');
|
||||||
|
|
||||||
|
const tbody = { innerHTML: '' };
|
||||||
|
const controls = {
|
||||||
|
'storage-transfer-list-body': tbody,
|
||||||
|
'storage-transfer-total': { textContent: '' },
|
||||||
|
'storage-transfer-page-number': { textContent: '' },
|
||||||
|
'storage-transfer-prev': { disabled: false },
|
||||||
|
'storage-transfer-next': { disabled: false }
|
||||||
|
};
|
||||||
|
const renderStorageTransferList = vm.runInNewContext(
|
||||||
|
`(${extractFunction('renderStorageTransferList')})`,
|
||||||
|
{
|
||||||
|
document: { getElementById: id => controls[id] || null },
|
||||||
|
storageTransferFilteredOrders: null,
|
||||||
|
loadStorageTransferOrders: () => [{
|
||||||
|
id: 'order-1', orderNo: 'ZY001', inboundNo: 'RK001', boxCount: 2,
|
||||||
|
warehouse: '美西洛杉矶仓', fbaCode: 'ONT8', shipmentId: 'FBA001',
|
||||||
|
deliveryMethod: '卡车派送', relabel: 'yes', tailOrderNo: 'LC001',
|
||||||
|
relabelOrderNo: 'HB001', status: '待处理', createdAt: '2026-09-04 09:00'
|
||||||
|
}],
|
||||||
|
STORAGE_TRANSFER_PAGE_SIZE: 10,
|
||||||
|
storageTransferCurrentPage: 1,
|
||||||
|
escapeStorageTransferHtml: value => String(value == null ? '' : value)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
renderStorageTransferList();
|
||||||
|
assert.match(tbody.innerHTML, />查看<\/button>[\s\S]*?>编辑<\/button>/, 'each row should show edit after view');
|
||||||
|
assert.match(tbody.innerHTML, /openStorageTransferEdit\(this\.dataset\.storageTransferId\)/);
|
||||||
|
|
||||||
|
const mergeStorageTransferOrder = vm.runInNewContext(`(${extractFunction('mergeStorageTransferOrder')})`);
|
||||||
|
const original = {
|
||||||
|
id: 'order-1', orderNo: 'ZY001', tailOrderNo: 'LC001', relabelOrderNo: '',
|
||||||
|
status: '处理中', createdAt: '2026-09-01 08:00', cargo: [{ sku: 'KEEP' }],
|
||||||
|
inboundNo: 'OLD', fbaCode: 'OLD1', relabel: 'no'
|
||||||
|
};
|
||||||
|
const updated = mergeStorageTransferOrder(original, {
|
||||||
|
inboundNo: 'RK009', warehouse: '美东新泽西仓', fbaCode: 'ABE8',
|
||||||
|
address: 'New address', boxCount: 9, deliveryMethod: '快递派送',
|
||||||
|
expectedArrival: '2026-09-10T10:30', shipmentId: 'FBA009', trackingId: '1Z009',
|
||||||
|
boxId: 'B001-B009', note: 'updated', relabel: 'yes',
|
||||||
|
relabelRequirement: 'Case2', relabelCartonLabel: '', relabelSkuLabel: 'sku-new.pdf',
|
||||||
|
relabelRelationFile: '关联明细.xlsx', relabelRemark: '优先处理 A 箱',
|
||||||
|
relabelOrderNo: 'HB009'
|
||||||
|
});
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(updated)), {
|
||||||
|
id: 'order-1', orderNo: 'ZY001', tailOrderNo: 'LC001', relabelOrderNo: 'HB009',
|
||||||
|
status: '处理中', createdAt: '2026-09-01 08:00', cargo: [{ sku: 'KEEP' }],
|
||||||
|
inboundNo: 'RK009', warehouse: '美东新泽西仓', fbaCode: 'ABE8',
|
||||||
|
address: 'New address', boxCount: 9, deliveryMethod: '快递派送',
|
||||||
|
expectedArrival: '2026-09-10T10:30', shipmentId: 'FBA009', trackingId: '1Z009',
|
||||||
|
boxId: 'B001-B009', note: 'updated', relabel: 'yes',
|
||||||
|
relabelRequirement: 'Case2', relabelCartonLabel: '', relabelSkuLabel: 'sku-new.pdf',
|
||||||
|
relabelRelationFile: '关联明细.xlsx', relabelRemark: '优先处理 A 箱'
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStorageTransferLegacyRelabelFile = vm.runInNewContext(`(${extractFunction('getStorageTransferLegacyRelabelFile')})`);
|
||||||
|
const getStorageTransferEditExistingFiles = vm.runInNewContext(
|
||||||
|
`(${extractFunction('getStorageTransferEditExistingFiles')})`,
|
||||||
|
{ getStorageTransferLegacyRelabelFile }
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(getStorageTransferEditExistingFiles({
|
||||||
|
relabel: 'yes', relabelRequirement: 'Case2', cargo: [{ fileName: 'legacy-sku.pdf' }]
|
||||||
|
}, 'Case2'))),
|
||||||
|
{ carton: '', sku: 'legacy-sku.pdf' },
|
||||||
|
'legacy Case2 files should refill the SKU upload instead of the carton upload'
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(getStorageTransferEditExistingFiles({
|
||||||
|
relabel: 'yes', relabelRequirement: 'Case1', relabelCartonLabel: 'carton.pdf', relabelSkuLabel: 'old-sku.pdf'
|
||||||
|
}, 'Case1'))),
|
||||||
|
{ carton: 'carton.pdf', sku: 'old-sku.pdf' },
|
||||||
|
'explicit stored files should win over legacy cargo data'
|
||||||
|
);
|
||||||
|
|
||||||
|
let filterCalls = 0;
|
||||||
|
let renderCalls = 0;
|
||||||
|
const refreshContext = {
|
||||||
|
document: { getElementById: id => ({ value: id === 'storage-transfer-filter-status' ? '处理中' : '' }) },
|
||||||
|
filterStorageTransferOrders: () => { filterCalls += 1; },
|
||||||
|
renderStorageTransferList: () => { renderCalls += 1; },
|
||||||
|
storageTransferFilteredOrders: [{ id: 'stale' }]
|
||||||
|
};
|
||||||
|
const refreshStorageTransferListAfterEdit = vm.runInNewContext(
|
||||||
|
`(${extractFunction('refreshStorageTransferListAfterEdit')})`,
|
||||||
|
refreshContext
|
||||||
|
);
|
||||||
|
refreshStorageTransferListAfterEdit();
|
||||||
|
assert.equal(filterCalls, 1, 'saving under an active filter should reapply that filter');
|
||||||
|
assert.equal(renderCalls, 0);
|
||||||
|
|
||||||
|
filterCalls = 0;
|
||||||
|
renderCalls = 0;
|
||||||
|
refreshContext.document.getElementById = () => ({ value: '' });
|
||||||
|
refreshStorageTransferListAfterEdit();
|
||||||
|
assert.equal(filterCalls, 0);
|
||||||
|
assert.equal(renderCalls, 1, 'saving without filters should render the full list');
|
||||||
|
assert.equal(refreshContext.storageTransferFilteredOrders, null);
|
||||||
|
|
||||||
|
console.log('storage transfer edit: ok');
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailPage = html.match(/<div class="page-content" id="page-storage-transfer-detail">([\s\S]*?)<!-- ========== PAGE: 货物废弃列表/)?.[1] || '';
|
||||||
|
assert.ok(detailPage, 'storage transfer detail page should exist');
|
||||||
|
assert.doesNotMatch(detailPage, /business-detail-tabs-shell|data-detail-tab|data-detail-panel/);
|
||||||
|
assert.match(detailPage, /class="page-header-actions storage-transfer-detail-header"/);
|
||||||
|
assert.match(detailPage, /id="storage-transfer-detail-order-no"/);
|
||||||
|
assert.match(detailPage, /class="labeling-detail-status" id="storage-transfer-detail-status"/);
|
||||||
|
assert.match(detailPage, /class="labeling-detail-grid"[\s\S]*?class="info-section-title">单据信息<\/div>[\s\S]*?id="storage-transfer-detail-document"/);
|
||||||
|
assert.match(detailPage, /class="info-section-title">货物信息<\/div>[\s\S]*?id="storage-transfer-detail-cargo"/);
|
||||||
|
assert.match(detailPage, /class="info-section-title">派送信息<\/div>[\s\S]*?id="storage-transfer-detail-delivery"/);
|
||||||
|
assert.match(detailPage, /class="info-section-title">换标信息<\/div>[\s\S]*?id="storage-transfer-detail-relabel"/);
|
||||||
|
|
||||||
|
const storageTransferRelabelRequirementText = vm.runInNewContext(`(${extractFunction('storageTransferRelabelRequirementText')})`);
|
||||||
|
assert.equal(storageTransferRelabelRequirementText('Case1'), '换箱唛');
|
||||||
|
assert.equal(storageTransferRelabelRequirementText('Case2'), '换SKU标');
|
||||||
|
assert.equal(storageTransferRelabelRequirementText('Case3'), '既换箱唛,又换SKU标');
|
||||||
|
|
||||||
|
const getStorageTransferRelabelDetail = vm.runInNewContext(
|
||||||
|
`(${extractFunction('getStorageTransferRelabelDetail')})`,
|
||||||
|
{ storageTransferRelabelRequirementText }
|
||||||
|
);
|
||||||
|
|
||||||
|
const getStorageTransferDetailSections = vm.runInNewContext(`(${extractFunction('getStorageTransferDetailSections')})`);
|
||||||
|
const sections = getStorageTransferDetailSections({
|
||||||
|
orderNo: 'ZY202608050002', inboundNo: 'RK20260805023', status: '处理中', createdAt: '2026-08-05 14:35',
|
||||||
|
boxCount: 8, shipmentId: 'FBA18R2M6X4D', trackingId: '1Z80526039', boxId: 'E001-E008',
|
||||||
|
warehouse: '美东新泽西仓', fbaCode: 'ABE8', deliveryMethod: '快递派送',
|
||||||
|
expectedArrival: '2026-08-13T14:00', address: '309 Cedar Ln, Florence, NJ 08518'
|
||||||
|
});
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(sections)), {
|
||||||
|
document: [
|
||||||
|
['转运单号', 'ZY202608050002', true], ['进仓编号', 'RK20260805023'],
|
||||||
|
['状态', '处理中', true], ['创建时间', '2026-08-05 14:35']
|
||||||
|
],
|
||||||
|
cargo: [
|
||||||
|
['发货箱数', 8, true], ['FBA 货件编号', 'FBA18R2M6X4D'],
|
||||||
|
['FBA 追踪编号', '1Z80526039'], ['FBA BOX 编号', 'E001-E008']
|
||||||
|
],
|
||||||
|
delivery: [
|
||||||
|
['操作仓', '美东新泽西仓'], ['FBA 目的仓代码', 'ABE8', true],
|
||||||
|
['派送方式', '快递派送'], ['期望到达时间', '2026-08-13 14:00'],
|
||||||
|
['详细派送地址', '309 Cedar Ln, Florence, NJ 08518']
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const enabledDetail = getStorageTransferRelabelDetail({
|
||||||
|
relabel: 'yes',
|
||||||
|
relabelRequirement: 'Case3',
|
||||||
|
relabelCartonLabel: '箱唛-A1001.pdf',
|
||||||
|
relabelSkuLabel: 'SKU-A1001.pdf',
|
||||||
|
relabelRelationFile: '关联明细.xlsx',
|
||||||
|
relabelRemark: '请按要求完成换标。',
|
||||||
|
cargo: []
|
||||||
|
});
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(enabledDetail)), {
|
||||||
|
enabled: true,
|
||||||
|
rows: [
|
||||||
|
['是否换标', '是', true],
|
||||||
|
['贴标要求', '既换箱唛,又换SKU标', true],
|
||||||
|
['上传箱唛', '箱唛-A1001.pdf', true, true],
|
||||||
|
['上传 SKU 标签', 'SKU-A1001.pdf', true, true],
|
||||||
|
['上传关联文件', '关联明细.xlsx', true, true],
|
||||||
|
['备注', '请按要求完成换标。']
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const legacyDetail = getStorageTransferRelabelDetail({
|
||||||
|
relabel: 'yes',
|
||||||
|
note: '',
|
||||||
|
cargo: [{ requirement: '覆盖原标签', fileName: 'FNSKU-2002.pdf' }]
|
||||||
|
});
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(legacyDetail)), {
|
||||||
|
enabled: true,
|
||||||
|
rows: [
|
||||||
|
['是否换标', '是', true],
|
||||||
|
['贴标要求', '覆盖原标签', true],
|
||||||
|
['上传文件', 'FNSKU-2002.pdf', true, true],
|
||||||
|
['备注', '-']
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(getStorageTransferRelabelDetail({ relabel: 'no' }))),
|
||||||
|
{ enabled: false, rows: [['是否换标', '否', true]] }
|
||||||
|
);
|
||||||
|
|
||||||
|
const list = { innerHTML: '', hidden: false };
|
||||||
|
const empty = { hidden: true };
|
||||||
|
const escapeStorageTransferHtml = value => String(value == null ? '' : value)
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
const labelingDetailField = vm.runInNewContext(
|
||||||
|
`(${extractFunction('labelingDetailField')})`,
|
||||||
|
{ escapeLabelingHtml: escapeStorageTransferHtml }
|
||||||
|
);
|
||||||
|
const renderStorageTransferRelabelDetail = vm.runInNewContext(
|
||||||
|
`(${extractFunction('renderStorageTransferRelabelDetail')})`,
|
||||||
|
{
|
||||||
|
document: { getElementById: id => id === 'storage-transfer-detail-relabel' ? list : id === 'storage-transfer-detail-relabel-empty' ? empty : null },
|
||||||
|
getStorageTransferRelabelDetail,
|
||||||
|
labelingDetailField
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
renderStorageTransferRelabelDetail({
|
||||||
|
relabel: 'yes', relabelRequirement: 'Case1', relabelCartonLabel: 'carton.pdf', note: '<核对>', cargo: []
|
||||||
|
});
|
||||||
|
assert.equal(list.hidden, false);
|
||||||
|
assert.equal(empty.hidden, true);
|
||||||
|
assert.match(list.innerHTML, /贴标要求[\s\S]*?换箱唛/);
|
||||||
|
assert.match(list.innerHTML, /labeling-detail-file[\s\S]*?carton\.pdf/);
|
||||||
|
assert.match(list.innerHTML, /<核对>/);
|
||||||
|
|
||||||
|
renderStorageTransferRelabelDetail({ relabel: 'no' });
|
||||||
|
assert.equal(list.hidden, false);
|
||||||
|
assert.equal(empty.hidden, false);
|
||||||
|
assert.match(list.innerHTML, /是否换标[\s\S]*?否/);
|
||||||
|
|
||||||
|
console.log('storage transfer relabel detail: ok');
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const start = html.indexOf(`function ${name}(`);
|
||||||
|
assert.notEqual(start, -1, `${name} should exist`);
|
||||||
|
const bodyStart = html.indexOf('{', start);
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = bodyStart; index < html.length; index += 1) {
|
||||||
|
if (html[index] === '{') depth += 1;
|
||||||
|
if (html[index] === '}') depth -= 1;
|
||||||
|
if (depth === 0) return html.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to extract ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPage = html.match(/<div class="page-content" id="page-storage-transfer-create">([\s\S]*?)<!-- ========== PAGE: 仓储转运单详情/)?.[1] || '';
|
||||||
|
assert.ok(createPage, 'storage transfer create page should exist');
|
||||||
|
assert.match(createPage, /<input[^>]+id="storage-transfer-relabel-toggle"[^>]+name="relabel"[^>]+type="checkbox"[^>]+checked/);
|
||||||
|
assert.match(createPage, /id="storage-transfer-relabel-status"[^>]*>已开启</);
|
||||||
|
assert.match(createPage, /name="relabelRequirement"[\s\S]*?换箱唛[\s\S]*?换SKU标[\s\S]*?既换箱唛,又换SKU标/);
|
||||||
|
|
||||||
|
function classList() {
|
||||||
|
const values = new Set();
|
||||||
|
return {
|
||||||
|
toggle(name, enabled) {
|
||||||
|
if (enabled) values.add(name);
|
||||||
|
else values.delete(name);
|
||||||
|
},
|
||||||
|
contains(name) { return values.has(name); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartonCard = { classList: classList() };
|
||||||
|
const skuCard = { classList: classList() };
|
||||||
|
const hint = { textContent: '' };
|
||||||
|
const controls = {
|
||||||
|
relabel: { checked: true },
|
||||||
|
relabelRequirement: { value: 'Case1', disabled: false },
|
||||||
|
relabelCartonLabel: { disabled: false, required: false, files: [], closest: () => cartonCard },
|
||||||
|
relabelSkuLabel: { disabled: false, required: false, files: [], closest: () => skuCard },
|
||||||
|
relabelRelationFile: { disabled: false, files: [] },
|
||||||
|
relabelRemark: { disabled: false, value: '' }
|
||||||
|
};
|
||||||
|
const form = { elements: controls };
|
||||||
|
const updateStorageTransferRelabelRequirements = vm.runInNewContext(
|
||||||
|
`(${extractFunction('updateStorageTransferRelabelRequirements')})`,
|
||||||
|
{ document: { getElementById: id => id === 'storage-transfer-form' ? form : id === 'storage-transfer-relabel-hint' ? hint : null } }
|
||||||
|
);
|
||||||
|
|
||||||
|
updateStorageTransferRelabelRequirements();
|
||||||
|
assert.equal(controls.relabelCartonLabel.disabled, false);
|
||||||
|
assert.equal(controls.relabelCartonLabel.required, true);
|
||||||
|
assert.equal(controls.relabelSkuLabel.disabled, true);
|
||||||
|
assert.equal(controls.relabelSkuLabel.required, false);
|
||||||
|
assert.equal(hint.textContent, '换箱唛:需要上传箱唛');
|
||||||
|
|
||||||
|
controls.relabelRequirement.value = 'Case3';
|
||||||
|
updateStorageTransferRelabelRequirements();
|
||||||
|
assert.equal(controls.relabelCartonLabel.disabled, false);
|
||||||
|
assert.equal(controls.relabelSkuLabel.disabled, false);
|
||||||
|
assert.equal(controls.relabelCartonLabel.required, true);
|
||||||
|
assert.equal(controls.relabelSkuLabel.required, true);
|
||||||
|
|
||||||
|
controls.relabel.checked = false;
|
||||||
|
updateStorageTransferRelabelRequirements();
|
||||||
|
assert.equal(controls.relabelCartonLabel.disabled, true);
|
||||||
|
assert.equal(controls.relabelSkuLabel.disabled, true);
|
||||||
|
assert.equal(controls.relabelCartonLabel.required, false);
|
||||||
|
assert.equal(controls.relabelSkuLabel.required, false);
|
||||||
|
assert.equal(hint.textContent, '未开启换标,无需上传标签文件');
|
||||||
|
|
||||||
|
const getStorageTransferRelabelConfig = vm.runInNewContext(`(${extractFunction('getStorageTransferRelabelConfig')})`);
|
||||||
|
controls.relabel.checked = true;
|
||||||
|
controls.relabelRequirement.value = 'Case2';
|
||||||
|
controls.relabelCartonLabel.files = [{ name: 'carton.pdf' }];
|
||||||
|
controls.relabelSkuLabel.files = [{ name: 'sku.pdf' }];
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(getStorageTransferRelabelConfig(form))),
|
||||||
|
{ relabel: 'yes', relabelRequirement: 'Case2', relabelCartonLabel: '', relabelSkuLabel: 'sku.pdf', relabelRelationFile: '', relabelRemark: '' }
|
||||||
|
);
|
||||||
|
controls.relabel.checked = false;
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(JSON.stringify(getStorageTransferRelabelConfig(form))),
|
||||||
|
{ relabel: 'no', relabelRequirement: '', relabelCartonLabel: '', relabelSkuLabel: '', relabelRelationFile: '', relabelRemark: '' }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('storage transfer relabel: ok');
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const html = fs.readFileSync('workbench.html', 'utf8');
|
||||||
|
const detail = html.match(/<section class="sea-business-detail-page" id="sea-truck-detail"[\s\S]*?<\/section>\s*<div class="sea-business-form-panel" data-order-panel="Truck"[^>]*>/)?.[0] || '';
|
||||||
|
|
||||||
|
assert.ok(detail, '美国本土卡详情视图 should exist');
|
||||||
|
assert.match(detail, /class="truck-detail-header"/);
|
||||||
|
assert.match(detail, /data-detail-content="orderNo"/);
|
||||||
|
assert.match(detail, /data-detail-content="status"/);
|
||||||
|
assert.match(detail, /class="truck-detail-grid"/);
|
||||||
|
assert.match(detail, /data-detail-card="transport"/);
|
||||||
|
assert.match(detail, /data-detail-card="contact"/);
|
||||||
|
assert.match(detail, /data-detail-card="cargo"/);
|
||||||
|
assert.match(detail, /data-detail-card="timing"/);
|
||||||
|
assert.doesNotMatch(detail, /business-detail-tabs-shell/);
|
||||||
|
|
||||||
|
console.log('truck detail layout requirements: ok');
|
||||||
+5441
-682
File diff suppressed because it is too large
Load Diff
+31
-25
@@ -8,6 +8,16 @@
|
|||||||
- 未列入本表的历史独立对话仍由用户直接管理,不构成跨对话调度、持续授权或文件写入权依据。
|
- 未列入本表的历史独立对话仍由用户直接管理,不构成跨对话调度、持续授权或文件写入权依据。
|
||||||
- 各专项只写入登记的唯一职责范围;跨端契约由总控冻结,发生生产文件重叠时由总控指定唯一写入者。
|
- 各专项只写入登记的唯一职责范围;跨端契约由总控冻结,发生生产文件重叠时由总控指定唯一写入者。
|
||||||
- 普通需求、缺陷、部署和验收不自动扩大专项授权;数据库、真实 HTTP 写请求、Jenkins 与部署仍按当前轮用户明确授权执行。
|
- 普通需求、缺陷、部署和验收不自动扩大专项授权;数据库、真实 HTTP 写请求、Jenkins 与部署仍按当前轮用户明确授权执行。
|
||||||
|
- 前端长期专项按部署应用划分,Admin、Client、国内 PDA、海外 PDA 各保留一个唯一写入者;不再按业务模块或单一业务节点登记长期前端专项。
|
||||||
|
- 单次跨模块需求由总控按当前验收节点拆成临时工作包或内部子代理,明确文件边界后并行;不得仅为提速新增用户可见长期前端任务。
|
||||||
|
|
||||||
|
## 测试环境前端派工规则(执行版)
|
||||||
|
|
||||||
|
1. `apps/admin-web/` 的页面、路由、导航、鉴权、请求基础能力、业务 API、权限消费、测试与构建统一进入 Admin 全流程。
|
||||||
|
2. `apps/client-web/` 的全部客户工作台能力统一进入 Client 全流程。
|
||||||
|
3. `apps/pda-uniapp/` 的全部国内现场作业进入国内 PDA;`apps/pda-overseas-uniapp/` 的全部海外现场作业及海外接柜仓校验进入海外 PDA。
|
||||||
|
4. 跨端问题按部署应用拆包;同一应用内涉及多个模块时仍由该应用专项作为唯一生产文件写入者。
|
||||||
|
5. 每个前端专项最多保留 2 个未完成工作包;超出时由总控合并到当前验收节点或改用内部子代理,不新增长期专项。
|
||||||
|
|
||||||
## 测试数据库与环境授权
|
## 测试数据库与环境授权
|
||||||
|
|
||||||
@@ -37,29 +47,25 @@
|
|||||||
| task_id | parent_task_id | project_id | 展示名称 | 唯一长期职责 |
|
| task_id | parent_task_id | project_id | 展示名称 | 唯一长期职责 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `01a046ea-cfc4-73a1-85f0-c69c714bff22` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【测试】海外仓全流程 | 在测试环境通过 Admin、Client 与海外 PDA 页面按业务节点持续走海外仓全流程,检查搜索、Tab、弹框、状态推进、错误态、视觉布局和请求量,必要时调用真实 HTTP 复现并记录业务编号、错误码、requestId/traceId 与截图;可唯一维护 `baiyue-firstleg/scripts/` 下直接服务于海外仓真实 HTTP 验收的脚本、合同测试和造数 Fixture,并精确提交这些测试文件,但不修改业务源码、不部署;发现生产缺陷只向测试环境全流程总控回报,数据库写入、真实 HTTP 写请求和 Jenkins 仍须用户当前轮明确授权。 |
|
| `01a046ea-cfc4-73a1-85f0-c69c714bff22` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【测试】海外仓全流程 | 在测试环境通过 Admin、Client 与海外 PDA 页面按业务节点持续走海外仓全流程,检查搜索、Tab、弹框、状态推进、错误态、视觉布局和请求量,必要时调用真实 HTTP 复现并记录业务编号、错误码、requestId/traceId 与截图;可唯一维护 `baiyue-firstleg/scripts/` 下直接服务于海外仓真实 HTTP 验收的脚本、合同测试和造数 Fixture,并精确提交这些测试文件,但不修改业务源码、不部署;发现生产缺陷只向测试环境全流程总控回报,数据库写入、真实 HTTP 写请求和 Jenkins 仍须用户当前轮明确授权。 |
|
||||||
| `01a07bb1-7a75-7933-b67f-75bcbafd7491` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】海外出库 | `baiyue-firstleg/lastleg-service/` 海外仓出库后端:出库单创建、列表、详情、取消,与排车转派送、PDA 拣货、出库及发车门禁相关的服务端实现、OpenAPI、模块 SQL 和测试;不修改前端、其他服务或统一 SQL,跨域契约由所属总控协调。 |
|
| `01a08051-01ab-72b0-9a8e-2c200f759eb8` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】Admin全流程 | `apps/admin-web/` 全部页面、路由、导航、鉴权与请求公共层、业务 API、权限消费、测试与构建;作为 Admin 生产文件唯一长期写入者,不修改 Client、PDA、后端或 SQL。 |
|
||||||
| `01a07a84-b3cb-7511-934a-72074fa29c22` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】海外接柜 | `baiyue-firstleg/lastleg-service/` 海外接柜仓默认分配、整柜原子修改、状态锁定、PDA 服务端校验、OpenAPI、模块 SQL 与测试;不修改其他服务或前端。 |
|
| `01a066b3-218b-7952-b8b7-311af4b44aed` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】Client全流程 | `apps/client-web/` 全部客户工作台页面、路由、API、权限消费、测试与构建,包括海外接柜仓只读展示;作为 Client 生产文件唯一长期写入者,不修改 Admin、PDA、后端或 SQL。 |
|
||||||
| `01a07a84-b5f8-7f12-801e-8f62465173e7` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】海外接柜Admin | `apps/admin-web/` 海外整柜接柜仓展示、柜级修改交互、API 适配、权限消费、测试与构建;不修改 Client、PDA、后端或 SQL。 |
|
|
||||||
| `01a07a84-b7e8-7e51-95df-e548a79a72b9` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】海外接柜Client | `apps/client-web/` 客户订单中海外接柜仓只读展示、API/types、权限与客户隔离、测试与构建;不修改 Admin、PDA、后端或 SQL。 |
|
|
||||||
| `01a07a84-b9f6-7d43-8c77-ec34d1a108f5` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】海外接柜PDA | `apps/pda-overseas-uniapp/` 海外接柜仓展示、已分配仓校验、错误态、恢复、测试与构建;不修改 Admin、Client、后端或 SQL。 |
|
|
||||||
| `01a06549-e9e0-7ea1-a83c-bb97010e2400` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】全局前端 | `apps/admin-web/` 除尾程订单与入仓计划、海外接柜仓之外的平台管理页面、路由、API、权限消费、测试与构建;不修改 Client、PDA、后端或 SQL。 |
|
|
||||||
| `01a0426d-2b01-7062-bc72-398c9060c7a3` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】Admin尾程入仓 | `apps/admin-web/src/modules/overseas-warehouse/` 的尾程订单、柜级摘要、入仓计划、FBA 预约及其 API、类型、测试与构建;海外接柜仓仍由登记专项负责,不修改其他前端、后端或 SQL。 |
|
|
||||||
| `01a066b3-218b-7952-b8b7-311af4b44aed` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】Client全流程 | `apps/client-web/` 全部客户工作台页面、路由、API、权限消费、测试与构建;海外接柜仓只读展示仍由登记专项负责,不修改 Admin、PDA、后端或 SQL。 |
|
|
||||||
| `01a041ba-52d5-71f1-b8f5-c5ff0f5166bb` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】国内PDA | `apps/pda-uniapp/` 全部国内现场作业、扫码、恢复、真实 API 适配、测试与构建;不修改海外 PDA、Admin、Client、后端或 SQL。 |
|
| `01a041ba-52d5-71f1-b8f5-c5ff0f5166bb` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】国内PDA | `apps/pda-uniapp/` 全部国内现场作业、扫码、恢复、真实 API 适配、测试与构建;不修改海外 PDA、Admin、Client、后端或 SQL。 |
|
||||||
| `01a03382-e812-7ea2-a992-cea04650efaf` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】海外PDA | `apps/pda-overseas-uniapp/` 除海外接柜仓展示与已分配仓校验之外的海外现场作业、扫码、恢复、真实 API 适配、测试与构建;不修改其他前端、后端或 SQL。 |
|
| `01a03382-e812-7ea2-a992-cea04650efaf` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【前端】海外PDA | `apps/pda-overseas-uniapp/` 全部海外现场作业、海外接柜仓展示与已分配仓校验、扫码、恢复、真实 API 适配、测试与构建;作为海外 PDA 生产文件唯一长期写入者,不修改其他前端、后端或 SQL。 |
|
||||||
| `01a046ea-d2c0-78e1-8c05-a3462fe67ead` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】认证权限审计 | `baiyue-firstleg/auth/`、`iam/`、`audit-service/` 及必要 `common-security/` 的账号、授权快照、RBAC、通知与审计接口、OpenAPI、模块 SQL 和测试;不修改业务服务、前端或统一 SQL。 |
|
| `01a046ea-d2c0-78e1-8c05-a3462fe67ead` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端1】认证权限审计 | `baiyue-firstleg/auth/`、`iam/`、`audit-service/` 及必要 `common-security/` 的账号、授权快照、RBAC、通知与审计接口、OpenAPI、模块 SQL 和测试;不修改业务服务、前端或统一 SQL。 |
|
||||||
| `01a046ea-d4ea-7ad0-8819-b6b3266b5252` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】Order订单域 | `baiyue-firstleg/order/` 的订单主数据、批量导入、统一订单聚合、状态动作、内部 DTO、OpenAPI、模块 SQL 和测试;不修改 Firstleg、IAM、前端或统一 SQL。 |
|
| `01a046ea-d4ea-7ad0-8819-b6b3266b5252` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端2】Order订单域 | `baiyue-firstleg/order/` 的订单主数据、批量导入、统一订单聚合、状态动作、内部 DTO、OpenAPI、模块 SQL 和测试;不修改 Firstleg、IAM、前端或统一 SQL。 |
|
||||||
| `01a046ea-d6c2-7bd0-9955-967a2ac34a67` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】国内仓储主数据 | `baiyue-firstleg/warehouse/` 的仓库主档、供应商、国内整柜、库位、托盘、资源预留、OpenAPI、模块 SQL 和测试;不修改海外资源服务、前端或统一 SQL。 |
|
| `01a046ea-d6c2-7bd0-9955-967a2ac34a67` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端3】国内仓储主数据 | `baiyue-firstleg/warehouse/` 的仓库主档、供应商、国内整柜、库位、托盘、资源预留、OpenAPI、模块 SQL 和测试;不修改海外资源服务、前端或统一 SQL。 |
|
||||||
| `01a04124-c8fe-7f52-99d9-c1d6e28b70cd` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】头程入库测量 | `baiyue-firstleg/firstleg/` 的入库、箱码箱唛、称重测量、库位占用、测量审核与扣货基础门禁、模块 OpenAPI/SQL 和测试;不修改计价、装柜、Order、前端或统一 SQL。 |
|
| `01a07a84-b3cb-7511-934a-72074fa29c22` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端4】海外接柜 | `baiyue-firstleg/lastleg-service/` 海外接柜仓默认分配、整柜原子修改、状态锁定、PDA 服务端校验、OpenAPI、模块 SQL 与测试;不修改其他服务或前端。 |
|
||||||
| `01a046ea-d88d-79e0-8e52-ec3b58592e6a` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】头程价格费用 | `baiyue-firstleg/firstleg/` 的尺寸策略、FBA 报价、计价、费用确认和申报渠道变更计价校验、模块 OpenAPI/SQL 与测试;不修改 Finance 主账、装柜、前端或统一 SQL。 |
|
| `01a03d00-fab2-76d3-a96c-b689bac67a40` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端5】海外PDA收货入库 | `baiyue-firstleg/lastleg-service/` 的海外 PDA 柜号认领、拆柜照片、扫码实收、托盘绑定、完成入库、库存入账编排及卸柜完成事件、OpenAPI、模块 SQL、权限和测试;入仓计划生成归尾程计划,接柜仓分配归海外接柜,库位托盘资源能力归海外资源专项,不修改前端或统一 SQL。 |
|
||||||
| `01a04148-6f68-7393-868d-c095d0e92026` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】装柜主链路 | `baiyue-firstleg/firstleg/` 的分柜、同柜不可拆组、智能排柜、预装计划、柜次、装柜历史与服务端门禁、模块 OpenAPI/SQL 和测试;不修改 PDA 适配、柜运报关、前端或统一 SQL。 |
|
| `01a04124-c8fe-7f52-99d9-c1d6e28b70cd` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端6】头程入库测量 | `baiyue-firstleg/firstleg/` 的入库、箱码箱唛、称重测量、库位占用、测量审核与扣货基础门禁、模块 OpenAPI/SQL 和测试;不修改计价、装柜、Order、前端或统一 SQL。 |
|
||||||
| `01a06a6d-66f5-77f1-a0ab-f0383a2d5b7e` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】装柜PDA | `baiyue-firstleg/firstleg/` 的国内 PDA 拣货、装柜证据、任务启动完成、幂等上传与资源结算服务端实现、模块 OpenAPI/SQL 和测试;不修改装柜编排、前端或统一 SQL。 |
|
| `01a046ea-d88d-79e0-8e52-ec3b58592e6a` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端7】头程价格费用 | `baiyue-firstleg/firstleg/` 的尺寸策略、FBA 报价、计价、费用确认和申报渠道变更计价校验、模块 OpenAPI/SQL 与测试;不修改 Finance 主账、装柜、前端或统一 SQL。 |
|
||||||
| `01a046ea-da69-72c0-af09-8d38fefadf4f` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】头程柜运报关 | `baiyue-firstleg/firstleg/` 的订舱、放舱、拖车、整柜报关、国内出仓及相关文件引用、模块 OpenAPI/SQL 和测试;不修改船节点集成、装柜编排、前端或统一 SQL。 |
|
| `01a04148-6f68-7393-868d-c095d0e92026` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端8】装柜主链路 | `baiyue-firstleg/firstleg/` 的分柜、同柜不可拆组、智能排柜、预装计划、柜次、装柜历史与服务端门禁、模块 OpenAPI/SQL 和测试;不修改 PDA 适配、柜运报关、前端或统一 SQL。 |
|
||||||
| `01a01e56-c30b-7513-be9e-03262a387696` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】船节点集成 | `baiyue-firstleg/firstleg/portschedule/`、`integration-service/` 的船司查询、标准节点、异常与查询历史及其接口、事件、模块 SQL 和测试;公共跨区网关与事件基础设施由对应专项负责。 |
|
| `01a06a6d-66f5-77f1-a0ab-f0383a2d5b7e` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端9】装柜PDA | `baiyue-firstleg/firstleg/` 的国内 PDA 拣货、装柜证据、任务启动完成、幂等上传与资源结算服务端实现、模块 OpenAPI/SQL 和测试;不修改装柜编排、前端或统一 SQL。 |
|
||||||
| `01a046ea-dc58-7393-bac9-8d2ef218bfb6` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】文件打印 | `baiyue-firstleg/file/`、`print/` 的文件类别、上传下载、业务归属、模板与打印接口、OpenAPI、模块 SQL 和测试;业务状态与文件可见性由所属业务专项负责,不修改前端或统一 SQL。 |
|
| `01a046ea-da69-72c0-af09-8d38fefadf4f` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端10】头程柜运报关 | `baiyue-firstleg/firstleg/` 的订舱、放舱、拖车、整柜报关、国内出仓及相关文件引用、模块 OpenAPI/SQL 和测试;不修改船节点集成、装柜编排、前端或统一 SQL。 |
|
||||||
| `01a0426d-2d39-7d20-bfe0-e1bf084e3343` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】尾程计划 | `baiyue-firstleg/lastleg-service/` 的尾程订单、柜级摘要、入仓计划转换、计划查询和 FBA 预约后端实现、OpenAPI、模块 SQL 与测试;海外接柜仓校验和出库后端仍由现有登记专项负责。 |
|
| `01a01e56-c30b-7513-be9e-03262a387696` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端11】船节点集成 | `baiyue-firstleg/firstleg/portschedule/`、`integration-service/` 的船司查询、标准节点、异常与查询历史及其接口、事件、模块 SQL 和测试;公共跨区网关与事件基础设施由对应专项负责。 |
|
||||||
| `01a03d00-fab2-76d3-a96c-b689bac67a40` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】海外PDA收货入库 | `baiyue-firstleg/lastleg-service/` 的海外 PDA 柜号认领、拆柜照片、扫码实收、托盘绑定、完成入库、库存入账编排及卸柜完成事件、OpenAPI、模块 SQL、权限和测试;入仓计划生成归尾程计划,接柜仓分配归海外接柜,库位托盘资源能力归海外资源专项,不修改前端或统一 SQL。 |
|
| `01a046ea-dc58-7393-bac9-8d2ef218bfb6` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端12】文件打印 | `baiyue-firstleg/file/`、`print/` 的文件类别、上传下载、业务归属、模板与打印接口、OpenAPI、模块 SQL 和测试;业务状态与文件可见性由所属业务专项负责,不修改前端或统一 SQL。 |
|
||||||
| `01a01e56-bc99-7170-a180-7b6e9f25fc9a` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】海外资源与本土业务 | `baiyue-firstleg/warehouse-overseas-service/` 及 `lastleg-service/` 的库存、库位、托盘、换标、转运、废弃、退货、采购、本土卡车、本土散板和海外主数据接口、OpenAPI、模块 SQL 与测试;不修改接柜、出库、派送专项或前端。 |
|
| `01a0426d-2d39-7d20-bfe0-e1bf084e3343` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端13】尾程计划 | `baiyue-firstleg/lastleg-service/` 的尾程订单、柜级摘要、入仓计划转换、计划查询和 FBA 预约后端实现、OpenAPI、模块 SQL 与测试;海外接柜仓校验和出库后端仍由现有登记专项负责。 |
|
||||||
| `01a05720-1940-7fd2-96b7-38a4a51f98b9` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】Finance财务 | `baiyue-firstleg/finance/` 的客户资金账户、费用池、应收应付、账单、收付款、核销、凭证代理、国内外费用来源、OpenAPI、模块 SQL 和测试;不修改上游业务状态机、前端或统一 SQL。 |
|
| `01a07bb1-7a75-7933-b67f-75bcbafd7491` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端14】海外出库 | `baiyue-firstleg/lastleg-service/` 海外仓出库后端:出库单创建、列表、详情、取消,与排车转派送、PDA 拣货、出库及发车门禁相关的服务端实现、OpenAPI、模块 SQL 和测试;不修改前端、其他服务或统一 SQL,跨域契约由所属总控协调。 |
|
||||||
| `01a07be1-f0ab-7ec1-9dba-7d0adef3ca9e` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】海外派送与客户服务 | `baiyue-firstleg/lastleg-service/` 的派送、一件代发、异常退回和客户回访接口、OpenAPI、模块 SQL、权限强制与测试;不修改接柜、入仓计划、海外资源、本土业务、出库排车、前端或统一 SQL。 |
|
| `01a01e56-bc99-7170-a180-7b6e9f25fc9a` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端15】海外资源与本土业务 | `baiyue-firstleg/warehouse-overseas-service/` 及 `lastleg-service/` 的库存、库位、托盘、换标、转运、废弃、退货、采购、本土卡车、本土散板和海外主数据接口、OpenAPI、模块 SQL 与测试;不修改接柜、出库、派送专项或前端。 |
|
||||||
| `01a07be1-f533-7fb1-9bee-6a6ff779a5d8` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端】跨区域网关与事件 | `baiyue-firstleg/gateway-overseas/`、`event-bridge-service/`、`common-event-contract/` 的跨区域认证投影、服务身份、Outbox/Inbox、公共事件契约、模块 SQL 和测试;不修改船司业务转换、业务服务私有状态机、前端或统一 SQL。 |
|
| `01a07be1-f0ab-7ec1-9dba-7d0adef3ca9e` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端16】海外派送与客户服务 | `baiyue-firstleg/lastleg-service/` 的派送、一件代发、异常退回和客户回访接口、OpenAPI、模块 SQL、权限强制与测试;不修改接柜、入仓计划、海外资源、本土业务、出库排车、前端或统一 SQL。 |
|
||||||
|
| `01a05720-1940-7fd2-96b7-38a4a51f98b9` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端17】Finance财务 | `baiyue-firstleg/finance/` 的客户资金账户、费用池、应收应付、账单、收付款、核销、凭证代理、国内外费用来源、OpenAPI、模块 SQL 和测试;不修改上游业务状态机、前端或统一 SQL。 |
|
||||||
|
| `01a07be1-f533-7fb1-9bee-6a6ff779a5d8` | `01a05b10-a5fc-7d51-8203-c671f3a823ad` | `c64729ff-6710-43c6-8ef2-d84e9666cc1e` | 【后端18】跨区域网关与事件 | `baiyue-firstleg/gateway-overseas/`、`event-bridge-service/`、`common-event-contract/` 的跨区域认证投影、服务身份、Outbox/Inbox、公共事件契约、模块 SQL 和测试;不修改船司业务转换、业务服务私有状态机、前端或统一 SQL。 |
|
||||||
|
|||||||
Reference in New Issue
Block a user