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:
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* 附件缩略图墙(v24 用户第 3 条)
|
||||
*
|
||||
* 一处渲染、全站通用:旅程节点 / 资源 / 任务 / 日程的附件都长一样。
|
||||
*
|
||||
* 规则(用户原话):
|
||||
* · 图片、视频 → **缩略图**,点开在**本页**预览(不再新开标签页)
|
||||
* · PDF → 浏览器里直接预览(自带阅读器)
|
||||
* · Word / Excel / PPT / 其它 → 一个**和缩略图一样大**的图标,点了直接下载
|
||||
* (浏览器渲染不了的东西,绝不装一个"点了没反应"的预览)
|
||||
* · 预览框**右上角**恒有一个下载按钮
|
||||
*
|
||||
* 用法(先准备变量再 require):
|
||||
* $attRows 附件行(Attachment::list 的结果)
|
||||
* $attCanEdit 是否显示删除按钮
|
||||
* $attDelUrl 删除动作的路由名(如 'resources/attach_del')
|
||||
* $attHostId 删除时要回传的宿主 id(表单里的隐藏字段用 $attHostKey 指定键名)
|
||||
* $attDelField 删除表单里「附件 id」的字段名(各路由不一致:有的叫 id、有的叫 attach_id)
|
||||
* $attExtra 额外隐藏字段 [键 => 值](如日程要连带传宿主 id 才能判权限)
|
||||
*
|
||||
* ⚠️ 无 JS 也能用:可预览的是 <a href=预览地址>,不可预览的是 <a href=下载地址>,
|
||||
* 脚本只是把"可预览"的那部分拦下来改成弹层。
|
||||
*/
|
||||
|
||||
$attCanEdit = !empty($attCanEdit);
|
||||
$attRows = $attRows ?? [];
|
||||
$attDelField = $attDelField ?? 'id';
|
||||
$attExtra = $attExtra ?? [];
|
||||
$attDelUrl = $attDelUrl ?? '';
|
||||
?>
|
||||
<?php if ($attRows): ?>
|
||||
<div class="att-wall">
|
||||
<?php foreach ($attRows as $a): ?>
|
||||
<?php
|
||||
$kind = \App\Attachment::kind($a);
|
||||
$canPrev = \App\Attachment::previewable($a);
|
||||
$view = media_url((string)$a['key']);
|
||||
$dl = media_url((string)$a['key'], ['dl' => 1]);
|
||||
$nm = (string)($a['name'] ?? '');
|
||||
$sz = \App\Attachment::sizeWord($a['size'] ?? 0);
|
||||
?>
|
||||
<div class="att-cell">
|
||||
<?php if ($canPrev): ?>
|
||||
<a class="att-thumb att-kind-<?= e($kind) ?>"
|
||||
href="<?= e($view) ?>"
|
||||
data-prev="<?= e($kind === 'pdf' ? 'pdf' : ($kind === 'video' ? 'video' : 'image')) ?>"
|
||||
data-src="<?= e($view) ?>"
|
||||
data-dl="<?= e($dl) ?>"
|
||||
data-name="<?= e($nm !== '' ? $nm : \App\Attachment::kindWord($kind)) ?>"
|
||||
data-size="<?= e($sz) ?>"
|
||||
title="<?= e($nm) ?>">
|
||||
<?php if ($kind === 'image'): ?>
|
||||
<img src="<?= e($view) ?>" alt="<?= e($nm) ?>" loading="lazy">
|
||||
<?php elseif ($kind === 'video'): ?>
|
||||
<?php /* #t=0.1 让浏览器把第 0.1 秒那一帧当封面显示出来 ——
|
||||
没有它视频位就是一块黑板,看不出这是段什么视频。 */ ?>
|
||||
<video src="<?= e($view) ?>#t=0.1" preload="metadata" muted playsinline></video>
|
||||
<span class="att-play" aria-hidden="true">▶</span>
|
||||
<?php else: ?>
|
||||
<span class="att-doc att-doc-pdf">PDF</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a class="att-thumb att-kind-<?= e($kind) ?>" href="<?= e($dl) ?>"
|
||||
title="<?= e($nm) ?>(点击下载)">
|
||||
<span class="att-doc att-doc-<?= e($kind) ?>"><?= e(\App\Attachment::kindWord($kind)) ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="att-meta">
|
||||
<span class="att-fname" title="<?= e($nm) ?>"><?= e($nm !== '' ? $nm : \App\Attachment::kindWord($kind)) ?></span>
|
||||
<span class="att-fsize"><?= e($sz) ?></span>
|
||||
<a class="link-quiet att-dl" href="<?= e($dl) ?>">下载</a>
|
||||
<?php if ($attCanEdit && !empty($attDelUrl)): ?>
|
||||
<form method="post" action="<?= e(url($attDelUrl)) ?>" class="att-del">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="<?= e($attDelField) ?>" value="<?= (int)$a['id'] ?>">
|
||||
<?php foreach ($attExtra as $ek => $ev): ?>
|
||||
<input type="hidden" name="<?= e((string)$ek) ?>" value="<?= e((string)$ev) ?>">
|
||||
<?php endforeach; ?>
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这个附件?')">删除</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php /* ⚠️ 循环里用到的临时变量一律用完即清:宿主模板里很可能也有 $a / $kind /
|
||||
$view / $dl / $nm / $sz(各模块页面都爱用这种短名),留着就是静默覆盖。 */ ?>
|
||||
<?php unset($a, $kind, $canPrev, $view, $dl, $nm, $sz); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -24,7 +24,13 @@ $dtRequired = (bool)($dtRequired ?? false);
|
||||
$dtId = (string)($dtId ?? '');
|
||||
$dtSeconds = (bool)($dtSeconds ?? true);
|
||||
|
||||
$dtTs = (!empty($dtValue) && ($t = strtotime(trim((string)$dtValue)))) ? $t : false;
|
||||
// ⚠️⚠️ 临时变量**一律带 dt 前缀**,一个都不能漏:这里原来写成
|
||||
// `$t = strtotime(...)`,把宿主模板里表示「这条任务 / 这条记录」的 `$t`
|
||||
// 覆盖成了 int —— 于是 include 之后凡是读 `$t['id']`、`$t['status']` 的地方
|
||||
// 全部拿到 null,子任务列表空、评论不渲染、@高亮消失,而且**不报任何错**
|
||||
// (PHP 8 对 "Trying to access array offset on value of type int" 只 Warning)。
|
||||
// tasks/show.php 上就是这么被 e2e 抓到的(2026-10-02)。
|
||||
$dtTs = (!empty($dtValue) && ($dtTsTmp = strtotime(trim((string)$dtValue)))) ? $dtTsTmp : false;
|
||||
$dtDate = $dtTs ? date('Y-m-d', $dtTs) : '';
|
||||
$dtHour = $dtTs ? (int)date('G', $dtTs) : 0; // G = 0..23,天然 24 小时制
|
||||
$dtMin = $dtTs ? (int)date('i', $dtTs) : 0;
|
||||
@@ -62,6 +68,6 @@ $dtSec = $dtTs ? (int)date('s', $dtTs) : 0;
|
||||
</div>
|
||||
<?php /* 用完即清,避免残留变量影响下一次 include */ unset(
|
||||
$dtName, $dtValue, $dtRequired, $dtId, $dtSeconds,
|
||||
$dtTs, $dtDate, $dtHour, $dtMin, $dtSec,
|
||||
$dtTs, $dtTsTmp, $dtDate, $dtHour, $dtMin, $dtSec,
|
||||
$dtH, $dtM, $dtS, $hv, $mv, $sv
|
||||
); ?>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* 全局人员选择器(v24 用户第 11 条)
|
||||
*
|
||||
* 「所有要查找人的地方」统一走这一个片段:chips + 弹窗 + 拼音首字母联想。
|
||||
* 之前只有观影表单有这套(.att-*),旅程是平铺的 checkbox 网格、共享/归属是
|
||||
* <select> 下拉 —— 名册一多就变成"在一长串里瞪眼找",而且三种地方三种手感。
|
||||
*
|
||||
* 用法(在模板里先准备好变量再 include):
|
||||
* $ppName 表单字段名。多选传 'attendees[]';单选传 'owner_id' 等
|
||||
* $ppPeople 候选的人(Person::roster() 的行,至少要有 id / name)
|
||||
* $ppSelected 已选的 id 数组(单选传 0 或 1 个)
|
||||
* $ppMode 'multi'(默认)| 'single'
|
||||
* $ppOpenBtn 打开按钮文案(默认 multi=「+ 添加人」/ single=「选择」)
|
||||
* $ppTitle 弹窗标题(默认「选择人」)
|
||||
* $ppAllowEmpty 单选模式下是否给一个「不选」选项(筛选表单用;值为 0)
|
||||
* $ppEmptyText 单选且未选时的占位(默认「未选择」)
|
||||
* $ppTip 可选,渲染在按钮下面的一句话
|
||||
*
|
||||
* ⚠️ 无 JS 时也能用:选项就是弹窗列表里的原生 checkbox / radio,
|
||||
* noscript 样式把弹窗摊平成一个普通列表,提交语义与有 JS 时完全一致。
|
||||
*
|
||||
* ⚠️ 同一页出现多个选择器时 $ppId 必须不同 —— 片段自己按出现顺序编号,
|
||||
* 调用方不用管(但同一页重复 include 同一份变量也不会撞)。
|
||||
*/
|
||||
|
||||
$ppMode = $ppMode ?? 'multi';
|
||||
$ppPeople = $ppPeople ?? [];
|
||||
$ppSelected = array_map('intval', (array)($ppSelected ?? []));
|
||||
$ppOpenBtn = $ppOpenBtn ?? ($ppMode === 'single' ? '选择' : '+ 添加人');
|
||||
$ppTitle = $ppTitle ?? '选择';
|
||||
$ppEmptyText = $ppEmptyText ?? '未选择';
|
||||
$ppAllowEmpty = !empty($ppAllowEmpty);
|
||||
|
||||
// 同一页多个实例要各有一套 id,否则 document.getElementById 全抓到第一个
|
||||
static $ppSeq = 0;
|
||||
$ppSeq++;
|
||||
$ppId = 'pp' . $ppSeq;
|
||||
$ppIsMulti = $ppMode === 'single' ? false : true;
|
||||
$ppInputType = $ppIsMulti ? 'checkbox' : 'radio';
|
||||
// ⚠️ 「已选」要按**非零**判断:筛选表单常用 0 表示「全部」,[0] 是个非空数组,
|
||||
// 直接 !$ppSelected 会误判成"选了人",于是「不选」那一项反而不勾。
|
||||
$ppHasSel = count(array_filter($ppSelected)) > 0;
|
||||
?>
|
||||
<div class="pp" id="<?= e($ppId) ?>" data-pp="<?= e($ppId) ?>" data-pp-mode="<?= e($ppIsMulti ? 'multi' : 'single') ?>">
|
||||
<div class="pp-chips" id="<?= e($ppId) ?>-chips">
|
||||
<?php /* ⚠️ 循环变量一律用 $ppRow,绝不叫 $p:宿主模板里 $p 常常是别的东西
|
||||
(高级搜索页的 $p 是"查询条件数组"),被这个片段一覆盖,
|
||||
页面后半段的 $p['xxx'] 就全成了 Warning。_datetime.php 踩过同款。 */ ?>
|
||||
<?php foreach ($ppPeople as $ppRow): ?>
|
||||
<?php if (in_array((int)$ppRow['id'], $ppSelected, true)): ?>
|
||||
<span class="pp-chip" data-id="<?= (int)$ppRow['id'] ?>"><img src="<?= e(avatar_url($ppRow['avatar'] ?? null)) ?>" alt=""><span class="pp-name"><?= e(person_name($ppRow)) ?></span><?php if ($ppIsMulti): ?><button type="button" class="pp-x" data-id="<?= (int)$ppRow['id'] ?>" title="移除">×</button><?php endif; ?></span>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!$ppIsMulti && !$ppHasSel): ?>
|
||||
<span class="pp-chip pp-chip-empty"><?= e($ppEmptyText) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost btn-sm pp-open" id="<?= e($ppId) ?>-open"><?= e($ppOpenBtn) ?></button>
|
||||
<?php if (!empty($ppTip)): ?><span class="tip"><?= e($ppTip) ?></span><?php endif; ?>
|
||||
<div class="pp-modal" id="<?= e($ppId) ?>-modal" hidden>
|
||||
<div class="pp-modal-box">
|
||||
<div class="pp-modal-head">
|
||||
<b><?= e($ppTitle) ?></b>
|
||||
<button type="button" class="pp-close" id="<?= e($ppId) ?>-close" title="关闭">×</button>
|
||||
</div>
|
||||
<input class="input pp-search" id="<?= e($ppId) ?>-search" type="text"
|
||||
placeholder="输入名字或拼音首字母,如 yyq 找「杨永全」" autocomplete="off">
|
||||
<div class="pp-list" id="<?= e($ppId) ?>-list">
|
||||
<?php if (!$ppIsMulti && $ppAllowEmpty): ?>
|
||||
<label class="pp-item pp-item-none" data-name="不选" data-shown="不选">
|
||||
<input type="radio" name="<?= e($ppName) ?>" value="0" <?= $ppHasSel ? '' : 'checked' ?>>
|
||||
<span class="pp-name">不选</span>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($ppPeople as $ppRow): ?>
|
||||
<label class="pp-item"
|
||||
data-name="<?= e((string)$ppRow['name']) ?>"
|
||||
data-shown="<?= e(person_name($ppRow)) ?>">
|
||||
<input type="<?= e($ppInputType) ?>" name="<?= e($ppName) ?>" value="<?= (int)$ppRow['id'] ?>"
|
||||
<?= in_array((int)$ppRow['id'], $ppSelected, true) ? 'checked' : '' ?>>
|
||||
<img class="pp-av" src="<?= e(avatar_url($ppRow['avatar'] ?? null)) ?>" alt="" loading="lazy">
|
||||
<span class="pp-name"><?= e(person_name($ppRow)) ?></span>
|
||||
<span class="pp-initials" hidden></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<?php unset($ppRow); ?>
|
||||
<div class="pp-empty" hidden>没有匹配的人。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<noscript><style>.pp-open { display: none; } .pp-modal { position: static; display: block; background: none; padding: 0; } .pp-modal-box { box-shadow: none; border: 1px solid var(--line); max-height: none; }</style></noscript>
|
||||
</div>
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/**
|
||||
* 日程表单的字段块(新建与编辑共用一份 —— 复制一遍迟早改漏一处)。
|
||||
*
|
||||
* 字段按 **RFC 5545(iCalendar)/ RFC 4791(CalDAV)** 的 VEVENT 属性定:
|
||||
* 标题 = SUMMARY
|
||||
* 地点 = LOCATION
|
||||
* 开始/结束 = DTSTART / DTEND(DATETIME,精确到秒)
|
||||
* 全天 —— VEVENT 没有"全天"这个属性,约定俗成是把 DTSTART 写成 DATE 值;
|
||||
* 我们统一存 DATETIME,全天时时间部分为 00:00:00,导出 ics 时再落成 DATE 值。
|
||||
* 提醒 = VALARM 的 TRIGGER(提前多久);不提醒 = 不写 VALARM
|
||||
* 状态 = STATUS(CONFIRMED / TENTATIVE / CANCELLED)
|
||||
* 忙闲 = TRANSP(OPAQUE=忙 / TRANSPARENT=空闲)
|
||||
* 备注 = DESCRIPTION
|
||||
* 附件 = ATTACH
|
||||
*
|
||||
* 用法(include 前设好):
|
||||
* $evFields 正在编辑的日程(新建时传空数组)
|
||||
* $evAllDay 全天的默认值(新建默认**不勾**,用户 2026-10-02 第 6 条)
|
||||
*/
|
||||
$evF = is_array($evFields ?? null) ? $evFields : [];
|
||||
$evAllDayDefault = (bool)($evAllDay ?? false);
|
||||
$evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evAllDayDefault;
|
||||
?>
|
||||
<div class="field full">
|
||||
<label for="ev-title">标题 *</label>
|
||||
<input class="input" id="ev-title" name="title" required
|
||||
value="<?= e((string)($evF['title'] ?? '')) ?>" placeholder="如:家长会">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="ev-location">地点</label>
|
||||
<input class="input" id="ev-location" name="location"
|
||||
value="<?= e((string)($evF['location'] ?? '')) ?>" placeholder="如:学校三楼会议室">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>提醒</label>
|
||||
<?php /* 不提醒 = NULL(导出 ics 时不写 VALARM);档位就是用户第 6 条列的那些。 */ ?>
|
||||
<select class="select" name="reminder_minutes" aria-label="提前多久提醒">
|
||||
<?php $cur = $evF['reminder_minutes'] ?? null; ?>
|
||||
<option value=""<?= $cur === null || $cur === '' ? ' selected' : '' ?>>不提醒</option>
|
||||
<?php foreach (\App\Calendar::REMINDERS as $mins => $word): ?>
|
||||
<option value="<?= (int)$mins ?>"<?= (string)$cur === (string)$mins ? ' selected' : '' ?>>
|
||||
<?= e($word) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<?php /* v24 第 5、6 条:起止各一个「日期 + 时分秒」合在一起的控件(全局统一),
|
||||
不再拆成"日期一格 + 时刻一格"。两个控件长得一样,扫一眼就知道哪个是开始。 */ ?>
|
||||
<div class="field ev-dt" data-dt>
|
||||
<label>开始 *</label>
|
||||
<?php $dtName = 'starts_at'; $dtId = 'ev-start';
|
||||
$dtValue = $evF['starts_at'] ?? null; $dtRequired = true; $dtSeconds = true;
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div>
|
||||
<div class="field ev-dt" data-dt>
|
||||
<label>结束</label>
|
||||
<?php $dtName = 'ends_at'; $dtId = 'ev-end';
|
||||
$dtValue = $evF['ends_at'] ?? null; $dtRequired = false; $dtSeconds = true;
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
<input type="checkbox" id="ev-allday" name="all_day" value="1"
|
||||
<?= $evIsAllDay ? 'checked' : '' ?>>
|
||||
全天
|
||||
</label>
|
||||
<span class="field-hint">勾上就不管具体几点,只占这一天</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="ev-status">状态</label>
|
||||
<select class="select" id="ev-status" name="status">
|
||||
<?php $st = (string)($evF['status'] ?? 'confirmed'); ?>
|
||||
<option value="confirmed"<?= $st === 'confirmed' ? ' selected' : '' ?>>确定</option>
|
||||
<option value="tentative"<?= $st === 'tentative' ? ' selected' : '' ?>>待定</option>
|
||||
<option value="cancelled"<?= $st === 'cancelled' ? ' selected' : '' ?>>取消</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="ev-transp">这段时间算</label>
|
||||
<select class="select" id="ev-transp" name="transp">
|
||||
<?php $tp = (int)($evF['transp'] ?? 0); ?>
|
||||
<option value="0"<?= $tp === 0 ? ' selected' : '' ?>>忙碌</option>
|
||||
<option value="1"<?= $tp === 1 ? ' selected' : '' ?>>空闲(别人还能约)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="ev-note">备注</label>
|
||||
<textarea class="input" id="ev-note" name="note" rows="3"
|
||||
placeholder="要带什么、跟谁一起…"><?= e((string)($evF['note'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="ev-attach">附件(可多选)</label>
|
||||
<input class="input" type="file" id="ev-attach" name="attach[]" multiple>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
/* 「全天」勾上就把两个时刻控件的**时间部分**收起来 —— 日期还得留着。
|
||||
只藏 .dt-time(时/分/秒那一段),.dt-date 是日期,藏了就没法选哪天了。 */
|
||||
var box = document.getElementById('ev-allday');
|
||||
if (!box) return;
|
||||
function sync() {
|
||||
Array.prototype.forEach.call(document.querySelectorAll('[data-dt] .dt-time'), function (el) {
|
||||
el.hidden = box.checked;
|
||||
});
|
||||
}
|
||||
box.addEventListener('change', sync);
|
||||
sync();
|
||||
})();
|
||||
</script>
|
||||
<?php unset($evF, $evAllDayDefault, $evIsAllDay, $evFields, $evAllDay); ?>
|
||||
@@ -102,12 +102,17 @@ $canWrite = \App\Auth::canWrite();
|
||||
<form class="inline-form" method="post" action="<?= e(url('calendar/cal_share')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||||
<select class="select" name="person_id">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<?php if ((int)$p['id'] === (int)\App\Auth::personId()) { continue; } ?>
|
||||
<option value="<?= (int)$p['id'] ?>"><?= e(person_name($p)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php
|
||||
$ppName = 'person_id';
|
||||
$ppPeople = array_values(array_filter($people,
|
||||
fn($p) => (int)$p['id'] !== (int)\App\Auth::personId()));
|
||||
$ppSelected = [];
|
||||
$ppMode = 'single';
|
||||
$ppOpenBtn = '选择要共享给谁';
|
||||
$ppTitle = '共享给谁';
|
||||
$ppEmptyText = '未选择';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
<select class="select" name="perm">
|
||||
<option value="read">只读</option>
|
||||
<option value="write">可写</option>
|
||||
@@ -129,6 +134,9 @@ $canWrite = \App\Auth::canWrite();
|
||||
<?php if ($canWrite): ?>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<?php /* v24 第 7 条:每本都能导出 .ics(预置本也能 —— 导的是派生出来的内容)。 */ ?>
|
||||
<a class="btn btn-ghost btn-sm"
|
||||
href="<?= e(url('calendar/export', ['id' => (int)$c['id']])) ?>">导出 ics</a>
|
||||
<?php if (($c['kind'] ?? '') === 'custom'): ?>
|
||||
<a class="btn btn-ghost btn-sm"
|
||||
href="<?= e(url('calendar/calendars', ['edit' => (int)$c['id']])) ?>">编辑</a>
|
||||
@@ -152,6 +160,89 @@ $canWrite = \App\Auth::canWrite();
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canWrite): ?>
|
||||
<?php /* v24 第 7 条:把一个 .ics 文件里的日程整批导进某一本。
|
||||
只能导进自定义本 —— 观影/旅行两本是派生的,塞进去就成了孤儿日程
|
||||
(日历里有、观影列表里没有)。 */ ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">
|
||||
导入 ics
|
||||
<span class="hint">把别的日历导出的 .ics 文件整批装进来</span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 16px">
|
||||
同一个文件导两次也<b>不会重复</b>:每条日程有个全球唯一的 UID,
|
||||
已经导过的会被更新,而不是再插一条。
|
||||
</p>
|
||||
<form method="post" action="<?= e(url('calendar/import')) ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label for="im-cal">导进哪一本 *</label>
|
||||
<select class="select" id="im-cal" name="calendar_id">
|
||||
<?php foreach ($cals as $c): ?>
|
||||
<?php if (($c['kind'] ?? '') !== 'custom') { continue; } ?>
|
||||
<option value="<?= (int)$c['id'] ?>"><?= e((string)$c['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="im-file">.ics 文件 *</label>
|
||||
<input class="input" type="file" id="im-file" name="ics" accept=".ics,text/calendar" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-sm" type="submit">导入</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* v24 第 7 条后半:CalDAV —— 把我的日历本订阅到手机/别的日历程序里 */ ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">
|
||||
CalDAV 订阅
|
||||
<span class="hint">在手机日历 / DAVx⁵ / Thunderbird 里订阅你的日历本</span>
|
||||
</h2>
|
||||
<p class="small muted" style="margin:0 0 12px">
|
||||
填下面这个地址 + 你的<b>姓名或手机号</b> + <b>CalDAV 口令</b>,就能把「我的日历本」
|
||||
同步到别的日历程序里。这是<b>只读</b>订阅的 —— 在手机上改日程请回到本站来改。
|
||||
<br>口令与登录密码是<b>分开的</b>:订阅凭据会被客户端长期存着,所以另发一个,
|
||||
随时可换、不影响登录。
|
||||
</p>
|
||||
<div class="field">
|
||||
<label>服务器地址</label>
|
||||
<input class="input mono" readonly
|
||||
value="<?= e(rtrim((string)fb_config('base_url', ''), '/') . '/dav/') ?>">
|
||||
<span class="tip">在日历客户端里填这一条即可,会列出你所有的日历本。</span>
|
||||
</div>
|
||||
<?php if (!empty($cdPlain)): ?>
|
||||
<p class="tip" style="background:#FFF6E5;border-color:#E8C98A;padding:8px 12px;border-radius:8px">
|
||||
<b>这是你的新口令,只显示这一次,请立刻复制走:</b>
|
||||
<br><code class="mono" style="font-size:15px"><?= e($cdPlain) ?></code>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if ($canWrite): ?>
|
||||
<div class="btn-row">
|
||||
<form method="post" action="<?= e(url('calendar/caldav_token')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<button class="btn btn-sm" type="submit">
|
||||
<?= !empty($cdHas) ? '重置 CalDAV 口令' : '生成 CalDAV 口令' ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php if (!empty($cdHas)): ?>
|
||||
<form method="post" action="<?= e(url('calendar/caldav_revoke')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('撤销后所有已订阅的客户端都会失去访问。')">撤销口令</button>
|
||||
</form>
|
||||
<span class="node-badge">口令已配置</span>
|
||||
<?php else: ?>
|
||||
<span class="node-badge">还没配置口令</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($shared): ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">别人共享给我的</h2>
|
||||
|
||||
@@ -16,61 +16,75 @@ $canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<?php $ym = substr((string)$ev['starts_at'], 0, 7); ?>
|
||||
<?php if (!$canWrite): ?>
|
||||
<dl class="kv">
|
||||
<div class="kv-row"><dt>日期</dt><dd><?= e((string)$ev['starts_at']) ?></dd></div>
|
||||
<?php if (!empty($ev['ends_at'])): ?>
|
||||
<div class="kv-row"><dt>到</dt><dd><?= e((string)$ev['ends_at']) ?></dd></div>
|
||||
<?php endif; ?>
|
||||
<div class="kv-row"><dt>时间</dt>
|
||||
<dd><?= empty($ev['all_day']) ? e(substr((string)$ev['start_time'], 0, 5) . ' – ' . substr((string)$ev['end_time'], 0, 5)) : '全天' ?></dd>
|
||||
<div class="kv-row"><dt>开始</dt>
|
||||
<dd><?= e(empty($ev['all_day']) ? (string)$ev['starts_at'] : substr((string)$ev['starts_at'], 0, 10)) ?></dd>
|
||||
</div>
|
||||
<?php if (!empty($ev['ends_at'])): ?>
|
||||
<div class="kv-row"><dt>结束</dt>
|
||||
<dd><?= e(empty($ev['all_day']) ? (string)$ev['ends_at'] : substr((string)$ev['ends_at'], 0, 10)) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($ev['all_day'])): ?>
|
||||
<div class="kv-row"><dt></dt><dd>全天</dd></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($ev['location'])): ?>
|
||||
<div class="kv-row"><dt>地点</dt><dd><?= e((string)$ev['location']) ?></dd></div>
|
||||
<?php endif; ?>
|
||||
<?php if (($ev['reminder_minutes'] ?? null) !== null): ?>
|
||||
<div class="kv-row"><dt>提醒</dt>
|
||||
<dd><?= e(\App\Calendar::REMINDERS[(int)$ev['reminder_minutes']] ?? '') ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($ev['note'])): ?>
|
||||
<div class="kv-row"><dt>备注</dt><dd><?= nl2br(e((string)$ev['note'])) ?></dd></div>
|
||||
<?php endif; ?>
|
||||
<?php $roAs = \App\Attachment::list(\App\Calendar::ATTACH, (int)$ev['id']); ?>
|
||||
<?php if ($roAs): ?>
|
||||
<div class="kv-row"><dt>附件</dt>
|
||||
<dd class="attach-row">
|
||||
<?php foreach ($roAs as $a): ?>
|
||||
<span class="attach-item">
|
||||
<a href="<?= e(media_url((string)$a['key'])) ?>" target="_blank" rel="noopener">
|
||||
<?= (int)$a['is_image'] === 1 ? '🖼' : '📄' ?> <?= e((string)($a['name'] ?: '附件')) ?>
|
||||
</a>
|
||||
<a class="link-quiet" href="<?= e(media_url((string)$a['key'], ['dl' => 1])) ?>">下载</a>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
<p class="empty-hint">这本日历你只有只读权限,看得到但不能改。</p>
|
||||
<?php else: ?>
|
||||
<form method="post" action="<?= e(url('calendar/event_update')) ?>">
|
||||
<form method="post" action="<?= e(url('calendar/event_update')) ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$ev['id'] ?>">
|
||||
<?php $ym = substr((string)$ev['starts_at'], 0, 7); ?>
|
||||
<input type="hidden" name="ym" value="<?= e($ym) ?>">
|
||||
<div class="form-grid">
|
||||
<div class="field full">
|
||||
<label for="ev-title">标题 *</label>
|
||||
<input class="input" id="ev-title" name="title" required value="<?= e((string)$ev['title']) ?>">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ev-date">开始日期 *</label>
|
||||
<input class="input" type="date" id="ev-date" name="date" required
|
||||
value="<?= e((string)$ev['starts_at']) ?>">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ev-end">结束日期(跨天时填)</label>
|
||||
<input class="input" type="date" id="ev-end" name="end_date" value="<?= e((string)($ev['ends_at'] ?? '')) ?>">
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label><input type="checkbox" name="all_day" value="1" <?= empty($ev['all_day']) ? '' : 'checked' ?>> 全天(不填具体时分)</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ev-st">开始时刻</label>
|
||||
<input class="input" type="time" id="ev-st" name="start_time" value="<?= e(substr((string)($ev['start_time'] ?? ''), 0, 5)) ?>">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ev-et">结束时刻</label>
|
||||
<input class="input" type="time" id="ev-et" name="end_time" value="<?= e(substr((string)($ev['end_time'] ?? ''), 0, 5)) ?>">
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label for="ev-note">备注</label>
|
||||
<textarea class="input" id="ev-note" name="note" rows="3"><?= e((string)($ev['note'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
<?php $evFields = $ev; $evAllDay = false; require __DIR__ . '/_event_fields.php'; ?>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit">保存</button>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $ym])) ?>">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php $as = \App\Attachment::list(\App\Calendar::ATTACH, (int)$ev['id']); ?>
|
||||
<?php if ($as): ?>
|
||||
<div class="ev-attach-list">
|
||||
<span class="res-label">已有附件</span>
|
||||
<?php
|
||||
$attRows = $as;
|
||||
$attCanEdit = true;
|
||||
$attDelUrl = 'calendar/attach_del';
|
||||
$attDelField = 'attach_id';
|
||||
$attExtra = ['id' => (int)$ev['id']];
|
||||
require __DIR__ . '/../_attachments.php';
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php /* ⚠️ 删除表单必须放在上面那个表单**外面**:<form> 套 <form> 是非法 HTML,
|
||||
浏览器会把外层表单在嵌套处切开,"保存"按钮就失效了。 */ ?>
|
||||
<form method="post" action="<?= e(url('calendar/event_del')) ?>" class="ev-del"
|
||||
|
||||
+105
-46
@@ -17,19 +17,55 @@
|
||||
* @param array $people 名册(共享给别人时用)
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$view = (string)($view ?? 'month');
|
||||
$anchor = (string)($anchor ?? date('Y-m-d'));
|
||||
$ts = strtotime($ym . '-01') ?: time();
|
||||
$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())));
|
||||
|
||||
// 三种看法各自的「上一段 / 下一段」:月按整月走,周按 7 天走,天按 1 天走
|
||||
$step = $view === 'week' ? 7 : ($view === 'day' ? 1 : 0);
|
||||
$prevD = date('Y-m-d', strtotime('-' . $step . ' days', strtotime($anchor)));
|
||||
$nextD = date('Y-m-d', strtotime('+' . $step . ' days', strtotime($anchor)));
|
||||
$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>
|
||||
<span class="sub"><?= e(date('Y 年 n 月', $ts)) ?></span>
|
||||
<span class="sub"><?= e($headText) ?></span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $prevYm])) ?>">上一月</a>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $thisYm])) ?>">今天</a>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $nextYm])) ?>">下一月</a>
|
||||
<?php /* v24 第 4 条:月 / 周 / 天三选一 */ ?>
|
||||
<span class="seg" role="group" aria-label="看法">
|
||||
<a class="seg-btn<?= $view === 'month' ? ' is-on' : '' ?>"
|
||||
href="<?= e(url('calendar', ['view' => 'month', 'ym' => $ym])) ?>">月</a>
|
||||
<a class="seg-btn<?= $view === 'week' ? ' is-on' : '' ?>"
|
||||
href="<?= e(url('calendar', ['view' => 'week', 'd' => $anchor])) ?>">周</a>
|
||||
<a class="seg-btn<?= $view === 'day' ? ' is-on' : '' ?>"
|
||||
href="<?= e(url('calendar', ['view' => 'day', 'd' => $anchor])) ?>">天</a>
|
||||
</span>
|
||||
<?php if ($view === 'month'): ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => 'month', 'ym' => $prevYm])) ?>">上一月</a>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => 'month', 'ym' => $thisYm])) ?>">今天</a>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => 'month', 'ym' => $nextYm])) ?>">下一月</a>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => $view, 'd' => $prevD])) ?>">
|
||||
<?= $view === 'week' ? '上一周' : '前一天' ?>
|
||||
</a>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => $view, 'd' => $todayD])) ?>">今天</a>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => $view, 'd' => $nextD])) ?>">
|
||||
<?= $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>
|
||||
@@ -68,7 +104,7 @@ $writable = array_values(array_filter($cals, static fn($c) => \App\Calendar::can
|
||||
<?php if ($canWrite && $writable): ?>
|
||||
<div class="card card-pad cal-add" id="calAddCard" hidden>
|
||||
<h2 class="cinema-h2">新建日程</h2>
|
||||
<form method="post" action="<?= e(url('calendar/event_add')) ?>">
|
||||
<form method="post" action="<?= e(url('calendar/event_add')) ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ym" value="<?= e($ym) ?>">
|
||||
<div class="form-grid">
|
||||
@@ -80,34 +116,14 @@ $writable = array_values(array_filter($cals, static fn($c) => \App\Calendar::can
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ce-title">标题 *</label>
|
||||
<input class="input" id="ce-title" name="title" required placeholder="如:家长会">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ce-date">日期 *</label>
|
||||
<input class="input" type="date" id="ce-date" name="date" required
|
||||
value="<?= e($ym === $thisYm ? date('Y-m-d') : $ym . '-01') ?>">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ce-end">结束日期(跨天时填)</label>
|
||||
<input class="input" type="date" id="ce-end" name="end_date">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label><input type="checkbox" id="ce-allday" name="all_day" value="1" checked> 全天(不填具体时分)</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ce-note">备注</label>
|
||||
<input class="input" id="ce-note" name="note">
|
||||
</div>
|
||||
<div class="field" id="ce-time-s">
|
||||
<label for="ce-st">开始时刻</label>
|
||||
<input class="input" type="time" id="ce-st" name="start_time">
|
||||
</div>
|
||||
<div class="field" id="ce-time-e">
|
||||
<label for="ce-et">结束时刻</label>
|
||||
<input class="input" type="time" id="ce-et" name="end_time">
|
||||
</div>
|
||||
<?php /* v24 第 6 条:字段按 iCalendar / CalDAV 的 VEVENT 定(见 _event_fields.php)。
|
||||
新建时「全天」默认**不勾**(用户原话),开始时间默认今天 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'];
|
||||
$evAllDay = false;
|
||||
require __DIR__ . '/_event_fields.php';
|
||||
?>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-sm" type="submit">加上</button>
|
||||
@@ -117,6 +133,60 @@ $writable = array_values(array_filter($cals, static fn($c) => \App\Calendar::can
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($view === 'week' || $view === 'day'): ?>
|
||||
<?php /* ===== 周 / 天:按小时铺的时间网格(全天日程单独挂顶上) ===== */ ?>
|
||||
<div class="card cal-tg-card">
|
||||
<div class="tg-head">
|
||||
<div class="tg-gutter"></div>
|
||||
<?php foreach (($cols ?? []) as $c): ?>
|
||||
<div class="tg-dayname<?= $c['is_today'] ? ' is-today' : '' ?>">
|
||||
<span class="tg-wd"><?= e(['日','一','二','三','四','五','六'][$c['w']]) ?></span>
|
||||
<span class="tg-dd"><?= (int)$c['day'] ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php $tgAll = $tgrid['all_day'] ?? []; ?>
|
||||
<?php if (array_filter($tgAll)): ?>
|
||||
<div class="tg-allday">
|
||||
<div class="tg-gutter"><span class="tg-tag">全天</span></div>
|
||||
<?php foreach (($cols ?? []) as $i => $c): ?>
|
||||
<div class="tg-adcell">
|
||||
<?php foreach (($tgAll[$i] ?? []) as $ev): ?>
|
||||
<a class="cal-ev is-allday" data-cal="<?= (int)$ev['cal'] ?>"
|
||||
style="background: <?= e((string)$ev['color']) ?>;"
|
||||
href="<?= e((string)$ev['href']) ?>"
|
||||
title="<?= e((string)$ev['title']) ?>"><?= e((string)$ev['title']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="tg-body">
|
||||
<div class="tg-gutter tg-hours">
|
||||
<?php for ($h = 0; $h < 24; $h++): ?>
|
||||
<span class="tg-hr"><?= sprintf('%02d', $h) ?></span>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php foreach (($cols ?? []) as $i => $c): ?>
|
||||
<div class="tg-col<?= $c['is_today'] ? ' is-today' : '' ?>">
|
||||
<?php for ($h = 0; $h < 24; $h++): ?>
|
||||
<span class="tg-line"></span>
|
||||
<?php endfor; ?>
|
||||
<?php foreach (($tgrid['timed'][$i] ?? []) as $ev): ?>
|
||||
<a class="tg-block" data-cal="<?= (int)$ev['cal'] ?>"
|
||||
style="background: <?= e((string)$ev['color']) ?>; top: <?= (float)$ev['top'] ?>%; height: <?= (float)$ev['height'] ?>%;"
|
||||
href="<?= e((string)$ev['href']) ?>"
|
||||
title="<?= e((string)$ev['title']) ?>">
|
||||
<b><?= e((string)$ev['time']) ?></b> <?= e((string)$ev['title']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card cal-grid-card">
|
||||
<div class="cal-head">
|
||||
<?php foreach (['日', '一', '二', '三', '四', '五', '六'] as $wd): ?>
|
||||
@@ -150,6 +220,7 @@ $writable = array_values(array_filter($cals, static fn($c) => \App\Calendar::can
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -180,18 +251,6 @@ $writable = array_values(array_filter($cals, static fn($c) => \App\Calendar::can
|
||||
});
|
||||
}
|
||||
if (cancel && card) { cancel.addEventListener('click', function () { card.hidden = true; }); }
|
||||
|
||||
/* 「全天」勾上就把两个时刻输入框藏掉 —— 没有具体时分就是全天(用户第 7 条) */
|
||||
var allday = document.getElementById('ce-allday');
|
||||
var ts = document.getElementById('ce-time-s');
|
||||
var te = document.getElementById('ce-time-e');
|
||||
if (allday && ts && te) {
|
||||
function syncTime() {
|
||||
var hide = allday.checked;
|
||||
ts.hidden = hide; te.hidden = hide;
|
||||
}
|
||||
allday.addEventListener('change', syncTime);
|
||||
syncTime();
|
||||
}
|
||||
/* 「全天」勾上藏掉时刻那一段的逻辑在 _event_fields.php 里(两个页面共用一份) */
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* 新建 / 编辑纪念日(v24 用户第 13 条)
|
||||
*
|
||||
* 用户原话:「可以根据阴历和阳历来设置纪念日 …… 纪念日,需要按照阴历和阳历进行重复。」
|
||||
* 所以重复方式这一栏是核心:阳历 / 农历 / 只此一次。
|
||||
* 选农历时,下面的月/日填的是**农历**的月日(不是什么公历日期换算)。
|
||||
*
|
||||
* @param array|null $editing 编辑时的既有记录
|
||||
* @param array $people 名册
|
||||
*/
|
||||
$e0 = $editing ?: [];
|
||||
$v = static fn(string $k, $def = '') => $e0[$k] ?? $def;
|
||||
$repeat = (string)($e0['repeat_type'] ?? 'solar');
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1><?= $editing ? '编辑纪念日' : '新建纪念日' ?></h1>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e(url('days')) ?>">返回</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="card card-pad" method="post" action="<?= e(url('days/save')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<?php if ($editing): ?><input type="hidden" name="id" value="<?= (int)$e0['id'] ?>"><?php endif; ?>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field full">
|
||||
<label for="an-title">叫什么 *</label>
|
||||
<input class="input" id="an-title" name="title" required
|
||||
value="<?= e((string)$v('title')) ?>" placeholder="如:结婚纪念日">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="an-repeat">按什么重复 *</label>
|
||||
<select class="select" id="an-repeat" name="repeat_type">
|
||||
<?php foreach (\App\Anniversary::REPEAT as $rk => $rv): ?>
|
||||
<option value="<?= e($rk) ?>" <?= $repeat === $rk ? 'selected' : '' ?>><?= e($rv) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<span class="tip">选「农历」时,下面填的是**农历**的月日(不是公历)。</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="an-kind">分类</label>
|
||||
<input class="input" id="an-kind" name="kind" list="anKinds"
|
||||
value="<?= e((string)$v('kind')) ?>" placeholder="如:结婚纪念日">
|
||||
<datalist id="anKinds">
|
||||
<?php foreach (\App\Anniversary::PRESET_KINDS as $k): ?>
|
||||
<option value="<?= e($k) ?>"></option>
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="an-month">月 *</label>
|
||||
<input class="input" id="an-month" name="month" type="number" min="1" max="12" required
|
||||
value="<?= (int)$v('month', 1) ?>">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="an-day">日 *</label>
|
||||
<input class="input" id="an-day" name="day" type="number" min="1" max="31" required
|
||||
value="<?= (int)$v('day', 1) ?>">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="an-person">与谁相关</label>
|
||||
<select class="select" id="an-person" name="person_id">
|
||||
<option value="0">不指定</option>
|
||||
<?php foreach ($people as $p): ?>
|
||||
<option value="<?= (int)$p['id'] ?>" <?= (int)$v('person_id', 0) === (int)$p['id'] ? 'selected' : '' ?>>
|
||||
<?= e(person_name($p)) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="an-leap">农历闰月</label>
|
||||
<label class="chk-inline">
|
||||
<input type="checkbox" id="an-leap" name="is_leap" value="1" <?= !empty($e0['is_leap']) ? 'checked' : '' ?>>
|
||||
<span>这个农历月是闰月</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="an-note">备注</label>
|
||||
<textarea class="input" id="an-note" name="note" rows="2"><?= e((string)$v('note')) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit"><?= $editing ? '保存' : '添加' ?></button>
|
||||
<a class="btn btn-ghost" href="<?= e(url('days')) ?>">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* 纪念日(v24 用户第 13 条)
|
||||
*
|
||||
* 用户原话:「增加纪念日模块,可以根据阴历和阳历来设置纪念日,在纪念日模块中,
|
||||
* 可以默认看到已经保存的名册中的人的各种生日,生日本身就是纪念日的一种。」
|
||||
*
|
||||
* 所以这一页两块:
|
||||
* ① 临近的(今天起 60 天内)—— 一眼看到"该过什么日子了";
|
||||
* ② 全部纪念日 —— 名册里每个人的生日(实时派生)+ 你自己加的,混在一起按日期排。
|
||||
*
|
||||
* @param array $rows 全部纪念日(含派生生日:source='birthday_solar'/'birthday_lunar')
|
||||
* @param array $upcoming 接下来 60 天的
|
||||
* @param array $people 名册(新建表单里选"与谁相关")
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$kindBadge = static function (array $r): string {
|
||||
$k = (string)($r['kind'] ?? '');
|
||||
return $k === '' ? '' : '<span class="node-badge">' . e($k) . '</span>';
|
||||
};
|
||||
$srcWord = static function (array $r): string {
|
||||
$src = (string)($r['source'] ?? 'custom');
|
||||
if ($src === 'birthday_solar') {
|
||||
return '名册生日(阳历)';
|
||||
}
|
||||
if ($src === 'birthday_lunar') {
|
||||
return '名册生日(农历)';
|
||||
}
|
||||
return \App\Anniversary::repeatWord((string)($r['repeat_type'] ?? 'solar'));
|
||||
};
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>纪念日</h1>
|
||||
<span class="sub">生日本身就是一种纪念日 · 阳历与农历都能记</span>
|
||||
<div class="actions">
|
||||
<?php if ($canWrite): ?>
|
||||
<a class="btn" href="<?= e(url('days/new')) ?>">新建纪念日</a>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar')) ?>">看日历</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">接下来的日子</h2>
|
||||
<?php if (!$upcoming): ?>
|
||||
<p class="empty-hint">未来 60 天里没有纪念日。</p>
|
||||
<?php else: ?>
|
||||
<ul class="ann-up">
|
||||
<?php foreach ($upcoming as $u): ?>
|
||||
<li class="ann-up-item">
|
||||
<span class="ann-days"><?= (int)$u['days'] === 0 ? '今天' : ((int)$u['days'] . ' 天后') ?></span>
|
||||
<span class="ann-date"><?= e(substr((string)$u['date'], 5)) ?></span>
|
||||
<span class="ann-title"><?= e((string)$u['title']) ?></span>
|
||||
<?php if ($u['age'] !== null): ?><span class="small muted"><?= (int)$u['age'] ?> 岁</span><?php endif; ?>
|
||||
<?= $kindBadge($u) ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">全部纪念日(<?= count($rows) ?>)</h2>
|
||||
<?php if (!$rows): ?>
|
||||
<p class="empty-hint">还没有纪念日。先到名册里把家人的生日填上,这里就会自动出现。</p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>日期</th><th>纪念日</th><th>分类</th><th>重复</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $r): ?>
|
||||
<tr>
|
||||
<td class="nowrap"><?= (int)$r['month'] ?> 月 <?= (int)$r['day'] ?> 日</td>
|
||||
<td>
|
||||
<?= e((string)$r['title']) ?>
|
||||
<?php if (!empty($r['is_leap'])): ?><span class="small muted">(闰月)</span><?php endif; ?>
|
||||
</td>
|
||||
<td><?= $kindBadge($r) ?></td>
|
||||
<td class="small muted"><?= e($srcWord($r)) ?></td>
|
||||
<td class="nowrap">
|
||||
<?php if (($r['source'] ?? 'custom') === 'custom' && $canWrite): ?>
|
||||
<a class="link-quiet" href="<?= e(url('days/edit', ['id' => (int)$r['id']])) ?>">改</a>
|
||||
<form method="post" action="<?= e(url('days/destroy')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这条纪念日?')">删</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="small muted">来自名册</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<p class="tip" style="margin-top:10px">
|
||||
名册里那部分(生日)是**实时读出来的** —— 改生日,这里跟着变,不用重复维护。
|
||||
所有纪念日都会作为**全天日程**出现在日历里。
|
||||
</p>
|
||||
</div>
|
||||
+4
-3
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
$kinCount = count($kin);
|
||||
// 生日事件上用简称,卡片才不会一行塞不下;长名字在详情页里出
|
||||
$shortKind = ['id' => '身份证生日', 'solar' => '阳历生日', 'lunar' => '农历生日'];
|
||||
$shortKind = \App\Birthday::KINDS;
|
||||
$today = date('Y-m-d');
|
||||
?>
|
||||
<section class="hero">
|
||||
@@ -159,14 +159,15 @@ $today = date('Y-m-d');
|
||||
<?php /* ==================== 往年今日 ==================== */ ?>
|
||||
<section class="section">
|
||||
<div class="page-head" style="margin-bottom:12px">
|
||||
<?php /* v24 第 19 条:只认「往年的今天」,不再退回同月 —— 见 home_memories() 的说明。 */ ?>
|
||||
<h1 style="font-size:15px">
|
||||
<?= !empty($memories['exact']) ? '往年今日' : '往年这个月' ?>
|
||||
往年的今天
|
||||
<span class="sub"><?= e(date('n 月 j 日')) ?></span>
|
||||
</h1>
|
||||
</div>
|
||||
<?php if (empty($memories['rows'])): ?>
|
||||
<div class="card card-pad empty">
|
||||
<b><?= !empty($memories['exact']) ? '往年的这一天还没有记录' : '往年的这个月还没有记录' ?></b>
|
||||
<b>往年的这一天还没有记录</b>
|
||||
日子还长,等有了第一次,明年这时候就会自己冒出来。
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
+79
-14
@@ -20,12 +20,18 @@ $people = $people ?? [];
|
||||
// familyMap 的 $up/$down 只影响**代数标号**(超出就压到边界那一档),
|
||||
// 遍历本身是走遍整个连通图的 —— 所以这两个值不放宽的话,
|
||||
// 曾祖辈与祖辈会被压成同一代,排序就乱了。给足余量。
|
||||
$map = \App\Kinship::familyMap((int)\App\Auth::familyId(), (int)$meId, $people, 9, 9);
|
||||
// ⚠️ v24 第 12 条:中心是 $focusId(默认=我)。点姻亲入口进来时,中心换成那个人,
|
||||
// 这样 TA 那一侧的姻亲(岳父母等)才画得出来。
|
||||
$focusId = (int)($focusId ?? $meId);
|
||||
if (!isset($people[$focusId]) || !is_array($people[$focusId])) {
|
||||
$focusId = (int)$meId;
|
||||
}
|
||||
$map = \App\Kinship::familyMap((int)\App\Auth::familyId(), $focusId, $people, 9, 9);
|
||||
$gen = $map['gen'];
|
||||
$units = $map['units'];
|
||||
$unitOf = $map['unit_of'];
|
||||
|
||||
if (!isset($people[$meId]) || !is_array($people[$meId])) {
|
||||
if (!isset($people[$focusId]) || !is_array($people[$focusId])) {
|
||||
$canChart = false;
|
||||
} else {
|
||||
$onTree = array_fill_keys(array_map('intval', array_keys($gen)), true);
|
||||
@@ -37,7 +43,7 @@ if (!isset($people[$meId]) || !is_array($people[$meId])) {
|
||||
// 把树根换成我的最高祖先,整棵树就变成「这位祖先的全部后代」,
|
||||
// 上一代、这一代、下一代的血亲与旁支才能全部落进来。
|
||||
// 代价是「我」不再是几何中心 —— 用「回到我」按钮 + 加载后自动居中补回来。
|
||||
$rootId = (int)$meId;
|
||||
$rootId = (int)$focusId;
|
||||
for ($guard = 0; $guard < 8; $guard++) {
|
||||
$up = \App\Kinship::parentIds($graph, $rootId);
|
||||
$next = 0;
|
||||
@@ -108,7 +114,9 @@ if (!isset($people[$meId]) || !is_array($people[$meId])) {
|
||||
'name' => \App\Nickname::display((int)$meId, $p) ?: (string)($p['name'] ?? ''),
|
||||
'label' => \App\Kinship::label($graph, (int)$meId, $pid, $people),
|
||||
'avatar' => avatar_url($p['avatar'] ?? null),
|
||||
'me' => $pid === (int)$meId,
|
||||
// 「我」在图上高亮;中心那个人另有标记($focusId)
|
||||
'me' => $pid === (int)$focusId,
|
||||
'is_me_viewer' => $pid === (int)$meId,
|
||||
'ph' => (int)($p['is_placeholder'] ?? 0) === 1,
|
||||
],
|
||||
'rels' => [
|
||||
@@ -119,6 +127,16 @@ if (!isset($people[$meId]) || !is_array($people[$meId])) {
|
||||
];
|
||||
}
|
||||
|
||||
// v24 第 12 条:本图上「有姻亲但没画出来」的人 —— 卡片上给个入口
|
||||
$branchOuts = \App\Kinship::branchOuts($graph, $people, $onTree);
|
||||
foreach ($datum as &$dn) {
|
||||
$did = (int)$dn['id'];
|
||||
if (isset($branchOuts[$did])) {
|
||||
$dn['data']['branch'] = $branchOuts[$did]['name'];
|
||||
}
|
||||
}
|
||||
unset($dn);
|
||||
|
||||
$canChart = count($datum) > 1;
|
||||
}
|
||||
|
||||
@@ -130,8 +148,18 @@ $datumJson = json_encode(
|
||||
?>
|
||||
|
||||
<div class="page-head">
|
||||
<h1>我的家谱</h1>
|
||||
<span class="sub">以我为中心 · 上下一共五代 · 夫妻并排,子女从两人之间的连线连出</span>
|
||||
<h1><?= $focusId === (int)$meId ? '我的家谱' : e((string)($people[$focusId]['name'] ?? '') . ' 的家谱') ?></h1>
|
||||
<span class="sub">
|
||||
<?= $focusId === (int)$meId
|
||||
? '以我为中心 · 夫妻并排,子女从两人之间的连线连出'
|
||||
: '以 TA 为中心(这样 TA 那一侧的姻亲才露得出来)' ?>
|
||||
</span>
|
||||
<div class="actions">
|
||||
<?php if ($focusId !== (int)$meId): ?>
|
||||
<?php /* v24 第 12 条:从姻亲入口进来的,给一个返回,回到「我的家谱」 */ ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('kin')) ?>">← 返回我的家谱</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$canChart): ?>
|
||||
@@ -144,7 +172,8 @@ $datumJson = json_encode(
|
||||
|
||||
<div class="kin-tools">
|
||||
<button type="button" class="btn btn-ghost btn-sm" id="kinFit">看全部</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm" id="kinMe">回到我</button>
|
||||
<?php /* 中心是别人时这颗按钮是"跳回我的家谱"(不是在本图里找我的坐标) */ ?>
|
||||
<button type="button" class="btn btn-ghost btn-sm" id="kinMe"><?= $focusId === (int)$meId ? '回到我' : '回到我的家谱' ?></button>
|
||||
</div>
|
||||
|
||||
<!-- ⚠️ `f3` 这个类名是**库的 CSS 作用域**,不是我们起的名字。
|
||||
@@ -152,7 +181,8 @@ $datumJson = json_encode(
|
||||
少了它,库里 `.f3 svg.main_svg{width:100%;height:100%}` 这条撑满规则不生效,
|
||||
SVG 会退化成浏览器默认的 300×150(连线全被裁掉,而 HTML 卡片层不受影响 →
|
||||
表现为「卡在、线没了」)。 -->
|
||||
<div class="kin-chart f3" id="kinChart" data-datum="<?= e($datumJson) ?>" data-me="<?= e((string)$meId) ?>"></div>
|
||||
<div class="kin-chart f3" id="kinChart" data-datum="<?= e($datumJson) ?>"
|
||||
data-focus="<?= e((string)$focusId) ?>" data-me-viewer="<?= e((string)$meId) ?>"></div>
|
||||
|
||||
<p class="tree-legend">
|
||||
每代一行;<b>一对夫妻之间有一条短横线</b>,
|
||||
@@ -197,22 +227,29 @@ $datumJson = json_encode(
|
||||
// 会跳过深度截断,等于**不限代数**,有多少画多少。
|
||||
// 之前写死了具体代数,真实家庭一超过就被砍掉。
|
||||
// 用户要求「全部取出来」,所以这里也不设。
|
||||
.setCardXSpacing(180)
|
||||
.setCardXSpacing(190)
|
||||
.setCardYSpacing(180);
|
||||
|
||||
chart.setCardHtml()
|
||||
.setStyle('rect')
|
||||
.setCardDim({ w: 140, h: 120, mini_tree: false })
|
||||
.setCardDim({ w: 148, h: 138, mini_tree: false })
|
||||
.setCardInnerHtmlCreator(function (d) {
|
||||
var p = (d.data && d.data.data) || {};
|
||||
var id = (d.data && d.data.id) || '';
|
||||
var cls = 'kin-card' + (p.me ? ' is-me' : '') + (p.ph ? ' is-ph' : '');
|
||||
// v24 第 12 条:头像改**方形**(原来是圆角矩形 + cover,把脸裁掉了)。
|
||||
// 卡片本身是 <a> 进成员页;有姻亲没画出来的,再叠一个独立的小按钮
|
||||
// (用 <span role=button> 而不是 <a>:<a> 不能套 <a>)。
|
||||
return '<a class="' + cls + '" data-kin-id="' + esc(id) + '" href="/index.php?r=people/show&id='
|
||||
+ encodeURIComponent(id) + '">'
|
||||
+ '<span class="kin-ph"><img src="' + esc(p.avatar) + '" alt=""></span>'
|
||||
+ '<span class="kin-nm">' + esc(p.name) + '</span>'
|
||||
+ '<span class="kin-lb">' + esc(p.label) + '</span>'
|
||||
+ (p.ph ? '<span class="kin-mark">待确认</span>' : '')
|
||||
+ (p.branch
|
||||
? '<span class="kin-branch" role="button" tabindex="0" title="看看 '
|
||||
+ esc(p.branch) + ' 那一侧的家谱" data-branch-id="' + esc(id) + '">⇄ 姻亲</span>'
|
||||
: '')
|
||||
+ '</a>';
|
||||
});
|
||||
|
||||
@@ -256,12 +293,15 @@ $datumJson = json_encode(
|
||||
}
|
||||
window.kinCenterOn = centerOn;
|
||||
|
||||
var meId = root.getAttribute('data-me') || '';
|
||||
var focusId = root.getAttribute('data-focus') || '';
|
||||
var meId = root.getAttribute('data-me-viewer') || focusId;
|
||||
// ⚠️ 中心是别人时,「回到我」要跳到 focus=我 的图,而不是在图里找我的坐标
|
||||
var centerTarget = root.getAttribute('data-me-viewer') || focusId;
|
||||
// fit 动画完成后把镜头挪到「我」(树根是最高祖先,我不再是几何中心)
|
||||
if (meId) {
|
||||
setTimeout(function () { centerOn(meId); }, 900);
|
||||
setTimeout(function () { centerOn(focusId); }, 900);
|
||||
// 万一 zoom 对象还没初始化好,再补一次
|
||||
setTimeout(function () { centerOn(meId); }, 1400);
|
||||
setTimeout(function () { centerOn(focusId); }, 1400);
|
||||
}
|
||||
|
||||
var btnFit = document.getElementById('kinFit');
|
||||
@@ -271,8 +311,33 @@ $datumJson = json_encode(
|
||||
chart.updateTree({ tree_position: 'fit' });
|
||||
});
|
||||
}
|
||||
/* v24 第 12 条:卡片右下角的「⇄ 姻亲」按钮 —— 以那个人为中心重画一张家谱。
|
||||
⚠️ 这个 span 在卡片的 <a> 里,必须 preventDefault + stopPropagation,
|
||||
否则会先触发外面那个链接跳去成员页。 */
|
||||
Array.prototype.forEach.call(root.querySelectorAll('.kin-branch'), function (btn) {
|
||||
function go(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var bid = btn.getAttribute('data-branch-id') || '';
|
||||
if (bid) { window.location.href = '/index.php?r=kin&focus=' + encodeURIComponent(bid); }
|
||||
}
|
||||
btn.addEventListener('click', go);
|
||||
btn.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') { go(e); }
|
||||
});
|
||||
// 键盘/触摸都别让它冒泡到外层链接
|
||||
btn.addEventListener('mousedown', function (e) { e.stopPropagation(); });
|
||||
});
|
||||
|
||||
if (btnMe && meId) {
|
||||
btnMe.addEventListener('click', function () { centerOn(meId); });
|
||||
btnMe.addEventListener('click', function () {
|
||||
// 图是别人的家谱时,先跳回「我的家谱」再居中
|
||||
if (focusId !== meId) {
|
||||
window.location.href = '/index.php?r=kin';
|
||||
return;
|
||||
}
|
||||
centerOn(centerTarget);
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -99,6 +99,20 @@ $isLoggedIn = \App\Auth::check();
|
||||
|
||||
</main>
|
||||
|
||||
<?php /* 全局附件预览层(v24 第 3 条):图片 / 视频 / PDF 都在本页这一层里看,
|
||||
右上角恒有下载按钮。内容由 assets/js/preview.js 按 data-prev 现填。 */ ?>
|
||||
<div class="pv" id="pv" role="dialog" aria-modal="true" aria-hidden="true" aria-label="附件预览" hidden>
|
||||
<div class="pv-head">
|
||||
<span class="pv-name"></span>
|
||||
<span class="pv-meta"></span>
|
||||
<a class="pv-dl" href="#">下载</a>
|
||||
<button type="button" class="pv-close" aria-label="关闭">✕</button>
|
||||
</div>
|
||||
<div class="pv-stage"></div>
|
||||
</div>
|
||||
|
||||
<script src="<?= e(asset('/assets/js/app.js')) ?>"></script>
|
||||
<script src="<?= e(asset('/assets/js/person_picker.js')) ?>"></script>
|
||||
<script src="<?= e(asset('/assets/js/preview.js')) ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
/**
|
||||
* 财务 / 账本(v24 用户第 15 条)
|
||||
*
|
||||
* 用户原话:「先创建一个家庭,或者说,创建一个账本,这个账本,可以共享给某个人,
|
||||
* 共享后,这个人就可以往这个账本加入账目,这样,双方都可以看到所有的账目,
|
||||
* 最后计算出总数,这就是一个家庭的总账目。」
|
||||
*
|
||||
* 页面结构就是照着这句话来的:
|
||||
* 顶上一排账本(切换 / 新建)→ 汇总卡(资产 / 负债 / 净资产)→ 账目表 → 共享设置。
|
||||
*
|
||||
* ⚠️ 金额全程以**分**参与计算(Ledger 里做的),这里只负责显示成元。
|
||||
* 任何地方都不要拿元去加总。
|
||||
*
|
||||
* @param array $books 我能看到的账本(带 my_perm / entry_count)
|
||||
* @param array $cur 当前账本
|
||||
* @param array $entries 当前账本的账目
|
||||
* @param array $summary ['asset','debt','net','by_kind','count'](单位:分)
|
||||
* @param array $shares 当前账本的共享列表
|
||||
* @param array $people 名册
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite() && \App\Ledger::canWrite($cur);
|
||||
$curId = (int)$cur['id'];
|
||||
$money = static fn($c): string => \App\Ledger::money($c);
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>财务</h1>
|
||||
<span class="sub">家庭的账 · 共享后双方都能记、都能看</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e(url('ledger', ['book' => $curId])) ?>">刷新</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (count($books) > 1): ?>
|
||||
<div class="seg">
|
||||
<?php foreach ($books as $b): ?>
|
||||
<a class="seg-btn<?= (int)$b['id'] === $curId ? ' is-on' : '' ?>"
|
||||
href="<?= e(url('ledger', ['book' => (int)$b['id']])) ?>">
|
||||
<?= e((string)$b['name']) ?><?= ($b['my_perm'] ?? '') === 'read' ? '(只读)' : '' ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card card-pad led-sum">
|
||||
<div class="led-sum-head">
|
||||
<h2 class="cinema-h2" style="margin:0"><?= e((string)$cur['name']) ?></h2>
|
||||
<span class="small muted">
|
||||
<?= (int)($cur['entry_count'] ?? count($entries)) ?> 笔账
|
||||
<?php if (($cur['my_perm'] ?? '') === 'read'): ?>
|
||||
· <b>只读</b>(不能记账)
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="led-total">
|
||||
<div class="led-total-item">
|
||||
<span class="led-total-lb">总资产</span>
|
||||
<span class="led-total-num led-asset"><?= e($money($summary['asset'])) ?></span>
|
||||
</div>
|
||||
<div class="led-total-item">
|
||||
<span class="led-total-lb">总负债</span>
|
||||
<span class="led-total-num led-debt"><?= e($money($summary['debt'])) ?></span>
|
||||
</div>
|
||||
<div class="led-total-item led-net">
|
||||
<span class="led-total-lb">净资产</span>
|
||||
<span class="led-total-num"><?= e($money($summary['net'])) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($summary['by_kind']): ?>
|
||||
<div class="led-kinds">
|
||||
<?php foreach ($summary['by_kind'] as $k => $c): ?>
|
||||
<span class="led-kind-chip<?= \App\Ledger::isDebt((string)$k) ? ' is-debt' : '' ?>">
|
||||
<?= \App\Ledger::kindIcon((string)$k) ?> <?= e(\App\Ledger::kindWord((string)$k)) ?>
|
||||
<b><?= e($money($c)) ?></b>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canWrite): ?>
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">记一笔</h2>
|
||||
<form method="post" action="<?= e(url('ledger/entry_add')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label for="le-kind">类别 *</label>
|
||||
<select class="select" id="le-kind" name="kind">
|
||||
<?php foreach (\App\Ledger::KINDS as $k => $v): ?>
|
||||
<option value="<?= e($k) ?>"><?= e($v[2] . ' ' . $v[0]) ?><?= $v[1] === 'debt' ? '(负债)' : '' ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="le-title">名称 *</label>
|
||||
<input class="input" id="le-title" name="title" required placeholder="如:工商银行定期">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="le-amount">金额(元)*</label>
|
||||
<input class="input" id="le-amount" name="amount" required inputmode="decimal"
|
||||
placeholder="如:120000.00">
|
||||
<span class="tip">负债(贷款/信用卡)也填正数,系统按类别算成负债。</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="le-person">挂在谁名下</label>
|
||||
<select class="select" id="le-person" name="person_id">
|
||||
<option value="0">不指定</option>
|
||||
<?php foreach ($people as $p): ?>
|
||||
<option value="<?= (int)$p['id'] ?>"><?= e(person_name($p)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label for="le-note">备注</label>
|
||||
<input class="input" id="le-note" name="note" placeholder="可选">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row"><button class="btn" type="submit">添加账目</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">账目(<?= count($entries) ?>)</h2>
|
||||
<?php if (!$entries): ?>
|
||||
<p class="empty-hint">这本账还没有账目。上面「记一笔」开始记吧。</p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>类别</th><th>名称</th><th class="num">金额</th><th>挂在谁名下</th><th>备注</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($entries as $en): ?>
|
||||
<?php $ek = (string)$en['kind']; $debt = \App\Ledger::isDebt($ek); ?>
|
||||
<tr>
|
||||
<td class="nowrap">
|
||||
<span class="led-kind-chip<?= $debt ? ' is-debt' : '' ?>">
|
||||
<?= \App\Ledger::kindIcon($ek) ?> <?= e(\App\Ledger::kindWord($ek)) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= e((string)$en['title']) ?></td>
|
||||
<td class="num<?= $debt ? ' led-debt' : ' led-asset' ?>">
|
||||
<?= $debt ? '-' : '' ?><?= e($money((int)$en['amount_cents'])) ?>
|
||||
</td>
|
||||
<td class="nowrap">
|
||||
<?php if (!empty($en['person_name'])): ?>
|
||||
<?= person_chip([
|
||||
'id' => (int)$en['person_id'], 'name' => (string)$en['person_name'],
|
||||
'my_nickname' => (string)($en['person_nick'] ?? ''),
|
||||
'avatar' => $en['avatar'] ?? null, 'avatar_color' => $en['avatar_color'] ?? null,
|
||||
]) ?>
|
||||
<?php else: ?><span class="small muted">—</span><?php endif; ?>
|
||||
</td>
|
||||
<td class="small muted"><?= e((string)($en['note'] ?? '')) ?></td>
|
||||
<td class="nowrap">
|
||||
<?php if ($canWrite): ?>
|
||||
<details class="led-edit">
|
||||
<summary class="link-quiet">改</summary>
|
||||
<form method="post" action="<?= e(url('ledger/entry_update')) ?>" class="node-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<input type="hidden" name="id" value="<?= (int)$en['id'] ?>">
|
||||
<div class="nf-grid">
|
||||
<div class="field">
|
||||
<label>类别</label>
|
||||
<select class="select" name="kind">
|
||||
<?php foreach (\App\Ledger::KINDS as $k => $v): ?>
|
||||
<option value="<?= e($k) ?>" <?= $ek === $k ? 'selected' : '' ?>>
|
||||
<?= e($v[0]) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>名称</label>
|
||||
<input class="input" name="title" value="<?= e((string)$en['title']) ?>" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>金额(元)</label>
|
||||
<input class="input" name="amount"
|
||||
value="<?= e(\App\Ledger::amountYuan((int)$en['amount_cents'])) ?>" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>备注</label>
|
||||
<input class="input" name="note" value="<?= e((string)($en['note'] ?? '')) ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row"><button class="btn btn-sm" type="submit">保存</button></div>
|
||||
</form>
|
||||
<form method="post" action="<?= e(url('ledger/entry_del')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<input type="hidden" name="id" value="<?= (int)$en['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这笔账目?')">删除</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="2">合计(资产 − 负债)</th>
|
||||
<th class="num led-net"><?= e($money($summary['net'])) ?></th>
|
||||
<th colspan="3"></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">共享与账本</h2>
|
||||
<div class="led-shares">
|
||||
<span class="res-label">已共享给</span>
|
||||
<?php if (!$shares): ?>
|
||||
<span class="small muted">还没有共享给任何人。</span>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($shares as $s): ?>
|
||||
<span class="led-share-chip">
|
||||
<?= person_chip([
|
||||
'id' => (int)$s['person_id'], 'name' => (string)$s['name'],
|
||||
'avatar' => $s['avatar'] ?? null, 'avatar_color' => $s['avatar_color'] ?? null,
|
||||
]) ?>
|
||||
<span class="node-badge"><?= ($s['perm'] ?? 'read') === 'write' ? '可写' : '只读' ?></span>
|
||||
<?php if ($canWrite): ?>
|
||||
<form method="post" action="<?= e(url('ledger/unshare')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<input type="hidden" name="person_id" value="<?= (int)$s['person_id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit">取消</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canWrite): ?>
|
||||
<div class="led-ops">
|
||||
<?php
|
||||
$ppName = 'person_id';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = [];
|
||||
$ppMode = 'single';
|
||||
$ppOpenBtn = '选择共享给谁';
|
||||
$ppTitle = '账本共享给谁';
|
||||
$ppEmptyText = '未选择';
|
||||
?>
|
||||
<form class="inline-form" method="post" action="<?= e(url('ledger/share')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<?php require __DIR__ . '/../_person_picker.php'; ?>
|
||||
<select class="select" name="perm">
|
||||
<?php foreach (\App\Ledger::PERM as $pk => $pv): ?>
|
||||
<option value="<?= e($pk) ?>"><?= e($pv) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="btn btn-sm" type="submit">共享</button>
|
||||
</form>
|
||||
|
||||
<form class="inline-form" method="post" action="<?= e(url('ledger/book_rename')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<input class="input" name="name" value="<?= e((string)$cur['name']) ?>" required>
|
||||
<button class="btn btn-ghost btn-sm" type="submit">账本改名</button>
|
||||
</form>
|
||||
|
||||
<form class="inline-form" method="post" action="<?= e(url('ledger/book_add')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input class="input" name="name" placeholder="新账本名,如:孩子的教育金" required>
|
||||
<button class="btn btn-ghost btn-sm" type="submit">新建账本</button>
|
||||
</form>
|
||||
|
||||
<form class="inline-form" method="post" action="<?= e(url('ledger/book_del')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这本账?里面的账目会一起删掉。')">删除这本账</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
+14
-163
@@ -105,44 +105,21 @@ $selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
|
||||
|
||||
<div class="field full">
|
||||
<label>同行的人</label>
|
||||
<?php /* v10:名册人多时 checkbox 网格会拉得很长 —— 改为「chips + 弹窗选择」。
|
||||
attendees[] 的 checkbox 仍保留(在弹窗列表里,选中即提交),无 JS 时
|
||||
借 noscript 样式把弹窗平铺成普通勾选列表,提交语义完全不变。
|
||||
v11:chips 与弹窗列表都带上头像($p 来自 Person::roster,含 p.avatar)。 */ ?>
|
||||
<?php /* v24 第 11 条:换成全局人员选择器(templates/_person_picker.php)。
|
||||
原来这套是观影页私有的 .att-*,它的「过滤」其实一直是坏的 ——
|
||||
.att-item 自己写了 display:flex,压过了浏览器的 [hidden]{display:none},
|
||||
JS 里 el.hidden = true 根本没生效,搜什么都还是一整列。 */ ?>
|
||||
<?php if ($people): ?>
|
||||
<div class="att-chips" id="attChips">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<?php if (in_array((int)$p['id'], $selAttendees, true)): ?>
|
||||
<span class="att-chip" data-id="<?= (int)$p['id'] ?>"><img src="<?= e(avatar_url($p['avatar'] ?? null)) ?>" alt=""><span class="att-name"><?= e($p['shown'] ?? $p['name']) ?></span><button type="button" class="att-x" data-id="<?= (int)$p['id'] ?>" title="移除">×</button></span>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost btn-sm att-open-btn" id="attOpen">+ 添加观影人</button>
|
||||
<div class="att-modal" id="attModal" hidden>
|
||||
<div class="att-modal-box">
|
||||
<div class="att-modal-head">
|
||||
<b>选择观影人</b>
|
||||
<button type="button" class="att-close" id="attClose" title="关闭">×</button>
|
||||
</div>
|
||||
<input class="input" id="attSearch" type="text"
|
||||
placeholder="输入名字或拼音首字母,如 zy 找「张三」" autocomplete="off">
|
||||
<div class="att-list" id="attList">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<label class="att-item"
|
||||
data-name="<?= e((string)$p['name']) ?>"
|
||||
data-shown="<?= e((string)($p['shown'] ?? '')) ?>">
|
||||
<input type="checkbox" name="attendees[]" value="<?= (int)$p['id'] ?>"
|
||||
<?= in_array((int)$p['id'], $selAttendees, true) ? 'checked' : '' ?>>
|
||||
<img class="att-av" src="<?= e(avatar_url($p['avatar'] ?? null)) ?>" alt="" loading="lazy">
|
||||
<span class="att-name"><?= e($p['shown'] ?? $p['name']) ?></span>
|
||||
<span class="att-initials" hidden></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<div class="att-empty" hidden>没有匹配的人。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<noscript><style>.att-open-btn { display: none; } .att-modal { position: static; display: block; background: none; padding: 0; } .att-modal-box { box-shadow: none; border: 1px solid var(--line); max-height: none; }</style></noscript>
|
||||
<?php
|
||||
$ppName = 'attendees[]';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = $selAttendees;
|
||||
$ppMode = 'multi';
|
||||
$ppOpenBtn = '+ 添加观影人';
|
||||
$ppTitle = '选择观影人';
|
||||
$ppTip = '';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
<?php else: ?>
|
||||
<span class="tip">名册里还没有人,先把家人加进名册再来记。</span>
|
||||
<?php endif; ?>
|
||||
@@ -192,131 +169,6 @@ $selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($people): ?>
|
||||
<!-- 观影人选择弹窗:chips 同步 + 拼音首字母联想(Intl pinyin 排序 + 23 锚字二分,零依赖) -->
|
||||
<script>
|
||||
(function () {
|
||||
var modal = document.getElementById('attModal');
|
||||
if (!modal) return;
|
||||
var openBtn = document.getElementById('attOpen');
|
||||
var closeBtn = document.getElementById('attClose');
|
||||
var search = document.getElementById('attSearch');
|
||||
var list = document.getElementById('attList');
|
||||
var chips = document.getElementById('attChips');
|
||||
var emptyTip = list.querySelector('.att-empty');
|
||||
|
||||
/* ---- 汉字 → 拼音首字母:锚字表 a..z(23 个,无 i/u/v 开头字),
|
||||
用拼音 collation 做二分定位;不支持的浏览器返回 null(只按名字搜) ---- */
|
||||
var initialsOf = (function () {
|
||||
var anchors = '阿八擦搭娥发嘎哈击喀垃妈拿哦啪期然撒塌挖昔压匝';
|
||||
var letters = 'abcdefghjklmnopqrstwxyz';
|
||||
var coll;
|
||||
try { coll = new Intl.Collator('zh-Hans-CN-u-co-pinyin'); } catch (e) { return null; }
|
||||
if (!coll || coll.compare('阿', '匝') >= 0) return null; // collation 没生效
|
||||
return function (s) {
|
||||
var out = '';
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
var ch = s[i];
|
||||
if (/[a-zA-Z]/.test(ch)) { out += ch.toUpperCase(); continue; }
|
||||
if (ch.charCodeAt(0) < 0x2E80) continue; // 非 CJK:不产字母
|
||||
var lo = 0, hi = anchors.length - 1, best = -1;
|
||||
while (lo <= hi) {
|
||||
var mid = (lo + hi) >> 1;
|
||||
if (coll.compare(ch, anchors[mid]) >= 0) { best = mid; lo = mid + 1; }
|
||||
else { hi = mid - 1; }
|
||||
}
|
||||
if (best >= 0) out += letters[best];
|
||||
}
|
||||
return out;
|
||||
};
|
||||
})();
|
||||
|
||||
/* 预计算每个人的首字母串,挂在 data-initials 上 */
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.att-item'), function (el) {
|
||||
var s = (el.getAttribute('data-name') || '') + (el.getAttribute('data-shown') || '');
|
||||
var ini = initialsOf ? initialsOf(s) : '';
|
||||
el.setAttribute('data-initials', ini.toLowerCase());
|
||||
var badge = el.querySelector('.att-initials');
|
||||
if (badge && ini) { badge.textContent = ini; badge.hidden = false; }
|
||||
});
|
||||
|
||||
function syncChips() {
|
||||
if (!chips) return;
|
||||
chips.innerHTML = '';
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.att-item input:checked'), function (box) {
|
||||
var item = box.closest('.att-item');
|
||||
// ⚠️ 必须点名取 .att-name:v11 起 .att-item 里第一个元素是头像 <img>,
|
||||
// 早先靠「取第一个 span 当名字」碰巧成立,现在第一个 span 是隐藏的
|
||||
// 首字母占位,选完人 chip 上就没名字了。
|
||||
var nameEl = item.querySelector('.att-name');
|
||||
var label = nameEl ? nameEl.textContent : box.value;
|
||||
var av = item.querySelector('.att-av');
|
||||
var id = box.value;
|
||||
var chip = document.createElement('span');
|
||||
chip.className = 'att-chip';
|
||||
chip.setAttribute('data-id', id);
|
||||
if (av) {
|
||||
var im = document.createElement('img');
|
||||
im.src = av.getAttribute('src'); // 服务端已 e() 过,这里原样搬
|
||||
im.alt = '';
|
||||
chip.appendChild(im);
|
||||
}
|
||||
var nm = document.createElement('span');
|
||||
nm.className = 'att-name';
|
||||
nm.textContent = label;
|
||||
chip.appendChild(nm);
|
||||
var x = document.createElement('button');
|
||||
x.type = 'button'; x.className = 'att-x'; x.textContent = '×';
|
||||
x.title = '移除'; x.setAttribute('data-id', id);
|
||||
chip.appendChild(x);
|
||||
chips.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
chips.addEventListener('click', function (e) {
|
||||
var x = e.target.closest ? e.target.closest('.att-x') : null;
|
||||
if (!x) return;
|
||||
var id = x.getAttribute('data-id');
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.att-item input'), function (box) {
|
||||
if (box.value === id) box.checked = false;
|
||||
});
|
||||
syncChips();
|
||||
});
|
||||
|
||||
function applyFilter() {
|
||||
var q = (search.value || '').trim().toLowerCase();
|
||||
var shown = 0;
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.att-item'), function (el) {
|
||||
var hit = !q ||
|
||||
(el.getAttribute('data-name') || '').toLowerCase().indexOf(q) >= 0 ||
|
||||
(el.getAttribute('data-shown') || '').toLowerCase().indexOf(q) >= 0 ||
|
||||
(el.getAttribute('data-initials') || '').indexOf(q) >= 0;
|
||||
el.hidden = !hit;
|
||||
if (hit) shown++;
|
||||
});
|
||||
emptyTip.hidden = shown > 0;
|
||||
}
|
||||
|
||||
function openModal() { modal.hidden = false; search.value = ''; applyFilter(); syncChips(); search.focus(); }
|
||||
function closeModal() { modal.hidden = true; }
|
||||
openBtn.addEventListener('click', openModal);
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', function (e) { if (e.target === modal) closeModal(); });
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && !modal.hidden) closeModal();
|
||||
});
|
||||
search.addEventListener('input', applyFilter);
|
||||
|
||||
/* 选一个 = 勾上 + 出 chips + 关弹窗(再添加就再点一次「+ 添加观影人」);
|
||||
在弹窗里取消勾选只同步 chips,不关弹窗 */
|
||||
list.addEventListener('change', function (e) {
|
||||
if (!e.target || e.target.tagName !== 'INPUT') return;
|
||||
syncChips();
|
||||
if (e.target.checked) closeModal();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- 豆瓣自动补充:下拉联想(320ms 防抖 + 中文输入法组字期不请求)+ 一键抓取 -->
|
||||
<script>
|
||||
(function () {
|
||||
@@ -454,4 +306,3 @@ $selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -35,15 +35,19 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="person_id">观影人</label>
|
||||
<select class="select" id="person_id" name="person_id">
|
||||
<option value="0">全部观影人</option>
|
||||
<?php foreach ($persons as $pp): ?>
|
||||
<option value="<?= (int)$pp['id'] ?>" <?= (int)$p['person_id'] === (int)$pp['id'] ? 'selected' : '' ?>>
|
||||
<?= e((string)$pp['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label>观影人</label>
|
||||
<?php /* 筛选用的单选:要留一个「不选」= 全部,所以开 $ppAllowEmpty */ ?>
|
||||
<?php
|
||||
$ppName = 'person_id';
|
||||
$ppPeople = $persons;
|
||||
$ppSelected = [(int)$p['person_id']];
|
||||
$ppMode = 'single';
|
||||
$ppAllowEmpty = true;
|
||||
$ppOpenBtn = '选择观影人';
|
||||
$ppTitle = '按观影人筛选';
|
||||
$ppEmptyText = '全部观影人';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>日期范围</label>
|
||||
|
||||
@@ -422,6 +422,8 @@ $commentBox = static function (int $reviewId, int $parentId, string $summary) us
|
||||
<?php if ($hasPoster || $hasTicket || $m['photos']): ?>
|
||||
<!-- 图片灯箱:点任意一张图在当前页弹出,可左右翻看 / Esc 关闭(MovieLog 同款) -->
|
||||
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-hidden="true" aria-label="图片查看器">
|
||||
<?php /* v24 第 3 条:预览层右上角恒有下载按钮(与全局附件预览一个规矩) */ ?>
|
||||
<a class="lb-btn lb-dl" id="lbDl" href="#" download>下载</a>
|
||||
<button type="button" class="lb-btn lb-close" id="lbClose" aria-label="关闭">✕</button>
|
||||
<button type="button" class="lb-btn lb-nav lb-prev" id="lbPrev" aria-label="上一张">‹</button>
|
||||
<button type="button" class="lb-btn lb-nav lb-next" id="lbNext" aria-label="下一张">›</button>
|
||||
@@ -449,12 +451,17 @@ $commentBox = static function (int $reviewId, int $parentId, string $summary) us
|
||||
var capEl = document.getElementById('lbCaption');
|
||||
var cntEl = document.getElementById('lbCounter');
|
||||
var closeEl = document.getElementById('lbClose');
|
||||
var dlEl = document.getElementById('lbDl');
|
||||
var prevEl = document.getElementById('lbPrev');
|
||||
var nextEl = document.getElementById('lbNext');
|
||||
if (!box || !items.length) return;
|
||||
var idx = 0, opener = null;
|
||||
function render() {
|
||||
imgEl.src = items[idx].url;
|
||||
// 下载地址:与预览同一个键,只是带上 dl=1(服务端会改 Content-Disposition)
|
||||
if (dlEl) {
|
||||
dlEl.setAttribute('href', items[idx].url + (items[idx].url.indexOf('?') < 0 ? '?' : '&') + 'dl=1');
|
||||
}
|
||||
capEl.textContent = items[idx].cap;
|
||||
cntEl.textContent = (idx + 1) + ' / ' + items.length;
|
||||
var multi = items.length > 1;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 三处与早期版本不同的地方:
|
||||
* · 「小名」没了 —— 称呼是**视角属性**(妈妈叫"明明"、姐姐叫"小弟"),
|
||||
* 在「成员详情」页里各自设置自己怎么叫,不写在这个人的档案里。
|
||||
* · 生日变成**三个**:身份证生日(由证件推导、只读)、阳历生日、农历生日。
|
||||
* · 生日有两种记法:阳历生日与农历生日(证件上那天照常存库,但按用户要求不再显示)。
|
||||
* 阳历与农历是同一个人的同一天,所以用「以哪个为准」明确这次填的是哪一个,
|
||||
* 服务端据此反推另一个 —— 不靠 JS,避免"JS 没跑到就静默存错"。
|
||||
* · 生日精确到**时分秒**:原生 datetime-local 只有时分、且中文环境会显示成
|
||||
@@ -28,7 +28,6 @@ if ($lunVal === '') {
|
||||
$lv = (string)($person['birthday_lunar'] ?? '');
|
||||
$lunVal = $lv;
|
||||
}
|
||||
$idBd = trim((string)($person['birthday_id_card'] ?? ''));
|
||||
// 补充联系方式(v6 起):新增时是空数组;编辑时是库里解密后的行;
|
||||
// 校验失败重画时是用户刚提交的那批(连同填错的也要原样带回)。
|
||||
$contacts = $contacts ?? [];
|
||||
@@ -165,8 +164,7 @@ $ctHints = [
|
||||
value="<?= e(old('id_card', $idCardValue ?? '')) ?>" placeholder="可留空">
|
||||
<span class="tip">
|
||||
加密存储,页面上一律只显示前 6 后 4 位。
|
||||
<b>填了它,最下面的「身份证生日」会自动读出来</b>(当前:<span id="idCardBirthday"><?= $idBd !== '' ? e($idBd) : '' ?></span>),
|
||||
这个日期系统永远不会改动。家庭场景里真正需要它的场合很少 —— 能不填就别填。
|
||||
家庭场景里真正需要它的场合很少 —— 能不填就别填。
|
||||
</span>
|
||||
<?php if (!empty($errors['id_card'])): ?><span class="err"><?= e($errors['id_card']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
@@ -230,16 +228,10 @@ $ctHints = [
|
||||
<div class="field full">
|
||||
<label>生日</label>
|
||||
<p class="small muted" style="margin:0 0 12px">
|
||||
一个人有三个生日:<b>身份证上的</b>(由证件推导,系统永远不会改它)、
|
||||
<b>真实阳历生日</b>、<b>农历生日</b>。填了身份证号会自动读出第一个;
|
||||
阳历与农历只要填一个,另一个自动算出来。三个不在同一天时,首页会分别提醒。
|
||||
一个人的生日分两种记法:<b>真实阳历生日</b>与<b>农历生日</b>。
|
||||
阳历与农历只要填一个,另一个自动算出来。两个不在同一天时,首页会分别提醒。
|
||||
</p>
|
||||
|
||||
<div class="field" style="margin-bottom:12px">
|
||||
<label for="birthday_id_card_display">身份证生日(只读)</label>
|
||||
<span class="bd-readonly" id="birthday_id_card_display"><?= $idBd !== '' ? e($idBd) : '填了身份证号后自动读出' ?></span>
|
||||
</div>
|
||||
|
||||
<div class="bd-source">
|
||||
<label for="bdSource">我这次填的是</label>
|
||||
<select class="select" id="bdSource" name="birthday_source" style="width:auto">
|
||||
|
||||
@@ -34,13 +34,14 @@ $theirParents = \App\Kinship::parents($graph, $pid);
|
||||
$theirSpouses = \App\Kinship::spouseIds($graph, $pid);
|
||||
$theirKids = \App\Kinship::childIds($graph, $pid);
|
||||
$theirSibs = \App\Kinship::siblingIds($graph, $pid);
|
||||
$shortKind = ['id' => '身份证生日', 'solar' => '阳历生日', 'lunar' => '农历生日'];
|
||||
$shortKind = \App\Birthday::KINDS;
|
||||
?>
|
||||
|
||||
<div class="page-head">
|
||||
<h1><?= e($pShown) ?></h1>
|
||||
<span class="sub">
|
||||
<?php if (!$isSelf): ?>
|
||||
<?php if (!$isSelf && (string)$labelToMe !== ''): ?>
|
||||
<?php /* v24:没关系就不显示标签(Kinship::label() 推不出关系时返回空串) */ ?>
|
||||
<span class="rel-label"><?= e($labelToMe) ?></span>
|
||||
<?php if ($isMyNick): ?>
|
||||
· 本名 <?= e((string)$p['name']) ?>
|
||||
|
||||
@@ -28,22 +28,9 @@ $canWrite = \App\Auth::canWrite();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="search" method="get" action="index.php">
|
||||
<input type="hidden" name="r" value="resources">
|
||||
<input class="input" type="text" name="keyword" value="<?= e($keyword) ?>" placeholder="搜标题或正文">
|
||||
<select class="select" name="owner">
|
||||
<option value="0">所有人</option>
|
||||
<?php foreach ($people as $p): ?>
|
||||
<option value="<?= (int)$p['id'] ?>"<?= (int)$ownerId === (int)$p['id'] ? ' selected' : '' ?>>
|
||||
<?= e(person_name($p)) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="btn" type="submit">搜索</button>
|
||||
<?php if ($keyword !== '' || $ownerId > 0): ?>
|
||||
<a class="search-clear" href="<?= e(url('resources')) ?>">清除</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<?php /* v24(用户 2026-10-02 第 18 条):新建资源页去掉「搜索资源」。
|
||||
资源本来就不多,列表默认折叠、一眼能扫完;搜索框反而把「新建」按钮挤下去了。
|
||||
真的找不着再靠浏览器的 Ctrl+F,不为一个短列表专门养一套搜索。 */ ?>
|
||||
|
||||
<?php if ($canWrite): ?>
|
||||
<div class="card card-pad" id="resAddCard" <?= $isEditing ? '' : 'hidden' ?>>
|
||||
@@ -56,15 +43,18 @@ $canWrite = \App\Auth::canWrite();
|
||||
<?php endif; ?>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label for="rs-owner">归属于谁 *</label>
|
||||
<select class="select" id="rs-owner" name="owner_id" required>
|
||||
<?php foreach ($people as $p): ?>
|
||||
<option value="<?= (int)$p['id'] ?>"
|
||||
<?= (int)($editing['owner_id'] ?? (int)\App\Auth::personId()) === (int)$p['id'] ? ' selected' : '' ?>>
|
||||
<?= e(person_name($p)) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label>归属于谁 *</label>
|
||||
<?php /* v24 第 11 条:与全局选人组件统一(原来是下拉,名册一长就难找) */ ?>
|
||||
<?php
|
||||
$ppName = 'owner_id';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = [(int)($editing['owner_id'] ?? (int)\App\Auth::personId())];
|
||||
$ppMode = 'single';
|
||||
$ppOpenBtn = '选择归属人';
|
||||
$ppTitle = '资源归属于谁';
|
||||
$ppEmptyText = '未选择';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="rs-title">标题 *</label>
|
||||
@@ -95,8 +85,7 @@ $canWrite = \App\Auth::canWrite();
|
||||
<h2 class="cinema-h2">资源列表</h2>
|
||||
<?php if (!$rows): ?>
|
||||
<p class="empty-hint">
|
||||
<?= ($keyword !== '' || $ownerId > 0) ? '没有匹配的资源,换个条件试试。'
|
||||
: '还没有资源。证件、照片、账号这些"挂在某人名下的东西"都可以记在这里。' ?>
|
||||
还没有资源。证件、照片、账号这些"挂在某人名下的东西"都可以记在这里。
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<div class="res-list">
|
||||
@@ -122,31 +111,14 @@ $canWrite = \App\Auth::canWrite();
|
||||
<p class="empty-hint">(没有正文)</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($as): ?>
|
||||
<div class="attach-row">
|
||||
<?php foreach ($as as $a): ?>
|
||||
<span class="attach-item">
|
||||
<?php if (\App\Attachment::previewable($a)): ?>
|
||||
<a href="<?= e(media_url((string)$a['key'])) ?>" target="_blank" rel="noopener">
|
||||
<?= (int)$a['is_image'] === 1 ? '🖼' : '📄' ?> <?= e((string)($a['name'] ?: '附件')) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?= (int)$a['is_image'] === 1 ? '🖼' : '📄' ?> <?= e((string)($a['name'] ?: '附件')) ?>
|
||||
<?php endif; ?>
|
||||
<span class="res-size"><?= e(\App\Attachment::sizeWord($a['size'] ?? 0)) ?></span>
|
||||
<a class="link-quiet" href="<?= e(media_url((string)$a['key'], ['dl' => 1])) ?>">下载</a>
|
||||
<?php if ($canWrite && \App\Resource::canWrite($r, (int)\App\Auth::personId())): ?>
|
||||
<form method="post" action="<?= e(url('resources/attach_del')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$a['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这个附件?')">删除</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$attRows = $as;
|
||||
$attCanEdit = $canWrite && \App\Resource::canWrite($r, (int)\App\Auth::personId());
|
||||
$attDelUrl = 'resources/attach_del';
|
||||
$attDelField = 'id';
|
||||
$attExtra = [];
|
||||
require __DIR__ . '/../_attachments.php';
|
||||
?>
|
||||
|
||||
<?php if ($sh): ?>
|
||||
<div class="res-shares">
|
||||
@@ -175,12 +147,17 @@ $canWrite = \App\Auth::canWrite();
|
||||
<form class="inline-form" method="post" action="<?= e(url('resources/share')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= $rid ?>">
|
||||
<select class="select" name="person_id">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<?php if ((int)$p['id'] === (int)$r['owner_id']) { continue; } ?>
|
||||
<option value="<?= (int)$p['id'] ?>"><?= e(person_name($p)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php
|
||||
$ppName = 'person_id';
|
||||
$ppPeople = array_values(array_filter($people,
|
||||
fn($p) => (int)$p['id'] !== (int)$r['owner_id']));
|
||||
$ppSelected = [];
|
||||
$ppMode = 'single';
|
||||
$ppOpenBtn = '选择要共享给谁';
|
||||
$ppTitle = '共享给谁';
|
||||
$ppEmptyText = '未选择';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
<select class="select" name="perm">
|
||||
<option value="read">只读</option>
|
||||
<option value="write">可读写</option>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* 任务模块的「设置」下拉(v24 用户第 10 条)
|
||||
*
|
||||
* 用户原话:「和观影一样,新建任务旁边,也是设置,对任务的设置。」
|
||||
* 所以这里与观影的 _settings_menu.php 用**同一套外观**:只留图标(三条横线)、
|
||||
* 排在最右侧(靠 CSS 的 margin-left:auto)、条目文字前都有图标。
|
||||
*
|
||||
* 用法(require 前 set 好):
|
||||
* $setCurrent 当前页 key('board'|'settings'|'done'),用来高亮
|
||||
*/
|
||||
$setCurrent = (string)($setCurrent ?? '');
|
||||
$setOn = static function (string $key) use ($setCurrent): string {
|
||||
return $setCurrent === $key ? ' class="on"' : '';
|
||||
};
|
||||
$setIcon = static function (string $name): string {
|
||||
$paths = [
|
||||
// 看板设置:分栏
|
||||
'board' => '<rect x="2" y="3" width="4" height="10" rx="1"/><rect x="7.5" y="3" width="4" height="6" rx="1"/>',
|
||||
// 已完成:对勾
|
||||
'done' => '<path d="M3 8.5l3 3 7-7"/>',
|
||||
];
|
||||
$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.6" stroke-linecap="round"'
|
||||
. ' stroke-linejoin="round">' . $d . '</svg>';
|
||||
};
|
||||
?>
|
||||
<details class="movie-set menu">
|
||||
<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">
|
||||
<a href="<?= e(url('tasks/settings')) ?>"<?= $setOn('settings') ?>><?= $setIcon('board') ?><span>分类与看板</span></a>
|
||||
<a href="<?= e(url('tasks/done')) ?>"<?= $setOn('done') ?>><?= $setIcon('done') ?><span>已完成</span></a>
|
||||
</div>
|
||||
</details>
|
||||
<?php
|
||||
unset($setCurrent, $setOn, $setIcon);
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* 已完成的任务(v24 用户第 10 条)
|
||||
*
|
||||
* 用户原话:「还可以在这里查看已完成的任务,已完成的任务,
|
||||
* 按照分类来进行展示和搜索就可以了。」
|
||||
*
|
||||
* 所以这一页是**只看已完成**的归档:按分类筛 + 按标题搜,不再分列、不拖拽。
|
||||
*
|
||||
* @param array $rows 已完成的任务
|
||||
* @param array $categories 分类清单(做筛选条)
|
||||
* @param int $cat 当前选中的分类(0=全部)
|
||||
* @param string $kw 搜索词
|
||||
*/
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>已完成</h1>
|
||||
<span class="sub">共 <?= count($rows) ?> 项</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e(url('tasks')) ?>">返回看板</a>
|
||||
<a class="btn btn-ghost" href="<?= e(url('tasks/settings')) ?>">任务设置</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="card card-pad" method="get" action="index.php">
|
||||
<input type="hidden" name="r" value="tasks/done">
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label for="td-cat">分类</label>
|
||||
<select class="select" id="td-cat" name="cat">
|
||||
<option value="0">全部分类</option>
|
||||
<?php foreach ($categories as $c): ?>
|
||||
<option value="<?= (int)$c['id'] ?>" <?= $cat === (int)$c['id'] ? 'selected' : '' ?>>
|
||||
<?= e((string)$c['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="td-kw">搜索标题</label>
|
||||
<input class="input" id="td-kw" name="kw" value="<?= e($kw) ?>" placeholder="输入关键字">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit">筛选</button>
|
||||
<a class="btn btn-ghost" href="<?= e(url('tasks/done')) ?>">清空</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card card-pad">
|
||||
<?php if (!$rows): ?>
|
||||
<p class="empty-hint">
|
||||
<?= ($cat > 0 || $kw !== '') ? '没有符合条件的已完成任务。' : '还没有已完成的任务。' ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<ul class="done-list">
|
||||
<?php foreach ($rows as $t): ?>
|
||||
<li class="done-item">
|
||||
<a class="done-title" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>">
|
||||
<?= e((string)$t['title']) ?>
|
||||
</a>
|
||||
<?php if (($t['category_name'] ?? '') !== ''): ?>
|
||||
<span class="node-badge"><?= e((string)$t['category_name']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (($t['done_at'] ?? '') !== ''): ?>
|
||||
<span class="small muted">完成于 <?= e(substr((string)$t['done_at'], 0, 10)) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($t['assignee_name'])): ?>
|
||||
<?= person_chip([
|
||||
'id' => (int)$t['assignee_id'], 'name' => (string)$t['assignee_name'],
|
||||
'my_nickname' => (string)($t['assignee_nick'] ?? ''),
|
||||
'avatar' => $t['avatar'] ?? null, 'avatar_color' => $t['avatar_color'] ?? null,
|
||||
]) ?>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -12,20 +12,35 @@
|
||||
* @param array $people 名册(选负责人 / 共享给别人)
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$cols = [['todo', '待办'], ['doing', '进行中'], ['done', '已完成']];
|
||||
// ⚠️ v24 第 10 条:看板上只有「待办 / 进行中」两列 —— 已完成的任务从看板消失,
|
||||
// 要看去「任务设置 → 已完成」按分类看/搜。留着第三列就不是"直接消失"了。
|
||||
$cols = [['todo', '待办'], ['doing', '进行中']];
|
||||
$boards = $boards ?? [];
|
||||
$categories = $categories ?? [];
|
||||
$curBoard = (int)($curBoard ?? 0);
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>任务</h1>
|
||||
<span class="sub">共 <?= (int)$total ?> 项</span>
|
||||
<span class="sub">共 <?= (int)$total ?> 项未完成</span>
|
||||
<div class="actions">
|
||||
<?php if ($canWrite): ?>
|
||||
<button class="btn" type="button" id="taskAddBtn">新建任务</button>
|
||||
<?php else: ?>
|
||||
<span class="link-quiet">只读账号</span>
|
||||
<?php endif; ?>
|
||||
<?php $setCurrent = 'board'; require __DIR__ . '/_settings_menu.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (count($boards) > 1): ?>
|
||||
<div class="seg kb-boards">
|
||||
<?php foreach ($boards as $b): ?>
|
||||
<a class="seg-btn<?= (int)$b['id'] === $curBoard ? ' is-on' : '' ?>"
|
||||
href="<?= e(url('tasks', ['board' => (int)$b['id']])) ?>"><?= e((string)$b['name']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($canWrite): ?>
|
||||
<div class="card card-pad" id="taskAddCard" hidden>
|
||||
<h2 class="cinema-h2">新建任务</h2>
|
||||
@@ -36,9 +51,14 @@ $cols = [['todo', '待办'], ['doing', '进行中'], ['done', '已完成']];
|
||||
<label for="tk-title">要做什么 *</label>
|
||||
<input class="input" id="tk-title" name="title" required placeholder="如:周末大扫除">
|
||||
</div>
|
||||
<?php /* v24 第 5 条:截止时刻也走全局统一的「日期 + 时分秒」控件。
|
||||
秒这一档保留(与其它模块同一口径),默认 23:59:59 —— "这天之前做完"
|
||||
比"这天 00:00 之前"更符合直觉。 */ ?>
|
||||
<div class="field">
|
||||
<label for="tk-due">截止时间(有才会出现在日历里)</label>
|
||||
<input class="input" type="date" id="tk-due" name="due_date">
|
||||
<?php $dtName = 'due_at'; $dtId = 'tk-due'; $dtValue = null;
|
||||
$dtRequired = false; $dtSeconds = true;
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tk-assign">负责人</label>
|
||||
@@ -57,6 +77,25 @@ $cols = [['todo', '待办'], ['doing', '进行中'], ['done', '已完成']];
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tk-cat">分类</label>
|
||||
<select class="select" id="tk-cat" name="category_id">
|
||||
<option value="0">未分类</option>
|
||||
<?php foreach ($categories as $c): ?>
|
||||
<option value="<?= (int)$c['id'] ?>"><?= e((string)$c['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tk-board">看板</label>
|
||||
<select class="select" id="tk-board" name="board_id">
|
||||
<?php foreach ($boards as $b): ?>
|
||||
<option value="<?= (int)$b['id'] ?>" <?= (int)$b['id'] === $curBoard ? 'selected' : '' ?>>
|
||||
<?= e((string)$b['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tk-share">共享给(可多选,任何人完成都算完成)</label>
|
||||
<select class="select" id="tk-share" name="shares[]" multiple size="3">
|
||||
@@ -123,6 +162,9 @@ $cols = [['todo', '待办'], ['doing', '进行中'], ['done', '已完成']];
|
||||
<?php if ((int)($t['attach_count'] ?? 0) > 0): ?>
|
||||
<span class="kb-ico" title="附件">📎 <?= (int)$t['attach_count'] ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (($t['category_name'] ?? '') !== ''): ?>
|
||||
<span class="node-badge" title="分类"><?= e((string)$t['category_name']) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($total > 0): ?>
|
||||
<div class="kb-sub">
|
||||
@@ -194,21 +236,36 @@ $cols = [['todo', '待办'], ['doing', '进行中'], ['done', '已完成']];
|
||||
});
|
||||
|
||||
/* ---- 拖动(渐进增强:拖不动还有上面的左右按钮)---- */
|
||||
/* ⚠️ v24(用户 2026-10-02 第 8 条):拖完一张卡,松手那一下浏览器还会
|
||||
补一个 click 给卡片里的标题链接 —— 于是"刚拖到位就立刻跳进详情页",
|
||||
看板直接没法用了。
|
||||
修法:拖结束就立个牌子 dragEnded,标题链接的 click 看到牌子就吞掉。
|
||||
牌子在**下一次 mousedown** 时摘掉(真想点进去总得再按一次鼠标),
|
||||
所以不会误伤正常点击 —— 比用 setTimeout 猜时间准得多。 */
|
||||
var dragging = null;
|
||||
var dragEnded = false;
|
||||
document.addEventListener('mousedown', function () { dragEnded = false; }, true);
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.kb-card[draggable="true"]'), function (card) {
|
||||
card.addEventListener('dragstart', function (ev) {
|
||||
dragging = card;
|
||||
dragEnded = false;
|
||||
card.classList.add('is-dragging');
|
||||
if (ev.dataTransfer) { ev.dataTransfer.effectAllowed = 'move'; }
|
||||
});
|
||||
card.addEventListener('dragend', function () {
|
||||
card.classList.remove('is-dragging');
|
||||
dragging = null;
|
||||
dragEnded = true;
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.kb-body.is-over'), function (el) {
|
||||
el.classList.remove('is-over');
|
||||
});
|
||||
});
|
||||
});
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.kb-card .kb-title'), function (a) {
|
||||
a.addEventListener('click', function (ev) {
|
||||
if (dragEnded) { ev.preventDefault(); ev.stopPropagation(); }
|
||||
});
|
||||
});
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.kb-body'), function (body) {
|
||||
body.addEventListener('dragover', function (ev) {
|
||||
if (!dragging) return;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* 任务设置(v24 用户第 9、10 条)
|
||||
*
|
||||
* 用户原话:「和观影一样,新建任务旁边,也是设置,对任务的设置。
|
||||
* 用户可以设置任务分类、看板。还可以在这里查看已完成的任务,
|
||||
* 已完成的任务,按照分类来进行展示和搜索就可以了。」
|
||||
*
|
||||
* 所以这一页两块:看板 / 分类,各有增删改;旁边给一个进「已完成」的入口。
|
||||
*
|
||||
* @param array $boards 看板(带 open_count:里面还有几件没做完)
|
||||
* @param array $categories 分类(带 used_count:被几条任务用过)
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>任务设置</h1>
|
||||
<span class="sub">看板与分类都由你自己定</span>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost" href="<?= e(url('tasks')) ?>">返回任务</a>
|
||||
<a class="btn" href="<?= e(url('tasks/done')) ?>">已完成的任务</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$canWrite): ?>
|
||||
<div class="card card-pad"><p class="empty-hint">只读账号不能改设置。</p></div>
|
||||
<?php else: ?>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">看板</h2>
|
||||
<p class="small muted" style="margin-top:0">
|
||||
看板是「分开看几摊事」用的(家务 / 装修 / 旅行准备 …)。任务看板顶上可以切换。
|
||||
删掉一个看板时,里面的任务不会被删,会挪到默认看板。
|
||||
</p>
|
||||
<div class="set-list">
|
||||
<?php foreach ($boards as $b): ?>
|
||||
<form class="inline-form set-row" method="post" action="<?= e(url('tasks/board_update')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
|
||||
<input class="input" name="name" value="<?= e((string)$b['name']) ?>" required>
|
||||
<span class="small muted"><?= (int)($b['open_count'] ?? 0) ?> 件未完成</span>
|
||||
<button class="btn btn-sm" type="submit">改名</button>
|
||||
</form>
|
||||
<?php if (count($boards) > 1): ?>
|
||||
<form class="inline-form" method="post" action="<?= e(url('tasks/board_del')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这个看板?里面的任务会挪到默认看板,不会丢。')">删除</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="small muted set-tail">最后一个看板不能删</span>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<form class="inline-form" method="post" action="<?= e(url('tasks/board_add')) ?>" style="margin-top:10px">
|
||||
<?= csrf_field() ?>
|
||||
<input class="input" name="name" placeholder="新看板名,如:装修" required>
|
||||
<button class="btn btn-sm" type="submit">添加看板</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">分类</h2>
|
||||
<p class="small muted" style="margin-top:0">
|
||||
分类是给任务贴的标签(证件 / 报销 / 维修 …),用来在「已完成」里归类查看与搜索。
|
||||
</p>
|
||||
<div class="set-list">
|
||||
<?php if (!$categories): ?>
|
||||
<p class="empty-hint">还没有分类。</p>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($categories as $c): ?>
|
||||
<form class="inline-form set-row" method="post" action="<?= e(url('tasks/cat_update')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||||
<input class="input" name="name" value="<?= e((string)$c['name']) ?>" required>
|
||||
<span class="small muted"><?= (int)($c['used_count'] ?? 0) ?> 条用过</span>
|
||||
<button class="btn btn-sm" type="submit">改名</button>
|
||||
</form>
|
||||
<form class="inline-form" method="post" action="<?= e(url('tasks/cat_del')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit"
|
||||
onclick="return confirm('删掉这个分类?用它标过的任务会变成「未分类」。')">删除</button>
|
||||
</form>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<form class="inline-form" method="post" action="<?= e(url('tasks/cat_add')) ?>" style="margin-top:10px">
|
||||
<?= csrf_field() ?>
|
||||
<input class="input" name="name" placeholder="新分类名,如:证件" required>
|
||||
<button class="btn btn-sm" type="submit">添加分类</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
+46
-27
@@ -88,8 +88,10 @@ $isChild = !empty($t['parent_id']);
|
||||
<input class="input" name="title" required value="<?= e((string)$t['title']) ?>">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>截止日期(留空=不上日历)</label>
|
||||
<input class="input" type="date" name="due_date" value="<?= e(substr((string)($t['due_at'] ?? ''), 0, 10)) ?>">
|
||||
<label>截止时间(留空=不上日历)</label>
|
||||
<?php $dtName = 'due_at'; $dtValue = $t['due_at'] ?? null;
|
||||
$dtRequired = false; $dtSeconds = true;
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>负责人</label>
|
||||
@@ -123,6 +125,28 @@ $isChild = !empty($t['parent_id']);
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php /* v24 第 9、10 条:分类与看板(看板把任务放到哪一块,分类是标签) */ ?>
|
||||
<div class="field">
|
||||
<label>分类</label>
|
||||
<select class="select" name="category_id">
|
||||
<option value="0">未分类</option>
|
||||
<?php foreach (($categories ?? []) as $c): ?>
|
||||
<option value="<?= (int)$c['id'] ?>"<?= (int)($t['category_id'] ?? 0) === (int)$c['id'] ? ' selected' : '' ?>>
|
||||
<?= e((string)$c['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>看板</label>
|
||||
<select class="select" name="board_id">
|
||||
<?php foreach (($boards ?? []) as $b): ?>
|
||||
<option value="<?= (int)$b['id'] ?>"<?= (int)($t['board_id'] ?? 0) === (int)$b['id'] ? ' selected' : '' ?>>
|
||||
<?= e((string)$b['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label>备注</label>
|
||||
<textarea class="input" name="note" rows="2"><?= e((string)($t['note'] ?? '')) ?></textarea>
|
||||
@@ -175,7 +199,8 @@ $isChild = !empty($t['parent_id']);
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="parent_id" value="<?= (int)$t['id'] ?>">
|
||||
<input class="input" name="title" required placeholder="如:擦玻璃">
|
||||
<input class="input" type="date" name="due_date">
|
||||
<?php $dtName = 'due_at'; $dtValue = null; $dtRequired = false; $dtSeconds = true;
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<button class="btn btn-sm" type="submit">加上</button>
|
||||
</form>
|
||||
</details>
|
||||
@@ -189,24 +214,14 @@ $isChild = !empty($t['parent_id']);
|
||||
<?php if (!$t['attachs']): ?>
|
||||
<p class="empty-hint">还没有附件。</p>
|
||||
<?php else: ?>
|
||||
<div class="attach-row">
|
||||
<?php foreach ($t['attachs'] as $a): ?>
|
||||
<span class="attach-item">
|
||||
<a href="<?= e(media_url((string)$a['key'])) ?>" target="_blank" rel="noopener">
|
||||
<?= (int)$a['is_image'] === 1 ? '🖼' : '📄' ?> <?= e((string)($a['name'] ?: '附件')) ?>
|
||||
</a>
|
||||
<span class="res-size"><?= e(\App\Attachment::sizeWord($a['size'] ?? 0)) ?></span>
|
||||
<a class="link-quiet" href="<?= e(media_url((string)$a['key'], ['dl' => 1])) ?>">下载</a>
|
||||
<?php if ($canWrite): ?>
|
||||
<form method="post" action="<?= e(url('tasks/attach_del')) ?>" class="res-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$a['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit" onclick="return confirm('删掉这个附件?')">删除</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
$attRows = $t['attachs'];
|
||||
$attCanEdit = $canWrite;
|
||||
$attDelUrl = 'tasks/attach_del';
|
||||
$attDelField = 'id';
|
||||
$attExtra = [];
|
||||
require __DIR__ . '/../_attachments.php';
|
||||
?>
|
||||
<?php endif; ?>
|
||||
<?php if ($canWrite): ?>
|
||||
<form class="inline-form" method="post" enctype="multipart/form-data" action="<?= e(url('tasks/attach_add')) ?>">
|
||||
@@ -243,12 +258,16 @@ $isChild = !empty($t['parent_id']);
|
||||
<form class="inline-form" method="post" action="<?= e(url('tasks/share')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
|
||||
<select class="select" name="person_id">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<?php if ((int)$p['id'] === $meId) { continue; } ?>
|
||||
<option value="<?= (int)$p['id'] ?>"><?= e(person_name($p)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php
|
||||
$ppName = 'person_id';
|
||||
$ppPeople = array_values(array_filter($people, fn($p) => (int)$p['id'] !== $meId));
|
||||
$ppSelected = [];
|
||||
$ppMode = 'single';
|
||||
$ppOpenBtn = '选择要共享给谁';
|
||||
$ppTitle = '共享给谁';
|
||||
$ppEmptyText = '未选择';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
<select class="select" name="perm">
|
||||
<option value="write">可写</option>
|
||||
<option value="read">只读</option>
|
||||
|
||||
@@ -57,12 +57,12 @@ $nfPrefix = $nfIsEdit ? 'ne' . (int)$nfNode['id'] : ('n' . ($nfCategory === 'rea
|
||||
<div class="field">
|
||||
<label>开始</label>
|
||||
<?php $dtName = 'starts_at'; $dtValue = $nfNode['starts_at'] ?? '';
|
||||
$dtId = $nfPrefix . '-s'; $dtSeconds = false; require __DIR__ . '/../_datetime.php'; ?>
|
||||
$dtId = $nfPrefix . '-s'; $dtSeconds = true; require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>结束</label>
|
||||
<?php $dtName = 'ends_at'; $dtValue = $nfNode['ends_at'] ?? '';
|
||||
$dtId = $nfPrefix . '-e'; $dtSeconds = false; require __DIR__ . '/../_datetime.php'; ?>
|
||||
$dtId = $nfPrefix . '-e'; $dtSeconds = true; require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label for="<?= e($nfPrefix) ?>-detail">记录</label>
|
||||
|
||||
+14
-16
@@ -47,13 +47,13 @@ if ($isEdit) {
|
||||
<div class="field">
|
||||
<label for="t-start">出发</label>
|
||||
<?php $dtName = 'starts_at'; $dtValue = $t['starts_at'] ?? '';
|
||||
$dtId = 't-start'; $dtSeconds = false; require __DIR__ . '/../_datetime.php'; ?>
|
||||
$dtId = 't-start'; $dtSeconds = true; require __DIR__ . '/../_datetime.php'; ?>
|
||||
<?php if (!empty($errors['starts_at'])): ?><span class="err"><?= e($errors['starts_at']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="t-end">回来</label>
|
||||
<?php $dtName = 'ends_at'; $dtValue = $t['ends_at'] ?? ''; $dtId = 't-end'; $dtSeconds = false;
|
||||
<?php $dtName = 'ends_at'; $dtValue = $t['ends_at'] ?? ''; $dtId = 't-end'; $dtSeconds = true;
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<?php if (!empty($errors['ends_at'])): ?><span class="err"><?= e($errors['ends_at']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
@@ -77,23 +77,21 @@ if ($isEdit) {
|
||||
|
||||
<div class="field full">
|
||||
<label>同行的人</label>
|
||||
<?php /* v24 第 11 条:与全局选人组件统一(原来是平铺的 checkbox 网格,
|
||||
名册一多就变成"在一长串里瞪眼找",也搜不了拼音) */ ?>
|
||||
<?php if (!$people): ?>
|
||||
<p class="empty-hint">名册里还没有人。</p>
|
||||
<?php else: ?>
|
||||
<div class="pick-list">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<label class="pick-item">
|
||||
<input type="checkbox" name="attendees[]" value="<?= (int)$p['id'] ?>"
|
||||
<?= in_array((int)$p['id'], $selIds, true) ? 'checked' : '' ?>>
|
||||
<?php if (!empty($p['avatar'])): ?>
|
||||
<img class="pick-ph" src="<?= e(media_url((string)$p['avatar'])) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<span class="pick-ph"></span>
|
||||
<?php endif; ?>
|
||||
<span class="pick-name"><?= e(person_name($p)) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
$ppName = 'attendees[]';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = $selIds;
|
||||
$ppMode = 'multi';
|
||||
$ppOpenBtn = '+ 添加同行人';
|
||||
$ppTitle = '选择同行的人';
|
||||
$ppTip = '';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -71,7 +71,13 @@ $memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'i
|
||||
<div class="photo-grid">
|
||||
<?php foreach ($photos as $ph): ?>
|
||||
<figure class="photo-cell">
|
||||
<a href="<?= e(media_url((string)$ph['key'])) ?>" target="_blank" rel="noopener">
|
||||
<?php /* v24 第 3 条:照片也在**本页**预览(原来 target=_blank 是另开一个标签页
|
||||
只显示一张图,既没有下载按钮也不好回来看) */ ?>
|
||||
<a href="<?= e(media_url((string)$ph['key'])) ?>"
|
||||
data-prev="image"
|
||||
data-src="<?= e(media_url((string)$ph['key'])) ?>"
|
||||
data-dl="<?= e(media_url((string)$ph['key'], ['dl' => 1])) ?>"
|
||||
data-name="<?= e((string)($ph['name'] ?? '照片')) ?>">
|
||||
<img src="<?= e(media_url((string)$ph['key'])) ?>" alt="<?= e((string)($ph['name'] ?? '')) ?>">
|
||||
</a>
|
||||
<?php if ($canWrite): ?>
|
||||
@@ -107,7 +113,8 @@ $memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'i
|
||||
<?php else: ?>
|
||||
<ol class="node-list">
|
||||
<?php foreach ($list as $n): ?>
|
||||
<li class="node-row status-<?= e((string)$n['status']) ?>">
|
||||
<?php /* v24:日历里点「行程节点」是跳 #node-<id> 过来的,这里给个锚点好定位。 */ ?>
|
||||
<li class="node-row status-<?= e((string)$n['status']) ?>" id="node-<?= (int)$n['id'] ?>">
|
||||
<span class="node-ico" title="<?= e(\App\Trip::typeWord((string)$n['node_type'])) ?>">
|
||||
<?= e(\App\Trip::typeIcon((string)$n['node_type'])) ?>
|
||||
</span>
|
||||
@@ -132,13 +139,12 @@ $memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'i
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($n['attachs'])): ?>
|
||||
<div class="attach-row">
|
||||
<?php foreach ($n['attachs'] as $at): ?>
|
||||
<a class="attach-item" href="<?= e(media_url((string)$at['key'], ['dl' => 1])) ?>">
|
||||
<?= (int)$at['is_image'] === 1 ? '🖼' : '📄' ?> <?= e((string)($at['name'] ?: '附件')) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
$attRows = $n['attachs'];
|
||||
$attCanEdit = false; // 节点附件不单独给删除入口(防越权:得先判整趟旅程)
|
||||
$attDelUrl = '';
|
||||
require __DIR__ . '/../_attachments.php';
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($canWrite): ?>
|
||||
|
||||
Reference in New Issue
Block a user