v24 第 5 轮:十条清单(天气填城市 / 输入即筛选 / 看板列=看板 / 账本盘点折线图 / 纪念日分类)
用户 2026-10-03 的十条,逐条落点: 1. 天气改为**个人设置里填城市**(每人一份):新增 app/Geo.php,内置 197 个 地级市坐标(离线、逐条核对,生成脚本 _tooling/_fb_geo_gen.py)。填坐标仍 作为内置表没有时的出口;老的家庭级坐标保留为兜底,升级当天天气不会凭空消失。 2. 所有"找个人"的地方**输入即筛选**:新增 assets/js/filter.js(纯前端显隐, 不发请求、回车不刷新)+ app/PersonQuery.php 把服务端判据收敛成一份 (名册 / 挑人 / 高级搜索共用,避免三处实现漂移)。 3. 名册列表:有昵称就不重复显示本名、行变矮、补齐手机/阴历阳历生日(带年份)/ 邮箱/工作单位;「清除」与「搜索」同款同尺寸。 4/5. 日程:撤掉左侧日历本栏改满宽,功能(显隐/颜色/导入/默认视图)全收进 右侧 ☰;**色块高度改为按起止时长**(旧实现一律 30 分钟,三小时的会跟 十五分钟的同高);点日历空白处即建日程,开始时间=点的那一格。 6/10. 资源:搜索提到最上;按"最近改动/最新新增"排序,默认只显示最新 10 条并 给「显示全部」出口。 7. 任务:看板的**列=用户自定义的看板**(不再是写死的待办/进行中), 拖动/左右键=换看板(改 board_id,不动 status)。 8. 财务:概览在上账目在下、记一笔改右侧按钮、账本设置(共享/改名/新建/删除) 收进 ☰、默认账本名「xxx的账本」、"挂在谁名下"默认自己且用标准选人框; **新增盘点**(新增 ledger_entry_snapshots 只增不改的历史表,旧值不被清空) + 上面画金额变化折线图(app/chart.php 服务端画 SVG,零依赖)。 9. 纪念日:手动添加的与名册生日派生的**分两块**、手动的在上;右侧设置可增删 分类(家庭级,删分类不强改已用的历史)。 本轮另外修掉/加固的四类问题(都由新增的静态或运行时守卫兜住): - index.php 少写 2 个 use 导入(Pinyin / UserSetting)→ 名册与个人设置 返回 200 但正文是 Fatal;顺带修了一处早已存在的 Attachment 未导入。 → 新增 _fb_import_check.py(1f 段)。 - 一次不精确替换弄丢了 app/Calendar.php 开头的 `<?php`,整份源码被当 HTML 吐给浏览器,而 `php -l` 报"无语法错误"(纯文本输出确实合法)。 → 新增 _fb_php_open_check.py(1e 段)。 - 模板片段 unset 了宿主模板的变量(账本片段 unset($canWrite))→ 正文里一片 Undefined variable 而 HTTP 仍 200。同 _datetime.php 早年 $t 的坑。 → 新增 _fb_fragment_scope_check.py(1g 段)+ _fb_batch_e.py 的 26 页无告警巡检。 - 窄屏下页头动作区不换行 → 周视图横向溢出且按钮被挤成三种高度(用户提过三次 "按钮要同高")→ .page-head .actions 加 flex-wrap。 验证战绩(全绿): e2e 773/0 · 第 5 轮专项 91/0 · 结构升级 126/0 · 几何 428/0 · 移动端 684/0 · 交互 97/0 · 家谱 26/0 · 按钮 60/0;三轮反向验证各 3/3 抓到,还原后复绿。 线上:schema v14、指纹一致(9df6c9fccc6796db)、snapshots 表已建、197 城、 8 个关键文件 md5 与本地逐字节一致、app/ 403 无源码泄漏、错误日志无新增。
This commit is contained in:
+112
-65
@@ -1,20 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* 日程 · 月视图(v12)。
|
||||
* 日程 · 月 / 周 / 天 三视图(v12,v24 第 4、5 条重排)。
|
||||
*
|
||||
* 参照群晖 Calendar 的外观:左边一列"我的日历本"(可勾选显隐),右边 7×6 的月网格,
|
||||
* 有日程的日子在格子里露出彩色小条,点小条跳到对应模块(观影/旅程/任务)。
|
||||
* ⚠️ v24 用户 2026-10-03 第 4 条:**左侧那一栏「我的日历本」撤掉了**,日历占满整宽。
|
||||
* 原话:「日程模块,左侧的日历本显示与否,去掉,让日历充满整个宽度。
|
||||
* 在右侧,也加一个设置按钮,设置中,可以设置哪些日历本显示,
|
||||
* 调整日历的颜色,导入日历等功能,都放在这个设置下。」
|
||||
* 所以那些功能全搬进了页头最右边那个 ☰ 里(见 _settings_menu.php)。
|
||||
* 显隐改由**服务端**处理(Calendar::hiddenCalendars):藏掉的本子不参与取数,
|
||||
* "+N 条"之类按可见条目算 —— 前端 display:none 会让这些数字对不上。
|
||||
*
|
||||
* 两条设计:
|
||||
* ① **日历本显隐是纯前端的**(勾选框 + 给日程条加 data-cal):
|
||||
* 不做成"勾一次刷一次页面",否则翻月、勾选都在等服务端。没有 JS 时全部照常显示。
|
||||
* ② **观影 / 旅行 / 任务不复制进日历**,是从各自模块实时汇总出来的(Calendar::month),
|
||||
* 所以改了观影日期,日历里立刻跟着变。反过来在日历里给观影本加日程 = 真的建一条观影记录。
|
||||
* ⚠️ 用户第 5 条:在日历里点一下就建日程(开始时间 = 点的那一格)。
|
||||
* 月视图的格子按天、周/日视图按半小时——
|
||||
* 后者用鼠标在列里的纵向位置反算时刻(见页面底部那两段 JS)。
|
||||
*
|
||||
* @param string $ym 'Y-m'
|
||||
* @param array $weeks 月网格(build_month_grid 产出)
|
||||
* @param array $cals 我可见的日历本(含 perm)
|
||||
* @param array $people 名册(共享给别人时用)
|
||||
* @param array $cals 我可见的日历本(含 perm);$visCals 是其中显示着的 id
|
||||
* @param string $newAt 点进来时预填的开始时间('Y-m-d H:i' 或 'Y-m-d')
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$view = (string)($view ?? 'month');
|
||||
@@ -24,6 +27,11 @@ $prevYm = date('Y-m', strtotime('-1 month', $ts));
|
||||
$nextYm = date('Y-m', strtotime('+1 month', $ts));
|
||||
$thisYm = date('Y-m');
|
||||
$writable = array_values(array_filter($cals, static fn($c) => \App\Calendar::canWrite($c, (int)\App\Auth::personId())));
|
||||
$visCals = array_map('intval', (array)($visCals ?? []));
|
||||
$hiddenCals = array_map('intval', (array)($hiddenCals ?? []));
|
||||
$newAt = (string)($newAt ?? '');
|
||||
// 能不能"点一下建日程":只读账号点了没反应,所以连十字光标都不给(见 CSS 的 rt-writable)
|
||||
$canNew = $canWrite && (bool)$writable;
|
||||
|
||||
// 三种看法各自的「上一段 / 下一段」:月按整月走,周按 7 天走,天按 1 天走
|
||||
$step = $view === 'week' ? 7 : ($view === 'day' ? 1 : 0);
|
||||
@@ -33,12 +41,6 @@ $todayD = date('Y-m-d');
|
||||
$headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
: ($view === 'week' ? date('Y 年 n 月 j 日', strtotime($anchor)) . ' 起这一周'
|
||||
: date('Y 年 n 月 j 日', strtotime($anchor)));
|
||||
$mkUrl = static function (string $v) use ($ym, $prevD, $nextD, $todayD, $prevYm, $nextYm, $thisYm) {
|
||||
if ($v === 'month') {
|
||||
return url('calendar', ['view' => 'month', 'ym' => $ym]);
|
||||
}
|
||||
return null; // 周/天要把具体哪天传进去,由调用方拼
|
||||
};
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>日程</h1>
|
||||
@@ -66,41 +68,22 @@ $mkUrl = static function (string $v) use ($ym, $prevD, $nextD, $todayD, $prevYm,
|
||||
<?= $view === 'week' ? '下一周' : '后一天' ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar/calendars')) ?>">日历本</a>
|
||||
<?php if ($canWrite && $writable): ?>
|
||||
<button class="btn" type="button" id="calAddBtn">新建日程</button>
|
||||
<?php endif; ?>
|
||||
<?php /* 用户第 4、5 条:日历本 / 颜色 / 导入 / 默认视图 全收进这个 ☰ ——
|
||||
它顶替了原来左边那一栏,所以排在**最右**(靠 .page-head .actions 的顺序)。 */ ?>
|
||||
<?php $setCurrent = 'view'; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cal-wrap">
|
||||
<?php /* ---- 左:日历本 ---- */ ?>
|
||||
<aside class="cal-side">
|
||||
<h2 class="cinema-h2">我的日历本</h2>
|
||||
<?php if (!$cals): ?>
|
||||
<p class="empty-hint">还没有日历本。</p>
|
||||
<?php else: ?>
|
||||
<ul class="cal-list">
|
||||
<?php foreach ($cals as $c): ?>
|
||||
<li class="cal-item">
|
||||
<label class="cal-toggle">
|
||||
<input type="checkbox" class="cal-cb" data-cal="<?= (int)$c['id'] ?>" checked>
|
||||
<span class="color-preview" style="background: <?= e((string)$c['color']) ?>;"></span>
|
||||
<span class="cal-name"><?= e((string)$c['name']) ?></span>
|
||||
</label>
|
||||
<?php if ((int)$c['shared'] === 1): ?>
|
||||
<span class="node-badge"><?= ($c['perm'] ?? '') === 'write' ? '共享·可写' : '共享·只读' ?></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php /* 观影/旅行/任务三路是**自动汇总**的,没有本子开关,这里说明一下来源 */ ?>
|
||||
<p class="cal-note">观影、旅行、有截止时间的任务会自动出现在这里,不需要另建本子。</p>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
|
||||
<?php /* ---- 右:月网格 ---- */ ?>
|
||||
<div class="cal-wrap cal-wrap-full">
|
||||
<?php /* ---- 右:月网格 / 时间网格 ----
|
||||
用户第 4 条:左边那一栏「我的日历本」已经撤掉(功能搬进右上角 ☰),
|
||||
所以这里直接就是主区域,`.cal-wrap-full` 把网格调成"只有一列"。 */ ?>
|
||||
<div class="cal-main">
|
||||
<?php /* 用户第 5 条:这张表单既服务「新建日程」按钮,也服务「点日历空白处」——
|
||||
后者由 JS 现填开始时间,所以表单本身不再写死 09:00。 */ ?>
|
||||
<?php if ($canWrite && $writable): ?>
|
||||
<div class="card card-pad cal-add" id="calAddCard" hidden>
|
||||
<h2 class="cinema-h2">新建日程</h2>
|
||||
@@ -117,10 +100,17 @@ $mkUrl = static function (string $v) use ($ym, $prevD, $nextD, $todayD, $prevYm,
|
||||
</select>
|
||||
</div>
|
||||
<?php /* v24 第 6 条:字段按 iCalendar / CalDAV 的 VEVENT 定(见 _event_fields.php)。
|
||||
新建时「全天」默认**不勾**(用户原话),开始时间默认今天 09:00。 */ ?>
|
||||
新建时「全天」默认**不勾**(用户原话)。开始时间默认:
|
||||
带了 new_at 就用它(点日历的那一格),否则今天 09:00。 */ ?>
|
||||
<?php
|
||||
$evFields = ['starts_at' => ($ym === $thisYm ? date('Y-m-d') : $ym . '-01') . ' 09:00:00',
|
||||
'ends_at' => ($ym === $thisYm ? date('Y-m-d') : $ym . '-01') . ' 10:00:00'];
|
||||
// 点的位置优先;没点就"月视图当月 → 该月 1 号 / 本月 → 今天"的 09:00
|
||||
$defDay = $newAt !== '' ? substr($newAt, 0, 10)
|
||||
: (($ym === $thisYm ? date('Y-m-d') : $ym . '-01'));
|
||||
$defHM = $newAt !== '' && strlen($newAt) > 10
|
||||
? substr($newAt, 11, 5) : '09:00';
|
||||
$defEndH = sprintf('%02d', (min(23, (int)substr($defHM, 0, 2) + 1)));
|
||||
$evFields = ['starts_at' => $defDay . ' ' . $defHM . ':00',
|
||||
'ends_at' => $defDay . ' ' . $defEndH . ':' . substr($defHM, 3, 2) . ':00'];
|
||||
$evAllDay = false;
|
||||
require __DIR__ . '/_event_fields.php';
|
||||
?>
|
||||
@@ -170,7 +160,8 @@ $mkUrl = static function (string $v) use ($ym, $prevD, $nextD, $todayD, $prevYm,
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php foreach (($cols ?? []) as $i => $c): ?>
|
||||
<div class="tg-col<?= $c['is_today'] ? ' is-today' : '' ?>">
|
||||
<div class="tg-col<?= $c['is_today'] ? ' is-today' : '' ?><?= $canNew ? ' rt-writable' : '' ?>"
|
||||
data-new-at="<?= e((string)$c['date']) ?>">
|
||||
<?php for ($h = 0; $h < 24; $h++): ?>
|
||||
<span class="tg-line"></span>
|
||||
<?php endfor; ?>
|
||||
@@ -196,7 +187,11 @@ $mkUrl = static function (string $v) use ($ym, $prevD, $nextD, $todayD, $prevYm,
|
||||
<div class="cal-grid">
|
||||
<?php foreach ($weeks as $wk): ?>
|
||||
<?php foreach ($wk as $cell): ?>
|
||||
<div class="cal-cell<?= $cell['in_month'] ? '' : ' is-out' ?><?= $cell['is_today'] ? ' is-today' : '' ?>">
|
||||
<?php /* 用户第 5 条:点格子里的空白 = 在这天建日程。
|
||||
用 data-new-at 把日期带过去,由底部 JS 交给新建表单
|
||||
(点了小条本身是"打开那条日程",由 <a> 自己处理,JS 会排除掉)。 */ ?>
|
||||
<div class="cal-cell<?= $cell['in_month'] ? '' : ' is-out' ?><?= $cell['is_today'] ? ' is-today' : '' ?><?= $canNew ? ' rt-writable' : '' ?>"
|
||||
data-new-at="<?= e((string)$cell['date']) ?>">
|
||||
<div class="cal-dnum"><?= (int)$cell['day'] ?></div>
|
||||
<div class="cal-evs">
|
||||
<?php $n = 0; foreach ($cell['events'] as $ev): $n++; ?>
|
||||
@@ -226,22 +221,11 @@ $mkUrl = static function (string $v) use ($ym, $prevD, $nextD, $todayD, $prevYm,
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
/* 日历本显隐:只切 display,不重新请求 —— 翻月/勾选都是立刻的。
|
||||
没有 JS 时全部照常显示(勾选框失效而已),不会丢内容。 */
|
||||
var boxes = document.querySelectorAll('.cal-cb');
|
||||
function apply() {
|
||||
var off = {};
|
||||
Array.prototype.forEach.call(boxes, function (b) {
|
||||
if (!b.checked) { off[b.getAttribute('data-cal')] = true; }
|
||||
});
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.cal-ev'), function (el) {
|
||||
el.style.display = off[el.getAttribute('data-cal')] ? 'none' : '';
|
||||
});
|
||||
}
|
||||
Array.prototype.forEach.call(boxes, function (b) { b.addEventListener('change', apply); });
|
||||
apply();
|
||||
|
||||
var btn = document.getElementById('calAddBtn');
|
||||
/* ---- 新建日程:按钮开合 + 「点日历空白处」预填开始时间 ----
|
||||
⚠️ 上一版的「日历本显隐」JS 已经**整段删掉**:显隐改成服务端处理
|
||||
(Calendar::hiddenCalendars,见路由里的说明),前端不再需要它。
|
||||
留着会让人以为"改一下勾选就能切换",但那些勾选框已经不在这儿了。 */
|
||||
var btn = document.getElementById('calAddBtn');
|
||||
var card = document.getElementById('calAddCard');
|
||||
var cancel = document.getElementById('calAddCancel');
|
||||
if (btn && card) {
|
||||
@@ -251,6 +235,69 @@ $mkUrl = static function (string $v) use ($ym, $prevD, $nextD, $todayD, $prevYm,
|
||||
});
|
||||
}
|
||||
if (cancel && card) { cancel.addEventListener('click', function () { card.hidden = true; }); }
|
||||
|
||||
/* ---- 用户第 5 条:在日历里点一下 → 弹新建,开始时间 = 点的位置 ----
|
||||
月视图:一格 = 一天 → 开始时间用那天 09:00("当日默认"比 00:00 更像个日程)。
|
||||
周/日视图:按鼠标在**那一列里的纵向位置**反算时刻,最接近的半小时 ——
|
||||
点在"下午三点那一段"就该得到 15:00,这是"点哪儿建哪儿"的全部意义。 */
|
||||
var startField = document.querySelector('.field.ev-dt[data-dt]');
|
||||
var startDate = startField ? startField.querySelector('input.dt-date') : null;
|
||||
var startHour = startField ? startField.querySelector('select.dt-hour') : null;
|
||||
var startMin = startField ? startField.querySelector('select.dt-minute') : null;
|
||||
var startSec = startField ? startField.querySelector('select.dt-second') : null;
|
||||
|
||||
function setStart(dateStr, hh, mm) {
|
||||
if (!startDate) return;
|
||||
startDate.value = dateStr;
|
||||
if (hh !== null && startHour) { startHour.value = hh; }
|
||||
if (mm !== null && startMin) { startMin.value = mm; }
|
||||
if (startSec) { startSec.value = '00'; }
|
||||
}
|
||||
|
||||
function openNew(dateStr, hh, mm) {
|
||||
if (!card) return;
|
||||
card.hidden = false;
|
||||
setStart(dateStr, hh, mm);
|
||||
card.scrollIntoView({ block: 'nearest' });
|
||||
// 标题是第一个输入框,直接把光标放过去 —— 点完就能打字
|
||||
var t = document.getElementById('ev-title');
|
||||
if (t) { t.focus(); }
|
||||
}
|
||||
|
||||
/* 月视图格子 */
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.cal-cell[data-new-at]'), function (cell) {
|
||||
cell.addEventListener('click', function (ev) {
|
||||
// 点在小条上 = 想打开那条日程(<a> 的默认行为),不要抢
|
||||
if (ev.target.closest && ev.target.closest('.cal-ev')) { return; }
|
||||
if (!<?= $canWrite && $writable ? 'true' : 'false' ?>) { return; }
|
||||
openNew(cell.getAttribute('data-new-at'), '09', '00');
|
||||
});
|
||||
});
|
||||
|
||||
/* 周/日视图的列:纵向位置 → 时刻。
|
||||
列高 1056px = 24 小时(见 .tg-col 的 CSS),所以 1 分钟 = 1056/1440 px。 */
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.tg-col[data-new-at]'), function (col) {
|
||||
col.addEventListener('click', function (ev) {
|
||||
if (ev.target.closest && ev.target.closest('.tg-block')) { return; }
|
||||
if (!<?= $canWrite && $writable ? 'true' : 'false' ?>) { return; }
|
||||
var box = col.getBoundingClientRect();
|
||||
var ratio = (ev.clientY - box.top) / Math.max(1, box.height);
|
||||
var mins = Math.max(0, Math.min(24 * 60 - 30, Math.round(ratio * 1440 / 30) * 30));
|
||||
var hh = String(Math.floor(mins / 60)).padStart(2, '0');
|
||||
var mm = String(mins % 60).padStart(2, '0');
|
||||
openNew(col.getAttribute('data-new-at'), hh, mm);
|
||||
});
|
||||
});
|
||||
|
||||
/* 从 ?new_at= 进来(比如别处链过来)→ 直接开弹窗并预填 */
|
||||
var preset = <?= json_encode($newAt, JSON_UNESCAPED_UNICODE) ?>;
|
||||
if (preset) {
|
||||
var parts = String(preset).split(/[ T]/);
|
||||
if (parts[0]) {
|
||||
var hm = (parts[1] || '').split(':');
|
||||
openNew(parts[0], hm[0] || '09', hm[1] || '00');
|
||||
}
|
||||
}
|
||||
/* 「全天」勾上藏掉时刻那一段的逻辑在 _event_fields.php 里(两个页面共用一份) */
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user