diff --git a/app/MovieStats.php b/app/MovieStats.php index 346874b..461dd45 100644 --- a/app/MovieStats.php +++ b/app/MovieStats.php @@ -72,7 +72,56 @@ final class MovieStats return $st->fetchAll(); } - /** 按观影人:参与了多少场(带头像,供统计页画条;名称按「我叫他什么」显示) */ + /** + * 月份 / 星期的**图表配色**(v40)。 + * + * 🔴 用户 2026-10-10:「观影统计里面,月份分布和星期分布,12 个月用 12 种不同的颜色, + * 星期 7 天用 7 天用 7 种不同的颜色。」 + * + * ⚠️ 放在这里(而不是模板里各写一份)的理由:月份分布与星期分布分别出现在 + * `report.php`(年度报告)与 `stats.php`(月度趋势)两处,颜色必须是**同一套**, + * 否则同一个月在两个页面是两种颜色 —— 那种不一致用户一眼就会看出来。 + * ⚠️ 色板是**环绕色相**取的 12 档(红→橙→黄→绿→青→蓝→靛→紫→品红), + * 相邻两档明度接近、色相拉开,在宣纸底上都能看清; + * 没有用"彩虹高饱和"是因为那和大面积暖棕的品牌底色打架。 + */ + public const MONTH_COLORS = [ + 1 => '#C0533F', '#D2703C', '#DFA13A', '#C7B23E', + '#8CB04A', '#5FA05C', '#3F9E85', '#3E8FA6', + '#4478B5', '#5B63B0', '#8455AC', '#B0548F', + ]; + + /** 星期配色:日 → 六(7 档,与月份色板取同一条色相环上均匀间隔的 7 个点) */ + public const WEEKDAY_COLORS = [ + '日' => '#C0533F', '一' => '#D98A3C', '二' => '#C7B23E', + '三' => '#5FA05C', '四' => '#3E8FA6', '五' => '#4478B5', + '六' => '#8455AC', + ]; + + /** 取某个月(1-12)的颜色;越界回退到品牌色,绝不返回空串(那会画成黑块)。 */ + public static function monthColor(int $m): string + { + return self::MONTH_COLORS[$m] ?? '#A84A28'; + } + + /** 取某个星期('日'..'六')的颜色 */ + public static function weekdayColor(string $w): string + { + return self::WEEKDAY_COLORS[$w] ?? '#A84A28'; + } + + /** 按观影人:参与了多少场(带头像,供统计页画条;名称按「我叫他什么」显示) + * + * 🔴 v40(用户 2026-10-10):「观影统计里面,如果这个人在名册里面,但是他一部电影的 + * 记录也没有,就不要在下面的这个按观影人统计里面出现了,太长了。」 + * ⇒ **只列出真的参与过至少一场的人**(`HAVING cnt > 0`)。 + * + * ⚠️ 为什么原来是 LEFT JOIN 全列:那是"按人统计"字面意义上的做法,也方便看出 + * "谁一场都没看过"。但用户的实际痛点是**列表太长**(名册上千人时, + * 这个区块会被几百行"0 场"淹没,真正有数据的人反而找不到)。 + * ⇒ 按用户要求过滤;"0 场"这件事本来也不是这个页面要回答的问题。 + * ⚠️ `HAVING` 而不是 `WHERE cnt > 0`:`cnt` 是聚合结果,`WHERE` 阶段还不存在。 + * 写成 `WHERE` 会被 MySQL 报 `Unknown column 'cnt'`。 */ public static function byPerson(int $familyId, int $viewerId = 0): array { $st = Database::get()->prepare( @@ -83,6 +132,7 @@ final class MovieStats LEFT JOIN nicknames n ON n.target_id = p.id AND n.viewer_id = ? WHERE p.family_id = ? AND p.kind = 'member' GROUP BY p.id, p.name, p.avatar, n.nickname + HAVING cnt > 0 ORDER BY cnt DESC, p.id" ); $st->execute([$familyId, $viewerId, $familyId]); diff --git a/app/View.php b/app/View.php index 090c373..afa6620 100644 --- a/app/View.php +++ b/app/View.php @@ -28,6 +28,31 @@ final class View require dirname(__DIR__) . '/templates/layout.php'; } + /** + * 只渲染**模板本身**,返回字符串(不套 layout)。 + * + * 🔴 v40(用户 2026-10-10):「观影详情里面的编辑按钮应该也是弹出的,和新增是一样的, + * 编辑完以后,页面动态更新。」 + * ⇒ AJAX 保存**校验失败**时,必须把这同一个表单**原样重画**回去 + * (字段报错、`old()` 回填、附件/海报面板全靠模板自己产出)。 + * 如果不重画而只回一个错误文案,用户就会看到"值全丢了、红字也没有"—— + * 与整页路径(`View::render`)行为不一致,那种"两条路各说各话"的 bug 最难查。 + * + * ⚠️ 与 `render()` 的唯一区别:不 require layout.php。 + * layout 会输出 ``/顶栏,塞进一段 innerHTML 里会直接坏掉结构。 + */ + public static function renderPartial(string $tpl, array $data = []): string + { + $file = dirname(__DIR__) . '/templates/' . $tpl . '.php'; + if (!is_file($file)) { + throw new \RuntimeException('模板不存在:' . $tpl); + } + extract($data, EXTR_SKIP); + ob_start(); + require $file; + return (string)ob_get_clean(); + } + /** * 独立的整页(登录、安装、错误),不套顶栏 —— 但**仍然要是一份完整 HTML**。 * diff --git a/assets/css/app.css b/assets/css/app.css index 486f600..f96ddcb 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -15,6 +15,11 @@ --brand-ink:#6E3820; --green: #6F9E5C; --red: #C0533F; + /* 🔴 v40(用户 2026-10-10):「影院管理部分…编辑使用蓝色的背景,删除使用红色的背景。」 + ⇒ 新增一个中性的"操作蓝"(不是链接蓝、也不是模块品牌色)—— 它是**语义色**: + 蓝=编辑、红=删除,全站统一,跟着模块换肤也不该变。 + ⚠️ 暖纸底上要够暗才压得住白字:这里取 #2F6FB0(对比度 ≈ 4.9:1)。 */ + --blue: #2F6FB0; --line: rgba(44, 35, 29, .10); --line-2: rgba(44, 35, 29, .06); --radius: 16px; @@ -1422,6 +1427,26 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb, .page-head .actions > a.btn-danger:not(.btn-icon):hover, .page-head .actions > form > .btn-danger:not(.btn-icon):hover, .page-head .actions > form > button.btn-danger:not(.btn-icon):hover { background: rgba(192, 83, 63, .09); } + +/* 层 4:**实心**底色按钮(v40 用户要求)。 + 原话(2026-10-10):「删除按钮就用红色背景颜色…影院管理部分…编辑使用蓝色的背景, + 删除使用红色的背景。」 + ⇒ 这两个要的是**实心色块**(一眼分得清),而不是层 3 那种淡色描边。 + ⚠️ 为什么不直接改 `.btn-danger`:全站十来处「删除」都在用它,改成实心会到处变样; + 而且**页头动作区的层 1**(`.actions > form > button.btn`,特异性 0,4,1) + 会盖过基类 `.btn-del`(0,1,0)—— 必须在同一层里再写一条才生效(与层 3 同一个坑)。 */ +.page-head .actions > .btn-del:not(.btn-icon), +.page-head .actions > a.btn-del:not(.btn-icon), +.page-head .actions > form > .btn-del:not(.btn-icon), +.page-head .actions > form > button.btn-del:not(.btn-icon) { + background: var(--red); color: #fff; border-color: transparent; +} +.page-head .actions > .btn-edit:not(.btn-icon), +.page-head .actions > a.btn-edit:not(.btn-icon), +.page-head .actions > form > .btn-edit:not(.btn-icon), +.page-head .actions > form > button.btn-edit:not(.btn-icon) { + background: var(--blue); color: #fff; border-color: transparent; +} /* 「设置」图标按钮:**唯一无色的**(白底描边、方形),与上面所有彩色按钮区分。 padding 归零、做成正方形,但高度必须仍是 --act-h(= 文字按钮的 43.8px)—— 写死过 40px,比旁边按钮矮 3.8px,是用户反复反馈的那个问题。 */ @@ -5323,3 +5348,81 @@ h5.md-h, h6.md-h { font-size: 14px; } (它们本来就只有一块,当时的"并排网格"里只有一个孩子, grid 让它只占半宽 —— 那正是"海报的上传框比简介窄"的原因)。 ⚠️ 不留悬念:全站搜 `.mv-pair` 应该**零命中**(模板与 CSS 都清干净)。 */ + +/* ============================================================================ + 🔴 v40(用户 2026-10-10)观影模块四处界面反馈 + ============================================================================ */ + +/* ---- ① 影院管理表格:操作列不再被挤成两行 ---- + 用户原话:「影院管理部分,注意控制列的宽度,因为最右侧的操作、编辑和删除两个按钮, + 已经被挤成两行了,尝试让编辑和删除排在一行,宽度给它留够。」 + 🔴 真因:`.table` 是 `width:100%` + 自动布局,浏览器按内容分配列宽 —— + 长地址那一列会把「操作」列压到只剩几十像素,两个按钮只好换行。 + ⇒ 两件事一起做才稳: + ① 表格改 `table-layout: fixed`(列宽由我们给,不再被内容牵着走); + ② 「操作」列给一个**够放两个按钮的最小宽度**(编辑 48 + 间距 8 + 删除 48 + + 单元格左右 padding 16 ≈ 128,留到 132 更从容)。 + ⚠️ 为什么不只是 `white-space:nowrap`:那会把列撑成"刚好够", + 但**表格还是按内容分配**,窗口一窄就又轮到别的列挤压它; + 固定布局才是真的把这件事钉死。 */ +.cn-table { table-layout: fixed; } +.cn-table .cn-col-name { width: 30%; } +.cn-table .cn-col-place { width: 15%; } +.cn-table .cn-col-addr { width: 32%; } +.cn-table .cn-col-count { width: 10%; } +.cn-table .cn-col-ops { width: 132px; } +.cn-table td.cn-addr { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.cn-ops { + display: flex; align-items: center; gap: 8px; + /* 🔴 不换行是本条的正题 */ + flex-wrap: nowrap; white-space: nowrap; +} +.cn-ops > form { display: inline-flex; margin: 0; flex: 0 0 auto; } +.cn-ops > .btn { flex: 0 0 auto; } +/* 窄屏:地址列让位(它最长、也最不影响判断),操作列宽度保持不变 */ +@media (max-width: 720px) { + .cn-table .cn-col-name { width: 34%; } + .cn-table .cn-col-place { width: 18%; } + .cn-table .cn-col-addr { width: 22%; } + .cn-table .cn-col-count { width: 12%; } +} + +/* ---- ② 实心语义按钮:编辑=蓝、删除=红 ---- + ⚠️ 单独定义(不去改 `.btn-danger` 的既有语义):`.btn-danger` 是**淡红描边**的 + "危险动作",全站十来处「删除」都在用它;用户这次要的是**实心色块**, + 改 `.btn-danger` 会让所有页面一起变样。 + 三处都要写:裸类 / `a.` / `button.` —— 与页头动作区那套同一个理由 + (`.btn`、`a.btn`、`button.btn` 特异性不同,漏一种就有一种是错的)。 */ +.btn-edit, a.btn-edit, button.btn-edit { + background: var(--blue); color: #fff; border-color: transparent; +} +.btn-del, a.btn-del, button.btn-del { + background: var(--red); color: #fff; border-color: transparent; +} +/* ⚠️ hover 统一交给文件靠后的「全站悬停统一」段(有底色的一律同色系加深)—— + 但那段是按 `.btn-ok/.btn-del/...` 的类名白名单写的,`.btn-edit` 是新的, + 要在这里补一条,否则它会退回 `.btn` 的默认(品牌色底),看着像变了个按钮。 */ +.btn-edit:hover { filter: brightness(.92); } + +/* ---- ③ 观影详情页 = 列表页的头部口径 ---- + 用户原话:「观影详情页面左上角的观影记录,应该和观影的列表页面里面保持一致。」 + 列表页是 `

观影记录

` + sub;详情页原来只有一个 ``, + 没有 h1 ⇒ 左上角看起来"不像是同一件事"。模板已补上 h1, + 这里只处理**窄屏**:h1 与 sub 在页头会挤,sub 允许收缩并省略。 */ +.page-head > .sub { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* ---- ④ 高级搜索的日期范围:换成 AntD RangePicker(纯日期)---- + 🔴 v40(用户 2026-10-10):「高级搜索里面的日期范围,也使用我们最新的控件。」 + 挂上之后把**原生那两个日期框收起来**(它们仍是 GET 字段,只是不显示)。 + ⚠️ 与原观影那对 `.dt-range.is-antd > .dt-input` 用同一套"clip 收起"手法: + 不用 `display:none`(保持可聚焦,避免将来加 required 时不可聚焦的坑)。 */ +.date-antd-field .date-range.is-antd > .input, +.date-antd-field .date-range.is-antd > .to { + position: absolute; width: 1px; height: 1px; + overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; + border: 0; padding: 0; margin: -1px; +} +.date-antd-field .date-range.is-antd { display: block; } +/* 纯日期版控件与观影那边同款外观(同高、同圆角、同描边)—— 复用 .dt-antd 的规则, + 这里只补"它是块级、占满字段宽度"这一点(.dt-antd 原为 flex 子项)。 */ +.dt-antd-dateonly { display: block; width: 100%; } diff --git a/assets/js/app.js b/assets/js/app.js index 67109dd..c05ed72 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1298,7 +1298,14 @@ ⚠️ 顺序:**先**把原生下拉绑好(bindParts)、**再**升级成 antd —— 这样即使 antd 拉不动,用户手上的控件也是完整可用的。 */ var antdHosts = document.querySelectorAll('[data-dt-antd]'); - bootAntd(antdHosts); + /* 🔴 v40(用户 2026-10-10):「高级搜索里面的日期范围,也使用我们最新的控件。」 + ⇒ 高级搜索的日期范围是**纯日期**(GET 参数 `date_from` / `date_to`, + 没有"时:分"),与观影表单那对"时刻"不是一个东西 ⇒ 单独一条标记 + `[data-antd-daterange]`,挂的是**不带 showTime** 的 RangePicker。 + ⚠️ 两条路共用下面同一套"按需加载 + 中文 locale"的 ensure(), + 只是挂载函数不同 —— 别复制一份加载逻辑出来(那必然有一天两边不同步)。 */ + var antdDateRanges = document.querySelectorAll('[data-antd-daterange]'); + if (antdHosts.length || antdDateRanges.length) { bootAntd(antdHosts, antdDateRanges); } /* ⚠️ 只要还有 month 控件就得拉 Semi;纯 .dt-input 页面(绝大多数)不用拉。 */ if (!hasMonth) { @@ -1506,7 +1513,29 @@ y.addEventListener('change', function () { rebuildDays(true); sync(); }); mo.addEventListener('change', function () { rebuildDays(true); sync(); }); d.addEventListener('change', sync); - rebuildDays(false); // 首屏:把"日"列按当前年月铺对 + /* 🔴🔴 v40 修一个**数据丢失级**的真 bug(v37 引入,影响**所有** `.dt-range` 页面): + 这里原来是 `rebuildDays(false)` —— `keep=false` 会**把"日"下拉清空** + (它是"重建这一列"的写法,本意用于"换了年月要重新铺天数")。 + 紧接着的 `sync()` 看到 `y/mo` 有值、`d` 是空的 ⇒ 算出 `v=''` + ⇒ **把隐藏的 `*_date` 写成了空串**。 + + 后果:任何带着已存时间的编辑表单,只要 JS 跑起来(正常情况都会), + 日期就在用户**还没动任何东西**时被抹掉了 —— 用户提交后库里 `starts_at` + 变成 NULL。实测(`_dbg_submit.py`):打开编辑弹框后 + `starts_at_date` 读出来是 `''`(而年/月下拉与 antd 显示都还是正确的 + `2026 / 2026-10-05`),点保存 → 库里的 `starts_at` 从 + `2026-10-05 19:30:00` 变成 **NULL**。 + + ⚠️ 为什么以前没被发现:e2e 是**服务端**驱动的(直接 POST 字段), + 不经过 JS;而真机探针当时只验"控件可见可点"与"改值能写回", + **没验"什么都不动时值还在不在"**。 + ⇒ 补一条回归断言(见 `_fb_b40_movie.py`:打开编辑弹框时 + `starts_at_date` 必须非空、提交后库里时间不丢)。 + + ⇒ 首屏应当 `rebuildDays(true)`:把**服务端已经选好**的那一天保留下来。 + (`true` 的语义是"能保留就保留":那天在新月份里不存在时仍然清空 —— + 31 号 → 2 月那种,见上面 `d.value` 那行的判断。) */ + rebuildDays(true); // ⚠️ 不是 false!见上面那段说明(传 false 会把日期抹掉) sync(); // 供 fbSetDT / writeDT 程序化改值之后让三个下拉跟上 @@ -1560,8 +1589,10 @@ ⚠️ 这个适配层**不认识的页面不受影响**:没有 `[data-dt-antd]` 就直接返回。 ============================================================================ */ - function bootAntd(hosts) { - if (!hosts.length) { return; } + function bootAntd(hosts, dateRanges) { + hosts = hosts || []; + dateRanges = dateRanges || []; + if (!hosts.length && !dateRanges.length) { return; } var meta = document.querySelector('meta[name="fb-antd"]'); if (!meta) { return; } var conf; @@ -1600,13 +1631,82 @@ Array.prototype.forEach.call(hosts, function (box) { if (mountAntd(box)) { done++; } }); - if (window.console && done) { console.info('AntD RangePicker 已挂上 ' + done + ' 处'); } + Array.prototype.forEach.call(dateRanges, function (el) { + if (mountAntdDateRange(el)) { done++; } + }); + if (window.console && done) { console.info('AntD 日期控件已挂上 ' + done + ' 处'); } }).catch(function (err) { // 拉不动就留在自建下拉上 —— 渐进增强的兜底,不弹任何打扰 if (window.console) { console.warn('AntD DatePicker 未启用:', err && err.message); } }); } + /** + * 把一对**纯日期**输入(``)升级成 + * Ant Design 的 RangePicker(**不带时间列**)。 + * + * 🔴 v40(用户 2026-10-10):「高级搜索里面的日期范围,也使用我们最新的控件。」 + * ⚠️ 与 `mountAntd()`(观影那对"时刻")的分工: + * · 那个操作的是 `.dt-input[data-dt]`(隐藏 `*_date` + `*_hour/_minute` 契约); + * · 这个直接操作两个 `input[type=date]`(GET 表单参数),**写成 `YYYY-MM-DD`**。 + * 两边的"值载体"完全不同 ⇒ 共享加载/中文逻辑,但**回写各自实现**。 + * ⚠️ 原生输入**不删**:用 `class="dt-antd-hidden"` 视觉收起(仍是表单字段、仍可聚焦)。 + * 这样 antd 拉不动时,用户看到的就是原来那两个原生日期框 —— 不倒退。 + */ + function mountAntdDateRange(el) { + if (!el || el.getAttribute('data-antd-done')) { return false; } + var fm = el.querySelector('input[name$="date_from"], input[name="date_from"]'); + var to = el.querySelector('input[name$="date_to"], input[name="date_to"]'); + // 兼容调用方自定义字段名:按 DOM 顺序取前两个 date 输入 + if (!fm || !to) { + var all = el.querySelectorAll('input[type="date"]'); + if (all.length < 2) { return false; } + fm = all[0]; to = all[1]; + } + + var host = document.createElement('span'); + host.className = 'dt-antd dt-antd-dateonly'; + el.insertBefore(host, el.firstChild); + + function toDayjs(str) { + var m = (str || '').trim().match(/^(\d{4})-(\d{2})-(\d{2})$/); + return m ? window.dayjs(m[1] + '-' + m[2] + '-' + m[3], 'YYYY-MM-DD') : null; + } + var last = ['', '']; + function writeBack(strs) { + var s0 = (strs && strs[0]) || '', s1 = (strs && strs[1]) || ''; + if (s0 === last[0] && s1 === last[1]) { return; } + last = [s0, s1]; + // 直接写原生字段(GET 提交的就是它们) + fm.value = s0; to.value = s1; + fm.dispatchEvent(new Event('change', { bubbles: true })); + to.dispatchEvent(new Event('change', { bubbles: true })); + } + + var v0 = (fm.value || '').trim(), v1 = (to.value || '').trim(); + try { + var picker = React.createElement(antd.DatePicker.RangePicker, { + format: 'YYYY-MM-DD', + allowClear: true, + placeholder: ['开始日期', '结束日期'], + defaultValue: (v0 || v1) ? [toDayjs(v0 || v1), toDayjs(v1 || v0)] : undefined, + // 与观影那对同一条实测结论:单端改动走 onCalendarChange,两端齐了才 on + onCalendarChange: function (vals, strs) { writeBack(strs); }, + onChange: function (vals, strs) { writeBack(strs); } + }); + var loc = (window.antd.locales && window.antd.locales.zh_CN) || undefined; + if (loc) { picker = React.createElement(antd.ConfigProvider, { locale: loc }, picker); } + ReactDOM.createRoot(host).render(picker); + } catch (e) { + if (window.console) { console.warn('antd 日期范围挂载失败,留在原生输入:', e && e.message); } + host.parentNode.removeChild(host); + return false; + } + el.classList.add('is-antd'); + el.setAttribute('data-antd-done', '1'); + return true; + } + /** 把 dayjs 切到简体中文(星期/月份/相对时间都由它出)。 * ⚠️ 拿不到就静默跳过 —— 那只会让星期显示成英文,不该拦住整个控件。 */ function applyDayjsZh() { @@ -1871,6 +1971,21 @@ return true; }; + /** + * 对外接口:**重新扫描并挂 antd RangePicker**(v40 新增)。 + * + * 为什么要它:观影详情页的「编辑」现在弹出一个**动态插入**的弹框(AJAX 校验失败 + * 时还会整块重画),里面那对起止控件是**后来才出现在 DOM 里**的 —— + * 而 `bootAntd()` 只在页面初始化时扫一次 `[data-dt-antd]`,扫不到它们 + * ⇒ 用户看到一个空白的日期行(原生下拉被 CSS 收起、antd 又没挂上)。 + * ⚠️ `mountAntd` 有 `data-antd-done` 幂等标记,重复调用不会挂两遍。 + */ + window.fbBootAntd = function () { + bootAntd(document.querySelectorAll('[data-dt-antd]'), + document.querySelectorAll('[data-antd-daterange]')); + return true; + }; + window.fbSetDT = function (name, str) { if (!name) { return false; } var box = null; diff --git a/index.php b/index.php index a68aca0..f154c0c 100644 --- a/index.php +++ b/index.php @@ -2857,6 +2857,37 @@ switch ($r) { if ($errors) { set_old($_POST); + /* 🔴🔴 v40(用户 2026-10-10):「观影详情里面的编辑按钮应该也是弹出的, + 和新增是一样的,编辑完以后,页面动态更新。」 + ⇒ 详情页的编辑现在走**弹框 + AJAX**。校验失败时**两条路必须给同一件事**: + · AJAX(弹框里提交)→ 回 JSON,带上**重画后的整个弹窗体**, + 前端直接塞回弹框容器 ⇒ 用户看到红字与填过的值(与整页路径一模一样); + · 普通 POST(无 JS 兜底)→ 照旧整页重画。 + ⚠️ 判断放在**这里**而不是函数入口:只有"真的校验失败"才需要重画, + 成功路径不管谁来的都是 302(AJAX 那边靠 `fetch` 跟到最终页再替换)。 + ⚠️ 弹窗体用 `$modalOnly = false` 渲染(要那个 cal-modal **壳**), + 因为详情页就是靠这一整块壳来弹的(列表页内嵌走的是另一条 + `$mvModalOnly = true`,别混)。 */ + $isAjax = (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest'); + if ($isAjax) { + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'ok' => false, + 'errors' => $errors, + // ⚠️ 关键:用 `$mvModalForce = true` 让模板**一定**输出弹框壳 + // 并处于打开态(编辑态的 `$mvOpen` 本来是 false,见 form.php)。 + 'modal' => View::renderPartial('movies/form', [ + 'm' => $m, + 'people' => Person::roster($familyId, $me, 'member', ''), + 'cinemas' => Cinema::all($familyId), + 'errors' => $errors, + 'mvModalOnly' => true, // 不输出 page-head(塞回弹框里) + 'mvModalForce' => true, // 强制成"弹框壳" + 'mvOpen' => true, // 且保持打开(用户正在看它) + ]), + ], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); + exit; + } View::render('movies/form', [ 'title' => $isEdit ? '编辑观影记录' : '记一场电影', 'm' => $m, diff --git a/templates/movies/cinemas.php b/templates/movies/cinemas.php index 5303767..1a76256 100644 --- a/templates/movies/cinemas.php +++ b/templates/movies/cinemas.php @@ -79,9 +79,24 @@ $canWrite = \App\Auth::canWrite();

还没有影院,先在上面添加一家。

- + +
- + + + + + + + @@ -91,17 +106,17 @@ $canWrite = \App\Auth::canWrite(); - + - diff --git a/templates/movies/form.php b/templates/movies/form.php index ae948ad..fd48c91 100644 --- a/templates/movies/form.php +++ b/templates/movies/form.php @@ -75,18 +75,32 @@ $mvModalOnly = !empty($mvModalOnly); ?> ⚠️ 表单本身**一个字没动**(字段名 / 服务端契约 / CSRF / enctype 全照旧), 只是把它的容器从"页面上的卡片"换成"浮层里的面板"。 */ ?> - + 外部表),所以不用 !important。 */ ?>

月份分布 每个月看几场

$cnt): ?>
- + 月
@@ -90,7 +98,9 @@ $fmtDate = static function (?string $d): string {
周 - +
diff --git a/templates/movies/search.php b/templates/movies/search.php index 5c0e763..a556550 100644 --- a/templates/movies/search.php +++ b/templates/movies/search.php @@ -49,12 +49,19 @@ require __DIR__ . '/../_person_picker.php'; ?>
-
+ +
-
- +
+ 至 - +
diff --git a/templates/movies/show.php b/templates/movies/show.php index 096f34a..74ea9b5 100644 --- a/templates/movies/show.php +++ b/templates/movies/show.php @@ -140,19 +140,34 @@ $commentBox = static function (int $reviewId, int $parentId, string $summary) us }; ?>
- 观影记录 + 观影记录`(没有 h1)改成与列表页**同款** + `

观影记录

`,片名降级到 sub —— 两页左上角看起来就是一件事; + ② 「编辑」「返回观影」用**裸 `.btn`**(= 列表页「记一场电影」那个品牌实心款); + ③ 「删除」用 `.btn-del`(**实心红底**,不是原来那支淡红描边)。 */ ?> +

观影记录

+
- 编辑 + +
- + - 返回观影 + 返回观影
@@ -557,3 +572,199 @@ $commentBox = static function (int $reviewId, int $parentId, string $summary) us })(); + + + + + + + diff --git a/templates/movies/stats.php b/templates/movies/stats.php index 6fba8f0..16cc7c9 100644 --- a/templates/movies/stats.php +++ b/templates/movies/stats.php @@ -105,12 +105,17 @@ $wd = static function (?string $d): string {

月度趋势 最近 12 个月

+
- +
- +
名称所在地方详细地址观影操作
名称所在地方详细地址观影操作
场 -
- 编辑 +
+
+ 编辑
- +