Files
zkxw/docs/unused-material-demos/02-split-panel.html
T
2026-06-14 17:20:52 +08:00

389 lines
13 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.0" />
<title>02 左右分栏 · 登记未用完 Demo</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="_shared.css" />
<style>
.split-layout {
display: grid;
grid-template-columns: 340px 1fr;
gap: 16px;
min-height: 480px;
}
@media (max-width: 900px) {
.split-layout {
grid-template-columns: 1fr;
}
}
.list-panel {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
display: flex;
flex-direction: column;
overflow: hidden;
background: #fafafa;
}
.list-panel__tools {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
background: var(--surface);
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.list-panel__tools .input {
flex: 1;
min-width: 120px;
}
.line-list {
flex: 1;
overflow-y: auto;
max-height: 420px;
}
.line-item {
display: block;
width: 100%;
text-align: left;
padding: 10px 12px;
border: none;
border-bottom: 1px solid var(--border);
background: var(--surface);
cursor: pointer;
transition: background 0.12s;
}
.line-item:hover {
background: var(--row-hover);
}
.line-item.is-active {
background: var(--primary-bg);
border-left: 3px solid var(--primary);
padding-left: 9px;
}
.line-item__top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.line-item__name {
font-size: 13px;
font-weight: 600;
color: var(--text);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line-item__meta {
font-size: 11px;
color: var(--text-muted);
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.detail-panel {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 16px;
background: var(--surface);
overflow-y: auto;
max-height: 480px;
}
.detail-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.detail-panel__title {
font-size: 15px;
font-weight: 600;
}
.detail-panel__sub {
font-size: 12px;
color: var(--text-muted);
margin-top: 2px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px 16px;
}
.form-col-span-3 {
grid-column: 1 / -1;
}
.form-col-span-2 {
grid-column: span 2;
}
.field-group-title {
grid-column: 1 / -1;
font-size: 12px;
font-weight: 600;
color: var(--primary-dark);
padding: 8px 0 0;
margin-top: 4px;
border-top: 1px dashed var(--border);
}
.field-group-title:first-child {
border-top: none;
padding-top: 0;
margin-top: 0;
}
.detail-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 320px;
color: var(--text-muted);
font-size: 13px;
}
.nav-hint {
font-size: 11px;
color: var(--text-muted);
padding: 8px 12px;
border-top: 1px solid var(--border);
background: var(--surface);
}
kbd {
padding: 1px 5px;
border-radius: 4px;
border: 1px solid var(--border);
background: #f8fafc;
font-size: 10px;
}
</style>
</head>
<body>
<header class="demo-banner">
<div>
<h1><span class="demo-badge">02 左右分栏</span> 登记未用完物料</h1>
<p>左侧列表扫选 · 右侧完整表单 · 支持 ↑↓ 切换当前行</p>
</div>
<div class="demo-banner__actions">
<a href="index.html">← 选型索引</a>
<a class="is-current" href="02-split-panel.html">02 分栏</a>
<a href="01-inline-table.html">01 表格</a>
<a href="03-collapsible.html">03 折叠</a>
</div>
</header>
<div class="page-wrap">
<div class="dialog-backdrop">
<div class="dialog dialog--wide" role="dialog">
<div class="dialog__head">
<h2 class="dialog__title">登记未用完物料</h2>
<button type="button" class="dialog__close">×</button>
</div>
<div class="dialog__body">
<div class="section-head">
<div class="section-head__left">
<div class="section-title">未用完明细</div>
<div class="section-hint">左侧选择要编辑的行;右侧展示完整字段,避免一屏塞满多张卡片。</div>
</div>
<div class="section-tools">
<span class="stat-pill">共 <strong id="line-count">0</strong> 项</span>
<button type="button" class="btn btn-sm">+ 从批次添加</button>
<button type="button" class="btn btn-sm">+ 手工登记</button>
</div>
</div>
<div class="split-layout">
<div class="list-panel">
<div class="list-panel__tools">
<input type="search" class="input" placeholder="搜索物料/批号…" id="filter" />
</div>
<div class="line-list" id="line-list"></div>
<div class="nav-hint"><kbd>↑</kbd> <kbd>↓</kbd> 切换 · 点击左侧行编辑</div>
</div>
<div class="detail-panel" id="detail-panel"></div>
</div>
</div>
<div class="dialog__foot">
<button type="button" class="btn btn-ghost">取消</button>
<button type="button" class="btn btn-primary">保存</button>
</div>
</div>
</div>
<p class="style-note">
<strong>适用场景:</strong>每条都需要核对完整批次信息,或手工登记字段较多。
<ul>
<li>左侧仅展示摘要(名称、批号、数量、持有人),一屏可浏览 8~12 条</li>
<li>右侧 3 列标准表单(label-position top),控件高度 36px,与全站弹窗一致</li>
<li>批次关联字段只读灰底分区;登记字段(数量/持有人/备注)单独成组突出</li>
</ul>
</p>
</div>
<script src="_demo-data.js"></script>
<script>
let rows = [...DEMO_UNUSED_LINES]
let activeId = rows[0]?.id
const lineList = document.getElementById('line-list')
const detailPanel = document.getElementById('detail-panel')
function renderList() {
const kw = (document.getElementById('filter').value || '').trim().toLowerCase()
const filtered = rows.filter((r) => {
if (!kw) return true
return materialSummary(r).toLowerCase().includes(kw)
})
if (!filtered.find((r) => r.id === activeId)) activeId = filtered[0]?.id
lineList.innerHTML = filtered
.map(
(row) => `<button type="button" class="line-item ${row.id === activeId ? 'is-active' : ''}" data-id="${row.id}">
<div class="line-item__top">
<span class="row-index">${rows.indexOf(row) + 1}</span>
<span class="line-item__name">${row.materialName}</span>
<span class="tag ${row.fromBatch ? 'tag--success' : 'tag--warn'}">${row.fromBatch ? '批次' : '手工'}</span>
</div>
<div class="line-item__meta">
<span>${row.unusedQty}${row.unit || ''}</span>
<span>${row.batchNo || '—'}</span>
<span>${row.holder || '未填持有人'}</span>
</div>
</button>`
)
.join('')
document.getElementById('line-count').textContent = rows.length
renderDetail()
}
function renderDetail() {
const row = rows.find((r) => r.id === activeId)
if (!row) {
detailPanel.innerHTML = '<div class="detail-empty">请选择左侧一行,或点击「新增」</div>'
return
}
const readonly = row.fromBatch
detailPanel.innerHTML = `
<div class="detail-panel__head">
<div>
<div class="detail-panel__title">第 ${rows.indexOf(row) + 1} 行 · ${row.materialName}</div>
<div class="detail-panel__sub">${materialSummary(row)}</div>
</div>
<button type="button" class="btn btn-danger-link btn-sm" id="del-current">删除此行</button>
</div>
<div class="form-grid">
<div class="field-group-title">批次检索(可选)</div>
<div class="form-col-span-3">
<label class="field-label">检索批次</label>
<select class="select" ${readonly ? 'disabled' : ''}>
<option>${readonly ? `${row.stockInNo} · ${row.materialName}` : '输入入库编号/物料名检索…'}</option>
</select>
</div>
<div class="field-group-title">批次信息 ${readonly ? '(只读)' : '(可手工填写)'}</div>
${field('入库编号', row.stockInNo, readonly)}
${field('入库日期', row.stockInDate, readonly)}
${field('过期日期', row.expireDate, readonly)}
${field('物料编码', row.materialCode, readonly)}
${field('物料名称', row.materialName, readonly && row.fromBatch)}
${field('规格型号', row.specModel, readonly)}
${field('单位', row.unit, readonly)}
${field('货号', row.catalogNo, readonly)}
${field('批号', row.batchNo, readonly)}
${field('厂家', row.manufacturer, readonly)}
${field('单价(元)', formatMoney(row.unitPrice), readonly)}
<div class="field-group-title">登记信息(必填/重点)</div>
<div>
<label class="field-label">未用完数量</label>
<input class="input input-num" type="number" id="f-qty" value="${row.unusedQty}" min="0.01" step="0.01" />
</div>
<div>
<label class="field-label">剩余金额(元)</label>
<input class="input input--readonly" readonly value="${formatMoney(calcAmount(row.unitPrice, row.unusedQty))}" />
</div>
<div>
<label class="field-label">持有人</label>
<input class="input" type="text" id="f-holder" value="${row.holder || ''}" placeholder="可选" />
</div>
<div class="form-col-span-3">
<label class="field-label">备注</label>
<input class="input" type="text" id="f-remark" value="${row.remark || ''}" placeholder="可选" />
</div>
</div>`
document.getElementById('f-qty').addEventListener('input', (e) => {
row.unusedQty = Number(e.target.value) || 0
renderList()
})
document.getElementById('f-holder').addEventListener('input', (e) => {
row.holder = e.target.value
renderList()
})
document.getElementById('f-remark').addEventListener('input', (e) => {
row.remark = e.target.value
})
document.getElementById('del-current').addEventListener('click', () => {
rows = rows.filter((r) => r.id !== activeId)
activeId = rows[0]?.id
renderList()
})
}
function field(label, value, ro) {
return `<div>
<label class="field-label">${label}</label>
<input class="input ${ro ? 'input--readonly' : ''}" ${ro ? 'readonly' : ''} value="${value || ''}" placeholder="${ro ? '—' : '可选'}" />
</div>`
}
lineList.addEventListener('click', (e) => {
const btn = e.target.closest('.line-item')
if (!btn) return
activeId = Number(btn.dataset.id)
renderList()
})
document.getElementById('filter').addEventListener('input', renderList)
document.addEventListener('keydown', (e) => {
if (!['ArrowUp', 'ArrowDown'].includes(e.key)) return
const idx = rows.findIndex((r) => r.id === activeId)
if (idx < 0) return
e.preventDefault()
const next = e.key === 'ArrowUp' ? Math.max(0, idx - 1) : Math.min(rows.length - 1, idx + 1)
activeId = rows[next].id
renderList()
lineList.querySelector('.is-active')?.scrollIntoView({ block: 'nearest' })
})
renderList()
</script>
</body>
</html>