diff --git a/app/Calendar.php b/app/Calendar.php index 7ca36fe..29e64f9 100644 --- a/app/Calendar.php +++ b/app/Calendar.php @@ -868,6 +868,18 @@ final class Calendar return in_array($v, ['confirmed', 'tentative', 'cancelled'], true) ? $v : 'confirmed'; } + /** + * 🔴 v43(用户第 6 条):日程状态的**中文说法** —— 条目详情浮层里要显示"状态"。 + * 与 `templates/calendar/_event_fields.php` 那个 ` 视觉收起** —— 它仍是值载体(antd 通过 change 写它),只是不再显示给用户。 - ⚠️ 用 clip 收起(不是 display:none):保留可聚焦、可被表单读到, - 与 v40 高级搜索那处的取舍一致。 */ -.dt-input.is-antd > .dt-native { position: absolute; width: 1px; height: 1px; - overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px; } + ⚠️ 用 clip 收起(不是 display:none):保留可聚焦、可被表单读到。 + 🔴🔴 v43(用户 2026-10-11 第 1 条):「日期那里多了一个控件, + 只保留我们最新的控件就行,之前那种控件删除」——真根因有两层: + ① 纪念日的原生框当初**没包在 `.dt-native` 里** ⇒ 这条选择器根本不命中 + (模板已补上包装); + ② **更要命的一层**:`.dt-input .dt-native { display: contents }`(第 619 行) + 会让这个包装盒**自己不生成盒子**、子元素"提升"成 `.dt-input` 的直接布局项 —— + 此时给它 `position:absolute; width:1px` **完全无效**(它没有盒子可定位), + 原生框照样按 flex 项排出来 ⇒ 两个控件并排。 + ⇒ 修法:这里必须把 `display` **从 contents 改成 block**,定位才生效。 + (只写 position/width/height 是不够的 —— 这是我第一版的错,探针当场抓到。) */ +.dt-input.is-antd > .dt-native { + display: block; position: absolute; width: 1px; height: 1px; + overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px; +} .dt-input.is-antd > .dt-antd { display: block; width: 100%; } .dt-input.dt-antd-single { display: block; width: 100%; } +/* 兜底:万一哪天原生框又变成 `.dt-input` 的**直接子元素**(不包 .dt-native 了), + 这条按直接子元素也能把它收起来。 */ +.dt-input.is-antd > input[type="date"] { + display: block; position: absolute; width: 1px; height: 1px; + overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px; +} /* ---- v42(用户第 6 条)简介为空时的那句可点占位 ---- 与 `.trip-sum` 同一视觉位置,只是颜色淡一档(它是要点进去补的提示,不是内容)。 */ .trip-sum-empty { margin: 10px 0 0; font-size: 13px; color: var(--muted); } .trip-sum-empty a { color: var(--brand); } + +/* ============================================================================ + 🔴 v43(用户 2026-10-11 第 6 条)条目详情浮层 + ---------------------------------------------------------------------------- + 与 .cal-modal 同一形态(遮罩 + 居中面板 + 底栏),只是层级更高一点: + 它可能从**另一个浮层**里打开(例如详情页弹框里点了条目), + 所以要在 200 之上;但低于"看图"的灯箱(300)/预览层(320)。 + ⚠️ 与 .cn-modal(260)、.crop-overlay(260) 同层 —— 它们不会同时出现。 + ⚠️ 面板用 flex 列 + body 滚动,长内容(编辑表单)才有足够的可视区。 */ +.fb-entry-modal { + position: fixed; inset: 0; z-index: 260; + display: flex; align-items: flex-start; justify-content: center; + padding: 24px 16px; overflow-y: auto; + background: rgba(44, 35, 29, .55); overscroll-behavior: contain; +} +.fb-entry-modal[hidden] { display: none; } +.fb-entry-panel { width: min(620px, 100%); max-height: calc(100vh - 64px); } +/* 编辑态下正文里就是一张完整表单(内边距由它自己那份 CSS 给)—— + 这里只保证它不双份留白。 */ +.fb-entry-panel .cal-modal-body > .mv-form-inner, +.fb-entry-panel .cal-modal-body > .nf-wrap { padding: 0; } +@media (max-width: 640px) { + .fb-entry-panel { width: 100%; } +} diff --git a/assets/js/entry_popup.js b/assets/js/entry_popup.js new file mode 100644 index 0000000..58ceb5c --- /dev/null +++ b/assets/js/entry_popup.js @@ -0,0 +1,245 @@ +/* ============================================================================ + 🔴 v43(用户 2026-10-11 第 6 条)—— 条目详情浮层 + ---------------------------------------------------------------------------- + 用户原话: + 「首页上的日程、首页上的近期任务,以及日程里面的日程,点击它们的时候, + 都会弹出一个框显示更详细的信息。上面有不同的功能按钮,比如说详细信息, + 在日程里面,如果是观影啊、旅程啊,点击详细信息就会进入到观影或者旅程的 + 那个相关的页面。如果是日程的话,点击详细信息就进到日程的详细页面。 + 当然还可以直接点击编辑,点击编辑不同的来源就进入不同的编辑页面, + 就在当前的页面弹窗,把之前的窗给去掉,然后弹出编辑的那个框来就可以了。」 + + ⇒ 一个浮层 + 事件委托,服务全站三种入口: + · 首页「今天的日程」(`.ag-ev` / `.ag-allday-item`) + · 首页「近期任务」(`.tk-row` 的标题) + · 日程页月/周/日视图的条目(`.cal-ev` / `.tg-block`) + + 设计取舍(每条都对应一个踩过的坑): + ① **事件委托挂 document**:列表会被 AJAX 换掉(新建日程后就地插入、 + 首页任务勾选后重画),一次性绑定永久有效(v40 的教训)。 + ② **点条目 = 弹浮层,不跳页**:拦掉 `` 的默认行为。 + ⚠️ 必须按 `[data-fb-entry]` 精确匹配 —— 不能笼统拦 `.cal-ev`: + 页面里还有"点某天新建日程"那套逻辑(它同样排除 `.cal-ev`)。 + ③ **数据全在 `data-fb-entry` 上**:浮层不发请求就能画出来,秒开; + 服务端不参与"点开一条"这个动作。 + ④ **「编辑」就地换内容**:取该来源的编辑表单片段(`?embed=1`)。 + 用户原话「把之前的窗给去掉,然后弹出编辑的那个框来」—— + 所以是"正文整块换掉",不是叠第二层浮层。 + ⑤ **保存走原生提交**:四个来源的 update 路由本来就收普通 POST 并跳转, + 不依赖任何页面自带脚本 ⇒ 浮层在任何页面都能保存成功。 + (自己写 AJAX 保存要为四个来源各写一套"成功后回哪"的规则,得不偿失。 + 原生提交会整页刷新 —— 对"改了数据"这件事来说,刷新一次反而更清楚。) + ⑥ **Esc 只关最上面那层**:antd 面板开着时先让它收(v38/v39 的坑)。 + ⚠️ 本脚本**全站加载**(layout.php):三处入口分散在不同页面,按需加载的判据 + 会在这些页面之间来回漏;它只有几 KB,不值得为省这点流量赌漏加载。 + ============================================================================ */ +(function () { + 'use strict'; + + var modal = null; + var detailCache = null; /* 详情态的数据(从编辑态「返回」时用它重画) */ + + function M() { if (!modal) { modal = document.getElementById('fbEntryModal'); } return modal; } + function bodyEl() { return document.getElementById('fbEntryBody'); } + function titleEl(){ return document.getElementById('fbEntryTitle'); } + function tipEl() { return document.getElementById('fbEntryTip'); } + function byId(id) { return document.getElementById(id); } + + function esc(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, '''); + } + + function setTip(msg, err) { + var t = tipEl(); + if (!t) { return; } + t.textContent = msg || ''; + t.className = 'cal-modal-tip' + (err ? ' err' : ''); + } + + /* ---- 底栏两种形态 --------------------------------------------------------- */ + function footDetail() { + ['fbEntryDetail', 'fbEntryEdit'].forEach(function (id) { + var e = byId(id); if (e) { e.hidden = false; } + }); + ['fbEntryBack', 'fbEntrySave'].forEach(function (id) { + var e = byId(id); if (e) { e.hidden = true; } + }); + } + function footEdit() { + ['fbEntryDetail', 'fbEntryEdit'].forEach(function (id) { + var e = byId(id); if (e) { e.hidden = true; } + }); + ['fbEntryBack', 'fbEntrySave'].forEach(function (id) { + var e = byId(id); if (e) { e.hidden = false; } + }); + } + function footNone() { /* 载入中 / 出错:全都先藏起来 */ + ['fbEntryDetail', 'fbEntryEdit', 'fbEntryBack', 'fbEntrySave'].forEach(function (id) { + var e = byId(id); if (e) { e.hidden = true; } + }); + } + + /* ---- 详情态渲染 ----------------------------------------------------------- */ + function rowsHtml(list) { + var html = ''; + (list || []).forEach(function (r) { + if (!r || r[1] === '' || r[1] == null) { return; } + html += '
' + esc(r[0]) + '
' + esc(r[1]) + '
'; + }); + return html || '

这条没有更多信息。

'; + } + + function paintDetail(data) { + if (titleEl()) { titleEl().textContent = data.title || '详情'; } + if (bodyEl()) { bodyEl().innerHTML = '
' + rowsHtml(data.rows) + '
'; } + var det = byId('fbEntryDetail'); + if (det) { + if (data.detail) { det.href = data.detail; det.hidden = false; } + else { det.hidden = true; } + } + var ed = byId('fbEntryEdit'); + if (ed) { ed.disabled = false; ed.hidden = !data.edit; } + // 「编辑」不可用时别把它留在底栏上(否则点了没反应) + if (!data.edit) { var e2 = byId('fbEntryEdit'); if (e2) { e2.hidden = true; } } + setTip(''); + footDetail(); + if (!data.edit) { var e3 = byId('fbEntryEdit'); if (e3) { e3.hidden = true; } } + } + + function restoreDetail() { + if (!detailCache) { return; } + var d = detailCache; + paintDetail(d); + } + + /* ---- 打开 / 关闭 ---------------------------------------------------------- */ + function setOpen(on) { + var m = M(); + if (!m) { return; } + m.hidden = !on; + if (!on) { detailCache = null; if (bodyEl()) { bodyEl().innerHTML = ''; } } + } + + function antdOpen() { + var d = document.querySelector('.ant-picker-dropdown'); + return !!(d && !d.classList.contains('ant-picker-dropdown-hidden')); + } + + /* ---- 编辑片段:取回 → 剥壳 → 塞进正文 → 补跑初始化 ------------------------ */ + /* innerHTML 不执行脚本(v42 的血泪结论)⇒ 抠出来重跑。 */ + function runInlineScripts(container, html) { + var re = /]*>([\s\S]*?)<\/script>/gi, m, code = ''; + while ((m = re.exec(html)) !== null) { + if (!/\bsrc\s*=/i.test(m[0].split('>')[0])) { code += m[1] + '\n;\n'; } + } + if (!code) { return; } + var s = document.createElement('script'); + s.textContent = code; + (container || document.body).appendChild(s); + s.parentNode.removeChild(s); + } + + /** + * 片段可能是三种形态:整页(带 layout)/只带弹框壳/只有表单。 + * 我们要的是**表单本体** —— 弹框壳必须剥掉,否则正文里又多一层遮罩。 + */ + function pickForm(html) { + var tmp = document.createElement('div'); + tmp.innerHTML = html; + var form = tmp.querySelector('#anForm, #mvForm, #nodeForm, #tripForm, #calEditForm, form'); + if (form) { return { form: form, html: html }; } + return null; + } + + function loadEdit(url) { + if (!url) { setTip('这条没有可编辑的表单。', true); return; } + setTip(''); + footNone(); + if (titleEl()) { titleEl().textContent = '载入中…'; } + if (bodyEl()) { bodyEl().innerHTML = '

正在载入…

'; } + if (!window.fetch) { window.location.href = url; return; } + fetch(url, { credentials: 'same-origin', + headers: { 'X-Requested-With': 'XMLHttpRequest' } }) + .then(function (r) { return r.text(); }) + .then(function (html) { + if (!bodyEl()) { return; } + var picked = pickForm(html); + if (!picked) { window.location.href = url; return; } + bodyEl().innerHTML = ''; + bodyEl().appendChild(picked.form); + // 片段里的初始化脚本 + 宿主这侧的通用升级(日期 / antd / 选人 / 上传 / 行程表单) + runInlineScripts(bodyEl(), picked.html); + if (window.fbUpgradeRange) { window.fbUpgradeRange(); } + if (window.fbBootAntd) { window.fbBootAntd(); } + if (window.fbNodeForm) { window.fbNodeForm(bodyEl()); } + if (typeof window.fbLoadPicker === 'function') { window.fbLoadPicker(); } + if (typeof window.fbInitPickers === 'function') { window.fbInitPickers(bodyEl()); } + // 标题取表单片段里的自我描述(各模板的弹框头) + var h = picked.form.querySelector('.cal-modal-head b') + || (html.match(/aria-label="([^"]*编辑[^"]*)"/) || [])[1]; + if (titleEl()) { titleEl().textContent = h ? String(h).trim() : '编辑'; } + footEdit(); + var f = bodyEl().querySelector('#nf-title, #an-title, #ev-title, #title, input[name="title"]'); + if (f) { f.focus(); } + }) + .catch(function () { window.location.href = url; }); + } + + /** 编辑态「保存」= 原生提交那张表单(服务端 update 路由会跳转,刷新到新页)。 */ + function submitEdit() { + var form = bodyEl() && (bodyEl().querySelector('form#anForm, form#mvForm, form#nodeForm, form#tripForm, form#calEditForm') + || bodyEl().querySelector('form')); + if (!form) { setTip('找不到可保存的表单。', true); return; } + form.setAttribute('data-fb-entry-submit', '1'); + if (typeof form.requestSubmit === 'function') { form.requestSubmit(); } + else { form.submit(); } + } + + /* ---- 打开一条 ------------------------------------------------------------- */ + function openEntry(el) { + var raw = el.getAttribute('data-fb-entry') || ''; + var data; + try { data = JSON.parse(raw); } catch (e) { return; } /* 数据坏了就别拦点击 */ + if (!data || typeof data !== 'object') { return; } + detailCache = data; + paintDetail(data); + setOpen(true); + } + + document.addEventListener('click', function (e) { + var t = e.target; + if (!t || !t.closest) { return; } + + if (t.closest('[data-fb-entry-close]')) { e.preventDefault(); setOpen(false); return; } + if (t.closest('#fbEntryEdit')) { + e.preventDefault(); + if (detailCache && detailCache.edit) { loadEdit(detailCache.edit); } + return; + } + if (t.closest('#fbEntryBack')) { e.preventDefault(); restoreDetail(); return; } + if (t.closest('#fbEntrySave')) { e.preventDefault(); submitEdit(); return; } + + var item = t.closest('[data-fb-entry]'); + if (item) { e.preventDefault(); openEntry(item); return; } + + var m = M(); + if (m && t === m) { setOpen(false); } /* 点遮罩关;点面板内不关 */ + }); + + document.addEventListener('keydown', function (e) { + if (e.key !== 'Escape') { return; } + var m = M(); + if (!m || m.hidden) { return; } + if (antdOpen()) { return; } /* 让 antd 先收它自己的面板 */ + e.preventDefault(); + setOpen(false); + }); + + /* 编辑态里表单用回车提交时,别让浮层的 Esc 逻辑挡住(默认行为即可)—— + 这里不再另挂 submit 监听:原生提交就是要它跳走。 */ + + window.fbEntryOpen = openEntry; + window.fbEntryClose = function () { setOpen(false); }; +})(); \ No newline at end of file diff --git a/assets/js/person_picker.js b/assets/js/person_picker.js index ed0f457..cc87a55 100644 --- a/assets/js/person_picker.js +++ b/assets/js/person_picker.js @@ -83,6 +83,14 @@ function init(root) { var id = root.getAttribute('data-pp') || ''; + /* 🔴 v43:**幂等闸门**。组件现在可能在"动态插入"之后被重复 init + (见文件末尾 `window.fbInitPickers`)。没有这道闸门会挂上第二套监听: + · 打开弹框被处理两次、暂存区 chip 加两次; + · `change` 里来回切选中态。 + ⚠️ 事件委托本来就是"一次绑定、永久有效",重复调用只需要它**什么也不做**。 + ⚠️ 标记放在**取到必需元素之后**打 —— 否则"结构不全时提前 return"会把 + 这个组件永久标记成"已初始化",之后再补它也没用了。 */ + if (root.getAttribute('data-pp-init') === '1') { return; } var isMulti = root.getAttribute('data-pp-mode') !== 'single'; var modal = root.querySelector('.pp-modal'); var openBtn = root.querySelector('.pp-open'); @@ -92,6 +100,8 @@ var chips = root.querySelector('.pp-chips'); var emptyTip = root.querySelector('.pp-empty'); if (!modal || !list || !chips) { return; } + /* 🔴 v43:必需元素取齐了才打"已初始化"标记(见上面幂等闸门的说明)。 */ + root.setAttribute('data-pp-init', '1'); // ---- 第 19 批 用户第 8 条:多选弹框隐藏每行前面的复选框 ---- // 用户原话:「弹框选择人员那里,暂存区太小了…前面的复选框就没用了吧,去掉吧。」 @@ -715,6 +725,30 @@ Array.prototype.forEach.call(document.querySelectorAll('.pp'), init); } + /** + * 🔴🔴 v43(用户 2026-10-11 第 4 条):「在旅程功能里面,编辑一个行程的时候, + * 现在"参与的人"添加参与人的那个功能已经不好使了,弹不出选人的框了。」 + * + * 真根因:本组件的绑定只跑一次 —— 脚本加载时 `boot()` 扫描当时的 `.pp`。 + * 而**详情页的行程弹框**是 AJAX 取片段、`innerHTML` 塞进 DOM 的 + * ⇒ 那一刻才出现的 `.pp` **从来没被 init 过** + * ⇒「+ 添加参与的人」按钮上没有任何 click 监听 ⇒ 点了没反应(弹不出弹框)。 + * (同一个根因也解释了"暂存区/确定/取消"全都按不动。) + * + * ⇒ 对外暴露 `window.fbInitPickers(scope)`:任何"动态插入含 `.pp` 的 HTML"之后 + * 调它一次即可。`init()` 内部有 `data-pp-init` 幂等标记,重复调用安全。 + * + * ⚠️ 本脚本是**按需加载**的(layout.php 里页面上有 `.pp` 才拉),而弹框里的 `.pp` + * 是后来才出现的 ⇒ 加载判据可能在脚本拉起时还没看到它。所以宿主调这个钩子时, + * 要容忍"函数还不存在"(那时从 `.pp` 到脚本的按需加载链还没走完); + * 见 trips/show.php 里那段"等它就绪"的处理。 + */ + window.fbInitPickers = function (scope) { + var root = scope || document; + Array.prototype.forEach.call(root.querySelectorAll('.pp'), init); + return true; + }; + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { diff --git a/index.php b/index.php index f8cc018..c70215f 100644 --- a/index.php +++ b/index.php @@ -2626,13 +2626,24 @@ switch ($r) { flash('err', '找不到这条观影记录。'); redirect('movies'); } - View::render('movies/form', [ - 'title' => '编辑观影记录', + /* 🔴 v43(用户 2026-10-11 第 6 条):条目详情浮层里点「编辑」时, + 要把它**就地**换成这张编辑表单 —— `?embed=1` 只吐弹框那一块 + (不套 layout),由浮层塞进正文。与 `trips/edit` 同一套约定。 + ⚠️ 详情页那一路(`movies/show` 内嵌 `$mvModalOnly+$mvModalForce`) + 继续走原样,不受影响。 */ + $mvArgs = [ 'm' => $m, 'people' => Person::roster($familyId, (int)Auth::personId(), 'member', ''), 'cinemas' => Cinema::all($familyId), 'errors' => [], - ], 'movies'); + ]; + if ((string)($_GET['embed'] ?? '') === '1') { + header('Content-Type: text/html; charset=utf-8'); + echo View::renderPartial('movies/form', + $mvArgs + ['mvModalOnly' => true, 'mvModalForce' => true, 'mvOpen' => true]); + exit; + } + View::render('movies/form', ['title' => '编辑观影记录'] + $mvArgs, 'movies'); break; /** 收集表单里和观影有关的字段(create / update 共用一份)。 */ @@ -4377,8 +4388,23 @@ $want = array_keys($want); $pids = Anniversary::peopleOf($editing); $editing['person_ids'] = $pids; } - View::render('days/form', [ - 'title' => $id > 0 ? '编辑纪念日' : '新建纪念日', + /* 🔴🔴 v43(用户 2026-10-11 第 2 条):「新建纪念日的时候,原来的页面还保存着。 + 但是点击"修改纪念日",怎么好像原来的页面全都没了?就是下面那一层页面没了, + 这好像不太合理吧?」 + + 真根因:**「修改」是 `
` 整页跳转** —— 它套 layout, + 渲染出的是"一张只有弹框的空白页",列表页被整个换掉 + (正是用户说的"下面那一层页面没了")。而「新建」走的是列表页内嵌弹框, + 两条路观感不一致。 + + ⇒ 修法:`days/edit` 支持 `?embed=1` —— **只吐弹框那一块**(不套 layout), + 列表页 JS 拿它替换掉当前 `#anNewCard` 并打开。 + 这样"编辑"与"新建"共用**同一个弹框、同一个底层页面**。 + (与 `trips/edit` 的 `?embed=1` 完全同一套约定。) + ⚠️ 内嵌形态标 `$anModalOnly = true`:否则模板会给它打上 `data-an-standalone`, + 关闭时去 `window.location.href` 跳页 —— 那正是要去掉的行为。 + ⚠️ 非 embed 整页路径**保留**(直接访问 URL / 无 JS 兜底)。 */ + $anArgs = [ 'editing' => $editing, 'kinds' => Anniversary::kinds($familyId), 'people' => Person::roster($familyId, $me, 'member', ''), @@ -4387,7 +4413,15 @@ $want = array_keys($want); 否则用户看到的是一张空白页(内容全在 hidden 的弹框里)。 */ 'anModal' => true, 'anOpen' => true, - ], 'days'); + ]; + if ((string)($_GET['embed'] ?? '') === '1') { + header('Content-Type: text/html; charset=utf-8'); + echo View::renderPartial('days/form', + $anArgs + ['anModalOnly' => true, 'anShell' => true]); + exit; + } + View::render('days/form', + ['title' => $id > 0 ? '编辑纪念日' : '新建纪念日'] + $anArgs, 'days'); break; case 'days/save': @@ -4912,14 +4946,23 @@ $want = array_keys($want); $occ = preg_match('/^\d{4}-\d{2}-\d{2}$/', $occ) ? $occ : ''; // 编辑页要回填"哪些天被跳过了"(跳过的那次不再出现在日历里) $ev['skips'] = Calendar::skipsOf((int)$ev['id']); - View::render('calendar/event', [ + /* 🔴 v43(用户 2026-10-11 第 6 条):条目详情浮层里点「编辑」时, + `?embed=1` 只吐**编辑表单**(不套 layout、不带页头/删除), + 由浮层塞进正文。与 days/edit、movies/edit、trips/node_edit 同一套。 */ + $evArgs = [ 'title' => (string)$ev['title'], 'ev' => $ev, 'cal' => $cal, 'occ' => $occ, // ⚠️ 不要再传 'ym':模板自己从 $ev['starts_at'] 算(模板里那行更权威, // 传进来反而会在"改过日期后"与模板算出的值不一致)。 - ], 'calendar'); + ]; + if ((string)($_GET['embed'] ?? '') === '1') { + header('Content-Type: text/html; charset=utf-8'); + echo View::renderPartial('calendar/event', $evArgs + ['evEmbed' => true]); + exit; + } + View::render('calendar/event', $evArgs, 'calendar'); break; case 'calendar/event_update': @@ -5557,6 +5600,35 @@ $want = array_keys($want); ], 'tasks'); break; + /* 🔴 v43(用户 2026-10-11 第 6 条):条目详情浮层里点「编辑」→ 取这张表单片段。 + ⚠️ 与 `tasks/show` 共用同一份 `_edit_form.php`(不许各写一张,那必然有一天不同步)。 */ + case 'tasks/edit': + Auth::requireWrite(); + $familyId = (int)Auth::familyId(); + $me = (int)Auth::personId(); + $t = Task::find($familyId, (int)($_GET['id'] ?? 0), $me); + if (!$t || !Task::canWrite($t, $me)) { + http_response_code(403); + echo '这项任务不能改。'; + exit; + } + $tkParents = []; + foreach (['todo', 'doing', 'done'] as $sk) { + foreach (Task::board($familyId, $me)[$sk] as $p) { + if ((int)$p['id'] !== (int)$t['id']) { $tkParents[] = $p; } + } + } + header('Content-Type: text/html; charset=utf-8'); + echo View::renderPartial('tasks/_edit_form', [ + 't' => $t, + 'people' => Person::roster($familyId, $me, 'member', ''), + 'parents' => $tkParents, + 'boards' => Task::boards($familyId), + 'categories' => Task::categories($familyId), + 'tkEditEmbed' => true, // 浮层形态:保存键由浮层底栏给 + ]); + exit; + case 'tasks/create': case 'tasks/update': $familyId = (int)Auth::familyId(); diff --git a/templates/_entry_popup.php b/templates/_entry_popup.php new file mode 100644 index 0000000..309e1f1 --- /dev/null +++ b/templates/_entry_popup.php @@ -0,0 +1,46 @@ + + \ No newline at end of file diff --git a/templates/calendar/_event_fields.php b/templates/calendar/_event_fields.php index 2aa9244..aca98ff 100644 --- a/templates/calendar/_event_fields.php +++ b/templates/calendar/_event_fields.php @@ -17,31 +17,54 @@ * 用法(include 前设好): * $evFields 正在编辑的日程(新建时传空数组) * $evAllDay 全天的默认值(新建默认**不勾**,用户 2026-10-02 第 6 条) + * $evMvStyle 🔴 v43:「标签 62px 右对齐 + 冒号 + 控件」的**观影式**排版开关。 + * 新建日程那个弹框要它(用户:日程新建页排版和观影/行程一样); + * 编辑页(`calendar/event.php`)继续用原来的 `.form-grid` 两列。 */ $evF = is_array($evFields ?? null) ? $evFields : []; $evAllDayDefault = (bool)($evAllDay ?? false); $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evAllDayDefault; +/* 🔴 v43(用户 2026-10-11 第 5 条):观影式排版开关。 */ +$evMv = !empty($evMvStyle); +/* 标签包装:观影式用 `.mv-row` + `.mv-lb`(冒号由 CSS ::after 给); + 否则用原来的 `.field` + `