笔记附件:修好拖放、传完回编辑态、选中即上传 #21
用户原话:「网页端,笔记应该可以同时上传多个附件。」 诊断后确认「多选」本来就是好的(multiple + name=file[] + 后端数组分支, 一次选 3 个混合类型真的存了 3 行)。真正坏的是另外两件事: 1) 拖放是假承诺。界面写「把文件拖到这里」,但没有任何 drop 监听器: 拖完 input.files 仍是 0、POST 0 个文件。更糟的是 dragover 没有 preventDefault ⇒ 浏览器按默认行为直接打开那个文件,正在写的笔记 连未保存正文一起丢。 修法:给框加 data-sd-for="noteAttInput",复用 app.js 里既有的 bindInput() → bindZone()。一行属性,不另写一套拖放。 踩过的坑:data-sd-for 写的是**裸 id**,第一版带了个 `#`,选择器 变成 [data-sd-for="#noteAttInput"],一个都匹配不上,拖放照样没人接。 2) 传完跳回阅读态(redirect 不带 edit=1),附件区在 $editMode 分支里 根本不渲染 ⇒ 用户看到的是"没传上"。attach_add / attach_del 两处 都补 edit => 1。 另外补了即时反馈:change 一来就把提示换成「已选择 N 个文件:…,正在 上传…」并自动提交。用 requestSubmit(btn) 而不是 form.submit() —— 后者绕过 submit 事件、也不带按钮的 formaction/formenctype,提交会跑去 notes/save 且非 multipart,文件直接丢。 判据: - _b40_note_multi_attach.py 16/0(点选多选 / 拖放 / 回编辑态 / 单选回归 / 提示文案) - _b40_falsify21.py 三处注入各自红在预期编号(data-sd-for 带# → ③④a-d; requestSubmit→submit → ①-b;去掉 edit=1 → ③-a/b) - _b40_regress21.py 20/0(删除生效且回编辑态 / 越权删除被拒 / 公共绑定器 未受影响 / MAX_PER_HOST=60 端到端仍生效) - _b40_regress21_falsify.py 关掉上限与归属校验各自变红
This commit is contained in:
@@ -8111,7 +8111,12 @@ $want = array_keys($want);
|
||||
$warns = attach_save_files($familyId, Note::ATTACH, $id, 'file', $me);
|
||||
flash($warns ? 'err' : 'ok',
|
||||
$warns ? '有附件没有保存:' . implode(';', $warns) : '附件已上传。');
|
||||
redirect('notes/form', ['id' => $id]);
|
||||
/* 🔴 工单 #21:回**编辑态**(`edit=1`),不是阅读态。
|
||||
`notes/form` 的 `$editMode = $id <= 0 || !empty($_GET['edit'])` ⇒
|
||||
原来这里不带 edit 参数,id>0 就落到阅读态,附件区(在 $editMode 分支里)
|
||||
根本不渲染。用户上传完看到的是"附件没传上"——体感就是功能坏了,
|
||||
实际只是跳错了态。 */
|
||||
redirect('notes/form', ['id' => $id, 'edit' => 1]);
|
||||
break;
|
||||
|
||||
case 'notes/attach_del':
|
||||
@@ -8134,7 +8139,10 @@ $want = array_keys($want);
|
||||
}
|
||||
\App\Attachment::remove($aid);
|
||||
flash('ok', '附件已删除。');
|
||||
redirect('notes/form', ['id' => $nid]);
|
||||
/* 🔴 工单 #21:删完同样回**编辑态** —— 与 attach_add 保持一致的道理:
|
||||
回阅读态的话,用户点「✕」删掉一个,跳转后看不见附件列表,
|
||||
不知道删成功了没有,也找不到继续删/继续传的入口。 */
|
||||
redirect('notes/form', ['id' => $nid, 'edit' => 1]);
|
||||
break;
|
||||
|
||||
/* 段落 Markdown 的**实时预览**(v28 用户第 12 条:"允许所见即所得编辑")。
|
||||
|
||||
+105
-3
@@ -408,10 +408,40 @@ $imgParts = static function (string $c): array {
|
||||
<div id="noteAttForm">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
|
||||
<div class="note-att-drop" id="noteAttDrop">
|
||||
<?php /* 🔴 工单 #21(用户 2026-10-28):「网页端,笔记应该可以同时上传多个附件。」
|
||||
|
||||
实测查清的三件事(别凭印象改):
|
||||
① **多选本来就是好的**:`multiple` + `name="file[]"` + 后端
|
||||
`attach_save_files()` 那段数组分支,一次选 3 个(txt/md/png 混合)
|
||||
真的存了 3 行。这条不用动。
|
||||
② **拖放是假承诺**:上面那句「把文件拖到这里」从来没人接。
|
||||
`app.js` 的 `scan()` 会给每个 file input 挂 `bindZone(input, input)`,
|
||||
但落点绑在 **input 自己**身上 —— 而用户拖的是**外层这个框**,
|
||||
事件发生在框上、不会反向冒泡到 input ⇒ input.files 永远是 0。
|
||||
更糟的是没有 `dragover` 的 preventDefault,浏览器按默认行为
|
||||
**直接打开那个文件**,正在写的笔记连未保存的正文一起丢。
|
||||
③ 传完跳回**阅读态**(见 index.php 的 attach_add),用户看不见刚传的附件,
|
||||
体感就是"没传上"。
|
||||
|
||||
修法:给框加 `data-sd-for="noteAttInput"` —— 这是项目里既有的约定
|
||||
(`bindInput()` 见到它就 `bindZone(box, input)` + `bindBox`),
|
||||
一行属性就能复用成熟实现,不另写一套拖放。
|
||||
|
||||
🔴🔴 `data-sd-for` 里写的是**裸 id,不带 `#`**!
|
||||
取值处是 `querySelectorAll('[data-sd-for="' + input.id + '"]')`,
|
||||
而 `input.id` 是 `noteAttInput`。我第一版写成 `#noteAttInput`,
|
||||
选择器就变成 `[data-sd-for="#noteAttInput"]` —— 一个都匹配不上,
|
||||
拖放照样没人接(判据 ④-a/④-b 直接红)。**照抄 _stage_drop.php:135
|
||||
的写法**(那里是 `data-sd-for="<?= e($sdId) ?>"`,$sdId 是裸 id)。
|
||||
|
||||
⚠️ 不能改成 `data-drop-stage`(暂存式):那套要配合 `upload_stage.php`
|
||||
与 `stage_token[]` 认领,笔记这边是"直接 POST 到 attach_add",两套不能混。
|
||||
⚠️ `bindFileEcho` 只认 `.upbox-msg` / `.upbox-ico`,本框用的是 `.note-att-tip`,
|
||||
所以它插不上手 —— 选中后的即时反馈由下面那段小脚本自己负责。 */ ?>
|
||||
<div class="note-att-drop" id="noteAttDrop" data-sd-for="noteAttInput">
|
||||
<input type="file" name="file[]" id="noteAttInput" multiple
|
||||
accept="image/*,.pdf,.txt,.md,.zip,.doc,.docx,.xls,.xlsx,.ppt,.pptx">
|
||||
<span class="note-att-tip">把文件拖到这里,或<label for="noteAttInput">点这里选文件</label></span>
|
||||
<span class="note-att-tip" data-note-att-tip>把文件拖到这里,或<label for="noteAttInput">点这里选文件</label>(可一次选多个)</span>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<?php /* ⚠️ formaction/formenctype 必须写在**按钮**上:表单主体是"保存",
|
||||
@@ -448,8 +478,80 @@ $imgParts = static function (string $c): array {
|
||||
|
||||
<?php /* 初始块数据:JSON 塞进 <script type="application/json"> 而不是 data-* ——
|
||||
属性里的引号/换行会被 HTML 实体化,读出来还得反转义一次;JSON 块原样读。 */ ?>
|
||||
<script type="application/json" id="noteSeed"><?= json_encode(
|
||||
<script type="application/json" id="noteSeed"><?php echo json_encode(
|
||||
$blocks,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
|
||||
) ?></script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isEditing): ?>
|
||||
<script>
|
||||
/* ============================================================================
|
||||
笔记附件:选中 / 拖入多个文件后**立刻**给出反馈,并自动提交上传(工单 #21)
|
||||
----------------------------------------------------------------------------
|
||||
用户原话:「网页端,笔记应该可以同时上传多个附件。」
|
||||
|
||||
拖放本身不在这里做 —— `app.js` 的 `bindInput()` 靠框上的
|
||||
`data-sd-for="noteAttInput"` 已经挂好了 `bindZone`,会把 drop 的文件
|
||||
写进 `input.files` 并派发一次 `change`。**本段只做两件事**:
|
||||
|
||||
① `change` 一来就把提示文案换成「已选择 N 个文件」——
|
||||
否则用户点完/拖完看不到任何变化,会再点一遍(重复选同一批还会重叠)。
|
||||
② 顺手自动提交到 `notes/attach_add`:以前要再点一次「上传所选文件」,
|
||||
多选时这个多余的步骤特别容易漏点,漏点就等于"没传上"。
|
||||
|
||||
⚠️ 为什么用 `requestSubmit(btn)` 而不是 `form.submit()`:
|
||||
`form.submit()` 会**绕过** submit 事件(`note_editor.js` 在 submit 里
|
||||
serialize 正文块),而且不会带上按钮的 `formaction`/`formenctype`——
|
||||
那样这次提交会跑去 `notes/save` 且不是 multipart,文件直接丢。
|
||||
`requestSubmit(btn)` 会带着那颗按钮的 `formaction` 走,语义正确。
|
||||
⚠️ 老浏览器没有 `requestSubmit` 就退回 `form.submit()` 之前先手动把
|
||||
action/enctype 改对——但更简单的做法是让用户点那颗按钮(下面兜底提示)。
|
||||
⚠️ 提交前把 `data-sd-echo`/拖放高亮之类收尾交给 app.js,这里不碰。
|
||||
============================================================================ */
|
||||
(function () {
|
||||
var input = document.getElementById('noteAttInput');
|
||||
var form = document.getElementById('noteForm');
|
||||
var tip = document.querySelector('[data-note-att-tip]');
|
||||
if (!input || !form || !tip) { return; }
|
||||
|
||||
/* 原始文案要记下来:用户取消选择时要还原(不能变成空字符串) */
|
||||
var original = tip.innerHTML;
|
||||
var btn = document.querySelector('#noteAttForm button[formaction]');
|
||||
|
||||
function paint() {
|
||||
var n = input.files ? input.files.length : 0;
|
||||
if (n === 0) { tip.innerHTML = original; return; }
|
||||
var names = Array.prototype.slice.call(input.files, 0, 3)
|
||||
.map(function (f) { return f.name; }).join('、');
|
||||
tip.textContent = '已选择 ' + n + ' 个文件:' + names + (n > 3 ? ' 等' : '')
|
||||
+ ',正在上传…';
|
||||
}
|
||||
|
||||
input.addEventListener('change', function () {
|
||||
paint();
|
||||
/* 没选到东西(用户点取消)就别提交了 */
|
||||
if (!input.files || input.files.length === 0) { return; }
|
||||
if (!btn) { return; }
|
||||
/* requestSubmit 会**带上这颗按钮的 formaction** 触发完整提交流程;
|
||||
form.submit() 不会 —— 见上面那条 ⚠️。 */
|
||||
if (typeof form.requestSubmit === 'function') {
|
||||
form.requestSubmit(btn);
|
||||
} else {
|
||||
/* 兜底:把 action/enctype 临时改成附件端点再提交。
|
||||
⚠️ 改完**不还原**——页面马上就要跳走了,还原反而可能在
|
||||
"跳转前的最后一刻"被别的代码读到错误状态。 */
|
||||
form.setAttribute('action', btn.getAttribute('formaction'));
|
||||
form.setAttribute('enctype', 'multipart/form-data');
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
|
||||
/* 上传回来(页面重载)时把提示复位 —— 服务端渲染的是原始文案,
|
||||
不需要额外做;这里只在"从往返缓存恢复"的极端情况下兜一下。 */
|
||||
window.addEventListener('pageshow', function (ev) {
|
||||
if (ev.persisted) { paint(); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
Reference in New Issue
Block a user