用户 6 条原话(要点):
①「选择名册里面人员的那个弹出框有个问题,就是会把页面里面已有的人员给冲掉。
比方说电影里面的观影人或者是旅程里面的参与人…这是一个 bug 应该通篇检查。」
并按「输入拼音 → 空格选中第一个 → 输入框清空回焦 → 再输下一个 → 回车=确定」重做交互。
② 首页简要信息改成:问候 + 姓名 + 日期 + 星期 + 农历 + 节气/节日/纪念日/生日,
下面「今天几条日程 / 几件待办」,天气**彩色图标**放**最右侧**,温度风力,最下面地址。
③「家里的镜框这里呃全部不要了」→ 删模块卡片墙 + 未来七天;日程与任务**左右各半**、
高度固定、内部滚动且**不显滚动条**;日程**掐头去尾** + 当前时刻**红线**;
任务显示标题+状态+**所在看板名**;最下面往年的今天。
④「把个人导航项目完整搬过来,放在读书后面」;免登录展示页 `/nav/?<短语>`;
右上角给**登录**入口;展示页头像用**用户自己的头像**。
⑤ 读书加笔记 + 导入微信读书笔记(用户明确「**用微信读书 api key**」)。
⑥ 财务页第二张多线图不随类别标签筛选。
用户拍板(AskUserQuestion):① 首页删「模块卡片墙 + 未来七天」两块;
② 微信读书用官方 API Key;③ 导航短语「自己起名 + 可一键换随机码」都做;
④ 6 条一次做完再上线。
════ 一、 🔴 第 1 条:复现出的真 bug(全站性)════
**根因**:选人清单是**打开弹框时才建 DOM**(懒建,为性能),而承载"已选"的
checkbox 就在那份清单里 → **没打开过弹框就提交**时表单里一个值都没有 →
后端把"表单里没有 attendees"理解成"用户要把人全删掉" → **静默清空**。
实测复现:库里同行人 `2,3,4` → 只改标题保存后变成 **NULL**。
修法:模板加**承运字段**(`.pp-carry` 隐藏 input,承载当前已选);
JS 建清单时**撤掉**它(否则同一个 name 会带两份值)。
⚠️ 裸 POST(完全不带 attendees)**本来就该清空** —— 那是显式地"不选任何人",
所以断言也必须照着"浏览器实际提交的样子"(带承运字段)来测。
**键盘流程**(用户第 1 条后半):空格 = 选**当前可见列表里第一个还没选的**
(⚠️ 必须跳过已选中的 —— 否则会把它**取消**,与"我要选人"正好相反,本轮真机跑出来的缺陷);
选中后清空输入框 + 光标回输入框;回车 = 确定。空格必须 `preventDefault`
(否则输入框里多一个空格字符)。
覆盖范围:4 个多选(`attendees[]` / `members[]` / `shares[]` / 修订 `members[]`)
+ 全部单选(归属人 / 负责人 / 纪念日人等)。
════ 二、 首页改版(第 2、3 条)════
自上而下只有三块:
① 信息面板:问候(早/中/晚 + 姓名)→ 日期 / 星期 / 农历 → 日子标记
(法定节假日 / 节气 / 传统节日 / 纪念日 / 生日,各一色)
→ 今日数字(今天几条日程、几件待办)| **最右**:彩色天气图标 + 温度 + 风力 + 地址
② 日程 / 任务 **左右各半**(各 360px 固定高、内部滚动、**不显滚动条**三套写法)
日程是**日视图形式的刻度**、**掐头去尾**(首末日程前后各 1h)、**当前时刻红线**
任务是标题 + **状态** + **看板名** + 截止
③ 往年的今天
新增 `Home::todayMarks()`(把五类来源合成一句话:"今天是国庆节 · 大暑")与
天气的 `tone` 色调档(彩色图标靠 CSS 上色,**不用 emoji** —— 避免旧系统豆腐块)。
删掉:`mod-cards` 模块卡片墙、`未来七天`。
════ 三、 个人导航(第 4 条,SimpleNav 完整搬运)════
`app/Nav.php`(4 张 `nav_*` 表 + 稀疏排序 + 短语)· `app/NavFetch.php`
(抓取与图标生成,27KB,从原项目 functions.php 机械搬运,**原文那批"线上实测"注释全部保留**)
· `nav_show.php`(**全站唯一免登录页**)· `nav_suggest.php`(联想代理,凭短语门禁)
· `templates/nav/index.php`(管理页:板块/站点/引擎/设置 + 拖拽排序)。
搬运只改两处(不改会坏):① 独立 SQLite → 家书的 MySQL;② **所有查询加 `person_id`**
(原项目是单用户系统,不加这一维全家人会共用同一份导航)。
免登录展示页:`/nav/?<短语>`(也认 `/nav/?<短语>` 不带参数名);
短语用户自起名(4~32 位字母数字`_-`,全局唯一、保留词拦截)或一键换随机码;
右上角**登录**入口(进家书登录页);头像用**他自己**的头像;空态给一句话。
路由三处同步(`router.php` / `.htaccess` / 部署脚本 5c **独立标记块**)——
这是本仓库第三次复用这条规律:**每加一个"站根上的独立入口",就必须是自己的标记块**。
⚠️ 线上重写规则**顺序有语义**:`^nav/suggest\.php$` 必须在 `^nav/?$` 之前
(后者会把前者吞掉)。
════ 四、 微信读书(第 5 条)════
用户提示「用微信读书 api key」是对的 —— **我的知识过时了**:2026 年官方已开放
(网关 `POST https://i.weread.qq.com/api/agent/gateway`、`Bearer wrk-` 认证、
接口 `/user/notebooks`(游标 lastSort 分页)、`/book/bookmarklist`、
`/review/list/mine`、`/book/chapterinfo`)。
`app/WeRead.php`:Key 用 `Crypto::encrypt` **加密落库**、界面只回显掩码
(官方原话:这串 Key 等价于账号凭据);导入走「拉预览 → 勾选 → 导入」,
落 `book_notes`(去重 = kind + 内容 + 位置)。
⚠️ 业务参数必须**平铺在顶层**,不能包进 `params`(官方文档专门列了这个反例)。
另一条路「手动粘贴」保留(`books/notes_import`,早已有)。
════ 五、 财务第二张图(第 6 条,**上一批我引入的 bug**)════
`g.hidden = true` 对 **SVG 的 `<g>` 无效**(它没有 `hidden` 属性)→ 线条一条都没被藏;
**而我的探针也读同一个属性** → 两边一起绿(假绿)。
改为 `style.display`(恢复用 `''` 而不是 `'block'`);探针改读
`getComputedStyle(el).display !== 'none'`(落在**渲染结果**上)。
⚠️ "探针与实现读同一个属性"是**互相掩盖**的典型,比单纯写错断言更危险。
════ 六、 验证 ════
e2e **1218/0**(本批新增 102 条)· 真机探针 **19/0**(选人)+ **24/0**(首页/导航)
+ **34/0**(导航模块)· 桌面几何 0 失败 · 移动端几何 674/0 · 老库升级 149/0
· 部署静态断言空跑 **310/0** · **反向验证 13/13**(逐条改回旧行为,点名断言都变红)
· 线上验收:9 个新文件全在、4 张 nav 表建好(schema_version=24)、
三条重写规则顺序正确、免登录页 / 空短语 / 错短语 / 联想门禁 全部响应正确。
════ 七、 本批的"夹具与断言"教训(都写进注释了)════
· `calendar_events.all_day` **默认值是 1** → 插夹具不写它就成了全天事件 →
`time` 被抹成 `''` → "刻度掐头去尾"看着像没做(白查三轮)。
· 日历本要挑 `kind='custom'`(我原以为"普通本没有 kind",实际**普通本就是 custom**)。
· 量"同高/同排"要拿**内容区**(去 padding)比,不能拿 border-box(`.topline` 有 14px 上内边距)。
· 判据不能把**边界上的正确行为**当 bug(22 点有日程时末刻度**就是** 23:00)。
· 「HTML 写了、CSS 没写」e2e 永远查不出来 —— 导航展示页的登录按钮就落到了左边
(实测按钮右 116px / 整行右 1210px),只有真机量几何才抓得到。
· 反向验证里 e2e 会**重铺副本**把注入覆盖掉 → 给 e2e 加了 `FB_PATCH_FILE` 钩子
(重铺完自动打补丁),13 例里 8 例"没牙齿"其实是这个假象。
════ 八、 新增/改动文件 ════
新增:`app/Nav.php` · `app/NavFetch.php` · `app/WeRead.php` · `nav_show.php` ·
`nav_suggest.php` · `templates/nav/index.php` · `templates/books/weread.php` ·
`assets/css/nav-show.css` · `assets/js/nav-show.js`
改动:`app/Schema.php`(v24 + 4 张 nav 表)· `app/Module.php`(+nav)· `app/Home.php` ·
`index.php` · `templates/_person_picker.php` · `assets/js/person_picker.js` ·
`templates/home.php` · `templates/ledger/index.php` · `templates/books/_settings_menu.php` ·
`assets/css/app.css` · `router.php` · `.htaccess`
257 lines
12 KiB
CSS
257 lines
12 KiB
CSS
/* ============================================================
|
||
导航展示页样式(v24 第 24 批 用户第 4 条)
|
||
—— **从用户自己的 SimpleNav 项目整份搬运**(assets/css/style.css)。
|
||
逻辑与选择器一行未改,只做了两件事:
|
||
① 变量改名:`--primary` → `--n-primary`(家书全站已有一堆 `--primary` 语义的变量,
|
||
撞名会让两边的主题色互相串;加 `n-` 前缀把这份样式**关在自己家里**);
|
||
② 底色/文字/描边换成家书的**纸墨暖色调**(原项目是冷灰蓝),
|
||
让展示页与家书看起来是同一个产品,而不是"外链了一个别的站"。
|
||
⚠️ 除了上面两样,其余一律照抄 —— 用户要的是「完全照搬原来的导航系统」。
|
||
============================================================ */
|
||
/* 简航 SimpleNav —— 前台样式
|
||
在经典「浅灰底 + 白卡片」导航站风格基础上做了现代化处理:
|
||
圆角更大、阴影分层、悬停微动效、顶部品牌区与胶囊式搜索引擎切换。 */
|
||
:root {
|
||
--n-primary: #A84A28;
|
||
--n-primary-dark: #6E3820;
|
||
--n-primary-soft: #FBEFE8;
|
||
--bg-1: #F7F4EF;
|
||
--bg-2: #F1EBE2;
|
||
--card: #FFFFFF;
|
||
--text: #2C231D;
|
||
--muted: #7E6B5A;
|
||
--line: rgba(44, 35, 29, .10);
|
||
--radius-lg: 18px;
|
||
--radius: 12px;
|
||
/* 版心:只推一次 —— 顶栏与正文共用同一个左右内边距,两边算出来严格相等 */
|
||
--wrap: 1140px;
|
||
--gutter: 16px;
|
||
--pad-x: max(var(--gutter), calc((100% - var(--wrap)) / 2));
|
||
--shadow-sm: 0 1px 2px rgba(23, 43, 77, .05), 0 4px 12px rgba(23, 43, 77, .05);
|
||
--shadow: 0 2px 4px rgba(23, 43, 77, .04), 0 10px 30px rgba(23, 43, 77, .08);
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html { -webkit-text-size-adjust: 100%; }
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
font-size: 14px;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||
color: var(--text);
|
||
background: linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 100%) fixed;
|
||
overflow-x: hidden;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
a { text-decoration: none; color: inherit; }
|
||
.container { padding: 0 var(--pad-x); margin: 0 0 40px; }
|
||
|
||
/* ---------- 顶部品牌区 ---------- */
|
||
.hero { text-align: center; padding: 52px 0 22px; }
|
||
.hero .logo {
|
||
display: block; width: 60px; height: 60px; margin: 0 auto 14px; border-radius: 18px;
|
||
box-shadow: 0 8px 22px rgba(47, 107, 247, .28);
|
||
}
|
||
.hero h1 { margin: 0; font-size: 25px; font-weight: 600; letter-spacing: .5px; }
|
||
.hero p { margin: 7px 0 0; font-size: 13px; color: var(--muted); }
|
||
|
||
/* ---------- 搜索 ---------- */
|
||
.s-search { position: relative; z-index: 2000; max-width: 760px; margin: 0 auto 6px; }
|
||
.search-type { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-bottom: 14px; }
|
||
.s-engine {
|
||
display: inline-block; padding: 6px 16px; border-radius: 999px; cursor: pointer;
|
||
background: rgba(255, 255, 255, .72); border: 1px solid rgba(255, 255, 255, .9);
|
||
font-size: 14px; line-height: 20px; color: #4a5568;
|
||
transition: transform .18s, box-shadow .18s, background .18s, color .18s;
|
||
user-select: none;
|
||
}
|
||
.s-engine span { color: var(--ec, #555); }
|
||
.s-engine:hover { background: #fff; transform: translateY(-1px); box-shadow: var(--shadow-sm); }
|
||
.s-engine.active {
|
||
background: var(--ec, var(--n-primary)); border-color: var(--ec, var(--n-primary));
|
||
box-shadow: 0 6px 16px rgba(47, 107, 247, .26);
|
||
}
|
||
.s-engine.active span { color: #fff; }
|
||
|
||
#search-form { position: relative; }
|
||
#search-text {
|
||
width: 100%; height: 56px; line-height: 56px; text-indent: 22px; padding-right: 64px;
|
||
font-size: 16px; border: 2px solid transparent; border-radius: 28px; background: #fff;
|
||
color: var(--text); outline: 0; box-shadow: var(--shadow);
|
||
transition: border-color .2s, box-shadow .25s;
|
||
}
|
||
#search-text::placeholder { color: #a9b3c0; }
|
||
#search-text:focus {
|
||
border-color: var(--n-primary);
|
||
box-shadow: 0 0 0 5px rgba(47, 107, 247, .12), var(--shadow);
|
||
}
|
||
#search button.submit {
|
||
position: absolute; top: 8px; right: 8px; width: 40px; height: 40px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
border: 0; border-radius: 50%; background: var(--n-primary); color: #fff;
|
||
cursor: pointer; outline: none; transition: background .2s, transform .2s;
|
||
}
|
||
#search button.submit:hover { background: var(--n-primary-dark); transform: scale(1.05); }
|
||
#search button.submit:active { transform: scale(.96); }
|
||
|
||
#word {
|
||
position: absolute; list-style: none; top: 64px; left: 0; width: 100%; margin: 0;
|
||
background: #fff; border-radius: 16px; z-index: 20000; padding: 8px;
|
||
box-shadow: 0 16px 44px rgba(23, 43, 77, .16); display: none;
|
||
max-height: 336px; overflow-y: auto;
|
||
}
|
||
#word li {
|
||
padding: 0 14px; height: 38px; line-height: 38px; cursor: pointer;
|
||
font-size: 15px; border-radius: 10px; color: #3d4753;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
#word li:hover, #word li.active { background: var(--n-primary-soft); color: var(--n-primary); }
|
||
|
||
/* ---------- 板块卡片 ---------- */
|
||
.mylist {
|
||
list-style: none; margin: 18px 0; padding: 16px 20px 14px;
|
||
background: var(--card); border-radius: var(--radius-lg); box-shadow: var(--shadow);
|
||
display: flex; flex-wrap: wrap; transition: box-shadow .25s;
|
||
}
|
||
.mylist:hover { box-shadow: 0 4px 8px rgba(23, 43, 77, .05), 0 14px 38px rgba(23, 43, 77, .1); }
|
||
.mylist > li.title {
|
||
width: 100%; margin: 0 0 12px; padding: 0 0 10px 14px; height: auto;
|
||
font-size: 16px; font-weight: 600; line-height: 22px; color: #1f2733;
|
||
border-bottom: 1px solid var(--line); position: relative;
|
||
}
|
||
.mylist > li.title::before {
|
||
content: ""; position: absolute; left: 0; top: 3px; width: 4px; height: 17px;
|
||
background: linear-gradient(180deg, var(--n-primary), #6f9bff);
|
||
border-radius: 2px;
|
||
}
|
||
.mylist > li.site { margin: 2px; }
|
||
.mylist li.site a {
|
||
display: flex; flex-direction: column; align-items: center; width: 104px;
|
||
padding: 12px 6px 10px; border-radius: 14px;
|
||
transition: background .2s, transform .2s;
|
||
}
|
||
.mylist li.site a:hover { background: linear-gradient(180deg, #f7f9ff, var(--n-primary-soft)); transform: translateY(-3px); }
|
||
.mylist li.site .icon {
|
||
width: 46px; height: 46px; border-radius: 12px; background: #fff;
|
||
object-fit: contain; box-shadow: 0 2px 8px rgba(23, 43, 77, .09);
|
||
transition: transform .2s;
|
||
}
|
||
.mylist li.site a:hover .icon { transform: scale(1.07); }
|
||
.mylist li.site .ico-letter {
|
||
width: 46px; height: 46px; border-radius: 12px; color: #fff; font-size: 20px; font-weight: 600;
|
||
display: flex; align-items: center; justify-content: center;
|
||
box-shadow: 0 2px 8px rgba(23, 43, 77, .14); transition: transform .2s;
|
||
}
|
||
.mylist li.site a:hover .ico-letter { transform: scale(1.07); }
|
||
.mylist li.site .name {
|
||
width: 100%; margin-top: 8px; text-align: center; font-size: 13px; color: #5b6673;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
transition: color .2s;
|
||
}
|
||
.mylist li.site a:hover .name { color: var(--n-primary); }
|
||
|
||
/* ---------- 合集标签页 ---------- */
|
||
.collections { margin: 24px 0 0; }
|
||
.collections-title {
|
||
font-size: 16px; font-weight: 600; color: #1f2733;
|
||
padding: 0 0 10px 14px; margin: 0 0 14px; position: relative;
|
||
}
|
||
.collections-title::before {
|
||
content: ""; position: absolute; left: 0; top: 3px; width: 4px; height: 17px;
|
||
background: linear-gradient(180deg, var(--n-primary), #6f9bff); border-radius: 2px;
|
||
}
|
||
.tabs { display: flex; flex-wrap: wrap; gap: 9px; margin-bottom: 14px; }
|
||
.tabs .tab {
|
||
border: 1px solid rgba(255, 255, 255, .9); background: rgba(255, 255, 255, .72);
|
||
color: #4a5568; cursor: pointer; padding: 8px 18px; border-radius: 999px;
|
||
font-size: 14px; transition: all .18s;
|
||
}
|
||
.tabs .tab:hover { background: #fff; transform: translateY(-1px); box-shadow: var(--shadow-sm); }
|
||
.tabs .tab.active {
|
||
background: var(--n-primary); border-color: var(--n-primary); color: #fff;
|
||
box-shadow: 0 6px 16px rgba(47, 107, 247, .26);
|
||
}
|
||
.tab-panel { display: none; }
|
||
.tab-panel.active { display: flex; animation: fadeUp .28s ease both; }
|
||
/* 「全部平铺」模式:每个合集各占一块,与上面的网格板块一致(此时没有 .tabs) */
|
||
.collections.flat .collections-title { margin-bottom: 16px; }
|
||
.collections.flat .mylist { margin: 0 0 16px; }
|
||
.collections.flat .mylist:last-child { margin-bottom: 0; }
|
||
@keyframes fadeUp {
|
||
from { opacity: 0; transform: translateY(8px); }
|
||
to { opacity: 1; transform: none; }
|
||
}
|
||
|
||
/* ---------- 页脚 ---------- */
|
||
.foot {
|
||
text-align: center; color: #97a1ae; font-size: 12px; margin: 30px 0 0;
|
||
padding-top: 18px; border-top: 1px solid rgba(23, 43, 77, .06);
|
||
}
|
||
.foot a { color: #97a1ae; transition: color .2s; }
|
||
.foot a:hover { color: var(--n-primary); }
|
||
|
||
/* ---------- 响应式 ---------- */
|
||
@media (max-width: 767px) {
|
||
.hero { padding: 28px 0 16px; }
|
||
.hero .logo { width: 52px; height: 52px; border-radius: 15px; }
|
||
.hero h1 { font-size: 21px; }
|
||
.s-search { margin-bottom: 2px; }
|
||
.search-type { gap: 6px; margin-bottom: 10px; }
|
||
.s-engine { padding: 5px 13px; font-size: 13px; }
|
||
#search-text { height: 50px; line-height: 50px; font-size: 15px; text-indent: 18px; }
|
||
#word { top: 58px; }
|
||
.mylist { border-radius: 14px; padding: 12px 12px 10px; }
|
||
/* ⚠️ 百分比宽度必须写在 **flex item(li)自身**,不能写在它内部的 <a> 上:
|
||
li 自己没有宽度,a 的 25% 要相对 li 解析、而 li 的宽又由内容 a 反推 ——
|
||
循环依赖浏览器解析不了,会塌成 min-content(实测约 12px),
|
||
于是 42px 图标撑破格子、名称被裁到只剩几像素(2026-09-28 手机端实测)。
|
||
margin 也要归零:4 × (25% + 左右 margin) 会超出一行宽,把第 4 个挤到下一行。 */
|
||
.mylist li.site { width: 25%; margin: 0; }
|
||
.mylist li.site a { width: 100%; padding: 10px 2px 8px; }
|
||
.mylist li.site .icon, .mylist li.site .ico-letter { width: 42px; height: 42px; }
|
||
.mylist li.site .name { font-size: 12px; }
|
||
}
|
||
|
||
/* ============================================================
|
||
导航**展示页**的顶部行(v24 第 24 批 用户第 4 条)
|
||
------------------------------------------------------------
|
||
⚠️ 这一段是**本批新加的,不是从 SimpleNav 搬来的** —— 原项目没有这一行
|
||
(它自己的页面顶部只有品牌区,没有"登录"入口)。
|
||
用户原话:「在这个页面里面要增加一个功能,右上角增加一个功能,就是登录,
|
||
点击这个页面之后再进入到我们现在这个系统就是家书的这个系统的登录页面。」
|
||
「导航页的展示页面上面的那个头像就变成用户自己的头像就可以了。」
|
||
⚠️ 我第一版只写了 HTML、**忘了写这段 CSS**,于是按钮落在左边(真机几何实测:
|
||
按钮右边缘 116px,而整行右边缘 1210px)—— 这类"HTML 写了样式没写"的漏项,
|
||
e2e 查字符串永远绿,只有真机量几何才看得出来。
|
||
============================================================ */
|
||
.topline {
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 14px 0 0;
|
||
}
|
||
.topline .who { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||
.topline .who-av {
|
||
width: 28px; height: 28px; border-radius: 50%; object-fit: cover;
|
||
border: 1px solid var(--line); flex: 0 0 auto;
|
||
}
|
||
.topline .who-name {
|
||
font-size: 13.5px; color: var(--muted);
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
/* ⚠️ 靠右全靠这一行:不写它按钮就贴在左边 */
|
||
.topline .login-btn { margin-left: auto; flex: 0 0 auto; }
|
||
.login-btn {
|
||
display: inline-flex; align-items: center; gap: 5px;
|
||
padding: 5px 12px; border-radius: 999px;
|
||
border: 1px solid var(--line); background: var(--card); color: var(--text);
|
||
font-size: 13px; text-decoration: none;
|
||
}
|
||
.login-btn:hover { border-color: var(--n-primary); color: var(--n-primary); }
|
||
|
||
/* ---- 展示页空态(主人还没加过东西时)---- */
|
||
.nav-empty {
|
||
text-align: center; padding: 34px 16px; margin: 10px 0 24px;
|
||
background: var(--card); border: 1px dashed var(--line); border-radius: var(--radius);
|
||
color: var(--muted);
|
||
}
|
||
.nav-empty p { margin: 0 0 6px; }
|
||
.nav-empty p:last-child { margin-bottom: 0; }
|