v39 第 39 批:观影弹框(antd 中文化 + 影院搜索 + 简介备注各占一行 + 海报照片对齐)
用户原话(四条):
①「观影模块的时间控件,比如说时间呀,还有下面的确定按钮啊。现在显示的都是英文,
应该给它设置成中文。」
②「影院那个地方应该有一个搜索的功能,下拉的地方上面有一个搜索筛选的功能。」
③「简介和备注不要放在一行,也就是说一个字段它就占一行。所以简介后面是一行,
备注后面是一行。」
④「海报和观影照片为什么和上面的标题不一样呢?海报的字体和简介是一样的,也带一个冒号。
然后把后面点击或拖动进行上传往后放,和简介后面的输入框保持一样大小。观影照片也是
类似的,观影照片也是加粗。和简介的字体保持一致,不带冒号,后面放上点击或拖动进行上传。」
① antd 中文化 —— 三个条件缺一不可(缺任一条按钮就还是英文)
· 🔴 **英文的真根因**:`antd.min.js` 这个产物**不含 locale 包**
(实测 `window.antd.locales` 是 undefined)⇒ 只能用它内置的英文默认值。
**换文件**才解决,不是加参数 ⇒ 改用 `antd-with-locales.min.js`
(多带各语言包,全局多挂 `antd.locales`,`antd.locales.zh_CN` 即简体中文包)。
· 🔴 dayjs 的中文 locale 是**单独一个文件**(核心包只有 en)⇒ 引 `dayjs-zh-cn.js`
并调 `dayjs.locale('zh-cn')`(星期/月份由 dayjs 出)。
· 🔴 必须用 `<ConfigProvider locale={zh_CN}>` 包住控件 —— antd 读的是**上下文里的**
locale,不包的话**日历文字会变中文、按钮却仍是英文**(最容易误判成"已经中文化了")。
· 独立探针 `_tooling/_antd_probe/{zh.html,zh_probe.py}` 9/0 把这三条钉死。
· 删除不再引用的 `assets/vendor/antd/antd.min.js`(1.5MB 冗余,留着会让
"两个 antd 哪个是真的"变成下一个坑);部署断言改为钉四资源 + ConfigProvider + zh-cn。
② 影院下拉加搜索筛选
· 搜索框放在弹出列表**内部**(`.cs-search` 吸顶),打开下拉**即聚焦**;回车选第一项。
· 🔴 `.cs-item[hidden] { display: none; }` **必须显式写** —— `.cs-item` 自己声明了
`display:flex`,会盖过浏览器默认的 `[hidden]{display:none}` ⇒ 过滤"看着完全没生效"
(项目第 11 批选人组件踩过同一个坑)。
· 三个事件都 stopPropagation,否则冒到 document 的"点外面关"⇒ 一点搜索框下拉就没了。
· 「不记录影院」恒保留(清空选择的出口);收起时清空搜索词(否则下次带着旧过滤)。
③ 简介 / 备注各占一行
· 撤销 v38 的 `.mv-pair` 并排(半宽的多行文本框每行都换行,更难读);
CSS 与模板里的并排容器**全部清干净**。
④ 海报 / 观影照片与简介对齐 —— 它们本来就不需要特殊规则
· 用户看到的"更窄/不一样"有两个原因:① 海报那块被误包进 `.mv-grid.mv-pair`
且**里面只有一个孩子** ⇒ grid 两列让它只占**半宽**;② 标签被一条
"宽度自适应、左对齐"的规则覆盖掉了固定 62px 右对齐 + 冒号。
· ⇒ 两块都改成普通 `.mv-row.mv-row-media`,标签用 `.mv-lb`(**继承**简介那套),
内容包 `.mv-media-body`(占满剩余);观影照片标签加 `.no-colon`。
实测:所有控件左缘 **424**、宽度 **624**,与简介**完全一致**。
· 🔴🔴 **通识**:v38 的 `.mv-media`(手填 margin-left:72px)是**被一个取错元素的
探针误导出来的** —— 它按文档序取"第一个非标签子元素",而 `_stage_drop.php` 吐的
`.sr-only` 文件框是 `position:absolute`(**脱离文档流、不参与 flex**),却被算成
"控件",于是得出"1px 隐框把上传框推走 130px"的**假结论**。
⇒ "对齐"类断言必须取直接子元素、且只取真正参与布局的那个;量到反常数字先怀疑探针。
验证(全部真机 + 反向验证):
· _fb_b39_movie.py **39/0**(中文化三条 + 搜索框打开即聚焦/输入即过滤/无匹配空态/
点搜索框不关下拉/回车选中/重开已清词 + 简介备注各占一行且占整行宽 +
海报照片标签字重字号同简介/海报带冒号/照片不带/上传框左缘与宽度同简介);
· _fb_b39_narrow.py **21/0**(宽 1440 / 平板 768 / 窄屏 390:不横向溢出、
标签与控件仍同行、影院下拉不出视口、搜索框可用);
· _fb_b39_falsify.py **14/0**(四处各自证伪成功:拿掉 ConfigProvider→按钮变英文、
藏起搜索框→②红、塞回两列并排→③红、标签退回普通 span→④红;复原回全绿);
· v38 两套按 v39 更新后 **36/0**(新增"已无 mv-pair / 主列 6 行");
v38 的旧窄屏套件(断言"简介备注并排")已作废删除,由 v39 窄屏完全覆盖;
· 全路由 e2e **1503/0**、日期控件真机 **53/0** + 反向 **7/0**、v37 两套、
观影照片两套、b30 三件套、各批真机与 DAV 三套、部署静态断言空跑 —— 全绿;
· 线上四个 antd 资源 HTTP **200 可达**;app.js / app.css / form.php / layout.php
的 SHA256 与本地**逐字节一致**。
⚠️ 顺带修的探针/夹具问题(都不是页面 bug):
· 夹具库重铺后 `cinemas` 是**空的** ⇒ v39 探针**自带播种**(造两家影院),
不再依赖别的套件先跑过(与 v38 的"名册只有 1 人"同类:页面没问题,是夹具没数据);
· 部署断言第一版按裸词 grep `mv-pair`,把注释里那句"已撤销 .mv-pair"当成回归
⇒ 改为只认 `class="...mv-pair..."` 这种**真的类名用法**;
· v39 探针第一版判"简介/备注各占一行"看的是"是不是同一个 DOM 节点"——
那个在**并排**时也是 false(并排仍是两个 .mv-row),连并排都抓不住
(反向验证当场抓到 13/1)⇒ 改为按**垂直位置**判(top 相差 ≥20px)。
This commit is contained in:
+60
-27
@@ -2884,11 +2884,35 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
|
||||
padding: 4px; max-height: 280px; overflow-y: auto;
|
||||
}
|
||||
.cs-menu[hidden] { display: none; }
|
||||
/* 🔴 v39(用户 2026-10-10):「影院那个地方应该有一个搜索的功能,下拉的地方上面
|
||||
有一个搜索筛选的功能。」
|
||||
⇒ 弹出列表顶部一条搜索框。⚠️ 它要**吸顶**(列表长时往下滚,它跟着走)——
|
||||
不然滚两下搜索框就没了,"搜索筛选"等于白加。 */
|
||||
.cs-search {
|
||||
position: sticky; top: -4px; z-index: 2;
|
||||
margin: -4px -4px 4px; padding: 6px 6px 6px;
|
||||
background: var(--surface); border-bottom: 1px solid var(--line-2);
|
||||
}
|
||||
.cs-search-input {
|
||||
width: 100%; box-sizing: border-box;
|
||||
padding: 7px 10px; font: inherit; font-size: 13px; color: var(--ink);
|
||||
background: var(--paper, #FDFBF7); border: 1px solid var(--line);
|
||||
border-radius: 7px;
|
||||
}
|
||||
.cs-search-input::placeholder { color: var(--muted); }
|
||||
.cs-search-input:focus { outline: none; border-color: var(--brand); }
|
||||
/* 没有匹配项时的提示(没搜过 / 有结果时都 hidden,不占高度) */
|
||||
.cs-empty { padding: 10px 12px; font-size: 13px; color: var(--muted); text-align: center; }
|
||||
.cs-empty[hidden] { display: none; }
|
||||
.cs-item {
|
||||
display: flex; align-items: center; gap: 8px; width: 100%;
|
||||
padding: 8px 10px; border: 0; border-radius: 7px; background: transparent;
|
||||
font: inherit; font-size: 13.5px; color: var(--ink); cursor: pointer; text-align: left;
|
||||
}
|
||||
/* ⚠️ 过滤用的 `hidden` 必须写出来:`.cs-item` 自己声明了 `display:flex`,
|
||||
它会**盖过**浏览器默认的 `[hidden]{display:none}` ——
|
||||
这正是第 11 批"选人过滤一直是坏的"的同一个坑,别再踩第二次。 */
|
||||
.cs-item[hidden] { display: none; }
|
||||
.cs-item:hover, .cs-item:focus { background: var(--line-2); outline: none; }
|
||||
|
||||
/* 新建影院:居中弹框(与名册的动作弹框同一形态,只是更宽) */
|
||||
@@ -5204,21 +5228,37 @@ h5.md-h, h6.md-h { font-size: 14px; }
|
||||
}
|
||||
.mv-row.dt-range .err { flex: 1 1 100%; margin-left: 72px; }
|
||||
|
||||
/* 海报 / 观影照片:**标签一行、内容另起一行**(不用 .mv-row)。
|
||||
🔴 为什么不能跟上面的字段一样"标签在左、控件在右"(真机量出来的):
|
||||
这两块的控件区里有好几个东西,而且 `_stage_drop.php` 会在最前面插一个
|
||||
`.sr-only` 的 `<input>`(绝对定位、1px)。按 flex 排的话,那个 1px 的隐框
|
||||
会占掉一整份 `flex:1 1 auto`,把真正的上传框**推到 130px 之外**
|
||||
(实测:左缘 443 vs 隔壁 573 —— 一眼就是歪的)。
|
||||
⇒ 纵向结构最省心:内容从**标签列右侧**(72px = 62 标签 + 10 间距)开始,
|
||||
与上面各行的控件左缘**同一条竖线**。 */
|
||||
/* 海报 / 观影照片:**与简介同一套 `.mv-row`**(标签在左、内容在右)。
|
||||
🔴🔴 v39(用户 2026-10-10):「海报和观影照片为什么和上面的标题不一样呢?
|
||||
海报的字体和简介是一样的,也带一个冒号。然后把后面点击或拖动进行上传往后放,
|
||||
和简介后面的输入框保持一样大小。观影照片…也是加粗。和简介的字体保持一致,
|
||||
不带冒号。]
|
||||
|
||||
⇒ 关键认识:**这两块不需要任何特殊规则**,它们就是普通字段行 ——
|
||||
标签 = `.mv-lb`(继承上面那条:14px / 600 / var(--ink) / 固定 62px / 右对齐
|
||||
+ `::after` 冒号),内容 = 行里唯一参与弹性布局的那一项(占满剩余、左缘 444)。
|
||||
v38 之所以把它们写成 `.mv-media`(另起一行 + 手填 `margin-left:72px`),
|
||||
是因为当时**探针取错了元素** —— 它按文档序取"第一个非标签子元素",
|
||||
而 `.sr-only` 的文件框是 `position:absolute`(**脱离文档流、不参与 flex**),
|
||||
却被算成了"控件",于是量出"1px 隐框把上传框推走 130px"这个**假结论**。
|
||||
⇒ 这轮改用真机逐行量左缘来定,不再靠推断。
|
||||
|
||||
⚠️ 想让它别再"另起一行":这里**不设** flex-direction —— `.mv-row` 本来就是
|
||||
`display:flex; align-items:flex-start`,标签与内容天然同一行。 */
|
||||
.mv-grid-media { margin-top: 2px; gap: 14px; }
|
||||
.mv-media { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
||||
.mv-media > .mv-lb { flex: 0 0 auto; width: auto; text-align: left; padding-top: 0; }
|
||||
.mv-media-body { margin-left: 72px; display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
||||
/* 内容区:纵向排(缩略图 + 上传框),并**占满**标签右侧的剩余宽度
|
||||
(`flex:1 1 auto` + `min-width:0` 与 `.mv-row > *:not(.mv-lb)` 那条一致,
|
||||
这里显式写一遍是为了让"它的宽度 = 简介输入框的宽度"这件事一眼可读)。 */
|
||||
.mv-row-media > .mv-media-body {
|
||||
flex: 1 1 auto; min-width: 0;
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
}
|
||||
.mv-media-body .photo-grid { margin-top: 0; }
|
||||
/* 观影照片的标签**不带冒号**(用户原话:「观影照片也是加粗。和简介的字体保持一致,
|
||||
不带冒号。」)—— 这类"有标签但不带冒号"的情况靠一个类开关,别去改 ::after 的全局规则。 */
|
||||
.mv-lb.no-colon::after { content: none; }
|
||||
/* ⚠️ `.sr-only` 是**绝对定位**的(见 .sr-only 的定义);它不该影响任何布局。
|
||||
这里再钉一次"不参与 flex 分配",免得又被当成一个控件。 */
|
||||
这里再钉一次"不参与 flex 分配"(万一将来有人把 .sr-only 的定义改了)。 */
|
||||
.mv-media-body > .sr-only,
|
||||
.mv-row > .sr-only { position: absolute; flex: 0 0 0; }
|
||||
|
||||
@@ -5275,18 +5315,11 @@ h5.md-h, h6.md-h { font-size: 14px; }
|
||||
.mv-row.dt-range.dt-cols > .dt-range-label { flex: 0 0 52px; width: 52px; font-size: 13px; }
|
||||
}
|
||||
|
||||
/* ---- v38 ②:能并排的都并排(用户:「能把它排成一行的都排成一行」)----
|
||||
两处成对的字段:简介+备注、海报+观影照片。
|
||||
⚠️ 用 **grid 两列**而不是 flex-basis:fr 单位保证两列**永远等宽**,
|
||||
不会因为左边内容天然更宽(比如海报的图)就把右边挤窄 —— 那正是"不对齐"。
|
||||
⚠️ 窄屏(≤560px)回到单列:手机上一行放两个多行文本框会窄到没法用。 */
|
||||
.mv-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 20px; }
|
||||
.mv-pair > .mv-row, .mv-pair > .mv-media { min-width: 0; }
|
||||
/* 并排里每块自己的标签列窄一点(总宽只有一半了) */
|
||||
.mv-pair > .mv-row > .mv-lb { flex: 0 0 52px; width: 52px; }
|
||||
/* 并排里的媒体块:内容不再缩进 72px(本来就只占半列,再缩就没宽度了) */
|
||||
.mv-pair > .mv-media > .mv-media-body { margin-left: 0; }
|
||||
@media (max-width: 560px) {
|
||||
.mv-pair { grid-template-columns: 1fr; }
|
||||
.mv-pair > .mv-row > .mv-lb { flex: 0 0 62px; width: 62px; }
|
||||
}
|
||||
/* ---- 🔴 v39:撤销 v38 的"并排对" ----
|
||||
用户 2026-10-10:「简介和备注不要放在一行,也就是说一个字段它就占一行。
|
||||
所以简介后面是一行,备注后面是一行。」—— 简介/备注各占一整行。
|
||||
⇒ `.mv-pair` 那套(grid 两列)整个删掉:它是 v38 为了压高度加的,
|
||||
但半宽的多行文本框反而每行都换行、更难读。海报/观影照片也不再并排
|
||||
(它们本来就只有一块,当时的"并排网格"里只有一个孩子,
|
||||
grid 让它只占半宽 —— 那正是"海报的上传框比简介窄"的原因)。
|
||||
⚠️ 不留悬念:全站搜 `.mv-pair` 应该**零命中**(模板与 CSS 都清干净)。 */
|
||||
|
||||
+38
-2
@@ -1547,6 +1547,17 @@
|
||||
③ `onChange` 只在**两端都齐了**才触发,单端改动走 `onCalendarChange`。
|
||||
⇒ 回写必须同时挂 `onCalendarChange`(实时)**和** `onChange`(确认),
|
||||
否则"只填了开始时间就提交"会丢值。
|
||||
|
||||
🔴🔴 v39(用户 2026-10-10):「时间控件…还有下面的确定按钮,现在显示的都是英文,
|
||||
应该给它设置成中文。」
|
||||
· 英文的**根因**:普通 `antd.min.js` 产物**不含** locale 包(`antd.locales` 是
|
||||
undefined),所以面板只能用它内置的英文默认值 ⇒ 改载 **antd-with-locales**;
|
||||
· 另外 dayjs 核心包只有 en,星期/月份是 dayjs 出的 ⇒ 必须**另加载**
|
||||
`dayjs-zh-cn.js` 并调 `dayjs.locale('zh-cn')`;
|
||||
· 最后用 `<ConfigProvider locale={zh_CN}>` 包住控件 —— antd 读的是**上下文里的**
|
||||
locale,不包的话即使资源在、面板照样是英文(但**日历文字**会变中文,容易误判)。
|
||||
⚠️ 三个条件缺一不可;这也是为什么单独写了 `_antd_probe/zh.html` 把它钉死(9/0)。
|
||||
|
||||
⚠️ 这个适配层**不认识的页面不受影响**:没有 `[data-dt-antd]` 就直接返回。
|
||||
============================================================================ */
|
||||
function bootAntd(hosts) {
|
||||
@@ -1572,12 +1583,20 @@
|
||||
if (!window.ReactDOM) { chain = chain.then(function () { return loadJs(conf.dom || ''); }); }
|
||||
loading = chain
|
||||
.then(function () { return window.dayjs ? null : loadJs(conf.dayjs); })
|
||||
// 🔴 v39:dayjs 的中文 locale 是**单独一个文件**(核心包只有 en)。
|
||||
// 它必须在 dayjs 之后、antd 之前加载 —— 加载完是挂一个
|
||||
// `window.dayjs_locale_zh_cn` 函数(见文件头的说明)。
|
||||
.then(function () {
|
||||
if (!conf.dayjsZh || window.dayjs_locale_zh_cn) { return null; }
|
||||
return loadJs(conf.dayjsZh).catch(function () { return null; });
|
||||
})
|
||||
.then(function () { return window.antd ? null : loadJs(conf.antd); });
|
||||
return loading;
|
||||
}
|
||||
|
||||
var done = 0;
|
||||
ensure().then(function () {
|
||||
applyDayjsZh();
|
||||
Array.prototype.forEach.call(hosts, function (box) {
|
||||
if (mountAntd(box)) { done++; }
|
||||
});
|
||||
@@ -1588,6 +1607,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
/** 把 dayjs 切到简体中文(星期/月份/相对时间都由它出)。
|
||||
* ⚠️ 拿不到就静默跳过 —— 那只会让星期显示成英文,不该拦住整个控件。 */
|
||||
function applyDayjsZh() {
|
||||
try {
|
||||
if (window.dayjs && window.dayjs.locale) { window.dayjs.locale('zh-cn'); }
|
||||
} catch (e) { /* 忽略:dayjs locale 未注册时不做任何事 */ }
|
||||
}
|
||||
|
||||
/** 把**一对**端点升级成 antd RangePicker;成功返回 true。 */
|
||||
function mountAntd(box) {
|
||||
var group = box.closest ? box.closest('.dt-range') : null;
|
||||
@@ -1625,7 +1652,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
ReactDOM.createRoot(host).render(React.createElement(antd.DatePicker.RangePicker, {
|
||||
var picker = React.createElement(antd.DatePicker.RangePicker, {
|
||||
showTime: { format: 'HH:mm' },
|
||||
format: 'YYYY-MM-DD HH:mm',
|
||||
allowClear: true,
|
||||
@@ -1635,7 +1662,16 @@
|
||||
// onChange 只在两端齐了才响。两个都挂才不会丢值。
|
||||
onCalendarChange: function (vals, strs) { writeBack(strs); },
|
||||
onChange: function (vals, strs) { writeBack(strs); }
|
||||
}));
|
||||
});
|
||||
/* 🔴 v39:**必须用 ConfigProvider 包住**,否则面板上的「确定 / 取消 / 此刻」
|
||||
和星期格式还是英文 —— antd 读的是**上下文里的** locale,不是全局变量。
|
||||
(实测过:资源挂了但没包,日历标题/日期会变中文,**按钮却仍是英文**,
|
||||
最容易误判成"已经中文化了"。) */
|
||||
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 RangePicker 挂载失败,留在自建下拉:', e && e.message); }
|
||||
host.parentNode.removeChild(host);
|
||||
|
||||
+353
File diff suppressed because one or more lines are too long
Vendored
-353
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
||||
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_zh_cn=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),d={name:"zh-cn",weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),ordinal:function(e,_){return"W"===_?e+"周":e+"日"},weekStart:1,yearStart:4,formats:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日Ah点mm分",LLLL:"YYYY年M月D日ddddAh点mm分",l:"YYYY/M/D",ll:"YYYY年M月D日",lll:"YYYY年M月D日 HH:mm",llll:"YYYY年M月D日dddd HH:mm"},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1 分钟",mm:"%d 分钟",h:"1 小时",hh:"%d 小时",d:"1 天",dd:"%d 天",M:"1 个月",MM:"%d 个月",y:"1 年",yy:"%d 年"},meridiem:function(e,_){var t=100*e+_;return t<600?"凌晨":t<900?"早上":t<1100?"上午":t<1300?"中午":t<1800?"下午":"晚上"}};return t.default.locale(d,null,!0),d}));
|
||||
+14
-3
@@ -70,15 +70,26 @@ $isLoggedIn = \App\Auth::check();
|
||||
https://ant-design.antgroup.com/components/date-picker-cn 现在观影这个模块
|
||||
进行测试吧…等这个模块测试完没问题了,再更新到其他的模块。」
|
||||
⇒ Ant Design 的 RangePicker **只在观影模块试点**(其余页面继续用自建下拉)。
|
||||
同样按需加载:只有页面真出现 `[data-dt-antd]` 才去拉这三个文件
|
||||
(antd.min.js 单文件约 1.5 MB,绝不能让用不到的页面白拉)。
|
||||
同样按需加载:只有页面真出现 `[data-dt-antd]` 才去拉那几个文件
|
||||
(antd-with-locales 单文件约 1.8 MB,绝不能让用不到的页面白拉)。
|
||||
⚠️ 必须同时引 `reset.css` —— antd 的组件依赖它做 box-sizing/边距归一,
|
||||
不引会让面板里的日期格子错位(它是 antd 官方文档里指定的用法)。
|
||||
⚠️ `dayjs` 也要引:antd 5 的值类型是 dayjs 对象,UMD 版从全局 `dayjs` 取。 */ ?>
|
||||
<meta name="fb-antd" content="<?= e(json_encode([
|
||||
'css' => asset('/assets/vendor/antd/reset.css'),
|
||||
'antd' => asset('/assets/vendor/antd/antd.min.js'),
|
||||
/* 🔴 v39(用户 2026-10-10):「时间控件…还有下面的确定按钮,现在显示的都是英文,
|
||||
应该给它设置成中文。」
|
||||
⇒ 换成 **antd-with-locales**(同一个包的另一个产物,多带各语言包),
|
||||
它在全局多挂一个 `antd.locales`,`antd.locales.zh_CN` 就是简体中文包
|
||||
(含「确定 / 取消 / 此刻 / 今天」与星期/月份格式)。
|
||||
⚠️ 普通 `antd.min.js` **不含** locales(实测 `antd.locales` 是 undefined)
|
||||
—— 这是"按钮还是英文"的真正原因,换掉这个文件才解决。
|
||||
⚠️ 它还依赖 dayjs 的中文 locale(星期/月份由 dayjs 出),见下面的 dayjsZh。 */
|
||||
'antd' => asset('/assets/vendor/antd/antd-with-locales.min.js'),
|
||||
'dayjs' => asset('/assets/vendor/antd/dayjs.min.js'),
|
||||
/* dayjs 的核心包**只有 en**;中文 locale 是单独一个文件,
|
||||
加载后调 `dayjs.locale('zh-cn')` 才生效(否则面板里星期还是 Su/Mo…)。 */
|
||||
'dayjsZh' => asset('/assets/vendor/antd/dayjs-zh-cn.js'),
|
||||
/* ⚠️ antd 的 UMD **要求全局有 React / ReactDOM**(缺一个就直接挂不出来 ——
|
||||
实测报错是 `Cannot read properties of undefined (reading 'createContext')`)。
|
||||
项目里已经有一套 React 18.3.1(原本给 Semi 用的),**直接复用同一份**:
|
||||
|
||||
+126
-31
@@ -171,6 +171,19 @@ $mvOpen = $mvModal && !$mvModalOnly;
|
||||
<span class="cs-caret" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<div class="cs-menu" id="csMenu" hidden role="listbox">
|
||||
<?php /* 🔴 v39(用户 2026-10-10):「影院那个地方应该有一个搜索的功能,
|
||||
下拉的地方上面有一个搜索筛选的功能。」
|
||||
⇒ 弹出列表**顶部**放一个搜索框,输入即过滤(按地点+名称)。
|
||||
⚠️ 搜索框是**这个列表的一部分**,不放在外面 ——
|
||||
它只在"下拉打开时"才有意义,放在外面会一直占一行高度。
|
||||
⚠️ 它不是表单字段(没有 name),只是筛选手;
|
||||
选中仍然靠点列表项 → 写进隐藏的 cinema_id。 */ ?>
|
||||
<div class="cs-search">
|
||||
<input type="text" class="cs-search-input" id="csSearch"
|
||||
placeholder="搜索影院" autocomplete="off"
|
||||
aria-label="搜索影院" role="searchbox">
|
||||
</div>
|
||||
<div class="cs-list" id="csList">
|
||||
<button type="button" class="cs-item" data-id="0" data-color="" role="option">
|
||||
<span class="cs-dot is-none" aria-hidden="true"></span><span>不记录影院</span>
|
||||
</button>
|
||||
@@ -183,6 +196,9 @@ $mvOpen = $mvModal && !$mvModalOnly;
|
||||
<span><?= e(trim(((string)($c['place'] ?? '') !== '' ? $c['place'] . ' · ' : '') . $c['name'])) ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div><?php /* /.cs-list */ ?>
|
||||
<?php /* 一条都没匹配上时露出来(平时 hidden,不占高度) */ ?>
|
||||
<div class="cs-empty" id="csEmpty" hidden>没有匹配的影院</div>
|
||||
</div>
|
||||
<input type="hidden" id="cinema_id" name="cinema_id" value="<?= (int)$selCinema ?>">
|
||||
</div>
|
||||
@@ -212,20 +228,20 @@ $mvOpen = $mvModal && !$mvModalOnly;
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* 🔴 v38 用户:「能把它排成一行的都排成一行」——
|
||||
简介与备注都是多行文本、又都不长,并排成一对最省高度
|
||||
(各自仍带自己的内联标签,左右两列**各自内部对齐**)。 */ ?>
|
||||
<div class="mv-pair">
|
||||
<div class="mv-row">
|
||||
<label class="mv-lb" for="info">简介</label>
|
||||
<textarea class="input" id="info" name="info" rows="3"
|
||||
placeholder="剧情简介"><?= e(old('info', $m['info'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
<?php /* 🔴 v39(用户 2026-10-10):「简介和备注不要放在一行,也就是说一个字段
|
||||
它就占一行。所以简介后面是一行,备注后面是一行。」
|
||||
⇒ 撤销 v38 的 `.mv-pair` 并排,两块各占一整行(回到普通 `.mv-row`)。
|
||||
⚠️ 并排当时是为了压高度,但多行文本框挤半宽后每行都换行、反而更难读 ——
|
||||
用户看过真实效果后的选择是"各占一行",按他说的做。 */ ?>
|
||||
<div class="mv-row">
|
||||
<label class="mv-lb" for="info">简介</label>
|
||||
<textarea class="input" id="info" name="info" rows="3"
|
||||
placeholder="剧情简介"><?= e(old('info', $m['info'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mv-row">
|
||||
<label class="mv-lb" for="note">备注</label>
|
||||
<textarea class="input" id="note" name="note" rows="3"><?= e(old('note', $m['note'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
<div class="mv-row">
|
||||
<label class="mv-lb" for="note">备注</label>
|
||||
<textarea class="input" id="note" name="note" rows="2"><?= e(old('note', $m['note'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
@@ -249,16 +265,29 @@ $mHasTicket = $isEdit && !empty($m['ticket_key']);
|
||||
$mPhotos = $isEdit ? (array)($m['photos'] ?? []) : [];
|
||||
$mDelForms = []; // 收集起来,在表单**外面**统一渲染
|
||||
?>
|
||||
<?php /* 🔴 v38 排版:海报 / 观影照片这两块不用 `.mv-row`。
|
||||
原因(真机量出来的):它们的控件区里有**多个**东西 ——
|
||||
· `_stage_drop.php` 会吐一个 `.sr-only` 的 `<input>`(绝对定位、1px)
|
||||
排在 `.upfield` **前面**;
|
||||
· 于是"谁才是控件"就说不清了 —— 按 `.mv-row` 的 flex 排,那个 1px 的
|
||||
隐框会占掉一份 `flex:1 1 auto`,把后面的上传框**推到 130px 之外**
|
||||
(实测左缘 443 vs 隔壁 573,一眼就是歪的)。
|
||||
⇒ 这两块改成纵向:**标签一行、内容另起一行**,内容从**标签列右侧**开始
|
||||
(`margin-left` 对齐那 72px),这样既不会歪,也不会被无关元素挤走。 */ ?>
|
||||
<div class="mv-grid mv-pair mv-grid-media" style="margin-top:10px">
|
||||
<?php /* 🔴🔴 v39(用户 2026-10-10):「海报和观影照片为什么和上面的标题不一样呢?
|
||||
海报的字体和简介是一样的,也带一个冒号。然后把后面点击或拖动进行上传往后放,
|
||||
和简介后面的输入框保持一样大小。观影照片也是类似的,观影照片也是加粗。
|
||||
和简介的字体保持一致,不带冒号,后面放上点击或拖动进行上传。」
|
||||
|
||||
⇒ 两块都改成**与简介完全相同的 `.mv-row`**:
|
||||
· 标签用 `.mv-lb`(= 简介那个:14px / 600 / var(--ink) / 固定 62px / 右对齐),
|
||||
冒号由 `.mv-lb::after` 统一给 —— 所以**海报自带冒号**、
|
||||
而观影照片的标签多一个 `.no-colon`(用户明确说"不带冒号")。
|
||||
· 内容(缩略图 + 上传框)包一层 `.mv-media-body`,它是 `.mv-row` 里
|
||||
**唯一**参与弹性布局的项 ⇒ 左缘与简介输入框同为 444、宽度也同为"占满剩余"。
|
||||
|
||||
⚠️ 为什么不再用 v38 那套 `.mv-media`(标签一行、内容另起一行 + `margin-left:72px`):
|
||||
· 那套是靠"手填 72px"去凑对齐的,75px/72px 这类数字一旦改标签宽度就散;
|
||||
· 更关键的是**海报那块当时被误包进了一个"并排网格"**
|
||||
(`.mv-grid.mv-pair` 里面只有它一个孩子)⇒ grid 的两列布局让它只占**半宽**,
|
||||
于是它的上传框明显比简介的输入框窄 —— 用户看到的正是这个。
|
||||
⇒ 现在回归 `.mv-row`,宽度/左缘/字号全部与上面各字段**同一套规则**,
|
||||
"看起来一样"不是靠对齐数字,而是因为它们本来就是同一种行。
|
||||
⚠️ `.sr-only` 的文件框是 `position:absolute`(脱离文档流),**不会**参与
|
||||
flex 分配;v38 说的"1px 隐框占掉一份宽度"其实是当时**探针量错了元素**
|
||||
(它按文档序取"第一个非标签子元素",正好取到那个绝对定位的 input)。
|
||||
这一轮用真机逐行量左缘来验证,不靠推断。 */ ?>
|
||||
<?php /* ---- 观影照片(v35:不再分类,合并成一块)----
|
||||
用户原话:「除了海报之外,票根、现场照片等等都不分类别,都只分一个类,
|
||||
放在一块。…只有海报是单独的…其他的都放在一个模块就行,
|
||||
@@ -269,7 +298,7 @@ $mDelForms = []; // 收集起来,在表单**外面**统一渲染
|
||||
⚠️ **旧的 ticket_key 仍然要显示**(历史数据不能丢):编辑页把已有票根
|
||||
列进下面这块的网格里(它没有 `event_photos` 行 id,所以只能"看"、
|
||||
不能用照片的删除按钮删;要换就直接上传新的)。 */ ?>
|
||||
<div class="mv-media">
|
||||
<div class="mv-row mv-row-media">
|
||||
<span class="mv-lb">海报</span>
|
||||
<div class="mv-media-body">
|
||||
<?php if ($mHasPoster): ?>
|
||||
@@ -293,12 +322,11 @@ $mDelForms = []; // 收集起来,在表单**外面**统一渲染
|
||||
require __DIR__ . '/../_stage_drop.php'; ?>
|
||||
</div><?php /* /.mv-media-body */ ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 观影照片:票根 + 合影 + 其它相关图片,**全部放这一块** ---- */ ?>
|
||||
<div class="mv-media">
|
||||
<span class="mv-lb">观影照片</span>
|
||||
<div class="mv-media-body">
|
||||
<div class="mv-row mv-row-media">
|
||||
<span class="mv-lb no-colon">观影照片</span>
|
||||
<div class="mv-media-body">
|
||||
<?php if ($mHasTicket || $mPhotos): ?>
|
||||
<div class="photo-grid">
|
||||
<?php if ($mHasTicket): /* 历史数据:旧票根(不在 event_photos 里,只展示) */ ?>
|
||||
@@ -333,7 +361,7 @@ $mDelForms = []; // 收集起来,在表单**外面**统一渲染
|
||||
$sdMsg = '点击或拖动进行上传';
|
||||
require __DIR__ . '/../_stage_drop.php'; ?>
|
||||
</div><?php /* /.mv-media-body */ ?>
|
||||
</div><?php /* /.mv-media */ ?>
|
||||
</div><?php /* /.mv-row(观影照片) */ ?>
|
||||
</div><?php /* /.mv-grid */ ?>
|
||||
</div><?php /* /.mv-form-inner */ ?>
|
||||
<?php /* 🔴🔴 下面这一段**严格按分支配对**,别再把 </div> 与 <?php if ?> 混着写 ——
|
||||
@@ -605,9 +633,49 @@ $mDelForms = []; // 收集起来,在表单**外面**统一渲染
|
||||
var dot = document.getElementById('csDot');
|
||||
var text = document.getElementById('csText');
|
||||
var input = document.getElementById('cinema_id');
|
||||
var search = document.getElementById('csSearch'); /* v39:下拉顶部的搜索框 */
|
||||
var list = document.getElementById('csList');
|
||||
var empty = document.getElementById('csEmpty');
|
||||
|
||||
function close() { menu.hidden = true; btn.setAttribute('aria-expanded', 'false'); }
|
||||
function openMenu() { menu.hidden = false; btn.setAttribute('aria-expanded', 'true'); }
|
||||
function close() {
|
||||
menu.hidden = true;
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
/* 🔴 v39:收起时把搜索词**清掉并恢复全部项** ——
|
||||
否则下次打开会带着上次的过滤结果,用户以为"影院丢了"。
|
||||
(不做成"保留搜索词",因为这里的搜索是"临时找人",不是"长期筛选"。) */
|
||||
if (search) { search.value = ''; }
|
||||
filter('');
|
||||
}
|
||||
function openMenu() {
|
||||
menu.hidden = false;
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
/* 打开即聚焦搜索框 —— 用户要的"上面有一个搜索筛选",
|
||||
多一次点击才用得上就白加了。 */
|
||||
if (search) {
|
||||
try { search.focus({ preventScroll: true }); } catch (e) { search.focus(); }
|
||||
}
|
||||
}
|
||||
|
||||
/* v39:按关键词过滤列表。
|
||||
⚠️ 过滤的是**列表项**(`.cs-item`),"不记录影院"那一项**永远保留** ——
|
||||
它是"清空选择"的出口,被搜没了用户就取消不掉了。
|
||||
⚠️ 用 `hidden` 属性而不是内联 display —— CSS 里 `.cs-item` 写了 display:flex,
|
||||
内联 display 会被它覆盖(这个坑项目里踩过:见第 11 批"过滤一直是坏的")。 */
|
||||
function filter(q) {
|
||||
if (!list) { return; }
|
||||
var kw = (q || '').trim().toLowerCase();
|
||||
var items = list.querySelectorAll('.cs-item');
|
||||
var shown = 0;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var it = items[i];
|
||||
var keep = it.getAttribute('data-id') === '0'; // "不记录影院"恒显
|
||||
var hay = (it.getAttribute('data-text') || it.textContent || '').toLowerCase();
|
||||
var hit = !kw || keep || hay.indexOf(kw) !== -1;
|
||||
it.hidden = !hit;
|
||||
if (hit && !keep) { shown++; }
|
||||
}
|
||||
if (empty) { empty.hidden = shown > 0 || !kw; }
|
||||
}
|
||||
|
||||
function apply(id, color, label) {
|
||||
input.value = String(id);
|
||||
@@ -648,6 +716,33 @@ $mDelForms = []; // 收集起来,在表单**外面**统一渲染
|
||||
close();
|
||||
});
|
||||
|
||||
/* v39:搜索框的输入 / 键盘。
|
||||
⚠️ 三个事件都要 stopPropagation —— 否则会冒到 document 上那个"点外面关",
|
||||
一点搜索框下拉就没了(这是"加搜索"最容易踩的一脚)。 */
|
||||
if (search) {
|
||||
search.addEventListener('input', function () { filter(search.value); });
|
||||
search.addEventListener('mousedown', function (e) { e.stopPropagation(); });
|
||||
search.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||
search.addEventListener('keydown', function (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape') { close(); return; }
|
||||
if (e.key !== 'Enter') { return; }
|
||||
// 回车 = 选中**第一个可见**的项(除"不记录影院"外优先真实影院)
|
||||
e.preventDefault();
|
||||
var vis = list ? list.querySelectorAll('.cs-item:not([hidden])') : [];
|
||||
var pick = null;
|
||||
for (var i = 0; i < vis.length; i++) {
|
||||
if (vis[i].getAttribute('data-id') !== '0') { pick = vis[i]; break; }
|
||||
}
|
||||
if (!pick && vis.length) { pick = vis[0]; }
|
||||
if (pick) {
|
||||
var lb = pick.querySelector('span:last-child');
|
||||
apply(pick.getAttribute('data-id'), pick.getAttribute('data-color'), lb ? lb.textContent : '');
|
||||
close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 点外面关(冒泡阶段,只关下拉本身)
|
||||
document.addEventListener('click', function () { close(); });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user