v24 第 2 轮:19 条清单收尾(附件预览/CalDAV/任务分类看板/全局选人/家谱/纪念日/财务)

用户 2026-10-02 提的 19 条需求,本轮完成剩下的 8 条 + CalDAV 服务端:

- ③ 附件:图片/视频给缩略图,PDF/Word/Excel 给**同尺寸**图标;可预览的在本页
     弹层里看(不再新开标签页),渲染不了的点了直接下载;预览层右上角恒有下载按钮。
- ⑦b CalDAV:把「我的日历本」按 RFC 4791 只读子集分享出去(OPTIONS/PROPFIND/
     REPORT/GET,写方法一律 403)。用**专用口令**(库里只存 sha256),不是登录口令。
- ⑨⑩ 任务:分类与看板都由用户自建;已完成的任务从看板消失,改在「已完成」页
     按分类查看/搜索;设置入口在「新建任务」旁的 ☰。
- ⑪ 全局人员选择器:一个片段铺到观影/旅程/资源/日历共享/任务共享/财务共享;
     修掉了老组件「名字和拼音都搜不动」的 bug —— .att-item 自己声明了 display:flex,
     压过了浏览器 [hidden]{display:none},过滤根本没生效。
- ⑫ 家谱:头像改方形不再裁切;姻亲不在图上的卡片给「⇄ 姻亲」入口,
     以 TA 为中心重画 + 「返回我的家谱」。
- ⑬ 纪念日:阴历/阳历都能设且按对应历法重复;名册里每个人的生日默认可见
     (实时派生,不复制一份);全部作为全天日程进日历。
- ⑮ 财务:账本可共享,双方都能记账、都能看全部账目,资产−负债=净资产;
     金额一律以「分」存,不用浮点。

验证:端到端 724 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 24、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
This commit is contained in:
2026-10-03 08:18:05 +08:00
parent 72dc6c0006
commit 8f80f3e835
47 changed files with 5363 additions and 578 deletions
+89 -4
View File
@@ -128,13 +128,98 @@ final class Attachment
}
}
/** 能在浏览器里直接预览吗(图片 / PDF) */
public static function previewable(array $a): bool
/**
* 这个附件是「哪一类」—— 决定页面上画缩略图还是画图标、点开是预览还是下载。
*
* 返回:image / video / pdf / doc / xls / ppt / text / file
*
* ⚠️ 判据是**库里的 mime**(上传时由服务端嗅探内容得来),不看文件名后缀:
* 改个后缀骗不过它,而"文档其实是张图"这种也认得出来。
*/
public static function kind(array $a): string
{
if ((int)($a['is_image'] ?? 0) === 1) {
return true;
return 'image';
}
return strpos((string)($a['mime'] ?? ''), 'pdf') !== false;
$mime = strtolower((string)($a['mime'] ?? ''));
$name = strtolower((string)($a['name'] ?? ''));
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if ($mime === '' && $ext !== '') {
$mime = self::EXT_MIME[$ext] ?? '';
}
if (strpos($mime, 'video/') === 0) {
return 'video';
}
if (strpos($mime, 'audio/') === 0) {
return 'video'; // 音频用播放器放,与视频同一条路
}
if (strpos($mime, 'pdf') !== false || $ext === 'pdf') {
return 'pdf';
}
if (strpos($mime, 'text/') === 0 || in_array($ext, ['txt', 'md', 'csv', 'log', 'json'], true)) {
return 'text';
}
// Office 的 MIME 一长串且各家写法不一(含 -msword / officedocument / opendocument),
// 扩展名反而更稳,所以这里两条路都认。
if (self::inAny($mime . ' ' . $ext, ['word', 'doc', 'rtf', 'odt', 'wps'])) {
return 'doc';
}
if (self::inAny($mime . ' ' . $ext, ['excel', 'sheet', 'xls', 'xlsx', 'csv', 'ods', 'et'])) {
return 'xls';
}
if (self::inAny($mime . ' ' . $ext, ['powerpoint', 'presentation', 'ppt', 'pps', 'odp', 'dps'])) {
return 'ppt';
}
return 'file';
}
/**
* 能在**本页**直接预览吗。
*
* 图片与视频不用说;PDF 浏览器自带阅读器,也能直接看。
* Word / Excel 之类浏览器渲染不了 —— 点了就下载(不装"点了没反应"的预览)。
*/
public static function previewable(array $a): bool
{
return in_array(self::kind($a), ['image', 'video', 'pdf'], true);
}
/** 这一类在人话里叫什么(图标上的角标、预览标题都用它) */
public static function kindWord(string $kind): string
{
return self::KIND_WORDS[$kind] ?? '文件';
}
private const KIND_WORDS = [
'image' => '图片', 'video' => '视频', 'pdf' => 'PDF',
'doc' => 'Word', 'xls' => 'Excel', 'ppt' => 'PPT',
'text' => '文本', 'file' => '文件',
];
/** 扩展名兜底用的 MIME 表(库里 mime 为空时) */
private const EXT_MIME = [
'pdf' => 'application/pdf', 'txt' => 'text/plain', 'md' => 'text/markdown',
'csv' => 'text/csv', 'json' => 'application/json',
'mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'webm' => 'video/webm',
'mov' => 'video/quicktime', 'avi' => 'video/x-msvideo', 'mkv' => 'video/x-matroska',
'mp3' => 'audio/mpeg', 'm4a' => 'audio/mp4', 'wav' => 'audio/wav',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'zip' => 'application/zip', 'rar' => 'application/x-rar-compressed',
];
private static function inAny(string $hay, array $needles): bool
{
foreach ($needles as $n) {
if (strpos($hay, $n) !== false) {
return true;
}
}
return false;
}
/** 人话的大小 */