Files
shanxia2.0/docs/demos/pl-site-driver-brand-qty-demo.html
T

765 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>运输计划回传 · 按品牌填量 Demo</title>
<style>
:root {
--mint: #059669;
--mint-soft: #ecfdf5;
--mint-line: #a7f3d0;
--slate: #64748b;
--ink: #0f172a;
--warn: #d97706;
--danger: #dc2626;
--bg: #f1f5f9;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
background: linear-gradient(165deg, #ecfdf5 0%, #f8fafc 42%, #e2e8f0 100%);
color: var(--ink);
min-height: 100vh;
}
.page {
max-width: 1180px;
margin: 0 auto;
padding: 28px 20px 48px;
}
h1 {
margin: 0 0 6px;
font-size: 22px;
font-weight: 700;
}
.lead {
margin: 0 0 20px;
color: var(--slate);
font-size: 14px;
line-height: 1.6;
}
.layout-picker {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.layout-opt {
border: 2px solid #e2e8f0;
background: #fff;
border-radius: 12px;
padding: 14px 14px 12px;
cursor: pointer;
text-align: left;
transition: border-color .15s, box-shadow .15s;
}
.layout-opt:hover { border-color: var(--mint-line); }
.layout-opt.active {
border-color: var(--mint);
box-shadow: 0 0 0 3px rgb(16 185 129 / 18%);
}
.layout-opt .tag {
display: inline-block;
font-size: 11px;
font-weight: 700;
color: var(--mint);
background: var(--mint-soft);
padding: 2px 8px;
border-radius: 999px;
margin-bottom: 8px;
}
.layout-opt h3 {
margin: 0 0 4px;
font-size: 15px;
}
.layout-opt p {
margin: 0;
font-size: 12px;
color: var(--slate);
line-height: 1.5;
}
.phones {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 28px;
align-items: start;
}
.phone-wrap h2 {
margin: 0 0 10px;
font-size: 15px;
color: var(--slate);
font-weight: 600;
}
.phone {
width: 100%;
max-width: 390px;
margin: 0 auto;
background: #0f172a;
border-radius: 36px;
padding: 12px;
box-shadow: 0 24px 48px rgb(15 23 42 / 18%);
}
.phone-screen {
background: var(--bg);
border-radius: 28px;
overflow: hidden;
min-height: 640px;
display: flex;
flex-direction: column;
}
.nav {
background: var(--mint);
color: #fff;
text-align: center;
font-weight: 600;
font-size: 16px;
padding: 14px 16px 16px;
border-radius: 0 0 14px 14px;
}
.scroll {
flex: 1;
overflow: auto;
padding: 12px 0 80px;
}
.card {
margin: 0 12px 10px;
background: #fff;
border-radius: 12px;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.card-title {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
color: #047857;
background: linear-gradient(90deg, #ecfdf5 0%, transparent 72%);
}
.card-title::before {
content: "";
width: 3px;
height: 12px;
border-radius: 2px;
background: var(--mint);
}
.cell {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
font-size: 14px;
border-top: 1px solid #f1f5f9;
}
.cell:first-child { border-top: 0; }
.cell .k { color: var(--slate); }
.cell .v { font-weight: 600; text-align: right; }
.product {
margin: 10px 12px;
padding: 14px;
border-radius: 12px;
background: linear-gradient(180deg, #f0fdf4 0%, #fff 48%);
border: 1px solid #d1fae5;
}
.product-head {
display: flex;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.product-name { font-size: 15px; font-weight: 700; }
.badge {
flex-shrink: 0;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 999px;
background: #d1fae5;
color: #047857;
height: fit-content;
}
.badge.warn { background: #fef3c7; color: #b45309; }
.meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 12px;
}
.meta-item {
background: rgb(255 255 255 / 85%);
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 8px 10px;
}
.meta-item.span { grid-column: 1 / -1; }
.meta-label { font-size: 11px; color: var(--slate); margin-bottom: 2px; }
.meta-value { font-size: 13px; font-weight: 600; }
.meta-value.plan { color: var(--mint); font-size: 16px; }
.actual-box {
background: #fff;
border: 1px solid var(--mint-line);
border-radius: 10px;
padding: 10px 12px;
}
.actual-box__label {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
margin-bottom: 6px;
}
.actual-box__label strong { font-size: 13px; }
.actual-box__label span { font-size: 11px; color: var(--slate); }
.actual-row {
display: flex;
align-items: center;
gap: 8px;
}
.actual-row input {
flex: 1;
border: 0;
outline: none;
font-size: 22px;
font-weight: 700;
color: var(--ink);
width: 0;
background: transparent;
}
.unit { color: var(--slate); font-size: 13px; font-weight: 600; }
.brand-block {
margin-top: 10px;
border-top: 1px dashed #bbf7d0;
padding-top: 10px;
}
.brand-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.brand-head h4 {
margin: 0;
font-size: 13px;
color: #047857;
}
.brand-sum {
font-size: 11px;
color: var(--slate);
}
.brand-sum.bad { color: var(--danger); font-weight: 600; }
.brand-sum.ok { color: var(--mint); font-weight: 600; }
.brand-row {
display: grid;
grid-template-columns: 1fr 88px 28px;
gap: 6px;
align-items: center;
margin-bottom: 6px;
}
.brand-row select,
.brand-row input {
height: 36px;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 0 8px;
font-size: 13px;
background: #fff;
width: 100%;
}
.brand-row input { text-align: right; font-weight: 600; }
.icon-btn {
width: 28px;
height: 28px;
border: 0;
border-radius: 8px;
background: #fee2e2;
color: var(--danger);
font-size: 16px;
cursor: pointer;
line-height: 1;
}
.add-brand {
width: 100%;
margin-top: 4px;
height: 34px;
border: 1px dashed var(--mint);
border-radius: 8px;
background: var(--mint-soft);
color: var(--mint);
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.chip {
border: 1px solid #cbd5e1;
background: #fff;
border-radius: 999px;
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
}
.chip.on {
border-color: var(--mint);
background: var(--mint-soft);
color: #047857;
font-weight: 600;
}
.chip .q {
margin-left: 4px;
color: var(--slate);
font-weight: 500;
}
.footer {
position: sticky;
bottom: 0;
padding: 12px 16px 16px;
background: linear-gradient(180deg, transparent, #f1f5f9 28%);
}
.submit {
width: 100%;
height: 44px;
border: 0;
border-radius: 999px;
background: var(--mint);
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
}
.note {
margin-top: 20px;
padding: 14px 16px;
background: #fff;
border-radius: 12px;
border: 1px solid #e2e8f0;
font-size: 13px;
color: var(--slate);
line-height: 1.6;
}
.note strong { color: var(--ink); }
@media (max-width: 900px) {
.phones, .layout-picker { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="page">
<h1>运输计划回传 · 按品牌填量 Demo</h1>
<p class="lead">
左侧模拟<strong>现场链接</strong>(入/出库单回执),右侧模拟<strong>司机链接</strong>。
有品牌的产品(烧碱)可填 1~N 条「品牌 + 数量」;无品牌产品(盐酸)只填总实际数量。
请先选择下方交互方案,再在手机框内试用。
</p>
<div class="layout-picker" id="layoutPicker">
<button type="button" class="layout-opt active" data-layout="a" onclick="setLayout('a')">
<span class="tag">方案 A · 推荐</span>
<h3>下拉选品牌 + 数量行</h3>
<p>可增减行;合计自动回写实际数量。与归档弹窗逻辑一致,适合现场快速录入。</p>
</button>
<button type="button" class="layout-opt" data-layout="b" onclick="setLayout('b')">
<span class="tag">方案 B</span>
<h3>品牌芯片 + 点选填量</h3>
<p>先点选参与品牌,再为每个已选品牌填数量。品牌少时更直观。</p>
</button>
<button type="button" class="layout-opt" data-layout="c" onclick="setLayout('c')">
<span class="tag">方案 C</span>
<h3>仅现场填品牌;司机只填总量</h3>
<p>司机链接保持现状(一个总实际数量);品牌明细只在现场链接填写。</p>
</button>
</div>
<div class="phones">
<!-- 现场 -->
<div class="phone-wrap">
<h2>现场链接 · 入库单回执</h2>
<div class="phone">
<div class="phone-screen">
<div class="nav">入库单 · 现场回执</div>
<div class="scroll">
<div class="card">
<div class="cell"><span class="k">业务类型</span><span class="v">采购入库</span></div>
<div class="cell"><span class="k">往来单位</span><span class="v">君正化工</span></div>
<div class="cell"><span class="k">车牌</span><span class="v">皖A·12X34</span></div>
<div class="cell"><span class="k">计划号</span><span class="v">JH20260731001</span></div>
</div>
<div class="card">
<div class="card-title">产品实际数量</div>
</div>
<!-- 有品牌:烧碱 -->
<div class="product" id="siteBranded">
<div class="product-head">
<div class="product-name">烧碱</div>
<span class="badge warn">不限品牌</span>
</div>
<div class="meta">
<div class="meta-item">
<div class="meta-label">规格型号</div>
<div class="meta-value">32%</div>
</div>
<div class="meta-item">
<div class="meta-label">包装规格</div>
<div class="meta-value">散装</div>
</div>
<div class="meta-item span">
<div class="meta-label">计划数量</div>
<div class="meta-value plan">30 吨</div>
</div>
</div>
<div class="actual-box">
<div class="actual-box__label">
<strong>实际数量</strong>
<span class="layout-a-only layout-b-only">由品牌明细合计</span>
<span class="layout-c-only" style="display:none">请按现场实装填写</span>
</div>
<div class="actual-row">
<input id="siteActualCaustic" type="number" value="28" step="0.001" readonly />
<span class="unit">吨</span>
</div>
</div>
<div class="brand-block layout-a-only" id="siteBrandRowsA">
<div class="brand-head">
<h4>按品牌填量</h4>
<span class="brand-sum ok" id="siteSumA">合计 28 = 实际数量 ✓</span>
</div>
<div id="siteRowsA"></div>
<button type="button" class="add-brand" onclick="addSiteRowA()">+ 添加品牌</button>
</div>
<div class="brand-block layout-b-only" id="siteBrandRowsB" style="display:none">
<div class="brand-head">
<h4>选择品牌并填量</h4>
<span class="brand-sum ok" id="siteSumB">合计 28 = 实际数量 ✓</span>
</div>
<div class="chips" id="siteChips"></div>
<div id="siteRowsB"></div>
</div>
</div>
<!-- 无品牌:盐酸 -->
<div class="product">
<div class="product-head">
<div class="product-name">盐酸</div>
<span class="badge">产品 2/2</span>
</div>
<div class="meta">
<div class="meta-item">
<div class="meta-label">规格型号</div>
<div class="meta-value">31%</div>
</div>
<div class="meta-item">
<div class="meta-label">包装规格</div>
<div class="meta-value">吨袋</div>
</div>
<div class="meta-item span">
<div class="meta-label">计划数量</div>
<div class="meta-value plan">10 吨</div>
</div>
</div>
<div class="actual-box">
<div class="actual-box__label">
<strong>实际数量</strong>
<span>无品牌产品,直接填写总量</span>
</div>
<div class="actual-row">
<input type="number" value="9.8" step="0.001" />
<span class="unit">吨</span>
</div>
</div>
</div>
</div>
<div class="footer">
<button type="button" class="submit" onclick="alert('Demo:现场回执已提交(含品牌明细)')">提交</button>
</div>
</div>
</div>
</div>
<!-- 司机 -->
<div class="phone-wrap">
<h2>司机链接 · 运输回传</h2>
<div class="phone">
<div class="phone-screen">
<div class="nav">运输计划</div>
<div class="scroll">
<div class="card">
<div class="cell"><span class="k">计划编号</span><span class="v">JH20260731001</span></div>
<div class="cell"><span class="k">车牌</span><span class="v">皖A·12X34</span></div>
<div class="cell"><span class="k">司机</span><span class="v">张师傅 138****0001</span></div>
</div>
<div class="card">
<div class="card-title">产品明细</div>
<div class="cell"><span class="k">烧碱 32% · 散装</span><span class="v">计划 30 吨</span></div>
<div class="cell"><span class="k">盐酸 31% · 吨袋</span><span class="v">计划 10 吨</span></div>
</div>
<div class="card layout-c-only" style="display:none">
<div class="card-title">回传数量</div>
<div style="padding:12px 14px 14px">
<div class="actual-box">
<div class="actual-box__label">
<strong>实际数量</strong>
<span>方案 C:司机只填总量</span>
</div>
<div class="actual-row">
<input type="number" value="37.8" step="0.001" />
<span class="unit">吨</span>
</div>
</div>
</div>
</div>
<div class="layout-a-only layout-b-only" id="driverProducts">
<div class="card"><div class="card-title">回传数量(按产品)</div></div>
<div class="product">
<div class="product-head">
<div class="product-name">烧碱</div>
<span class="badge warn">不限品牌</span>
</div>
<div class="actual-box">
<div class="actual-box__label">
<strong>实际数量</strong>
<span>由品牌明细合计</span>
</div>
<div class="actual-row">
<input id="driverActualCaustic" type="number" value="28" step="0.001" readonly />
<span class="unit">吨</span>
</div>
</div>
<div class="brand-block">
<div class="brand-head">
<h4>按品牌填量</h4>
<span class="brand-sum ok" id="driverSum">合计 28 = 实际数量 ✓</span>
</div>
<div id="driverRows"></div>
<button type="button" class="add-brand" onclick="addDriverRow()">+ 添加品牌</button>
</div>
</div>
<div class="product">
<div class="product-head">
<div class="product-name">盐酸</div>
<span class="badge">无品牌</span>
</div>
<div class="actual-box">
<div class="actual-box__label">
<strong>实际数量</strong>
<span>无品牌,直接填总量</span>
</div>
<div class="actual-row">
<input type="number" value="9.8" step="0.001" />
<span class="unit">吨</span>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
<button type="button" class="submit" onclick="alert('Demo:司机回执已保存')">保存回执</button>
</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>规则预览:</strong>
① 产品有品牌概念(brandMode = any/required)时展示品牌明细;无品牌则隐藏。
② 可填 1 个或多个品牌行,数量 &gt; 0 才计入。
③ 各品牌数量合计 = 该产品实际数量;不限品牌与指定品牌(偏好)均可多选品牌,指定品牌默认预选且可取消。
④ 现场回传写入后,归档弹窗可预填品牌明细(与现有归档按品牌库存衔接)。
<br /><br />
点击上方方案卡片选择偏好(会写入选择事件)。本地文件也可直接打开:
<code>.superpowers/brainstorm/.../content/site-driver-brand-qty-demo.html</code>
</div>
</div>
<script>
const BRANDS = [
{ id: 'b1', name: '君正' },
{ id: 'b2', name: '红四方' },
{ id: 'b3', name: '中盐' }
]
let layout = 'a'
let siteRows = [
{ brandId: 'b1', qty: 16 },
{ brandId: 'b2', qty: 12 }
]
let driverRows = [
{ brandId: 'b1', qty: 16 },
{ brandId: 'b2', qty: 12 }
]
let selectedChips = new Set(['b1', 'b2'])
function setLayout(key) {
layout = key
document.querySelectorAll('.layout-opt').forEach(el => {
el.classList.toggle('active', el.dataset.layout === key)
})
document.querySelectorAll('.layout-a-only').forEach(el => {
el.style.display = key === 'a' || (el.classList.contains('layout-b-only') && key === 'b') ? '' : 'none'
})
document.querySelectorAll('.layout-b-only').forEach(el => {
if (el.classList.contains('layout-a-only')) {
el.style.display = (key === 'a' || key === 'b') ? '' : 'none'
} else {
el.style.display = key === 'b' ? '' : 'none'
}
})
document.querySelectorAll('.layout-c-only').forEach(el => {
el.style.display = key === 'c' ? '' : 'none'
})
// fix dual-class elements
document.querySelectorAll('.layout-a-only.layout-b-only').forEach(el => {
el.style.display = (key === 'a' || key === 'b') ? '' : 'none'
})
document.getElementById('siteBrandRowsA').style.display = key === 'a' ? '' : 'none'
document.getElementById('siteBrandRowsB').style.display = key === 'b' ? '' : 'none'
const actual = document.getElementById('siteActualCaustic')
actual.readOnly = key !== 'c'
if (key === 'c') actual.value = '28'
renderAll()
if (typeof toggleSelect === 'function') {
const opt = document.querySelector(`.layout-opt[data-layout="${key}"]`)
if (opt) toggleSelect(opt)
}
}
function sumRows(rows) {
return rows.reduce((s, r) => s + (Number(r.qty) || 0), 0)
}
function brandOptionsHtml(selectedId) {
return BRANDS.map(b =>
`<option value="${b.id}" ${b.id === selectedId ? 'selected' : ''}>${b.name}</option>`
).join('')
}
function updateSumLabel(el, total, actual) {
const ok = Math.abs(total - actual) < 0.0005
el.className = 'brand-sum ' + (ok ? 'ok' : 'bad')
el.textContent = ok
? `合计 ${total} = 实际数量 ✓`
: `合计 ${total} ≠ 实际数量 ${actual}`
}
function renderSiteA() {
const box = document.getElementById('siteRowsA')
box.innerHTML = siteRows.map((r, i) => `
<div class="brand-row">
<select onchange="siteRows[${i}].brandId=this.value; renderAll()">
${brandOptionsHtml(r.brandId)}
</select>
<input type="number" step="0.001" min="0" value="${r.qty}"
oninput="siteRows[${i}].qty=Number(this.value)||0; syncSiteActual()" />
<button type="button" class="icon-btn" onclick="siteRows.splice(${i},1); renderAll()" title="删除">×</button>
</div>
`).join('')
syncSiteActual()
}
function syncSiteActual() {
const total = Number(sumRows(siteRows).toFixed(3))
document.getElementById('siteActualCaustic').value = total
updateSumLabel(document.getElementById('siteSumA'), total, total)
updateSumLabel(document.getElementById('siteSumB'), total, total)
}
function addSiteRowA() {
const used = new Set(siteRows.map(r => r.brandId))
const next = BRANDS.find(b => !used.has(b.id)) || BRANDS[0]
siteRows.push({ brandId: next.id, qty: 0 })
renderAll()
}
function renderSiteB() {
const chips = document.getElementById('siteChips')
chips.innerHTML = BRANDS.map(b => {
const on = selectedChips.has(b.id)
const row = siteRows.find(r => r.brandId === b.id)
const q = row ? row.qty : 0
return `<button type="button" class="chip ${on ? 'on' : ''}" onclick="toggleChip('${b.id}')">
${b.name}${on ? `<span class="q">${q || ''}</span>` : ''}
</button>`
}).join('')
const box = document.getElementById('siteRowsB')
const active = BRANDS.filter(b => selectedChips.has(b.id))
box.innerHTML = active.map(b => {
let row = siteRows.find(r => r.brandId === b.id)
if (!row) {
row = { brandId: b.id, qty: 0 }
siteRows.push(row)
}
const i = siteRows.indexOf(row)
return `<div class="brand-row" style="grid-template-columns: 72px 1fr">
<div style="font-size:13px;font-weight:600;padding-left:4px">${b.name}</div>
<input type="number" step="0.001" min="0" value="${row.qty}"
oninput="siteRows[${i}].qty=Number(this.value)||0; syncSiteActual(); renderSiteB()" />
</div>`
}).join('')
siteRows = siteRows.filter(r => selectedChips.has(r.brandId))
syncSiteActual()
}
function toggleChip(id) {
if (selectedChips.has(id)) selectedChips.delete(id)
else selectedChips.add(id)
renderAll()
}
function renderDriver() {
const box = document.getElementById('driverRows')
box.innerHTML = driverRows.map((r, i) => `
<div class="brand-row">
<select onchange="driverRows[${i}].brandId=this.value; renderAll()">
${brandOptionsHtml(r.brandId)}
</select>
<input type="number" step="0.001" min="0" value="${r.qty}"
oninput="driverRows[${i}].qty=Number(this.value)||0; syncDriverActual()" />
<button type="button" class="icon-btn" onclick="driverRows.splice(${i},1); renderAll()">×</button>
</div>
`).join('')
syncDriverActual()
}
function syncDriverActual() {
const total = Number(sumRows(driverRows).toFixed(3))
document.getElementById('driverActualCaustic').value = total
updateSumLabel(document.getElementById('driverSum'), total, total)
}
function addDriverRow() {
const used = new Set(driverRows.map(r => r.brandId))
const next = BRANDS.find(b => !used.has(b.id)) || BRANDS[0]
driverRows.push({ brandId: next.id, qty: 0 })
renderAll()
}
function renderAll() {
renderSiteA()
renderSiteB()
renderDriver()
}
renderAll()
setLayout('a')
</script>
</body>
</html>