v63:卡片配色 / 日历本靠右 / 地图题图 / 设置页清理 / 公共标签颜色 / 令牌下线

- 卡片右上角按钮改用「来自哪个模块」的主题色: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 四个变量一并撤掉
This commit is contained in:
2026-10-19 17:27:37 +08:00
parent 2e40133d8f
commit 2f7dc6ca23
11 changed files with 450 additions and 189 deletions
+27 -3
View File
@@ -43,47 +43,71 @@ final class HomeCards
public const HEIGHTS = [2, 4, 6, 8];
/**
* 卡片注册表:key => [名字, 说明, 默认宽, 默认高, 详情页路由, 按钮文字, 是否有"显示几条"]
* 卡片注册表:key => [名字, 说明, 默认宽, 默认高, 详情页路由, 按钮文字, 是否有"显示几条", 来自哪个模块]
*
* ⚠️ key 就是模板名(`_card_<key>.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'] ?? '');
}
/**
* 默认布局(第一次打开首页的人看到的样子)。
*
+69 -10
View File
@@ -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];
}
+13
View File
@@ -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) {