v62:纪念日列表修正 / 分类图标与设置菜单 / 首页卡片体系
- 纪念日列表:display:flex 从 td 移到内层 div(写在 td 上会破坏表格列算法, 表现为关联人列与其它列高度不一致);行内列序按表头对齐; 改完再点「新建」不再带出上次修改的数据;文案改为「与我相关」「来自名册」; 别人记的、把我挂成关联人的条目一并显示(split() 返回 hiddenIds, 路由层据此过滤「接下来的日子」,与两张表同一套可见性) - 分类图标:新增 app/AnnivIcon.php(17 个行内 SVG 图标 + 同 key 的 emoji 供日历纯文本处使用,内网无 CDN 不用 FontAwesome);去掉「看日历」按钮; 设置按钮改为菜单,第一项进 days/settings 独立页;分类可自选图标, 列表徽标与日历条目都按分类显示 - 首页卡片:新增 app/HomeCards.php 与 templates/home_cards.php; 8 格宽 × 高 2/4/6/8;首页拆成 templates/home/_card_*.php 共 8 个片段; home/cards 可改尺寸、调顺序、开关
This commit is contained in:
@@ -1,63 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* 纪念日模块的「设置」下拉(v24 用户 2026-10-03 第 9 条)
|
||||
* 纪念日模块的「设置」下拉(v24 用户 2026-10-03 第 9 条;🔴 v62 改成菜单)。
|
||||
*
|
||||
* 用户原话:「在新建纪念日和右侧,也增加设置按钮,可以设置纪念日的分类。」
|
||||
* v24 原话:「在新建纪念日和右侧,也增加设置按钮,可以设置纪念日的分类。」
|
||||
* —— 当时做成了"下拉里直接铺一张分类增删表单"。
|
||||
*
|
||||
* 分类清单是**家庭级**的(settings.anniv_kinds)—— 全家共用一套叫法,
|
||||
* 不然「结婚纪念日」在两个人屏幕上是两套名字,看的时候对不上。
|
||||
* 所以这里跟「模块主题色」一样:**能改的是所有人**,但改的是同一份。
|
||||
* 🔴 v62(用户 2026-10-18 第 2 条):「设置按钮,点击后弹菜单,**第一个菜单是分类设置**,
|
||||
* 进去之后可以设置分类,每一类纪念日都有不同的图标……」
|
||||
* ⇒ 与日程/观影/任务三个模块**统一手感**(☰ → 一串列表 → 点进独立页):
|
||||
* 下拉里**不放表单**,只放一条条目的链接;分类的增删改与图标选择都在
|
||||
* `days/settings` 那一页上(见 templates/days/settings.php)。
|
||||
*
|
||||
* 为什么必须这么改(与日程那条同一个理由,别丢):分类是**家庭级**的,
|
||||
* 一个家能有十几二十个分类,每个分类还要选图标 —— 全铺在下拉里,
|
||||
* 下拉会被撑到屏幕外,而"设置"按钮点开本来就该是"一个目录"。
|
||||
*
|
||||
* 用法(require 前 set 好):
|
||||
* $kinds 当前分类清单
|
||||
* $kindCounts 每个分类用了几条(显示"这个分类有没有在用")
|
||||
* $setCurrent 当前页 key('kinds')用来高亮;'' 也行(从别处挂过来时没有当前项)
|
||||
*/
|
||||
$daySetKinds = (array)($kinds ?? \App\Anniversary::PRESET_KINDS);
|
||||
$daySetCounts = (array)($kindCounts ?? []);
|
||||
$daySetCanWrite = \App\Auth::canWrite();
|
||||
$setCurrent = (string)($setCurrent ?? '');
|
||||
$setOn = static function (string $key) use ($setCurrent): string {
|
||||
return $setCurrent === $key ? ' class="on"' : '';
|
||||
};
|
||||
$setIcon = static function (string $name): string {
|
||||
$paths = [
|
||||
// 分类:三个小方块("一格一类")
|
||||
'kinds' => '<rect x="2" y="2.2" width="5" height="5" rx="1.2"/>'
|
||||
. '<rect x="9" y="2.2" width="5" height="5" rx="1.2"/>'
|
||||
. '<rect x="2" y="9" width="5" height="5" rx="1.2"/>'
|
||||
. '<rect x="9" y="9" width="5" height="5" rx="1.2"/>',
|
||||
];
|
||||
$d = $paths[$name] ?? '';
|
||||
if ($d === '') {
|
||||
return '';
|
||||
}
|
||||
return '<svg class="mi" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"'
|
||||
. ' fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"'
|
||||
. ' stroke-linejoin="round">' . $d . '</svg>';
|
||||
};
|
||||
?>
|
||||
<details class="movie-set menu ann-set">
|
||||
<summary class="btn btn-icon" title="纪念日设置" aria-label="纪念日设置"><svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/></svg></summary>
|
||||
<div class="menu-list ann-set-list">
|
||||
<div class="cal-set-h">纪念日分类(全家共用一套)</div>
|
||||
|
||||
<?php if (!$daySetKinds): ?>
|
||||
<p class="small muted" style="margin:0 0 8px">一个分类都没有。下面加一个。</p>
|
||||
<?php else: ?>
|
||||
<ul class="ann-kind-list">
|
||||
<?php foreach ($daySetKinds as $k): ?>
|
||||
<li>
|
||||
<span class="ann-kind-name"><?= e((string)$k) ?></span>
|
||||
<span class="small muted"><?= (int)($daySetCounts[(string)$k] ?? 0) ?> 条在用</span>
|
||||
<?php if ($daySetCanWrite): ?>
|
||||
<form method="post" action="<?= e(url('days/kinds')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="del" value="<?= e((string)$k) ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
title="删掉这个分类(已用它的纪念日不变)">删</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($daySetCanWrite): ?>
|
||||
<form method="post" action="<?= e(url('days/kinds')) ?>" class="led-set-form-in" style="margin-top:8px">
|
||||
<?= csrf_field() ?>
|
||||
<input class="input" name="add" maxlength="20" placeholder="新分类,如:相识纪念日">
|
||||
<button class="btn btn-sm" type="submit">加分类</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="small muted" style="margin:8px 0 0">
|
||||
删掉一个分类**不会**改已用过它的纪念日 —— 只是新建时不再出现在下拉里。
|
||||
</p>
|
||||
<?php /* 第一项(也是目前唯一一项):分类设置 —— 名称 + 图标 + 删除 + 排序 */ ?>
|
||||
<a href="<?= e(url('days/settings')) ?>"<?= $setOn('kinds') ?>><?= $setIcon('kinds') ?><span>分类设置</span></a>
|
||||
</div>
|
||||
</details>
|
||||
<?php
|
||||
// ⚠️ 只 unset **本片段自己定义**的那三个前缀变量。
|
||||
// 绝不能再写 `unset($kinds, $kindCounts, $canWrite)` —— days/index.php 里
|
||||
// 同样有 $canWrite(还有 $kinds),那样会把宿主模板的变量一起干掉,
|
||||
// 后面每一处用到的地方都变成 Undefined variable(账本那个片段刚这么炸过一次)。
|
||||
unset($daySetKinds, $daySetCounts, $daySetCanWrite);
|
||||
// ⚠️ 只 unset **本片段自己定义**的变量,绝不碰宿主的 $setCurrent / $kinds / $canWrite。
|
||||
unset($setCurrent, $setOn, $setIcon);
|
||||
|
||||
+14
-5
@@ -30,11 +30,20 @@ $kinds = $kinds ?? null;
|
||||
$e0 = $editing ?: [];
|
||||
$v = static fn(string $k, $def = '') => $e0[$k] ?? $def;
|
||||
$repeat = (string)($e0['repeat_type'] ?? 'solar');
|
||||
// 用户 2026-10-03 第 9 条:分类下拉的选项来自**可配置**的分类清单
|
||||
$kinds = (array)($kinds ?? \App\Anniversary::PRESET_KINDS);
|
||||
/* 用户 2026-10-03 第 9 条:分类下拉的选项来自**可配置**的分类清单。
|
||||
🔴 v62:清单改成"名称 + 图标"(`kindsFull`)。下拉里的 `<option>` 放不进 SVG,
|
||||
所以这里用同一图标的 emoji 形态打头(与日历里一致)—— 用户一眼能对上
|
||||
"下拉里选的那个"和"列表/日历上显示的那个"是同一个。 */
|
||||
$kindsFull = (array)($kindsFull ?? \App\Anniversary::kindsFull((int)\App\Auth::familyId()));
|
||||
$kinds = array_column($kindsFull, 'name');
|
||||
$curKind = (string)($e0['kind'] ?? '');
|
||||
if ($curKind !== '' && !in_array($curKind, $kinds, true)) {
|
||||
array_unshift($kinds, $curKind);
|
||||
// 老数据里的分类名可能已经从清单里删掉了 —— 仍然列出来,否则一保存就被清空
|
||||
array_unshift($kindsFull, [
|
||||
'name' => $curKind,
|
||||
'icon' => \App\Anniversary::defaultIcon($curKind),
|
||||
]);
|
||||
$kinds = array_column($kindsFull, 'name');
|
||||
}
|
||||
|
||||
/* 🔴 v41 弹框开关(与观影/旅程同一套命名习惯):
|
||||
@@ -108,8 +117,8 @@ $backUrl = url('days');
|
||||
<span class="mv-lb">分类</span>
|
||||
<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 foreach ($kindsFull as $kf): ?>
|
||||
<option value="<?= e((string)$kf['name']) ?>" <?= $curKind === (string)$kf['name'] ? 'selected' : '' ?>><?= e(\App\AnnivIcon::emo((string)$kf['icon']) . ' ' . (string)$kf['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
+110
-31
@@ -13,20 +13,39 @@
|
||||
*
|
||||
* ⇒ 三块(顺序按用户第 9 条要求):
|
||||
* ① 接下来的日子(未来 60 天)
|
||||
* ② **手动添加的**纪念日(在上)—— 可改可删;右侧按钮:改=蓝(.btn)、删=黄(.btn-warn)
|
||||
* ③ 来自名册生日的(在下,实时派生、不能改删)
|
||||
* ② **与我相关**的纪念日(在上)—— 可改可删;右侧按钮:改=蓝(.btn)、删=黄(.btn-warn)
|
||||
* ③ 来自名册的生日(在下,实时派生、不能改删)
|
||||
*
|
||||
* @param array $custom 用户自建的(行上带 person_ids)
|
||||
* 🔴 v62(用户 2026-10-18 第 1、2 条)改了四处:
|
||||
* ① 两块标题:「我自己添加的」→「与我相关」、「来自名册的」→「来自名册」,
|
||||
* 并**删掉**那句"这一块是实时派生的…"的说明;
|
||||
* ② 「关于我相关」这一块要收进"别人添加、但把我也挂成关联人"的条目
|
||||
* (判据见 `Anniversary::split()`);
|
||||
* ③ 「看日历」按钮去掉(日历在顶栏就有,页面里再放一个只是多一个入口);
|
||||
* ④ 分类徽标带图标(`AnnivIcon`),日历里用同一图标的 emoji 形态。
|
||||
*
|
||||
* @param array $custom 与我相关的(行上带 person_ids)
|
||||
* @param array $derived 从名册生日派生的(行上也有 person_ids)
|
||||
* @param int $hidden 别人记的、与我无关的条数(只用于一句小字说明)
|
||||
* @param array $upcoming 接下来 60 天的
|
||||
* @param array $kinds 分类清单(设置里维护)
|
||||
* @param array $people 名册(把 person id 换成头像 + 称呼)
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$kindBadge = static function (array $r): string {
|
||||
$famId = (int)\App\Auth::familyId();
|
||||
/* 分类徽标:图标 + 名字(v62 第 2 条「列表中…要根据分类,显示不同的图标」)。
|
||||
⚠️ 图标走 `Anniversary::iconOf()`(单点来源)—— 不要在模板里再写一遍
|
||||
"生日用蛋糕、忌日用蜡烛"的映射,否则设置页改了图标这里不跟着变。 */
|
||||
$kindBadge = static function (array $r) use ($famId): string {
|
||||
$k = (string)($r['kind'] ?? '');
|
||||
return $k === '' ? '' : '<span class="node-badge">' . e($k) . '</span>';
|
||||
if ($k === '') {
|
||||
return '';
|
||||
}
|
||||
return '<span class="node-badge kind-badge">'
|
||||
. \App\AnnivIcon::svg(\App\Anniversary::iconOf($famId, $k), 13, 'kind-ic')
|
||||
. '<span class="kind-name">' . e($k) . '</span></span>';
|
||||
};
|
||||
|
||||
$srcWord = static function (array $r): string {
|
||||
$src = (string)($r['source'] ?? 'custom');
|
||||
if ($src === 'birthday_solar') {
|
||||
@@ -105,10 +124,24 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="ann-people"><?= $peopleCell($r) ?></td>
|
||||
<?php /* 🔴🔴 v62(用户 2026-10-18 第 1 条):「我自己添加的部分,现在显示有错位,
|
||||
好像关联人和其他列,高度不一样。」
|
||||
⚠️ 这里有**两个**独立的错位,别只修一个:
|
||||
① 高度错位(已修):这两格原来把 `display:flex` 直接写在 **`<td>`** 上
|
||||
(`.ann-people` / `.ann-acts`)。`display` 一改成 flex,这个元素
|
||||
**就不再是 table-cell** —— 它从表格的列算法里脱了出来,
|
||||
于是整行的列宽/基线全乱(同一行里有的格按表格排、有的格按 flex 排)。
|
||||
⇒ `<td>` 恢复成普通单元格,flex 放到**里面一层 div** 上。
|
||||
② **内容错位(本次修)**:表头写的是「关联人 | 分类 | 重复」,
|
||||
但行里依次塞的是「分类 | 重复 | 关联人」——
|
||||
三列的内容整体往左挪了一格,用户看到的"关联人"那一栏里其实是分类。
|
||||
⇒ **表头保持原顺序不动**(「关联人」在「分类」前是用户当初指定的),
|
||||
把**行里的顺序**改成跟表头一致:
|
||||
日期 | 纪念日 | **关联人** | **分类** | 重复。 */ ?>
|
||||
<td><div class="ann-people"><?= $peopleCell($r) ?></div></td>
|
||||
<td><?= $kindBadge($r) ?></td>
|
||||
<td class="small muted"><?= e($srcWord($r)) ?></td>
|
||||
<td class="nowrap ann-acts">
|
||||
<td class="nowrap"><div class="ann-acts">
|
||||
<?php if ($editable && $canWrite): ?>
|
||||
<?php /* 用户第 2 条:「右侧按钮,和其他模块的按钮样式保持一致,
|
||||
修改用蓝色,删除用黄色。」
|
||||
@@ -135,7 +168,7 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
||||
<?php else: ?>
|
||||
<span class="small muted">来自名册</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</div></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
@@ -156,8 +189,12 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
||||
data-href="<?= e(url('days/new')) ?>"
|
||||
aria-haspopup="dialog" aria-expanded="false">新建纪念日</button>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar')) ?>">看日历</a>
|
||||
<?php /* 用户第 9 条:设置按钮就在「新建纪念日」右边(☰,排最右) */ ?>
|
||||
<?php /* 🔴 v62(用户 2026-10-18 第 2 条):「纪念日,看日历这个按钮去掉。」
|
||||
理由(用户没说的那一半):日历在**顶栏**上一直有,这一页再放一个
|
||||
"看日历"只是同一件事的第二入口,还占掉了本该给「设置」的位置。
|
||||
⚠️ 不要再"改个名字留着" —— 用户说的是去掉。 */ ?>
|
||||
<?php /* 用户第 9 条:设置按钮就在「新建纪念日」右边(☰,排最右)。
|
||||
v62:点开是**菜单**,第一项「分类设置」(可给每个分类配图标)。 */ ?>
|
||||
<?php $setCurrent = 'kinds'; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,12 +251,34 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
||||
(container || document.body).appendChild(s);
|
||||
s.parentNode.removeChild(s);
|
||||
}
|
||||
function loadEdit(url) {
|
||||
/* 🔴🔴 v62(用户 2026-10-18 第 1 条):「如果我修改的一个纪念日,然后再点击新建纪念日,
|
||||
竟然把我刚才修改的纪念日带进来了,其实不是新建,而是修改。」
|
||||
|
||||
真根因:**「修改」和「新建」共用同一个 `#anNewCard`**(v43 那轮为了"编辑也开弹框"
|
||||
刻意做的)。而「修改」是把 `days/edit?embed=1` 吐回来的那块**替换**掉当前的卡片 ——
|
||||
替换之后,卡片里已经躺着一份带 `input[name=id]` 的编辑表单。
|
||||
这时点「新建」,旧代码只做了一件事:`setOpen(true)`(把同一张卡片显示出来),
|
||||
于是屏幕上就是"刚才那条被编辑的纪念日",按确定也仍然是**更新**而不是新增。
|
||||
|
||||
⇒ 修法:开「新建」之前先看这张卡片**是不是还停在"改过/正在改"的状态**:
|
||||
· 有 `input[name=id]` → 这是编辑表单,必须换回新建表单;
|
||||
· 或者标题框里已经有内容(上次新建填了一半又关掉、或刚保存过一条)→ 同样换新,
|
||||
否则"新建"会带出上一次输了一半的内容,是同一类错觉。
|
||||
⇒ 换新的方式与「修改」完全对称:fetch `days/new`(内嵌片段)替换卡片。
|
||||
⚠️ 只有"脏"的时候才发这一趟请求 —— 干净的弹框直接开,不多一次网络往返。 */
|
||||
function needsFresh() {
|
||||
var c = card();
|
||||
if (!c || !window.fetch) { if (url) { window.location.href = url; } return; }
|
||||
if (!c) { return false; }
|
||||
if (c.querySelector('#anForm input[name="id"]')) { return true; }
|
||||
var t = c.querySelector('#an-title');
|
||||
return !!(t && String(t.value || '').trim() !== '');
|
||||
}
|
||||
function loadForm(url, title) {
|
||||
var c = card();
|
||||
if (!c || !window.fetch || !url) { if (url) { window.location.href = url; } return; }
|
||||
c.hidden = false; // 先打开(显示"载入中"占位)
|
||||
var lb = c.querySelector('#anNewCardTitle, .cal-modal-head b');
|
||||
if (lb) { lb.textContent = '编辑纪念日'; }
|
||||
if (lb) { lb.textContent = title; }
|
||||
fetch(url, { credentials: 'same-origin',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.text(); })
|
||||
@@ -231,17 +290,29 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
||||
c.parentNode.replaceChild(fresh, c);
|
||||
runInlineScripts(fresh, html);
|
||||
setOpen(true);
|
||||
/* 换了新片段之后,标题框里是空的 ⇒ 后面几次开合都不会再发请求 */
|
||||
var b = document.getElementById('anNewBtn');
|
||||
if (b) { b.setAttribute('aria-expanded', 'true'); }
|
||||
})
|
||||
.catch(function () { window.location.href = url.replace(/[?&]embed=1/, ''); });
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var t = e.target;
|
||||
if (!t || !t.closest) { return; }
|
||||
if (t.closest('#anNewBtn')) { e.preventDefault(); setOpen(true); return; }
|
||||
if (t.closest('#anNewBtn')) {
|
||||
e.preventDefault();
|
||||
var btn = t.closest('#anNewBtn');
|
||||
if (needsFresh()) {
|
||||
loadForm(btn.getAttribute('data-href') || '', '新建纪念日');
|
||||
} else {
|
||||
setOpen(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var ed = t.closest('[data-an-edit]');
|
||||
if (ed) {
|
||||
e.preventDefault();
|
||||
loadEdit(ed.getAttribute('data-href') || '');
|
||||
loadForm(ed.getAttribute('data-href') || '', '编辑纪念日');
|
||||
return;
|
||||
}
|
||||
if (t.closest('#anNewX') || t.closest('#anNewCancel')) { setOpen(false); return; }
|
||||
@@ -341,15 +412,17 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* ==================== ② 手动添加的(在上,用户第 9 条)==================== */ ?>
|
||||
<?php /* ==================== ② 与我相关的(在上,用户第 9 条)====================
|
||||
🔴 v62(用户 2026-10-18 第 1 条):标题「我自己添加的(4)」+「只有这些能改能删」
|
||||
→ **「与我相关」**(hint 整条删掉)。
|
||||
理由:这一块现在不只有"我添加的",还有"别人添加、但把我挂成了关联人"的
|
||||
(判据见 Anniversary::split())。继续叫"我自己添加的"就是错的;
|
||||
而"只有这些能改能删"这句在**功能上**没必要跟用户解释(按按钮能改能删,一看就懂)。 */ ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">
|
||||
我自己添加的(<?= count($custom) ?>)
|
||||
<span class="hint">只有这些能改能删</span>
|
||||
</h2>
|
||||
<h2 class="cinema-h2">与我相关(<?= count($custom) ?>)</h2>
|
||||
<?php if (!$custom): ?>
|
||||
<p class="empty-hint">
|
||||
还没有自己添加的纪念日。
|
||||
还没有与你相关的纪念日。
|
||||
<?php if ($canWrite): ?>
|
||||
结婚纪念日、忌日、相识的日子…点右上角「新建纪念日」加一条。
|
||||
<?php endif; ?>
|
||||
@@ -357,24 +430,30 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
||||
<?php else: ?>
|
||||
<?php $renderTable($custom, true); ?>
|
||||
<?php endif; ?>
|
||||
<?php /* ⚠️ 别人记的、只跟他自己相关的那些不在这儿出现("与我相关"就该是相关的)。
|
||||
但如果**一条都不提示**,用户可能以为数据丢了 —— 给一句小字,并说清去哪儿找。 */ ?>
|
||||
<?php if ((int)($hidden ?? 0) > 0): ?>
|
||||
<p class="small muted" style="margin:10px 0 0">
|
||||
另有 <?= (int)$hidden ?> 条是别人记的、与你无关,这里不列;日历里照旧能看到。
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* ==================== ③ 来自名册的生日(在下)==================== */ ?>
|
||||
<?php /* ==================== ③ 来自名册的生日(在下)====================
|
||||
🔴 v62(用户 2026-10-18 第 1 条):标题「来自名册的(20)」+「实时读出来的,
|
||||
改生日这里跟着变」→ **「来自名册」**;下面那段"这一块是实时派生的…"整段删掉。
|
||||
为什么删:那是**实现说明**,用户看的是日子,不是数据从哪来 ——
|
||||
而"改生日这里跟着变"已经由表格右上那句「来自名册」暗示得够清楚了。 */ ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">
|
||||
来自名册的(<?= count($derived) ?>)
|
||||
<span class="hint">实时读出来的,改生日这里跟着变</span>
|
||||
</h2>
|
||||
<h2 class="cinema-h2">来自名册(<?= count($derived) ?>)</h2>
|
||||
<?php if (!$derived): ?>
|
||||
<p class="empty-hint">
|
||||
名册里还没有人填过生日。到「名册 → 编辑成员」把生日填上,这里就会自动出现。
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<?php $renderTable($derived, false); ?>
|
||||
<p class="tip" style="margin-top:10px">
|
||||
这一块是**实时派生**的 —— 改生日,这里跟着变,不用重复维护。
|
||||
不需要在这里新建,也没法单独删(删掉生日就等于删了它)。
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div><?php /* /#anListHost */ ?>
|
||||
</div><?php /* /#anListHost —— 这个 div 只开了一次(见上面 `<div id="anListHost">`),
|
||||
收尾也只能有一次。曾经这一行被复制成两行 ⇒ 整个页面的 div 深度变成 -1,
|
||||
元素跑到容器外(流水线的"div 嵌套"检查抓到的就是这个)。 */ ?>
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
/**
|
||||
* 纪念日「分类设置」页(v62 用户 2026-10-18 第 2 条)。
|
||||
*
|
||||
* 用户原话:「设置按钮,点击后弹菜单,第一个菜单是分类设置,进去之后可以设置分类,
|
||||
* 每一类纪念日都有不同的图标,现在的分类,你自己把图标补全。如果是用的
|
||||
* fontawesome,那么如果用户添加了自己的分类,就让用户自己选。
|
||||
* 在列表中、日历中,都要根据分类,显示不同的图标。」
|
||||
*
|
||||
* ⇒ 这一页做三件事(都在**一张表单**里,改完点一次保存):
|
||||
* ① 改名(已用它的纪念日**不会**跟着改名 —— 页面底下有一句提示,别让用户以为丢了);
|
||||
* ② 换图标(下拉里是 `AnnivIcon` 那一套,选完左边立刻能预览);
|
||||
* ③ 调顺序 / 增 / 删。
|
||||
*
|
||||
* ⚠️ 三个动作(保存 / 删除 / 上移下移 / 新增)**共用同一个表单**,靠 submit 按钮的
|
||||
* `name` 区分(`del` / `move` / `add`)。为什么不用"每行一个小表单":
|
||||
* 一行一个小表单的话,用户改了 A 行的图标、又点了 B 行的「删」,
|
||||
* A 行那次修改会被**静默丢弃**(浏览器只提交被点的那张表单)。
|
||||
* 一张表单 = 一次提交 = 屏幕上看到的改动全部生效,没有"改了一半"的中间态。
|
||||
*
|
||||
* ⚠️ 分类是**家庭级**的(settings.anniv_kinds):全家共用一套叫法。
|
||||
* 所以这一页对**所有能写的成员**开放(不是只有管理员),但改的是同一份。
|
||||
*
|
||||
* @param array $kindsFull array<int,array{name:string,icon:string}>
|
||||
* @param array $kindCounts 分类名 => 用了几条
|
||||
*/
|
||||
$kindList = (array)($kindsFull ?? \App\Anniversary::kindsFull((int)\App\Auth::familyId()));
|
||||
$kindCount = (array)($kindCounts ?? []);
|
||||
$canEdit = \App\Auth::canWrite();
|
||||
$n = count($kindList);
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>纪念日分类</h1>
|
||||
<span class="sub">每个分类一个图标 · 全家共用一套</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e(url('days')) ?>">返回纪念日</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">
|
||||
分类与图标
|
||||
<span class="hint">列表、日历、卡片上都按这里的图标显示</span>
|
||||
</h2>
|
||||
|
||||
<?php if (!$canEdit): ?>
|
||||
<p class="empty-hint">你没有编辑权限,这里只显示现有分类。</p>
|
||||
<ul class="kind-list">
|
||||
<?php foreach ($kindList as $k): ?>
|
||||
<li class="kind-row is-ro">
|
||||
<span class="kind-prev"><?= \App\AnnivIcon::svg((string)$k['icon'], 16) ?></span>
|
||||
<span class="kind-name-ro"><?= e((string)$k['name']) ?></span>
|
||||
<span class="kind-use"><?= (int)($kindCount[(string)$k['name']] ?? 0) ?> 条</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<form method="post" action="<?= e(url('days/settings')) ?>" id="kindForm">
|
||||
<?= csrf_field() ?>
|
||||
<ol class="kind-list" id="kindList">
|
||||
<?php foreach ($kindList as $i => $k): ?>
|
||||
<li class="kind-row">
|
||||
<span class="kind-move">
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="<?= (int)$i ?>:up"
|
||||
title="上移" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="<?= (int)$i ?>:down"
|
||||
title="下移" <?= $i === $n - 1 ? 'disabled' : '' ?>>▼</button>
|
||||
</span>
|
||||
<?php /* 左边这个 svg 是"当前图标"的预览;右边下拉一动,JS 立刻换成新的
|
||||
(图标库见页面底部的 .kind-icon-bank,不在 JS 里手抄一遍 path)。 */ ?>
|
||||
<span class="kind-prev" data-kind-prev="<?= (int)$i ?>"><?= \App\AnnivIcon::svg((string)$k['icon'], 16) ?></span>
|
||||
<select class="select kind-sel" name="k_icon[<?= (int)$i ?>]"
|
||||
aria-label="<?= e((string)$k['name']) ?>的图标" data-kind-sel="<?= (int)$i ?>">
|
||||
<?php foreach (\App\AnnivIcon::choices() as $icKey => $icLabel): ?>
|
||||
<option value="<?= e($icKey) ?>" <?= (string)$k['icon'] === $icKey ? 'selected' : '' ?>><?= e($icLabel) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input class="input kind-name-in" name="k_name[<?= (int)$i ?>]" maxlength="20"
|
||||
value="<?= e((string)$k['name']) ?>" aria-label="分类名">
|
||||
<span class="kind-use"><?= (int)($kindCount[(string)$k['name']] ?? 0) ?> 条</span>
|
||||
<?php /* ⚠️ confirm 里**不要**拼分类名:e() 把单引号转成 `'`,
|
||||
而浏览器会先解实体再交给 JS ⇒ 名字里带引号时这段脚本直接语法错,
|
||||
「删除」按钮点了没反应(本项目踩过同型的坑)。用固定文案最稳。 */ ?>
|
||||
<button class="btn btn-warn btn-sm" type="submit" name="del" value="<?= (int)$i ?>"
|
||||
onclick="return confirm('删掉这个分类?已用它的纪念日不变,只是新建时不再出现。')">删除</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
|
||||
<?php if (!$kindList): ?>
|
||||
<p class="empty-hint" style="margin:8px 0 14px">还没有分类。下面加一个。</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="kind-add">
|
||||
<span class="kind-add-lb">加一个分类</span>
|
||||
<select class="select" name="new_icon" aria-label="新分类的图标">
|
||||
<?php foreach (\App\AnnivIcon::choices() as $icKey => $icLabel): ?>
|
||||
<option value="<?= e($icKey) ?>" <?= $icKey === 'star' ? 'selected' : '' ?>><?= e($icLabel) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input class="input" name="new_name" maxlength="20" placeholder="如:相识纪念日">
|
||||
<button class="btn btn-sm" type="submit" name="add" value="1">加分类</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-row" style="margin-top:16px">
|
||||
<button class="btn" type="submit" name="save" value="1">保存分类</button>
|
||||
<?php /* 动过图标/名字但还没保存时 JS 会把这句显出来(见页面底部脚本):
|
||||
这一页**不自动保存**,不提示的话用户挑完图标直接返回,改动就静默丢了。 */ ?>
|
||||
<span class="small kind-dirty" id="kindDirty" hidden>改过了,记得点左边的「保存分类」</span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="small muted" style="margin:14px 0 0">
|
||||
改名<b>不会</b>改掉已经用过旧名字的那些纪念日(它们仍记着旧分类);
|
||||
删掉一个分类也只是"新建时不再出现",历史记录照旧。
|
||||
<br>分类加得越多,新建时的下拉越长 —— 建议按"这件事是什么性质"来分,不要按人来分(人有关联人那一列)。
|
||||
</p>
|
||||
|
||||
<?php /* 图标库:全部图标在这里渲染一次,JS 只做"挑一个塞进预览",
|
||||
避免在 JS 里再抄一份 SVG path(两份迟早不一致)。 */ ?>
|
||||
<div class="kind-icon-bank" hidden aria-hidden="true">
|
||||
<?php foreach (array_keys(\App\AnnivIcon::LIST) as $icKey): ?>
|
||||
<span data-ic="<?= e($icKey) ?>"><?= \App\AnnivIcon::svg($icKey, 16) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var bank = document.querySelector('.kind-icon-bank');
|
||||
if (!bank) { return; }
|
||||
/* 改完图标/名字之后**一定要点保存**(这一页不自动存)。
|
||||
不提示的话,用户挑完图标顺手点「返回纪念日」,改动就静默丢了 ——
|
||||
所以只要动过,就把保存按钮标脏,并挂一句小字。 */
|
||||
var saveBtn = document.querySelector('button[name=save]');
|
||||
var dirty = document.getElementById('kindDirty');
|
||||
function markDirty() {
|
||||
if (saveBtn) { saveBtn.classList.add('is-dirty'); }
|
||||
if (dirty) { dirty.hidden = false; }
|
||||
}
|
||||
Array.prototype.forEach.call(document.querySelectorAll('[data-kind-sel]'), function (sel) {
|
||||
sel.addEventListener('change', function () {
|
||||
var i = sel.getAttribute('data-kind-sel');
|
||||
var prev = document.querySelector('[data-kind-prev="' + i + '"]');
|
||||
var src = bank.querySelector('[data-ic="' + sel.value + '"]');
|
||||
if (prev && src) { prev.innerHTML = src.innerHTML; }
|
||||
markDirty();
|
||||
});
|
||||
});
|
||||
Array.prototype.forEach.call(document.querySelectorAll('#kindList input.kind-name-in'),
|
||||
function (inp) { inp.addEventListener('input', markDirty); });
|
||||
/* 任何一次提交(保存 / 加分类 / 删除 / 上移下移)都算"处理过了",
|
||||
标志就地清掉 —— 免得提交完回来还挂着红点。 */
|
||||
var f = document.getElementById('kindForm');
|
||||
if (f) { f.addEventListener('submit', function () {
|
||||
if (saveBtn) { saveBtn.classList.remove('is-dirty'); }
|
||||
if (dirty) { dirty.hidden = true; }
|
||||
}); }
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
+77
-560
@@ -1,60 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页 —— 一块 dashboard。
|
||||
* 首页 —— 卡片网格(v62 用户 2026-10-18 第 3 条)。
|
||||
*
|
||||
* 用户原话:「今天,我们正式引入卡片的概念,在首页上,概述是卡片,日程是卡片,
|
||||
* 近期任务是卡片,往年的今天也是卡片,纪念日,要有一个卡片,电影、日程、
|
||||
* 读书,都要有自己的卡片。……卡片的规格,我们可以定义一下,例如,把宽度
|
||||
* 定义为 8,高度可以是 2、4、6、8,这样。如果是 2x2,那么就可以一排放 4 个,
|
||||
* 如果是 2x8,那就是一行放一个,类似于这样的定义。」
|
||||
*
|
||||
* ────────────────────────────────────────────────────────────────
|
||||
* v24 第 24 批 用户第 2、3 条(本次重排,用户原话):
|
||||
* 结构变了,但**内容一字未改**:原来的三块(信息面板 / 日程+任务 / 往年的今天)
|
||||
* 现在各自是一张卡片,内容块原样搬进 `templates/home/_card_*.php`。
|
||||
*
|
||||
* 第 2 条:「首页上的功能…一个就是上面的简要信息,这样去写。早上好,中午好,
|
||||
* 或者晚上好,某某某今天是几月几日?星期几农历几月几日?如果有节气的
|
||||
* 话,或者有重要的节日的话,在这里显示。比如说今天是这个国庆节啊,
|
||||
* 今天是端午节,或者是今天是中秋节,或者是今天是芒种,今天是大寒,
|
||||
* 今天是小寒,今天是什么纪念日,今天是谁的生日等等。显示在这里
|
||||
* 紧接着它的下面显示今天一共有多少条日程,今天有几件代办正在进行。
|
||||
* 把天气放在这个信息面板的最右侧,以图标的形式,彩色图标的形式去进行
|
||||
* 展示。比如说晴天多云下雨,然后下面显示呃温度、风力等等,在最下面
|
||||
* 再显示呃地址。就是比如说他选中的是他在个人设置里面设置的是青岛,
|
||||
* 那么就显示山东青岛就是让它放在这个信息面板的最右侧,靠右放。」
|
||||
* 尺寸口径(与 app.css 的 `.hc-*`、设置页文字完全一致):
|
||||
* · 一排 **8 格**;宽 2 / 4 / 6 / 8 → 一排 4 张 / 2 张 / 一大一小 / 独占一排;
|
||||
* · 高 2 / 4 / 6 / 8 行(一行 76px + 10px 间距)。
|
||||
* · 排版完全由 CSS 网格决定:**卡片顺序 = 数组顺序**,不需要任何浮动/清浮。
|
||||
*
|
||||
* 第 3 条:「家里的镜框这里呃全部不要了…紧接着直接显示日程和任务日程和任务在
|
||||
* 整个页面里面左右,并排放各占一半,日程就显示今天的日程,也就是
|
||||
* 日视图那样形式,为了好看高度需要固定。这样的话,鼠标放上去之后,
|
||||
* 向下可以滚动,暂时不要出现滚动条太丑了。为了节省上下滚动的这个空间,
|
||||
* 我们可以把没有日程的那些时间掐头去尾给它去掉。…同样的和日视图一样,
|
||||
* 需要在当前的时间或者是时刻那里显示一条红线。右侧显示任务模块里面的
|
||||
* 数据显示当前正在进行当中的所有的任务,除了显示任务的标题之外,
|
||||
* 还要显示他的状态。比如说正在进行中待确认等等。除了这之外,
|
||||
* 还要显示他所处的看板的名称,就是他现在被放在哪一个看板里面,
|
||||
* 就后最下面显示的是往年的今天这个模块。」
|
||||
* ⚠️ 别在模板里自己算"该多宽":宽高由 `$homeCards`(用户设置)给,
|
||||
* 模板只负责把类名拼对(`hc-w4 hc-h8`)。
|
||||
* ⚠️ 通知块仍在**卡片网格之前**:v30 第 5 条要求它出现在「往年的今天」前面,
|
||||
* 而"往年的今天"现在是网格里的一张卡片,位置由用户定 —— 放在网格前是唯一
|
||||
* 无论用户怎么摆都成立的方案。
|
||||
*
|
||||
* ⇒ 所以现在首页自上而下只有三块:
|
||||
* ① 信息面板(问候 + 日子标记 + 今日数字 | 最右:彩色天气图标 + 温度风力 + 地址)
|
||||
* ② 日程 / 任务 **左右各半**(各自固定高度、内部滚动、不显滚动条)
|
||||
* ③ 往年的今天
|
||||
*
|
||||
* ⚠️ 被删掉的(用户明确说"全部不要了"):
|
||||
* · 「家里的近况」模块卡片墙(moduleCards)—— 那张一片一片的统计卡
|
||||
* · 「未来七天」清单 —— 与左侧「今天日程」重复
|
||||
* 两块都不再渲染。服务端仍照旧算 moduleCards(个人设置里那个开关还留着),
|
||||
* 但首页不再用 —— 留着它是为了"以后要恢复"时不用动后端。
|
||||
*
|
||||
* ⚠️ 「家人卡片」「两个月内的生日」早先就已按用户要求撤掉,本批不变。
|
||||
* @var array $homeCards HomeCards::layout() 的结果(每项 k/w/h/n)
|
||||
* @var array $cardData HomeCards::data() 取的那几张卡片的额外数据
|
||||
*/
|
||||
// 生日事件上用简称,卡片才不会一行塞不下;长名字在详情页里出
|
||||
$today = date('Y-m-d');
|
||||
// 首页要显示哪些模块、什么顺序 —— 用户在「个人设置 → 首页上的模块」里定(每人一份)
|
||||
$homeMods = array_values(array_filter(
|
||||
(array)($homeModules ?? []),
|
||||
static fn($k) => \App\Module::exists((string)$k)
|
||||
));
|
||||
$todayMarks = $todayMarks ?? ['tags' => [], 'line' => ''];
|
||||
?>
|
||||
<?php /* 用户 2026-10-03 第 3 条:
|
||||
① 标题下显示**个人 slogan**(原来撤掉了,现在要回来);
|
||||
① 标题下显示**个人 slogan**;
|
||||
② 右侧那三块统计(与我有关 / 名册总数 / 亲属连线)**去掉**;
|
||||
③ 「我的资料 / 我的家谱」保留,但从文字按钮换成**图标按钮**;
|
||||
④ 「名册」按钮去掉 —— 它与顶部导航条里的「名册」重复。
|
||||
⚠️ 图标用行内 SVG(不引外部字体/图片),与观影设置下拉同一套做法。 */ ?>
|
||||
v62 追加第五个图标按钮:「首页卡片」布局设置(与这排图标同一套做法)。 */ ?>
|
||||
<section class="hero">
|
||||
<img class="hero-logo" src="<?= e(asset('/assets/icons/icon-192.png')) ?>" alt="">
|
||||
<div class="hero-main">
|
||||
@@ -70,6 +48,16 @@ $todayMarks = $todayMarks ?? ['tags' => [], 'line' => ''];
|
||||
`$me = Auth::check()` 把模板里的 $me 覆盖成布尔值,链接就变成 id=0
|
||||
("补充亲属关系"点进去是空白页)。所以这里显式用 $meId。 */ ?>
|
||||
<div class="hero-actions">
|
||||
<a class="icon-btn" href="<?= e(url('home/cards')) ?>"
|
||||
title="首页卡片(摆哪几张、多大)" aria-label="首页卡片">
|
||||
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" fill="none"
|
||||
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2.4" y="2.4" width="6" height="6" rx="1.2"/>
|
||||
<rect x="11.6" y="2.4" width="6" height="6" rx="1.2"/>
|
||||
<rect x="2.4" y="11.6" width="6" height="6" rx="1.2"/>
|
||||
<rect x="11.6" y="11.6" width="6" height="6" rx="1.2"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-btn" href="<?= e(url('people/show', ['id' => (int)$meId])) ?>"
|
||||
title="我的资料" aria-label="我的资料">
|
||||
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" fill="none"
|
||||
@@ -90,443 +78,11 @@ $todayMarks = $todayMarks ?? ['tags' => [], 'line' => ''];
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php /* ==================== ① 信息面板(用户第 2 条)====================
|
||||
左:问候 + 姓名 → 日期/星期/农历 → 日子标记(节气/节日/纪念日/生日)→ 今日数字
|
||||
右:**彩色**天气图标 + 温度 + 风力 → 地址
|
||||
⚠️ 天气靠右:用户原话「把天气放在这个信息面板的最右侧…靠右放」。
|
||||
用 flex + margin-left:auto 把它顶到右端(窄屏会自动折到下一行,见 CSS)。 */ ?>
|
||||
<section class="section">
|
||||
<div class="card card-pad home-brief">
|
||||
<div class="hb-left">
|
||||
<?php /* ① 问候(早上好/中午好…)+ 我的称呼 —— 问候语里已经带了名字,
|
||||
但用户明确要求「早上好,某某某」这样的形式,所以名字单独再强调一次。
|
||||
(Home::greeting 本身会拼 "早上好,老杨,今天有 3 件事",
|
||||
这里只用它的"时段"部分,尾巴换成面板里更该看的信息。) */ ?>
|
||||
<?php
|
||||
$hbPart = '你好';
|
||||
$hbH = (int)date('G');
|
||||
if ($hbH < 5) { $hbPart = '夜深了'; }
|
||||
elseif ($hbH < 11) { $hbPart = '早上好'; }
|
||||
elseif ($hbH < 13) { $hbPart = '中午好'; }
|
||||
elseif ($hbH < 18) { $hbPart = '下午好'; }
|
||||
else { $hbPart = '晚上好'; }
|
||||
?>
|
||||
<div class="hb-hello">
|
||||
<b><?= e($hbPart) ?></b><?php if (trim((string)($meName ?? '')) !== ''): ?>,<?= e((string)$meName) ?><?php endif; ?>
|
||||
</div>
|
||||
<div class="hb-date">
|
||||
<span class="hb-d"><?= e((string)($todayLabel ?? date('n 月 j 日'))) ?></span>
|
||||
<span class="hb-w"><?= e((string)($weekday ?? '')) ?></span>
|
||||
<?php if (($lunarShort ?? '') !== ''): ?>
|
||||
<span class="hb-lunar" title="<?= e((string)($lunarLine ?? '')) ?>"><?= e((string)$lunarShort) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if (!empty($todayMarks['tags'])): ?>
|
||||
<div class="hb-marks">
|
||||
<?php foreach ($todayMarks['tags'] as $mk): ?>
|
||||
<span class="hb-mark is-<?= e((string)$mk['kind']) ?>"><?= e((string)$mk['word']) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="hb-stats">
|
||||
<a class="hbs" href="<?= e(url('calendar')) ?>">
|
||||
<b><?= count($todayEvents ?? []) ?></b> 条日程(今天)
|
||||
</a>
|
||||
<?php /* 🔴🔴 v24 第 25 批 用户第 3 条修:「为什么显示 0 条待办呢?
|
||||
任务看板明明有一个进行中的任务啊。」
|
||||
|
||||
根因:这句话原来读的是 `stat7['todos']` = `count(Task::byStatus(...,'todo'))`
|
||||
—— 只数**待办**。而右边那块「手头在做」列的是 **doing(+ 已逾期的待办)**,
|
||||
用户刚在看板上拖成「进行中」的那件任务于是**不计入**,数字就显示 0。
|
||||
两个口径不同源,数字和列表当场对不上。
|
||||
|
||||
⇒ 现在这个数字与右侧列表**同源**(与 index.php 传给本页的**同一个数组**)。
|
||||
|
||||
🔴 v53(用户 2026-10-16 第 2 条):右侧列表现在**也含待办**了(放在进行中下面)
|
||||
⇒ 「件在做」不能再数整个数组(那会把待办也算成"在做")。
|
||||
改成**按状态拆开数**:进行中 →「件在做」,待办 →「件待办」。
|
||||
⚠️ 两个数字相加 = 右侧列表的行数(列表也是这两段拼起来的),仍然同源。
|
||||
|
||||
⚠️ 「已经动手」与「排队等着」是两件事(用户第 6 条要求说清区别):
|
||||
首页要回答的是"我手头在做什么",所以主数字用「在做」的口径;
|
||||
排队的那些(todo)另给一个小字,不必让人点进任务页才知道还有多少。 */ ?>
|
||||
<?php $nDoing = 0; $nTodo = 0;
|
||||
foreach (($tasks ?? []) as $__t) {
|
||||
if ((string)($__t['status'] ?? 'todo') === 'doing') { $nDoing++; } else { $nTodo++; }
|
||||
} ?>
|
||||
<a class="hbs" href="<?= e(url('tasks')) ?>"
|
||||
title="近期任务(进行中)">
|
||||
<b><?= $nDoing ?></b> 件在做
|
||||
</a>
|
||||
<?php if ($nTodo > 0): ?>
|
||||
<a class="hbs" href="<?= e(url('tasks')) ?>" title="记下来了、还没动手的那些">
|
||||
<b><?= $nTodo ?></b> 件待办
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ((int)($stat7['overdue'] ?? 0) > 0): ?>
|
||||
<a class="hbs is-late" href="<?= e(url('tasks')) ?>">
|
||||
<b><?= (int)$stat7['overdue'] ?></b> 件逾期
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($weather)): ?>
|
||||
<div class="hb-weather is-<?= e((string)($weather['tone'] ?? 'cloud')) ?>">
|
||||
<span class="hbw-icon" aria-hidden="true"><?= e((string)($weather['icon'] ?? '')) ?></span>
|
||||
<div class="hbw-main">
|
||||
<div class="hbw-temp"><?= e((string)($weather['temp'] ?? '')) ?></div>
|
||||
<div class="hbw-text"><?= e((string)($weather['text'] ?? '')) ?></div>
|
||||
<?php if (($weather['wind'] ?? '') !== ''): ?>
|
||||
<div class="hbw-wind">
|
||||
<?= $weather['wdir'] !== '' ? e((string)$weather['wdir']) . ' ' : '' ?><?= e((string)$weather['wind']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if (($weather['place'] ?? '') !== ''): ?>
|
||||
<div class="hbw-place"><?= e((string)$weather['place']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php /* ==================== ② 日程 / 任务 左右各半(用户第 3 条)====================
|
||||
⚠️ 高度**固定**(CSS 里给 .hd-pane-body 一个 max-height):
|
||||
用户原话「为了好看高度需要固定。这样的话,鼠标放上去之后,向下可以滚动,
|
||||
暂时不要出现滚动条太丑了」——
|
||||
所以是 `overflow-y:auto` + 隐藏滚动条(`scrollbar-width:none` 等三套写法)。
|
||||
⚠️ 日程掐头去尾:见下面 $hr 的算法 —— 只画"第一个日程前一小时"到"最后一个日程后一小时",
|
||||
中间空档不裁(裁了会让人以为日程连在一起)。全天日程永远在顶上。 */ ?>
|
||||
<?php
|
||||
// ---- 日程时间轴:掐头去尾(用户第 3 条"没有日程的那些时间掐头去尾给它去掉")----
|
||||
$allDay = [];
|
||||
$timed = [];
|
||||
foreach (($todayEvents ?? []) as $ev) {
|
||||
if ((string)($ev['time'] ?? '') === '') { $allDay[] = $ev; }
|
||||
else { $timed[] = $ev; }
|
||||
}
|
||||
$startH = 0; $endH = 24;
|
||||
if ($timed) {
|
||||
$mins = array_map(static function (array $e): int {
|
||||
$p = explode(':', (string)$e['time']);
|
||||
return ((int)($p[0] ?? 0)) * 60 + (int)($p[1] ?? 0);
|
||||
}, $timed);
|
||||
// 前后各留一小时:紧了会让人以为"到点就结束了",松了又浪费高度
|
||||
$startH = max(0, intdiv(min($mins), 60) - 1);
|
||||
$endH = min(24, intdiv(max($mins), 60) + 2); // +2 = 跨过那一小时再收尾
|
||||
/* 🔴 v28 用户第 1 条:「今天的日程,并没有定位到红线所在的位置」。
|
||||
根因:**区间里没有"现在"**,于是红线不画、定位算出来是个越界的值。
|
||||
(实测:现在 22:44、日程 09:00 与 14:00 → 区间掐成 08–17,
|
||||
`$nowPos = (22-8)/9 = 1.56` → `$showNow=false` → `class="ag-now"` 整个不渲染;
|
||||
而定位脚本用同一个越界的 RATIO 去乘网格高度,被 clamp 到最底 → 停在底部。)
|
||||
⇒ 修法不是"没红线就别定位",而是**让区间永远包含当前时刻**:
|
||||
用户要的正是"一眼看到现在处在哪儿",那就必须把"现在"纳入刻度。
|
||||
下面的 `$nowH` 是当前小时(服务端时区,与页面口径一致),
|
||||
把它并进区间端点 —— 于是红线必然落在区间内,定位也就有了正确的目标。
|
||||
⚠️ 必须在**算 $nowH 之前**完成这一步(原代码把 $nowH 放在区间之后算,
|
||||
这里把它提上来,避免"改了区间却没重新算位置"这种漏改)。
|
||||
🔴🔴 v44(用户 2026-10-12 第 3 条):「首页上的红线刻度显示不准确。
|
||||
现在明明已经 7:44 了,但它那个红线还停留在最早的 7 点钟,
|
||||
并没有走到 7:44 那个位置。这个只有在首页上是这样的。我点到日程里面,
|
||||
那个红线的刻度好像是对的。」
|
||||
⇒ 真根因:首页用的是 `date('G')`(**只取整小时**),而日程页用的是
|
||||
`(int)date('G')*60 + (int)date('i')`(**精确到分**)。
|
||||
7:44 时首页算出 7 → 红线停在 7:00,正是用户看到的。
|
||||
⇒ 这里改成与日程页**同一套**(小时*60+分钟),两处的口径不再分叉。 */
|
||||
$nowHNow = (int)date('G');
|
||||
if ($nowHNow < $startH) { $startH = max(0, $nowHNow - 1); }
|
||||
if ($nowHNow >= $endH) { $endH = min(24, $nowHNow + 2); }
|
||||
if ($endH <= $startH) { $endH = min(24, $startH + 2); }
|
||||
}
|
||||
/* 红线位置:**分钟级**(与日程页同一算法)。见上面 v44 那条 —— 原来是 `date('G')`
|
||||
(整小时),所以"7:44 的红线停在 7:00"。 */
|
||||
$nowMinutes = ((int)date('G')) * 60 + (int)date('i');
|
||||
$nowPos = (($nowMinutes / 60) - $startH) / max(1, ($endH - $startH)); // 红线位置(0~1)
|
||||
$showNow = $nowPos >= 0 && $nowPos <= 1;
|
||||
|
||||
/* 🔴 v30 第 2/4 条:首页日程也用**和日程模块同一套冲突分列**。
|
||||
⇒ 复用 App\Calendar::conflictLanes(算法借鉴 SyncCalendar 的 TimelineUtil):
|
||||
两条撞车就左右各半、三条就三分 —— 首页与日历页看到的是同一个布局。
|
||||
⚠️ 必须**先按时间排序**再分列(lanes 是按传入顺序返回的):
|
||||
`$todayEvents` 从路由出来已是"全天在前、其余按时刻"升序,这里再稳一次。
|
||||
⚠️ 入参只挑**有时间**的那些(全天日程不进时间网格)。 */
|
||||
usort($timed, static function (array $a, array $b): int {
|
||||
return strcmp((string)($a['time'] ?? ''), (string)($b['time'] ?? ''));
|
||||
});
|
||||
$laneInfo = \App\Calendar::conflictLanes($timed);
|
||||
foreach ($timed as $i => &$evRef) {
|
||||
[$l, $w] = $laneInfo[$i] ?? [0.0, 1.0];
|
||||
$evRef['left'] = round($l * 100, 4);
|
||||
$evRef['width'] = round($w * 100, 4);
|
||||
}
|
||||
unset($evRef);
|
||||
?>
|
||||
<section class="section">
|
||||
<div class="home-duo">
|
||||
<?php /* ---- 左:今天的日程(日视图形式)---- */ ?>
|
||||
<div class="card card-pad hd-pane" id="homeAgenda">
|
||||
<div class="hd-pane-head">
|
||||
<h2 class="cinema-h2">今天的日程</h2>
|
||||
<?php /* 🔴 v28 用户第 2 条:「看正月这个按钮,改成:更多日程,
|
||||
背景颜色和日程的主题颜色保持一致。」
|
||||
(「看正月」是用户对「看整月」的口误。) */ ?>
|
||||
<a class="btn btn-sm hd-more" href="<?= e(url('calendar')) ?>"
|
||||
style="--hd-c: <?= e(\App\Module::color('calendar', (int)\App\Auth::familyId())) ?>;">更多日程</a>
|
||||
</div>
|
||||
<?php /* 🔴 v24 第 26 批 用户第 2 条(前半):「今天的日程,除了全天日程**一直显示在上面**外,
|
||||
其他日程随着滚动」。
|
||||
⇒ DOM 结构必须分成两层:
|
||||
① `.ag-allday` **留在 pane 里、不在滚动容器内** —— 它永远钉在标题下方;
|
||||
② `.hd-pane-body`(滚动容器)**只包** `.ag-grid` —— 只有时段网格随滚动。
|
||||
为什么不能在同一个滚动容器里给全天区 `position: sticky`:
|
||||
它在滚动区**内部**,sticky 只能粘在滚动区顶端,而滚动区顶端在标题下面 ——
|
||||
滚起来时全天区会被"标题 + 滚动区"两层挤着,看起来仍然在动;
|
||||
而且 sticky 需要不透明底色遮住底下滚过去的日程,与现在的浅色卡片叠色不好看。
|
||||
直接把它移出滚动容器,是**零副作用**的做法。 */ ?>
|
||||
<?php if (!$todayEvents): ?>
|
||||
<div class="hd-pane-body">
|
||||
<p class="empty-hint">今天没有安排。</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php if ($allDay): ?>
|
||||
<div class="ag-allday">
|
||||
<?php foreach ($allDay as $ev): ?>
|
||||
<?php /* 🔴 v43(用户第 6 条):点条目弹详情浮层(不直接跳页)。 */ ?>
|
||||
<?php
|
||||
$evKind = (string)($ev['kind'] ?? 'custom');
|
||||
$evRef = (int)($ev['ref_id'] ?? 0);
|
||||
$evEdit = '';
|
||||
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
||||
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
||||
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||
?>
|
||||
<a class="ag-allday-item" href="<?= e((string)$ev['href']) ?>"
|
||||
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'>
|
||||
<span class="dash-dot" style="background:<?= e((string)$ev['color']) ?>;"></span>
|
||||
<span class="ag-ad-t">全天</span>
|
||||
<span class="ag-ad-w"><?= e((string)$ev['title']) ?></span>
|
||||
<span class="dash-cal" style="background:<?= e((string)$ev['color']) ?>;"><?= e((string)$ev['calname']) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="hd-pane-body" data-ag-scroll>
|
||||
<div class="ag-grid">
|
||||
<?php for ($h = $startH; $h < $endH; $h++): ?>
|
||||
<div class="ag-row">
|
||||
<span class="ag-hour"><?= sprintf('%02d:00', $h) ?></span>
|
||||
<span class="ag-line"></span>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
<?php /* 当前时刻红线:用户原话「需要在当前的时间或者是时刻那里显示一条红线」 */ ?>
|
||||
<?php /* 🔴 v44(用户 2026-10-12 第 3 条):「红线左侧是有一个红点吗?
|
||||
这个红点显示也不全,也不太好看。直接把红点去掉就行了,
|
||||
直接一根红线就可以了。」
|
||||
⇒ 那颗 `.ag-now-dot` 整个删掉(原来它是个 8×8 的圆,左端 `left:-4px`,
|
||||
靠外溢一点点来"指示起点";用户觉得显示不全又不好看)。
|
||||
⚠️ CSS 里那条 `.ag-now-dot` 也一并删(留在那儿是死规则,
|
||||
还会让"红线只有一根"这件事在样式表里看不出结论)。 */ ?>
|
||||
<?php if ($showNow): ?>
|
||||
<?php /* 🔴 v45:把**服务端算红线用的那个分钟数**也写到元素上(`data-now-min`)。
|
||||
为什么:探针原来只能"拿服务端渲染出的 top% 去和浏览器此刻的时间比" ——
|
||||
两者的容差被 0.3% 一道卡着,而"整小时 vs 分钟"的差只等于
|
||||
`分钟/60/跨度`:当分钟数很小时(如第 2 分钟)占比不到 0.3%,
|
||||
**注入旧行为也不会变红** ⇒ 反向验证当场判它"恒真摆设"。
|
||||
写上 data-now-min 之后,探针可以做一个**零容差**的判据:
|
||||
"服务端用的分钟数 == 浏览器此刻的分钟数",整小时注入必然对不上。
|
||||
⚠️ 它只是给测试用的观测点,不影响任何渲染/行为。 */ ?>
|
||||
<div class="ag-now" style="top: <?= number_format($nowPos * 100, 2) ?>%"
|
||||
data-now-min="<?= (int)$nowMinutes ?>"></div>
|
||||
<?php endif; ?>
|
||||
<?php /* 日程块:按"相对第一个刻度的小时数"定位(与日视图同一套比例)---- */ ?>
|
||||
<?php
|
||||
/* 🔴 v30 第 2 条:「首页上的今天的日程,用色块表示,和日程保持一致。」
|
||||
用户要的是**和日程模块(周/日视图)一样的实心色块**,而不是原来的
|
||||
"白底 + 左边一条色线" —— 后者色只占 3px,一眼看不出是哪个日历本。
|
||||
|
||||
⇒ 三处对齐日程模块的 `.tg-block`:
|
||||
① 底色 = 该日程所属日历本的颜色(不是主题色);
|
||||
② 白字(`.tg-block` 就是 `color:#fff`);
|
||||
③ 冲突错开(left/width 由 Calendar::conflictLanes 算,见 index.php)。
|
||||
⚠️ 深浅:`--tg-*` 那几个本子的颜色都是中低明度,白字压得住;
|
||||
但用户可能自定义成很浅的色 —— 所以标题那一行加 `text-shadow`
|
||||
兜底(比"再加一层半透明黑罩"更不破坏本色)。
|
||||
⚠️ 高度用 `min-height` 而不是固定值:15 分钟的日程也要看得见
|
||||
(与 .tg-block 的 15 分钟最短块口径一致)。 */
|
||||
foreach ($timed as $ev):
|
||||
$p = explode(':', (string)$ev['time']);
|
||||
$evMin = ((int)($p[0] ?? 0)) * 60 + ((int)($p[1] ?? 0));
|
||||
$topPct = (($evMin / 60) - $startH) / max(1, ($endH - $startH)) * 100;
|
||||
$topPct = max(0, min(96, $topPct));
|
||||
// 结束时刻:缺省时按"下一件事的起点"或 30 分钟(与日历口径一致)
|
||||
$et2 = (string)($ev['end_time'] ?? '');
|
||||
if ($et2 !== '' && preg_match('/^(\d{1,2}):(\d{2})/', $et2, $mm2)) {
|
||||
$endMin = ((int)$mm2[1]) * 60 + ((int)$mm2[2]);
|
||||
} else {
|
||||
$endMin = $evMin + 30;
|
||||
}
|
||||
if ($endMin <= $evMin) { $endMin = $evMin + 30; }
|
||||
$endMin = min($endMin, $endH * 60);
|
||||
$hPct = ($endMin / 60 - $startH) / max(1, ($endH - $startH)) * 100 - $topPct;
|
||||
$hPct = max(2.2, $hPct); // 至少能塞下一行
|
||||
?>
|
||||
<?php
|
||||
$evKind = (string)($ev['kind'] ?? 'custom');
|
||||
$evRef = (int)($ev['ref_id'] ?? 0);
|
||||
$evEdit = '';
|
||||
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
||||
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
||||
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||
?>
|
||||
<a class="ag-ev" href="<?= e((string)$ev['href']) ?>"
|
||||
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
||||
style="background: <?= e((string)$ev['color']) ?>;
|
||||
top: <?= number_format($topPct, 2) ?>%;
|
||||
height: <?= number_format($hPct, 2) ?>%;
|
||||
<?php /* 🔴 v37 用户第 3 条:「首页上今天的日程…如果它分了三列
|
||||
或者是几列,这三列之间的距离或者是空隙非常非常的大,
|
||||
是分开的,中间有非常大的空隙。我看了一下日程里面的那个
|
||||
显示,那两个色块就是紧挨着的。所以…应该让它们紧挨着。」
|
||||
|
||||
根因(与日历页 .tg-block 逐字对比出来的):
|
||||
· 日历页写的是 `left: calc(2px + L%)` / `width: calc(W% - 4px)`
|
||||
—— 只留 2px+2px 的缝,所以色块**紧挨**;
|
||||
· 这里却写死了 `calc(46px + L%)` / `calc(W% - 50px)`
|
||||
—— 每块左右各扣 23px/25px。两列时那 46px 的扣减
|
||||
**没法均分**,全挤进了两块之间的缝里 →
|
||||
实测缝 ≈ 50px(等于把 46px 全加在了中间),
|
||||
日历页只有 ≈ 4px。
|
||||
|
||||
⇒ 改法与日历页对齐:
|
||||
left = 46px(给小时刻度)+ 列内的 2px
|
||||
width = 列宽 - 4px(左右各 2px)
|
||||
这样列间缝恒为 4px(紧挨),而且与"放得下多少列、
|
||||
每列多宽"的解耦是干净的 —— 列宽由 width% 决定,
|
||||
缝由那 4px 决定,两者不再互相污染。 */ ?>
|
||||
left: calc(46px + <?= (float)($ev['left'] ?? 0) ?>% + 2px);
|
||||
width: calc(<?= (float)($ev['width'] ?? 100) ?>% - 4px);"
|
||||
title="<?= e((string)$ev['title']) ?>"
|
||||
data-cal="<?= (int)$ev['cal'] ?>">
|
||||
<span class="ag-ev-t"><?= e((string)$ev['time']) ?></span>
|
||||
<span class="ag-ev-w"><?= e((string)$ev['title']) ?></span>
|
||||
<span class="ag-ev-cal"><?= e((string)$ev['calname']) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div><?php /* /.ag-grid */ ?>
|
||||
</div><?php /* /.hd-pane-body(滚动区,只含时段网格) */ ?>
|
||||
<?php endif; ?>
|
||||
</div><?php /* /.hd-pane */ ?>
|
||||
|
||||
<?php /* ---- 右:进行中的任务(标题 + 状态 + 看板名)---- */ ?>
|
||||
<div class="card card-pad hd-pane" id="homeTasks">
|
||||
<div class="hd-pane-head">
|
||||
<h2 class="cinema-h2">近期任务</h2>
|
||||
<?php /* 🔴 v28 用户第 3 条:「『手头在做』改成『近期任务』,『任务看板』这个按钮
|
||||
改成『全部任务』,按钮颜色和任务的主题颜色保持一致。」 */ ?>
|
||||
<a class="btn btn-sm hd-more" href="<?= e(url('tasks')) ?>"
|
||||
style="--hd-c: <?= e(\App\Module::color('tasks', (int)\App\Auth::familyId())) ?>;">全部任务</a>
|
||||
</div>
|
||||
<div class="hd-pane-body">
|
||||
<?php /* 用户第 6 条:「所有的任务,前面都带一个复选框,在任何地方,点这个复选框,
|
||||
这个任务就完成了。」→ 首页这一块也算"任何地方",
|
||||
所以每行前面也有复选框,点了就地完成(AJAX,不跳页)。
|
||||
表单与 JS 在页面末尾各一份(见 home.php 的 taskDoneForm 段)。 */ ?>
|
||||
<?php if (!$tasks): ?>
|
||||
<p class="empty-hint">没有进行中的任务,也没有待办。</p>
|
||||
<?php else: ?>
|
||||
<ul class="dash-list tk-list">
|
||||
<?php foreach ($tasks as $t): ?>
|
||||
<?php $late = \App\Task::isOverdue($t['due_at'] ?? null); ?>
|
||||
<?php
|
||||
/* 🔴 v32 第 4 条(用户 2026-10-08):「首页,近期任务,任务的文字和前面的圆点,
|
||||
颜色并没有跟**看板颜色**一致,这个需要修正。」
|
||||
|
||||
⚠️ v30 那轮我把它改成了 `--tk-c`(= **任务模块**的主题色),
|
||||
当时理解成"用户说的主题色"。**理解错了** —— 用户要的是
|
||||
「这条任务属于哪个**看板**」的颜色(任务模块自己早就支持
|
||||
"给看板选颜色",用户原话在第 26 批:「任务在设置看板的时候,
|
||||
可以选择颜色,这样在首页列表中显示的时候,就可以更加直观的
|
||||
看到他属于哪个看板。」)。
|
||||
⇒ 用 `Task::boardColorById(家庭, 看板id)`:
|
||||
看板没设色时它按**看板名派生**一个稳定色(同名永远同色),
|
||||
所以这里不需要任何回退分支。
|
||||
⚠️ 逾期仍用红色(状态语义,不能被看板色盖掉)——
|
||||
但那是"整行变红",与"没逾期时跟看板一致"并不冲突。
|
||||
⚠️ 没归看板的(board_id 为空)走"默认看板"的派生色,
|
||||
与任务模块列表页看到的完全一致(同一函数)。 */
|
||||
$tkColor = \App\Task::boardColorById((int)\App\Auth::familyId(),
|
||||
(int)($t['board_id'] ?? 0) ?: null);
|
||||
?>
|
||||
<li class="dash-row tk-row" data-task-row="<?= (int)$t['id'] ?>"
|
||||
style="--tk-c: <?= e($tkColor) ?>;">
|
||||
<?php
|
||||
$tkId = (int)$t['id']; $tkStatus = (string)($t['status'] ?? 'todo');
|
||||
$tkBack = 'home'; $tkStatic = !\App\Auth::canWrite();
|
||||
require __DIR__ . '/_task_check.php';
|
||||
?>
|
||||
<span class="dash-dot" style="background: <?= e($late ? 'var(--red)' : 'var(--tk-c)') ?>;"></span>
|
||||
<?php /* 🔴 v43(用户第 6 条):点任务标题也弹详情浮层(不直接跳页)。
|
||||
要点在**标题那个 <a>** 上而不是整行 ——
|
||||
行里还有复选框(勾一下完成)与其它链接,
|
||||
整行可点会让"勾选"和"打开"互相抢。 */ ?>
|
||||
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>"
|
||||
<?php /* 🔴 v54(用户 2026-10-16 第 1 条):标题被 CSS 截成一行 + 省略号,
|
||||
所以这里给 `title` —— 鼠标停上去能看全名。
|
||||
(点开还有详情浮层,但"只看一眼全名"不该逼人点进去。) */ ?>
|
||||
title="<?= e((string)$t['title']) ?>"
|
||||
data-fb-entry='<?= fb_entry_json([
|
||||
'title' => (string)$t['title'],
|
||||
'status_word' => \App\Task::statusWord((string)($t['status'] ?? 'todo')),
|
||||
'note' => (string)($t['note'] ?? ''),
|
||||
], url('tasks/show', ['id' => (int)$t['id']]),
|
||||
\App\Auth::canWrite() ? url('tasks/edit', ['id' => (int)$t['id']]) : '',
|
||||
[['截止', ($t['due_at'] ?? '') !== '' ? substr((string)$t['due_at'], 0, 16) : ''],
|
||||
['看板', trim((string)($t['board_name'] ?? ''))]]) ?>'
|
||||
style="color: <?= e($late ? 'var(--red)' : $tkColor) ?>;">
|
||||
<?= e((string)$t['title']) ?>
|
||||
</a>
|
||||
<?php /* 用户第 3 条:「除了显示任务的标题之外,还要显示他的状态。
|
||||
比如说正在进行中待确认等等。」 */ ?>
|
||||
<span class="kb-st is-<?= e((string)($t['status'] ?? 'todo')) ?> is-static">
|
||||
<?= e(\App\Task::statusWord((string)($t['status'] ?? 'todo'))) ?>
|
||||
</span>
|
||||
<?php /* 用户第 3 条:「还要显示他所处的看板的名称,就是他现在被放在哪一个看板里面」 */ ?>
|
||||
<?php if (trim((string)($t['board_name'] ?? '')) !== ''): ?>
|
||||
<?php /* 🔴 v24 第 26 批 用户第 3 条(后半):「任务在设置看板的时候,可以选择颜色,
|
||||
这样在首页列表中显示的时候,就可以更加直观的看到他属于哪个看板。」
|
||||
⇒ 徽标**同时带颜色**:左边一个小圆点(看板色)+ 看板名。
|
||||
为什么不用"整块背景染色":首页这一行已经有状态徽标、看板徽标、逾期等几个
|
||||
色块,整块染色会互相打架、反而看不出哪个色对应哪个语义。
|
||||
一个小圆点信息量足够、又不抢戏。 */
|
||||
$bColor = \App\Task::boardColor($t['board_color'] ?? null, (string)$t['board_name']); ?>
|
||||
<span class="tk-board" style="--bc: <?= e($bColor) ?>;">
|
||||
<i class="tk-board-dot" aria-hidden="true"></i><?= e((string)$t['board_name']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ((int)($t['child_total'] ?? 0) > 0): ?>
|
||||
<span class="dash-tag"><?= (int)$t['child_done'] ?>/<?= (int)$t['child_total'] ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (($t['due_at'] ?? '') !== ''): ?>
|
||||
<span class="tk-due"><?= e(\App\Task::dueWord($t['due_at'] ?? null)) ?></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php /* ==================== 通知消息(v30 第 5 条)====================
|
||||
用户原话:「…发一个通知,通知出现在首页中,**往年今天的前面**。」
|
||||
⇒ 位置是硬性要求:必须在「往年的今天」**之前**。
|
||||
⚠️ 没有通知时**整块不渲染** —— 首页是"每天第一眼看的地方",
|
||||
不要用一个空盒子占掉一屏(与"今天没有安排"那种信息位不同:
|
||||
通知本来就是事件驱动的,没有就真的不该出现)。 */ ?>
|
||||
⇒ 放在卡片网格**之前**:往年的今天是网格里的一张卡片,位置由用户摆,
|
||||
只有"在网格前面"才能保证永远在它上面。
|
||||
⚠️ 没有通知时整块不渲染 —— 首页是"每天第一眼看的地方",不要用一个空盒子占一屏。 */ ?>
|
||||
<?php if (!empty($notifs)): ?>
|
||||
<section class="section">
|
||||
<div class="page-head" style="margin-bottom:12px">
|
||||
@@ -564,43 +120,58 @@ unset($evRef);
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ==================== ③ 往年的今天 ==================== */ ?>
|
||||
<?php /* ==================== 卡片网格(v62)====================
|
||||
⚠️ `require` 进来的每个 `_card_*.php` 与这里**同一作用域**(共享 $tasks / $todayEvents
|
||||
/ $cardData …):这是刻意的 —— 卡片的取数与首页的取数必须同源,
|
||||
各自再查一遍就会出现"数字和列表对不上"那类老问题。 */ ?>
|
||||
<section class="section">
|
||||
<div class="page-head" style="margin-bottom:12px">
|
||||
<?php /* v24 第 19 条:只认「往年的今天」,不再退回同月 —— 见 home_memories() 的说明。 */ ?>
|
||||
<h1 style="font-size:15px">
|
||||
往年的今天
|
||||
<span class="sub"><?= e(date('n 月 j 日')) ?></span>
|
||||
</h1>
|
||||
</div>
|
||||
<?php if (empty($memories['rows'])): ?>
|
||||
<?php if (empty($homeCards)): ?>
|
||||
<div class="card card-pad empty">
|
||||
<b>往年的这一天还没有记录</b>
|
||||
日子还长,等有了第一次,明年这时候就会自己冒出来。
|
||||
<b>首页卡片都被关掉了</b>
|
||||
到 <a href="<?= e(url('home/cards')) ?>">首页卡片</a> 里重新勾几张出来。
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card card-pad">
|
||||
<ul class="dash-list">
|
||||
<?php foreach ($memories['rows'] as $mm): ?>
|
||||
<li class="dash-row">
|
||||
<span class="dash-dot" style="background: <?= $mm['kind'] === 'trip' ? '#3F7D5A' : '#7A4E8C' ?>;"></span>
|
||||
<span class="dash-when"><?= (int)$mm['years_ago'] ?> 年前</span>
|
||||
<a class="dash-what" href="<?= e((string)$mm['href']) ?>">
|
||||
<?= $mm['kind'] === 'trip' ? '🧳 ' : '🎬 ' ?><?= e((string)$mm['title']) ?>
|
||||
</a>
|
||||
<span class="dash-tag"><?= e(substr((string)$mm['starts_at'], 0, 10)) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="hc-grid" id="homeCards">
|
||||
<?php foreach ($homeCards as $card): ?>
|
||||
<?php
|
||||
$hcKey = (string)($card['k'] ?? '');
|
||||
if (!\App\HomeCards::exists($hcKey)) {
|
||||
continue;
|
||||
}
|
||||
$hcDef = \App\HomeCards::TYPES[$hcKey];
|
||||
$hcTpl = __DIR__ . '/home/_card_' . $hcKey . '.php';
|
||||
if (!is_file($hcTpl)) {
|
||||
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())
|
||||
: '';
|
||||
?>
|
||||
<section class="card hc-card hc-w<?= (int)$card['w'] ?> hc-h<?= (int)$card['h'] ?>"
|
||||
data-hc="<?= e($hcKey) ?>" id="homeCard-<?= e($hcKey) ?>">
|
||||
<div class="hc-head">
|
||||
<h2 class="cinema-h2"><?= e((string)$hcDef['name']) ?></h2>
|
||||
<?php if ($hcMore !== ''): ?>
|
||||
<a class="btn btn-sm hd-more<?= $hcColor === '' ? ' btn-ghost' : '' ?>"
|
||||
href="<?= e($hcMore) ?>"<?= $hcColor !== '' ? ' style="--hd-c: ' . e($hcColor) . ';"' : '' ?>><?= e((string)$hcDef['more']) ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php require $hcTpl; ?>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<?php /* ==================== 首页任务复选框的表单与脚本(用户第 6 条)====================
|
||||
<?php /* ==================== 首页任务复选框的表单与脚本(v24 第 25 批 用户第 6 条)====================
|
||||
「在任何地方点复选框,这个任务就完成了」—— 首页也算"任何地方"。
|
||||
与任务看板同一套做法:**表单全页一份**,行上只放 data-tk-done。
|
||||
⚠️ 改完**不摘掉这一行**:用户点完正想看自己刚完成什么,
|
||||
立刻消失会让人怀疑"是不是点到删除了"。刷新时由服务端决定它还在不在。 */ ?>
|
||||
⚠️ 改完**不摘掉这一行**:用户点完正想看自己刚完成什么,立刻消失会让人怀疑
|
||||
"是不是点到删除了"。刷新时由服务端决定它还在不在。
|
||||
⚠️ 这一段是**页面级**的(不属于某张卡片):任务卡片被关掉后它也跟着没用,
|
||||
但留着无害 —— 别为了"省几行"把它挪进卡片模板里,那样卡片一换模板就丢功能。 */ ?>
|
||||
<?php if (!empty($tasks) && \App\Auth::canWrite()): ?>
|
||||
<form method="post" action="<?= e(url('tasks/status')) ?>" id="taskDoneForm" class="hidden-form">
|
||||
<?= csrf_field() ?>
|
||||
@@ -651,7 +222,6 @@ unset($evRef);
|
||||
b.setAttribute('data-tk-status', d.status);
|
||||
b.classList.toggle('is-done', d.status === 'done');
|
||||
b.setAttribute('aria-pressed', d.status === 'done' ? 'true' : 'false');
|
||||
// 状态徽标(那行「进行中」)跟着换,否则勾上了徽标还写着"进行中"
|
||||
var row = b.closest('[data-task-row]');
|
||||
var badge = row ? row.querySelector('.kb-st') : null;
|
||||
if (badge) {
|
||||
@@ -665,56 +235,3 @@ unset($evRef);
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ==================== 日程滚动区:进场定位到当前时刻(v24 第 26 批 用户第 2 条)======
|
||||
用户原话:「每次进来,都要定位到当前时刻,就是红线出现的位置,让红线在中间。」
|
||||
⇒ 打开首页时把**滚动容器**滚到一个位置,使"当前时刻"落在可视区**正中**。
|
||||
|
||||
⚠️ 触发条件:**今天有日程就定位**(只要 `$todayEvents` 非空,页面上就有
|
||||
`.ag-grid` 与红线)。曾经写成 `if (!$todayEvents || empty($timed))`,
|
||||
后果是**今天只有全天日程 / 全天日程都没有分时段的那种日子,脚本整段不输出**
|
||||
—— 而网格(0–24h)和红线照样渲染,于是"红线在,页面却停在最上面"
|
||||
(v29 用户第 1 条仍然报「没有定位到红线」,实测 `scrollTop=0`、脚本不在页面上,
|
||||
就是这个条件卡掉的;有分时段日程时定位其实完全正确、偏差 0px)。
|
||||
⇒ 只判"有没有网格可滚",别判"是不是分时段日程"。
|
||||
|
||||
⚠️ 用 `scrollTop = 目标偏移 - 容器高/2`,**不要**用 `scrollIntoView({block:'center'})`:
|
||||
它会**连带滚动外层页面**(把整个浏览器窗口也滚下去),
|
||||
用户一进首页就被拽到"页面中部" —— 那比不定位更糟。
|
||||
直接给容器赋 `scrollTop` 只动这一个盒子,外层纹丝不动。
|
||||
⚠️ 容器"没得滚"时(内容比容器矮)赋任何值都是 0,天然安全,不必特判。
|
||||
⚠️ 目标位置**由服务端算好**传进来(`$nowPos`),不在 JS 里再算一遍时间 ——
|
||||
两处各算一次迟早不一致(服务端用的是站点时区,JS 用的是浏览器时区)。 */ ?>
|
||||
<?php if (!$todayEvents): ?>
|
||||
<?php /* 今天一条日程都没有 → 页面是「今天没有安排」的空态,没有网格可滚 */ ?>
|
||||
<?php else: ?>
|
||||
<script>
|
||||
(function () {
|
||||
var box = document.querySelector('[data-ag-scroll]');
|
||||
if (!box) { return; }
|
||||
// 红线(在的话)优先;不在就用服务端算好的 nowPos 现算一个位置。
|
||||
var line = box.querySelector('.ag-now');
|
||||
var RATIO = <?= json_encode(round($nowPos, 6)) ?>; // 0~1(可能落在区间外)
|
||||
function targetOffset() {
|
||||
if (line) { return line.offsetTop; }
|
||||
var grid = box.querySelector('.ag-grid');
|
||||
if (!grid) { return 0; }
|
||||
// ⚠️ 用**排掉全天区之后**的内容高度算:全天区现在不在滚动容器里,
|
||||
// 所以 grid 的高度就是"整条时间轴"的高度(与服务端算 nowPos 的口径一致)。
|
||||
return grid.offsetHeight * RATIO;
|
||||
}
|
||||
function centerOnNow() {
|
||||
// offsetTop 相对**定位祖先**(.ag-grid,position:relative),即"内容坐标"
|
||||
box.scrollTop = Math.max(0, targetOffset() - (box.clientHeight / 2));
|
||||
}
|
||||
centerOnNow();
|
||||
/* ⚠️ 再补两拍:字体加载完 / 浏览器恢复滚动位置(back-forward cache)之后,
|
||||
内容高度会变,第一次算出来的 scrollTop 可能偏。
|
||||
用 rAF 而不是 setTimeout:rAF 在排版完成后跑,时序更准;
|
||||
两拍是为了覆盖"布局在渲染后才最终确定"的那一帧。 */
|
||||
if (window.requestAnimationFrame) {
|
||||
requestAnimationFrame(function () { requestAnimationFrame(centerOnNow); });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页卡片 · 今天的日程(v62 从 templates/home.php 拆出来的,逻辑一字未改)。
|
||||
*
|
||||
* 用户原话(v24 第 24 批 第 3 条):「日程就显示今天的日程,也就是日视图那样形式,
|
||||
* 为了好看高度需要固定。这样的话,鼠标放上去之后,向下可以滚动,暂时不要出现
|
||||
* 滚动条太丑了。为了节省上下滚动的这个空间,我们可以把没有日程的那些时间
|
||||
* 掐头去尾给它去掉。…需要在当前的时间或者是时刻那里显示一条红线。」
|
||||
*
|
||||
* ⚠️ 高度现在由**卡片高度**决定(`.hc-h*`),不再写死 440px —— 这正是"卡片化"
|
||||
* 的核心:同一份内容,用户让它多高它就多高。
|
||||
*
|
||||
* @var array $todayEvents 今天的事件(全天在前、其余按时刻升序)
|
||||
*/
|
||||
$allDay = [];
|
||||
$timed = [];
|
||||
foreach (($todayEvents ?? []) as $ev) {
|
||||
if ((string)($ev['time'] ?? '') === '') {
|
||||
$allDay[] = $ev;
|
||||
} else {
|
||||
$timed[] = $ev;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 掐头去尾:只画"第一个日程前一小时"到"最后一个日程后一小时" ----
|
||||
$startH = 0;
|
||||
$endH = 24;
|
||||
if ($timed) {
|
||||
$mins = array_map(static function (array $e): int {
|
||||
// ⚠️ 变量名带 `ag` 前缀:这是**被 require 进来的片段**,裸赋值 $p 会撞上
|
||||
// 宿主模板里同名的 $p(流水线的"片段不许覆盖宿主变量"检查会拦下来)。
|
||||
$agHM = explode(':', (string)$e['time']);
|
||||
return ((int)($agHM[0] ?? 0)) * 60 + (int)($agHM[1] ?? 0);
|
||||
}, $timed);
|
||||
$startH = max(0, intdiv(min($mins), 60) - 1);
|
||||
$endH = min(24, intdiv(max($mins), 60) + 2);
|
||||
/* 🔴 v28 用户第 1 条:「今天的日程,并没有定位到红线所在的位置」。
|
||||
根因:**区间里没有"现在"**,于是红线不画、定位算出来是越界值。
|
||||
⇒ 让区间永远包含当前时刻(用户要的正是"一眼看到现在处在哪儿")。
|
||||
🔴🔴 v44 第 3 条:首页红线"停在整点"的真根因是这里以前用 `date('G')`
|
||||
(只取小时),而日程页用的是 `小时*60+分钟`。现在两处同一口径。 */
|
||||
$nowHNow = (int)date('G');
|
||||
if ($nowHNow < $startH) {
|
||||
$startH = max(0, $nowHNow - 1);
|
||||
}
|
||||
if ($nowHNow >= $endH) {
|
||||
$endH = min(24, $nowHNow + 2);
|
||||
}
|
||||
if ($endH <= $startH) {
|
||||
$endH = min(24, $startH + 2);
|
||||
}
|
||||
}
|
||||
$nowMinutes = ((int)date('G')) * 60 + (int)date('i');
|
||||
$nowPos = (($nowMinutes / 60) - $startH) / max(1, ($endH - $startH));
|
||||
$showNow = $nowPos >= 0 && $nowPos <= 1;
|
||||
|
||||
/* 冲突分列(v30 第 2/4 条):与日程页同一套算法 —— 两条撞车就左右各半。 */
|
||||
usort($timed, static function (array $a, array $b): int {
|
||||
return strcmp((string)($a['time'] ?? ''), (string)($b['time'] ?? ''));
|
||||
});
|
||||
$laneInfo = \App\Calendar::conflictLanes($timed);
|
||||
foreach ($timed as $i => &$evRef) {
|
||||
[$l, $w] = $laneInfo[$i] ?? [0.0, 1.0];
|
||||
$evRef['left'] = round($l * 100, 4);
|
||||
$evRef['width'] = round($w * 100, 4);
|
||||
}
|
||||
unset($evRef);
|
||||
?>
|
||||
<div class="hc-body hc-agenda">
|
||||
<?php if (!$todayEvents): ?>
|
||||
<div class="hd-pane-body">
|
||||
<p class="empty-hint">今天没有安排。</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php /* 全天日程**不在滚动容器里**(v24 第 26 批 第 2 条:要"一直显示在上面")。
|
||||
为什么不用 sticky:sticky 只能粘在滚动区顶端,滚起来仍会被"标题+滚动区"
|
||||
两层挤着,看起来还是在动;直接移出滚动容器是零副作用的做法。 */ ?>
|
||||
<?php if ($allDay): ?>
|
||||
<div class="ag-allday">
|
||||
<?php foreach ($allDay as $ev): ?>
|
||||
<?php
|
||||
$evKind = (string)($ev['kind'] ?? 'custom');
|
||||
$evRef = (int)($ev['ref_id'] ?? 0);
|
||||
$evEdit = '';
|
||||
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
||||
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
||||
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||
?>
|
||||
<a class="ag-allday-item" href="<?= e((string)$ev['href']) ?>"
|
||||
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'>
|
||||
<span class="dash-dot" style="background:<?= e((string)$ev['color']) ?>;"></span>
|
||||
<span class="ag-ad-t">全天</span>
|
||||
<span class="ag-ad-w"><?= e((string)$ev['title']) ?></span>
|
||||
<span class="dash-cal" style="background:<?= e((string)$ev['color']) ?>;"><?= e((string)$ev['calname']) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="hd-pane-body" data-ag-scroll>
|
||||
<div class="ag-grid">
|
||||
<?php for ($h = $startH; $h < $endH; $h++): ?>
|
||||
<div class="ag-row">
|
||||
<span class="ag-hour"><?= sprintf('%02d:00', $h) ?></span>
|
||||
<span class="ag-line"></span>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
<?php if ($showNow): ?>
|
||||
<?php /* `data-now-min` 是给探针的零容差观测点(服务端用的分钟数),
|
||||
不影响渲染 —— 见 v45 那条注释。 */ ?>
|
||||
<div class="ag-now" style="top: <?= number_format($nowPos * 100, 2) ?>%"
|
||||
data-now-min="<?= (int)$nowMinutes ?>"></div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
/* 日程块:按"相对第一个刻度的小时数"定位。
|
||||
⚠️ left/width 与日历页 `.tg-block` 同一套(left = 46px 刻度 + 2px,
|
||||
width = 列宽 - 4px)—— 曾经这里扣了 46px 的缝,导致两块之间空出 50px。 */
|
||||
foreach ($timed as $ev):
|
||||
$agHM = explode(':', (string)$ev['time']);
|
||||
$evMin = ((int)($agHM[0] ?? 0)) * 60 + ((int)($agHM[1] ?? 0));
|
||||
$topPct = (($evMin / 60) - $startH) / max(1, ($endH - $startH)) * 100;
|
||||
$topPct = max(0, min(96, $topPct));
|
||||
$et2 = (string)($ev['end_time'] ?? '');
|
||||
if ($et2 !== '' && preg_match('/^(\d{1,2}):(\d{2})/', $et2, $mm2)) {
|
||||
$endMin = ((int)$mm2[1]) * 60 + ((int)$mm2[2]);
|
||||
} else {
|
||||
$endMin = $evMin + 30;
|
||||
}
|
||||
if ($endMin <= $evMin) { $endMin = $evMin + 30; }
|
||||
$endMin = min($endMin, $endH * 60);
|
||||
$hPct = ($endMin / 60 - $startH) / max(1, ($endH - $startH)) * 100 - $topPct;
|
||||
$hPct = max(2.2, $hPct);
|
||||
$evKind = (string)($ev['kind'] ?? 'custom');
|
||||
$evRef = (int)($ev['ref_id'] ?? 0);
|
||||
$evEdit = '';
|
||||
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
||||
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
||||
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||
?>
|
||||
<a class="ag-ev" href="<?= e((string)$ev['href']) ?>"
|
||||
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
||||
style="background: <?= e((string)$ev['color']) ?>;
|
||||
top: <?= number_format($topPct, 2) ?>%;
|
||||
height: <?= number_format($hPct, 2) ?>%;
|
||||
left: calc(46px + <?= (float)($ev['left'] ?? 0) ?>% + 2px);
|
||||
width: calc(<?= (float)($ev['width'] ?? 100) ?>% - 4px);"
|
||||
title="<?= e((string)$ev['title']) ?>"
|
||||
data-cal="<?= (int)$ev['cal'] ?>">
|
||||
<span class="ag-ev-t"><?= e((string)$ev['time']) ?></span>
|
||||
<span class="ag-ev-w"><?= e((string)$ev['title']) ?></span>
|
||||
<span class="ag-ev-cal"><?= e((string)$ev['calname']) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($todayEvents): ?>
|
||||
<script>
|
||||
/* 进场定位到"现在"(v24 第 26 批 第 2 条):让红线落在可视区**正中**。
|
||||
⚠️ 用 scrollTop 直接赋值,**不要** scrollIntoView({block:'center'}) ——
|
||||
后者会把整个浏览器窗口也滚下去,一进首页就被拽到页面中部。
|
||||
⚠️ 只判"有没有网格可滚",别判"有没有分时段日程"(v29 踩过:只有全天日程时
|
||||
脚本整段不输出,而网格和红线照样渲染 ⇒ 红线在、页面停在最上面)。 */
|
||||
(function () {
|
||||
var box = document.querySelector('#homeCard-agenda [data-ag-scroll]')
|
||||
|| document.querySelector('[data-ag-scroll]');
|
||||
if (!box) { return; }
|
||||
var line = box.querySelector('.ag-now');
|
||||
var RATIO = <?= json_encode(round($nowPos, 6)) ?>;
|
||||
function targetOffset() {
|
||||
if (line) { return line.offsetTop; }
|
||||
var grid = box.querySelector('.ag-grid');
|
||||
return grid ? grid.offsetHeight * RATIO : 0;
|
||||
}
|
||||
function centerOnNow() {
|
||||
box.scrollTop = Math.max(0, targetOffset() - (box.clientHeight / 2));
|
||||
}
|
||||
centerOnNow();
|
||||
if (window.requestAnimationFrame) {
|
||||
requestAnimationFrame(function () { requestAnimationFrame(centerOnNow); });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页卡片 · 读书(v62 新增)。
|
||||
*
|
||||
* 数据来自 `HomeCards::booksData()`:**在读优先**,其余按新加进来的顺序排(最多 6 本)。
|
||||
*
|
||||
* @var array $cardData['books'] 每项含 id/title/author/status/progress/chapter_count/cover_key
|
||||
*/
|
||||
$bkRows = (array)($cardData['books'] ?? []);
|
||||
?>
|
||||
<div class="hc-body">
|
||||
<?php if (!$bkRows): ?>
|
||||
<p class="empty-hint">书架还是空的。</p>
|
||||
<?php else: ?>
|
||||
<ul class="hc-bk-list">
|
||||
<?php foreach ($bkRows as $b): ?>
|
||||
<?php
|
||||
$bkStatus = \App\Book::normalizeStatus((string)($b['status'] ?? 'wish'));
|
||||
$bkCh = (int)($b['chapter_count'] ?? 0);
|
||||
$bkPr = (int)($b['progress'] ?? 0);
|
||||
$bkPct = $bkCh > 0 ? min(100, (int)round($bkPr / $bkCh * 100)) : 0;
|
||||
?>
|
||||
<li class="hc-bk">
|
||||
<a class="hc-bk-cover" href="<?= e(url('books/show', ['id' => (int)$b['id']])) ?>">
|
||||
<?php if (!empty($b['cover_key'])): ?>
|
||||
<img src="<?= e(media_url((string)$b['cover_key'])) ?>" alt="" loading="lazy">
|
||||
<?php else: ?>
|
||||
<span class="hc-bk-ph" aria-hidden="true">📖</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<div class="hc-bk-main">
|
||||
<a class="hc-bk-t" href="<?= e(url('books/show', ['id' => (int)$b['id']])) ?>"
|
||||
title="<?= e((string)$b['title']) ?>"><?= e((string)$b['title']) ?></a>
|
||||
<span class="small muted">
|
||||
<?= e((string)($b['author'] ?? '')) ?>
|
||||
<?php if ($bkCh > 0): ?>
|
||||
· <?= $bkPr ?>/<?= $bkCh ?> 章
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<span class="hc-bk-bar" aria-hidden="true"><i style="width: <?= (int)$bkPct ?>%"></i></span>
|
||||
</div>
|
||||
<span class="hc-bk-st is-<?= e($bkStatus) ?>"><?= e(\App\Book::statusWord($bkStatus)) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页卡片 · 概述(v62 从 templates/home.php 拆出来的)。
|
||||
*
|
||||
* 内容与 v24 第 24 批 用户第 2 条完全一致(问候 → 日期/星期/农历 → 日子标记 →
|
||||
* 今日数字 | 右侧彩色天气图标 + 温度/风力 + 地址),只换了外壳:
|
||||
* 以前是 `section.section > div.card.home-brief`,现在是卡片的 `.hc-body`。
|
||||
*
|
||||
* ⚠️ 这块**永远是横贯的**(默认 8 宽):它是"首页第一眼",被挤成一半宽就会折行折得很碎。
|
||||
* ⚠️ 用到的变量都由 home 路由传进来($hbPart 除外,这里自己按时段算)。
|
||||
*/
|
||||
$hbPart = '你好';
|
||||
$hbH = (int)date('G');
|
||||
if ($hbH < 5) { $hbPart = '夜深了'; }
|
||||
elseif ($hbH < 11) { $hbPart = '早上好'; }
|
||||
elseif ($hbH < 13) { $hbPart = '中午好'; }
|
||||
elseif ($hbH < 18) { $hbPart = '下午好'; }
|
||||
else { $hbPart = '晚上好'; }
|
||||
|
||||
$nDoing = 0;
|
||||
$nTodo = 0;
|
||||
foreach (($tasks ?? []) as $__t) {
|
||||
if ((string)($__t['status'] ?? 'todo') === 'doing') {
|
||||
$nDoing++;
|
||||
} else {
|
||||
$nTodo++;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="hc-body home-brief">
|
||||
<div class="hb-left">
|
||||
<div class="hb-hello">
|
||||
<b><?= e($hbPart) ?></b><?php if (trim((string)($meName ?? '')) !== ''): ?>,<?= e((string)$meName) ?><?php endif; ?>
|
||||
</div>
|
||||
<div class="hb-date">
|
||||
<span class="hb-d"><?= e((string)($todayLabel ?? date('n 月 j 日'))) ?></span>
|
||||
<span class="hb-w"><?= e((string)($weekday ?? '')) ?></span>
|
||||
<?php if (($lunarShort ?? '') !== ''): ?>
|
||||
<span class="hb-lunar" title="<?= e((string)($lunarLine ?? '')) ?>"><?= e((string)$lunarShort) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if (!empty($todayMarks['tags'])): ?>
|
||||
<div class="hb-marks">
|
||||
<?php foreach ($todayMarks['tags'] as $mk): ?>
|
||||
<span class="hb-mark is-<?= e((string)$mk['kind']) ?>"><?= e((string)$mk['word']) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="hb-stats">
|
||||
<a class="hbs" href="<?= e(url('calendar')) ?>">
|
||||
<b><?= count($todayEvents ?? []) ?></b> 条日程(今天)
|
||||
</a>
|
||||
<?php /* 两个数字与右侧「近期任务」卡片**同源**(同一个 $tasks 数组),
|
||||
「在做」与「待办」按状态拆开数 —— 见 v53 第 2 条那条注释。 */ ?>
|
||||
<a class="hbs" href="<?= e(url('tasks')) ?>" title="近期任务(进行中)">
|
||||
<b><?= $nDoing ?></b> 件在做
|
||||
</a>
|
||||
<?php if ($nTodo > 0): ?>
|
||||
<a class="hbs" href="<?= e(url('tasks')) ?>" title="记下来了、还没动手的那些">
|
||||
<b><?= $nTodo ?></b> 件待办
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ((int)($stat7['overdue'] ?? 0) > 0): ?>
|
||||
<a class="hbs is-late" href="<?= e(url('tasks')) ?>">
|
||||
<b><?= (int)$stat7['overdue'] ?></b> 件逾期
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($weather)): ?>
|
||||
<div class="hb-weather is-<?= e((string)($weather['tone'] ?? 'cloud')) ?>">
|
||||
<span class="hbw-icon" aria-hidden="true"><?= e((string)($weather['icon'] ?? '')) ?></span>
|
||||
<div class="hbw-main">
|
||||
<div class="hbw-temp"><?= e((string)($weather['temp'] ?? '')) ?></div>
|
||||
<div class="hbw-text"><?= e((string)($weather['text'] ?? '')) ?></div>
|
||||
<?php if (($weather['wind'] ?? '') !== ''): ?>
|
||||
<div class="hbw-wind">
|
||||
<?= $weather['wdir'] !== '' ? e((string)$weather['wdir']) . ' ' : '' ?><?= e((string)$weather['wind']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if (($weather['place'] ?? '') !== ''): ?>
|
||||
<div class="hbw-place"><?= e((string)$weather['place']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页卡片 · 联系人(v62 新增)。
|
||||
*
|
||||
* 用户原话:「例如,联系人,可以有卡片,让用户选择显示几个联系人在首页上。」
|
||||
* ⇒ 张数由卡片设置里的「显示 N 个」决定(`HomeCards::contactsData($familyId,$me,$n)`);
|
||||
* 卡片高度不够时内部滚动 —— 不留一个"被截断但看不出来"的列表。
|
||||
*
|
||||
* ⚠️ 只列**我的名册**里的人(含别人分享给我的),不是全家所有人 —— 首页是"我的"。
|
||||
*
|
||||
* @var array $cardData['contacts'] ['rows'=>array,'total'=>int]
|
||||
*/
|
||||
$ctRows = (array)($cardData['contacts']['rows'] ?? []);
|
||||
$ctTotal = (int)($cardData['contacts']['total'] ?? 0);
|
||||
?>
|
||||
<div class="hc-body">
|
||||
<?php if (!$ctRows): ?>
|
||||
<p class="empty-hint">名册里还没有人。</p>
|
||||
<?php else: ?>
|
||||
<div class="hc-people">
|
||||
<?php foreach ($ctRows as $p): ?>
|
||||
<?php /* 头像 + 称呼(`person_chip` 自带底色与"我该怎么叫他")。
|
||||
⚠️ 这里用块级排列而不是一行一个 pill:卡片可能很窄(宽 2),
|
||||
pill 会挤成一列文字。 */ ?>
|
||||
<a class="hc-person" href="<?= e(url('people/show', ['id' => (int)$p['id']])) ?>"
|
||||
title="<?= e(person_name($p)) ?>">
|
||||
<?php if (!empty($p['avatar'])): ?>
|
||||
<img class="hc-person-av" src="<?= e(media_url((string)$p['avatar'])) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<span class="hc-person-av hc-person-tx"
|
||||
style="background: <?= e(person_bg($p)) ?>; color: <?= e(person_fg($p)) ?>;">
|
||||
<?= e(mb_substr(person_name($p), 0, 1, 'UTF-8')) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<span class="hc-person-nm"><?= e(person_name($p)) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if ($ctTotal > count($ctRows)): ?>
|
||||
<p class="small muted" style="margin:8px 0 0">共 <?= $ctTotal ?> 人,这里显示 <?= count($ctRows) ?> 位。</p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页卡片 · 纪念日(v62 新增)。
|
||||
*
|
||||
* 数据来自 `Anniversary::upcoming($familyId, $me, 60)`(与「纪念日」页面顶上那块**同一份**),
|
||||
* 所以"首页看到的"和"点进去看到的"永远一致 —— 不要在卡片里自己再算一遍农历。
|
||||
*
|
||||
* ⚠️ 图标按**分类**给(v62 第 2 条):走 `AnnivIcon::svg()`,颜色跟着文字色;
|
||||
* 与日历里的 emoji 是同一套 key(见 AnnivIcon 的类注释)。
|
||||
*
|
||||
* @var array $cardData['days'] 每项含 date/title/kind/icon/age/nth/days
|
||||
*/
|
||||
$annRows = (array)($cardData['days'] ?? []);
|
||||
?>
|
||||
<div class="hc-body">
|
||||
<?php if (!$annRows): ?>
|
||||
<p class="empty-hint">未来 60 天里没有纪念日。</p>
|
||||
<?php else: ?>
|
||||
<ul class="ann-up hc-ann">
|
||||
<?php foreach ($annRows as $a): ?>
|
||||
<li class="ann-up-item">
|
||||
<span class="ann-days"><?= (int)$a['days'] === 0 ? '今天' : ((int)$a['days'] . ' 天后') ?></span>
|
||||
<span class="ann-ic"><?= \App\AnnivIcon::svg((string)($a['icon'] ?? ''), 15) ?></span>
|
||||
<span class="ann-title"><?= e((string)$a['title']) ?></span>
|
||||
<?php if (($a['age'] ?? null) !== null): ?>
|
||||
<span class="small muted"><?= (int)$a['age'] ?> 岁</span>
|
||||
<?php elseif (($a['nth'] ?? null) !== null): ?>
|
||||
<span class="small muted">第 <?= (int)$a['nth'] ?> 个</span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页卡片 · 往年的今天(v62 从 templates/home.php 拆出来的)。
|
||||
*
|
||||
* ⚠️ 只认「往年的今天」,**不退回同月**(v24 第 19 条)—— 见 home_memories() 的说明。
|
||||
* @var array $memories ['rows' => [['kind','years_ago','title','href','starts_at'], ...]]
|
||||
*/
|
||||
$mmRows = (array)($memories['rows'] ?? []);
|
||||
?>
|
||||
<div class="hc-body">
|
||||
<?php if (!$mmRows): ?>
|
||||
<div class="empty">
|
||||
<b>往年的这一天还没有记录</b>
|
||||
日子还长,等有了第一次,明年这时候就会自己冒出来。
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<ul class="dash-list">
|
||||
<?php foreach ($mmRows as $mm): ?>
|
||||
<li class="dash-row">
|
||||
<span class="dash-dot" style="background: <?= $mm['kind'] === 'trip' ? '#3F7D5A' : '#7A4E8C' ?>;"></span>
|
||||
<span class="dash-when"><?= (int)$mm['years_ago'] ?> 年前</span>
|
||||
<a class="dash-what" href="<?= e((string)$mm['href']) ?>">
|
||||
<?= $mm['kind'] === 'trip' ? '🧳 ' : '🎬 ' ?><?= e((string)$mm['title']) ?>
|
||||
</a>
|
||||
<span class="dash-tag"><?= e(substr((string)$mm['starts_at'], 0, 10)) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页卡片 · 观影(v62 新增)。
|
||||
*
|
||||
* 数据来自 `HomeCards::moviesData()`(最近 8 条,只查卡片要用的那几列 ——
|
||||
* 不用 Movie::all():那个会顺带查同行人与影评统计,卡片上都不显示)。
|
||||
*
|
||||
* @var array $cardData['movies'] 每项含 id/title/starts_at/poster_key/cinema_name
|
||||
*/
|
||||
$mvRows = (array)($cardData['movies'] ?? []);
|
||||
?>
|
||||
<div class="hc-body">
|
||||
<?php if (!$mvRows): ?>
|
||||
<p class="empty-hint">还没有观影记录。</p>
|
||||
<?php else: ?>
|
||||
<ul class="hc-mv-list">
|
||||
<?php foreach ($mvRows as $m): ?>
|
||||
<li class="hc-mv">
|
||||
<a class="hc-mv-poster" href="<?= e(url('movies/show', ['id' => (int)$m['id']])) ?>">
|
||||
<?php if (!empty($m['poster_key'])): ?>
|
||||
<img src="<?= e(media_url((string)$m['poster_key'])) ?>" alt="" loading="lazy">
|
||||
<?php else: ?>
|
||||
<span class="hc-mv-ph" aria-hidden="true">🎬</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<div class="hc-mv-main">
|
||||
<a class="hc-mv-t" href="<?= e(url('movies/show', ['id' => (int)$m['id']])) ?>"
|
||||
title="<?= e((string)$m['title']) ?>"><?= e((string)$m['title']) ?></a>
|
||||
<span class="small muted">
|
||||
<?= e(substr((string)($m['starts_at'] ?? ''), 0, 10)) ?>
|
||||
<?php if (trim((string)($m['cinema_name'] ?? '')) !== ''): ?>
|
||||
· <?= e((string)$m['cinema_name']) ?>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页卡片 · 近期任务(v62 从 templates/home.php 拆出来的)。
|
||||
*
|
||||
* 用户原话(v24 第 24 批 第 3 条):「右侧显示任务模块里面的数据,显示当前正在
|
||||
* 进行当中的所有的任务,除了显示任务的标题之外,还要显示他的状态。比如说正在
|
||||
* 进行中待确认等等。除了这之外,还要显示他所处的看板的名称。」
|
||||
*
|
||||
* 🔴 v53 第 2 条:待办也放进来,放在进行中的**下面**(不是混排)。
|
||||
* ⚠️ 每行前面有复选框,点了就地完成(AJAX,不跳页);表单与脚本在 home.php 末尾
|
||||
* 各一份 —— 那份**必须留着**,别跟着卡片一起删。
|
||||
* ⚠️ 标题的颜色跟着**看板色**(不是任务模块的主题色),见 v32 第 4 条那条注释。
|
||||
*/
|
||||
?>
|
||||
<div class="hc-body">
|
||||
<?php if (!$tasks): ?>
|
||||
<p class="empty-hint">没有进行中的任务,也没有待办。</p>
|
||||
<?php else: ?>
|
||||
<ul class="dash-list tk-list">
|
||||
<?php foreach ($tasks as $t): ?>
|
||||
<?php $late = \App\Task::isOverdue($t['due_at'] ?? null); ?>
|
||||
<?php $tkColor = \App\Task::boardColorById((int)\App\Auth::familyId(),
|
||||
(int)($t['board_id'] ?? 0) ?: null); ?>
|
||||
<li class="dash-row tk-row" data-task-row="<?= (int)$t['id'] ?>"
|
||||
style="--tk-c: <?= e($tkColor) ?>;">
|
||||
<?php
|
||||
$tkId = (int)$t['id']; $tkStatus = (string)($t['status'] ?? 'todo');
|
||||
$tkBack = 'home'; $tkStatic = !\App\Auth::canWrite();
|
||||
require __DIR__ . '/../_task_check.php';
|
||||
?>
|
||||
<span class="dash-dot" style="background: <?= e($late ? 'var(--red)' : 'var(--tk-c)') ?>;"></span>
|
||||
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>"
|
||||
title="<?= e((string)$t['title']) ?>"
|
||||
data-fb-entry='<?= fb_entry_json([
|
||||
'title' => (string)$t['title'],
|
||||
'status_word' => \App\Task::statusWord((string)($t['status'] ?? 'todo')),
|
||||
'note' => (string)($t['note'] ?? ''),
|
||||
], url('tasks/show', ['id' => (int)$t['id']]),
|
||||
\App\Auth::canWrite() ? url('tasks/edit', ['id' => (int)$t['id']]) : '',
|
||||
[['截止', ($t['due_at'] ?? '') !== '' ? substr((string)$t['due_at'], 0, 16) : ''],
|
||||
['看板', trim((string)($t['board_name'] ?? ''))]]) ?>'
|
||||
style="color: <?= e($late ? 'var(--red)' : $tkColor) ?>;">
|
||||
<?= e((string)$t['title']) ?>
|
||||
</a>
|
||||
<span class="kb-st is-<?= e((string)($t['status'] ?? 'todo')) ?> is-static">
|
||||
<?= e(\App\Task::statusWord((string)($t['status'] ?? 'todo'))) ?>
|
||||
</span>
|
||||
<?php if (trim((string)($t['board_name'] ?? '')) !== ''): ?>
|
||||
<?php $bColor = \App\Task::boardColor($t['board_color'] ?? null, (string)$t['board_name']); ?>
|
||||
<span class="tk-board" style="--bc: <?= e($bColor) ?>;">
|
||||
<i class="tk-board-dot" aria-hidden="true"></i><?= e((string)$t['board_name']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ((int)($t['child_total'] ?? 0) > 0): ?>
|
||||
<span class="dash-tag"><?= (int)$t['child_done'] ?>/<?= (int)$t['child_total'] ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (($t['due_at'] ?? '') !== ''): ?>
|
||||
<span class="tk-due"><?= e(\App\Task::dueWord($t['due_at'] ?? null)) ?></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
/**
|
||||
* 首页卡片布局(v62 用户 2026-10-18 第 3 条)。
|
||||
*
|
||||
* 用户原话:「今天,我们正式引入卡片的概念……卡片的规格,我们可以定义一下,
|
||||
* 例如,把宽度定义为 8,高度可以是 2、4、6、8……如果是 2x2,那么就可以
|
||||
* 一排放 4 个,如果是 2x8,那就是一行放一个,类似于这样的定义。
|
||||
* 你可以先规划一些,实现出来,我们再讨论。」
|
||||
*
|
||||
* ⇒ 这一页是**摆卡片**的地方:勾上要哪几张、各自多宽多高、什么顺序。
|
||||
* 尺寸口径(与 templates/home.php / app.css 的 `.hc-*` 完全一致):
|
||||
* · 一排 **8 格**;宽 = 占几格 → 2:一排 4 张 / 4:一排 2 张 / 8:独占一排;
|
||||
* · 高 = 占几行 → 2 / 4 / 6 / 8(一行 76px + 10px 间距)。
|
||||
* ⚠️ 卡片宽度**允许 6**(不是只有 2/4/8):6 宽 = 一排"一大一小",
|
||||
* 这是 8 格制里很自然的一种排法,实现上零成本,就没必要禁掉。
|
||||
*
|
||||
* 交互:▲▼ 与拖动都能排序(拖动的状态只存在于 DOM 里,提交时按 DOM 顺序读 order[])。
|
||||
*
|
||||
* @param array $rows HomeCards::orderedForPicker() 的结果(含 on/w/h/n)
|
||||
*/
|
||||
$hcRows = (array)($rows ?? []);
|
||||
$hcN = count($hcRows);
|
||||
$hcWs = \App\HomeCards::WIDTHS;
|
||||
$hcHs = \App\HomeCards::HEIGHTS;
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>首页卡片</h1>
|
||||
<span class="sub">一排 8 格 · 宽 2 就是一排 4 张,宽 8 就是独占一排</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e(url('home')) ?>">返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?= e(url('home/cards_save')) ?>" class="card card-pad">
|
||||
<?= csrf_field() ?>
|
||||
<h2 class="cinema-h2">
|
||||
摆哪几张、多大
|
||||
<span class="hint">只影响你自己看到的首页</span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 16px">
|
||||
<b>宽</b>:2 = 一排 4 张,4 = 一排 2 张,6 = 一大一小,8 = 独占一排。
|
||||
<b>高</b>:2 / 4 / 6 / 8 行(一行 76px),卡片内容多的时候内部会自己滚。
|
||||
<br>关掉的卡片<b>不会丢数据</b>,只是不摆在首页上。
|
||||
</p>
|
||||
|
||||
<ol class="hc-set-list" id="hcSetList">
|
||||
<?php foreach ($hcRows as $i => $r): $k = (string)$r['k']; $def = \App\HomeCards::TYPES[$k]; ?>
|
||||
<li class="hc-set-row" draggable="true" data-hc-key="<?= e($k) ?>">
|
||||
<?php /* 顺序藏在 DOM 里:每行自带 order[],拖动搬整行 ⇒ 提交上来的顺序
|
||||
就是屏幕上的顺序(与「模块设置」那张表同一手法)。 */ ?>
|
||||
<input type="hidden" name="order[]" value="<?= e($k) ?>">
|
||||
<span class="hc-set-order">
|
||||
<span class="drag-handle" title="拖我排序" aria-hidden="true">⋮⋮</span>
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="up:<?= e($k) ?>"
|
||||
title="上移" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
|
||||
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="down:<?= e($k) ?>"
|
||||
title="下移" <?= $i === $hcN - 1 ? 'disabled' : '' ?>>▼</button>
|
||||
</span>
|
||||
<label class="hc-set-on">
|
||||
<input type="checkbox" name="on[<?= e($k) ?>]" value="1" <?= !empty($r['on']) ? 'checked' : '' ?>>
|
||||
<span class="hc-set-name">
|
||||
<b><?= e((string)$def['name']) ?></b>
|
||||
<span class="small muted"><?= e((string)$def['desc']) ?></span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="hc-set-dim">
|
||||
<span class="small muted">宽</span>
|
||||
<select class="select" name="w[<?= e($k) ?>]">
|
||||
<?php foreach ($hcWs as $w): ?>
|
||||
<option value="<?= (int)$w ?>" <?= (int)$r['w'] === (int)$w ? 'selected' : '' ?>><?= (int)$w ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label class="hc-set-dim">
|
||||
<span class="small muted">高</span>
|
||||
<select class="select" name="h[<?= e($k) ?>]">
|
||||
<?php foreach ($hcHs as $h): ?>
|
||||
<option value="<?= (int)$h ?>" <?= (int)$r['h'] === (int)$h ? 'selected' : '' ?>><?= (int)$h ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<?php if ((int)$def['n'] > 0): ?>
|
||||
<?php /* 用户原话:「联系人,可以有卡片,让用户选择显示几个联系人在首页上。」 */ ?>
|
||||
<label class="hc-set-dim">
|
||||
<span class="small muted">显示</span>
|
||||
<input class="input hc-set-n" type="number" name="n[<?= e($k) ?>]"
|
||||
min="1" max="<?= (int)\App\HomeCards::N_MAX ?>"
|
||||
value="<?= (int)$r['n'] ?>" aria-label="显示几个联系人">
|
||||
<span class="small muted">个</span>
|
||||
</label>
|
||||
<?php else: ?>
|
||||
<span class="hc-set-dim hc-set-null"></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
|
||||
<div class="btn-row" style="margin-top:16px">
|
||||
<button class="btn" type="submit" name="save" value="1">保存布局</button>
|
||||
<span class="small muted">改完记得保存 —— 这一页不会边改边生效。</span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
/* 拖动排序(▲▼ 是没有 JS 时的兜底,两条路都能用)。
|
||||
⚠️ 搬的是整行 <li>:它自带的 order[] 隐藏域跟着走,
|
||||
提交时按 DOM 顺序读,不用再同步一份顺序数组。 */
|
||||
var list = document.getElementById('hcSetList');
|
||||
if (!list) { return; }
|
||||
var dragEl = null;
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.hc-set-row'), function (li) {
|
||||
li.addEventListener('dragstart', function (ev) {
|
||||
dragEl = li;
|
||||
li.classList.add('is-dragged');
|
||||
if (ev.dataTransfer) { ev.dataTransfer.effectAllowed = 'move'; }
|
||||
});
|
||||
li.addEventListener('dragend', function () {
|
||||
li.classList.remove('is-dragged');
|
||||
dragEl = null;
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.hc-set-row'), function (x) {
|
||||
x.classList.remove('is-drop-before');
|
||||
});
|
||||
});
|
||||
li.addEventListener('dragover', function (ev) {
|
||||
if (!dragEl || dragEl === li) { return; }
|
||||
ev.preventDefault();
|
||||
var box = li.getBoundingClientRect();
|
||||
li.classList.toggle('is-drop-before', ev.clientY < box.top + box.height / 2);
|
||||
});
|
||||
li.addEventListener('dragleave', function () { li.classList.remove('is-drop-before'); });
|
||||
li.addEventListener('drop', function (ev) {
|
||||
if (!dragEl || dragEl === li) { return; }
|
||||
ev.preventDefault();
|
||||
var box = li.getBoundingClientRect();
|
||||
if (ev.clientY < box.top + box.height / 2) {
|
||||
list.insertBefore(dragEl, li);
|
||||
} else {
|
||||
list.insertBefore(dragEl, li.nextSibling);
|
||||
}
|
||||
li.classList.remove('is-drop-before');
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
+13
-13
@@ -428,9 +428,14 @@ $fbCityN = count(\App\Geo::CITIES);
|
||||
② 导航条上的模块(勾选 + 排序)
|
||||
③ 模块主题色(改色)
|
||||
同一串模块名出现两遍、颜色又另起一张 —— 用户看得累、改起来要点三次保存。
|
||||
⇒ 现在合成**一张表**:一行一个模块,四列 = 导航 / 首页 / 颜色 / 顺序。
|
||||
⇒ 现在合成**一张表**:一行一个模块,四列 = 顺序 / 模块 / 导航 / 颜色。
|
||||
提交走 `settings/modules_save`(它内部把三个 save 依次调一遍,见 index.php 的说明)。
|
||||
|
||||
⚠️ v62:**「首页」那一列撤掉了** —— 首页上摆什么改由「首页卡片」自己管
|
||||
(每张卡片可设宽高/顺序/开关,还包括联系人、往年的今天这两张不是模块的卡片),
|
||||
这张表只留"导航里有没有它 + 它是什么颜色 + 排第几"。
|
||||
所以路由那边只有表单真的带了 `home[]` 才写 HomeMods,否则会把老数据清空。
|
||||
|
||||
⚠️ 颜色那一列的权限**没有变**:它仍是家庭一份、只有管理员能改;
|
||||
非管理员那一列显示成只读色块(看得到、改不了)。
|
||||
⚠️ 拖动排序仍搬整行(行自带 order[]),▲▼ 是没 JS 时的兜底 —— 两条路都在。 */
|
||||
@@ -447,14 +452,16 @@ foreach ($plMAll as $k) { if (!in_array($k, $plMOrder, true)) { $plMOrder[] = $k
|
||||
<div class="card card-pad" id="modules" style="margin-top:20px">
|
||||
<h2 class="section-title">
|
||||
模块设置
|
||||
<span class="hint">导航 / 首页 / 颜色 / 顺序,一页改完</span>
|
||||
<span class="hint">导航 / 颜色 / 顺序,一页改完</span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 16px">
|
||||
模块越来越多,但每个人关心的不一样 —— 这里一行一个模块:
|
||||
<b>导航</b>管"顶栏上有没有它",<b>首页</b>管"打开首页看不看得到",
|
||||
<b>颜色</b>是进到那个模块后顶栏高亮的色(<b>全家共用一套</b>),
|
||||
<b>顺序</b>用拖动手柄或 ▲▼ 调。
|
||||
<br>关掉的模块<b>不会丢数据</b>,只是从顶栏 / 首页收起来,别处仍能进。
|
||||
<b>导航</b>管"顶栏上有没有它",<b>颜色</b>是进到那个模块后顶栏高亮的色
|
||||
(<b>全家共用一套</b>),<b>顺序</b>用拖动手柄或 ▲▼ 调。
|
||||
<br>关掉的模块<b>不会丢数据</b>,只是从顶栏收起来,别处仍能进。
|
||||
<br>🔴 首页上摆什么,**已经改成「卡片」自己管**(每张卡片可以设宽高与顺序,
|
||||
还包括联系人、往年的今天这两张不是模块的卡片)——
|
||||
<a href="<?= e(url('home/cards')) ?>">去「首页卡片」摆</a>。
|
||||
</p>
|
||||
|
||||
<form method="post" action="<?= e(url('settings/modules_save')) ?>" id="modulesForm">
|
||||
@@ -466,7 +473,6 @@ foreach ($plMAll as $k) { if (!in_array($k, $plMOrder, true)) { $plMOrder[] = $k
|
||||
<th class="mod-th-order">顺序</th>
|
||||
<th class="mod-th-name">模块</th>
|
||||
<th class="mod-th-on">导航</th>
|
||||
<th class="mod-th-on">首页</th>
|
||||
<th class="mod-th-color">颜色</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -495,12 +501,6 @@ foreach ($plMAll as $k) { if (!in_array($k, $plMOrder, true)) { $plMOrder[] = $k
|
||||
<?= in_array($k, $plMNav, true) ? 'checked' : '' ?>>
|
||||
</label>
|
||||
</td>
|
||||
<td class="mod-td-on">
|
||||
<label class="mod-chk" title="显示在首页">
|
||||
<input type="checkbox" name="home[]" value="<?= e($k) ?>"
|
||||
<?= in_array($k, $plMHome, true) ? 'checked' : '' ?>>
|
||||
</label>
|
||||
</td>
|
||||
<td class="mod-td-color">
|
||||
<?php if ($plMIsAdmin): ?>
|
||||
<input type="color" class="color-picker" name="c[<?= e($k) ?>]"
|
||||
|
||||
Reference in New Issue
Block a user