722 lines
25 KiB
HTML
722 lines
25 KiB
HTML
<!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>
|