名册点击看详情拖动才排序 / 任务复选框与状态 / 财务标签多选 / 微信读书同步与统计 /
导航线上数据与图标
用户本轮 9 条需求(原话见 templates/ 与 app/ 内的逐条注释):
1. 名册:列表里给某人分组改为 AJAX 就地挪卡片(不再整页刷新)。
2. 纪念日:关联人改**多选**(复用 _person_picker,字段 person_ids[]);
自建纪念日的按钮与其它模块统一(改=蓝 .btn、删=黄 .btn-warn);
列表新增「关联人」列并给人名前加头像。
3. 首页:日程/任务两块**完全等高**(align-items:stretch + 固定总高 + flex 子项
min-height:0);修「0 件待办」——数字与右侧列表同源(原来各算各的)。
4. 🔴 名册里被移出的人过一阵又回来:根因是 PersonRoster::seedFromPersons()
**每次 Schema::ensure() 都无条件重灌**(线上实测每跑一次 +1 行)。
修:给它加自守卫(只有在 persons 还留着 group_id/sort_order 的**老库**才跑)。
5. 名册:点击=看详情、**只有拖 ⠿ 手柄**才是排序(原来整行既能点又能拖,语义混乱)。
6. 任务:所有任务前加复选框,任何位置点勾即完成;
状态语义照业界整理为 To Do / Doing / Done 并写出说明(用户要求参考别人的做法)。
7. 财务:类别标签改**多选非互斥**(再点一次取消该类别),上下两图 + 图例 + 表格
全部按同一个集合联动;高亮条刻意不给「合计」(多选下各类金额不能相加)。
8. 微信读书:新增**懒同步**(访问时顺手做,6h TTL,只补空字段不覆盖用户填的)、
封面图**拉下来存本站存储**(原来只存外链 → 看不到)、接官方 /readdata/detail 统计。
9. 导航:线上数据只搬了一半(5 板块 29 站 → 7 板块 67 站 + 67 图标)、
展示页头像不显示 / 搜索按钮错位 / 点登录 404。三个真根因:
· 相对地址在 /nav/ 下全 404 → 新增 Nav::abs();
· 搜索容器丢了 id="search"(CSS 是 #search button.submit 绝对定位);
· Nav::iconKey() 与 SimpleNav 的 icon_key() **算法不一致**(少一次 md5 归一化)
→ 搬来的图标文件名对不上;且 uploads/ 整目录 Require all denied。
新增两个**免登录公开出口** nav_icon.php / nav_avatar.php。
顺带修掉一个「数据损坏级」的真 bug(做第 4 条线上反证时发现的,之前一直没人注意):
app/Region.php 三处用中文标点当 trim/ltrim 的 charlist,而 PHP 的 charlist 是
**按字节**匹配的 —— 「。」= E3 80 **82** → 字节 82 进了可剥集合 →
「层」= E5 B1 **82** 被切掉尾字节 → 非法 UTF-8 → backfillAddress 的 UPDATE 报
`1366 Incorrect string value` → **地址拆分升级从上线起一直在失败被跳过**(每请求报一次)。
改成字符级 preg_replace('/…/u');线上实测 ensure() 现在无报错、待拆行数真的在下降。
验证战绩:
- e2e 1289/0(新增 Region 字节级用例等)
- 真机探针:选人 19/0 · 首页/导航 24/0 · 导航模块 34/0 · b18 暂存+确定 10/0 ·
第23批 52/0
- 桌面几何 428/0 · 移动端几何 680/0 · 交互 92/0 · 窄屏 24/0
- 老库升级 153/0(含 anniversary_people 迁移 + 「再跑 ensure() 名册行数不变」)
- 部署静态断言 180 OK / 0 FAIL · 反向验证 3/3 + Region 三变体各被两条断言同时抓住
- 线上 26 个文件与本地**逐字节一致**;展示页 200、图标 66/66 200、登录 200、
越权/不存在均 404、签发短语后 ensure() 不再报错。
121 lines
5.4 KiB
PHP
121 lines
5.4 KiB
PHP
<?php
|
|
/**
|
|
* 新建 / 编辑纪念日(v24 用户第 13 条)
|
|
*
|
|
* 用户原话:「可以根据阴历和阳历来设置纪念日 …… 纪念日,需要按照阴历和阳历进行重复。」
|
|
* 所以重复方式这一栏是核心:阳历 / 农历 / 只此一次。
|
|
* 选农历时,下面的月/日填的是**农历**的月日(不是什么公历日期换算)。
|
|
*
|
|
* @param array|null $editing 编辑时的既有记录
|
|
* @param array $people 名册
|
|
*/
|
|
$e0 = $editing ?: [];
|
|
$v = static fn(string $k, $def = '') => $e0[$k] ?? $def;
|
|
$repeat = (string)($e0['repeat_type'] ?? 'solar');
|
|
// 用户 2026-10-03 第 9 条:分类下拉的选项来自**可配置**的分类清单
|
|
// (原来写死 PRESET_KINDS,用户要能自己增删 —— 见「纪念日设置」)。
|
|
$kinds = (array)($kinds ?? \App\Anniversary::PRESET_KINDS);
|
|
$curKind = (string)($e0['kind'] ?? '');
|
|
// 编辑一条用了**已被删掉的分类**的纪念日时,要把它自己也列进下拉,
|
|
// 否则保存一次就把它悄悄改成了第一个选项(用户没动它,值却变了)。
|
|
if ($curKind !== '' && !in_array($curKind, $kinds, true)) {
|
|
array_unshift($kinds, $curKind);
|
|
}
|
|
?>
|
|
<div class="page-head">
|
|
<h1><?= $editing ? '编辑纪念日' : '新建纪念日' ?></h1>
|
|
<div class="actions">
|
|
<a class="btn btn-ghost" href="<?= e(url('days')) ?>">返回</a>
|
|
</div>
|
|
</div>
|
|
|
|
<form class="card card-pad" method="post" action="<?= e(url('days/save')) ?>">
|
|
<?= csrf_field() ?>
|
|
<?php if ($editing): ?><input type="hidden" name="id" value="<?= (int)$e0['id'] ?>"><?php endif; ?>
|
|
|
|
<div class="form-grid">
|
|
<div class="field full">
|
|
<label for="an-title">叫什么 *</label>
|
|
<input class="input" id="an-title" name="title" required
|
|
value="<?= e((string)$v('title')) ?>" placeholder="如:结婚纪念日">
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="an-repeat">按什么重复 *</label>
|
|
<select class="select" id="an-repeat" name="repeat_type">
|
|
<?php foreach (\App\Anniversary::REPEAT as $rk => $rv): ?>
|
|
<option value="<?= e($rk) ?>" <?= $repeat === $rk ? 'selected' : '' ?>><?= e($rv) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<span class="tip">选「农历」时,下面填的是**农历**的月日(不是公历)。</span>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="an-kind">分类</label>
|
|
<?php /* 用 <select> 而不是 <input list=…>:分类现在是**用户自己维护的一套**,
|
|
下拉能保证选到的都是存在的分类;datalist 允许自由填,
|
|
填出来的新词不会进清单,下次别人也不知道有这个词。 */ ?>
|
|
<select class="select" id="an-kind" name="kind">
|
|
<option value="">不分类</option>
|
|
<?php foreach ($kinds as $k): ?>
|
|
<option value="<?= e((string)$k) ?>" <?= $curKind === (string)$k ? 'selected' : '' ?>><?= e((string)$k) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<span class="tip">分类可以在「纪念日」页右侧的设置里增删。</span>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="an-month">月 *</label>
|
|
<input class="input" id="an-month" name="month" type="number" min="1" max="12" required
|
|
value="<?= (int)$v('month', 1) ?>">
|
|
</div>
|
|
<div class="field">
|
|
<label for="an-day">日 *</label>
|
|
<input class="input" id="an-day" name="day" type="number" min="1" max="31" required
|
|
value="<?= (int)$v('day', 1) ?>">
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>与谁相关</label>
|
|
<?php /* 用户第 2 条:「每个纪念日现在只能关联一个人,应该可以关联多个人。
|
|
和选择观影人一样,多选模式。」
|
|
→ 换 `_person_picker.php` 的 **multi** 模式(与观影选人同一套:
|
|
弹窗 + 拼音首字母联想 + 暂存区 + 确定)。
|
|
⚠️ 字段名从 `person_id`(单值)改成 **`person_ids[]`**(多值)——
|
|
`[]` 不是可有可无的记号:不写它,PHP 只会收到最后一个值,
|
|
选了 3 个人只存上 1 个,而且**不报任何错**。
|
|
⚠️ 已选的人:优先用新表的 person_ids;老记录回落到 person_id
|
|
(升级过渡期的兜底,见 Anniversary::split)。 */ ?>
|
|
<?php
|
|
$ppName = 'person_ids[]';
|
|
$ppPeople = $people;
|
|
$ppSelected = (array)($e0['person_ids'] ?? []);
|
|
if (!$ppSelected && (int)($e0['person_id'] ?? 0) > 0) {
|
|
$ppSelected = [(int)$e0['person_id']];
|
|
}
|
|
$ppMode = 'multi';
|
|
$ppOpenBtn = '+ 关联人';
|
|
$ppTitle = '这个纪念日与谁相关';
|
|
require __DIR__ . '/../_person_picker.php';
|
|
?>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="an-leap">农历闰月</label>
|
|
<label class="chk-inline">
|
|
<input type="checkbox" id="an-leap" name="is_leap" value="1" <?= !empty($e0['is_leap']) ? 'checked' : '' ?>>
|
|
<span>这个农历月是闰月</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="field full">
|
|
<label for="an-note">备注</label>
|
|
<textarea class="input" id="an-note" name="note" rows="2"><?= e((string)$v('note')) ?></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<button class="btn" type="submit"><?= $editing ? '保存' : '添加' ?></button>
|
|
<a class="btn btn-ghost" href="<?= e(url('days')) ?>">取消</a>
|
|
</div>
|
|
</form>
|