From 2f7dc6ca2317413990fc45fce6c5894acc58454a Mon Sep 17 00:00:00 2001 From: Yongquan Date: Mon, 19 Oct 2026 17:27:37 +0800 Subject: [PATCH] =?UTF-8?q?v63=EF=BC=9A=E5=8D=A1=E7=89=87=E9=85=8D?= =?UTF-8?q?=E8=89=B2=20/=20=E6=97=A5=E5=8E=86=E6=9C=AC=E9=9D=A0=E5=8F=B3?= =?UTF-8?q?=20/=20=E5=9C=B0=E5=9B=BE=E9=A2=98=E5=9B=BE=20/=20=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E6=B8=85=E7=90=86=20/=20=E5=85=AC=E5=85=B1?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E9=A2=9C=E8=89=B2=20/=20=E4=BB=A4=E7=89=8C?= =?UTF-8?q?=E4=B8=8B=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 卡片右上角按钮改用「来自哪个模块」的主题色:HomeCards 登记表新增 mod 字段 与 module(),首页从登记表取色(原来是模板里硬编码 in_array(['agenda','tasks']) 白名单,新加带按钮的卡片会默默变灰) - 首页日程把日历本放到色块右侧:.ag-ev 改弹性行,「时间+标题」包进 .ag-ev-main(flex:1 1 auto; min-width:0),日历本作右侧兄弟。 原来 display:block 会把日历本顶到第二行,色块一矮就完全看不见 - 旅程地图显示题图:服务端把 media_url(cover_key) 塞进地图数据, 地图卡片左侧 40x40 题图(object-fit:cover),没有题图不留灰框 - 系统设置第一块改叫「系统标语」;删掉「首页上那一行小字」提示、 「不是全家的统一口号」说明、「上次结构升级有步骤失败了」整框; 运行环境删「排错时看这一块」与「亲属连线」那一行(功能与数据未动) - 公共标签:说明文字全删;列表与编辑合并成一行(色块+名字+N人+箭头+编辑); person_groups 加 color 列(Schema addColumn,老库可升);点「编辑」弹框 改名字与颜色、点「完成」提交;下面那个独立编辑列表撤掉。删除挪进弹框 - 个人设置删掉「手动签发接口令牌」整块;settings/api_token 路由保留但挡下提交 (只删界面会留下知道地址就能用的后门);令牌只由登录后自动签发, 设备管理卡保留。apiTokens/apiPlain/apiLabel/apiBase 四个变量一并撤掉 --- app/HomeCards.php | 30 +++- app/PersonGroup.php | 79 ++++++++-- app/Schema.php | 13 ++ assets/css/app.css | 72 +++++++++- assets/js/trip_map.js | 11 +- index.php | 58 +++++--- templates/home.php | 10 +- templates/home/_card_agenda.php | 12 +- templates/settings.php | 248 ++++++++++++++++++++++++-------- templates/settings/me.php | 95 ++---------- templates/trips/_map_body.php | 11 +- 11 files changed, 450 insertions(+), 189 deletions(-) diff --git a/app/HomeCards.php b/app/HomeCards.php index a09dc22..e3246be 100644 --- a/app/HomeCards.php +++ b/app/HomeCards.php @@ -43,47 +43,71 @@ final class HomeCards public const HEIGHTS = [2, 4, 6, 8]; /** - * 卡片注册表:key => [名字, 说明, 默认宽, 默认高, 详情页路由, 按钮文字, 是否有"显示几条"] + * 卡片注册表:key => [名字, 说明, 默认宽, 默认高, 详情页路由, 按钮文字, 是否有"显示几条", 来自哪个模块] * * ⚠️ key 就是模板名(`_card_.php`)与 `data-hc` 的值,三处必须一致。 * ⚠️ 新加一张卡片只改这里 + 写一个 `_card_xxx.php`:布局设置页、默认布局、 * 首页渲染全部自动出现(`HomeMods` 那条"新增模块的唯一入口"的同一条经验)。 + * + * 🔴 v63(用户 2026-10-19 第 1 条):「卡片右上角的按钮,应该跟这个卡片来自于 + * 什么模块的主题色一样才对。」⇒ 每张卡片登记它**来自哪个模块**(`mod`), + * 右上角按钮的底色就取那个模块的主题色(家庭级、可在设置里改,见 `Module::color`)。 + * ⚠️ 之前只有日程/任务两张硬编码写死(`in_array($hcKey, ['agenda','tasks'])`), + * 那张白名单放在模板里 —— 新加一张带按钮的卡片就忘了加,按钮默默变灰。 + * 现在登记在这里:**有 route 就该有 mod**,两件事一次改完。 + * ⚠️ `mod` 只影响按钮配色,不参与权限/路由;认不出的 key 走 `Module::color` 的兜底。 */ public const TYPES = [ 'brief' => [ 'name' => '概述', 'desc' => '问候、日期、农历、天气,以及今天的数字', - 'w' => 8, 'h' => 2, 'route' => '', 'more' => '', 'n' => 0, + 'w' => 8, 'h' => 2, 'route' => '', 'more' => '', 'n' => 0, 'mod' => '', ], 'agenda' => [ 'name' => '今天的日程', 'desc' => '按日视图那样排的今天', 'w' => 4, 'h' => 8, 'route' => 'calendar', 'more' => '更多日程', 'n' => 0, + 'mod' => 'calendar', ], 'tasks' => [ 'name' => '近期任务', 'desc' => '进行中与待办(含看板与状态)', 'w' => 4, 'h' => 8, 'route' => 'tasks', 'more' => '全部任务', 'n' => 0, + 'mod' => 'tasks', ], 'memories' => [ 'name' => '往年的今天', 'desc' => '往年同一天的观影与旅程', - 'w' => 4, 'h' => 4, 'route' => '', 'more' => '', 'n' => 0, + 'w' => 4, 'h' => 4, 'route' => '', 'more' => '', 'n' => 0, 'mod' => '', ], 'days' => [ 'name' => '纪念日', 'desc' => '接下来 60 天的纪念日与生日', 'w' => 4, 'h' => 4, 'route' => 'days', 'more' => '全部纪念日', 'n' => 0, + 'mod' => 'days', ], 'movies' => [ 'name' => '观影', 'desc' => '最近看过的片子', 'w' => 4, 'h' => 4, 'route' => 'movies', 'more' => '观影记录', 'n' => 0, + 'mod' => 'movies', ], 'books' => [ 'name' => '读书', 'desc' => '在读与最近加进书架的书', 'w' => 4, 'h' => 4, 'route' => 'books', 'more' => '书架', 'n' => 0, + 'mod' => 'books', ], 'contacts' => [ 'name' => '联系人', 'desc' => '名册里的人,可以选显示几个', 'w' => 4, 'h' => 4, 'route' => 'people', 'more' => '名册', 'n' => 8, + 'mod' => 'people', ], ]; + /** + * 这张卡片来自哪个模块(没有归属的返回空串 —— 模板据此决定按钮用不用主题色)。 + * + * ⚠️ 别在模板里再写一份白名单:`mod` 是唯一来源。 + */ + public static function module(string $key): string + { + return (string)(self::TYPES[$key]['mod'] ?? ''); + } + /** * 默认布局(第一次打开首页的人看到的样子)。 * diff --git a/app/PersonGroup.php b/app/PersonGroup.php index 184a6fc..a77577a 100644 --- a/app/PersonGroup.php +++ b/app/PersonGroup.php @@ -24,6 +24,44 @@ final class PersonGroup /** 公共分组(管理员维护,全家共用)的 owner_id */ public const PUBLIC_OWNER = 0; + /** + * 🔴 v63(用户 2026-10-19 第 5 条):「每个标签增加一个颜色。」 + * + * 色板 = 标签可以挑的颜色。与「模块主题色」(`Module::PRESET_COLORS`)**同一条 + * 审美规矩**:中低饱和、中低明度 —— 标签是纸面上的一行字,颜色太艳会盖过内容。 + * + * ⚠️ 挑色是"选"不是"填":界面上只给这些色块(外加一个系统取色器兜底), + * 服务端一律按白名单校验(`sanitizeColor`),别让手改表单塞进 CSS。 + */ + public const SWATCHES = [ + '#A84A28', '#8A6B4B', '#4A7C82', '#7A4E8C', '#3F7D5A', '#B0763A', + '#5566A8', '#A8574F', '#5C8A48', '#6B5B95', '#9A5C7A', '#3D6B8E', + ]; + + /** 颜色白名单:只接受 #rrggbb,其余回退到 $fallback(同 `Nav::safeColor` 的做法) */ + public static function sanitizeColor(string $c, string $fallback): string + { + return preg_match('/^#[0-9a-fA-F]{6}$/', trim($c)) ? strtolower(trim($c)) : $fallback; + } + + /** + * 这个标签"实际显示成什么颜色"。 + * + * ⚠️ 没选过颜色(`color` 为 NULL/空)时**按 id 稳定地**从色板里挑一个 —— + * 不能用"列表里的第几个"来挑:那样改一次顺序,满屏标签颜色全跳一遍, + * 用户会以为自己点错了。用 id 取模则"这个标签一辈子是这个色",直到用户改它。 + */ + public static function swatch(array $g): string + { + $raw = trim((string)($g['color'] ?? '')); + if ($raw !== '') { + return self::sanitizeColor($raw, self::SWATCHES[0]); + } + $id = (int)($g['id'] ?? 0); + $n = count(self::SWATCHES); + return self::SWATCHES[$id % $n]; + } + /** 预置分组的来源在 Schema::DEFAULT_GROUPS(安装与升级共用一份,避免两处漂移) */ /** @@ -185,7 +223,8 @@ final class PersonGroup * @param int $ownerId 0=公共(只有管理员能建,调用方把关);否则=建给自己的 * @return array{ok:bool,id?:int,error?:string} */ - public static function add(int $familyId, string $name, int $ownerId = self::PUBLIC_OWNER): array + public static function add(int $familyId, string $name, int $ownerId = self::PUBLIC_OWNER, + string $color = ''): array { $ownerId = max(0, $ownerId); $name = trim($name); @@ -208,19 +247,28 @@ final class PersonGroup ); $st2->execute([$familyId, $ownerId]); $max = (int)$st2->fetchColumn(); + // 🔴 v63:颜色可空 —— 空串存 NULL(= "还没选过",见 swatch() 的说明)。 + // 传了颜色就按白名单归一(非法值退回空 = 也当没选过,不写脏数据进库)。 + $col = trim($color) === '' ? null : self::sanitizeColor($color, ''); $db->prepare( - 'INSERT INTO person_groups (family_id, owner_id, name, position, is_builtin) - VALUES (?, ?, ?, ?, 0)' - )->execute([$familyId, $ownerId, $name, max($max + 10, 80)]); + 'INSERT INTO person_groups (family_id, owner_id, name, color, position, is_builtin) + VALUES (?, ?, ?, ?, ?, 0)' + )->execute([$familyId, $ownerId, $name, $col, max($max + 10, 80)]); return ['ok' => true, 'id' => (int)$db->lastInsertId()]; } /** * 改名。$ownerId 用来确认"你改的是你自己那一套里的分组"—— * 公共分组只有管理员能改(调用方把关),别人的私自分组连管理员也不该动。 + * + * 🔴 v63:顺手支持改颜色(`$color === null` = **不动颜色**)。 + * ⚠️ 用 null 而不是空串表示"不改":空串是"清掉颜色"这个合法意图 + * (退回按 id 取默认色),两者必须能区分 —— 这是"部分更新"接口 + * 最容易写错的一处(传不传那个字段,语义完全不同)。 * @return array{ok:bool,error?:string} */ - public static function rename(int $familyId, int $id, string $name, int $ownerId): array + public static function rename(int $familyId, int $id, string $name, int $ownerId, + ?string $color = null): array { $ownerId = max(0, $ownerId); $name = trim($name); @@ -243,12 +291,23 @@ final class PersonGroup if ((int)$st->fetchColumn() > 0) { return ['ok' => false, 'error' => '已经有叫「' . $name . '」的分组了。']; } - $up = $db->prepare( - 'UPDATE person_groups SET name = ? WHERE family_id = ? AND owner_id = ? AND id = ?' - ); - $up->execute([$name, $familyId, $ownerId, $id]); + if ($color === null) { + $up = $db->prepare( + 'UPDATE person_groups SET name = ? WHERE family_id = ? AND owner_id = ? AND id = ?' + ); + $up->execute([$name, $familyId, $ownerId, $id]); + } else { + $col = trim($color) === '' ? null : self::sanitizeColor($color, ''); + $up = $db->prepare( + 'UPDATE person_groups SET name = ?, color = ? WHERE family_id = ? AND owner_id = ? AND id = ?' + ); + $up->execute([$name, $col, $familyId, $ownerId, $id]); + } if ($up->rowCount() === 0) { - return ['ok' => false, 'error' => '找不到这个分组。']; + // ⚠️ rowCount()===0 有两种可能:找不到(不可能,上面刚查过)与 + // **改的值和原来一模一样**(MySQL 对无变化的 UPDATE 不计数)。 + // 后者不是错误 —— 用户点了「完成」但什么都没改,不该报"找不到"。 + return ['ok' => true]; } return ['ok' => true]; } diff --git a/app/Schema.php b/app/Schema.php index 702d5d0..0dd5099 100644 --- a/app/Schema.php +++ b/app/Schema.php @@ -375,6 +375,7 @@ CREATE TABLE IF NOT EXISTS `person_groups` ( `family_id` INT UNSIGNED NOT NULL, `owner_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0=全家的公共分组(管理员在系统设置里维护);否则=这个人的私自分组,只有他自己看得到', `name` VARCHAR(20) NOT NULL COMMENT '分组名,如「家人」「同事」', + `color` VARCHAR(7) DEFAULT NULL COMMENT '标签颜色 #rrggbb(v63;NULL=还没选过,界面按顺序取默认色)', `position` INT NOT NULL DEFAULT 100 COMMENT '默认顺序(用户没自定义顺序时按它排)', `is_builtin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1=安装时预置的分组', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -2390,6 +2391,18 @@ SQL); } }); + /* 🔴 v63(用户 2026-10-19 第 5 条):「公共标签这个板块…每个标签增加一个颜色。」 + ⇒ `person_groups` 加 `color`。 + ⚠️ 老库没有这一列,**必须**用 addColumn 显式补(CREATE TABLE IF NOT EXISTS + 对已存在的表什么都不做 —— 这是本文件开头那段的全部意义)。 + ⚠️ 允许 NULL(不是 NOT NULL DEFAULT):NULL = "从来没选过颜色", + 界面按标签顺序从预设色板里挑一个(`PersonGroup::swatch()`)。 + 给了 DEFAULT 就分不清"用户故意选了那个色"和"系统默认给的"。 + ⚠️ 位置:`ADD COLUMN ... AFTER \`name\`` —— 列顺序影响不了程序, + 但影响"人看 information_schema 时的心智",放在名字后面最自然。 */ + self::addColumn($db, 'person_groups', 'color', + "ADD COLUMN `color` VARCHAR(7) DEFAULT NULL COMMENT '标签颜色 #rrggbb(v63;NULL=还没选过,界面按顺序取默认色)' AFTER `name`"); + // 被容错跳过的数据步骤记进设置,系统设置页会把它显出来 —— // 不然「升级没跑完」这件事只活在日志里,跟没发生一样。 if (self::$stepErrors) { diff --git a/assets/css/app.css b/assets/css/app.css index b7a0b4f..c7ad2f5 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -1423,6 +1423,36 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb, .order-move .btn { padding: 2px 8px; line-height: 1.4; } .order-move .btn[disabled] { opacity: .35; cursor: default; } +/* ---- 🔴 v63(用户 2026-10-19 第 5 条):公共标签的颜色 ---- + 一行 = 序号 · 色块 · 名字 · N人 · ▲▼ · 编辑。 */ +.pg-dot { + flex: 0 0 auto; width: 12px; height: 12px; border-radius: 50%; + box-shadow: inset 0 0 0 1px rgba(0,0,0,.12); /* 浅色底上也要看得见边界 */ +} +/* 色板(弹框里):点一下就选。⚠️ 选中态用 `is-on` 描一圈, + 不与 `.is-on` 的通用含义冲突(本页没有别的 .is-on)。 */ +.pg-swatches { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 6px; } +.pg-sw { + width: 26px; height: 26px; border-radius: 7px; cursor: pointer; + border: 2px solid transparent; padding: 0; + box-shadow: inset 0 0 0 1px rgba(0,0,0,.12); +} +.pg-sw.is-on { border-color: var(--ink); } +.pg-sw:hover { transform: scale(1.06); } +/* 自定义取色器:与色块同尺寸,外观上就是色板里的第 13 格 */ +.pg-sw-custom { + width: 26px; height: 26px; border-radius: 7px; overflow: hidden; cursor: pointer; + box-shadow: inset 0 0 0 1px rgba(0,0,0,.12); + display: inline-flex; +} +.pg-sw-custom input[type="color"] { + width: 40px; height: 40px; margin: -7px; border: 0; padding: 0; cursor: pointer; background: none; +} +/* 新建时那个颜色下拉(窄屏缩一半,别把"加一个"按钮挤到下一行) */ +.pg-swatch-pick { flex: 0 0 auto; max-width: 96px; } +/* 弹框底栏:删除在左、取消/完成在右 —— 与"危险动作离确认按钮远一点"的通行做法一致 */ +.rm-foot-gap { flex: 1 1 auto; } + /* ---------------- 个人设置:关系分类管理 ---------------- */ .inline-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } .inline-form .input { flex: 1 1 200px; max-width: 320px; } @@ -4520,7 +4550,11 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b ⚠️ 白字压浅色本的兜底:加一层极轻的 text-shadow(不加深色罩,保持本色)。 */ .ag-ev { position: absolute; left: 46px; right: 4px; - display: block; overflow: hidden; padding: 3px 7px; + /* 🔴 v63(用户 2026-10-19 第 2 条):日历本要显示在色块**右侧** ⇒ 本体必须是 + 一行弹性容器。⚠️ 纵向排列时(`flex-direction: column`)`block` 子元素会各占一行, + 日历本必然掉到第二行;色块很矮时就整条看不见。 */ + display: flex; align-items: center; gap: 6px; + overflow: hidden; padding: 3px 7px; background: var(--brand); border-radius: 4px; font-size: 12px; line-height: 1.35; color: #fff; text-decoration: none; box-sizing: border-box; @@ -4528,10 +4562,23 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b text-shadow: 0 1px 1px rgba(0,0,0,.18); } .ag-ev:hover { text-decoration: none; filter: brightness(1.08); z-index: 2; } -.ag-ev .ag-ev-t { display: block; font-weight: 600; font-variant-numeric: tabular-nums; margin-right: 4px; float: left; } -.ag-ev .ag-ev-w { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; } -/* 日历本名:色块右下角的极淡小字(色块面积小,不加大徽标) */ -.ag-ev .ag-ev-cal { display: block; font-size: 10.5px; opacity: .82; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* 「时间 + 标题」这一坨占满剩余宽度;`min-width:0` 是标题能被省略号截断的前提 + (没有它,flex 子项按内容撑宽 ⇒ 整行溢出、日历本被挤出可视区)。 */ +.ag-ev .ag-ev-main { + display: flex; align-items: baseline; gap: 4px; + flex: 1 1 auto; min-width: 0; +} +.ag-ev .ag-ev-t { flex: 0 0 auto; font-weight: 600; font-variant-numeric: tabular-nums; } +.ag-ev .ag-ev-w { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; } +/* 日历本名:色块**右侧**的极淡小字(色块面积小,不加大徽标)。 + ⚠️ 它 `flex: 0 0 auto`(**不参与收缩**):用户要的就是"看得见日历本" —— + 让它跟着一起缩,色块一窄就又没了。改为**标题**让步(`.ag-ev-main` 可缩到 0)。 + 同时给它 `max-width: 45%` 兜底:日历本名过长时自己截断,别把标题吃光。 */ +.ag-ev .ag-ev-cal { + flex: 0 0 auto; font-size: 10.5px; opacity: .82; + max-width: 45%; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} /* ---- 任务行:标题 + 状态 + **看板名** + 截止 ---- */ .tk-list { display: flex; flex-direction: column; } @@ -6090,15 +6137,26 @@ h5.md-h, h6.md-h { font-size: 14px; } pointer-events: auto; cursor: pointer; } .trip-pin-card { + display: flex; align-items: center; gap: 8px; /* 🔴 v63:题图在左、文字在右 */ background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--brand); border-radius: 8px; padding: 6px 10px; box-shadow: 0 2px 8px rgba(0,0,0,.10); - max-width: 190px; white-space: nowrap; + max-width: 220px; } +/* 🔴 v63(用户 2026-10-19 第 3 条):旅程题图。 + ⚠️ `object-fit: cover` 必须有:题图原图长宽比五花八门,默认 `fill` 会把人拉扁。 + ⚠️ 有题图时卡片最多 220px(原来是 190px),文字那一块要 `min-width:0` + 才能让 `.trip-pin-title` 的省略号真正生效(否则长标题把卡片撑破)。 */ +.trip-pin-thumb { + flex: 0 0 auto; width: 40px; height: 40px; + border-radius: 5px; overflow: hidden; background: var(--line-2); +} +.trip-pin-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; } +.trip-pin-body { display: block; min-width: 0; } .trip-pin-title { font-size: 13px; font-weight: 600; color: var(--ink); - overflow: hidden; text-overflow: ellipsis; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .trip-pin-sub { font-size: 11.5px; color: var(--muted); margin-top: 1px; } .trip-pin:hover .trip-pin-card { border-color: var(--brand); box-shadow: 0 4px 14px rgba(0,0,0,.16); } diff --git a/assets/js/trip_map.js b/assets/js/trip_map.js index 1a133d7..1d795fb 100644 --- a/assets/js/trip_map.js +++ b/assets/js/trip_map.js @@ -76,12 +76,21 @@ dx = Math.cos(ang) * rad; dy = Math.sin(ang) * rad; } + /* 🔴 v63(用户 2026-10-19 第 3 条):「旅程地图模式,应该显示旅程的题图。」 + ⇒ 卡片左侧一枚 40×40 的题图(`trip_details.cover_key` → 服务端算好的 URL, + 见 _map_body.php)。没有题图就**只显示文字**,不摆灰色占位方块 + (地图上每枚卡片都带一个灰框,整张图会显得脏)。 */ + var thumb = p.cover + ? '' + : ''; var html = '
' + '
' + + thumb + + '' + '
' + esc(p.title) + '
' + (p.when ? '
' + esc(p.when) + '
' : '') + (p.city ? '
📍 ' + esc(p.city) + '
' : '') - + '
'; + + ''; var mk = new AMap.Marker({ // ⚠️ 必须是 [lng, lat] 数组(见文件头的说明) position: [p.lng, p.lat], diff --git a/index.php b/index.php index 8be00d8..724d06d 100644 --- a/index.php +++ b/index.php @@ -313,17 +313,15 @@ function render_me_settings(array $person, array $errors = []): void // 通讯录同步范围要和名册页**完全一致**(用户 2026-10-06 第 22 批确认): // 名册页看到谁就同步谁。这里把人数带过去,用户当场就能对上。 'davRosterN' => PersonRoster::countFor($familyId, $me), - /* 🔴 v44(用户 2026-10-12 第 6 条):移动端 API 令牌。 - ⚠️ 明文同样只在**一次性会话槽位**里取一次(`api_plain`), - 取完后面 unset —— 与 DAV 口令同一条纪律。 */ - 'apiTokens' => \App\ApiToken::listFor($me), - 'apiPlain' => (string)($_SESSION['api_plain'] ?? ''), - 'apiLabel' => (string)($_SESSION['api_plain_label'] ?? ''), - 'apiBase' => rtrim((string)fb_config('base_url', ''), '/'), + /* 🔴 v63(用户 2026-10-19 第 7 条):`apiTokens` / `apiPlain` / `apiLabel` / `apiBase` + 四个变量**随"手动签发令牌"那块一起撤掉** —— 界面已经不渲染它们, + 还每次进设置页查一遍令牌表只是白花钱(`listFor` 是一次真实查询)。 + ⚠️ 但 `api_plain` 那两个会话槽位**仍然要 unset**(见本函数尾部): + 用户可能在升级前那一刻生成过令牌、明文还挂在会话里, + 不清掉它就等于这页永远留着一份能看的明文。 */ /* 🔴 v57(用户 2026-10-17):「在个人设置里面有这个设备管理」。 ⚠️ 这里传的是**按设备聚合**的列表(`listDevices`),不是令牌清单 —— - 同一台手机重登过 5 次,网页上只该出现 1 行。 - 与上面 `apiTokens`(原始令牌行、只给"手动签发/吊销"用)不是一回事。 */ + 同一台手机重登过 5 次,网页上只该出现 1 行。 */ 'devices' => \App\ApiToken::listDevices($me), 'deviceMax' => \App\ApiToken::MAX_DEVICES, 'deviceTtl' => \App\ApiToken::TTL_HOURS, @@ -1925,7 +1923,12 @@ switch ($r) { } else { $owner = (int)Auth::personId(); } - $res = PersonGroup::add((int)Auth::familyId(), (string)($_POST['name'] ?? ''), $owner); + // 🔴 v63:颜色可随新建一起给(`settings/groups/add` 的公共标签加色块选择)。 + // ⚠️ 没传 `color` 这个键 = 不设颜色(存 NULL,界面按 id 取默认色); + // 传了空值 = 用户主动选了"不要颜色"⇒ 同样存 NULL。 + // 两种都落到同一个值上,所以这里不用像 rename 那样区分 null / ''。 + $res = PersonGroup::add((int)Auth::familyId(), (string)($_POST['name'] ?? ''), $owner, + (string)($_POST['color'] ?? '')); flash($res['ok'] ? 'ok' : 'err', $res['ok'] ? '已加上分组「' . trim((string)($_POST['name'] ?? '')) . '」。' : (string)($res['error'] ?? '加不上。')); @@ -1946,8 +1949,16 @@ switch ($r) { } else { $owner = (int)Auth::personId(); } + /* 🔴 v63(用户 2026-10-19 第 5 条):改名与改颜色走**同一个动作** + (「每个标签增加一个颜色。如果要编辑,就点击这个标签后面的编辑按钮, + 然后弹出一个框,编辑后点击完成就行了」)。 + ⚠️ 用 `array_key_exists` 判断"表单里到底有没有这个字段", + 不能用 `!== ''` —— 空值是一个**合法输入**(= 清掉颜色), + 用 `!== ''` 就再也清不掉了。 */ + $hasColor = array_key_exists('color', $_POST); + $newColor = $hasColor ? (string)$_POST['color'] : null; $res = PersonGroup::rename((int)Auth::familyId(), (int)($_POST['id'] ?? 0), - (string)($_POST['name'] ?? ''), $owner); + (string)($_POST['name'] ?? ''), $owner, $newColor); flash($res['ok'] ? 'ok' : 'err', $res['ok'] ? '分组名已改。' : (string)($res['error'] ?? '改不了。')); @@ -5158,17 +5169,28 @@ $want = array_keys($want); /* 🔴 v44(用户 2026-10-12 第 6 条):移动端 API 令牌的**签发 / 吊销**。 与 DAV 那两条同一个形态(明文进一次性会话槽位、只显示一次)。 */ + /* 🔴🔴 v63(用户 2026-10-19 第 7 条):**手动签发令牌这条入口整条下线**。 + 用户原话:「个人设置中:手动签发接口令牌,这一部分删除,不允许用户修改, + 不允许用户手动签发令牌,只能通过登录后,系统自动签发令牌。」 + + ⇒ `settings/api_token`(生成)**不再接受提交**:界面上那块已经删了; + 若只删界面不删路由,它就变成一个"找不到按钮、但知道地址就能用"的后门 —— + 那种入口比显式按钮危险。这里直接 302 回个人设置并给一句明确的话。 + ⚠️ **`settings/api_revoke` 保留**:踢掉设备是用户该有的能力, + 「设备管理」卡里那两个动作(移除一台 / 退出全部设备)还在用它。 + 换句话说:**签发只自动、吊销仍可用**,这条线才是用户要的形态。 + ⚠️ 令牌机制本身(ApiToken 类、Bearer 校验、设备聚合)一个字没改。 */ case 'settings/api_token': + Auth::requireLogin(); + flash('err', '令牌由登录后自动签发,不能手动生成。要清掉某台设备,去「设备管理」移除它。'); + redirect('settings/me'); + break; + case 'settings/api_revoke': $me = (int)Auth::personId(); Auth::requireWrite(); csrf_check(); - if ($r === 'settings/api_token') { - $label = trim((string)($_POST['label'] ?? '')); - $_SESSION['api_plain'] = \App\ApiToken::issue($me, $label); - $_SESSION['api_plain_label'] = $label; - flash('ok', 'API 令牌已生成(只显示这一次,请立刻复制到 App 里)。'); - } elseif ((int)($_POST['id'] ?? 0) > 0) { + if ((int)($_POST['id'] ?? 0) > 0) { \App\ApiToken::revoke($me, (int)$_POST['id']); flash('ok', '这枚 API 令牌已吊销。'); } else { @@ -5176,7 +5198,7 @@ $want = array_keys($want); unset($_SESSION['api_plain'], $_SESSION['api_plain_label']); flash('ok', '全部 API 令牌已吊销,所有 App 都需要重新登录。'); } - redirect('settings/me', [], 'api'); + redirect('settings/me', [], 'devices'); break; /* 🔴 v57(用户 2026-10-17):「设备管理」——把某一台设备远程退掉。 diff --git a/templates/home.php b/templates/home.php index 9a9d594..56447d9 100644 --- a/templates/home.php +++ b/templates/home.php @@ -144,10 +144,12 @@ continue; // 注册了但还没写模板:跳过,不要白屏 } $hcMore = (string)($hcDef['route'] ?? '') !== '' ? url((string)$hcDef['route']) : ''; - // 日程/任务这两个按钮沿用模块主题色(v28 第 2、3 条) - $hcColor = in_array($hcKey, ['agenda', 'tasks'], true) - ? \App\Module::color($hcKey === 'agenda' ? 'calendar' : 'tasks', (int)\App\Auth::familyId()) - : ''; + /* 🔴 v63(用户 2026-10-19 第 1 条):「卡片右上角的按钮,应该跟这个卡片 + 来自于什么模块的主题色一样才对。」 + ⇒ 颜色从**卡片登记表**取(`HomeCards::module()`),不再在这里硬编码白名单。 + 日程卡来自 calendar、任务卡来自 tasks、观影来自 movies…… 一张不漏。 */ + $hcMod = \App\HomeCards::module($hcKey); + $hcColor = $hcMod !== '' ? \App\Module::color($hcMod, (int)\App\Auth::familyId()) : ''; ?>
diff --git a/templates/home/_card_agenda.php b/templates/home/_card_agenda.php index 566eda3..ee13406 100644 --- a/templates/home/_card_agenda.php +++ b/templates/home/_card_agenda.php @@ -147,8 +147,16 @@ unset($evRef); width: calc(% - 4px);" title="" data-cal=""> - - + + + + + diff --git a/templates/settings.php b/templates/settings.php index 4477808..4537920 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -14,16 +14,20 @@ ?>
-

家庭 首页上那一行小字

+ +

系统标语

- + - - 每个人都可以在「我的资料」里写**自己的一句**,有的话就显示他自己的; - 没写的人才用这里的这句。所以这是一句"兜底",不是全家的统一口号。 -
- -
-
- 上次结构升级有步骤失败了(其余步骤已经跑完,站点可用):
-
- 修好之后可以清掉这条提示。 -
-
- - ` + `` 会让页面 DOM 翻几倍 + (本项目在名册页踩过:2369 个 form 把浏览器拖卡)。 + ⚠️ 表单里留一个原生「完成」按钮并**不依赖 JS 才能提交**:脚本没跑时 + 行上的「编辑」按钮就是一个 `` 到锚点,弹框默认可见(`
+ + + + +
@@ -313,7 +435,13 @@ $pubTotal = count($pubGroups);
-

运行环境 排错时看这一块

+ +

运行环境

PHP
数据库
@@ -330,10 +458,6 @@ $pubTotal = count($pubGroups);
附件出口
统一走 media.php 代理 (本地与云端同一形态,需登录才能读)
-
-
亲属连线
-
条
-
账号
diff --git a/templates/settings/me.php b/templates/settings/me.php index 5b25310..2ae0c9a 100644 --- a/templates/settings/me.php +++ b/templates/settings/me.php @@ -246,87 +246,22 @@ $rosterCount = $rosterCount ?? 0;
-
-

- 手动签发接口令牌 - 第三方脚本 / 调试用;App 正常登录**不需要**这里 -

-

- 接口地址前缀 /api/v1/, - 请求头带 Authorization: Bearer <令牌>。 - 先调 GET /api/v1/ping 探连通(免令牌)。 -
完整接口清单见仓库里的 docs/API.md。 -

+ -
- - -
-
- - - 令牌本身就是凭据,这一行只是给你对账用。 -
-
- - -

- 这是你的新令牌,只显示这一次,请立刻复制到 App 里: - - (设备名:) - -
-

- - - - - - - - - - - - - - - - - -
设备名签发时间最近使用
- - - - - -
- -

还没有手动签发的令牌。App 登录走上面的「设备管理」,不用在这里生成。

- - - -
-
- - - -
- -
- - -
- -
-

- 令牌只在生成那一次显示明文(库里只存哈希)。手机丢了就吊销那一枚。 -

- - + ⇒ 这一整块(接口根地址说明 + 用户名对账行 + 明文令牌回显 + 已签发列表 + + 「生成新令牌」/「吊销全部」两个按钮)**全部撤掉**。 + ⚠️ 令牌机制本身一点没动:`api_tokens` 表、`Authorization: Bearer` 校验、 + `settings/device_remove`(按设备踢)全都还在 —— 撤掉的只是"人手签发"这条入口。 + 令牌从此只有一个来源:**App 登录成功后由服务端自动签发**(见 ApiTokens::issue + 在登录流程里的调用点),登记成一台设备,在上一张「设备管理」卡里可见可踢。 + ⚠️ 路由 `settings/api_token`(生成)也必须一起下线,否则它就是一个 + "界面上找不到、但知道地址就能用"的后门 —— 那种入口比显式按钮危险得多。 + 保留 `settings/api_revoke`:**踢设备**是用户该有的能力(同上卡在用)。 + ⚠️ 别把这张卡改成"只读展示已签发令牌":那等于把"哪些设备在用我的账号" + 又摆第二遍 —— 上面「设备管理」已经是那个清单了(按设备聚合,比按令牌行更准)。 */ ?> \App\Trip::activitySpan($row['starts_at'] ?? null, $row['ends_at'] ?? null), 'city' => (string)($row['city'] ?? ''), 'url' => url('trips/show', ['id' => (int)$row['id']]), + // 🔴 v63 第 3 条:题图。⚠️ 用 `media_url()` 而不是直接拼 storage 路径 —— + // 图片统一走 media.php 代理(桶不必开公共读),且**必须带版本号**, + // 否则换了题图缓存里还是老图。 + 'cover' => !empty($row['cover_key']) ? media_url((string)$row['cover_key']) : '', ]; } ?>