节点表单:时间框整块可点、起点终点一键导航、可直接传多张附件
三件事都是「界面上明明该有、但用起来不通」的地方:
1) 时间输入框只能点右侧那个小日历图标才弹选择器
原生行为如此(而且各浏览器还不一致),用户会以为输入框是只读的。
在全局脚本里给 datetime-local / date 挂 click → showPicker(),
等于把整块输入区都当成那个图标;选择器已在显示时再调是空操作,
不支持的浏览器静默跳过。顺带给可点光标。
写在 layout.php 里,所以签到时间等其它时间框一并生效。
2) 起点/终点的精确位置此前只存在页面上,没有落库
高德选点回调里拿到的 lng/lat 现在随节点一起存库,节点详情页和
表单页都生成高德导航链接,点一下就能直接导航过去(手机端会尝试
唤起高德 App)。链接只给 to 不给 from,让移动端自动用当前位置当起点。
⚠️ 高德 URI API 的坐标顺序是「经度,纬度」,写反了不报错、只会安静地
导到地球另一边,所以单独写了单元测试守住顺序。
手工改地名时会清掉旧坐标,退化成关键字搜索,避免「名字改了位置没改」。
3) 节点处不能上传图片附件
节点表单改成 multipart,加拖放区 + 多选 + 本地预览 + 逐个移除,
编辑态可勾选删除已有附件(逐条校验归属,防止改 id 删到别人家的文件)。
上传逻辑从 AttachmentController 抽到 src/Uploads.php,两处共用一份:
MIME 白名单、大小上限、孤儿回滚各抄一份迟早走偏。
请求体超过 post_max_size 时 PHP 会静默丢掉 $_POST 与 $_FILES,紧随的
CSRF 校验必然失败、报「请求已过期」——这是最难排查的一类问题。
现在按 CONTENT_LENGTH 提前识别并给出人话提示,且该守卫必须排在
csrf_check() 之前(只读长度、不改状态,所以顺序上是安全的)。
已验证:本地 php -l 全量、模板渲染 255 项、amap_nav_url 单测 18 项、
真实浏览器交互 47 项(时间框三处点击都弹、多选/移除后 input.files 真被
改写、超限被前端拦下、导航链接随选点刷新、勾选删除变红、三视口无溢出)、
打包 57 文件、远端 e2e 267 项,全部通过;部署后双 PHP 渲染逐字节一致。
This commit is contained in:
@@ -438,6 +438,12 @@ input[type="file"] { font-size: 13px; padding: 6px 0; border: 0; background: non
|
||||
textarea { resize: vertical; min-height: 84px; }
|
||||
select { cursor: pointer; }
|
||||
|
||||
/* 时间框:点框内任何一处都能弹选择器(由 layout.php 里的 showPicker 负责,
|
||||
原生只有点右侧那个小图标才弹)。光标给个「整块可点」的暗示,别让人以为只有图标能点。 */
|
||||
input[type="datetime-local"], input[type="date"] { cursor: pointer; }
|
||||
input[type="datetime-local"]::-webkit-calendar-picker-indicator,
|
||||
input[type="date"]::-webkit-calendar-picker-indicator { cursor: pointer; padding: 2px; }
|
||||
|
||||
.form-actions {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border);
|
||||
@@ -503,6 +509,22 @@ select { cursor: pointer; }
|
||||
.amap-dropdown-name { font-size: 13.5px; }
|
||||
.amap-dropdown-sub { font-size: 12px; color: var(--text-mute); }
|
||||
|
||||
/* 起点 / 终点的「导航到这里」。
|
||||
有没有坐标决定它是什么链接,所以由服务端与 JS 共同决定显隐;
|
||||
必须排在 .form-row .muted 之后(同权重,靠后者生效)。 */
|
||||
.loc-nav { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 8px; }
|
||||
.loc-nav-link {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
font-size: 13px; font-weight: 560; line-height: 1.4;
|
||||
padding: 4px 11px; border-radius: 999px;
|
||||
background: var(--accent-soft); color: var(--accent);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.loc-nav-link:hover { border-color: var(--accent); text-decoration: none; }
|
||||
.loc-nav .muted { margin-top: 0; font-size: 12px; }
|
||||
/* 详情页里跟在地址后面的那个链接(不是整块,只是行内追加一个胶囊) */
|
||||
.loc-nav-inline { margin-left: 8px; vertical-align: middle; font-size: 12.5px; padding: 3px 9px; }
|
||||
|
||||
/* emoji 选择 */
|
||||
.emoji-picker { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||
.emoji-btn {
|
||||
@@ -692,6 +714,39 @@ select { cursor: pointer; }
|
||||
gap: 12px;
|
||||
}
|
||||
.photo-grid-sm { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
|
||||
|
||||
/* 已有附件的「勾选即删除」格子:整格可点,勾上后变灰 + 红勾。
|
||||
原生 checkbox 藏起来但保留可聚焦,键盘也能用(勾选框上会有 focus 环)。 */
|
||||
.photo-pick {
|
||||
position: relative; display: block; cursor: pointer; margin: 0;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
overflow: hidden; background: var(--surface);
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.photo-pick:hover { border-color: var(--accent); }
|
||||
.photo-pick img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; background: var(--bg-soft); }
|
||||
.photo-pick-ext {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
aspect-ratio: 4 / 3; font-size: 12px; font-weight: 600;
|
||||
color: var(--text-mute); background: var(--bg-soft);
|
||||
}
|
||||
.photo-pick input { position: absolute; width: 0; height: 0; opacity: 0; }
|
||||
.photo-pick-mark {
|
||||
position: absolute; top: 6px; right: 6px;
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 13px; line-height: 1; color: transparent;
|
||||
background: rgba(20, 24, 34, .45);
|
||||
border: 1px solid rgba(255, 255, 255, .85);
|
||||
transition: background .15s, color .15s, border-color .15s;
|
||||
}
|
||||
.photo-pick input:checked ~ img,
|
||||
.photo-pick input:checked ~ .photo-pick-ext { opacity: .35; filter: grayscale(1); }
|
||||
.photo-pick input:checked ~ .photo-pick-mark { background: var(--danger); border-color: var(--danger); color: #fff; }
|
||||
.photo-pick input:focus-visible ~ .photo-pick-mark { box-shadow: 0 0 0 3px var(--accent-ring); }
|
||||
|
||||
/* 表单里刚选中的待上传文件(复用 .photo-card 的样式,只是要一个能挂 × 按钮的位置) */
|
||||
.photo-card > .photo-del { position: absolute; top: 6px; right: 6px; }
|
||||
.photo-card {
|
||||
position: relative; display: block;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
@@ -939,6 +994,7 @@ select { cursor: pointer; }
|
||||
|
||||
/* 表单里的小字说明 */
|
||||
.form-hint { font-size: 12.5px; color: var(--text-mute); margin-top: 5px; line-height: 1.65; }
|
||||
.form-hint-error { color: var(--danger); font-weight: 560; }
|
||||
.form-hint code, .settings-hint code, .alert code {
|
||||
background: var(--bg-soft); padding: 1px 5px; border-radius: 4px;
|
||||
font-size: 12px; word-break: break-all;
|
||||
|
||||
Reference in New Issue
Block a user