diff --git a/client/src/components/editor/Preview.vue b/client/src/components/editor/Preview.vue index 4316f86..4df9f91 100644 --- a/client/src/components/editor/Preview.vue +++ b/client/src/components/editor/Preview.vue @@ -43,6 +43,7 @@
`, ) doc.close() - doc.querySelector("iframe")!.srcdoc = getContent() + const tabFrame = doc.querySelector("iframe")! + // 全屏页里的写入也存到同一个地方 + listenPreviewStorage(newTab, () => tabFrame, storageScope) + tabFrame.srcdoc = getContent() } } @@ -192,6 +210,7 @@ watchDebounced( }, ) onMounted(preview) +onUnmounted(listenPreviewStorage(window, () => frame.value, storageScope)) ` : "" const script = round.js ? ` 提前结束脚本 + const config = JSON.stringify(storage ?? null).replace(/${SHIM}(${config}, ${SCOPE_MAX_CHARS}, "${MESSAGE_TYPE}")<\/script>` +} + +interface Saved { + // 最后写入时间,总量超了先删最久没用的 + t: number + d: Record +} + +function read(key: string): Saved | null { + try { + const saved = JSON.parse(localStorage.getItem(key) ?? "null") + return saved && typeof saved.d === "object" ? saved : null + } catch { + return null + } +} + +export function loadPreviewStorage(scope: string): PreviewStorage { + return { scope, data: read(KEY_PREFIX + scope)?.d ?? {} } +} + +function previewKeys() { + const keys: string[] = [] + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i) + if (k?.startsWith(KEY_PREFIX)) keys.push(k) + } + return keys +} + +function evictOthers(keep: string, all: boolean) { + const others = previewKeys() + .filter((k) => k !== keep) + .map((k) => ({ k, t: read(k)?.t ?? 0, n: localStorage.getItem(k)!.length })) + .sort((a, b) => a.t - b.t) + let total = others.reduce((n, o) => n + o.n, 0) + (localStorage.getItem(keep)?.length ?? 0) + for (const o of others) { + if (!all && total <= TOTAL_MAX_CHARS) break + localStorage.removeItem(o.k) + total -= o.n + } +} + +function save(scope: string, data: Record) { + const key = KEY_PREFIX + scope + const value = JSON.stringify({ t: Date.now(), d: data } satisfies Saved) + try { + localStorage.setItem(key, value) + evictOthers(key, false) + } catch { + // 配额满了:清掉别的预览数据再试一次,还不行就不存了 + try { + evictOthers(key, true) + localStorage.setItem(key, value) + } catch {} + } +} + +function apply(scope: string, msg: any) { + const data = { ...loadPreviewStorage(scope).data } + if (msg.op === "set") { + if (typeof msg.key !== "string" || typeof msg.value !== "string") return + data[msg.key] = msg.value + } else if (msg.op === "remove") { + if (typeof msg.key !== "string") return + delete data[msg.key] + } else if (msg.op === "clear") { + for (const k of Object.keys(data)) delete data[k] + } else { + return + } + const size = Object.entries(data).reduce((n, [k, v]) => n + k.length + v.length, 0) + if (size > SCOPE_MAX_CHARS) return + save(scope, data) +} + +// 在 win 上接收 iframe 发来的写入。只认这个 iframe、这个 scope 的消息;返回取消监听的函数 +export function listenPreviewStorage( + win: Window, + frame: () => HTMLIFrameElement | null | undefined, + scope: () => string, +) { + const handler = (e: MessageEvent) => { + const s = scope() + if (!s || !e.data || e.data.type !== MESSAGE_TYPE || e.data.scope !== s) return + if (!frame() || e.source !== frame()!.contentWindow) return + apply(s, e.data) + } + win.addEventListener("message", handler) + return () => win.removeEventListener("message", handler) +}