2026-10-02 09:28:07 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
2026-10-03 15:01:38 +08:00
|
|
|
|
* 日程 · 月 / 周 / 天 三视图(v12,v24 第 4、5 条重排)。
|
2026-10-02 09:28:07 +08:00
|
|
|
|
*
|
2026-10-03 15:01:38 +08:00
|
|
|
|
* ⚠️ v24 用户 2026-10-03 第 4 条:**左侧那一栏「我的日历本」撤掉了**,日历占满整宽。
|
|
|
|
|
|
* 原话:「日程模块,左侧的日历本显示与否,去掉,让日历充满整个宽度。
|
|
|
|
|
|
* 在右侧,也加一个设置按钮,设置中,可以设置哪些日历本显示,
|
|
|
|
|
|
* 调整日历的颜色,导入日历等功能,都放在这个设置下。」
|
|
|
|
|
|
* 所以那些功能全搬进了页头最右边那个 ☰ 里(见 _settings_menu.php)。
|
|
|
|
|
|
* 显隐改由**服务端**处理(Calendar::hiddenCalendars):藏掉的本子不参与取数,
|
|
|
|
|
|
* "+N 条"之类按可见条目算 —— 前端 display:none 会让这些数字对不上。
|
2026-10-02 09:28:07 +08:00
|
|
|
|
*
|
2026-10-03 15:01:38 +08:00
|
|
|
|
* ⚠️ 用户第 5 条:在日历里点一下就建日程(开始时间 = 点的那一格)。
|
|
|
|
|
|
* 月视图的格子按天、周/日视图按半小时——
|
|
|
|
|
|
* 后者用鼠标在列里的纵向位置反算时刻(见页面底部那两段 JS)。
|
2026-10-02 09:28:07 +08:00
|
|
|
|
*
|
|
|
|
|
|
* @param string $ym 'Y-m'
|
|
|
|
|
|
* @param array $weeks 月网格(build_month_grid 产出)
|
2026-10-03 15:01:38 +08:00
|
|
|
|
* @param array $cals 我可见的日历本(含 perm);$visCals 是其中显示着的 id
|
|
|
|
|
|
* @param string $newAt 点进来时预填的开始时间('Y-m-d H:i' 或 'Y-m-d')
|
2026-10-02 09:28:07 +08:00
|
|
|
|
*/
|
|
|
|
|
|
$canWrite = \App\Auth::canWrite();
|
2026-10-03 08:18:05 +08:00
|
|
|
|
$view = (string)($view ?? 'month');
|
|
|
|
|
|
$anchor = (string)($anchor ?? date('Y-m-d'));
|
2026-10-02 09:28:07 +08:00
|
|
|
|
$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');
|
2026-10-26 14:59:57 +08:00
|
|
|
|
/* 🔴 v95(用户 2026-10-26):只列**能放日程**的本子。
|
|
|
|
|
|
⚠️ 原来是 `array_filter($cals, canWrite)` —— 拦不住观影/旅行派生本(它们可写)
|
|
|
|
|
|
和系统只读本(perm 是 owner,canWrite 恒真)。统一走 `Calendar::writableForEvent()`,
|
|
|
|
|
|
详见那个方法的注释(里面记着实测的 perm/kind 对照表)。 */
|
|
|
|
|
|
$writable = \App\Calendar::filterWritableForEvent($cals, (int)\App\Auth::personId());
|
2026-10-03 15:01:38 +08:00
|
|
|
|
$visCals = array_map('intval', (array)($visCals ?? []));
|
|
|
|
|
|
$hiddenCals = array_map('intval', (array)($hiddenCals ?? []));
|
|
|
|
|
|
$newAt = (string)($newAt ?? '');
|
2026-10-14 14:30:37 +08:00
|
|
|
|
// 能不能"点一下建日程":只读账号点了没反应,所以连 rt-writable 标记都不给。
|
|
|
|
|
|
// 🔴 v48(用户 2026-10-14 第 3 条):**十字光标已去掉**(CSS 里不再有 cursor:crosshair),
|
|
|
|
|
|
// 但 `rt-writable` 这个**类名保留** —— 它是"这个人能不能写"在 DOM 上的标记位,
|
|
|
|
|
|
// 点击建日程的判断逻辑与此无关(见底部 JS 的 $canWrite && $writable)。
|
2026-10-03 15:01:38 +08:00
|
|
|
|
$canNew = $canWrite && (bool)$writable;
|
2026-10-03 08:18:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 三种看法各自的「上一段 / 下一段」:月按整月走,周按 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)));
|
2026-10-02 09:28:07 +08:00
|
|
|
|
?>
|
|
|
|
|
|
<div class="page-head">
|
|
|
|
|
|
<h1>日程</h1>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<span class="sub"><?= e($headText) ?></span>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<div class="actions">
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<?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; ?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<?php if ($canWrite && $writable): ?>
|
|
|
|
|
|
<button class="btn" type="button" id="calAddBtn">新建日程</button>
|
|
|
|
|
|
<?php endif; ?>
|
2026-10-04 10:46:50 +08:00
|
|
|
|
<?php /* 用户 2026-10-04 第 6 条原话:「日程页面,在设置按钮的前面,加一个日历按钮,
|
|
|
|
|
|
选择日历后,跳转到那一天。」
|
|
|
|
|
|
→ 一个带日历图标的按钮,点开是日期选择;选中即跳到那天的**日视图**。
|
|
|
|
|
|
⚠️ 用原生 <input type="date"> 而不是再引一个弹层:它的原生日历足够用,
|
|
|
|
|
|
而且**选中就有值**(change 事件)→ 直接跳转,不用再点一次"确定"。
|
|
|
|
|
|
桌面/手机/无 JS 三种情况下的行为都是可预期的。 */ ?>
|
2026-10-04 19:43:00 +08:00
|
|
|
|
<?php /* 用户 2026-10-05 第 12 批 #3 原话(三句):
|
|
|
|
|
|
· 「跳转到某一天时,不要进入日视图,保留当前本来的视图,只不过是跳转到这一天,
|
|
|
|
|
|
如果是周视图,那么就是跳转到这一天所在的周,如果是月视图,则跳转到这一天所在的月」
|
|
|
|
|
|
· 「选择这个日期,不用弹窗,直接把日历选择,弹出在这个按钮下面就可以了」
|
|
|
|
|
|
→ ① 不再固定 view=day:带上**当前视图**(月/周/日各自跳到对应跨度);
|
2026-10-05 10:58:17 +08:00
|
|
|
|
② 日期选择从"居中弹窗"改成**贴着按钮下方展开的下拉日历**。
|
|
|
|
|
|
|
|
|
|
|
|
🔴 2026-10-05 第 14 批 #1(用户报严重 bug):「点上一月,页面直接跳转,
|
|
|
|
|
|
我还没选择日期呢」。根因是这一版下拉里挂的是 Semi 的 DatePicker ——
|
|
|
|
|
|
它的日历面板**渲染在 body 的 portal 里**(不在下拉容器内),
|
|
|
|
|
|
而它在面板上翻月时会把"面板当前显示的日期"当成新值回调 onChange,
|
|
|
|
|
|
于是"翻月"被当成了"选中"。第三方控件把"翻页"与"选中"混在一个回调里,
|
|
|
|
|
|
这个语义我们改不了。
|
|
|
|
|
|
→ 改成**自绘内联日历**(几十行、零依赖):翻月/翻年只重绘面板、绝不提交,
|
|
|
|
|
|
只有点**某一天**才跳转,语义 100% 由我们自己定。 */ ?>
|
2026-10-04 10:46:50 +08:00
|
|
|
|
<form class="cal-jump" method="get" action="index.php">
|
|
|
|
|
|
<input type="hidden" name="r" value="calendar">
|
2026-10-04 19:43:00 +08:00
|
|
|
|
<?php /* 保留当前视图:月→该月 / 周→那一周 / 日→那一天。
|
|
|
|
|
|
⚠️ 原来写死 view=day,用户点"跳到 10 月 20 日"就被踢进日视图,
|
|
|
|
|
|
而他本来在月视图里 —— 这正是不满的点。 */ ?>
|
|
|
|
|
|
<input type="hidden" name="view" value="<?= e($view) ?>">
|
2026-10-04 17:42:21 +08:00
|
|
|
|
<input type="hidden" name="d" id="calJumpD" value="<?= e($anchor) ?>">
|
2026-10-04 19:43:00 +08:00
|
|
|
|
<?php /* 月视图用的是 ym(年月),跳转时也要带上,否则服务端会退回默认月 */ ?>
|
|
|
|
|
|
<?php if ($view === 'month'): ?>
|
|
|
|
|
|
<input type="hidden" name="ym" id="calJumpYM" value="<?= e(substr($anchor, 0, 7)) ?>">
|
|
|
|
|
|
<?php endif; ?>
|
2026-10-04 17:42:21 +08:00
|
|
|
|
<button type="button" class="btn btn-icon cal-jump-btn" id="calJumpBtn"
|
2026-10-04 19:43:00 +08:00
|
|
|
|
title="跳到某一天" aria-label="跳到某一天" aria-haspopup="dialog" aria-expanded="false">
|
2026-10-04 17:42:21 +08:00
|
|
|
|
<svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none"
|
|
|
|
|
|
stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
|
2026-10-04 10:46:50 +08:00
|
|
|
|
<rect x="2" y="3.2" width="12" height="10.8" rx="1.6"/>
|
|
|
|
|
|
<path d="M2 6.6h12M5.2 1.8v2.6M10.8 1.8v2.6"/>
|
|
|
|
|
|
</svg>
|
2026-10-04 17:42:21 +08:00
|
|
|
|
</button>
|
2026-10-05 10:58:17 +08:00
|
|
|
|
<?php /* 下拉日历的宿主:紧贴按钮下方展开(不是弹窗)。内容由下面那段 JS 画;
|
|
|
|
|
|
JS 不可用时它是空的、退到下面的 noscript(原生日期框 + 跳转按钮)。 */ ?>
|
2026-10-04 19:43:00 +08:00
|
|
|
|
<div class="cal-jump-drop" id="calJumpDrop" hidden></div>
|
2026-10-04 17:42:21 +08:00
|
|
|
|
<noscript>
|
|
|
|
|
|
<input class="input" type="date" name="d" value="<?= e($anchor) ?>" aria-label="选择日期">
|
|
|
|
|
|
<button class="btn btn-sm" type="submit">跳转</button>
|
|
|
|
|
|
</noscript>
|
2026-10-04 10:46:50 +08:00
|
|
|
|
</form>
|
2026-10-03 15:01:38 +08:00
|
|
|
|
<?php /* 用户第 4、5 条:日历本 / 颜色 / 导入 / 默认视图 全收进这个 ☰ ——
|
|
|
|
|
|
它顶替了原来左边那一栏,所以排在**最右**(靠 .page-head .actions 的顺序)。 */ ?>
|
|
|
|
|
|
<?php $setCurrent = 'view'; require __DIR__ . '/_settings_menu.php'; ?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-03 15:01:38 +08:00
|
|
|
|
<div class="cal-wrap cal-wrap-full">
|
|
|
|
|
|
<?php /* ---- 右:月网格 / 时间网格 ----
|
|
|
|
|
|
用户第 4 条:左边那一栏「我的日历本」已经撤掉(功能搬进右上角 ☰),
|
|
|
|
|
|
所以这里直接就是主区域,`.cal-wrap-full` 把网格调成"只有一列"。 */ ?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<div class="cal-main">
|
2026-10-06 12:08:40 +08:00
|
|
|
|
<?php /* 🔴 v24 第 21 批 第 7 条:新建日程改成**弹框**(用户原话:
|
|
|
|
|
|
「日历中新建日程,无论是点击按钮还是点击日历中间,都是采用弹框的形式进行添加,
|
|
|
|
|
|
这样不影响当前日历的显示,添加完成后,动态修改当前视图,不要刷新。」
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 原来是**内联卡片**(`#calAddCard`,就长在日历网格的正上方,展开时把整个
|
|
|
|
|
|
月视图往下推)—— 用户点一下"新建",正在看的日历就被顶走了,
|
|
|
|
|
|
填完还得自己找回来。这正是他说的"影响当前日历的显示"。
|
|
|
|
|
|
⚠️ 改成弹框用**既有的浮层形态**(与选人弹窗、名册动作框同一套 `.upfield`
|
|
|
|
|
|
不是,是 `.rm-modal`/`.crop-overlay` 那一路视觉:遮罩 + 居中面板),
|
|
|
|
|
|
不引第三方库 —— 原生 `<dialog>` 在部分旧 WebView 上行为不一致,
|
|
|
|
|
|
而且我们已有的浮层已经解决了"点遮罩关、Esc 关、滚动锁"这几件事。
|
|
|
|
|
|
⚠️ 表单本身**一个字没动**(字段名/服务端契约/CSRF 全照旧),
|
|
|
|
|
|
只把它的容器从"页面上的一格"换成"浮层里的一块"。
|
|
|
|
|
|
`calendar_id` 那个 select 也保留(第 7 条要求"选择日历本,带上颜色",
|
|
|
|
|
|
见下面给每个 option 加的色块)。 */ ?>
|
2026-10-24 04:25:02 +08:00
|
|
|
|
<?php /* 🔴 v82(用户 2026-10-23):这一大段弹框**抽成公共片段**了 ——
|
|
|
|
|
|
首页日程卡片要"在首页就能新建日程",两处必须用同一张表单。
|
|
|
|
|
|
片段:`templates/calendar/_add_modal.php`(表单契约一字未改)。
|
|
|
|
|
|
⚠️ 抽片段后**必须**同步改部署脚本的静态断言:那批断言原来按
|
|
|
|
|
|
`calendar/index.php` 找 `id="calPicker"` / `id="cbDot"` /
|
|
|
|
|
|
`class="cs-dot" style="background:` / `<span class="mv-lb">日历本</span>`
|
|
|
|
|
|
/ `$evMvStyle = true` —— 现在这些串在新片段里。
|
|
|
|
|
|
不改的话:正向断言当场中止部署,负向断言恒真没牙齿(v78 原话)。 */
|
|
|
|
|
|
?>
|
|
|
|
|
|
<?php
|
|
|
|
|
|
$calCanNew = $canWrite && (bool)$writable;
|
|
|
|
|
|
$calWritable = $writable;
|
|
|
|
|
|
$calCtx = [
|
|
|
|
|
|
'ym' => $ym, 'view' => $view, 'anchor' => $anchor,
|
|
|
|
|
|
'new_at' => $newAt,
|
|
|
|
|
|
// 模块页只管自己:'' = 提交完回日程模块(不写 'home')
|
|
|
|
|
|
'back' => '',
|
|
|
|
|
|
];
|
|
|
|
|
|
require __DIR__ . '/_add_modal.php';
|
|
|
|
|
|
/* 🔴🔴 v82:**只清掉三个"调用参数"**,别的什么都不许 unset。
|
|
|
|
|
|
为什么:`require` 与宿主**同一作用域**,片段里赋的每个变量都会留下来。
|
|
|
|
|
|
我第一版顺手把 `$ym/$view/$anchor/$newAt/$writable` 也 unset 了 ——
|
|
|
|
|
|
而这五个正是**模块页自己后文还要用的**(页头标题、翻页链接、周/日网格、
|
|
|
|
|
|
`.tg-now` 的 top 计算全依赖它们)。unset 掉不会报错,只会让后半页
|
|
|
|
|
|
静默变成空值:页头显示"1970 年"、翻页链接丢参数、时间网格画不出来。
|
|
|
|
|
|
这类"清了别人的变量"比"片段拿不到变量"更难查 —— 页面能开,只是全错。
|
|
|
|
|
|
⇒ 判据:unset 之前先 grep 一下后文还用不用它(本项目吃过"片段覆盖宿主变量"的亏)。
|
2026-10-12 15:31:03 +08:00
|
|
|
|
|
2026-10-24 04:25:02 +08:00
|
|
|
|
🔴 v82 再修(同日):片段内部现在已经**改用 `$cal*` 前缀**,不再覆盖
|
|
|
|
|
|
宿主的 `$anchor/$newAt/$writable/$view/$ym`(见该片段头部那段说明)。
|
|
|
|
|
|
所以这里连"片段残留物"都不必再清 —— 真正要清的只有本模板自己传进去的
|
|
|
|
|
|
三个 `$cal*` 入参。 */
|
|
|
|
|
|
unset($calCanNew, $calWritable, $calCtx);
|
|
|
|
|
|
?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<?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' : '' ?>">
|
2026-10-04 17:42:21 +08:00
|
|
|
|
<?php /* $c['w'] 是 date('w')(0=周日),所以这张索引表保持"日"开头——
|
|
|
|
|
|
它只是把数字换成汉字,与列的排列顺序无关。 */ ?>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<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): ?>
|
2026-10-12 00:24:20 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
$evKind = (string)($ev['kind'] ?? 'custom');
|
|
|
|
|
|
$evRef = (int)($ev['ref_id'] ?? 0);
|
|
|
|
|
|
$evEdit = '';
|
|
|
|
|
|
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
|
|
|
|
|
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
?>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<a class="cal-ev is-allday" data-cal="<?= (int)$ev['cal'] ?>"
|
2026-10-12 00:24:20 +08:00
|
|
|
|
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
2026-10-03 08:18:05 +08:00
|
|
|
|
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): ?>
|
2026-10-03 15:01:38 +08:00
|
|
|
|
<div class="tg-col<?= $c['is_today'] ? ' is-today' : '' ?><?= $canNew ? ' rt-writable' : '' ?>"
|
|
|
|
|
|
data-new-at="<?= e((string)$c['date']) ?>">
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<?php for ($h = 0; $h < 24; $h++): ?>
|
|
|
|
|
|
<span class="tg-line"></span>
|
|
|
|
|
|
<?php endfor; ?>
|
2026-10-06 03:14:18 +08:00
|
|
|
|
<?php /* ==================== 当前时间红线(v24 第 20 批 用户第 6 条)====================
|
|
|
|
|
|
用户原话:「日程模块,周视图和日视图,应该在视图中间显示一条红线,
|
|
|
|
|
|
用来表示当前进行的时间,就是说,用一个不断从上到下的红线,
|
|
|
|
|
|
来表示当前时间在视图上的刻度和位置。」
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 只在**今天那一列**画(别的日子没有"现在"可言)。
|
|
|
|
|
|
⚠️ 位置是服务端算好的百分比(与上面 .tg-block 用的是同一套:
|
|
|
|
|
|
列高 = 24h,所以 1 分钟 = 100/1440 %)。**不在 CSS 里写死**
|
|
|
|
|
|
某个时间 —— 那就成了"固定的一条线",不叫"当前时间"。
|
|
|
|
|
|
⚠️ 线的**移动**由 app.js 里的一分钟定时器负责(`initNowLines()`):
|
|
|
|
|
|
服务端给的是"渲染那一刻"的位置,用户开着页面不走,
|
|
|
|
|
|
过一小时线还停在那儿就荒谬了。
|
|
|
|
|
|
⚠️ 全天日程的"全天"那一行**不画** —— 那条线表达的是"一天之内的时间刻度",
|
|
|
|
|
|
画在全天行上会让人以为全天日程跟时间有关。 */ ?>
|
|
|
|
|
|
<?php if ($c['is_today'] && !empty($nowTop)): ?>
|
|
|
|
|
|
<span class="tg-now" data-now-line style="top: <?= (float)$nowTop ?>%;"
|
|
|
|
|
|
title="现在 <?= e(date('H:i')) ?>"></span>
|
|
|
|
|
|
<?php endif; ?>
|
2026-10-08 17:41:30 +08:00
|
|
|
|
<?php /* 🔴 v30 第 4 条:时间冲突的日程**错开画**(不再堆叠)。
|
|
|
|
|
|
用户原话:「如果两个事件时间上有冲突了,现在是堆叠在一起,应该错开,
|
|
|
|
|
|
显示两个色块,如果三个冲突了,就是三个色块,错开显示。」
|
|
|
|
|
|
left/width 由 App\Calendar::conflictLanes 算好(算法借鉴 SyncCalendar
|
|
|
|
|
|
的 TimelineUtil:贪心分列,列数 = 最大同时重叠数)。
|
|
|
|
|
|
⚠️ 用**内联 left/width 百分比**而不是 CSS 类:
|
|
|
|
|
|
列数 1/2/3/4 是运行时才知道的,类名会爆炸;
|
|
|
|
|
|
而且模板已经在内联 top/height 了,同一套坐标系最不容易错。 */ ?>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<?php foreach (($tgrid['timed'][$i] ?? []) as $ev): ?>
|
2026-10-12 00:24:20 +08:00
|
|
|
|
<?php /* 🔴 v43(用户第 6 条):点条目弹详情浮层(不再直接跳页)。
|
|
|
|
|
|
数据由 fb_entry_json() 统一编(三个入口共用一个函数)。
|
|
|
|
|
|
「详细信息」跳该来源的详情页;「编辑」取该来源的编辑表单片段。
|
|
|
|
|
|
⚠️ 编辑地址一律带可写判断:只读的(农历/节假日)不给「编辑」。 */ ?>
|
|
|
|
|
|
<?php
|
|
|
|
|
|
$evKind = (string)($ev['kind'] ?? 'custom');
|
|
|
|
|
|
$evRef = (int)($ev['ref_id'] ?? 0);
|
|
|
|
|
|
$evDet = (string)($ev['href'] ?? '');
|
|
|
|
|
|
$evEdit = '';
|
|
|
|
|
|
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
|
|
|
|
|
// trip 的条目指向"某个旅程里的某段行程",编辑器在旅程详情页里 —— 这里不给「编辑」
|
|
|
|
|
|
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
?>
|
2026-10-15 10:37:07 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/* 🔴 v51(用户 2026-10-15 第 3 条):分栏(冲突错开)**在浏览器里重算**要用到
|
|
|
|
|
|
每条色块**声明的起止分钟**(服务端算分栏时用的就是这一对数)。
|
|
|
|
|
|
⚠️ 必须取 `time` / `end_time`,**不能**从 top/height 反推 ——
|
|
|
|
|
|
服务端的 `top/height` 是按**"实际显示结束"**算的(没填结束时可能被
|
|
|
|
|
|
同列下一条的起点截断),那不是它的真实结束分钟;拿它去分栏会把冲突判错。
|
|
|
|
|
|
⚠️ `data-e` 缺失 = "没填结束" ⇒ 前端按 +30′ 处理(与服务端 `$probe` 同口径)。 */
|
|
|
|
|
|
preg_match('/^(\d{1,2}):(\d{2})/', (string)($ev['time'] ?? ''), $mS);
|
|
|
|
|
|
$evS = $mS ? ((int)$mS[1] * 60 + (int)$mS[2]) : 0;
|
|
|
|
|
|
preg_match('/^(\d{1,2}):(\d{2})/', (string)($ev['end_time'] ?? ''), $mE);
|
|
|
|
|
|
$evE = $mE ? ((int)$mE[1] * 60 + (int)$mE[2]) : null;
|
|
|
|
|
|
if ($evE !== null && $evE <= $evS) { $evE = null; }
|
|
|
|
|
|
?>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<a class="tg-block" data-cal="<?= (int)$ev['cal'] ?>"
|
2026-10-15 10:37:07 +08:00
|
|
|
|
data-s="<?= (int)$evS ?>"<?= $evE !== null ? ' data-e="' . (int)$evE . '"' : '' ?>
|
2026-10-12 00:24:20 +08:00
|
|
|
|
data-fb-entry='<?= fb_entry_json($ev, $evDet, $evEdit) ?>'
|
2026-10-08 17:41:30 +08:00
|
|
|
|
style="background: <?= e((string)$ev['color']) ?>;
|
|
|
|
|
|
top: <?= (float)$ev['top'] ?>%; height: <?= (float)$ev['height'] ?>%;
|
|
|
|
|
|
left: calc(2px + <?= (float)($ev['left'] ?? 0) ?>%);
|
|
|
|
|
|
width: calc(<?= (float)($ev['width'] ?? 100) ?>% - 4px);"
|
2026-10-03 08:18:05 +08:00
|
|
|
|
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: ?>
|
2026-10-21 13:22:37 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/* 🔴 v70:把「每格全部条目」一次性送到前端(点 "+N 条" 时浮层直接用,
|
|
|
|
|
|
不再发请求)。**只送当月格子里的日期**,量很小(一个月最多几百条)。
|
|
|
|
|
|
⚠️ 用 `<script type="application/json">` 而不是把 JSON 塞进 data-*:
|
|
|
|
|
|
data-* 属性里几千条会被 HTML 转义得又长又容易截断;
|
|
|
|
|
|
JSON 里若出现 `</script>` 也会截断 —— 所以下面统一走 json_encode 的
|
|
|
|
|
|
JSON_HEX_TAG(把 < > 转成 \u003C \u003E,永远不会提前闭合)。
|
|
|
|
|
|
⚠️ 这是**渲染层**的便利数据,字段与 `data-fb-entry` 同一套(fb_entry_json),
|
|
|
|
|
|
所以浮层能沿用全部既有逻辑(详情/编辑/删除/来源判断)。 */
|
|
|
|
|
|
$dayData = [];
|
|
|
|
|
|
foreach (($weeks ?? []) as $wk) {
|
|
|
|
|
|
foreach ($wk as $cell) {
|
|
|
|
|
|
if (!$cell['events']) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$items = [];
|
|
|
|
|
|
foreach ($cell['events'] as $ev) {
|
|
|
|
|
|
$evKind = (string)($ev['kind'] ?? 'custom');
|
|
|
|
|
|
$evRef = (int)($ev['ref_id'] ?? 0);
|
|
|
|
|
|
$evEdit = '';
|
|
|
|
|
|
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
|
|
|
|
|
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
$items[] = ['_rank' => \App\Calendar::displayRank($ev),
|
|
|
|
|
|
'_time' => (string)($ev['time'] ?? '')]
|
|
|
|
|
|
+ json_decode(
|
|
|
|
|
|
fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit),
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
$dayData[(string)$cell['date']] = $items;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
?>
|
|
|
|
|
|
<script type="application/json" id="calDayData"><?= json_encode(
|
|
|
|
|
|
$dayData,
|
|
|
|
|
|
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP
|
|
|
|
|
|
) ?></script>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<div class="card cal-grid-card">
|
|
|
|
|
|
<div class="cal-head">
|
2026-10-04 17:42:21 +08:00
|
|
|
|
<?php /* ⚠️ 用户第 11 批 #6:「月视图,从周一开始。而不是周日。」
|
|
|
|
|
|
表头顺序必须与 month_grid_range() 的起算规则一致(那里已改成周一)。 */ ?>
|
|
|
|
|
|
<?php foreach (['一', '二', '三', '四', '五', '六', '日'] as $wd): ?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<div class="cal-wd"><?= e($wd) ?></div>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="cal-grid">
|
|
|
|
|
|
<?php foreach ($weeks as $wk): ?>
|
|
|
|
|
|
<?php foreach ($wk as $cell): ?>
|
2026-10-03 15:01:38 +08:00
|
|
|
|
<?php /* 用户第 5 条:点格子里的空白 = 在这天建日程。
|
|
|
|
|
|
用 data-new-at 把日期带过去,由底部 JS 交给新建表单
|
|
|
|
|
|
(点了小条本身是"打开那条日程",由 <a> 自己处理,JS 会排除掉)。 */ ?>
|
|
|
|
|
|
<div class="cal-cell<?= $cell['in_month'] ? '' : ' is-out' ?><?= $cell['is_today'] ? ' is-today' : '' ?><?= $canNew ? ' rt-writable' : '' ?>"
|
|
|
|
|
|
data-new-at="<?= e((string)$cell['date']) ?>">
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<div class="cal-dnum"><?= (int)$cell['day'] ?></div>
|
|
|
|
|
|
<div class="cal-evs">
|
2026-10-21 13:22:37 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/* 🔴 v70(用户 2026-10-20 第 2 条):「日历导入进去了,但是不显示」。
|
|
|
|
|
|
真根因不在这里的 3 条,而在**排序 + "+N 条"点不开**的组合:
|
|
|
|
|
|
每格只画 3 条,而旧排序把"全天"(时间空串)排最前 ⇒ 农历/节气/
|
|
|
|
|
|
节假日天天占满前三,用户自己导入的日程全被挤进 "+N 条",
|
|
|
|
|
|
而那个 "+N 条" 只是个 <span>,点不开 = 完全看不到。
|
|
|
|
|
|
两处都已修:① `Calendar::displayRank()` 让派生条目排最后;
|
|
|
|
|
|
② 下面把 "+N 条" 换成**真按钮**,点开当天的全部条目浮层。
|
|
|
|
|
|
⚠️ 保留 3 条不改成"全画":格子是固定高度(min-height:104px),
|
|
|
|
|
|
15 条硬塞会把月网格撑得不像月历;超出部分交给浮层看。 */
|
|
|
|
|
|
$cellEvents = $cell['events'];
|
|
|
|
|
|
$shown = 3;
|
|
|
|
|
|
$hiddenCnt = count($cellEvents) - $shown;
|
|
|
|
|
|
?>
|
|
|
|
|
|
<?php $n = 0; foreach ($cellEvents as $ev): $n++; ?>
|
|
|
|
|
|
<?php if ($n <= $shown): ?>
|
2026-10-12 00:24:20 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
$evKind = (string)($ev['kind'] ?? 'custom');
|
|
|
|
|
|
$evRef = (int)($ev['ref_id'] ?? 0);
|
|
|
|
|
|
$evEdit = '';
|
|
|
|
|
|
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
|
|
|
|
|
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
|
|
|
|
|
?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<a class="cal-ev<?= empty($ev['all_day']) ? '' : ' is-allday' ?>"
|
|
|
|
|
|
data-cal="<?= (int)$ev['cal'] ?>"
|
2026-10-12 00:24:20 +08:00
|
|
|
|
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
2026-10-02 09:28:07 +08:00
|
|
|
|
style="background: <?= e((string)$ev['color']) ?>;"
|
|
|
|
|
|
href="<?= e((string)$ev['href']) ?>"
|
|
|
|
|
|
title="<?= e((string)$ev['title']) ?>">
|
|
|
|
|
|
<?php if (($ev['time'] ?? '') !== ''): ?><b><?= e((string)$ev['time']) ?></b> <?php endif; ?>
|
|
|
|
|
|
<?= e((string)$ev['title']) ?>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<?php endforeach; ?>
|
2026-10-21 13:22:37 +08:00
|
|
|
|
<?php if ($hiddenCnt > 0): ?>
|
|
|
|
|
|
<?php /* 点开 = 当天全部条目(含被折叠的),一律走全站的条目详情浮层;
|
|
|
|
|
|
每条都带 data-fb-entry,所以在浮层里还能继续点进详情/编辑/删除。
|
|
|
|
|
|
⚠️ 用一个 `<a href>` 兜住无 JS 的情况:直接跳那天的日视图
|
|
|
|
|
|
(日视图按时间网格铺开,不会再截断)。 */ ?>
|
|
|
|
|
|
<button type="button" class="cal-more"
|
|
|
|
|
|
data-cal-day="<?= e((string)$cell['date']) ?>"
|
|
|
|
|
|
aria-label="看这一天全部 <?= count($cellEvents) ?> 条">+<?= $hiddenCnt ?> 条</button>
|
|
|
|
|
|
<?php /* 浮层用的全部条目数据(服务端渲染好,点开时不需要再请求)。
|
|
|
|
|
|
放在格子外面(display:none 的 script 里)—— 塞进格子会撑高月网格。 */ ?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<?php endif; ?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
(function () {
|
2026-10-03 15:01:38 +08:00
|
|
|
|
/* ---- 新建日程:按钮开合 + 「点日历空白处」预填开始时间 ----
|
|
|
|
|
|
⚠️ 上一版的「日历本显隐」JS 已经**整段删掉**:显隐改成服务端处理
|
|
|
|
|
|
(Calendar::hiddenCalendars,见路由里的说明),前端不再需要它。
|
|
|
|
|
|
留着会让人以为"改一下勾选就能切换",但那些勾选框已经不在这儿了。 */
|
|
|
|
|
|
var btn = document.getElementById('calAddBtn');
|
2026-10-06 12:08:40 +08:00
|
|
|
|
var card = document.getElementById('calAddCard'); /* 现在是一个浮层 */
|
|
|
|
|
|
var cancel = document.getElementById('calAddCancel'); /* 头部 ✕ */
|
|
|
|
|
|
var cancel2 = document.getElementById('calAddCancel2'); /* 底部「取消」 */
|
2026-10-02 09:28:07 +08:00
|
|
|
|
if (btn && card) {
|
2026-10-06 12:08:40 +08:00
|
|
|
|
btn.addEventListener('click', function () { openNew(null, null, null); });
|
2026-10-02 09:28:07 +08:00
|
|
|
|
}
|
2026-10-06 12:08:40 +08:00
|
|
|
|
/* 关:✕ / 取消 / 点遮罩 / Esc 四条路都要通(与站内其它浮层一致)。
|
|
|
|
|
|
⚠️ 点遮罩才关、点面板内不关 —— 用 e.target === card 判,不用"点哪儿都关"。 */
|
|
|
|
|
|
function closeNew() { if (card) { card.hidden = true; } }
|
|
|
|
|
|
if (cancel) { cancel.addEventListener('click', closeNew); }
|
|
|
|
|
|
if (cancel2) { cancel2.addEventListener('click', closeNew); }
|
|
|
|
|
|
if (card) {
|
|
|
|
|
|
card.addEventListener('click', function (e) { if (e.target === card) { closeNew(); } });
|
|
|
|
|
|
}
|
|
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
|
|
|
|
if (e.key === 'Escape' && card && !card.hidden) { closeNew(); }
|
|
|
|
|
|
});
|
2026-10-03 15:01:38 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---- 用户第 5 条:在日历里点一下 → 弹新建,开始时间 = 点的位置 ----
|
|
|
|
|
|
月视图:一格 = 一天 → 开始时间用那天 09:00("当日默认"比 00:00 更像个日程)。
|
|
|
|
|
|
周/日视图:按鼠标在**那一列里的纵向位置**反算时刻,最接近的半小时 ——
|
|
|
|
|
|
点在"下午三点那一段"就该得到 15:00,这是"点哪儿建哪儿"的全部意义。 */
|
2026-10-12 00:24:20 +08:00
|
|
|
|
var startField = document.querySelector('.ev-dt[data-dt]');
|
2026-10-03 15:01:38 +08:00
|
|
|
|
var startDate = startField ? startField.querySelector('input.dt-date') : null;
|
|
|
|
|
|
var startHour = startField ? startField.querySelector('select.dt-hour') : null;
|
|
|
|
|
|
var startMin = startField ? startField.querySelector('select.dt-minute') : null;
|
|
|
|
|
|
var startSec = startField ? startField.querySelector('select.dt-second') : null;
|
|
|
|
|
|
|
|
|
|
|
|
function setStart(dateStr, hh, mm) {
|
2026-10-15 10:37:07 +08:00
|
|
|
|
var H = (hh === null || hh === undefined || hh === '') ? '09' : String(hh);
|
|
|
|
|
|
var M = (mm === null || mm === undefined || mm === '') ? '00' : String(mm);
|
|
|
|
|
|
// ⚠️ 优先走全局接口:它会把原生字段与 Semi/antd 控件**一起**更新。
|
|
|
|
|
|
// 直接写 startDate.value 的话,非受控控件显示不会跟着变 ——
|
2026-10-04 10:46:50 +08:00
|
|
|
|
// 用户会看到"我点了 15:00,控件里还写着 09:00"(提交的却是 15:00)。
|
2026-10-15 10:37:07 +08:00
|
|
|
|
var okStart = (typeof window.fbSetDT === 'function'
|
|
|
|
|
|
&& window.fbSetDT('starts_at', dateStr + ' ' + H + ':' + M + ':00'));
|
|
|
|
|
|
if (!okStart) {
|
|
|
|
|
|
// 兜底:Semi/antd 还没加载完(或加载失败)→ 原生控件照旧能用
|
|
|
|
|
|
if (!startDate) return;
|
|
|
|
|
|
startDate.value = dateStr;
|
|
|
|
|
|
if (hh !== null && hh !== undefined && startHour) { startHour.value = hh; }
|
|
|
|
|
|
if (mm !== null && mm !== undefined && startMin) { startMin.value = mm; }
|
|
|
|
|
|
if (startSec) { startSec.value = '00'; }
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 🔴🔴 v51(用户 2026-10-15 第 2 条):「我们的时间范围**默认给它设 1 个小时**,
|
|
|
|
|
|
就是说你点了之后带进来的是 8 点钟,那么它结束时间应该是当天的 9 点钟,
|
|
|
|
|
|
就按照 1 个小时把结束时间也给它放上去。」
|
|
|
|
|
|
⇒ 带入开始时间时**同时**把结束设为「开始 + 1 小时」。
|
|
|
|
|
|
⚠️ 跨午夜要顺延到次日(23:30 点下去 → 次日 00:30),不能让结束时间
|
|
|
|
|
|
在日期上跑回"比开始还早的今天"(那种提交上去是负时长)。
|
|
|
|
|
|
⚠️ 结束时间的设置**只在这里**做(用户"点了某一处"这个动作)——
|
|
|
|
|
|
不要捆进 fbSetDT:那个接口也被"编辑已有日程"复用,
|
|
|
|
|
|
把它的结束时间也强改成 +1h 会把用户原本填的时长弄丢。 */
|
|
|
|
|
|
var st = new Date(dateStr + 'T' + H + ':' + M + ':00');
|
|
|
|
|
|
if (!isNaN(st.getTime())) {
|
|
|
|
|
|
st.setHours(st.getHours() + 1);
|
|
|
|
|
|
var pad2 = function (n) { return (n < 10 ? '0' : '') + n; };
|
|
|
|
|
|
var eDate = st.getFullYear() + '-' + pad2(st.getMonth() + 1) + '-' + pad2(st.getDate());
|
|
|
|
|
|
var eStr = eDate + ' ' + pad2(st.getHours()) + ':' + pad2(st.getMinutes()) + ':00';
|
|
|
|
|
|
if (typeof window.fbSetDT !== 'function' || !window.fbSetDT('ends_at', eStr)) {
|
|
|
|
|
|
/* 兜底:按 data-dt 里的 JSON `name` 找结束端点(**不能**用 `[data-dt*="ends_at"]`
|
|
|
|
|
|
—— 那个属性是一整段 JSON,里面含 `name` 键,朴素子串匹配能中但语义不对,
|
|
|
|
|
|
将来加个字段就误命中)。 */
|
|
|
|
|
|
var endField = null;
|
|
|
|
|
|
Array.prototype.forEach.call(document.querySelectorAll('.dt-input[data-dt]'), function (bx) {
|
|
|
|
|
|
if (endField) { return; }
|
|
|
|
|
|
var cfg = {};
|
|
|
|
|
|
try { cfg = JSON.parse(bx.getAttribute('data-dt') || '{}'); } catch (e) {}
|
|
|
|
|
|
if (cfg.name === 'ends_at') { endField = bx; }
|
|
|
|
|
|
});
|
|
|
|
|
|
if (endField) {
|
|
|
|
|
|
var ed = endField.querySelector('input.dt-date');
|
|
|
|
|
|
var eh = endField.querySelector('select.dt-hour');
|
|
|
|
|
|
var em = endField.querySelector('select.dt-minute');
|
|
|
|
|
|
if (ed) { ed.value = eDate; }
|
|
|
|
|
|
if (eh) { eh.value = pad2(st.getHours()); }
|
|
|
|
|
|
if (em) { em.value = pad2(st.getMinutes()); }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-04 10:46:50 +08:00
|
|
|
|
}
|
2026-10-03 15:01:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-06 12:08:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 打开新建日程的**弹框**(v24 第 21 批 第 7 条)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 不再 `scrollIntoView` —— 那是"内联卡片"时代为了让用户找到卡片才需要的动作;
|
|
|
|
|
|
* 浮层固定在视口中央,底下的日历**一动不动**(这正是用户要的
|
|
|
|
|
|
* "不影响当前日历的显示")。
|
|
|
|
|
|
* ⚠️ dateStr 传 null 表示"从按钮进来":那时**不要再覆盖**已填的时间
|
|
|
|
|
|
* (用户可能刚填完改主意关掉、又点开;重置会把他填的东西抹掉)。
|
|
|
|
|
|
* 只有"点日历某一格/某一列"才带着具体时间进来,那次才 setStart。
|
|
|
|
|
|
*/
|
2026-10-03 15:01:38 +08:00
|
|
|
|
function openNew(dateStr, hh, mm) {
|
|
|
|
|
|
if (!card) return;
|
|
|
|
|
|
card.hidden = false;
|
2026-10-06 12:08:40 +08:00
|
|
|
|
if (dateStr) { setStart(dateStr, hh, mm); }
|
|
|
|
|
|
// 标题是第一个输入框,直接把光标放过去 —— 打开就能打字
|
2026-10-03 15:01:38 +08:00
|
|
|
|
var t = document.getElementById('ev-title');
|
|
|
|
|
|
if (t) { t.focus(); }
|
|
|
|
|
|
}
|
2026-10-21 13:22:37 +08:00
|
|
|
|
/* v70:「当天全部条目」浮层里有「在这天加一条」,也要走同一个 openNew
|
|
|
|
|
|
(它在该 IIFE 外面,看不到闭包里的 openNew)⇒ 暴露一个窗口入口。 */
|
|
|
|
|
|
window.fbCalOpenNew = openNew;
|
2026-10-03 15:01:38 +08:00
|
|
|
|
|
|
|
|
|
|
/* 月视图格子 */
|
|
|
|
|
|
Array.prototype.forEach.call(document.querySelectorAll('.cal-cell[data-new-at]'), function (cell) {
|
|
|
|
|
|
cell.addEventListener('click', function (ev) {
|
|
|
|
|
|
// 点在小条上 = 想打开那条日程(<a> 的默认行为),不要抢
|
|
|
|
|
|
if (ev.target.closest && ev.target.closest('.cal-ev')) { return; }
|
2026-10-21 13:22:37 +08:00
|
|
|
|
/* 🔴 v70:点「+N 条」= 要看**当天的全部条目**,不是要新建日程。
|
|
|
|
|
|
⚠️ 必须显式排除:它是个 <button>,不排除的话会**同时**触发
|
|
|
|
|
|
"新建日程浮层" 与 "当天全部条目浮层" 两个浮层叠在一起。 */
|
|
|
|
|
|
if (ev.target.closest && ev.target.closest('.cal-more')) { return; }
|
2026-10-03 15:01:38 +08:00
|
|
|
|
if (!<?= $canWrite && $writable ? 'true' : 'false' ?>) { return; }
|
|
|
|
|
|
openNew(cell.getAttribute('data-new-at'), '09', '00');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/* 周/日视图的列:纵向位置 → 时刻。
|
|
|
|
|
|
列高 1056px = 24 小时(见 .tg-col 的 CSS),所以 1 分钟 = 1056/1440 px。 */
|
|
|
|
|
|
Array.prototype.forEach.call(document.querySelectorAll('.tg-col[data-new-at]'), function (col) {
|
|
|
|
|
|
col.addEventListener('click', function (ev) {
|
|
|
|
|
|
if (ev.target.closest && ev.target.closest('.tg-block')) { return; }
|
|
|
|
|
|
if (!<?= $canWrite && $writable ? 'true' : 'false' ?>) { return; }
|
|
|
|
|
|
var box = col.getBoundingClientRect();
|
|
|
|
|
|
var ratio = (ev.clientY - box.top) / Math.max(1, box.height);
|
|
|
|
|
|
var mins = Math.max(0, Math.min(24 * 60 - 30, Math.round(ratio * 1440 / 30) * 30));
|
|
|
|
|
|
var hh = String(Math.floor(mins / 60)).padStart(2, '0');
|
|
|
|
|
|
var mm = String(mins % 60).padStart(2, '0');
|
|
|
|
|
|
openNew(col.getAttribute('data-new-at'), hh, mm);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/* 从 ?new_at= 进来(比如别处链过来)→ 直接开弹窗并预填 */
|
|
|
|
|
|
var preset = <?= json_encode($newAt, JSON_UNESCAPED_UNICODE) ?>;
|
|
|
|
|
|
if (preset) {
|
|
|
|
|
|
var parts = String(preset).split(/[ T]/);
|
|
|
|
|
|
if (parts[0]) {
|
|
|
|
|
|
var hm = (parts[1] || '').split(':');
|
|
|
|
|
|
openNew(parts[0], hm[0] || '09', hm[1] || '00');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-06 12:08:40 +08:00
|
|
|
|
/* ============================================================
|
|
|
|
|
|
v24 第 21 批 第 7 条:日历本色块 + **无刷新提交 + 动态改当前视图**
|
|
|
|
|
|
------------------------------------------------------------
|
|
|
|
|
|
用户原话:「添加完成后,动态修改当前视图,不要刷新。」
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 做法:拦掉表单提交 → fetch POST(带 X-Requested-With,服务端据此回 JSON)
|
|
|
|
|
|
→ 拿到新日程的 {starts_at, ends_at, title, all_day, color, href, cal} →
|
|
|
|
|
|
**直接往当前视图里插入那一条**(月视图找对应格子塞小条、周/日视图按时间
|
|
|
|
|
|
算 top/height 插块),然后关掉弹框。整个过程中页面不重载、滚动位置不动。
|
|
|
|
|
|
⚠️ 任何一步不成立(拿不到 JSON / 字段缺 / 当前视图不好插)→ **退回原生提交**。
|
|
|
|
|
|
"插入失败还假装成功"是最坏的结果:用户以为加上了、切到别的月份才发现没有。
|
|
|
|
|
|
⚠️ 插入用的定位方式必须与**服务端渲染的完全一致**(月视图按 data-new-at 找格子、
|
|
|
|
|
|
周/日视图按"24h = 100%"算 top/height),否则刷新一次小条就会跳位置。
|
|
|
|
|
|
这里刻意照抄模板里的选择器与算式,别另发明一套。
|
|
|
|
|
|
============================================================ */
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
var calDot = document.getElementById('ce-cal-dot');
|
|
|
|
|
|
var calSel = document.getElementById('ce-cal');
|
|
|
|
|
|
if (calDot && calSel) {
|
|
|
|
|
|
var syncDot = function () {
|
|
|
|
|
|
var o = calSel.options[calSel.selectedIndex];
|
|
|
|
|
|
var c = o ? o.getAttribute('data-color') : '';
|
|
|
|
|
|
if (c) { calDot.style.background = c; }
|
|
|
|
|
|
};
|
|
|
|
|
|
calSel.addEventListener('change', syncDot);
|
|
|
|
|
|
syncDot();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-25 14:52:43 +08:00
|
|
|
|
/* 🔴🔴 v93(用户 2026-10-25):日历本自绘下拉的整套交互**已抽到**
|
|
|
|
|
|
`assets/js/cs_picker.js`(全站加载,按容器上的 `data-cs-picker` 初始化)。
|
2026-10-12 15:31:03 +08:00
|
|
|
|
|
2026-10-25 14:52:43 +08:00
|
|
|
|
为什么必须抽走:首页「今天的日程」卡片那颗「新建日程」用的是**与本页同一份**
|
|
|
|
|
|
模板块(`templates/calendar/_add_modal.php`),HTML 一模一样;
|
|
|
|
|
|
交互却只写在下面这一段里 ⇒ 首页那张弹框的下拉**点了不展开**,
|
|
|
|
|
|
用户看到的就是"日历本拉不下来,只有一个『工作』"。
|
2026-10-12 15:31:03 +08:00
|
|
|
|
|
2026-10-25 14:52:43 +08:00
|
|
|
|
⚠️ 这里只留一句兜底调用。正常情况下通用脚本在 DOM ready 时已经扫过
|
|
|
|
|
|
(那时元素在、只是被 `hidden` 藏着,不影响绑定),这里什么都绑不上 ——
|
|
|
|
|
|
它是幂等的,绑过一次的容器自带 `data-cs-ready`,不会被绑第二遍。
|
|
|
|
|
|
⚠️ **不要**把交互搬回这里"顺便兼容一下":搬回来就等于又只有本页能用。 */
|
|
|
|
|
|
if (typeof window.fbInitCsPicker === 'function') { window.fbInitCsPicker(card || document); }
|
2026-10-12 15:31:03 +08:00
|
|
|
|
|
2026-10-06 12:08:40 +08:00
|
|
|
|
var form = document.getElementById('calAddForm');
|
|
|
|
|
|
var tip = document.getElementById('calAddTip');
|
|
|
|
|
|
if (!form || !card || !window.fetch) { return; }
|
|
|
|
|
|
|
|
|
|
|
|
function sayErr(msg) {
|
|
|
|
|
|
if (tip) { tip.textContent = msg; tip.className = 'cal-modal-tip err'; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-21 13:22:37 +08:00
|
|
|
|
/** 把一条 `fb_entry_json` 形态的条目补进 `#calDayData`(按天分组,幂等)。
|
|
|
|
|
|
* ⚠️ 只补**还没在里面**的那条(同 ref + 同 occ 视为同一条)——
|
|
|
|
|
|
* 否则连点两次「加上」或前端重放会把同一条塞两遍,清单里出现两条一样的。 */
|
|
|
|
|
|
function pushDayData(day, entry) {
|
|
|
|
|
|
var host = document.getElementById('calDayData');
|
|
|
|
|
|
if (!host || !day || !entry) { return; }
|
|
|
|
|
|
var all;
|
|
|
|
|
|
try { all = JSON.parse(host.textContent || '{}') || {}; } catch (e) { return; }
|
|
|
|
|
|
var list = all[day] || [];
|
|
|
|
|
|
var same = false;
|
|
|
|
|
|
for (var i = 0; i < list.length; i++) {
|
|
|
|
|
|
if (String(list[i].ref || '') === String(entry.ref || '')
|
|
|
|
|
|
&& String(list[i].occ || '') === String(entry.occ || '')
|
|
|
|
|
|
&& String(list[i].title || '') === String(entry.title || '')) { same = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!same) {
|
|
|
|
|
|
/* 位置按服务端那套 `displayRank + 时间` 插(派生条目垫底),
|
|
|
|
|
|
免得清单里的顺序与刷新后的顺序不一致。 */
|
|
|
|
|
|
var rk = function (o) {
|
|
|
|
|
|
var s = String(o.source || '');
|
|
|
|
|
|
return s === 'event' ? 0 : 4; /* 派生条目一律 4,与服务端 displayRank 的兜底一致 */
|
|
|
|
|
|
};
|
|
|
|
|
|
var tm = function (o) {
|
|
|
|
|
|
var rows = o.rows || [];
|
|
|
|
|
|
for (var k = 0; k < rows.length; k++) {
|
|
|
|
|
|
if (rows[k][0] === '时间') {
|
|
|
|
|
|
var v = String(rows[k][1] || '');
|
|
|
|
|
|
/* "YYYY-MM-DD HH:MM–HH:MM" / "YYYY-MM-DD 全天" → 取时刻部分比较 */
|
|
|
|
|
|
var seg = v.slice(day.length).trim();
|
|
|
|
|
|
return seg === '全天' ? '' : seg.slice(0, 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
};
|
|
|
|
|
|
var pos = list.length;
|
|
|
|
|
|
for (var j = 0; j < list.length; j++) {
|
|
|
|
|
|
if (rk(list[j]) > rk(entry)) { pos = j; break; }
|
|
|
|
|
|
if (rk(list[j]) === rk(entry) && tm(list[j]) > tm(entry)) { pos = j; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
list.splice(pos, 0, entry);
|
|
|
|
|
|
all[day] = list;
|
|
|
|
|
|
try { host.textContent = JSON.stringify(all); } catch (e2) { /* 序列化失败就别动 */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-06 12:08:40 +08:00
|
|
|
|
/** 月视图:往 data-new-at="YYYY-MM-DD" 那个格子里塞一条小条 */
|
|
|
|
|
|
function putMonth(ev) {
|
|
|
|
|
|
var day = String(ev.starts_at || '').slice(0, 10);
|
|
|
|
|
|
var cell = document.querySelector('.cal-cell[data-new-at="' + day + '"]');
|
|
|
|
|
|
if (!cell) { return false; }
|
|
|
|
|
|
var box = cell.querySelector('.cal-evs');
|
|
|
|
|
|
if (!box) { return false; }
|
|
|
|
|
|
var a = document.createElement('a');
|
|
|
|
|
|
a.className = 'cal-ev' + (ev.all_day ? ' is-allday' : '');
|
|
|
|
|
|
a.setAttribute('data-cal', String(ev.cal || ''));
|
|
|
|
|
|
a.style.background = ev.color || '';
|
|
|
|
|
|
a.setAttribute('href', ev.href || '#');
|
|
|
|
|
|
a.setAttribute('title', ev.title || '');
|
|
|
|
|
|
/* ⚠️ 时间前缀的形状必须与服务端一致(<b>HH:MM</b> 后面一个空格)——
|
|
|
|
|
|
不一致的话刷新一次就会看出差别。 */
|
|
|
|
|
|
if (ev.time) {
|
|
|
|
|
|
var b = document.createElement('b');
|
|
|
|
|
|
b.textContent = ev.time;
|
|
|
|
|
|
a.appendChild(b);
|
|
|
|
|
|
a.appendChild(document.createTextNode(' '));
|
|
|
|
|
|
}
|
|
|
|
|
|
a.appendChild(document.createTextNode(ev.title || ''));
|
|
|
|
|
|
/* 🔴 插到**正确的时间位置**,不能 append 到末尾。
|
|
|
|
|
|
为什么必须按时间插:服务端只渲染前 3 条(第 4 条起收进"+N 条"),
|
|
|
|
|
|
而"哪 3 条"是按**时间顺序**取的。若这边一律 append:
|
|
|
|
|
|
· 新加一条**早上 7 点**的日程,它会被排到最后 → 落到"+N 条"里看不见,
|
|
|
|
|
|
可刷新一次又会跑到第一位 —— 这正是"插进去和刷新后长得不一样"。
|
|
|
|
|
|
⚠️ 判据与服务端渲染同一套:先比 <b>HH:MM</b>,没有时间前缀的(全天)排前面
|
|
|
|
|
|
(与 Calendar::month 给 all_day 的处理一致),再比标题兜底稳定性。 */
|
|
|
|
|
|
var newTime = ev.time || '';
|
|
|
|
|
|
var placed = false;
|
|
|
|
|
|
var existing = Array.prototype.slice.call(box.querySelectorAll('.cal-ev'));
|
|
|
|
|
|
for (var k = 0; k < existing.length; k++) {
|
|
|
|
|
|
var tEl = existing[k].querySelector('b');
|
|
|
|
|
|
var tTxt = tEl ? tEl.textContent.trim() : '';
|
|
|
|
|
|
if (tTxt === '' && newTime !== '') { continue; } /* 空时间(全天)在前 */
|
|
|
|
|
|
if (tTxt !== '' && newTime === '') { box.insertBefore(a, existing[k]); placed = true; break; }
|
|
|
|
|
|
if (tTxt > newTime) { box.insertBefore(a, existing[k]); placed = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!placed) { box.appendChild(a); }
|
|
|
|
|
|
|
|
|
|
|
|
/* 再按"只显示前 3 条"收口,与服务端渲染对齐(否则会多出一条)。 */
|
|
|
|
|
|
var links = box.querySelectorAll('.cal-ev');
|
|
|
|
|
|
var more = box.querySelector('.cal-more');
|
2026-10-21 13:22:37 +08:00
|
|
|
|
/* 🔴 v70:新条目同步进 `#calDayData` —— 否则「+N 条」点开是**旧**清单,
|
|
|
|
|
|
刚建的这条不在里面(刷新一次又有了)。放在收口之前做,跟服务端渲染时
|
|
|
|
|
|
"先把当天全部条目收进数据岛、再只画前 3 条"的顺序一致。 */
|
|
|
|
|
|
if (ev.entry) { pushDayData(day, ev.entry); }
|
2026-10-06 12:08:40 +08:00
|
|
|
|
for (var j = 0; j < links.length; j++) {
|
|
|
|
|
|
links[j].style.display = (j < 3) ? '' : 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (links.length > 3) {
|
|
|
|
|
|
var extra = links.length - 3;
|
|
|
|
|
|
if (!more) {
|
2026-10-21 13:22:37 +08:00
|
|
|
|
/* 🔴 v70(用户 2026-10-20 第 2 条):「+N 条」原来是颗 `<span>`,
|
|
|
|
|
|
**点不动** ⇒ 被它折叠掉的条目永远看不到("导进去了但不显示")。
|
|
|
|
|
|
这里必须造**按钮**并带 `data-cal-day`(与模板渲染同形),
|
|
|
|
|
|
否则新建一条之后,这个格子就退回"点不动"。 */
|
|
|
|
|
|
more = document.createElement('button');
|
|
|
|
|
|
more.type = 'button';
|
2026-10-06 12:08:40 +08:00
|
|
|
|
more.className = 'cal-more';
|
|
|
|
|
|
box.appendChild(more);
|
|
|
|
|
|
}
|
2026-10-21 13:22:37 +08:00
|
|
|
|
more.setAttribute('data-cal-day', day);
|
|
|
|
|
|
more.setAttribute('aria-label', '看这一天全部 ' + links.length + ' 条');
|
2026-10-06 12:08:40 +08:00
|
|
|
|
more.textContent = '+' + extra + ' 条';
|
|
|
|
|
|
} else if (more) {
|
|
|
|
|
|
more.parentNode.removeChild(more);
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-15 10:37:07 +08:00
|
|
|
|
/* 🔴🔴 v51(用户 2026-10-15 第 3 条):「添加一个日程之后,只是把这个日程放在了
|
|
|
|
|
|
周视图或者是日视图上面。如果它这个时间段已经有日程了,它会**直接盖在上面**,
|
|
|
|
|
|
却并没有按照**分栏**的方式左右显示。这个需要再调整一下。」
|
|
|
|
|
|
|
|
|
|
|
|
真根因:`putTimed()` 只把自己那条色块按 top/height **整宽铺上去**,
|
|
|
|
|
|
完全不知道同一列里已有的日程 —— 于是两条重叠时后加的直接压在先有的上面
|
|
|
|
|
|
(服务端渲染时是有分栏的:`App\Calendar::conflictLanes`,但那是**整页重绘**时才算的)。
|
|
|
|
|
|
|
|
|
|
|
|
⇒ 修法:**在浏览器里重算整列的分栏**(与服务端同一套算法),插入后重排该列。
|
|
|
|
|
|
为什么"重算整列"而不是"给新块腾个位置":
|
|
|
|
|
|
新日程可能同时与**多条**已有日程冲突(比如插进一个 3 段重叠的时段),
|
|
|
|
|
|
只挪新块会让已有多条的宽度/位置也不对(它们现在是 2 列、重算后该是 3 列)
|
|
|
|
|
|
—— 必须让整列一起重排才是自洽的。
|
|
|
|
|
|
⚠️ 算法**照抄** app/Calendar.php 的 conflictLanes(贪心分列 + 分组复列):
|
|
|
|
|
|
两边算错一个边界,刷新一次位置就会跳。
|
|
|
|
|
|
⚠️ 高度仍按各自"声明的起止"算(没结束的用 +30′)—— 与服务端那套一致。
|
|
|
|
|
|
(服务端在"没填结束"时还会拿同列下一条的起点当视觉结束;这里简化成 +30′,
|
|
|
|
|
|
差别只在"未填结束"这一种情况,且插入后用户一刷新就归位。 */
|
|
|
|
|
|
function minsAttr(el, attr) {
|
|
|
|
|
|
var v = el.getAttribute(attr);
|
|
|
|
|
|
if (v === null || v === '') { return null; }
|
|
|
|
|
|
var n = parseInt(v, 10);
|
|
|
|
|
|
return isNaN(n) ? null : n;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 重算某一列里**所有** .tg-block 的分栏(left/width)与 top/height */
|
|
|
|
|
|
function relayoutColumn(col) {
|
|
|
|
|
|
var blocks = Array.prototype.slice.call(col.querySelectorAll('.tg-block'));
|
|
|
|
|
|
if (!blocks.length) { return; }
|
|
|
|
|
|
// ① 归一成 [{el, s, e}](缺 data-e → +30′;缺 data-s → 从 top 反推兜底)
|
|
|
|
|
|
var rows = [];
|
|
|
|
|
|
blocks.forEach(function (el) {
|
|
|
|
|
|
var s = minsAttr(el, 'data-s');
|
|
|
|
|
|
if (s === null) {
|
|
|
|
|
|
var tp = parseFloat(el.style.top) || 0;
|
|
|
|
|
|
s = Math.round(tp / 100 * 1440);
|
|
|
|
|
|
}
|
|
|
|
|
|
var e = minsAttr(el, 'data-e');
|
|
|
|
|
|
if (e === null || e <= s) { e = s + 30; }
|
|
|
|
|
|
rows.push({ el: el, s: s, e: Math.min(e, 1440) });
|
|
|
|
|
|
});
|
|
|
|
|
|
// ② 按开始时间升序(同开始则长的在前)—— 与服务端 usort 同序
|
|
|
|
|
|
rows.sort(function (a, b) { return a.s - b.s || b.e - a.e; });
|
|
|
|
|
|
// ③ 划分冲突组(与组内最晚结束仍重叠就并组)
|
|
|
|
|
|
var groups = [], cur = [], endMax = -1;
|
|
|
|
|
|
rows.forEach(function (r) {
|
|
|
|
|
|
if (cur.length && r.s >= endMax) { groups.push(cur); cur = []; endMax = -1; }
|
|
|
|
|
|
cur.push(r);
|
|
|
|
|
|
endMax = Math.max(endMax, r.e);
|
|
|
|
|
|
});
|
|
|
|
|
|
if (cur.length) { groups.push(cur); }
|
|
|
|
|
|
// ④ 组内贪心分列(每列记最后结束分钟,放进编号最小的可用列)
|
|
|
|
|
|
groups.forEach(function (g) {
|
|
|
|
|
|
var laneEnd = [], laneOf = [];
|
|
|
|
|
|
g.forEach(function (r, k) {
|
|
|
|
|
|
var lane = -1;
|
|
|
|
|
|
for (var li = 0; li < laneEnd.length; li++) {
|
|
|
|
|
|
if (laneEnd[li] <= r.s) { lane = li; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (lane < 0) { lane = laneEnd.length; laneEnd.push(r.e); }
|
|
|
|
|
|
else { laneEnd[lane] = r.e; }
|
|
|
|
|
|
laneOf[k] = lane;
|
|
|
|
|
|
});
|
|
|
|
|
|
var cols = Math.max(1, laneEnd.length);
|
|
|
|
|
|
g.forEach(function (r, k) {
|
|
|
|
|
|
var left = laneOf[k] / cols * 100, width = 100 / cols;
|
|
|
|
|
|
r.el.style.left = 'calc(2px + ' + left.toFixed(4) + '%)';
|
|
|
|
|
|
r.el.style.width = 'calc(' + width.toFixed(4) + '% - 4px)';
|
|
|
|
|
|
r.el.style.top = (r.s / 1440 * 100).toFixed(4) + '%';
|
|
|
|
|
|
r.el.style.height = ((Math.max(15, r.e - r.s)) / 1440 * 100).toFixed(4) + '%';
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-06 12:08:40 +08:00
|
|
|
|
/** 周/日视图:按"列高 = 24 小时"算 top/height 插时间块 */
|
|
|
|
|
|
function putTimed(ev) {
|
|
|
|
|
|
if (ev.all_day) {
|
|
|
|
|
|
var ad = document.querySelector('.tg-adcell');
|
|
|
|
|
|
if (!ad) { return false; }
|
|
|
|
|
|
var box2 = document.querySelector('.tg-allday');
|
|
|
|
|
|
var col = null;
|
|
|
|
|
|
if (box2) {
|
|
|
|
|
|
/* 找到"那一天"那一列:列的顺序与 .tg-dayname 一致,
|
|
|
|
|
|
用 data-new-at 反查(服务端给每列写了当天日期)。 */
|
|
|
|
|
|
var cols = Array.prototype.slice.call(document.querySelectorAll('.tg-col[data-new-at]'));
|
|
|
|
|
|
var day2 = String(ev.starts_at || '').slice(0, 10);
|
|
|
|
|
|
var idx = -1;
|
|
|
|
|
|
cols.forEach(function (c, i) { if (c.getAttribute('data-new-at') === day2) { idx = i; } });
|
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
|
var cells = document.querySelectorAll('.tg-adcell');
|
|
|
|
|
|
col = cells[idx] || null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
var host = col || ad;
|
|
|
|
|
|
if (!host) { return false; }
|
|
|
|
|
|
var a2 = document.createElement('a');
|
|
|
|
|
|
a2.className = 'cal-ev is-allday';
|
|
|
|
|
|
a2.setAttribute('data-cal', String(ev.cal || ''));
|
|
|
|
|
|
a2.style.background = ev.color || '';
|
|
|
|
|
|
a2.setAttribute('href', ev.href || '#');
|
|
|
|
|
|
a2.setAttribute('title', ev.title || '');
|
|
|
|
|
|
a2.textContent = ev.title || '';
|
|
|
|
|
|
host.appendChild(a2);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
var cols3 = Array.prototype.slice.call(document.querySelectorAll('.tg-col[data-new-at]'));
|
|
|
|
|
|
var day3 = String(ev.starts_at || '').slice(0, 10);
|
|
|
|
|
|
var ci = -1;
|
|
|
|
|
|
cols3.forEach(function (c, i) { if (c.getAttribute('data-new-at') === day3) { ci = i; } });
|
|
|
|
|
|
if (ci < 0) { return false; }
|
|
|
|
|
|
var target = cols3[ci];
|
|
|
|
|
|
var minsOf = function (t) {
|
|
|
|
|
|
var m = String(t || '').match(/[ T](\d{1,2}):(\d{1,2})/);
|
|
|
|
|
|
return m ? (+m[1]) * 60 + (+m[2]) : 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
var st = minsOf(ev.starts_at), en = minsOf(ev.ends_at);
|
|
|
|
|
|
if (en <= st) { en = st + 30; } /* 没填结束就给半小时 —— 与服务端一致 */
|
|
|
|
|
|
var top = (st / 1440) * 100, hgt = ((en - st) / 1440) * 100;
|
|
|
|
|
|
var blk = document.createElement('a');
|
|
|
|
|
|
blk.className = 'tg-block';
|
|
|
|
|
|
blk.setAttribute('data-cal', String(ev.cal || ''));
|
2026-10-15 10:37:07 +08:00
|
|
|
|
/* 🔴 v51:带上"声明的起止分钟",重算分栏要用(服务端渲染的同名属性)。
|
|
|
|
|
|
⚠️ 与服务端口径一致:结束缺失/倒挂 → 不带 data-e(按 +30′ 处理)。 */
|
|
|
|
|
|
blk.setAttribute('data-s', String(st));
|
|
|
|
|
|
var declE = minsOf(ev.ends_at);
|
|
|
|
|
|
if (declE > st) { blk.setAttribute('data-e', String(declE)); }
|
2026-10-06 12:08:40 +08:00
|
|
|
|
blk.style.background = ev.color || '';
|
|
|
|
|
|
blk.style.top = top.toFixed(4) + '%';
|
|
|
|
|
|
blk.style.height = hgt.toFixed(4) + '%';
|
|
|
|
|
|
blk.setAttribute('href', ev.href || '#');
|
|
|
|
|
|
blk.setAttribute('title', ev.title || '');
|
|
|
|
|
|
blk.textContent = ev.title || '';
|
|
|
|
|
|
target.appendChild(blk);
|
2026-10-15 10:37:07 +08:00
|
|
|
|
/* 🔴 v51(第 3 条):插完**重排整列** —— 否则这条会整宽盖在已有日程上(用户报的现象)。 */
|
|
|
|
|
|
relayoutColumn(target);
|
2026-10-06 12:08:40 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function insert(ev) {
|
|
|
|
|
|
/* 先看当前视图是哪一种:页面上有 .cal-grid 就是月视图;
|
|
|
|
|
|
有 .tg-col 就是周/日视图。两者都不在(理论上不会)→ 退回刷新。 */
|
|
|
|
|
|
if (document.querySelector('.cal-grid')) { return putMonth(ev); }
|
|
|
|
|
|
if (document.querySelector('.tg-col[data-new-at]')) { return putTimed(ev); }
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
form.addEventListener('submit', function (e) {
|
|
|
|
|
|
if (form.getAttribute('data-native-submit') === '1') { return; }
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
var sub = document.getElementById('calAddSubmit');
|
|
|
|
|
|
if (sub) { sub.disabled = true; }
|
|
|
|
|
|
if (tip) { tip.textContent = ''; tip.className = 'cal-modal-tip'; }
|
|
|
|
|
|
fetch(form.getAttribute('action'), {
|
|
|
|
|
|
method: 'POST', body: new FormData(form), credentials: 'same-origin',
|
|
|
|
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest',
|
|
|
|
|
|
'Accept': 'application/json' }
|
|
|
|
|
|
}).then(function (r) { return r.json().catch(function () { return null; }); })
|
|
|
|
|
|
.then(function (d) {
|
|
|
|
|
|
if (sub) { sub.disabled = false; }
|
|
|
|
|
|
if (d && d.ok && d.event) {
|
|
|
|
|
|
var ok = false;
|
|
|
|
|
|
try { ok = insert(d.event); } catch (err) { ok = false; }
|
|
|
|
|
|
if (ok) {
|
|
|
|
|
|
/* 观影 / 旅行本是**派生**的(内容来自观影/旅程表):AJAX 时服务端
|
|
|
|
|
|
不再自动跳详情页(跳走就离开了日历),改在提示行给一个链接。 */
|
|
|
|
|
|
if (d.goto && tip) {
|
|
|
|
|
|
tip.className = 'cal-modal-tip';
|
|
|
|
|
|
tip.textContent = (d.msg || '已加上。') + ' ';
|
|
|
|
|
|
var ga = document.createElement('a');
|
|
|
|
|
|
ga.href = d.goto;
|
|
|
|
|
|
ga.textContent = '去看详情 →';
|
|
|
|
|
|
tip.appendChild(ga);
|
|
|
|
|
|
setTimeout(function () {
|
|
|
|
|
|
if (tip) { tip.textContent = ''; }
|
|
|
|
|
|
}, 6000);
|
|
|
|
|
|
}
|
|
|
|
|
|
closeNew();
|
|
|
|
|
|
/* 重置"只填一次的字段",但**保留**日历本的选择(用户往往连加几条)。
|
|
|
|
|
|
⚠️ 标题必须清掉,否则下一条会带着上一条的标题(很容易漏掉)。 */
|
|
|
|
|
|
var t = document.getElementById('ev-title');
|
|
|
|
|
|
var lo = document.getElementById('ev-location');
|
|
|
|
|
|
var no = document.getElementById('ev-note');
|
|
|
|
|
|
if (t) { t.value = ''; }
|
|
|
|
|
|
if (lo) { lo.value = ''; }
|
|
|
|
|
|
if (no) { no.value = ''; }
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 插入不成功 = "没刷新却也没看到" → 宁可刷新一次,别假装成功 */
|
|
|
|
|
|
form.setAttribute('data-native-submit', '1');
|
|
|
|
|
|
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
|
|
|
|
|
else { form.submit(); }
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d && d.msg) { sayErr(d.msg); return; }
|
|
|
|
|
|
/* 拿不到 JSON:会话过期 / 500 → 退回原生提交,让服务端说话 */
|
|
|
|
|
|
form.setAttribute('data-native-submit', '1');
|
|
|
|
|
|
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
|
|
|
|
|
else { form.submit(); }
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(function () {
|
|
|
|
|
|
if (sub) { sub.disabled = false; }
|
|
|
|
|
|
form.setAttribute('data-native-submit', '1');
|
|
|
|
|
|
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
|
|
|
|
|
else { form.submit(); }
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
2026-10-03 08:18:05 +08:00
|
|
|
|
/* 「全天」勾上藏掉时刻那一段的逻辑在 _event_fields.php 里(两个页面共用一份) */
|
2026-10-02 09:28:07 +08:00
|
|
|
|
})();
|
2026-10-04 10:46:50 +08:00
|
|
|
|
|
2026-10-05 10:58:17 +08:00
|
|
|
|
/* ---- v24 第 14 批 #1:「跳到某一天」—— 图标按钮 + **按钮下方自绘的下拉日历** ----
|
|
|
|
|
|
原话(两条,都要满足):
|
|
|
|
|
|
· 「选择这个日期,不用弹窗,直接把日历选择,弹出在这个按钮下面就可以了」
|
|
|
|
|
|
· 「我点击上一个月,页面直接跳转,我还没选择日期呢」(严重 bug)
|
|
|
|
|
|
|
|
|
|
|
|
🔴 为什么**不用** Semi 的 DatePicker(上一版就是它):
|
|
|
|
|
|
Semi 的日历面板渲染在 **body 的 portal** 里(不在我们的下拉容器内),
|
|
|
|
|
|
而且它把"翻月/翻年"与"选中某天"混在同一个 onChange 回调里 ——
|
|
|
|
|
|
于是点"上一月"也被当成"选中了这一天",页面立刻提交跳转。
|
|
|
|
|
|
第三方控件的这个语义我们改不了;而这个交互又是"翻月不跳、点天才跳",
|
|
|
|
|
|
必须 100% 由我们自己定。所以这里自绘:
|
|
|
|
|
|
· « ‹ 年/月 › » → **只重绘面板**,绝不提交;
|
|
|
|
|
|
· 点某一天 → 立刻提交跳转(保留当前视图);
|
|
|
|
|
|
· 底部「跳到这天」→ 明确的确认入口(键盘/读屏用户也能用)。
|
|
|
|
|
|
⚠️ jump() 提交时带上当前 view(月的 ym / 周、日的 d),
|
2026-10-04 19:43:00 +08:00
|
|
|
|
由服务端决定该视图下这个日期落在哪一屏 —— 见 index.php 的 calendar 路由。 */
|
2026-10-04 10:46:50 +08:00
|
|
|
|
(function () {
|
2026-10-04 17:42:21 +08:00
|
|
|
|
var form = document.querySelector('.cal-jump');
|
|
|
|
|
|
var btn = document.getElementById('calJumpBtn');
|
|
|
|
|
|
var hid = document.getElementById('calJumpD');
|
2026-10-04 19:43:00 +08:00
|
|
|
|
var drop = document.getElementById('calJumpDrop');
|
2026-10-04 17:42:21 +08:00
|
|
|
|
if (!form || !btn || !hid) { return; }
|
|
|
|
|
|
|
2026-10-05 10:58:17 +08:00
|
|
|
|
var WD = ['一', '二', '三', '四', '五', '六', '日']; // 与月视图一致:周一起
|
|
|
|
|
|
var curY = 0, curM = 0; // 面板当前显示的年月
|
|
|
|
|
|
var sel = ''; // 已选日期('Y-m-d')
|
|
|
|
|
|
|
|
|
|
|
|
function ymd(y, m, d) {
|
|
|
|
|
|
return y + '-' + String(m).padStart(2, '0') + '-' + String(d).padStart(2, '0');
|
|
|
|
|
|
}
|
2026-10-04 17:42:21 +08:00
|
|
|
|
function jump(v) {
|
|
|
|
|
|
if (!v) { return; }
|
|
|
|
|
|
hid.value = v;
|
2026-10-04 19:43:00 +08:00
|
|
|
|
// 月视图还要带上 ym(服务端按月取数),否则会跳回默认月
|
|
|
|
|
|
var ym = document.getElementById('calJumpYM');
|
|
|
|
|
|
if (ym) { ym.value = String(v).slice(0, 7); }
|
2026-10-04 17:42:21 +08:00
|
|
|
|
form.submit();
|
2026-10-04 10:46:50 +08:00
|
|
|
|
}
|
2026-10-04 19:43:00 +08:00
|
|
|
|
function closeDrop() {
|
|
|
|
|
|
if (!drop) { return; }
|
|
|
|
|
|
drop.hidden = true;
|
|
|
|
|
|
btn.setAttribute('aria-expanded', 'false');
|
|
|
|
|
|
drop.innerHTML = '';
|
|
|
|
|
|
}
|
2026-10-04 17:42:21 +08:00
|
|
|
|
|
2026-10-05 10:58:17 +08:00
|
|
|
|
/** 画一个月:**只动面板内容**,不做任何跳转 */
|
|
|
|
|
|
function paint() {
|
|
|
|
|
|
var head = document.createElement('div');
|
|
|
|
|
|
head.className = 'cjd-head';
|
|
|
|
|
|
head.innerHTML =
|
|
|
|
|
|
'<button type="button" class="cjd-nav" data-step="-12" title="上一年" aria-label="上一年">«</button>' +
|
|
|
|
|
|
'<button type="button" class="cjd-nav" data-step="-1" title="上一月" aria-label="上一月">‹</button>' +
|
|
|
|
|
|
'<span class="cjd-title"></span>' +
|
|
|
|
|
|
'<button type="button" class="cjd-nav" data-step="1" title="下一月" aria-label="下一月">›</button>' +
|
|
|
|
|
|
'<button type="button" class="cjd-nav" data-step="12" title="下一年" aria-label="下一年">»</button>';
|
|
|
|
|
|
head.querySelector('.cjd-title').textContent = curY + ' 年 ' + curM + ' 月';
|
|
|
|
|
|
|
|
|
|
|
|
var grid = document.createElement('div');
|
|
|
|
|
|
grid.className = 'cjd-grid';
|
|
|
|
|
|
for (var w = 0; w < 7; w++) {
|
|
|
|
|
|
var wd = document.createElement('div');
|
|
|
|
|
|
wd.className = 'cjd-wd';
|
|
|
|
|
|
wd.textContent = WD[w];
|
|
|
|
|
|
grid.appendChild(wd);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 起算:本月 1 号是周几(周一=0 … 周日=6),前面补上月尾巴
|
|
|
|
|
|
var first = new Date(curY, curM - 1, 1);
|
|
|
|
|
|
var lead = (first.getDay() + 6) % 7;
|
|
|
|
|
|
var days = new Date(curY, curM, 0).getDate();
|
|
|
|
|
|
var today = new Date();
|
|
|
|
|
|
var todayStr = ymd(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
|
|
|
|
|
var start = new Date(curY, curM - 1, 1 - lead);
|
|
|
|
|
|
for (var i = 0; i < 42; i++) {
|
|
|
|
|
|
var dt = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
|
|
|
|
|
|
var s = ymd(dt.getFullYear(), dt.getMonth() + 1, dt.getDate());
|
|
|
|
|
|
var cell = document.createElement('button');
|
|
|
|
|
|
cell.type = 'button';
|
|
|
|
|
|
cell.className = 'cjd-day';
|
|
|
|
|
|
if (dt.getMonth() + 1 !== curM) { cell.className += ' is-out'; }
|
|
|
|
|
|
if (s === todayStr) { cell.className += ' is-today'; }
|
|
|
|
|
|
if (s === sel) { cell.className += ' is-on'; }
|
|
|
|
|
|
cell.textContent = String(dt.getDate());
|
|
|
|
|
|
cell.setAttribute('data-date', s);
|
|
|
|
|
|
grid.appendChild(cell);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var foot = document.createElement('div');
|
|
|
|
|
|
foot.className = 'cjd-foot';
|
|
|
|
|
|
foot.innerHTML =
|
|
|
|
|
|
'<span class="cjd-sel small muted"></span>' +
|
|
|
|
|
|
'<button type="button" class="btn btn-sm cjd-today">今天</button>' +
|
|
|
|
|
|
'<button type="button" class="btn btn-sm cjd-go">跳到这天</button>';
|
|
|
|
|
|
foot.querySelector('.cjd-sel').textContent = sel ? ('已选 ' + sel) : '点一天就跳过去';
|
|
|
|
|
|
|
|
|
|
|
|
drop.innerHTML = '';
|
|
|
|
|
|
drop.appendChild(head);
|
|
|
|
|
|
drop.appendChild(grid);
|
|
|
|
|
|
drop.appendChild(foot);
|
|
|
|
|
|
|
|
|
|
|
|
// 翻月 / 翻年:**只重画**
|
|
|
|
|
|
// ⚠️ 必须 stopPropagation:paint() 会把面板里的节点换掉,等这次 click 冒泡到
|
|
|
|
|
|
// document 上的「点外面就关」时,e.target 已经不在 DOM 里了 ——
|
|
|
|
|
|
// `drop.contains(e.target)` 为假 → 面板当场被关掉(实测:点一下"上一月",
|
|
|
|
|
|
// 面板立刻消失,看起来像"按钮没反应")。
|
|
|
|
|
|
Array.prototype.forEach.call(drop.querySelectorAll('.cjd-nav'), function (b) {
|
|
|
|
|
|
b.addEventListener('click', function (e) {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
var step = parseInt(b.getAttribute('data-step'), 10);
|
|
|
|
|
|
var t = new Date(curY, curM - 1 + step, 1);
|
|
|
|
|
|
curY = t.getFullYear();
|
|
|
|
|
|
curM = t.getMonth() + 1;
|
|
|
|
|
|
paint();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
// 点某一天 → 跳转(这是**唯一**会自动提交的地方)
|
|
|
|
|
|
Array.prototype.forEach.call(drop.querySelectorAll('.cjd-day'), function (b) {
|
|
|
|
|
|
b.addEventListener('click', function (e) {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
sel = b.getAttribute('data-date');
|
|
|
|
|
|
jump(sel);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
// 「跳到这天」:给不需要/不方便"点了就跳"的场景一个明确入口
|
|
|
|
|
|
drop.querySelector('.cjd-go').addEventListener('click', function (e) {
|
|
|
|
|
|
e.stopPropagation(); jump(sel);
|
|
|
|
|
|
});
|
|
|
|
|
|
drop.querySelector('.cjd-today').addEventListener('click', function (e) {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
var t = new Date();
|
|
|
|
|
|
sel = ymd(t.getFullYear(), t.getMonth() + 1, t.getDate());
|
|
|
|
|
|
jump(sel);
|
|
|
|
|
|
});
|
2026-10-04 19:43:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
btn.addEventListener('click', function (e) {
|
|
|
|
|
|
e.stopPropagation();
|
2026-10-05 10:58:17 +08:00
|
|
|
|
if (!drop) { return; }
|
2026-10-04 19:43:00 +08:00
|
|
|
|
if (!drop.hidden) { closeDrop(); return; }
|
2026-10-05 10:58:17 +08:00
|
|
|
|
var seed = String(hid.value || '').slice(0, 10);
|
|
|
|
|
|
var d0 = /^\d{4}-\d{2}-\d{2}$/.test(seed) ? new Date(seed + 'T00:00:00') : new Date();
|
|
|
|
|
|
if (isNaN(d0.getTime())) { d0 = new Date(); }
|
|
|
|
|
|
curY = d0.getFullYear();
|
|
|
|
|
|
curM = d0.getMonth() + 1;
|
|
|
|
|
|
sel = ymd(curY, curM, d0.getDate());
|
2026-10-04 19:43:00 +08:00
|
|
|
|
drop.hidden = false;
|
|
|
|
|
|
btn.setAttribute('aria-expanded', 'true');
|
2026-10-05 10:58:17 +08:00
|
|
|
|
paint();
|
2026-10-04 19:43:00 +08:00
|
|
|
|
});
|
2026-10-04 17:42:21 +08:00
|
|
|
|
|
2026-10-04 19:43:00 +08:00
|
|
|
|
// 点外面 / Esc 收起(下拉的常规行为)
|
|
|
|
|
|
document.addEventListener('click', function (e) {
|
|
|
|
|
|
if (!drop || drop.hidden) { return; }
|
|
|
|
|
|
if (drop.contains(e.target) || btn.contains(e.target)) { return; }
|
|
|
|
|
|
closeDrop();
|
|
|
|
|
|
});
|
|
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
|
|
|
|
if (e.key === 'Escape') { closeDrop(); }
|
2026-10-04 17:42:21 +08:00
|
|
|
|
});
|
2026-10-04 10:46:50 +08:00
|
|
|
|
})();
|
2026-10-02 09:28:07 +08:00
|
|
|
|
</script>
|
2026-10-21 13:22:37 +08:00
|
|
|
|
|
|
|
|
|
|
<?php /* ==================== 当天全部条目浮层(v70) ====================
|
|
|
|
|
|
用户 2026-10-20 第 2 条:「日历导入进去了,但是不显示」。
|
|
|
|
|
|
月视图每格只画 3 条(格子是固定高度,全画会把月历撑变形),
|
|
|
|
|
|
但原来的 "+N 条" 是**一颗点不动的 <span>** ⇒ 被折叠的条目**永远看不到**。
|
|
|
|
|
|
这里给那颗按钮一个真正的去处。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 为什么不直接复用条目详情浮层(`fbEntryModal`):
|
|
|
|
|
|
那是"**一条**日程的详情/编辑/删除",一次只画一条;
|
|
|
|
|
|
这里要的是"**当天的清单**",点其中任意一条才进那条的详情。
|
|
|
|
|
|
两者叠用是对的:先出清单,点清单里某条 → 调 `window.fbEntryOpen()` 进详情层。
|
|
|
|
|
|
⚠️ 详情层是 `position:fixed` 的高层,清单层在它下面 —— 关掉详情层会**回到清单**,
|
|
|
|
|
|
符合"我从当天的清单里点进来的"这个来路(不用再点一次 +N 条)。
|
|
|
|
|
|
⚠️ 数据直接读上面那个 `<script id="calDayData">`:不发请求、点开即时。
|
|
|
|
|
|
⚠️ 无 JS 兜底:那颗按钮外面本来是 <button>,禁用 JS 时它不会有反应 —
|
|
|
|
|
|
这是可接受的降级(此时"+N 条"本来也没有别的语义),
|
|
|
|
|
|
因为**没有 JS 的浏览器根本也不会有"点格子新建日程"那一套**,
|
|
|
|
|
|
用户会直接用「周 / 天」视图看全部条目(那两个视图不截断)。 */ ?>
|
|
|
|
|
|
<div class="cal-modal cal-day-modal" id="calDayModal" role="dialog" aria-modal="true"
|
|
|
|
|
|
aria-label="当天全部日程" hidden>
|
|
|
|
|
|
<div class="cal-modal-panel">
|
|
|
|
|
|
<div class="cal-modal-head">
|
|
|
|
|
|
<b id="calDayTitle">当天的日程</b>
|
|
|
|
|
|
<button type="button" class="cal-modal-x" id="calDayClose" aria-label="关闭">✕</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="cal-modal-body cal-day-body" id="calDayBody"></div>
|
|
|
|
|
|
<div class="cal-modal-foot cal-day-foot">
|
|
|
|
|
|
<?php /* 浮层里就地再建一条 —— 清单本来就在"这天",比退出去点格子顺手。
|
|
|
|
|
|
⚠️ 没写权限时**不渲染**(不是 hidden):只读家庭里这颗按钮点了也没用,
|
|
|
|
|
|
留着反而像坏了。 */ ?>
|
|
|
|
|
|
<?php if ($canNew): ?>
|
|
|
|
|
|
<button type="button" class="btn btn-sm" id="calDayAdd">+ 在这天加一条</button>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<span class="cal-modal-tip" id="calDayTip"></span>
|
|
|
|
|
|
<a class="btn btn-ghost btn-sm" id="calDayMore" href="#">在日视图里看</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
var host = document.getElementById('calDayModal');
|
|
|
|
|
|
if (!host) { return; }
|
|
|
|
|
|
var body = document.getElementById('calDayBody');
|
|
|
|
|
|
var title = document.getElementById('calDayTitle');
|
|
|
|
|
|
var link = document.getElementById('calDayMore');
|
|
|
|
|
|
|
|
|
|
|
|
function esc(s) {
|
|
|
|
|
|
return String(s == null ? '' : s)
|
|
|
|
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
|
|
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function dayData() {
|
|
|
|
|
|
var el = document.getElementById('calDayData');
|
|
|
|
|
|
if (!el) { return {}; }
|
|
|
|
|
|
try { return JSON.parse(el.textContent || '{}') || {}; } catch (e) { return {}; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openDay(day) {
|
|
|
|
|
|
var all = dayData()[day] || [];
|
|
|
|
|
|
if (!all.length) { return; }
|
|
|
|
|
|
title.textContent = day + ' · 共 ' + all.length + ' 条';
|
|
|
|
|
|
var html = '';
|
|
|
|
|
|
all.forEach(function (it, i) {
|
|
|
|
|
|
/* 时间行:全天显示"全天",有时刻显示 HH:MM–HH:MM(与详情浮层同一口径) */
|
|
|
|
|
|
var when = '';
|
|
|
|
|
|
var rows = it.rows || [];
|
|
|
|
|
|
for (var k = 0; k < rows.length; k++) {
|
|
|
|
|
|
if (rows[k][0] === '时间') { when = rows[k][1]; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (when.indexOf(day) === 0) { when = when.slice(day.length).trim(); }
|
|
|
|
|
|
var src = (it.source === 'event') ? '' : ' is-derived';
|
|
|
|
|
|
html += '<button type="button" class="cal-day-item' + src + '" data-day-idx="' + i + '">'
|
|
|
|
|
|
+ '<span class="cal-day-when">' + esc(when || '全天') + '</span>'
|
|
|
|
|
|
+ '<span class="cal-day-name">' + esc(it.title || '(无标题)') + '</span>'
|
|
|
|
|
|
+ '</button>';
|
|
|
|
|
|
});
|
|
|
|
|
|
body.innerHTML = html;
|
|
|
|
|
|
document.getElementById('calDayTip').textContent = '';
|
|
|
|
|
|
link.setAttribute('href', <?= json_encode(url('calendar', ['view' => 'day'])) ?> + '&d=' + encodeURIComponent(day));
|
|
|
|
|
|
host.hidden = false;
|
|
|
|
|
|
/* 记录当前是哪一天,点清单项时要按它取数据 */
|
|
|
|
|
|
host.setAttribute('data-day', day);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeDay() { host.hidden = true; }
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', function (e) {
|
|
|
|
|
|
var btn = e.target.closest && e.target.closest('.cal-more');
|
|
|
|
|
|
if (btn) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation(); /* 别让"点格子新建日程"也跟着触发 */
|
|
|
|
|
|
openDay(btn.getAttribute('data-cal-day') || '');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (e.target.closest && e.target.closest('#calDayClose')) {
|
|
|
|
|
|
e.preventDefault(); closeDay(); return;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 清单里"在这天加一条":把当前这天交给新建浮层(09:00 起,与服务端
|
|
|
|
|
|
"点月格"同口径),并先收起清单 —— 两层浮层不能叠着。 */
|
|
|
|
|
|
if (e.target.closest && e.target.closest('#calDayAdd')) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
var d0 = host.getAttribute('data-day') || '';
|
|
|
|
|
|
closeDay();
|
|
|
|
|
|
if (d0 && typeof window.fbCalOpenNew === 'function') { window.fbCalOpenNew(d0, '09', '00'); }
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 点清单里某一条 → 交给条目详情浮层。
|
|
|
|
|
|
走 `fbEntryOpenData`(v70 新增的"按数据对象开"入口)——
|
|
|
|
|
|
清单项只是文字行,页面上没有对应的 `[data-fb-entry]` 元素可点。 */
|
|
|
|
|
|
var item = e.target.closest && e.target.closest('.cal-day-item');
|
|
|
|
|
|
if (item) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
var day = host.getAttribute('data-day') || '';
|
|
|
|
|
|
var list = dayData()[day] || [];
|
|
|
|
|
|
var one = list[parseInt(item.getAttribute('data-day-idx'), 10)];
|
|
|
|
|
|
if (one && typeof window.fbEntryOpenData === 'function') {
|
|
|
|
|
|
closeDay(); /* 先收清单,再开详情 —— 否则两层叠在一起 */
|
|
|
|
|
|
window.fbEntryOpenData(one);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (e.target === host) { closeDay(); }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
|
|
|
|
if (e.key !== 'Escape') { return; }
|
|
|
|
|
|
if (host.hidden) { return; }
|
|
|
|
|
|
e.preventDefault(); closeDay();
|
|
|
|
|
|
});
|
|
|
|
|
|
})();
|
|
|
|
|
|
</script>
|