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:
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
require __DIR__ . '/app/bootstrap.php';
|
||||
|
||||
use App\Account;
|
||||
use App\Attachment;
|
||||
use App\Auth;
|
||||
use App\Birthday;
|
||||
use App\Calendar;
|
||||
@@ -19,6 +20,7 @@ use App\Crypto;
|
||||
use App\Database;
|
||||
use App\Douban;
|
||||
use App\Family;
|
||||
use App\Geo;
|
||||
use App\Home;
|
||||
use App\HomeMods;
|
||||
use App\Anniversary;
|
||||
@@ -32,7 +34,9 @@ use App\MovieStats;
|
||||
use App\Nickname;
|
||||
use App\Person;
|
||||
use App\PersonGroup;
|
||||
use App\PersonQuery;
|
||||
use App\PersonRoster;
|
||||
use App\Pinyin;
|
||||
use App\Relations;
|
||||
use App\Repeat;
|
||||
use App\Resource;
|
||||
@@ -41,6 +45,7 @@ use App\Setting;
|
||||
use App\Task;
|
||||
use App\Trip;
|
||||
use App\Upload;
|
||||
use App\UserSetting;
|
||||
use App\View;
|
||||
|
||||
if (!Database::hasTables()) {
|
||||
@@ -211,6 +216,12 @@ function render_me_settings(array $person, array $errors = []): void
|
||||
// 用户 2026-10-03 第 3 条:首页上显示哪些模块、什么顺序(每人一份)
|
||||
'homeOrder' => HomeMods::orderedForPicker($me),
|
||||
'homeOn' => HomeMods::forPerson($me),
|
||||
// 用户 2026-10-03 第 1 条:我所在的城市(决定首页那格天气)。
|
||||
// 同时把"查到的是哪儿"带过去,好让用户当场确认没填错(而不是回首页才发现)。
|
||||
'weatherCity' => (string)UserSetting::get($me, 'weather_city', ''),
|
||||
'weatherLat' => (string)UserSetting::get($me, 'weather_lat', ''),
|
||||
'weatherLon' => (string)UserSetting::get($me, 'weather_lon', ''),
|
||||
'weatherAt' => Home::weatherLocation($me, $familyId),
|
||||
], 'settings');
|
||||
}
|
||||
|
||||
@@ -345,6 +356,13 @@ function day_columns(string $anchor, int $span): array
|
||||
* ⚠️ 全天日程不进时间格 —— 它们没有时段,硬塞进 0 点会变成一条压在最上面的怪东西。
|
||||
* 单独挂在顶上的一行(跟群晖日历一样)。
|
||||
*
|
||||
* 用户 2026-10-03 第 5 条:「日程色块的高度不对,应该是从开始时间到结束时间,
|
||||
* 高度随着日程的时间而不同才对。」
|
||||
* 旧实现把**所有**日程都写成「30 分钟高」(`$endMin = $startMin + 30`),
|
||||
* 于是三小时的会议和十五分钟的站立会同高 —— 一眼看不出"哪段时间被占住了",
|
||||
* 而"时间网格"的全部意义就在于把时长画出来。
|
||||
* 现在:有 ends_at 就按真实时长;没填结束才算"最短可见块"(30 分钟)。
|
||||
*
|
||||
* @return array{all_day:array,timed:array} all_day 按列分组;timed 按列分组,含 top/height
|
||||
*/
|
||||
function build_time_grid(array $events, array $cols): array
|
||||
@@ -372,11 +390,23 @@ function build_time_grid(array $events, array $cols): array
|
||||
continue;
|
||||
}
|
||||
$startMin = ((int)$m[1]) * 60 + (int)$m[2];
|
||||
// 没有结束时刻的按 30 分钟画一条最短的可见块(不然高度是 0,等于看不见)
|
||||
$endMin = $startMin + 30;
|
||||
// 结束时刻:有就按真实时长;没有就画一条 30 分钟的最短可见块
|
||||
// (高度是 0 等于看不见,那比"画得不准"更糟)。
|
||||
$et = (string)($e['end_time'] ?? '');
|
||||
$endMin = null;
|
||||
if ($et !== '' && preg_match('/^(\d{2}):(\d{2})/', $et, $m2)) {
|
||||
$endMin = ((int)$m2[1]) * 60 + (int)$m2[2];
|
||||
if ($endMin <= $startMin) {
|
||||
// 结束时刻不在开始之后(跨天日程的第二天、或用户填反了)→ 当没有结束
|
||||
$endMin = null;
|
||||
}
|
||||
}
|
||||
$spanMin = $endMin === null ? 30 : max(15, $endMin - $startMin);
|
||||
// ⚠️ 上限到 24:00:超出的部分已经在网格外面,硬画出去会把整列撑长
|
||||
$spanMin = min($spanMin, 1440 - $startMin);
|
||||
$timed[$ci][] = $e + [
|
||||
'top' => round($startMin / 1440 * 100, 4),
|
||||
'height' => round(max(30, $endMin - $startMin) / 1440 * 100, 4),
|
||||
'height' => round($spanMin / 1440 * 100, 4),
|
||||
];
|
||||
}
|
||||
return ['all_day' => $allDay, 'timed' => $timed];
|
||||
@@ -596,13 +626,11 @@ switch ($r) {
|
||||
$meName = Nickname::display($me, $people[$me] ?? null) ?: (string)($people[$me]['name'] ?? '');
|
||||
$greeting = Home::greeting($meName, $today, count($soon), $lateN);
|
||||
|
||||
// 天气:经纬度在系统设置里配;没配就整块不显示(不猜城市)
|
||||
$weather = Home::weather(
|
||||
$familyId,
|
||||
(string)Setting::get($familyId, 'weather_lat', ''),
|
||||
(string)Setting::get($familyId, 'weather_lon', ''),
|
||||
(string)Setting::get($familyId, 'weather_place', '')
|
||||
);
|
||||
// 天气:地点在**个人设置**里(v24 用户 2026-10-03 第 1 条),每人一份。
|
||||
// 三级回落都在这一个调用里:自定义经纬度 → 内置城市表 → 不显示。
|
||||
$weatherLoc = Home::weatherLocation($me, $familyId);
|
||||
$weather = $weatherLoc === null ? null
|
||||
: Home::weather($familyId, $weatherLoc['lat'], $weatherLoc['lon'], $weatherLoc['place']);
|
||||
|
||||
View::render('home', [
|
||||
'title' => '首页',
|
||||
@@ -667,27 +695,13 @@ switch ($r) {
|
||||
foreach ($rows as &$row) {
|
||||
$row['label'] = Kinship::label($graph, $me, (int)$row['id'], $people);
|
||||
$row['shown'] = Nickname::display($me, $row) ?: (string)$row['name'];
|
||||
// 用户 2026-10-03 第 4 条:列表要显示手机号(密文解密)与生日。
|
||||
// ⚠️ 解密只对**能看名册的人**做(这页本来就要登录 + 在名册里),
|
||||
// 但页面上仍然只显示"前 3 后 4"以外的形态由模板决定。
|
||||
$row['phone_plain'] = '';
|
||||
if (!empty($row['phone_encrypted'])) {
|
||||
try {
|
||||
$row['phone_plain'] = Crypto::decrypt((string)$row['phone_encrypted']);
|
||||
} catch (\Throwable $e) {
|
||||
$row['phone_plain'] = '';
|
||||
}
|
||||
}
|
||||
// 生日优先显示阳历(真实生日),没有就显示农历
|
||||
$row['birthday_show'] = Birthday::pretty(Birthday::solarOf($row));
|
||||
$row['birthday_kind'] = '阳历';
|
||||
if ($row['birthday_show'] === '') {
|
||||
$lun = Birthday::dateOnly((string)($row['birthday_lunar'] ?? ''));
|
||||
if ($lun !== '') {
|
||||
$row['birthday_show'] = $lun;
|
||||
$row['birthday_kind'] = '农历';
|
||||
}
|
||||
}
|
||||
// 用户 2026-10-03 第 3 条:列表要显示手机号(密文解密)与生日。
|
||||
// 解密与生日格式化统一走 PersonQuery(见该类的类注释:
|
||||
// 名册 / 挑人 / 高级搜索必须**同一套判据**,否则同一句搜索在不同页面结果不同)。
|
||||
$row['phone_plain'] = PersonQuery::phonePlain($row);
|
||||
[$row['birthday_show'], $row['birthday_kind']] = PersonQuery::birthdayOf($row);
|
||||
// 首字母供前端"输入即筛选"用(filter.js 对 data-initials 做子序列匹配)
|
||||
$row['initials'] = Pinyin::initials((string)$row['name'] . ' ' . (string)($row['my_nickname'] ?? ''));
|
||||
}
|
||||
unset($row);
|
||||
|
||||
@@ -1004,6 +1018,46 @@ switch ($r) {
|
||||
redirect('settings/me', [], 'home-mods');
|
||||
break;
|
||||
|
||||
/**
|
||||
* 保存「我所在的城市」(v24 用户 2026-10-03 第 1 条)—— **每人一份**。
|
||||
*
|
||||
* 用户原话:「天气这个这样吧,让用户在个人设置中填写自己的城市。」
|
||||
*
|
||||
* 三段式:
|
||||
* ① 填了城市名 → 用内置表 app/Geo 查坐标(不联网,见 Geo 的类注释);
|
||||
* 查不到就**明说**查不到,不猜(猜错城市比不显示天气更糟)。
|
||||
* ② 内置表没有的地方 → 允许直接填经纬度(镇 / 村 / 海外)。
|
||||
* ③ 都不填 → 首页不显示天气那一块(不是错误,只是没配)。
|
||||
*
|
||||
* ⚠️ 存的键挂在 **user_settings**(每人一份)而不是家庭 settings 表:
|
||||
* 一家人可能分处两个城市(孩子在外地),天气是"我看到的那一份天空",
|
||||
* 家庭级的设置一改全家都变,正好是这里要避免的。
|
||||
*/
|
||||
case 'settings/weather':
|
||||
Auth::requireLogin();
|
||||
csrf_check();
|
||||
$me = (int)Auth::personId();
|
||||
$city = trim((string)($_POST['city'] ?? ''));
|
||||
$latIn = trim((string)($_POST['lat'] ?? ''));
|
||||
$lonIn = trim((string)($_POST['lon'] ?? ''));
|
||||
|
||||
UserSetting::set($me, 'weather_city', mb_substr($city, 0, 30, 'UTF-8'));
|
||||
// 坐标只收合法数字,非法值一律当"没填" ——
|
||||
// 一个手滑的字母会让首页每次请求都白白打一次外部接口。
|
||||
$okNum = static fn(string $v): string =>
|
||||
($v !== '' && is_numeric($v) && (float)$v >= -180 && (float)$v <= 180) ? $v : '';
|
||||
UserSetting::set($me, 'weather_lat', $okNum($latIn));
|
||||
UserSetting::set($me, 'weather_lon', $okNum($lonIn));
|
||||
|
||||
if ($city !== '' && Geo::lookup($city) === null && ($latIn === '' || $lonIn === '')) {
|
||||
flash('err', '没有「' . $city . '」这座城市的坐标。换个写法(如「山东青岛」),'
|
||||
. '或者直接在下面填经纬度。');
|
||||
} else {
|
||||
flash('ok', '天气地点已保存。');
|
||||
}
|
||||
redirect('settings/me', [], 'weather');
|
||||
break;
|
||||
|
||||
/**
|
||||
* 保存模块主题色(用户第 11 条,**家庭一份**,所以只有管理员能改)。
|
||||
*/
|
||||
@@ -1687,10 +1741,6 @@ switch ($r) {
|
||||
'title' => '设置',
|
||||
'family' => Family::find((int)Auth::familyId()),
|
||||
'dbVersion' => $dbVersion,
|
||||
// 用户第 3 条:首页天气的坐标(三个都空 = 不显示天气)
|
||||
'weatherPlace' => (string)Setting::get((int)Auth::familyId(), 'weather_place', ''),
|
||||
'weatherLat' => (string)Setting::get((int)Auth::familyId(), 'weather_lat', ''),
|
||||
'weatherLon' => (string)Setting::get((int)Auth::familyId(), 'weather_lon', ''),
|
||||
'keyOk' => $rawKey !== '' && strpos($rawKey, '请填写') !== 0,
|
||||
'roleCounts' => $acc,
|
||||
'relCount' => Relations::count((int)Auth::familyId()),
|
||||
@@ -1812,19 +1862,11 @@ switch ($r) {
|
||||
(string)($cur['name'] ?? '家书'),
|
||||
trim((string)($_POST['motto'] ?? ''))
|
||||
);
|
||||
// 用户第 3 条:天气坐标(家庭级)。只收合法数字,非法值一律当"没填",
|
||||
// 否则一个手滑的字母会让首页天气接口每次都白请求一次。
|
||||
foreach ([
|
||||
'weather_place' => 'weather_place',
|
||||
'weather_lat' => 'weather_lat',
|
||||
'weather_lon' => 'weather_lon',
|
||||
] as $formKey => $settingKey) {
|
||||
$v = trim((string)($_POST[$formKey] ?? ''));
|
||||
if ($settingKey !== 'weather_place') {
|
||||
$v = ($v !== '' && is_numeric($v)) ? $v : '';
|
||||
}
|
||||
Setting::set($fid, $settingKey, mb_substr($v, 0, 60, 'UTF-8'));
|
||||
}
|
||||
// ⚠️ 天气坐标**不再从系统设置收**(用户 2026-10-03 第 1 条:改到个人设置填城市)。
|
||||
// 这里也不再 Set 那三个键 —— 但**不删**库里已有的旧值:
|
||||
// 万一有人还没去个人设置填城市,旧的家庭级坐标留着还能兜底(见下)。
|
||||
// (Home::weatherLocation 只读 user_settings,旧的家庭值已经不参与;
|
||||
// 留着只是为了"哪天想回滚"时有据可查,代价是三个字符串。)
|
||||
flash('ok', '已保存。');
|
||||
redirect('settings');
|
||||
break;
|
||||
@@ -2677,9 +2719,16 @@ switch ($r) {
|
||||
'title' => '财务',
|
||||
'books' => $books,
|
||||
'cur' => $cur,
|
||||
// ⚠️ `me` 必须传进去:模板里「挂在谁名下默认是自己」(用户第 8 条)
|
||||
// 要用它当默认选中值。漏传的话默认值会静默变成"不选" ——
|
||||
// 功能"看起来在",只是每次都要手动点一次选人框。
|
||||
'me' => $me,
|
||||
'entries' => $entries,
|
||||
'summary' => Ledger::summary($entries),
|
||||
'shares' => Ledger::shares((int)$cur['id']),
|
||||
// 用户 2026-10-03 第 8 条:上面画"金额随时间怎么变",所以要历史。
|
||||
'charts' => Ledger::charts((int)$cur['id']),
|
||||
'timeline'=> Ledger::timeline((int)$cur['id']),
|
||||
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||
], 'ledger');
|
||||
break;
|
||||
@@ -2690,6 +2739,7 @@ switch ($r) {
|
||||
case 'ledger/entry_add':
|
||||
case 'ledger/entry_update':
|
||||
case 'ledger/entry_del':
|
||||
case 'ledger/reconcile':
|
||||
case 'ledger/share':
|
||||
case 'ledger/unshare':
|
||||
$familyId = (int)Auth::familyId();
|
||||
@@ -2741,6 +2791,17 @@ switch ($r) {
|
||||
} elseif ($act === 'ledger/entry_del') {
|
||||
Ledger::removeEntry($lid, (int)($_POST['id'] ?? 0));
|
||||
flash('ok', '账目已删除。');
|
||||
} elseif ($act === 'ledger/reconcile') {
|
||||
// 用户 2026-10-03 第 8 条:盘点 = 记一次新金额,**旧值不被清空**
|
||||
// (落一行快照,见 Ledger::reconcile 的注释)。
|
||||
$eid = (int)($_POST['id'] ?? 0);
|
||||
$ok = Ledger::reconcile(
|
||||
$lid, $eid,
|
||||
(string)($_POST['amount'] ?? ''),
|
||||
(string)($_POST['note'] ?? ''),
|
||||
$me
|
||||
);
|
||||
flash($ok ? 'ok' : 'err', $ok ? '盘点已记下(历史保留)。' : '找不到这笔账目。');
|
||||
} elseif ($act === 'ledger/share') {
|
||||
$pid = (int)($_POST['person_id'] ?? 0);
|
||||
if ($pid > 0 && isset(Person::map($familyId)[$pid])) {
|
||||
@@ -2764,14 +2825,58 @@ switch ($r) {
|
||||
case 'days':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
// 用户 2026-10-03 第 9 条:手动添加的与模块派生的**分开**,手动的在上。
|
||||
$sp = Anniversary::split($familyId, $me);
|
||||
View::render('days/index', [
|
||||
'title' => '纪念日',
|
||||
'rows' => Anniversary::all($familyId, $me),
|
||||
'custom' => $sp['custom'],
|
||||
'derived' => $sp['derived'],
|
||||
'upcoming' => Anniversary::upcoming($familyId, $me, 60),
|
||||
'kinds' => Anniversary::kinds($familyId),
|
||||
'kindCounts' => Anniversary::kindCounts($familyId),
|
||||
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||
], 'days');
|
||||
break;
|
||||
|
||||
/**
|
||||
* 纪念日分类设置(v24 用户 2026-10-03 第 9 条)。
|
||||
*
|
||||
* 用户原话:「在新建纪念日和右侧,也增加设置按钮,可以设置纪念日的分类。」
|
||||
*
|
||||
* 分类清单是**家庭级**的(全家共用一套,不然「结婚纪念日」在两个人屏幕上
|
||||
* 是两套叫法,筛选/看的时候对不上)。所以放在 settings 而不是 user_settings。
|
||||
*/
|
||||
case 'days/kinds':
|
||||
$familyId = (int)Auth::familyId();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
// 两个动作共用一个路由:加一个 / 删一个(表单里带 del 或 add)
|
||||
$kinds = Anniversary::kinds($familyId);
|
||||
$del = trim((string)($_POST['del'] ?? ''));
|
||||
if ($del !== '') {
|
||||
$kinds = array_values(array_filter($kinds, static fn($k) => (string)$k !== $del));
|
||||
Anniversary::saveKinds($familyId, $kinds);
|
||||
flash('ok', '分类已删除(已用它的纪念日不变,只是新建时不再出现在下拉里)。');
|
||||
redirect('days', [], 'anniv-kinds');
|
||||
}
|
||||
$add = trim((string)($_POST['add'] ?? ''));
|
||||
if ($add !== '') {
|
||||
if (mb_strlen($add, 'UTF-8') > 20) {
|
||||
flash('err', '分类名太长了(最多 20 个字)。');
|
||||
} else {
|
||||
$kinds[] = $add;
|
||||
Anniversary::saveKinds($familyId, $kinds);
|
||||
flash('ok', '分类已添加。');
|
||||
}
|
||||
redirect('days', [], 'anniv-kinds');
|
||||
}
|
||||
// 整份覆盖(设置面板里直接改一串)
|
||||
$list = (array)($_POST['kinds'] ?? []);
|
||||
Anniversary::saveKinds($familyId, $list);
|
||||
flash('ok', '分类已保存。');
|
||||
redirect('days', [], 'anniv-kinds');
|
||||
break;
|
||||
|
||||
case 'days/new':
|
||||
case 'days/edit':
|
||||
$familyId = (int)Auth::familyId();
|
||||
@@ -2785,6 +2890,7 @@ switch ($r) {
|
||||
View::render('days/form', [
|
||||
'title' => $id > 0 ? '编辑纪念日' : '新建纪念日',
|
||||
'editing' => $editing,
|
||||
'kinds' => Anniversary::kinds($familyId),
|
||||
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||
], 'days');
|
||||
break;
|
||||
@@ -2820,10 +2926,15 @@ switch ($r) {
|
||||
case 'calendar':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
// v24 第 4 条:月 / 周 / 天三种看法。view 只认这三个,认不出就回落月视图。
|
||||
$view = (string)($_GET['view'] ?? 'month');
|
||||
if (!in_array($view, ['month', 'week', 'day'], true)) {
|
||||
$view = 'month';
|
||||
// v24 第 4 条:月 / 周 / 天三种看法。view 只认这三个,认不出就回落**默认视图**。
|
||||
// ⚠️ 默认视图是**每人一份**的设置(用户第 5 条:设置里选月/周/天)。
|
||||
// 但只在**没带 view 参数**时才生效 —— 手点了「周」按钮之后,
|
||||
// 那个选择必须优先,否则每次翻页都被默认值拽回去。
|
||||
$viewIn = trim((string)($_GET['view'] ?? ''));
|
||||
if ($viewIn === '') {
|
||||
$view = Calendar::defaultView($me);
|
||||
} else {
|
||||
$view = in_array($viewIn, ['month', 'week', 'day'], true) ? $viewIn : 'month';
|
||||
}
|
||||
$anchor = trim((string)($_GET['d'] ?? ''));
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $anchor)) {
|
||||
@@ -2831,6 +2942,16 @@ switch ($r) {
|
||||
}
|
||||
$ym = substr($anchor, 0, 7);
|
||||
$cals = Calendar::mine($familyId, $me);
|
||||
// 用户第 4 条:哪些本子显示,改由设置里的复选框决定(以前是左侧那一栏)。
|
||||
// 藏掉的本子**不参与取数**,而不是"取出来再 display:none" ——
|
||||
// 前端藏的话,关掉一本之后月视图的"+N 条"仍然按它算,数字对不上。
|
||||
$hiddenCals = Calendar::hiddenCalendars($me);
|
||||
$visCals = [];
|
||||
foreach ($cals as $c) {
|
||||
if (!isset($hiddenCals[(int)$c['id']])) {
|
||||
$visCals[] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
if ($view === 'month') {
|
||||
$gym = trim((string)($_GET['ym'] ?? ''));
|
||||
@@ -2838,17 +2959,21 @@ switch ($r) {
|
||||
$ym = $gym;
|
||||
}
|
||||
[$from, $to] = month_grid_range($ym);
|
||||
$events = Calendar::month($familyId, $cals, $me, $from, $to);
|
||||
$events = Calendar::month($familyId, $visCals, $me, $from, $to);
|
||||
$grid = build_month_grid($ym, $events);
|
||||
} else {
|
||||
// 周视图 7 列、天视图 1 列,共用同一个时间网格
|
||||
$cols = day_columns($anchor, $view === 'week' ? 7 : 1);
|
||||
$from = $cols[0]['date'];
|
||||
$to = $cols[count($cols) - 1]['date'];
|
||||
$events = Calendar::month($familyId, $cals, $me, $from, $to);
|
||||
$events = Calendar::month($familyId, $visCals, $me, $from, $to);
|
||||
$grid = build_time_grid($events, $cols);
|
||||
}
|
||||
|
||||
// 用户第 5 条:在日历里点一下就建日程,**开始时间 = 点的那一格**。
|
||||
// 点空白处只带日期/时刻过来,标题等还是空的(不能替用户瞎填)。
|
||||
$newAt = trim((string)($_GET['new_at'] ?? ''));
|
||||
|
||||
View::render('calendar/index', [
|
||||
'title' => '日程',
|
||||
'ym' => $ym,
|
||||
@@ -2858,10 +2983,57 @@ switch ($r) {
|
||||
'weeks' => $view === 'month' ? $grid : [],
|
||||
'tgrid' => $view === 'month' ? null : $grid,
|
||||
'cals' => $cals,
|
||||
'visCals'=> array_map('intval', array_column($visCals, 'id')),
|
||||
'hiddenCals' => array_keys($hiddenCals),
|
||||
'newAt' => $newAt,
|
||||
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||
], 'calendar');
|
||||
break;
|
||||
|
||||
/**
|
||||
* 保存「日程显示设置」(v24 用户 2026-10-03 第 4、5 条)—— 每人一份。
|
||||
*
|
||||
* 用户原话:「在右侧,也加一个设置按钮,设置中,可以设置哪些日历本显示,
|
||||
* 调整日历的颜色,导入日历等功能,都放在这个设置下。」
|
||||
* 以及「在日程的设置中,可以设置默认显示的视图模式」。
|
||||
*
|
||||
* 这一条只存两件**个人偏好**:默认视图 + 关掉哪几本。
|
||||
* 颜色/新建/共享/导入仍在各自的既有路由上(它们改的是本子本身或数据,不是偏好),
|
||||
* 只是界面上统一收进了那个设置下拉里。
|
||||
*/
|
||||
case 'calendar/prefs':
|
||||
Auth::requireLogin();
|
||||
csrf_check();
|
||||
$me = (int)Auth::personId();
|
||||
Calendar::saveDefaultView($me, (string)($_POST['view'] ?? 'month'));
|
||||
// ⚠️ 复选框的语义是"**显示**",但库里存的是"关掉的"(见 hiddenCalendars 的注释)。
|
||||
// 所以这里要把「没勾的」收集起来存。
|
||||
//
|
||||
// ⚠️ 但**只有表单真的带了这部分字段时才动它**:
|
||||
// HTML 里"一个都没勾"会让浏览器整个省掉 `show` 这个键 ——
|
||||
// 与"这个请求压根没想改显隐"(比如将来某个页面只想改默认视图)
|
||||
// 在服务端看起来**一模一样**。
|
||||
// 靠 `show_present` 这个隐藏标记把两者分开:
|
||||
// 没有它 = 不改显隐;有它 + 一个都没勾 = 用户确实想把本子全关掉。
|
||||
// 不这么做的话,任何一次"只改默认视图"的提交都会静默把所有日历本藏掉。
|
||||
if (!empty($_POST['show_present'])) {
|
||||
$onIds = array_map('intval', (array)($_POST['show'] ?? []));
|
||||
$allIds = [];
|
||||
foreach (Calendar::mine((int)Auth::familyId(), $me) as $c) {
|
||||
$allIds[] = (int)$c['id'];
|
||||
}
|
||||
$off = [];
|
||||
foreach ($allIds as $cid) {
|
||||
if (!in_array($cid, $onIds, true)) {
|
||||
$off[] = $cid;
|
||||
}
|
||||
}
|
||||
Calendar::saveHiddenCalendars($me, $off);
|
||||
}
|
||||
flash('ok', '日程显示设置已保存。');
|
||||
redirect('calendar', ['view' => (string)($_POST['back_view'] ?? 'month')]);
|
||||
break;
|
||||
|
||||
/**
|
||||
* CalDAV 专用口令(v24 第 7 条后半):生成 / 重置 / 撤销。
|
||||
*
|
||||
@@ -3354,7 +3526,16 @@ switch ($r) {
|
||||
$me = (int)Auth::personId();
|
||||
$keyword = trim((string)($_GET['keyword'] ?? ''));
|
||||
$ownerId = (int)($_GET['owner'] ?? 0);
|
||||
$rows = Resource::all($familyId, $me, $ownerId, $keyword);
|
||||
// 用户 2026-10-03 第 10 条:按"最新新增 / 最近修改"排序,默认只显示最新 10 条。
|
||||
// 原话:「应该按照最新的或者近期修改使用,进行排序,比如,只显示最新新增或者
|
||||
// 修改或者使用过的 10 个,要不然太多了放不下。上面反正还有搜索呢。」
|
||||
$sort = ($_GET['sort'] ?? '') === 'new' ? 'new' : 'used';
|
||||
$limit = \App\Resource::LIST_LIMIT;
|
||||
// 搜索时不受"只显示 10 条"限制:用户是带着明确目标来找的,
|
||||
// 这时候还只给 10 条会让他以为"没搜到"。看全部也用 all=1 放开。
|
||||
$showAll = !empty($_GET['all']) || $keyword !== '';
|
||||
$rows = Resource::all($familyId, $me, $ownerId, $keyword, $sort, $showAll ? 0 : $limit);
|
||||
$totalAll = Resource::countVisible($familyId, $me, $ownerId, $keyword);
|
||||
// 附件与共享一次取齐(列表里每条都要显示"几个附件""共享给谁")
|
||||
$attachs = [];
|
||||
$shares = [];
|
||||
@@ -3378,6 +3559,10 @@ switch ($r) {
|
||||
'editing' => $editing,
|
||||
'keyword' => $keyword,
|
||||
'ownerId' => $ownerId,
|
||||
'sort' => $sort,
|
||||
'limit' => $limit,
|
||||
'totalAll'=> $totalAll,
|
||||
'showAll' => $showAll,
|
||||
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||
], 'resources');
|
||||
break;
|
||||
@@ -3519,17 +3704,23 @@ switch ($r) {
|
||||
case 'tasks':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
// v24 第 10 条:看板由用户自己建(家务 / 装修 / …),顶上有个切换条
|
||||
$boards = Task::boards($familyId);
|
||||
$bid = (int)($_GET['board'] ?? 0);
|
||||
$board = Task::board($familyId, $me, $bid);
|
||||
// v24(用户 2026-10-03 第 7 条):**列 = 用户自定义的看板本身**。
|
||||
// 原话:「每个人自定义的看板,不是显示在现在的看板上面,而是直接代替现在的
|
||||
// 待办和进行中,是看板,而不是看板的分类。」
|
||||
// 所以不再有「待办 / 进行中」两列、也不再有顶上那个"切换看板"的横条 ——
|
||||
// 一列就是一个看板,都在同一屏上,一眼看全。
|
||||
$bc = Task::boardColumns($familyId, $me);
|
||||
$cols = $bc['cols'];
|
||||
$total = 0;
|
||||
foreach ($cols as $list) {
|
||||
$total += count($list);
|
||||
}
|
||||
View::render('tasks/index', [
|
||||
'title' => '任务',
|
||||
'board' => $board,
|
||||
'boards' => $boards,
|
||||
'curBoard' => $bid > 0 ? $bid : Task::ensureDefaultBoard($familyId),
|
||||
'cols' => $cols,
|
||||
'boards' => $bc['boards'],
|
||||
'categories' => Task::categories($familyId),
|
||||
'total' => count($board['todo']) + count($board['doing']),
|
||||
'total' => $total,
|
||||
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||
], 'tasks');
|
||||
break;
|
||||
@@ -3704,6 +3895,15 @@ switch ($r) {
|
||||
redirect('tasks');
|
||||
}
|
||||
$before = (int)($_POST['before'] ?? 0);
|
||||
// ⚠️ v24(用户 2026-10-03 第 7 条):看板的列现在是**用户自定义的看板**,
|
||||
// 所以"拖到另一列" = **换看板**(改 board_id),不是改 status。
|
||||
// 两种搬法用一个路由:带 board 的走看板,带 status 的走状态
|
||||
// (状态那条保留给任务详情页的"标记完成"和子任务用)。
|
||||
if (array_key_exists('board', $_POST)) {
|
||||
Task::moveToBoard($familyId, $id, (int)$_POST['board'], $before, $me);
|
||||
redirect('tasks');
|
||||
break;
|
||||
}
|
||||
// 子任务只在 todo / done 之间切(看板里没有子任务的列,给它做定位没意义)
|
||||
if (!empty($t['parent_id'])) {
|
||||
$status = $status === 'done' ? 'done' : 'todo';
|
||||
|
||||
Reference in New Issue
Block a user