用户 2026-10-05 的 5 条。第 11 批交付后用户追问"确定这些需求都做完了?"—— 没有, 这 5 条是进一步要求。实现过程中查出 4 个真 bug。 #1 详情页删除按钮紧贴「开通账号」 上一版放在页面最下方独立区块(danger-zone),用户要「就放在开通账号按钮下面, 紧挨着,长度大小和开通账号一样,颜色改成红色」。→ 搬进「登录账号」卡片内 (.del-inline + 细分隔线 + .btn-block-lg)。 线上几何实测:同一张卡片、左边缘对齐 255==255、宽度一致 990==990、红色 rgb(192,83,63)。 #2 日期控件三修(两个真 bug + 一次闪烁) · 灰底:Semi **外层** .semi-datepicker-input 自带 rgba(46,50,56,.05)、高 32px; 内层 wrapper 白底 40px → 灰的露在外面。实测「灰底元素」= 1 个 → 修后 0 个。 · 不垂直居中:外层 32px < 内层 40px,实测 gapTop=1 / **gapBottom=17**(文字贴顶) → 修后 gapTop=1 / gapBottom=1。 · 闪烁:实测首 **200ms** 原生输入框可见(要等 3.4MB React 解析)。 用 no-js/js 二态:<html class="no-js"> + head 内联脚本立刻换 js + CSS `html.js .dt-input:not(.is-semi) .dt-native { visibility:hidden }` (配同高骨架 ::after 防布局跳动)。实测「出现过原生可见」= False。 #3 日历跳转保留视图 + 下拉日历 原来写死 view=day(在月视图点跳转会被踢进日视图)。→ 表单带当前 $view(月还带 ym); 选择器从"居中弹窗"改成**按钮下方下拉** .cal-jump-drop。 实测:月视图跳 8/15 仍在月视图且是 8 月;周视图跳 8/15 → 落 8/10 那一周; 日视图跳 8/15 → 显示 8/15。 #4 传统节日 + 二十四节气两个系统本(Schema v18) Holiday::SOLAR_TERMS(24 项 C 常数近似公式 + 例外表)+ traditionalRange (农历推导;除夕用"次日是否正月初一"反推,写死腊月廿九会有一半年份错)。 实测 2026:节气 24 条全对(清明 4/5、冬至 12/22);春节 2/17、除夕 2/16、中秋 9/25。 ⚠️ ENUM 必须显式 ALTER(对已存在的表不生效)——同一坑第二次踩,VERSION 17→18。 #5 财务(一个真 bug + 一个假起点 + 一个真 bug) · chart_line 读 `cents` 而 timeline() 返回 `net` → 键名不匹配,**总资产图每点读成 0** (正是用户说的"图上是 0")。修:兼容 cents ?? net ?? asset。 实测 timeline net=800000(8000 元),线上 polyline 有真实起伏。 · 去掉 timeline 人为插的"前一天=0"假起点(用户:"总资产不应该是 0, 应该是所有账目创建时的金额的总数")。 · ppSetSelected 只用 document.querySelector 按名字找,而财务页有**两个** person_id 组件(「记一笔」+ 修改弹框)→ 永远灌到第一个。加 root 参数、调用方传 panel。 验证:e2e 786/0、交互 92/0、升级 129/0(+3 条 v18 断言)、batch_e 91/0、batch10 46/0、 batch9 38/0、picker_unify 28/0、picker_lazy 11/0、crop 17/0、formbloat 9/0、semi 27/0、 mobile 688/0、drag 5/0、geom 5/5;第 12 批专项:财务 7/0、日历 19/0。 部署静态断言新增 5 组(含反向验证:注入回退 → FAIL=1)。 线上自检 19/19;线上真实账号验收 **30/0**(含真实浏览器几何量删除按钮位置)。
437 lines
23 KiB
PHP
437 lines
23 KiB
PHP
<?php
|
||
/**
|
||
* 日程 · 月 / 周 / 天 三视图(v12,v24 第 4、5 条重排)。
|
||
*
|
||
* ⚠️ v24 用户 2026-10-03 第 4 条:**左侧那一栏「我的日历本」撤掉了**,日历占满整宽。
|
||
* 原话:「日程模块,左侧的日历本显示与否,去掉,让日历充满整个宽度。
|
||
* 在右侧,也加一个设置按钮,设置中,可以设置哪些日历本显示,
|
||
* 调整日历的颜色,导入日历等功能,都放在这个设置下。」
|
||
* 所以那些功能全搬进了页头最右边那个 ☰ 里(见 _settings_menu.php)。
|
||
* 显隐改由**服务端**处理(Calendar::hiddenCalendars):藏掉的本子不参与取数,
|
||
* "+N 条"之类按可见条目算 —— 前端 display:none 会让这些数字对不上。
|
||
*
|
||
* ⚠️ 用户第 5 条:在日历里点一下就建日程(开始时间 = 点的那一格)。
|
||
* 月视图的格子按天、周/日视图按半小时——
|
||
* 后者用鼠标在列里的纵向位置反算时刻(见页面底部那两段 JS)。
|
||
*
|
||
* @param string $ym 'Y-m'
|
||
* @param array $weeks 月网格(build_month_grid 产出)
|
||
* @param array $cals 我可见的日历本(含 perm);$visCals 是其中显示着的 id
|
||
* @param string $newAt 点进来时预填的开始时间('Y-m-d H:i' 或 'Y-m-d')
|
||
*/
|
||
$canWrite = \App\Auth::canWrite();
|
||
$view = (string)($view ?? 'month');
|
||
$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())));
|
||
$visCals = array_map('intval', (array)($visCals ?? []));
|
||
$hiddenCals = array_map('intval', (array)($hiddenCals ?? []));
|
||
$newAt = (string)($newAt ?? '');
|
||
// 能不能"点一下建日程":只读账号点了没反应,所以连十字光标都不给(见 CSS 的 rt-writable)
|
||
$canNew = $canWrite && (bool)$writable;
|
||
|
||
// 三种看法各自的「上一段 / 下一段」:月按整月走,周按 7 天走,天按 1 天走
|
||
$step = $view === 'week' ? 7 : ($view === 'day' ? 1 : 0);
|
||
$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)));
|
||
?>
|
||
<div class="page-head">
|
||
<h1>日程</h1>
|
||
<span class="sub"><?= e($headText) ?></span>
|
||
<div class="actions">
|
||
<?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; ?>
|
||
<?php if ($canWrite && $writable): ?>
|
||
<button class="btn" type="button" id="calAddBtn">新建日程</button>
|
||
<?php endif; ?>
|
||
<?php /* 用户 2026-10-04 第 6 条原话:「日程页面,在设置按钮的前面,加一个日历按钮,
|
||
选择日历后,跳转到那一天。」
|
||
→ 一个带日历图标的按钮,点开是日期选择;选中即跳到那天的**日视图**。
|
||
⚠️ 用原生 <input type="date"> 而不是再引一个弹层:它的原生日历足够用,
|
||
而且**选中就有值**(change 事件)→ 直接跳转,不用再点一次"确定"。
|
||
桌面/手机/无 JS 三种情况下的行为都是可预期的。 */ ?>
|
||
<?php /* 用户 2026-10-05 第 12 批 #3 原话(三句):
|
||
· 「跳转到某一天时,不要进入日视图,保留当前本来的视图,只不过是跳转到这一天,
|
||
如果是周视图,那么就是跳转到这一天所在的周,如果是月视图,则跳转到这一天所在的月」
|
||
· 「选择这个日期,不用弹窗,直接把日历选择,弹出在这个按钮下面就可以了」
|
||
→ ① 不再固定 view=day:带上**当前视图**(月/周/日各自跳到对应跨度);
|
||
② 日期选择从"居中弹窗"改成**贴着按钮下方展开的下拉日历**。 */ ?>
|
||
<form class="cal-jump" method="get" action="index.php">
|
||
<input type="hidden" name="r" value="calendar">
|
||
<?php /* 保留当前视图:月→该月 / 周→那一周 / 日→那一天。
|
||
⚠️ 原来写死 view=day,用户点"跳到 10 月 20 日"就被踢进日视图,
|
||
而他本来在月视图里 —— 这正是不满的点。 */ ?>
|
||
<input type="hidden" name="view" value="<?= e($view) ?>">
|
||
<input type="hidden" name="d" id="calJumpD" value="<?= e($anchor) ?>">
|
||
<?php /* 月视图用的是 ym(年月),跳转时也要带上,否则服务端会退回默认月 */ ?>
|
||
<?php if ($view === 'month'): ?>
|
||
<input type="hidden" name="ym" id="calJumpYM" value="<?= e(substr($anchor, 0, 7)) ?>">
|
||
<?php endif; ?>
|
||
<button type="button" class="btn btn-icon cal-jump-btn" id="calJumpBtn"
|
||
title="跳到某一天" aria-label="跳到某一天" aria-haspopup="dialog" aria-expanded="false">
|
||
<svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none"
|
||
stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
|
||
<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>
|
||
</button>
|
||
<?php /* 下拉日历的宿主:紧贴按钮下方展开(不是弹窗)。JS 就绪后把 Semi
|
||
日期选择器挂进来;JS 不可用时它是空的、退到下面的 noscript。 */ ?>
|
||
<div class="cal-jump-drop" id="calJumpDrop" hidden></div>
|
||
<noscript>
|
||
<input class="input" type="date" name="d" value="<?= e($anchor) ?>" aria-label="选择日期">
|
||
<button class="btn btn-sm" type="submit">跳转</button>
|
||
</noscript>
|
||
</form>
|
||
<?php /* 用户第 4、5 条:日历本 / 颜色 / 导入 / 默认视图 全收进这个 ☰ ——
|
||
它顶替了原来左边那一栏,所以排在**最右**(靠 .page-head .actions 的顺序)。 */ ?>
|
||
<?php $setCurrent = 'view'; require __DIR__ . '/_settings_menu.php'; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cal-wrap cal-wrap-full">
|
||
<?php /* ---- 右:月网格 / 时间网格 ----
|
||
用户第 4 条:左边那一栏「我的日历本」已经撤掉(功能搬进右上角 ☰),
|
||
所以这里直接就是主区域,`.cal-wrap-full` 把网格调成"只有一列"。 */ ?>
|
||
<div class="cal-main">
|
||
<?php /* 用户第 5 条:这张表单既服务「新建日程」按钮,也服务「点日历空白处」——
|
||
后者由 JS 现填开始时间,所以表单本身不再写死 09:00。 */ ?>
|
||
<?php if ($canWrite && $writable): ?>
|
||
<div class="card card-pad cal-add" id="calAddCard" hidden>
|
||
<h2 class="cinema-h2">新建日程</h2>
|
||
<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">
|
||
<div class="field">
|
||
<label for="ce-cal">放在哪本</label>
|
||
<select class="select" id="ce-cal" name="calendar_id">
|
||
<?php foreach ($writable as $c): ?>
|
||
<option value="<?= (int)$c['id'] ?>"><?= e((string)$c['name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<?php /* v24 第 6 条:字段按 iCalendar / CalDAV 的 VEVENT 定(见 _event_fields.php)。
|
||
新建时「全天」默认**不勾**(用户原话)。开始时间默认:
|
||
带了 new_at 就用它(点日历的那一格),否则今天 09:00。 */ ?>
|
||
<?php
|
||
// 点的位置优先;没点就"月视图当月 → 该月 1 号 / 本月 → 今天"的 09:00
|
||
$defDay = $newAt !== '' ? substr($newAt, 0, 10)
|
||
: (($ym === $thisYm ? date('Y-m-d') : $ym . '-01'));
|
||
$defHM = $newAt !== '' && strlen($newAt) > 10
|
||
? substr($newAt, 11, 5) : '09:00';
|
||
$defEndH = sprintf('%02d', (min(23, (int)substr($defHM, 0, 2) + 1)));
|
||
$evFields = ['starts_at' => $defDay . ' ' . $defHM . ':00',
|
||
'ends_at' => $defDay . ' ' . $defEndH . ':' . substr($defHM, 3, 2) . ':00'];
|
||
$evAllDay = false;
|
||
require __DIR__ . '/_event_fields.php';
|
||
?>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-sm" type="submit">加上</button>
|
||
<button class="btn btn-ghost btn-sm" type="button" id="calAddCancel">取消</button>
|
||
</div>
|
||
</form>
|
||
</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' : '' ?>">
|
||
<?php /* $c['w'] 是 date('w')(0=周日),所以这张索引表保持"日"开头——
|
||
它只是把数字换成汉字,与列的排列顺序无关。 */ ?>
|
||
<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' : '' ?><?= $canNew ? ' rt-writable' : '' ?>"
|
||
data-new-at="<?= e((string)$c['date']) ?>">
|
||
<?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 /* ⚠️ 用户第 11 批 #6:「月视图,从周一开始。而不是周日。」
|
||
表头顺序必须与 month_grid_range() 的起算规则一致(那里已改成周一)。 */ ?>
|
||
<?php foreach (['一', '二', '三', '四', '五', '六', '日'] as $wd): ?>
|
||
<div class="cal-wd"><?= e($wd) ?></div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<div class="cal-grid">
|
||
<?php foreach ($weeks as $wk): ?>
|
||
<?php foreach ($wk as $cell): ?>
|
||
<?php /* 用户第 5 条:点格子里的空白 = 在这天建日程。
|
||
用 data-new-at 把日期带过去,由底部 JS 交给新建表单
|
||
(点了小条本身是"打开那条日程",由 <a> 自己处理,JS 会排除掉)。 */ ?>
|
||
<div class="cal-cell<?= $cell['in_month'] ? '' : ' is-out' ?><?= $cell['is_today'] ? ' is-today' : '' ?><?= $canNew ? ' rt-writable' : '' ?>"
|
||
data-new-at="<?= e((string)$cell['date']) ?>">
|
||
<div class="cal-dnum"><?= (int)$cell['day'] ?></div>
|
||
<div class="cal-evs">
|
||
<?php $n = 0; foreach ($cell['events'] as $ev): $n++; ?>
|
||
<?php if ($n <= 3): ?>
|
||
<a class="cal-ev<?= empty($ev['all_day']) ? '' : ' is-allday' ?>"
|
||
data-cal="<?= (int)$ev['cal'] ?>"
|
||
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; ?>
|
||
<?php if (count($cell['events']) > 3): ?>
|
||
<span class="cal-more">+<?= count($cell['events']) - 3 ?> 条</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
/* ---- 新建日程:按钮开合 + 「点日历空白处」预填开始时间 ----
|
||
⚠️ 上一版的「日历本显隐」JS 已经**整段删掉**:显隐改成服务端处理
|
||
(Calendar::hiddenCalendars,见路由里的说明),前端不再需要它。
|
||
留着会让人以为"改一下勾选就能切换",但那些勾选框已经不在这儿了。 */
|
||
var btn = document.getElementById('calAddBtn');
|
||
var card = document.getElementById('calAddCard');
|
||
var cancel = document.getElementById('calAddCancel');
|
||
if (btn && card) {
|
||
btn.addEventListener('click', function () {
|
||
card.hidden = !card.hidden;
|
||
if (!card.hidden) { card.scrollIntoView({ block: 'nearest' }); }
|
||
});
|
||
}
|
||
if (cancel && card) { cancel.addEventListener('click', function () { card.hidden = true; }); }
|
||
|
||
/* ---- 用户第 5 条:在日历里点一下 → 弹新建,开始时间 = 点的位置 ----
|
||
月视图:一格 = 一天 → 开始时间用那天 09:00("当日默认"比 00:00 更像个日程)。
|
||
周/日视图:按鼠标在**那一列里的纵向位置**反算时刻,最接近的半小时 ——
|
||
点在"下午三点那一段"就该得到 15:00,这是"点哪儿建哪儿"的全部意义。 */
|
||
var startField = document.querySelector('.field.ev-dt[data-dt]');
|
||
var startDate = startField ? startField.querySelector('input.dt-date') : null;
|
||
var startHour = startField ? startField.querySelector('select.dt-hour') : null;
|
||
var startMin = startField ? startField.querySelector('select.dt-minute') : null;
|
||
var startSec = startField ? startField.querySelector('select.dt-second') : null;
|
||
|
||
function setStart(dateStr, hh, mm) {
|
||
// ⚠️ 优先走全局接口:它会把原生字段与 Semi 控件**一起**更新。
|
||
// 直接写 startDate.value 的话,Semi 是非受控组件、显示不会跟着变 ——
|
||
// 用户会看到"我点了 15:00,控件里还写着 09:00"(提交的却是 15:00)。
|
||
if (typeof window.fbSetDT === 'function'
|
||
&& window.fbSetDT('starts_at', dateStr + ' ' + (hh || '09') + ':' + (mm || '00') + ':00')) {
|
||
return;
|
||
}
|
||
// 兜底:Semi 还没加载完(或加载失败)→ 原生控件照旧能用
|
||
if (!startDate) return;
|
||
startDate.value = dateStr;
|
||
if (hh !== null && startHour) { startHour.value = hh; }
|
||
if (mm !== null && startMin) { startMin.value = mm; }
|
||
if (startSec) { startSec.value = '00'; }
|
||
}
|
||
|
||
function openNew(dateStr, hh, mm) {
|
||
if (!card) return;
|
||
card.hidden = false;
|
||
setStart(dateStr, hh, mm);
|
||
card.scrollIntoView({ block: 'nearest' });
|
||
// 标题是第一个输入框,直接把光标放过去 —— 点完就能打字
|
||
var t = document.getElementById('ev-title');
|
||
if (t) { t.focus(); }
|
||
}
|
||
|
||
/* 月视图格子 */
|
||
Array.prototype.forEach.call(document.querySelectorAll('.cal-cell[data-new-at]'), function (cell) {
|
||
cell.addEventListener('click', function (ev) {
|
||
// 点在小条上 = 想打开那条日程(<a> 的默认行为),不要抢
|
||
if (ev.target.closest && ev.target.closest('.cal-ev')) { return; }
|
||
if (!<?= $canWrite && $writable ? 'true' : 'false' ?>) { return; }
|
||
openNew(cell.getAttribute('data-new-at'), '09', '00');
|
||
});
|
||
});
|
||
|
||
/* 周/日视图的列:纵向位置 → 时刻。
|
||
列高 1056px = 24 小时(见 .tg-col 的 CSS),所以 1 分钟 = 1056/1440 px。 */
|
||
Array.prototype.forEach.call(document.querySelectorAll('.tg-col[data-new-at]'), function (col) {
|
||
col.addEventListener('click', function (ev) {
|
||
if (ev.target.closest && ev.target.closest('.tg-block')) { return; }
|
||
if (!<?= $canWrite && $writable ? 'true' : 'false' ?>) { return; }
|
||
var box = col.getBoundingClientRect();
|
||
var ratio = (ev.clientY - box.top) / Math.max(1, box.height);
|
||
var mins = Math.max(0, Math.min(24 * 60 - 30, Math.round(ratio * 1440 / 30) * 30));
|
||
var hh = String(Math.floor(mins / 60)).padStart(2, '0');
|
||
var mm = String(mins % 60).padStart(2, '0');
|
||
openNew(col.getAttribute('data-new-at'), hh, mm);
|
||
});
|
||
});
|
||
|
||
/* 从 ?new_at= 进来(比如别处链过来)→ 直接开弹窗并预填 */
|
||
var preset = <?= json_encode($newAt, JSON_UNESCAPED_UNICODE) ?>;
|
||
if (preset) {
|
||
var parts = String(preset).split(/[ T]/);
|
||
if (parts[0]) {
|
||
var hm = (parts[1] || '').split(':');
|
||
openNew(parts[0], hm[0] || '09', hm[1] || '00');
|
||
}
|
||
}
|
||
/* 「全天」勾上藏掉时刻那一段的逻辑在 _event_fields.php 里(两个页面共用一份) */
|
||
})();
|
||
|
||
/* ---- v24 第 12 批 #3:「跳到某一天」—— 单个图标按钮 + **按钮下方的下拉日历** ----
|
||
用户原话:「选择这个日期,不用弹窗,直接把日历选择,弹出在这个按钮下面就可以了。」
|
||
所以从"居中弹窗(cal-jump-host 全屏遮罩)"改成"贴着按钮下方展开的下拉"。
|
||
⚠️ 中途会经过"保留视图"那一关:jump() 提交时带上当前 view(月的 ym / 周的 d / 日的 d),
|
||
由服务端决定该视图下这个日期落在哪一屏 —— 见 index.php 的 calendar 路由。 */
|
||
(function () {
|
||
var form = document.querySelector('.cal-jump');
|
||
var btn = document.getElementById('calJumpBtn');
|
||
var hid = document.getElementById('calJumpD');
|
||
var drop = document.getElementById('calJumpDrop');
|
||
if (!form || !btn || !hid) { return; }
|
||
var root = null; // React root(重开时先 unmount)
|
||
|
||
function jump(v) {
|
||
if (!v) { return; }
|
||
hid.value = v;
|
||
// 月视图还要带上 ym(服务端按月取数),否则会跳回默认月
|
||
var ym = document.getElementById('calJumpYM');
|
||
if (ym) { ym.value = String(v).slice(0, 7); }
|
||
form.submit();
|
||
}
|
||
|
||
function closeDrop() {
|
||
if (!drop) { return; }
|
||
drop.hidden = true;
|
||
btn.setAttribute('aria-expanded', 'false');
|
||
if (root) { root.unmount(); root = null; }
|
||
drop.innerHTML = '';
|
||
}
|
||
|
||
// 原生兜底:Semi 没就绪时用临时原生日期框(与无 JS 时行为一致)
|
||
function fallback() {
|
||
var t = document.createElement('input');
|
||
t.type = 'date';
|
||
t.value = hid.value;
|
||
t.style.position = 'fixed';
|
||
t.style.left = '-9999px';
|
||
document.body.appendChild(t);
|
||
t.addEventListener('change', function () { jump(t.value); });
|
||
if (typeof t.showPicker === 'function') { try { t.showPicker(); } catch (e2) {} }
|
||
else { t.focus(); t.click(); }
|
||
}
|
||
|
||
btn.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
if (!drop) { fallback(); return; }
|
||
if (!drop.hidden) { closeDrop(); return; }
|
||
|
||
if (!window.SemiUI || !window.SemiUI.DatePicker || !window.ReactDOM) { fallback(); return; }
|
||
|
||
drop.hidden = false;
|
||
btn.setAttribute('aria-expanded', 'true');
|
||
var inner = document.createElement('div');
|
||
drop.innerHTML = '';
|
||
drop.appendChild(inner);
|
||
root = ReactDOM.createRoot(inner);
|
||
root.render(React.createElement(SemiUI.DatePicker, {
|
||
type: 'date',
|
||
format: 'yyyy-MM-dd',
|
||
defaultValue: hid.value ? new Date(hid.value + 'T00:00:00') : new Date(),
|
||
// ⚠️ 不用 `open: true`:那会让 Semi 自己弹它的浮层(就又成弹窗了)。
|
||
// 这里要的是"日历长在下拉容器里",所以让它以内联面板渲染。
|
||
open: false,
|
||
defaultOpen: true,
|
||
preventScroll: true,
|
||
onChange: function (d, ds) {
|
||
if (ds) { jump(ds); }
|
||
}
|
||
}));
|
||
});
|
||
|
||
// 点外面 / 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(); }
|
||
});
|
||
})();
|
||
</script>
|