Files
FamilyBook/templates/calendar/calendars.php
T
laoyang 8a9629650a v72:日历本显示与管理合并成一张清单,颜色改用内置调色模态框
用户 2026-10-21 两条:
  「日历设置里面日历本显示和日历管理可以放在一起」
  「预制的颜色点了之后没反应……选颜色那个地方点了之后是弹出 Windows 的
    一个颜色选择框,这个不好,我们把它改成内置的」

改动:
· calendar/display 整页删除,并入 calendar/calendars(保留 302 老地址)
· 新增 templates/_color_picker.php(按钮 + 值载体,表单/独立双模式)
· 新增 templates/_color_picker_modal.php(全页唯一模态框,预置色 + hex 手输)
· 共享本提到同一清单,标注归属与权限;owner 校验服务端做
· 清掉 display.php 时代遗留的死 CSS(.cal-set-page / .cal-set-book* 等)

部署中抓出并修掉两处真问题:
· 部署脚本「清掉被取代文件」清单漏了 display.php —— tar 只新增/覆盖,
  git rm 掉的文件在线上永远留着,被第 7 段断言拦下(断言是对的,缺的是删除步骤)
· _color_picker.php 里残留 <input type="color" class="cp-trigger">:
  原生取色控件既不能有子元素、也没有 data-cp-open ⇒
  按钮回显失效(找不到 .cp-trigger-dot/hex)+ 模态框永远打不开,
  用户点到的还是 Windows 取色框。改成 <button type="button"> + 色点 + hex
  (v72 探针在修复前实测 2 条红,修复后 50/0)
2026-10-21 20:01:23 +08:00

550 lines
28 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 日历本(v72 用户 2026-10-21)—— 「显示设置」与「日历本管理」**合并成一页**。
*
* 用户原话:
* 「日历设置里面日历本显示和日历管理可以放在一起啊。就是把日历本全都列出来,
* 然后显示不显示,加个复选框就行了。另外把下面别人共享给我的也提到上面来,
* 把所有的日历都放在一块,只不过在后面标注出来,这个是别人谁谁谁共享给我的?
* 是只读的还是可写的,什么的就可以了呀。然后这样的话就是自己的可以改给编辑,
* 别人共享给我的,不让他编辑编辑就可以了。」
*
* ─────────────────────────────────────────────────────────────────────
* 改之前是两页(v51 拆的):
* · `calendar/display` —— 默认视图 + 「显示哪些日历本」(一堆复选框)
* · `calendar/calendars` —— 「我自己的」表格 + 页面最下面「别人共享给我的」小表格
* ⇒ 用户要在两个页面之间来回跳,才能回答"这本到底归谁、我要不要看见它"。
*
* 改成**一张清单**,每本日历一行,从左到右是一条完整的答案:
*
* [✓] ■ 名字 [谁共享给我的·只读] [共享设置] [编辑] [删除] [导出]
* │ │ │ │ │
* │ │ │ └ 归属与权限:我的本子这里空着(或只显示"系统"),
* │ │ │ 共享进来的写「谁共享·只读/可写」
* │ │ └ 名字
* │ └ 颜色(点一下就地改色 —— 见下面的「颜色」段)
* └ 显示/不显示(就是原来"显示设置"那堆复选框)
*
* 🔴 **共享给我的本子没有编辑/删除按钮**(用户明确要求「别人共享给我的,不让他编辑」)。
* 这不是"界面藏起来"而已:服务端 `cal_update` / `cal_del` 本来就校验
* `owner_id === $me`,所以即使手工构造请求也改不动。界面与服务端**两侧都拦**。
* ⚠️ 唯一例外是「共享设置」—— 那是我自己本子才有的;共享进来的本子由主人决定给谁,
* 我这里不该有那个入口。
*
* 🔴 **"显示/不显示"与"能不能编辑"是两件独立的事**:共享给我的只读本子,
* 我照样可以决定"显示不显示"(那只是我自己的视图偏好,不写对方的数据)。
* 所以复选框对**每一行**都在,包括共享进来的。
*
* ─────────────────────────────────────────────────────────────────────
* 变量:
* $cals 我**自己的**本子(含 perm='owner')
* $shared 别人共享给我的
* $shares [calId => 共享给谁](只对我自己的本子有意义)
* $hiddenCals 我关掉的那些本子 id
* $people 名册(共享下拉用)
* $editing 正在编辑的那一本(?edit=<id>)
*/
$isEditing = !empty($editing);
$canWrite = \App\Auth::canWrite();
$hiddenIds = array_map('intval', (array)($hiddenCals ?? []));
$cals = (array)($cals ?? []);
$shared = (array)($shared ?? []);
$backView = (string)($view ?? 'month');
/* 「所有的日历都放在一块」:先把"我的"排完,再排"共享给我的"。
⚠️ 两组的边界要在页面上**看得出来**(见下面的 `.cal-books-sep`),
但**仍然是同一张清单**(用户要的是"放一块",不是"分两张表")。 */
$allRows = [];
foreach ($cals as $c) {
$c['_mine'] = true;
$allRows[] = $c;
}
foreach ($shared as $c) {
$c['_mine'] = false;
$allRows[] = $c;
}
$totalCount = count($allRows);
?>
<div class="page-head">
<h1>日历本</h1>
<span class="sub">
共 <?= $totalCount ?> 本<?= $shared ? '(其中 ' . count($shared) . ' 本是别人共享给我的)' : '' ?>
</span>
<div class="actions">
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => $backView])) ?>">返回日程</a>
<?php $setCurrent = 'books'; require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
<?php if (!$canWrite): ?>
<div class="card card-pad empty"><b>只读账号</b>访客只能查看日历本,不能新建或修改。</div>
<?php endif; ?>
<?php /* ==================== 新建 / 编辑(仅自己的自定义本)====================
用户 2026-10-21 的新要求里没有动这块的形态,只要求"颜色选择别弹 Windows 框"。
⇒ 表单保留,但取色器换成内置的(`_color_picker.php`)。
⚠️ 共享进来的本子**永远进不了这个表单**:路由层只把 `kind='custom'` 且
`owner_id=$me` 的本子塞进 `$editing`(见 index.php `case 'calendar/calendars'`)。 */ ?>
<?php if ($canWrite): ?>
<div class="card card-pad">
<h2 class="cinema-h2"><?= $isEditing ? '编辑日历本' : '新建日历本' ?></h2>
<form method="post" action="<?= e(url($isEditing ? 'calendar/cal_update' : 'calendar/cal_add')) ?>">
<?= csrf_field() ?>
<?php if ($isEditing): ?>
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
<?php endif; ?>
<div class="form-grid">
<div class="field">
<label for="cb-name">名字 *</label>
<input class="input" id="cb-name" name="name" required
value="<?= e(old('name', $editing['name'] ?? '')) ?>" placeholder="如:体检 / 值日">
</div>
<div class="field">
<label>颜色</label>
<?php
/* 🔴 v72:不再用 `<input type="color">`(会弹 Windows 系统取色框)。
这里只出"按钮 + 隐藏域",模态框在页尾统一放一份。 */
$cpName = 'color';
$cpValue = (string)old('color', $editing['color'] ?? '#4A7C82');
$cpPalette = \App\Calendar::PRESET_CAL_COLORS;
require __DIR__ . '/../_color_picker.php';
?>
</div>
</div>
<div class="btn-row">
<button class="btn" type="submit"><?= $isEditing ? '保存' : '新建' ?></button>
<?php if ($isEditing): ?>
<a class="btn btn-ghost" href="<?= e(url('calendar/calendars')) ?>">取消</a>
<?php endif; ?>
</div>
</form>
</div>
<?php endif; ?>
<?php /* ==================== 全部日历本(一张清单)====================
⚠️ 显隐与"保存"必须**在同一个表单里**:显隐是整页一次提交的批量操作,
"改名/换色"是单本一次提交(各自的小表单,`form=` 关联到行内那颗按钮)。
⚠️ HTML 表单**不能嵌套** ⇒ 下面每个"改色/删除"的小按钮用 `form="xxx"` 关联
到页尾的隐藏表单,而不是把 <form> 套进大表单里(套了就整个失效)。 */ ?>
<form method="post" action="<?= e(url('calendar/prefs')) ?>" class="cal-books-form" id="calBooksForm">
<?= csrf_field() ?>
<input type="hidden" name="back_view" value="<?= e($backView) ?>">
<?php /* ⚠️ 这个隐藏标记是**必须**的(从 v51 的显示设置页原样搬过来):
HTML 里"一个都没勾"会让浏览器整个省掉 `show[]` 键,
服务端就分不清「用户想全关掉」与「这次请求没打算改显隐」——
没有它的话,将来任何只改默认视图的提交都会把所有日历本静默藏掉。 */ ?>
<input type="hidden" name="show_present" value="1">
<div class="card card-pad" id="cal-books">
<h2 class="cinema-h2">
全部日历本
<span class="hint">勾上就在日历里画出来;取消勾选只是不显示,数据都还在</span>
</h2>
<?php if (!$allRows): ?>
<p class="empty-hint">还没有日历本。</p>
<?php else: ?>
<div class="cal-books">
<?php foreach ($allRows as $i => $c): ?>
<?php
$cid = (int)$c['id'];
$isMine = (bool)$c['_mine'];
$kind = (string)($c['kind'] ?? 'custom');
$isCustom = $kind === 'custom';
$perm = (string)($c['perm'] ?? ($isMine ? 'owner' : 'read'));
$isShown = !in_array($cid, $hiddenIds, true);
// 分组分隔线:从"我的"切到"共享给我的"时插一条
$isFirstShared = (!$isMine && ($i === count($cals)));
?>
<?php if ($isFirstShared): ?>
<div class="cal-books-sep">
<span>别人共享给我的</span>
<span class="cal-books-sep-note">只读的不能改,要改请找主人</span>
</div>
<?php endif; ?>
<div class="cal-book-row<?= $isMine ? '' : ' is-shared' ?><?= $isShown ? '' : ' is-off' ?>"
data-cal-row="<?= $cid ?>">
<?php /* ① 显示/不显示 —— 对**每一行**都在,包括共享进来的
(视图偏好是我自己的,不写对方数据)。 */ ?>
<label class="cal-book-show" title="<?= $isShown ? '取消勾选就不在日历里显示' : '勾上就在日历里显示' ?>">
<input type="checkbox" name="show[]" value="<?= $cid ?>"
<?= $isShown ? 'checked' : '' ?>>
<span class="sr-only">显示「<?= e((string)$c['name']) ?>」</span>
</label>
<?php /* ② 颜色:**我的自定义本**点一下就就地改色(走内置调色模态框);
其它情况只显示色块(系统本、共享本不许改色)。
🔴 系统本(观影/旅行)也不能改色:它们的内容是从模块实时派生的,
颜色是"模块色"的一部分,改了会与观影/旅程页对不上。
🔴 v72:这里用**独立模式**(`$cpName=''`)—— 值不带 name,
不参与上面那张大表单的提交。行内改色走 AJAX 单独落库。
原因:大表单提交的是"显隐"(`show[]`),如果每行还带一个
`name="color"`,一次提交就有 N 个同名 color,
服务端只拿最后一个 ⇒ 改成"最后一本的颜色套到所有行"。 */ ?>
<?php if ($canWrite && $isMine && $isCustom): ?>
<?php
$cpName = ''; // 独立模式:不提交
$cpValue = (string)$c['color'];
$cpId = 'calColor' . $cid;
$cpPalette = \App\Calendar::PRESET_CAL_COLORS;
$cpData = ['cal-id' => $cid];
$cpLabel = '改「' . (string)$c['name'] . '」的颜色';
require __DIR__ . '/../_color_picker.php';
?>
<?php else: ?>
<span class="cal-book-dot" style="background: <?= e((string)$c['color']) ?>;"
title="<?= e((string)$c['color']) ?>"></span>
<?php endif; ?>
<?php /* ③ 名字 */ ?>
<span class="cal-book-name"><?= e((string)$c['name']) ?></span>
<?php /* ④ 归属与权限 —— 用户要的就是这一列:
「在后面标注出来,这个是别人谁谁谁共享给我的?是只读的还是可写的」 */ ?>
<span class="cal-book-tags">
<?php if ($isMine): ?>
<?php if (!$isCustom): ?>
<span class="node-badge node-badge-quiet">系统预置</span>
<?php endif; ?>
<?php else: ?>
<?php
/* 共享过来的行里"人"的字段带前缀(owner_name / owner_nick),
不是 person_name() 直接认的 name / my_nickname —— 在这里适配一次。 */
$ownerName = person_name([
'name' => $c['owner_name'] ?? '',
'my_nickname' => $c['owner_nick'] ?? '',
]);
?>
<span class="cal-owner"><?= e($ownerName) ?> 共享给我</span>
<span class="node-badge <?= $perm === 'write' ? 'is-write' : 'is-read' ?>">
<?= $perm === 'write' ? '可写' : '只读' ?>
</span>
<?php endif; ?>
</span>
<?php /* ⑤ 操作 —— 共享进来的**一个都不给**(用户:「不让他编辑」)。
系统本只给"导出":内容派生自观影/旅程,但导出来是合理的。 */ ?>
<span class="cal-book-acts">
<?php if (!$isMine): ?>
<span class="link-quiet">不可修改</span>
<?php else: ?>
<?php if ($canWrite && $people && $isCustom): ?>
<button type="button" class="link-quiet btn-inline"
data-cal-share-open
data-cal-id="<?= $cid ?>"
data-cal-name="<?= e((string)$c['name']) ?>">共享</button>
<?php endif; ?>
<a class="link-quiet" href="<?= e(url('calendar/export', ['id' => $cid])) ?>">导出</a>
<?php if ($canWrite && $isCustom): ?>
<a class="link-quiet"
href="<?= e(url('calendar/calendars', ['edit' => $cid])) ?>">编辑</a>
<form method="post" action="<?= e(url('calendar/cal_del')) ?>" class="inline-form"
onsubmit="return confirm('删除「<?= e((string)$c['name']) ?>」?本子里的日程会一起删掉。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $cid ?>">
<button class="link-quiet link-danger" type="submit">删除</button>
</form>
<?php endif; ?>
<?php endif; ?>
</span>
<?php /* ⑥ 已共享给谁(只对我自己的本子有意义)—— 挪到第二行,
免得把主行的"名字 + 归属"挤成一团。 */ ?>
<?php if ($isMine && $canWrite): ?>
<?php $sh = $shares[$cid] ?? []; ?>
<?php if ($sh): ?>
<div class="cal-book-shares">
<span class="cal-book-shares-label">已共享给</span>
<?php foreach ($sh as $s): ?>
<?= person_chip($s) ?>
<span class="node-badge"><?= ($s['perm'] ?? '') === 'write' ? '可写' : '只读' ?></span>
<form class="inline-form" method="post" action="<?= e(url('calendar/cal_unshare')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $cid ?>">
<input type="hidden" name="person_id" value="<?= (int)$s['person_id'] ?>">
<button class="link-quiet link-danger" type="submit"
title="取消共享给 <?= e(person_name($s)) ?>">取消</button>
</form>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<p class="small muted" style="margin:12px 0 0">
观影、旅行、有截止时间的任务会自动出现在这里,不用另建本子。
</p>
<div class="btn-row" style="margin-top:14px">
<button class="btn" type="submit"<?= $canWrite ? '' : ' disabled' ?>>保存显示设置</button>
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => $backView])) ?>">取消</a>
</div>
</div>
</form>
<?php /* 「默认打开时看哪个视图」—— 原来和"显示哪些日历本"挤在同一页(显示设置页),
现在日历本清单被合并到本页了,这条偏好也一起搬过来,否则它就成了
唯一一个还留在 `calendar/display` 的东西,用户又要多跳一次。
⚠️ 它**不在**上面那个大表单里(两个不同 action)⇒ 独立小表单。 */ ?>
<div class="card card-pad" style="margin-top:14px">
<h2 class="cinema-h2">默认打开时看
<span class="hint">打开日程页默认落在哪个视图</span></h2>
<form method="post" action="<?= e(url('calendar/prefs')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="back_view" value="<?= e($backView) ?>">
<?php /* ⚠️ **不能**带 `show_present`:不带 ⇒ 服务端不碰显隐(见 index.php 那段注释)。
带了又不带 `show[]` = "把所有本子都藏起来" —— 那是个灾难性副作用。 */ ?>
<div class="cal-set-views">
<?php foreach (\App\Calendar::VIEWS as $vk => $vw): ?>
<label class="cal-set-view">
<input type="radio" name="view" value="<?= e($vk) ?>"
<?= \App\Calendar::defaultView((int)\App\Auth::personId()) === $vk ? 'checked' : '' ?>>
<span><?= e($vw) ?>视图</span>
</label>
<?php endforeach; ?>
</div>
<div class="btn-row" style="margin-top:12px">
<button class="btn btn-sm" type="submit"<?= $canWrite ? '' : ' disabled' ?>>保存默认视图</button>
</div>
</form>
</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>
<?php /* 🔴 v30 用户第 1 条:$sdStage = false —— 导入型上传**不走暂存**。 */ ?>
<?php
$sdName = 'ics'; $sdId = 'im-file'; $sdAccept = '.ics,text/calendar';
$sdMultiple = false; $sdRequired = true; $sdStage = false;
require __DIR__ . '/../_stage_drop.php'; ?>
</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" id="caldav">
<h2 class="cinema-h2">
CalDAV 订阅
<span class="hint">在手机日历 / DAVx⁵ / Thunderbird 里订阅你的日历本</span>
</h2>
<p class="small muted" style="margin:0 0 12px">
填下面这个地址 + 你的<b>姓名或手机号</b> + <b>DAV 口令</b>,就能把「我的日历本」
同步到别的日历程序里。这是<b>只读</b>订阅的 —— 在手机上改日程请回到本站来改。
<br>口令与登录密码是<b>分开的</b>,而且<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>
<div class="btn-row">
<a class="btn btn-sm" href="<?= e(url('settings/me', [], 'dav')) ?>">
<?= !empty($cdHas) ? 'DAV 口令已配置(去管理 / 重置)' : '去生成 DAV 口令' ?>
</a>
<span class="node-badge">口令在「个人设置 → DAV 同步」里管理</span>
</div>
</div>
<?php /* ==================== 共享设置:全页**唯一**一份(v48)====================
与账目页 #ledPanels、名册 #rosterMenus 同一套思路:行上只留一个按钮,
真正的选人表单集中在这里。
🔴 为什么必须只有一份(用户 2026-10-14 第 1 条「弹出框特别慢」的真根因):
原来**每本日历**都内联一套 `_person_picker`,而每个组件都把全部 800 人
化成 JSON + `<noscript>` 兜底。12 本 ⇒ 整页 5.9 MB / 近万个 input
⇒ 浏览器与密码管理器逐个扫描,慢得能感觉到。 */ ?>
<?php if ($canWrite && $people): ?>
<div class="cal-modal" id="calSharePanel" hidden role="dialog" aria-modal="true"
aria-label="共享日历本">
<div class="cal-modal-panel" style="max-width:520px">
<div class="cal-modal-head">
<b>共享「<span class="cal-share-title"></span>」给谁</b>
<button type="button" class="cal-modal-x" data-cal-share-close aria-label="关闭">✕</button>
</div>
<form method="post" action="<?= e(url('calendar/cal_share')) ?>" class="cal-modal-body"
id="calShareForm">
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<div class="dt-range-label">选择要共享给谁</div>
<?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';
?>
<div class="field" style="margin-top:12px">
<label for="cal-share-perm">权限</label>
<select class="select" name="perm" id="cal-share-perm">
<option value="read">只读</option>
<option value="write">可写</option>
</select>
</div>
</form>
<div class="cal-modal-foot">
<?php /* ⚠️ 用 `form="calShareForm"` **显式关联**(表单在 .cal-modal-body 里、
按钮在 .cal-modal-foot 里,不是同一个父节点)。 */ ?>
<button class="btn btn-sm" type="submit" form="calShareForm">共享</button>
<button class="btn btn-ghost btn-sm" type="button" data-cal-share-close>取消</button>
</div>
</div>
</div>
<script>
(function () {
var panel = document.getElementById('calSharePanel');
if (!panel) { return; }
var form = panel.querySelector('form');
var idInp = form.querySelector('input[name="id"]');
var ttl = panel.querySelector('.cal-share-title');
function close() { panel.hidden = true; }
document.addEventListener('click', function (e) {
var t = e.target;
if (!t || !t.closest) { return; }
var open = t.closest('[data-cal-share-open]');
if (open) {
e.preventDefault();
idInp.value = open.getAttribute('data-cal-id') || '';
if (ttl) { ttl.textContent = open.getAttribute('data-cal-name') || ''; }
// 重置选人为"未选":单选组件没选人时显示占位(见 _person_picker.php)
if (window.ppSetSelected) { window.ppSetSelected('person_id', 0, panel); }
var perm = panel.querySelector('select[name="perm"]');
if (perm) { perm.value = 'read'; }
panel.hidden = false;
return;
}
if (t.closest('[data-cal-share-close]')) { close(); return; }
// 点遮罩关闭(点面板内不关)
if (t === panel) { close(); }
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !panel.hidden) { close(); }
});
})();
</script>
<?php endif; ?>
<?php /* ==================== 行内改色:AJAX 单独落库(v72)====================
模态框选完颜色只会改**隐藏域的值**并派发 `cp:change`,真正落库在这里。
🔴 为什么走 AJAX 而不是"顺手提交一下表单":
本页那张大表单管的是**显隐**(`show[]`,整页一次提交)。
如果改个颜色就整页提交,用户刚勾掉/勾上的复选框会跟着一起生效 ——
他本来只想换个色,结果显隐也被保存了,这叫"顺手改了别的东西"。
而且整页刷新会把滚动位置丢掉(日历本可能有几十本)。
⇒ 只发这一本的颜色,只刷这一行,其它什么都不动。
🔴 服务端仍然要校验"这本是不是我的、是不是 custom"(`cal_update` 里本来就有),
这里不做"信任前端"的假设。失败时把按钮恢复成旧颜色并提示。 */ ?>
<script>
(function () {
var form = document.getElementById('calBooksForm');
if (!form) { return; }
var endpoint = <?= json_encode(url('calendar/cal_color'), JSON_UNESCAPED_UNICODE) ?>;
var csrf = <?= json_encode(csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
function toast(msg, isErr) {
// 极简提示:本页没有全局 toast,插一条 2.6 秒后自己消失的浮条
var el = document.createElement('div');
el.className = 'cp-toast' + (isErr ? ' is-err' : '');
el.textContent = msg;
document.body.appendChild(el);
setTimeout(function () {
el.classList.add('is-out');
setTimeout(function () { el.remove(); }, 300);
}, 2600);
}
document.addEventListener('cp:change', function (e) {
var d = e.detail || {};
var field = d.field;
if (!field) { return; }
var calId = field.getAttribute('data-cal-id');
if (!calId) { return; } // 不是日历本行上的调色器(比如新建表单)
var dot = field.querySelector('.cp-trigger-dot');
var old = d.old || '';
var body = new FormData();
/* 🔴 键名必须是 `_csrf` —— 服务端读的是 `$_POST['_csrf']`(见 helpers 的 csrf_field)。
写成 'csrf' 的后果是**每一次都被判"页面已过期"**(419),
而前端只会把它当"改颜色没成功",用户看到的就是"改了但没保存"。 */
body.append('_csrf', csrf);
body.append('id', calId);
body.append('color', d.value);
fetch(endpoint, {
method: 'POST',
body: body,
credentials: 'same-origin',
headers: { 'X-Requested-With': 'fetch' }
}).then(function (r) {
return r.json().catch(function () { return { ok: false }; });
}).then(function (j) {
if (!j || !j.ok) {
// 回滚:值、色块都退回去,别让页面显示一个数据库里没有的颜色
var inp = document.getElementById(d.id);
if (inp) { inp.value = old; }
var txt = field.querySelector('.cp-trigger-hex');
if (txt) { txt.textContent = old; }
if (dot) { dot.style.background = old; }
toast((j && j.error) || '改颜色没成功,再试一次', true);
return;
}
toast('颜色已改');
}).catch(function () {
var inp = document.getElementById(d.id);
if (inp) { inp.value = old; }
var txt = field.querySelector('.cp-trigger-hex');
if (txt) { txt.textContent = old; }
if (dot) { dot.style.background = old; }
toast('网络不通,颜色没保存', true);
});
});
})();
</script>
<?php /* 页尾:内置调色模态框(全页**唯一**一份) */ ?>
<?php $cpPalette = \App\Calendar::PRESET_CAL_COLORS;
$cpModalTitle = '选个颜色';
require __DIR__ . '/../_color_picker_modal.php'; ?>
<?php unset($allRows, $cals, $shared, $hiddenIds, $backView, $canWrite, $totalCount, $isEditing); ?>