v24 第 10 批:名册真性能(根因是每卡内联表单)等九条
- #6 名册线上真实数据(1184 人)卡顿:根因是**每张卡片内联一整份分组弹层**—— 全页 2369 个 form / 27234 个 input / 22496 个 radio / 98100 DOM 节点, 密码管理器(Bitwarden 等)逐个扫描 form 把主线程占死(用户报的就是这个冲突)。 改为卡片只带 data 属性 + 全页**唯一一份**共享浮层 #rosterMenus。 实测:form 2369→3、input 27234→25、DOM 98100→17659、解压 7694→2306 KB、 长任务 1/52ms → 0。新增哨兵 _fb_formbloat_check.py(含反向验证)。 - #5 日视图翻页失效:day_columns() 无条件吸到周一,天视图只有 1 列 → 永远显示那周周一。 改为仅 span>1 时吸附(周视图行为不变)。 - #2 财务「欠款」类别(负债);并修一个真 bug:负债填负数会让净资产**反向变大**。 写入口 normalizeAmount 归一化 + summary/timeline 侧 abs 防御 + Schema v17 纠正老数据。 - #3 账目行「盘点」「修改」两个并列按钮(各自就地弹面板);图移到账目上方, 总金额图跨整行 + 每个账目一条线 + 类别筛选条;charts() 改为覆盖全部账目。 - #4 日程新增「农历」「节假日」两本不可删除只读日历(app/Holiday.php): 两级自动更新 —— 内置规则(公历固定 + 清明节气 + 农历节日)保证断网也有; 官方 JSON 镜像自动抓取补齐调休。抓取**不在渲染路径联网**(响应后 fastcgi_finish_request + 频率闸门),缓存落 storage/cache。仅派生本不可写日程。 - #6 日程页设置按钮前加「跳到某一天」(选中即跳)。 - #7 起止范围改用一个 Semi dateTimeRange 控件(观影/旅程/节点/日程共 4 处); 新增 window.fbSetDT / fbUpgradeRange;修范围重挂(React 非受控组件须 unmount 再 createRoot) 与范围里 required 未摘除导致表单无法提交两个真实问题。 验证:e2e 786/0、batch10 44/0、batch9 38/0、batch_e 91/0、upgrade 126/0、picker 28/0、 semi_integrate 27/0、formbloat 5/0(+反向 3 红)、mobile 688/0、interact 97/0、drag 5/0、geom 5/5; 线上自检 19/19 + 线上真实账号巡检 28/0(含 Firefox 实测)。
This commit is contained in:
+199
-83
@@ -152,40 +152,66 @@ $ledMe = (int)($me ?? 0);
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ==================== ② 变化图(用户第 8 条)==================== */ ?>
|
||||
<?php if ($charts || count($timeline) > 1): ?>
|
||||
<?php /* ==================== ② 变化图(v24 第 10 批 #3 重做)====================
|
||||
用户原话:「在账目上面,增加一个图表显示,**显示所有账目和总金额的变化曲线**,
|
||||
根据盘点的金额进行显示。横轴就是时间,纵轴是金额,用折线图。」
|
||||
⚠️ 与旧版的差别:① 总金额(净资产)**永远在最前**;② 每条账目都出线(不再只画盘点过的);
|
||||
③ 加一条"按类别看"的切换条 —— 因为"所有账目"在几百条时会变成一坨,
|
||||
把筛选交给用户,而不是我们偷偷少画几条。 */ ?>
|
||||
<?php if ($entries): ?>
|
||||
<div class="card card-pad led-charts">
|
||||
<h2 class="cinema-h2">
|
||||
金额变化
|
||||
<span class="hint">每次「盘点」记一个点,旧数字不会被清掉</span>
|
||||
<span class="hint">横轴时间 · 纵轴金额;每次「盘点」记一个点,旧数字不会被清掉</span>
|
||||
</h2>
|
||||
<div class="ck-grid">
|
||||
<?php if (count($timeline) > 1): ?>
|
||||
<div class="ck-card">
|
||||
<div class="ck-head">
|
||||
<b>整本账 · 净资产</b>
|
||||
<span class="small muted"><?= count($timeline) ?> 个时间点</span>
|
||||
</div>
|
||||
<?= chart_line($timeline, '净资产', 'asset', 320) ?>
|
||||
|
||||
<?php if ($timeline): ?>
|
||||
<div class="ck-card ck-total">
|
||||
<div class="ck-head">
|
||||
<b>总金额 · 净资产</b>
|
||||
<span class="small muted"><?= count($timeline) ?> 个时间点</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($charts as $ch): ?>
|
||||
<?php $en = $ch['entry']; $ek = (string)$en['kind']; ?>
|
||||
<div class="ck-card">
|
||||
<div class="ck-head">
|
||||
<b><?= e((string)$en['title']) ?></b>
|
||||
<span class="small muted">
|
||||
<?= \App\Ledger::kindIcon($ek) ?> <?= e(\App\Ledger::kindWord($ek)) ?>
|
||||
· <?= count($ch['series']) ?> 个点
|
||||
</span>
|
||||
<?= chart_line($timeline, '净资产', 'asset', 900) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($charts): ?>
|
||||
<?php
|
||||
// 类别筛选条:只列出**真有账目**的类别,点一下只看这一类。
|
||||
$kindsPresent = [];
|
||||
foreach ($charts as $ch) {
|
||||
$k = (string)$ch['entry']['kind'];
|
||||
$kindsPresent[$k] = ($kindsPresent[$k] ?? 0) + 1;
|
||||
}
|
||||
?>
|
||||
<div class="ck-filter" data-ck-filter>
|
||||
<button type="button" class="ck-chip is-on" data-ck-kind="">全部(<?= count($charts) ?>)</button>
|
||||
<?php foreach ($kindsPresent as $k => $n): ?>
|
||||
<button type="button" class="ck-chip<?= \App\Ledger::isDebt((string)$k) ? ' is-debt' : '' ?>"
|
||||
data-ck-kind="<?= e((string)$k) ?>">
|
||||
<?= \App\Ledger::kindIcon((string)$k) ?> <?= e(\App\Ledger::kindWord((string)$k)) ?>(<?= (int)$n ?>)
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="ck-grid">
|
||||
<?php foreach ($charts as $ch): ?>
|
||||
<?php $en = $ch['entry']; $ek = (string)$en['kind']; ?>
|
||||
<div class="ck-card" data-ck-card="<?= e($ek) ?>">
|
||||
<div class="ck-head">
|
||||
<b><?= e((string)$en['title']) ?></b>
|
||||
<span class="small muted">
|
||||
<?= \App\Ledger::kindIcon($ek) ?> <?= e(\App\Ledger::kindWord($ek)) ?>
|
||||
· <?= count($ch['series']) ?> 个点
|
||||
</span>
|
||||
</div>
|
||||
<?= chart_line($ch['series'], (string)$en['title'], \App\Ledger::isDebt($ek) ? 'debt' : 'asset', 320) ?>
|
||||
</div>
|
||||
<?= chart_line($ch['series'], (string)$en['title'], \App\Ledger::isDebt($ek) ? 'debt' : 'asset', 320) ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if (!$charts): ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<p class="small muted" style="margin:8px 2px 0">
|
||||
还没有任何盘点记录 —— 下面账目表里每行都有「盘点」,盘过之后这里就会出现它自己的曲线。
|
||||
没有盘点记录的账目只会显示一个点(它一直没变过)——
|
||||
在下面账目表里点「盘点」记下当前金额,这条线就长出来了。
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -225,32 +251,41 @@ $ledMe = (int)($me ?? 0);
|
||||
<?php else: ?><span class="small muted">—</span><?php endif; ?>
|
||||
</td>
|
||||
<td class="small muted"><?= e((string)($en['note'] ?? '')) ?></td>
|
||||
<td class="nowrap">
|
||||
<?php if ($canWrite): ?>
|
||||
<details class="led-edit">
|
||||
<summary class="link-quiet">操作</summary>
|
||||
<td class="nowrap led-acts">
|
||||
<?php /* 用户 2026-10-04 第 3 条原话:「账目列表,右侧两个按钮,
|
||||
一个**盘点**,点击后,输入最新的金额;一个按钮**修改**,
|
||||
用来修改名称、挂在谁名下、备注等。」
|
||||
→ 所以这两个是**并列的按钮**,各自弹自己的面板;
|
||||
不再是一个「操作」里塞两坨表单(那要展开两层才够得着)。 */ ?>
|
||||
<?php if ($canWrite): $histN = count(\App\Ledger::history($eid)); ?>
|
||||
<button type="button" class="btn btn-ghost btn-sm led-act"
|
||||
data-led-open="recon-<?= $eid ?>">盘点</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm led-act"
|
||||
data-led-open="edit-<?= $eid ?>">修改</button>
|
||||
|
||||
<?php /* ---- 盘点(用户第 8 条):重新输一个金额,旧值进历史 ---- */ ?>
|
||||
<form method="post" action="<?= e(url('ledger/reconcile')) ?>" class="node-form led-recon">
|
||||
<div class="led-panel" id="recon-<?= $eid ?>" hidden>
|
||||
<form method="post" action="<?= e(url('ledger/reconcile')) ?>" class="led-recon">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<input type="hidden" name="id" value="<?= $eid ?>">
|
||||
<h4 class="led-form-h">盘点这笔(旧金额会记进历史)</h4>
|
||||
<div class="nf-grid">
|
||||
<div class="field">
|
||||
<label>盘点后的金额(元)</label>
|
||||
<input class="input" name="amount" inputmode="decimal" required
|
||||
value="<?= e(\App\Ledger::amountYuan((int)$en['amount_cents'])) ?>">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>盘点备注</label>
|
||||
<input class="input" name="note" placeholder="如:2026 年初盘点">
|
||||
</div>
|
||||
<h4 class="led-form-h">盘点「<?= e((string)$en['title']) ?>」</h4>
|
||||
<div class="field">
|
||||
<label>最新金额(元)</label>
|
||||
<input class="input" name="amount" inputmode="decimal" required
|
||||
value="<?= e(\App\Ledger::amountYuan((int)$en['amount_cents'])) ?>">
|
||||
<span class="tip">盘点前的金额会记进历史,图表上就多一个点。</span>
|
||||
</div>
|
||||
<div class="btn-row"><button class="btn btn-sm" type="submit">盘点</button></div>
|
||||
<?php if (\App\Ledger::history($eid)): ?>
|
||||
<div class="field">
|
||||
<label>盘点备注</label>
|
||||
<input class="input" name="note" placeholder="如:2026 年初盘点">
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-sm" type="submit">记下这次盘点</button>
|
||||
<button class="btn btn-ghost btn-sm" type="button" data-led-close>取消</button>
|
||||
</div>
|
||||
<?php if ($histN): ?>
|
||||
<details class="led-hist">
|
||||
<summary class="link-quiet">看历史(<?= count(\App\Ledger::history($eid)) ?> 次)</summary>
|
||||
<summary class="link-quiet">看历史(<?= $histN ?> 次)</summary>
|
||||
<ul class="led-hist-list">
|
||||
<?php foreach (\App\Ledger::history($eid) as $hs): ?>
|
||||
<li>
|
||||
@@ -265,48 +300,73 @@ $ledMe = (int)($me ?? 0);
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 改 / 删(改 = 修正录错了,与"盘点"是两件事,见 Ledger::reconcile)---- */ ?>
|
||||
<form method="post" action="<?= e(url('ledger/entry_update')) ?>" class="node-form">
|
||||
<div class="led-panel" id="edit-<?= $eid ?>" hidden>
|
||||
<?php /* 用户第 3 条:修改的作用域是**名称 / 挂在谁名下 / 备注等** ——
|
||||
与"更正金额"分开:改金额属于"当初录错了",那走盘点(留历史);
|
||||
这里改的是描述性字段,所以**也可以含类别与金额**(都是描述),
|
||||
但文案上讲清楚它与盘点的区别。 */ ?>
|
||||
<form method="post" action="<?= e(url('ledger/entry_update')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<input type="hidden" name="id" value="<?= $eid ?>">
|
||||
<h4 class="led-form-h">更正这笔(当作当初就录错了,不留历史)</h4>
|
||||
<div class="nf-grid">
|
||||
<div class="field">
|
||||
<label>类别</label>
|
||||
<select class="select" name="kind">
|
||||
<?php foreach (\App\Ledger::KINDS as $k => $v): ?>
|
||||
<option value="<?= e($k) ?>" <?= $ek === $k ? 'selected' : '' ?>>
|
||||
<?= e($v[0]) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>名称</label>
|
||||
<input class="input" name="title" value="<?= e((string)$en['title']) ?>" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>金额(元)</label>
|
||||
<input class="input" name="amount"
|
||||
value="<?= e(\App\Ledger::amountYuan((int)$en['amount_cents'])) ?>" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>备注</label>
|
||||
<input class="input" name="note" value="<?= e((string)($en['note'] ?? '')) ?>">
|
||||
</div>
|
||||
<h4 class="led-form-h">修改「<?= e((string)$en['title']) ?>」</h4>
|
||||
<p class="tip" style="margin:0 0 8px">
|
||||
改的是名称、归属与备注。若只是**金额变了**(不是录错),
|
||||
请用「盘点」——那样才会记进历史、在图上多一个点。
|
||||
</p>
|
||||
<div class="field">
|
||||
<label>名称</label>
|
||||
<input class="input" name="title" value="<?= e((string)$en['title']) ?>" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>挂在谁名下</label>
|
||||
<?php
|
||||
$ppName = 'person_id';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = !empty($en['person_id']) ? [(int)$en['person_id']] : [];
|
||||
$ppMode = 'single';
|
||||
$ppOpenBtn = '更换归属人';
|
||||
$ppTitle = '这笔账挂在谁名下';
|
||||
$ppEmptyText= '不指定';
|
||||
$ppAllowEmpty = true;
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>类别</label>
|
||||
<select class="select" name="kind">
|
||||
<?php foreach (\App\Ledger::KINDS as $k => $v): ?>
|
||||
<option value="<?= e($k) ?>" <?= $ek === $k ? 'selected' : '' ?>>
|
||||
<?= e($v[2] . ' ' . $v[0]) ?><?= $v[1] === 'debt' ? '(负债)' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>金额(元)</label>
|
||||
<input class="input" name="amount"
|
||||
value="<?= e(\App\Ledger::amountYuan((int)$en['amount_cents'])) ?>" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>备注</label>
|
||||
<input class="input" name="note" value="<?= e((string)($en['note'] ?? '')) ?>">
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-sm" type="submit">保存</button>
|
||||
<button class="btn btn-ghost btn-sm" type="button" data-led-close>取消</button>
|
||||
</div>
|
||||
<div class="btn-row"><button class="btn btn-sm" type="submit">保存更正</button></div>
|
||||
</form>
|
||||
<form method="post" action="<?= e(url('ledger/entry_del')) ?>" class="res-inline">
|
||||
<form method="post" action="<?= e(url('ledger/entry_del')) ?>" class="res-inline"
|
||||
style="margin-top:8px">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<input type="hidden" name="id" value="<?= $eid ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这笔账目?')">删除</button>
|
||||
onclick="return confirm('删掉这笔账目?')">删除这笔</button>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -326,13 +386,69 @@ $ledMe = (int)($me ?? 0);
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
/* ---- 「记一笔」开合 ---- */
|
||||
var btn = document.getElementById('ledAddBtn');
|
||||
var card = document.getElementById('ledAddCard');
|
||||
if (!btn || !card) return;
|
||||
btn.addEventListener('click', function () {
|
||||
card.hidden = !card.hidden;
|
||||
btn.textContent = card.hidden ? '记一笔' : '收起';
|
||||
if (!card.hidden) { card.scrollIntoView({ block: 'nearest' }); }
|
||||
if (btn && card) {
|
||||
btn.addEventListener('click', function () {
|
||||
card.hidden = !card.hidden;
|
||||
btn.textContent = card.hidden ? '记一笔' : '收起';
|
||||
if (!card.hidden) { card.scrollIntoView({ block: 'nearest' }); }
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- 每行「盘点 / 修改」两个按钮:弹各自的面板(v24 第 10 批 #3)----
|
||||
面板贴着那一行展开(不是弹窗遮罩),改完就在原地看得到结果。
|
||||
同时只开一个;点别处 / Esc 收起。 */
|
||||
var openPanel = null;
|
||||
function closePanel() {
|
||||
if (openPanel) { openPanel.hidden = true; openPanel = null; }
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var t = e.target;
|
||||
if (!t || !t.closest) { return; }
|
||||
if (t.closest('[data-led-close]')) { closePanel(); return; }
|
||||
var trigger = t.closest('[data-led-open]');
|
||||
if (trigger) {
|
||||
var id = trigger.getAttribute('data-led-open');
|
||||
var panel = document.getElementById(id);
|
||||
if (!panel) { return; }
|
||||
var wasOpen = (panel === openPanel);
|
||||
closePanel();
|
||||
if (wasOpen) { return; } // 再点一次 = 收起
|
||||
panel.hidden = false;
|
||||
openPanel = panel;
|
||||
// 把面板插到这一行**后面**(作为整行的兄弟),而不是塞在 <td> 里 ——
|
||||
// 表格单元格会被列宽切碎,表单在格子里根本排不开。
|
||||
var tr = trigger.closest('tr');
|
||||
if (tr && panel.parentNode !== tr.parentNode) {
|
||||
tr.parentNode.insertBefore(panel, tr.nextSibling);
|
||||
}
|
||||
// 焦点给到第一个输入框:点完就能打字
|
||||
var inp = panel.querySelector('input:not([type=hidden]), select');
|
||||
if (inp) { inp.focus(); }
|
||||
return;
|
||||
}
|
||||
if (openPanel && !openPanel.contains(t)) { closePanel(); }
|
||||
}, true);
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') { closePanel(); }
|
||||
});
|
||||
|
||||
/* ---- 图表按类别筛选("所有账目"在几百条时会变成一坨)---- */
|
||||
var bar = document.querySelector('[data-ck-filter]');
|
||||
if (bar) {
|
||||
bar.addEventListener('click', function (e) {
|
||||
var chip = e.target.closest('.ck-chip');
|
||||
if (!chip) { return; }
|
||||
var kind = chip.getAttribute('data-ck-kind') || '';
|
||||
Array.prototype.forEach.call(bar.querySelectorAll('.ck-chip'), function (c) {
|
||||
c.classList.toggle('is-on', c === chip);
|
||||
});
|
||||
Array.prototype.forEach.call(document.querySelectorAll('[data-ck-card]'), function (c) {
|
||||
c.hidden = kind !== '' && c.getAttribute('data-ck-card') !== kind;
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user