Files
zTools2/static/common.js
zikai 30a263ed50 fix: 代码审查修复 + 精简重写 README
后端修复:
- 白板删除踢人失效:delete_whiteboard 改 async def,删除后直接 await
  hub.close_board()。原实现用 asyncio.get_running_loop() 在同步 REST handler
  (threadpool)里调用必抛 RuntimeError 被 except 吞掉,close_board 从不执行。
  同时移除 service 的 hub 依赖(close_board 改由 controller 调用,service 只管 DB)。
- delete_file 去重复查询:原先 get_out_with_disk_path + get_by_id 查两次,
  合并为一次;磁盘 unlink 失败加 logger.warning(原静默吞掉致磁盘泄漏无记录)。
- get_hub 单例加 threading.Lock 双重检查(防 REST threadpool 与 WS 事件循环
  并发首访各建一个 hub)。
- file_controller 公开 /api/files list 加 Query(ge=1, le=10000) 约束(原无上限可 DoS)。

前端修复:
- applyRemoteUpdate 有未发送编辑时重发:合并远端更新后若本地有 pending 编辑
  (editor.value !== lastSentText)重新 scheduleSend,避免被 lastSentText 短路丢弃。
- init 不覆盖未发送编辑:断线重连后若本地有未发送内容,作为新版本发上去而非被 init 覆盖。
- applyRemoteUpdate 仅在编辑器已有焦点时恢复焦点,避免抢按钮焦点。
- api() 401 时 location.reload() 触发浏览器 Basic Auth 弹窗(原只 toast 卡死)。

README:
- 精简重写,补全 Ubuntu 从 0 安装、Apache 反代(含 WS)、配置项表格、防火墙说明。
2026-07-22 01:03:48 +00:00

102 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* zikai 共享前端工具toast、复制、字节/时间格式化、fetch 封装。
Basic Auth 同源时浏览器自动带缓存的凭据无需额外处理fetch 默认 same-origin。 */
(function (global) {
"use strict";
// 布尔属性:用属性赋值而非 setAttributesetAttribute("checked","false") 仍会勾选)
const BOOL_PROPS = new Set([
"checked", "disabled", "readonly", "selected", "hidden", "multiple", "open",
"autofocus", "required", "async", "defer", "controls", "autoplay", "loop", "muted",
]);
function el(tag, attrs, ...children) {
const node = document.createElement(tag);
if (attrs) {
for (const [k, v] of Object.entries(attrs)) {
if (k === "class") node.className = v;
else if (k === "dataset") Object.assign(node.dataset, v);
else if (k.startsWith("on") && typeof v === "function")
node.addEventListener(k.slice(2).toLowerCase(), v);
else if (BOOL_PROPS.has(k)) node[k] = !!v;
else if (v !== null && v !== undefined && v !== false) node.setAttribute(k, v);
}
}
for (const c of children) {
if (c == null || c === false) continue;
node.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
}
return node;
}
let toastTimer = null;
function toast(msg, kind) {
let node = document.querySelector(".toast");
if (!node) {
node = el("div", { class: "toast" });
document.body.appendChild(node);
}
node.textContent = msg;
node.className = "toast show" + (kind ? " " + kind : "");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => (node.className = "toast"), 2200);
}
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// 降级:临时 textarea
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
let ok = false;
try { ok = document.execCommand("copy"); } catch {}
document.body.removeChild(ta);
return ok;
}
}
function fmtBytes(n) {
let x = Number(n);
if (n == null || !isFinite(x)) return "-";
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
let i = 0;
while (Math.abs(x) >= 1000 && i < units.length - 1) {
x /= 1000;
i++;
}
return i === 0 ? `${Math.round(x)} ${units[i]}` : `${x.toFixed(1)} ${units[i]}`;
}
function fmtTime(s) {
if (!s) return "-";
const d = new Date(s);
if (isNaN(d.getTime())) return s;
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
async function api(path, opts) {
const res = await fetch(path, opts);
if (res.status === 401) {
// fetch 不会触发浏览器的 Basic Auth 弹窗(只有导航/form 会)。
// 重载当前页:浏览器对页面导航的 401 会弹凭据框,凭据缓存后重试即可带上。
toast("需要登录");
if (location.href.indexOf("/api/") !== -1) {
// 纯 API 调用页(无页面壳),跳转到来源页触发鉴权
location.reload();
} else {
location.reload();
}
throw new Error("UNAUTHORIZED");
}
return res;
}
global.ZK = { el, toast, copyText, fmtBytes, fmtTime, api };
})(window);