Files
workspace/code/g3soft-libs/test.html
T
2026-10-14 22:24:12 +08:00

722 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.0">
<title>Table 行编辑 · 单元格状态 · 边框变体</title>
<style>
/* ============================================================
基础
============================================================ */
* { box-sizing: border-box; }
body {
margin: 0;
padding: 24px;
background: #f5f5f5;
color: rgba(0, 0, 0, 0.88);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
}
h1 { font-size: 20px; margin: 0 0 4px; }
h2 { font-size: 15px; margin: 0 0 12px; font-weight: 600; }
.hint { color: rgba(0, 0, 0, 0.45); font-size: 12px; margin: 0 0 20px; }
.card {
background: #fff;
border-radius: 8px;
padding: 16px 20px 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.card > .note {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
margin: -4px 0 12px;
}
/* ============================================================
表格骨架
表头与单元格的内边距、行高、文字省略都在这里统一。
============================================================ */
.tbl {
width: 100%;
border-collapse: separate; /* separate 才能让每格的边框独立控制 */
border-spacing: 0;
font-size: 14px;
background: #fff;
table-layout: fixed; /* 定宽布局,列宽不随内容抖动 */
}
.tbl th,
.tbl td {
padding: 8px 12px;
text-align: left;
vertical-align: middle;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tbl thead th {
background: #f5f5f5;
color: rgba(0, 0, 0, 0.65);
font-weight: 500;
white-space: nowrap;
}
.tbl td.is-num,
.tbl th.is-num { text-align: right; font-variant-numeric: tabular-nums; }
/* 行操作列宽一点,放得下「编辑/保存/取消」 */
.col-op { width: 132px; }
.col-idx { width: 48px; text-align: center; }
.col-name { width: 22%; }
.col-amount { width: 15%; }
.col-status { width: 15%; }
.col-remark { width: auto; }
/* ============================================================
★ 边框变体(本次重点)
用 CSS 变量驱动,一份标记切换四种边框:
--bw 线宽
--bc 线色
外框始终存在,差别只在「内部格线画哪些方向」
============================================================ */
.tbl {
--bw: 1px;
--bc: #d9d9d9;
--outer: var(--bc);
}
/* ---- 变体 1:全边框(横竖都有,含外框)---- */
.tbl.bordered-all {
border: var(--bw) solid var(--outer);
}
.tbl.bordered-all th,
.tbl.bordered-all td {
border-right: var(--bw) solid var(--bc);
border-bottom: var(--bw) solid var(--bc);
}
/* 最后一列不画右边线,否则与外框叠成 2px */
.tbl.bordered-all th:last-child,
.tbl.bordered-all td:last-child { border-right: 0; }
/* 最后一行不画下边线,同理 */
.tbl.bordered-all tbody tr:last-child td { border-bottom: 0; }
/* ---- 变体 2:只有横向线(+外框)----
行与行之间有横线,列与列之间没有竖线;最外面一圈仍在。
表头下方的那条线由 th 的 border-bottom 提供,天然包含在内。 */
.tbl.bordered-x {
border: var(--bw) solid var(--outer);
}
.tbl.bordered-x th,
.tbl.bordered-x td {
border-bottom: var(--bw) solid var(--bc);
border-right: 0;
}
.tbl.bordered-x tbody tr:last-child td { border-bottom: 0; }
/* ---- 变体 3:只有外框,内部无线 ---- */
.tbl.bordered-outer {
border: var(--bw) solid var(--outer);
}
.tbl.bordered-outer th,
.tbl.bordered-outer td {
border-right: 0;
border-bottom: 0;
}
/* 内无横线时,表头需要一条线把自己与表体分开,否则糊成一片 */
.tbl.bordered-outer thead th {
border-bottom: var(--bw) solid var(--bc);
}
/* ---- 变体 4:只有竖线(+外框),横向无线 ---- */
.tbl.bordered-y {
border: var(--bw) solid var(--outer);
}
.tbl.bordered-y th,
.tbl.bordered-y td {
border-right: var(--bw) solid var(--bc);
border-bottom: 0;
}
.tbl.bordered-y th:last-child,
.tbl.bordered-y td:last-child { border-right: 0; }
.tbl.bordered-y thead th {
border-bottom: var(--bw) solid var(--bc);
}
/* ============================================================
★ 单元格状态
与组件库同一套语义色:
error → color-danger #ec4246,8% 混入底色
warning → color-warning #e79a0d,5% 混入底色
disabled → 灰底 + 文字降一档亮度(is-readonly)
底色**不透明**:半透明会让行 hover 透上来,分不清「格有问题」还是「行被选中」。
============================================================ */
.tbl td.is-error {
background-color: color-mix(in srgb, #ec4246 8%, #fff);
}
.tbl td.is-warning {
background-color: color-mix(in srgb, #e79a0d 5%, #fff);
}
.tbl td.is-disabled {
background-color: color-mix(in srgb, rgba(0, 0, 0, 0.88) 3%, #fff);
color: rgba(0, 0, 0, 0.65);
cursor: not-allowed;
}
/* 校验格的编辑态让位:正在编辑时不铺底色,改用语义色焦点环 + 红字。
判据用 .is-editing 标记,而不是 :focus-within —— select 展开时焦点会跑进
浮层,:focus-within 会时有时无,环就会闪。 */
.tbl td.is-error.is-editing,
.tbl td.is-warning.is-editing {
background-color: #fff;
color: inherit;
}
.tbl td.is-error.is-editing {
box-shadow:
inset 0 0 0 2px #ec4246,
inset 0 0 0 3px #fff;
}
.tbl td.is-warning.is-editing {
box-shadow:
inset 0 0 0 2px #e79a0d,
inset 0 0 0 3px #fff;
}
/* 普通编辑格:主色焦点环。
与上面两条语义色环共存靠**权重**定胜负,不靠书写顺序:
.tbl td.is-error.is-editing → (0,4,0)
.tbl td.is-warning.is-editing → (0,4,0)
.tbl td.is-editing → (0,3,0)
前两条更高,所以校验格在编辑时仍然是红/橙环,不会被这条盖成主色。
这里刻意**不写** box-shadow: inherit 去「提权」:inherit 取的是父元素
(tr / table)的值,不是「上一条更具体的规则」,会把语义色环整个抹掉。 */
.tbl td.is-editing {
box-shadow:
inset 0 0 0 2px #18181b,
inset 0 0 0 3px #fff;
}
/* 实时输入框:融进单元格,看起来就是「格子在编辑」 */
.cell-input {
width: 100%;
border: 0;
padding: 0;
margin: 0;
background: transparent;
color: inherit;
font: inherit;
text-align: inherit;
outline: none;
}
.cell-input::placeholder { color: rgba(0, 0, 0, 0.25); }
/* 校验提示:贴在单元格下方,不占行高 */
.cell-msg {
display: block;
font-size: 12px;
line-height: 16px;
margin-top: 2px;
white-space: normal;
}
.is-error .cell-msg { color: #ec4246; }
.is-warning .cell-msg { color: #e79a0d; }
/* 带提示的行需要放开 nowrap,否则提示会被省略号截掉 */
.tbl td.has-msg { white-space: normal; }
/* ============================================================
只读 / 禁用视图
============================================================ */
.ro {
display: block;
color: rgba(0, 0, 0, 0.65);
}
/* ============================================================
按钮
============================================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 24px;
padding: 0 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #fff;
color: rgba(0, 0, 0, 0.88);
font: inherit;
font-size: 13px;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
}
.btn:hover:not(:disabled) { border-color: #18181b; }
.btn-primary {
background: #18181b;
border-color: #18181b;
color: #fafafa;
}
.btn-primary:hover:not(:disabled) { background: rgba(24, 24, 27, 0.9); }
.btn-link {
border-color: transparent;
background: transparent;
color: #18181b;
padding: 0 4px;
}
.btn-link:hover:not(:disabled) { color: rgba(24, 24, 27, 0.8); }
.btn-danger { color: #ec4246; }
.btn:disabled,
.btn.is-disabled {
color: rgba(0, 0, 0, 0.25);
border-color: #d9d9d9;
background: rgba(0, 0, 0, 0.04);
cursor: not-allowed;
}
.btn-primary:disabled,
.btn-primary.is-disabled {
background: rgba(0, 0, 0, 0.04);
border-color: #d9d9d9;
color: rgba(0, 0, 0, 0.25);
}
.op-group { display: flex; gap: 4px; }
/* 行 hover:只染可编辑行,禁用行不动 */
.tbl tbody tr:hover > td { background-color: rgba(0, 0, 0, 0.02); }
.tbl tbody tr:hover > td.is-error { background-color: color-mix(in srgb, #ec4246 11%, #fff); }
.tbl tbody tr:hover > td.is-warning { background-color: color-mix(in srgb, #e79a0d 8%, #fff); }
.tbl tbody tr:hover > td.is-disabled {
background-color: color-mix(in srgb, rgba(0, 0, 0, 0.88) 3%, #fff);
}
.tbl tbody tr:hover > td.is-editing { background-color: #fff; }
/* 序号列居中且用次级色 */
.tbl .col-idx,
.tbl td.col-idx { color: rgba(0, 0, 0, 0.45); text-align: center; }
</style>
</head>
<body>
<h1>Table 行编辑 · 单元格状态 · 边框变体</h1>
<p class="hint">
行内编辑:点「编辑」进入,可改单元格内容;金额为 error / warning 的格子会实时校验。
边框变体共四种,外框始终保留,差别只在内部格线画哪些方向。
</p>
<!-- ============================================================
1. 行编辑 + 三种单元格状态
============================================================ -->
<div class="card">
<h2>1. 行编辑 · error / warning / disabled 混合</h2>
<p class="note">
第 1 行可编辑;第 2 行整行禁用(灰底,操作列按钮禁用);金额格演示校验态。
</p>
<table class="tbl bordered-all" id="grid1">
<colgroup>
<col class="col-idx"><col class="col-name"><col class="col-amount">
<col class="col-status"><col class="col-remark"><col class="col-op">
</colgroup>
<thead>
<tr>
<th class="col-idx">#</th>
<th>名称</th>
<th class="is-num">金额</th>
<th>状态</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!-- 行 1:可编辑,金额 error(<=0),备注 warning(空) -->
<tr data-row="1">
<td class="col-idx">1</td>
<td data-field="name"><span class="ro">合同 A</span></td>
<td class="is-num is-error has-msg" data-field="amount" data-validate="positive">
<span class="ro">0</span>
<span class="cell-msg">金额必须大于 0</span>
</td>
<td data-field="status"><span class="ro">启用</span></td>
<td class="is-warning has-msg" data-field="remark" data-validate="notEmpty">
<span class="ro"></span>
<span class="cell-msg">建议填写备注</span>
</td>
<td>
<div class="op-group" data-op>
<button class="btn btn-link" data-act="edit">编辑</button>
<button class="btn btn-link btn-danger" data-act="del">删除</button>
</div>
</td>
</tr>
<!-- 行 2:整行禁用 -->
<tr data-row="2" data-locked="1">
<td class="col-idx">2</td>
<td class="is-disabled" data-field="name"><span class="ro">合同 B(已归档)</span></td>
<td class="is-num is-disabled" data-field="amount"><span class="ro">--</span></td>
<td class="is-disabled" data-field="status"><span class="ro">停用</span></td>
<td class="is-disabled" data-field="remark"><span class="ro">不可修改</span></td>
<td>
<div class="op-group" data-op>
<button class="btn btn-link" disabled>编辑</button>
<button class="btn btn-link" disabled>删除</button>
</div>
</td>
</tr>
<!-- 行 3:可编辑,全部合法 -->
<tr data-row="3">
<td class="col-idx">3</td>
<td data-field="name"><span class="ro">合同 C</span></td>
<td class="is-num" data-field="amount" data-validate="positive">
<span class="ro">43000</span>
</td>
<td data-field="status"><span class="ro">启用</span></td>
<td data-field="remark" data-validate="notEmpty"><span class="ro">待复核</span></td>
<td>
<div class="op-group" data-op>
<button class="btn btn-link" data-act="edit">编辑</button>
<button class="btn btn-link btn-danger" data-act="del">删除</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- ============================================================
2. 状态总览(静态,便于横向比对四种状态的观感)
============================================================ -->
<div class="card">
<h2>2. 单元格状态总览(静态)</h2>
<p class="note">
四种状态同屏对比:正常 / error / warning / disabled。
</p>
<table class="tbl bordered-all">
<colgroup>
<col style="width: 16%"><col style="width: 21%"><col style="width: 21%">
<col style="width: 21%"><col>
</colgroup>
<thead>
<tr>
<th>列</th><th>正常</th><th>error</th><th>warning</th><th>disabled</th>
</tr>
</thead>
<tbody>
<tr>
<td>文本</td>
<td>合同 A</td>
<td class="is-error has-msg">合同 A<span class="cell-msg">名称重复</span></td>
<td class="is-warning has-msg">合同 A<span class="cell-msg">建议改为全称</span></td>
<td class="is-disabled"><span class="ro">合同 A</span></td>
</tr>
<tr>
<td>数字(右对齐)</td>
<td class="is-num">12000</td>
<td class="is-num is-error has-msg">0<span class="cell-msg">金额必须大于 0</span></td>
<td class="is-num is-warning has-msg">999999<span class="cell-msg">超出建议范围</span></td>
<td class="is-num is-disabled"><span class="ro">12000</span></td>
</tr>
<tr>
<td>编辑中</td>
<td class="is-editing"><input class="cell-input" value="正在输入"></td>
<td class="is-error is-editing"><input class="cell-input" value="0"></td>
<td class="is-warning is-editing"><input class="cell-input" value="999999"></td>
<td class="is-disabled"><span class="ro">不可编辑</span></td>
</tr>
</tbody>
</table>
</div>
<!-- ============================================================
3. 边框变体
============================================================ -->
<div class="card">
<h2>3. 边框变体:全边框</h2>
<p class="note">横竖格线齐全,最外一圈与内部线宽一致。末行/末列不重复描边,避免叠成 2px。</p>
<table class="tbl bordered-all">
<thead>
<tr><th class="col-idx">#</th><th>名称</th><th class="is-num">金额</th><th>状态</th><th>备注</th></tr>
</thead>
<tbody>
<tr><td class="col-idx">1</td><td>合同 A</td><td class="is-num">12000</td><td>启用</td><td>已归档</td></tr>
<tr><td class="col-idx">2</td><td>合同 B</td><td class="is-num is-error has-msg">0<span class="cell-msg">金额必须大于 0</span></td><td>停用</td><td></td></tr>
<tr><td class="col-idx">3</td><td>合同 C</td><td class="is-num">43000</td><td>启用</td><td>待复核</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h2>4. 边框变体:只有横向线(+ 外框)</h2>
<p class="note">
行与行之间有横线,列与列之间没有竖线;最外一圈边框仍然完整保留。
表头下方那条线也在,用来把表头与表体分开。
</p>
<table class="tbl bordered-x">
<thead>
<tr><th class="col-idx">#</th><th>名称</th><th class="is-num">金额</th><th>状态</th><th>备注</th></tr>
</thead>
<tbody>
<tr><td class="col-idx">1</td><td>合同 A</td><td class="is-num">12000</td><td>启用</td><td>已归档</td></tr>
<tr><td class="col-idx">2</td><td>合同 B</td><td class="is-num is-warning has-msg">999999<span class="cell-msg">超出建议范围</span></td><td>停用</td><td></td></tr>
<tr><td class="col-idx">3</td><td>合同 C</td><td class="is-num">43000</td><td>启用</td><td>待复核</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h2>5. 边框变体:只有外框</h2>
<p class="note">内部完全无线,只有最外一圈;表头下方保留一条分隔线。</p>
<table class="tbl bordered-outer">
<thead>
<tr><th class="col-idx">#</th><th>名称</th><th class="is-num">金额</th><th>状态</th><th>备注</th></tr>
</thead>
<tbody>
<tr><td class="col-idx">1</td><td>合同 A</td><td class="is-num">12000</td><td>启用</td><td>已归档</td></tr>
<tr><td class="col-idx">2</td><td>合同 B</td><td class="is-num is-error has-msg">0<span class="cell-msg">金额必须大于 0</span></td><td>停用</td><td></td></tr>
<tr><td class="col-idx">3</td><td>合同 C</td><td class="is-num">43000</td><td>启用</td><td>待复核</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h2>6. 边框变体:只有竖向线(+ 外框)</h2>
<p class="note">列与列之间有竖线,行与行之间没有横线;外框保留。</p>
<table class="tbl bordered-y">
<thead>
<tr><th class="col-idx">#</th><th>名称</th><th class="is-num">金额</th><th>状态</th><th>备注</th></tr>
</thead>
<tbody>
<tr><td class="col-idx">1</td><td>合同 A</td><td class="is-num">12000</td><td>启用</td><td>已归档</td></tr>
<tr><td class="col-idx">2</td><td>合同 B</td><td class="is-num">0</td><td>停用</td><td></td></tr>
<tr><td class="col-idx">3</td><td>合同 C</td><td class="is-num">43000</td><td>启用</td><td>待复核</td></tr>
</tbody>
</table>
</div>
<script>
/* ============================================================
行编辑
约定:一行同时只有一格处于编辑态;编辑态那一格退掉校验底色,
改用语义色焦点环 + 红字(与组件库一致)。
============================================================ */
(function () {
var grid = document.getElementById('grid1');
var tbody = grid.tBodies[0];
/** 取某格当前的值(编辑态读 input,展示态读 .ro 的文字) */
function readValue(td) {
var input = td.querySelector('.cell-input');
if (input) return input.value;
var ro = td.querySelector('.ro');
return ro ? ro.textContent : td.textContent.trim();
}
/** 校验:返回 'error' | 'warning' | null */
function validate(td, value) {
var rule = td.dataset.validate;
if (!rule) return null;
if (rule === 'positive') {
if (String(value).trim() === '') return 'error';
return Number(value) > 0 ? null : 'error';
}
if (rule === 'notEmpty') {
return String(value).trim() === '' ? 'warning' : null;
}
return null;
}
/** 提示文案 */
function messageFor(rule, level) {
if (rule === 'positive') return level === 'error' ? '金额必须大于 0' : '';
if (rule === 'notEmpty') return level === 'warning' ? '建议填写备注' : '';
return '';
}
/** 把校验结果落到单元格上 */
function paint(td, level) {
td.classList.toggle('is-error', level === 'error');
td.classList.toggle('is-warning', level === 'warning');
var rule = td.dataset.validate;
var msg = td.querySelector('.cell-msg');
if (level) {
td.classList.add('has-msg');
if (!msg) {
msg = document.createElement('span');
msg.className = 'cell-msg';
td.appendChild(msg);
}
msg.textContent = messageFor(rule, level);
} else {
td.classList.remove('has-msg');
if (msg) msg.remove();
}
}
/** 进入编辑:展示 span 换成 input */
function enterEdit(td) {
if (td.classList.contains('is-editing')) return;
var value = readValue(td);
var isNum = td.classList.contains('is-num');
var input = document.createElement('input');
input.className = 'cell-input';
input.value = value;
if (isNum) input.inputMode = 'decimal';
var ro = td.querySelector('.ro');
if (ro) ro.style.display = 'none';
td.insertBefore(input, td.firstChild);
td.classList.add('is-editing');
// 实时校验
input.addEventListener('input', function () {
// 编辑态下先算结果,但底色让位(CSS 已处理),提示仍然实时更新
var level = validate(td, input.value);
// 编辑时不铺底色,但要把「离开编辑后该是什么状态」记在 dataset 上
td.dataset.pending = level || '';
var msg = td.querySelector('.cell-msg');
if (level) {
if (!msg) {
msg = document.createElement('span');
msg.className = 'cell-msg';
td.appendChild(msg);
}
td.classList.add('has-msg');
msg.textContent = messageFor(td.dataset.validate, level);
} else if (msg) {
msg.remove();
td.classList.remove('has-msg');
}
});
input.focus();
input.select();
return input;
}
/** 退出编辑:把 input 的值写回展示 span,并结算校验态 */
function exitEdit(td) {
if (!td.classList.contains('is-editing')) return;
var input = td.querySelector('.cell-input');
var value = input ? input.value : '';
if (input) input.remove();
var ro = td.querySelector('.ro');
if (ro) {
ro.style.display = '';
ro.textContent = value;
}
td.classList.remove('is-editing');
// 结算:写回后再按最终值判定一次
var level = validate(td, value);
paint(td, level);
}
/** 收集一行里所有可编辑格 */
function editableCells(tr) {
return Array.prototype.filter.call(tr.cells, function (td) {
return td.dataset.field && !td.classList.contains('is-disabled');
});
}
/** 进入某行的编辑态:把所有可编辑格变成 input,操作列换成保存/取消 */
function editRow(tr) {
editableCells(tr).forEach(enterEdit);
var op = tr.querySelector('[data-op]');
op.innerHTML =
'<button class="btn btn-primary" data-act="save">保存</button>' +
'<button class="btn" data-act="cancel">取消</button>';
tr.dataset.editing = '1';
}
/** 退出某行编辑态 */
function saveRow(tr, revert) {
var cells = editableCells(tr);
if (revert) {
// 取消:把 pending 清掉,按当前展示值重算(展示值此时就是原值)
cells.forEach(function (td) {
var input = td.querySelector('.cell-input');
if (input) {
var ro = td.querySelector('.ro');
var original = ro ? ro.dataset.original : '';
input.value = original != null ? original : input.value;
}
});
}
cells.forEach(exitEdit);
var op = tr.querySelector('[data-op]');
var locked = tr.dataset.locked === '1';
op.innerHTML =
'<button class="btn btn-link" data-act="edit"' + (locked ? ' disabled' : '') + '>编辑</button>' +
'<button class="btn btn-link btn-danger" data-act="del"' + (locked ? ' disabled' : '') + '>删除</button>';
delete tr.dataset.editing;
}
/* 记住每格的原始值,供「取消」回滚 */
Array.prototype.forEach.call(tbody.rows, function (tr) {
Array.prototype.forEach.call(tr.cells, function (td) {
var ro = td.querySelector('.ro');
if (ro) ro.dataset.original = ro.textContent;
});
});
/* 事件委托:编辑 / 保存 / 取消 / 删除 */
grid.addEventListener('click', function (e) {
var btn = e.target.closest('[data-act]');
if (!btn || btn.disabled) return;
var tr = btn.closest('tr');
var act = btn.dataset.act;
if (act === 'edit') {
editRow(tr);
} else if (act === 'save') {
saveRow(tr, false);
} else if (act === 'cancel') {
saveRow(tr, true);
} else if (act === 'del') {
if (tr.dataset.editing === '1') return; // 编辑中不给删,避免状态错乱
tr.remove();
renumber();
}
});
/* 回车保存 / Esc 取消 */
grid.addEventListener('keydown', function (e) {
var tr = e.target.closest('tr');
if (!tr || tr.dataset.editing !== '1') return;
if (e.key === 'Enter') {
e.preventDefault();
saveRow(tr, false);
} else if (e.key === 'Escape') {
e.preventDefault();
saveRow(tr, true);
}
});
/* 删行后重排序号 */
function renumber() {
Array.prototype.forEach.call(tbody.rows, function (tr, i) {
tr.cells[0].textContent = String(i + 1);
});
}
})();
</script>
</body>
</html>