diff --git a/assets/css/app.css b/assets/css/app.css index 2eedbab..961fc89 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -584,126 +584,126 @@ textarea.input { min-height: 76px; resize: vertical; } .search { display: flex; gap: 8px; margin-bottom: 16px; } .search .input { max-width: 260px; } -/* ---------------- 日期 + 时:分:秒 组合控件(templates/_datetime.php) ---------------- - 原生 datetime-local 的显示格式由浏览器 UI 语言/系统时间格式决定, - 中文环境渲染成「下午 03:30」且没有秒,所以自己拼一个。 - 日期部分仍是原生 date 控件 → app.js 的 showPicker 让它整框可点。 */ +/* ============================================================================ + 日期 + 时:分 组合控件(templates/_datetime.php)—— v37 重做 + ---------------------------------------------------------------------------- + 形态(用户 2026-10-09 第 4 次反馈点名要的): + 日期 = 年 / 月 / 日 三个原生下拉;时间 = 时 : 分 两个原生下拉。 + 起止两端默认同一行,放不下时自动换到下一行(flex-wrap,见 .dt-cols)。 + + 为什么把浏览器自带的 换掉: + 它的日历弹层是浏览器自己画的,我们碰不到 DOM、也控制不了流程; + Chrome 在"点顶部 2026年 10月 切到年月视图"之后要靠用户再点一次日期或 + 按 Esc 才回得到日期视图,各家还不一致 → 用户遇到的就是"选完年月就卡住、 + 选不了日"。三个原生下拉选完即生效,不需要任何确定按钮。 + + 这一整段不再有"未升级就藏原生控件"的逻辑(v24~v36 有过)。 + 服务端给的就是最终形态,JS 只负责绑事件 —— 所以"JS 没跑 / 加载失败"时 + 控件依然完整可用,不是"骨架占位"。 + ============================================================================ */ .dt-input { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } -/* v24 第 9 批 #9:Semi DatePicker 升级态 —— .dt-native 是 display:contents(不干扰 - flex 布局),升级成功后由 app.js 内联 display:none 藏掉;.dt-semi 顶上同一格。 */ .dt-input .dt-native { display: contents; } -/* ---- v24 第 12 批 #2:消除「先闪原生框、再变 Semi」---- - html.js 由 里的内联脚本立刻加上(见 layout.php 的 no-js/js 二态)。 - 有 JS 时:原生控件**从一开始就不显示**,等 Semi 就绪(app.js 加 .is-semi)再露出。 - 没 JS 时(html 仍是 no-js):什么都不藏,原生控件照旧可用 —— 渐进增强的兜底不变。 - ⚠️ 用 visibility:hidden 而不是 display:none —— 保留元素占位高度, - 否则升级完成的那一瞬表单会"跳"一下(高度从 0 变 40px)。 */ -html.js .dt-input:not(.is-semi) .dt-native { visibility: hidden; } -/* 🔴🔴 v36:`.is-native` 态下,原生控件**必须可见**。 - 用 `!important` 是**故意**的:v35 那次就是被 - `html.js .dt-input:not(.is-semi) .dt-native{visibility:hidden}` - (特异性 0,3,0,比我那条覆盖规则更强/更早)又藏回了 hidden —— - 用户看到"一片空白、点不到",而探针因为用错判据(offsetParent)还"全绿"两轮。 - ⇒ 这条用 `!important` 一次性钉死,别再让任何历史规则把它藏起来。 */ -html.js .dt-input.is-native .dt-native { visibility: visible !important; } -html.js .dt-input.is-native::after { display: none !important; } -html.js .dt-range.is-native-range .dt-input { visibility: visible !important; } -html.js .dt-range.is-native-range > .dt-range-label { visibility: visible !important; } -html.js .dt-range.is-native-range::before { display: none !important; } -html.js .dt-range.is-native-range > .dt-input, -html.js .dt-range.is-native-range > .field { display: flex !important; } -/* ⚠️ v24 第 21 批 第 7 条:两端现在各自被 `.field` 包着,所以选择器要跟着下沉一层。 - 写成 `> .dt-input` 的话这条**永远不会命中** → 未升级时原生控件照旧可见, - 与下面那块"占位骨架"叠在一起,会看到两套控件重影。 */ -html.js .dt-range:not(.is-semi-range) .dt-input { visibility: hidden; } -html.js .dt-range:not(.is-semi-range) > .dt-range-label { visibility: hidden; } -/* Semi 未就绪前,先给一个同高的"占位骨架",让表单高度在升级前后保持一致 */ -html.js .dt-input:not(.is-semi)::after { - content: ''; display: block; flex: 1 1 150px; height: 40px; - border: 1px solid var(--line); border-radius: var(--radius-s); - background: var(--surface); opacity: .55; -} -html.js .dt-range:not(.is-semi-range)::before { - content: ''; display: block; width: 100%; max-width: 460px; height: 40px; - border: 1px solid var(--line); border-radius: var(--radius-s); - background: var(--surface); opacity: .55; -} -html.js .dt-range.is-semi-range::before { display: none; } .dt-input .dt-semi { display: none; flex: 1 1 220px; min-width: 0; } -.dt-input.is-semi .dt-semi { display: block; } -.dt-input.is-semi::after { display: none; } -/* 工作经历的月份选择器(type="month" 升级):占满原 .job-cell 格子 */ + +/* ---- 日期:年 / 月 / 日 三个下拉 ---- */ +.dt-date-sel { display: flex; align-items: center; gap: 4px; min-width: 0; } +.dt-input .dt-year, +.dt-input .dt-month, +.dt-input .dt-day { + padding: 9px 6px; border: 1px solid var(--line); border-radius: var(--radius-s); + background: var(--surface); color: var(--ink); font: inherit; font-size: 13.5px; + font-variant-numeric: tabular-nums; cursor: pointer; min-width: 0; +} +/* 年最宽(4 位数 + 箭头),月/日短一些 —— 省下来的宽度留给时:分 */ +.dt-input .dt-year { flex: 0 1 86px; } +.dt-input .dt-month { flex: 0 1 66px; } +.dt-input .dt-day { flex: 0 1 64px; } +/* 隐藏的 `*_date` 真正的字段(提交契约)—— 它必须留在 DOM 里 */ +.dt-input input.dt-date[type="hidden"] { display: none; } +/* 旧「原生 date 框」形态($dtDateType='native' 时才出现)保留样式 */ +.dt-input .dt-date:not([type="hidden"]) { + flex: 1 1 150px; min-width: 0; cursor: pointer; + padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--radius-s); + background: var(--surface); color: var(--ink); font: inherit; + accent-color: var(--brand); +} +.dt-input .dt-date:not([type="hidden"])::-webkit-calendar-picker-indicator { cursor: pointer; opacity: .65; } + +/* ---- 时间:时 : 分 ---- */ +.dt-time { display: flex; align-items: center; gap: 3px; flex: 0 0 auto; } +.dt-input .dt-hour, +.dt-input .dt-minute, +.dt-input .dt-second { + padding: 9px 6px; border: 1px solid var(--line); border-radius: var(--radius-s); + background: var(--surface); color: var(--ink); font: inherit; font-size: 13.5px; + font-variant-numeric: tabular-nums; cursor: pointer; +} +.dt-input .dt-colon { color: var(--muted); font-weight: 600; } + +/* 必填但没填时描红(由 app.js 的 guardRequired 打类) */ +.dt-input.dt-missing .dt-year, +.dt-input.dt-missing .dt-month, +.dt-input.dt-missing .dt-day, +.dt-input.dt-missing .dt-date { border-color: #C0392B !important; } + +/* ============================================================================ + 起止范围(.dt-range):两端一行优先,放不下才换行 + ============================================================================ + 用户原话(2026-10-09 第 4 次反馈): + 现在开始和结束时间分在了两行,导致日期这个特别特别的长,后面的时和分又 + 非常非常的短,非常不协调。所以这两个日期如果能放一排,如果能放得下的话, + 就应该放一排。因为日期和时间,分和时和分的这个长度基本上是固定的, + 所以你可以判断一下它到底能不能一行,能不能放得下。如果能放下的话就放在一行。 + 如果在手机上,比如说它这个页面非常窄,它就可以把它挤到下一行。 + 但是现在不要让它默认显示在两行上,应该默认显示在一行上才对。 + + ⇒ 做法:flex 容器 + flex-wrap。每端有一个最小的可用宽度(.dt-part 的 + min-width),浏览器在每次布局时自己判断放不放得下;放得下 → 一行; + 放不下 → 第二端自然掉到下一行。 + ⚠️ 不用 JS 量宽度:字体没加载完 / 容器动画中都会量错,窗口缩放还要重算; + flex-wrap 是浏览器布局的一部分,永远是对的。 + ⚠️ .dt-range 的标签只在组上留一个(见 _event_fields.php 的说明): + 两端是同一个范围的两个端点,各挂一个标签会变成"看着像两个字段"的错觉。 */ +.dt-range { display: flex; flex-direction: column; gap: 8px; } +.dt-range-label { font-size: 14px; color: var(--ink); font-weight: 600; } +/* 升级(= app.js 挂上 .dt-cols)之后:两端并排、必要才换行 */ +.dt-range.dt-cols { + flex-direction: row; flex-wrap: wrap; align-items: center; + gap: 10px 18px; +} +/* 两端的"最小可用宽度":日期(年+月+日) + 时:分 摆得下。 + 值取 250px —— 实测"年(86)+月(66)+日(64)+时:分(约 96)+间距"约 330px 才舒服, + 但最小可用是 250px(会各自压缩一点),这样在常见的 560px+ 容器里 + 两端(250×2 + 18 间距 = 518)肯定放得下 → 默认就是一行。 */ +.dt-range.dt-cols > .dt-input, +.dt-range.dt-cols > .field { flex: 1 1 250px; min-width: 250px; display: flex; } +.dt-range.dt-cols > .field { flex-direction: column; gap: 6px; } +/* 标签占满整行(不然它会跟第一端挤在一起) */ +.dt-range.dt-cols > .dt-range-label { flex: 1 1 100%; } +/* 极窄屏:两端各自铺满一行(用户说的"手机上就挤到下一行") */ +@media (max-width: 560px) { + .dt-range.dt-cols > .dt-input, + .dt-range.dt-cols > .field { flex: 1 1 100%; min-width: 0; } +} + +/* ============================================================================ + Semi 仅剩的用途:工作经历的「年月选择」( 的升级) + ---------------------------------------------------------------------------- + ⚠️ 日期/时间控件(.dt-input)v37 起已完全不用 Semi —— 上面那套自建下拉 + 就是因为它给的弹层交互不可控(选完年月卡住)。这里保留的是另一处。 + ============================================================================ */ .dt-semi.dt-semi-month { display: none; } .dt-semi.dt-semi-month.is-live { display: block; width: 100%; } - -/* ---- v24 第 10 批 #7:起止范围用一个控件(Semi dateTimeRange)---- - 一对 .dt-input 被包在 .dt-range 里;升级后两端各自 display:none, - 由**一个** .dt-semi-range 顶上来。未升级(JS 没跑/加载失败)时, - 里面两个原生控件照旧各占一行 —— 也就是"功能没变,只是不好看"。 */ -.dt-range { display: flex; flex-direction: column; gap: 8px; } -/* 范围控件自己的标签(只有一个,见 _event_fields.php 的说明) */ -.dt-range-label { font-size: 13px; color: var(--muted); font-weight: 500; } -.dt-range.is-semi-range { display: block; } -.dt-range.is-semi-range > .dt-input { display: none; } /* 两端都让位给范围控件 */ -/* 两端被 .field 包着时的同一条规则(见上面第 21 批的说明) */ -.dt-range.is-semi-range > .field { display: none; } -.dt-semi-range { display: none; width: 100%; max-width: 460px; } -.dt-range.is-semi-range .dt-semi-range { display: block; } -/* Semi 范围输入框:与站内 .input 同高同圆角;宽度撑开一点(要放两个时间) */ -body[class] .dt-semi-range .semi-input-wrapper { - height: 40px; border: 1px solid var(--line); border-radius: var(--radius-s); - background: var(--surface); -} -body[class] .dt-semi-range .semi-input-wrapper:hover { border-color: var(--brand-2); } -body[class] .dt-semi-range .semi-input { font: inherit; font-size: 13.5px; color: var(--ink); } -/* ---- v24 第 12 批 #2:范围控件的「灰底 + 不垂直居中」(实测定位)---- - 用真浏览器量出来两个毛病: - ① 灰底:Semi 的 **外层** `.semi-datepicker-input` 自带 `rgba(46,50,56,.05)` - 灰底、高只有 32px;而内层 `.semi-input-wrapper` 是白底、40px —— - 两层套着,灰的那层露在外面(用户看到的"灰色背景色")。 - ② 不居中:外层 32px < 内层 40px,输入框上下都被挤出去 - (实测 gapTop=1 / gapBottom=17 → 文字贴顶)。 - 修法:把**外层容器**改成透明 + 不限高 + 垂直居中,让内层 40px 的白底框 - 自己撑起来(与站内 .input 完全一致)。 */ -body[class] .dt-semi-range .semi-datepicker-input { - background: transparent !important; - height: auto !important; - min-height: 0 !important; - align-items: center; - padding: 0; - border: 0; -} -body[class] .dt-semi-range .semi-datepicker-range-input-wrapper { - align-items: center; - background: transparent; -} -/* 两端的白底框就是"可见的输入框":撑满高度、文字垂直居中 */ -body[class] .dt-semi-range .semi-input-wrapper { - display: flex; align-items: center; -} -body[class] .dt-semi-range .semi-input-wrapper .semi-input { - height: 100%; line-height: 40px; padding: 0 8px; - background: transparent; -} -@media (max-width: 640px) { - .dt-semi-range { max-width: none; } -} -.dt-input.dt-missing .semi-input-wrapper { border-color: #C0392B !important; box-shadow: 0 0 0 1px #C0392B inset; } -/* Semi 主题对齐宣纸色。semi.min.css 是懒加载的、必然排在 app.css 之后, - 所以这里赢在**优先级**:body[class] 特异性(0,1,1) > semi 的 body(0,0,1)。 */ body[class] { --semi-color-primary: var(--brand, #A84A28); --semi-color-primary-hover: var(--brand-2, #C05F38); --semi-color-primary-active: var(--brand-ink, #8D3C20); --semi-color-focus-border: var(--brand, #A84A28); } -/* 输入框本体:跟站内 .input / .dt-date 同一轮廓(描边、圆角、纸面底色、同高) */ body[class] .dt-semi .semi-input-wrapper { width: 100%; height: 40px; border: 1px solid var(--line); border-radius: var(--radius-s); background: var(--surface); color: var(--ink); - /* v24 第 12 批 #2:单点控件也做同样处理 —— 外层半透明灰底去掉、文字垂直居中 - (用户说"这种情况同样出现在其他的日期选择功能中")。 */ display: flex; align-items: center; } body[class] .dt-semi .semi-input-wrapper:hover { border-color: var(--brand-2); background: var(--surface); } @@ -711,43 +711,16 @@ body[class] .dt-semi .semi-input { font: inherit; color: var(--ink); } body[class] .dt-semi .semi-input::placeholder { color: var(--muted); } body[class] .dt-semi .semi-datepicker-input, body[class] .dt-semi .semi-datepicker { - background: transparent !important; - height: auto !important; - min-height: 0 !important; - border: 0; - padding: 0; + background: transparent !important; height: auto !important; + min-height: 0 !important; border: 0; padding: 0; } body[class] .dt-semi .semi-input-wrapper .semi-input { height: 100%; line-height: 40px; padding: 0 11px; background: transparent; } -/* 日历面板:白纸底色统一成卡片的纸色,选中/悬停色走上面的 brand 变量 */ body[class] .semi-popover .semi-datepicker { background: var(--surface, #fff); } body[class] .semi-datepicker-day-selected { background-color: var(--brand, #A84A28); } body[class] .semi-datepicker-day-hoverday, body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb, var(--brand, #A84A28) 12%, transparent); } -.dt-input .dt-date { - flex: 1 1 150px; min-width: 0; - padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--radius-s); - background: var(--surface); color: var(--ink); font: inherit; - accent-color: var(--brand); -} -.dt-input .dt-date { cursor: pointer; } /* 整框可点,光标也要说明这一点 */ -.dt-input .dt-date::-webkit-calendar-picker-indicator { cursor: pointer; opacity: .65; } -.dt-time { display: flex; align-items: center; gap: 3px; } -.dt-input .dt-hour, -.dt-input .dt-minute, -.dt-input .dt-second { - padding: 9px 6px; border: 1px solid var(--line); border-radius: var(--radius-s); - background: var(--surface); color: var(--ink); font: inherit; - font-variant-numeric: tabular-nums; cursor: pointer; -} -.dt-input .dt-colon { color: var(--muted); font-weight: 600; } - -/* 🔴 v36:时间不再有"合成的时间框"(v35 试过 ``,已弃用)。 - `.dt-merged` / `.dt-time-input` 的规则一并删掉 —— 免得将来有人误以为 - 还有"被收起来的下拉"。用户要的形态就是**日期 + 时/分下拉**直接显示。 */ -.dt-input.is-native-range { gap: 8px; } -.dt-range.is-native-range { display: flex; flex-direction: column; gap: 10px; } /* 「以哪个生日为准」的两块:被选中的那块描一圈,一眼看出填的是哪边 */ .bd-source { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; } @@ -3206,6 +3179,29 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb, border: 1px solid var(--line); border-radius: 10px; } .rev-tip { margin: 0 0 10px; font-size: 12.5px; line-height: 1.7; color: var(--muted); } + +/* 🔴🔴 v37:修正面板必须**浮**在按钮列之外,否则它只有 ~22px 宽、完全没法填。 + 真机实测(1400px 视口、品心 1080px): + .dt-range 宽 **22px**、`.nf-grid` 宽 **22px** —— 整张修正表单被压成一条线。 + 根因:`.node-ops` 是"一排小按钮"的容器 + `flex: 0 0 auto; justify-content: flex-end;`(见上面), + 而「修正」是 `
`:**展开的内容也长在这个按钮列里** + ⇒ 列宽只有"修正"两个字那么宽(22px),里面的表单跟着一起被压扁。 + ⚠️ 这不只是"不好看":日期控件在 22px 里根本点不到(用户报的就是这个)。 + 修法:面板 `position: absolute` 脱离按钮列的宽度约束,浮到卡片上。 + · 宽度取 `min(720px, container 宽)` —— 720 刚好摆得下"两端各 304"(608+间距)。 + · `right: 0` 让它从按钮列右缘向左展开,不会超出卡片右边。 + · `z-index: 30` 盖在后面的行上面(它是临时展开的面板,本来就该在最上层)。 + ⚠️ 不动 templates/trips/show.php 的 DOM: + `.node-list`/`.node-row` 的 flex 结构一动就会牵动"节点列表"的排版 + (那是清单页最核心的视觉),而这里只需要把它**浮出来**,纯 CSS 就够。 */ +.rev-open { position: relative; } +.rev-panel { + position: absolute; right: 0; top: calc(100% + 6px); z-index: 30; + width: min(720px, calc(100vw - 48px)); + max-height: min(76vh, 720px); overflow-y: auto; + box-shadow: var(--shadow-l, 0 12px 32px rgba(0, 0, 0, .16)); +} .rev-form .btn-row { margin-top: 10px; } /* 修正历史对比表 */ @@ -3964,11 +3960,15 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b background: rgba(44, 35, 29, .55); overscroll-behavior: contain; } .cal-modal[hidden] { display: none; } +/* 🔴 v37:`max-height` 必须把 `.cal-modal`(本面板的父层)自己的 `padding: 24px` + 算进去 —— 只减 48 会让面板底部**刚好探出视口 4px**, + 于是 `.cal-modal-foot` 里那两个按钮有半截在屏幕外(用户"看不到按钮"的另一半原因)。 + ⇒ 减 **64px** = 上下各 24px 的遮罩内边距 + 16px 余量。 */ .cal-modal-panel { width: min(680px, 100%); margin: auto; background: var(--surface); border-radius: 14px; box-shadow: var(--shadow-l); display: flex; flex-direction: column; - max-height: calc(100vh - 48px); + max-height: calc(100vh - 64px); } .cal-modal-head { display: flex; align-items: center; gap: 10px; @@ -5084,3 +5084,63 @@ h5.md-h, h6.md-h { font-size: 14px; } .ntf-main { flex-wrap: wrap; gap: 2px 10px; } .ntf-title { flex: 1 1 100%; } } + +/* ============================================================================ + v37(2026-10-09) —— 弹框:按钮位置 / 标签加粗 / 观影弹框宽度 + ---------------------------------------------------------------------------- + 用户原话(4 条,都指向"弹框"): + ⑤「日程模块的新建日程里,弹出一个对话框。但是"加上"和"取消"两个按钮在 + 右上角,离得特别远,根本看不到。应该把它们放在下方,或者放在"新建日程" + 标题的右侧,不要放那么远。 + 另外,每个输入框前面的标题都太小了…比如"放在哪本""标题""地点""提醒"这些, + 都应该加粗、变大,这样跟里面的内容产生一些差异,看起来更醒目一些。」 + ⑥「新建任务也和新建日程一样,新建时弹出一个框…」 + ============================================================================ */ + +/* 🔴🔴 日程弹框"按钮跑到右上角、还根本看不到"的真根因(实测定位,不是猜): + `.cal-modal-panel` 是 flex 列(head / form / foot 三段),但**它下面的
+ 不是** —— form 是块级元素,`cal-modal-body{flex:1 1 auto}` 的 flex 上下文 + 只到 form 为止。于是 form 的高度 = head + 内容**全部**高度,而面板 `max-height` + 又把外层卡住 ⇒ foot 被挤出面板可视区,出现在**面板底部之外**, + 而面板本身不滚(滚的是 body)→ 用户看到的就是"按钮飘在很远的地方 / 看不见"。 + ⇒ 让 form 也参与那个 flex 列、body 才能真正被压缩到可滚动。 */ +.cal-modal-panel > form { + display: flex; flex-direction: column; + flex: 1 1 auto; min-height: 0; + overflow: hidden; /* 圆角裁切:body 滚动时不要顶出面板 */ +} +/* ⚠️ v37 起"加上"主键改用 `form="calAddForm"` 关联 —— + 它不再长在 form 内部,`:not()` 那条老规则会失效,这里按 id 补回。 */ +.cal-modal-foot .btn[type="submit"], +.cal-modal-foot #calAddSubmit { margin-left: auto; } + +/* ---- 弹框里的字段标签:加粗、变大、深色 ---- + 用户原话:「每个输入框前面的标题都太小了…都应该加粗、变大,这样跟里面的内容 + 产生一些差异,看起来更醒目一些。」 + ⚠️ 只改**弹框里**的(.cal-modal / .pp-modal 等浮层面板内): + 页面内的普通表单标签维持原样,避免"改一处动了全站"。 + ⚠️ 用 `.cal-modal .field > label` 而不是 `.cal-modal label`: + 后者会连"提醒"那排 checkbox 的每个小标签一起加粗,看起来一片糊。 */ +.cal-modal .field > label, +.cal-modal .field > .dt-range-label, +.cn-panel .field > label, +.pp-modal .field > label { + font-size: 14px; font-weight: 600; color: var(--ink); +} + +/* ---- 观影"记一场电影"弹框 ---- + ⚠️ 字段比日程多得多(海报 + 观影照片 + 简介 + 同行人…),620px 太窄了, + 用 760px 让两列字段摆得开;高度放到 88vh,长表单才有足够的可视区。 */ +.cal-modal-panel.mv-panel { width: min(760px, 100%); max-height: calc(100vh - 64px); } +.mv-form-inner { padding: 16px; } /* 弹框里的内边距(编辑态用的是 .card-pad) */ +/* 弹框形态下,页面上那条"记下这场电影 / 取消"一整行是多余的(foot 里已有), + 但它在**编辑态**必须留着 —— JS 没跑时它是唯一的提交入口。 */ +.mv-foot-inner { display: none; } +@media (max-width: 640px) { + .cal-modal-panel.mv-panel { width: 100%; } +} + +/* ---- 窄屏:弹框字段全部单列(form-grid 是两列,手机上会把标签挤成两个字) ---- */ +@media (max-width: 560px) { + .cal-modal .form-grid { grid-template-columns: 1fr; } +} diff --git a/assets/js/app.js b/assets/js/app.js index 988c9c0..7dac3df 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1268,18 +1268,39 @@ })(); })(); -// ==================== Semi Design DatePicker(v24 第 9 批 #9) ==================== -// 用户点名「所有日期选择换 https://semi.design 的 DatePicker,样式与系统统一」。 -// 这里是**渐进增强**的升级层:templates/_datetime.php 照旧渲染原生「日期+时/分/秒」 -// 四字段(表单契约与服务端 read_dt_input() 不动),Semi 的资源(合计约 4 MB)只在 -// 页面上真出现 .dt-input 时才懒加载;加载成功 → 藏原生、挂 DatePicker、值回写原字段; -// 加载失败 → 原生控件原样可用,页面不会坏。 +// ==================== 日期时间控件:绑事件 + 工作经历的年月选择 ==================== +/* 🔴🔴 v37:这一段的职责**大幅缩小**了。 + 原来它是"Semi DatePicker 渐进增强层":藏掉原生控件、挂上 Semi、把值写回原字段。 + 现在日期/时间控件(.dt-input)**完全自建**(年/月/日 + 时/分 原生下拉, + 见 templates/_datetime.php 与 upgradeRange / upgrade / bindParts) + —— 因为它们要的交互("选完年月要能回来选日")浏览器自带的弹层给不了, + Semi 的弹层同样给不了(v35 实测:它的时间面板根本是空的)。 + + ⚠️ 仍然**必须**跑起来,因为还有一件事离不开这套加载器: + **工作经历的 ``**(Firefox 压根不支持原生 month, + Chrome 的弹层也是同一个"选完年月卡住"的毛病)—— 它继续用 Semi 的 + 月选择器(`type: 'month'`,只需点月份格,没有"切回来"的问题)。 + ⚠️ 连带的好处:没有 month 控件的页面**不再下载那 4 MB** Semi 资源。 + 以前只要页面上有任何 .dt-input 就会去拉;现在只按 hasMonth 判断。 */ (function () { var boxes = Array.prototype.slice.call(document.querySelectorAll('.dt-input[data-dt]')); var hasMonth = !!document.querySelector('input[type="month"]'); - if (!boxes.length && !hasMonth) { return; } + /* ⚠️ 只要还有 month 控件就得拉 Semi;纯 .dt-input 页面(绝大多数)不用拉。 */ + if (!hasMonth) { + // 没有 month 控件:**只做绑定**,不下载任何外部资源。 + // 这一步不能省 —— 三个日期下拉写回隐藏 *_date 全靠它。 + Array.prototype.forEach.call(document.querySelectorAll('.dt-range'), upgradeRange); + boxes.forEach(upgrade); + watchNewControls(); + return; + } var meta = document.querySelector('meta[name="fb-semi"]'); - if (!meta) { return; } + if (!meta) { + Array.prototype.forEach.call(document.querySelectorAll('.dt-range'), upgradeRange); + boxes.forEach(upgrade); + watchNewControls(); + return; + } var conf; try { conf = JSON.parse(meta.getAttribute('content')); } catch (e) { return; } if (!conf || !conf.css || !conf.react || !conf.dom || !conf.ui) { return; } @@ -1385,73 +1406,28 @@ }); if (!a || !b) { return; } - /* 🔴🔴 v35:**范围控件不再用 Semi 的 dateTimeRange**,改用原生 - 「日期框 + 时间框」两个小控件。 - 用户原话:「选择日期那个地方给,非常不好用。如果我不选日期,只选时间, - 框里面仍然是空白的,也没有确定按钮,这个需要重新调整一下。」 + /* 🔴🔴 v37:**最彻底的简化 —— 不再"升级"任何东西、不再隐藏任何东西**。 + 控件从服务端出来就(应当)是它**最终该有的样子**(见 templates/_datetime.php): + · 日期 = 年 / 月 / 日 三个原生下拉; + · 时间 = 时 / 分 两个原生下拉。 + 这里只做两件事:① 把三个日期下拉绑成"改哪个算哪个"(写回隐藏的 *_date); + ② 给范围组排版(两端一行、窄屏自动换行)。 - 实测结论(独立最小复现 + 真机,2026-10-09): - · Semi 的 dateTimeRange 面板**只给日期网格**,时间藏在 - `.semi-datepicker-switch-time` 那个小时钟按钮后面; - · 点了那个按钮切到时间面板,**面板仍然是空的** - (`timeLists = 0`、无选项、无 footer、无"确定")→ 根本没法只选时间; - · 单点 `dateTime` 同样(面板里没有可选的时间列)。 - ⇒ 该组件在这套资源下**给不了"选完即确认"的时间选择**,也不能"只选时间", - 与用户描述的症状完全一致。 - ⇒ 改用原生 `type="time" step="1"`:24 小时制、精确到秒、点了就选、 - 不选日期也照样有值(实测 `21:45:30` 会保留),且**不需要**确认按钮。 + 用户原话(2026-10-09 第 4 次反馈): + 「…选日期的这个控件非常非常不好用。就是如果我把年和月那个地方拉下来, + 我选完年和月之后,它并不会回到让我选日的那个界面,它还是不动的, + 所以这时候就没有办法选日。」 + —— 那是**浏览器自带日历弹层**的行为(我们碰不到它的 DOM,也就没法治); + 三个原生下拉是我们能完全控制的形态,选完即生效、不需要"确定"。 + 「另外一个问题是,现在开始和结束时间分在了两行…如果能放得下的话, + 就应该放一排。」→ 见下面 layoutRange() + CSS 的 .dt-cols。 */ - ⚠️ 提交契约**一点没变**:仍是 `*_date` / `*_hour` / `*_minute` / `*_second` - 四个字段(服务端 read_dt_input 照收、既有 e2e 照过)。 - 这里只是把两端的日期框**露出来**、并把原来的 时/分/秒 三个下拉 - 合成**一个**原生时间框(三个下拉仍留在 DOM 里,只是不单独显示)。 */ + layoutRange(group); + bindParts(a); + bindParts(b); - - /* 🔴🔴 v36:**不合成任何"时间框",直接把原生的「日期 + 时/分下拉」露出来**。 - 用户原话(第 2 次反馈,2026-10-09): - 「观影模块,开始时间和结束时间是空白的,无法选择,可以调整为, - 前面是日期,后面是时和分,只记录到时和分就行了。时和分,是下拉的。」 - - ⚠️ v35 那一版为什么用户看到的是**一片空白**(我的锅): - 当时只给范围**组**加了 `.is-native-range`,**没给两个端点 `.dt-input` - 加 `.is-native`** —— 于是 CSS 里 - `html.js .dt-input:not(.is-semi) .dt-native { visibility: hidden; }` - 照样命中 ⇒ 控件 `visibility:hidden`(**占着 48px 高度、但完全不可见、点不到**)。 - 🔴 更糟的是探针当时用 `offsetParent !== null` 判"可见" —— - 对 `visibility:hidden` 的元素它**仍然返回 true**,于是连续两轮都"通过"。 - ⇒ 判可见**必须看 computed visibility**(_fb_b34_dtrange.py 已改)。 - - ⇒ 本版走最笨但最稳的路:**不造新控件**,直接把原有的 日期 + 时/分 下拉显示出来。 */ - function revealEndpoint(box) { - var native = box.querySelector('.dt-native'); - if (!native) { return; } - // ① 端点加 `.is-native`(压掉"未 .is-semi 就藏原生"那条规则) - box.classList.add('is-native'); - // ② 去掉"秒"那一档(用户只要到分);紧跟它的那个冒号一起收掉 - var secEl = native.querySelector('.dt-second'); - if (secEl) { - var prev = secEl.previousElementSibling; - if (prev && prev.classList && prev.classList.contains('dt-colon')) { prev.remove(); } - secEl.remove(); - } - // ③ 显示(inline 覆盖,避免再被任何 CSS 规则藏住 —— 这是 v35 的教训) - native.style.display = 'contents'; - native.style.visibility = 'visible'; - box.style.display = ''; - box.style.visibility = 'visible'; - box.classList.add('has-value'); - } - - revealEndpoint(a); - revealEndpoint(b); - - // 供 fbSetDT / fbUpgradeRange 的既有调用点使用(不再挂 Semi,无需同步) + // 供 fbSetDT / fbUpgradeRange 的既有调用点使用 group.__fbRemount = function () {}; - - /* ⚠️ 原来这里会隐藏"端点最外层的包装"(因为 Semi 范围控件顶替了两端)。 - v35 起两端**就是要显示**的,所以不再隐藏任何东西 —— - v34 那个"共用包装导致整组被藏"的 bug 也随之不再可能发生。 - (v34 的修法保留在 _fb_b34_dtrange.py 的断言里:组必须可见。) */ guardRequired(a); guardRequired(b); group.setAttribute('data-semi-range-done', '1'); @@ -1459,33 +1435,101 @@ group.classList.add('is-native-range'); } + /* ============================================================================ + v37 的共用件(单点与范围两端都走它们) + ============================================================================ */ + + /** + * 把「年 / 月 / 日」三个下拉绑成"改哪个算哪个",并写回隐藏的 `*_date`。 + * + * ⚠️ `*_date` 仍然是**真正的表单字段**(只是 type=hidden)—— + * 「提交契约一字不改」是这样在**结构上**保证的,而不是靠前后端约定。 + * ⚠️ 改年 / 月会重建"日"那一列:2 月在闰年 29 天、4 月 30 天。 + * 原来选的那一天若在新月份里不存在(31 号 → 2 月),**清空**而不是 + * 静默挪到别的一天 —— 用户没选过的日子不能替他选。 + */ + function bindParts(box) { + var native = box.querySelector('.dt-native'); + if (!native) { return; } + var date = native.querySelector('input.dt-date'); + var y = native.querySelector('select.dt-year'); + var mo = native.querySelector('select.dt-month'); + var d = native.querySelector('select.dt-day'); + if (!date || !y || !mo || !d) { return; } // 不是"下拉版"(native 模式)→ 无事可做 + if (box.__fbBound) { return; } + box.__fbBound = true; + + function daysIn(yy, mm) { + return (yy && mm) ? new Date(yy, mm, 0).getDate() : 31; // new Date(y, m, 0) = 上月末 + } + + function rebuildDays(keep) { + var yy = parseInt(y.value, 10), mm = parseInt(mo.value, 10); + var n = daysIn(yy, mm); + var cur = keep ? d.value : ''; + d.innerHTML = ''; + for (var i = 1; i <= n; i++) { + var o = document.createElement('option'); + o.value = String(i); + o.textContent = i + '日'; + d.appendChild(o); + } + d.value = (cur && parseInt(cur, 10) <= n) ? cur : ''; + } + + function sync() { + var v = (y.value && mo.value && d.value) + ? y.value + '-' + pad(+mo.value) + '-' + pad(+d.value) + : ''; + if (date.value !== v) { + date.value = v; + date.dispatchEvent(new Event('change', { bubbles: true })); + } + box.classList.toggle('has-value', v !== ''); + } + + y.addEventListener('change', function () { rebuildDays(true); sync(); }); + mo.addEventListener('change', function () { rebuildDays(true); sync(); }); + d.addEventListener('change', sync); + rebuildDays(false); // 首屏:把"日"列按当前年月铺对 + sync(); + + // 供 fbSetDT / writeDT 程序化改值之后让三个下拉跟上 + box.__fbSyncParts = function () { + var m = (date.value || '').match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!m) { y.value = ''; mo.value = ''; rebuildDays(false); d.value = ''; return; } + y.value = String(+m[1]); + mo.value = String(+m[2]); + rebuildDays(false); + d.value = String(+m[3]); + }; + } + + /** + * 范围组排版:两端各占一列,**宽屏一行、窄屏换行**。 + * + * ⚠️ 几何全部交给 CSS(flex + flex-wrap + 每端的 min-width), + * 这里**只挂类名**。为什么不在这里算宽度: + * 用 JS 量宽度会在字体没加载完 / 容器动画中量错,而且窗口缩放要重算; + * `flex-wrap` 由浏览器在**每次布局时**自己算,永远是对的。 + * 这也正是用户要的:「如果能放得下就放一排;手机上很窄就挤到下一行」。 + */ + function layoutRange(group) { + group.classList.add('dt-cols'); + } + function upgrade(box) { if (box.getAttribute('data-semi-done')) { return; } // 范围对的成员由 upgradeRange 统一处理 if (box.classList.contains('dt-part')) { return; } - var cfg = {}; - try { cfg = JSON.parse(box.getAttribute('data-dt') || '{}'); } catch (e) {} var native = box.querySelector('.dt-native'); if (!native) { return; } - /* 🔴 v36:单点控件也**不合成时间框**了 —— 与范围那两端同一套做法 - (见上面 revealEndpoint 的说明)。直接把原生的「日期 + 时/分下拉」显示出来。 - ⚠️ 秒那一档按用户要求去掉("只记录到时和分就行了")。 */ - if (!box.__fbRevealed) { - box.__fbRevealed = true; - var secEl1 = native.querySelector('.dt-second'); - if (secEl1) { - var prev1 = secEl1.previousElementSibling; - if (prev1 && prev1.classList && prev1.classList.contains('dt-colon')) { prev1.remove(); } - secEl1.remove(); - } - } - // 端点加 .is-native(压掉"未 .is-semi 就藏原生"的规则)+ 直接显示 + /* v37:单点控件**也不再"升级"** —— 服务端给的就是最终形态(年/月/日 + 时/分下拉)。 + 这里只负责绑定与打标记。 */ + bindParts(box); box.classList.add('is-native'); box.classList.add('has-value'); - native.style.display = 'contents'; - native.style.visibility = 'visible'; - box.style.visibility = 'visible'; box.__fbRemount = function () {}; guardRequired(box); box.setAttribute('data-semi-done', '1'); @@ -1507,8 +1551,14 @@ */ function guardRequired(box) { var req = box.querySelector('.dt-date[required]'); - if (!req) { return; } - req.removeAttribute('required'); + /* 🔴 v37:日期改成"年/月/日 下拉 + 隐藏 *_date"之后, + **隐藏框上不能挂 `required`** —— 隐藏控件在表单校验时不可聚焦, + Chrome 会报 "An invalid form control is not focusable":点了保存 + **什么都不发生、也没有红字提示**(这正是引入本守卫的原因)。 + 所以模板改用 `data-dt-required="1"` 声明"这个控件必填",由下面自查。 */ + var wantReq = box.getAttribute('data-dt-required') === '1'; + if (!req && !wantReq) { return; } + if (req) { req.removeAttribute('required'); } box.setAttribute('data-required', '1'); var form = box.closest('form'); if (!form || form.getAttribute('data-dt-guard')) { return; } @@ -1523,10 +1573,10 @@ if (bad.length) { ev.preventDefault(); bad.forEach(function (b) { b.classList.add('dt-missing'); }); - // v35:不再有 .dt-semi —— 焦点落到原生日期框(那才是"该补的字段") - var inp = bad[0].querySelector('.dt-semi input') + // v37:焦点落到**年**那一档(下拉版的第一个可见控件;那才是"该补的字段") + var inp = bad[0].querySelector('select.dt-year') || bad[0].querySelector('.dt-date') - || bad[0].querySelector('input, select'); + || bad[0].querySelector('select, input'); if (inp) { inp.focus(); } } }); @@ -1668,6 +1718,10 @@ }); if (!box) { return false; } writeDT(box, str); + /* 🔴 v37:隐藏的 *_date 写完还不算完 —— 页面上真正给用户看的是 + 「年 / 月 / 日」三个下拉,必须让它们**跟着走到同一个值**, + 否则会出现"程序设了 15:00,下拉里还写着 09:00"这种最难解释的不一致。 */ + if (box.__fbSyncParts) { box.__fbSyncParts(); } // 原生写完了 —— 现在把 Semi 一侧也刷新成同一个值 var group = box.closest ? box.closest('.dt-range') : null; if (group && group.getAttribute('data-semi-range-done') && group.__fbRemount) { diff --git a/index.php b/index.php index 7864239..a68aca0 100644 --- a/index.php +++ b/index.php @@ -2568,13 +2568,21 @@ switch ($r) { /** 观影瀑布流(v9,MovieLog 同款海报墙;带关键词搜索)。 */ case 'movies': $familyId = (int)Auth::familyId(); + $me = (int)Auth::personId(); $keyword = trim((string)($_GET['keyword'] ?? '')); - $rows = Movie::all($familyId, $keyword, (int)Auth::personId()); + $rows = Movie::all($familyId, $keyword, $me); + /* 🔴 v37 用户第 6 条第 4 点:「观影模块也是一样,新增一部电影时, + 也弹出一个框来输入所有信息。」⇒ 列表页内嵌"记一场电影"弹框, + 所以这里要把表单需要的两组数据一并给它(原来只有 movies/new 才传)。 */ View::render('movies/index', [ 'title' => '观影记录', 'rows' => $rows, 'total' => count($rows), 'keyword' => $keyword, + 'peopleForModal' => Person::roster($familyId, $me, 'member', ''), + 'cinemasForModal' => Cinema::all($familyId), + /* 从"新建影院"弹框存完回来的(?cinema=)→ 顺手把外层弹框打开 */ + 'openModal' => (int)($_GET['cinema'] ?? 0) > 0, ], 'movies'); break; @@ -5459,6 +5467,17 @@ $want = array_keys($want); } flash('ok', '任务已新建。'); } + /* 🔴 v37 用户第 6 条第 3 点:「新建任务也和新建日程一样,新建时弹出一个框, + 新建完之后刷新新建任务的**列表、看板**页面。」 + ⇒ 从看板上的"新建任务"弹框提交时带 `back=tasks` → 回看板(刷新了看板)。 + ⚠️ 白名单取值,绝不把 POST 的字符串直接当跳转地址(开放重定向)。 + ⚠️ **只对新建生效**:编辑是从详情页进来的,回详情页才对 + (子任务那个 inline-form 也不带 back → 照旧回详情页看子任务)。 + ⚠️ 新建后不再回详情页的另一个好处:附件/共享在表单里已经填完了, + 没必要让用户再看一遍同一份内容。 */ + if (!$isEdit && (string)($_POST['back'] ?? '') === 'tasks') { + redirect('tasks'); + } redirect('tasks/show', ['id' => $tid]); break; diff --git a/templates/_datetime.php b/templates/_datetime.php index c1056c0..f96e7a1 100644 --- a/templates/_datetime.php +++ b/templates/_datetime.php @@ -1,69 +1,124 @@ : * 它的显示格式由**浏览器 UI 语言 / 系统时间格式**决定, 与元素 lang - * 都压不过它。中文环境实测会渲染成「下午 03:30」,用户很容易把上下午填反; - * 而且它只有时:分,**没有秒**。拆成「原生日期选择器 + 三个下拉」后, - * 24 小时制与"精确到秒"都由控件本身保证,浏览器改不掉。 + * 都压不过它。中文环境实测会渲染成「下午 03:30」,用户很容易把上下午填反。 * - * 日期部分仍是原生 ``(app.js 里的 showPicker 脚本对它同样生效, - * 点输入框任意位置都能弹出日历 —— 这正是"整个输入框点击都可以弹出"的落点)。 + * ════════════════════════════════════════════════════════════════ + * 🔴🔴 v37:**日期部分默认改成「年 / 月 / 日」三个原生下拉**(不再用 ) + * ════════════════════════════════════════════════════════════════ + * 用户原话(2026-10-09 第 4 次反馈,两次都提到同一个痛点): + * 「编辑页面的时间,我还是不是特别满意…选日期的这个控件非常非常不好用。 + * 就是如果我把年和月那个地方拉下来,我选完年和月之后,它并不会回到让我选日的 + * 那个界面,它还是不动的,所以这时候就没有办法选日。这个地方要么换一个控件, + * 要么在那个地方加一个确定按钮…」 + * + * 为什么会这样(不是我们调错了参数):`` 的日历弹层是 + * **浏览器自己画的**,我们既拿不到它的 DOM,也控制不了它的交互流程。 + * Chrome 的弹层在"点顶部 `2026年 10月` 切到年月视图"之后,**切回日期视图要靠 + * 用户再点一次日期或按 Esc** —— 各家浏览器行为还不一致,用户遇到的就是"卡住"。 + * ⇒ 换成**我们能完全控制的原生 ` - value="" - aria-label="日期"> + + + + + value=""> + + + + + + + value="" + aria-label="日期"> + + 被静默清空、取消链接变 id=0(2026-09-30 e2e 抓到)。 */ ?> @@ -92,6 +147,8 @@ $dtCfg = ['name' => $dtName, 'seconds' => $dtSeconds, 'required' => $dtRequired, + $dtDateType, $dtUseParts, $dtYearFrom, $dtYearTo, + $dtTs, $dtTsTmp, $dtDate, $dtY, $dtMo, $dtD, $dtDays, + $dtHour, $dtMin, $dtSec, + $dtH, $dtM, $dtS, $hv, $mv, $sv, $dtYy, $dtMm, $dtDd +); ?> \ No newline at end of file diff --git a/templates/calendar/_event_fields.php b/templates/calendar/_event_fields.php index 13b7188..2aa9244 100644 --- a/templates/calendar/_event_fields.php +++ b/templates/calendar/_event_fields.php @@ -82,13 +82,13 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA 开始 / 结束 *
diff --git a/templates/calendar/index.php b/templates/calendar/index.php index cae1927..055fadd 100644 --- a/templates/calendar/index.php +++ b/templates/calendar/index.php @@ -229,11 +229,21 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts) $evAllDay = false; require __DIR__ . '/_event_fields.php'; ?> -
- + `** —— `.cal-modal-panel` 被提前闭合、 + 跟着又被多关一层,于是 `
` 跑到了 + **面板外面**(DOM 上它是 `.cal-modal` 的直接子元素)。 + 面板 `max-height` 一卡,底栏就被挤到面板下方 → 用户看不到。 + 另有 CSS 一层:`.cal-modal-panel > form` 原先不是 flex 列, + `.cal-modal-body{flex:1 1 auto}` 因此不起作用(两处都已修)。 + ⚠️ 提交键用 `form="calAddForm"` **显式关联**:多一层/少一层 DOM 的 + 意外都不会再让它失效(它本来就在 form 内部,但显式写更稳)。 */ ?> +
diff --git a/templates/home.php b/templates/home.php index 14e514b..acc0391 100644 --- a/templates/home.php +++ b/templates/home.php @@ -334,8 +334,28 @@ unset($evRef); style="background: ; top: %; height: %; - left: calc(46px + %); - width: calc(% - 50px);" + + left: calc(46px + % + 2px); + width: calc(% - 4px);" title="" data-cal=""> diff --git a/templates/movies/form.php b/templates/movies/form.php index 9ad7f9a..45d64ea 100644 --- a/templates/movies/form.php +++ b/templates/movies/form.php @@ -30,6 +30,13 @@ $selCinema = (int)old('cinema_id', (int)($_GET['cinema'] ?? 0) ?: ($m['cinema_id $curAttendees = array_map('intval', array_column($m['attendees'] ?? [], 'id')); $selAttendees = array_map('intval', (array)old('attendees', $curAttendees)); ?> + +

@@ -44,15 +51,42 @@ $selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
+ + + + +
+ + + +
+ + +
+ + 取消 +
+ + + + + + +
- 记一场电影 + +
+ + + + diff --git a/templates/movies/show.php b/templates/movies/show.php index 7b1e672..096f34a 100644 --- a/templates/movies/show.php +++ b/templates/movies/show.php @@ -282,17 +282,9 @@ $commentBox = static function (int $reviewId, int $parentId, string $summary) us - - -
-

- 要加照片请点上方「编辑」——「观影照片」在编辑页里一起传(可多选)。 -

-
- +
diff --git a/templates/tasks/index.php b/templates/tasks/index.php index 6e95455..aca7bcf 100644 --- a/templates/tasks/index.php +++ b/templates/tasks/index.php @@ -66,11 +66,30 @@ $lastColIdx = count($colList) - 1;
-