feat(console): 背景照片改存 IndexedDB 原始图片,显示零压缩全分辨率(旧压缩图仅回退)
This commit is contained in:
+127
-32
@@ -17298,7 +17298,8 @@ function ConsolePane() {
|
|||||||
const [cols, setColsState] = (0, import_react.useState)(() => splitEnv?.console?.getCols?.() ?? 3);
|
const [cols, setColsState] = (0, import_react.useState)(() => splitEnv?.console?.getCols?.() ?? 3);
|
||||||
const [shape, setShapeState] = (0, import_react.useState)(() => splitEnv?.console?.getShape?.() ?? "square");
|
const [shape, setShapeState] = (0, import_react.useState)(() => splitEnv?.console?.getShape?.() ?? "square");
|
||||||
const [bg, setBgState] = (0, import_react.useState)(() => splitEnv?.console?.getBg?.() ?? "glow");
|
const [bg, setBgState] = (0, import_react.useState)(() => splitEnv?.console?.getBg?.() ?? "glow");
|
||||||
const [bgPhoto, setBgPhoto] = (0, import_react.useState)(() => splitEnv?.console?.getPhoto?.() ?? "");
|
const [bgPhoto, setBgPhoto] = (0, import_react.useState)("");
|
||||||
|
const photoUrlRef = (0, import_react.useRef)("");
|
||||||
const [openMenu, setOpenMenu] = (0, import_react.useState)(null);
|
const [openMenu, setOpenMenu] = (0, import_react.useState)(null);
|
||||||
const [bgEdit, setBgEdit] = (0, import_react.useState)(null);
|
const [bgEdit, setBgEdit] = (0, import_react.useState)(null);
|
||||||
const [plainHsl, setPlainHslState] = (0, import_react.useState)(() => splitEnv?.console?.getPlainHsl?.() ?? { h: 220, s: 31, l: 6 });
|
const [plainHsl, setPlainHslState] = (0, import_react.useState)(() => splitEnv?.console?.getPlainHsl?.() ?? { h: 220, s: 31, l: 6 });
|
||||||
@@ -17325,6 +17326,23 @@ function ConsolePane() {
|
|||||||
setShapeState(s);
|
setShapeState(s);
|
||||||
splitEnv?.console?.setShape?.(s);
|
splitEnv?.console?.setShape?.(s);
|
||||||
};
|
};
|
||||||
|
const onFilePhoto = async (file) => {
|
||||||
|
try {
|
||||||
|
await splitEnv?.console?.setPhoto?.(file);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
if (photoUrlRef.current) {
|
||||||
|
try {
|
||||||
|
URL.revokeObjectURL(photoUrlRef.current);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
photoUrlRef.current = url;
|
||||||
|
setBgPhoto(url);
|
||||||
|
setBgState("photo");
|
||||||
|
splitEnv?.console?.setBg?.("photo");
|
||||||
|
};
|
||||||
const onBg = (m) => {
|
const onBg = (m) => {
|
||||||
if (m === "photo") {
|
if (m === "photo") {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
@@ -17333,33 +17351,7 @@ function ConsolePane() {
|
|||||||
input.onchange = () => {
|
input.onchange = () => {
|
||||||
const file = input.files?.[0];
|
const file = input.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const url = URL.createObjectURL(file);
|
onFilePhoto(file);
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
const maxDim = 2400;
|
|
||||||
const scale = Math.min(1, maxDim / Math.max(img.width, img.height));
|
|
||||||
const w = Math.max(1, Math.round(img.width * scale));
|
|
||||||
const h = Math.max(1, Math.round(img.height * scale));
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = w;
|
|
||||||
canvas.height = h;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (!ctx) {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = "high";
|
|
||||||
ctx.drawImage(img, 0, 0, w, h);
|
|
||||||
const webp = canvas.toDataURL("image/webp", 0.9);
|
|
||||||
const dataUrl = webp.indexOf("data:image/webp") === 0 ? webp : canvas.toDataURL("image/jpeg", 0.9);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
setBgPhoto(dataUrl);
|
|
||||||
setBgState("photo");
|
|
||||||
splitEnv?.console?.setBg?.("photo");
|
|
||||||
splitEnv?.console?.setPhoto?.(dataUrl);
|
|
||||||
};
|
|
||||||
img.src = url;
|
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
return;
|
return;
|
||||||
@@ -17463,6 +17455,33 @@ function ConsolePane() {
|
|||||||
(0, import_react.useEffect)(() => {
|
(0, import_react.useEffect)(() => {
|
||||||
splitEnv?.console?.refreshPreviews?.();
|
splitEnv?.console?.refreshPreviews?.();
|
||||||
}, []);
|
}, []);
|
||||||
|
(0, import_react.useEffect)(() => {
|
||||||
|
let alive = true;
|
||||||
|
splitEnv?.console?.getPhoto?.().then((url) => {
|
||||||
|
if (alive && url && url !== photoUrlRef.current) {
|
||||||
|
if (photoUrlRef.current) {
|
||||||
|
try {
|
||||||
|
URL.revokeObjectURL(photoUrlRef.current);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
photoUrlRef.current = url;
|
||||||
|
setBgPhoto(url);
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
(0, import_react.useEffect)(() => () => {
|
||||||
|
if (photoUrlRef.current) {
|
||||||
|
try {
|
||||||
|
URL.revokeObjectURL(photoUrlRef.current);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
(0, import_react.useEffect)(() => {
|
(0, import_react.useEffect)(() => {
|
||||||
const iv = window.setInterval(() => setNow(Date.now()), 1e3);
|
const iv = window.setInterval(() => setNow(Date.now()), 1e3);
|
||||||
return () => window.clearInterval(iv);
|
return () => window.clearInterval(iv);
|
||||||
@@ -18725,6 +18744,61 @@ try {
|
|||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/client/photoStore.ts
|
||||||
|
var DB_NAME = "dsh-worktable";
|
||||||
|
var STORE = "consoleBgPhoto";
|
||||||
|
var KEY = "original";
|
||||||
|
function openDb() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = indexedDB.open(DB_NAME, 1);
|
||||||
|
req.onupgradeneeded = () => {
|
||||||
|
const db = req.result;
|
||||||
|
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
|
||||||
|
};
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var photoStore = {
|
||||||
|
/** 保存原始图片(原样存,不重新编码) */
|
||||||
|
async save(blob) {
|
||||||
|
const db = await openDb();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(STORE, "readwrite");
|
||||||
|
tx.objectStore(STORE).put(blob, KEY);
|
||||||
|
tx.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
tx.onerror = () => {
|
||||||
|
db.close();
|
||||||
|
reject(tx.error);
|
||||||
|
};
|
||||||
|
tx.onabort = () => {
|
||||||
|
db.close();
|
||||||
|
reject(tx.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 读取原始图片;无记录返回 null */
|
||||||
|
async load() {
|
||||||
|
const db = await openDb();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(STORE, "readonly");
|
||||||
|
const req = tx.objectStore(STORE).get(KEY);
|
||||||
|
req.onsuccess = () => {
|
||||||
|
const v = req.result;
|
||||||
|
db.close();
|
||||||
|
resolve(v instanceof Blob ? v : null);
|
||||||
|
};
|
||||||
|
req.onerror = () => {
|
||||||
|
db.close();
|
||||||
|
reject(req.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// src/client/index.tsx
|
// src/client/index.tsx
|
||||||
var import_jsx_runtime2 = require("react/jsx-runtime");
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
||||||
var LOCAL_VERSION = false ? "dev" : "0.2.3";
|
var LOCAL_VERSION = false ? "dev" : "0.2.3";
|
||||||
@@ -19942,17 +20016,38 @@ function WorktableSection(props) {
|
|||||||
setShape: (s) => persistView({ consoleShape: s }),
|
setShape: (s) => persistView({ consoleShape: s }),
|
||||||
getBg: () => viewRef.current.consoleBg ?? "glow",
|
getBg: () => viewRef.current.consoleBg ?? "glow",
|
||||||
setBg: (m) => persistView({ consoleBg: m }),
|
setBg: (m) => persistView({ consoleBg: m }),
|
||||||
getPhoto: () => {
|
getPhoto: () => photoStore.load().then((blob) => {
|
||||||
|
if (blob) return URL.createObjectURL(blob);
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem("dsh.worktable.consoleBgPhoto.v1") ?? "";
|
return localStorage.getItem("dsh.worktable.consoleBgPhoto.v1") ?? "";
|
||||||
} catch {
|
} catch {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
},
|
}).catch(() => {
|
||||||
setPhoto: (dataUrl) => {
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("dsh.worktable.consoleBgPhoto.v1", dataUrl);
|
return localStorage.getItem("dsh.worktable.consoleBgPhoto.v1") ?? "";
|
||||||
} catch {
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
setPhoto: async (blob) => {
|
||||||
|
try {
|
||||||
|
await photoStore.save(blob);
|
||||||
|
try {
|
||||||
|
localStorage.removeItem("dsh.worktable.consoleBgPhoto.v1");
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
const dataUrl = await new Promise((resolve, reject) => {
|
||||||
|
const fr = new FileReader();
|
||||||
|
fr.onload = () => resolve(String(fr.result));
|
||||||
|
fr.onerror = () => reject(fr.error);
|
||||||
|
fr.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
localStorage.setItem("dsh.worktable.consoleBgPhoto.v1", dataUrl);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getPlainHsl: () => viewRef.current.consoleBgPlainHsl ?? { h: 220, s: 31, l: 6 },
|
getPlainHsl: () => viewRef.current.consoleBgPlainHsl ?? { h: 220, s: 31, l: 6 },
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { css } from './styles'
|
|||||||
import { NS, zh, en, type WorktableKey } from './locales'
|
import { NS, zh, en, type WorktableKey } from './locales'
|
||||||
import { isAbs, joinPath, parentPathOf, basenameOf } from './pathutil'
|
import { isAbs, joinPath, parentPathOf, basenameOf } from './pathutil'
|
||||||
import { splitStore, SplitWorkspace, setSplitT, setSplitEnv, type LayoutSpec, type SplitPane, type ConsoleCardData } from './split'
|
import { splitStore, SplitWorkspace, setSplitT, setSplitEnv, type LayoutSpec, type SplitPane, type ConsoleCardData } from './split'
|
||||||
|
import { photoStore } from './photoStore'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* dsh-worktable 客户端(v2):侧边栏底部「工作台」区块。
|
* dsh-worktable 客户端(v2):侧边栏底部「工作台」区块。
|
||||||
@@ -1355,8 +1356,32 @@ function WorktableSection(props: any) {
|
|||||||
setShape: (s: 'square' | 'circle') => persistView({ consoleShape: s }),
|
setShape: (s: 'square' | 'circle') => persistView({ consoleShape: s }),
|
||||||
getBg: () => viewRef.current.consoleBg ?? 'glow',
|
getBg: () => viewRef.current.consoleBg ?? 'glow',
|
||||||
setBg: (m: 'plain' | 'glow' | 'photo') => persistView({ consoleBg: m }),
|
setBg: (m: 'plain' | 'glow' | 'photo') => persistView({ consoleBg: m }),
|
||||||
getPhoto: () => { try { return localStorage.getItem('dsh.worktable.consoleBgPhoto.v1') ?? '' } catch { return '' } },
|
getPhoto: () => photoStore.load()
|
||||||
setPhoto: (dataUrl: string) => { try { localStorage.setItem('dsh.worktable.consoleBgPhoto.v1', dataUrl) } catch {} },
|
.then((blob) => {
|
||||||
|
if (blob) return URL.createObjectURL(blob)
|
||||||
|
try { return localStorage.getItem('dsh.worktable.consoleBgPhoto.v1') ?? '' } catch { return '' }
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
try { return localStorage.getItem('dsh.worktable.consoleBgPhoto.v1') ?? '' } catch { return '' }
|
||||||
|
}),
|
||||||
|
setPhoto: async (blob: Blob) => {
|
||||||
|
try {
|
||||||
|
await photoStore.save(blob)
|
||||||
|
// 成功存原图后清掉旧的压缩图键,避免回退时读到旧图
|
||||||
|
try { localStorage.removeItem('dsh.worktable.consoleBgPhoto.v1') } catch {}
|
||||||
|
} catch {
|
||||||
|
// IndexedDB 不可用(隐私模式等)→ 兜底:原图 dataURL 存旧键,超限则静默
|
||||||
|
try {
|
||||||
|
const dataUrl = await new Promise<string>((resolve, reject) => {
|
||||||
|
const fr = new FileReader()
|
||||||
|
fr.onload = () => resolve(String(fr.result))
|
||||||
|
fr.onerror = () => reject(fr.error)
|
||||||
|
fr.readAsDataURL(blob)
|
||||||
|
})
|
||||||
|
localStorage.setItem('dsh.worktable.consoleBgPhoto.v1', dataUrl)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
},
|
||||||
getPlainHsl: () => viewRef.current.consoleBgPlainHsl ?? { h: 220, s: 31, l: 6 },
|
getPlainHsl: () => viewRef.current.consoleBgPlainHsl ?? { h: 220, s: 31, l: 6 },
|
||||||
setPlainHsl: (v: { h: number; s: number; l: number }) => persistView({ consoleBgPlainHsl: { ...v } }),
|
setPlainHsl: (v: { h: number; s: number; l: number }) => persistView({ consoleBgPlainHsl: { ...v } }),
|
||||||
getGlowHsl: () => viewRef.current.consoleBgGlowHsl ?? { h: 0, s: 100, l: 100 },
|
getGlowHsl: () => viewRef.current.consoleBgGlowHsl ?? { h: 0, s: 100, l: 100 },
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/** dsh-worktable 控制室背景原图存取:IndexedDB 存原始 Blob,全分辨率显示(零压缩)。 */
|
||||||
|
const DB_NAME = 'dsh-worktable'
|
||||||
|
const STORE = 'consoleBgPhoto'
|
||||||
|
const KEY = 'original'
|
||||||
|
|
||||||
|
function openDb(): Promise<IDBDatabase> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = indexedDB.open(DB_NAME, 1)
|
||||||
|
req.onupgradeneeded = () => {
|
||||||
|
const db = req.result
|
||||||
|
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE)
|
||||||
|
}
|
||||||
|
req.onsuccess = () => resolve(req.result)
|
||||||
|
req.onerror = () => reject(req.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const photoStore = {
|
||||||
|
/** 保存原始图片(原样存,不重新编码) */
|
||||||
|
async save(blob: Blob): Promise<void> {
|
||||||
|
const db = await openDb()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(STORE, 'readwrite')
|
||||||
|
tx.objectStore(STORE).put(blob, KEY)
|
||||||
|
tx.oncomplete = () => { db.close(); resolve() }
|
||||||
|
tx.onerror = () => { db.close(); reject(tx.error) }
|
||||||
|
tx.onabort = () => { db.close(); reject(tx.error) }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 读取原始图片;无记录返回 null */
|
||||||
|
async load(): Promise<Blob | null> {
|
||||||
|
const db = await openDb()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(STORE, 'readonly')
|
||||||
|
const req = tx.objectStore(STORE).get(KEY)
|
||||||
|
req.onsuccess = () => { const v = req.result; db.close(); resolve(v instanceof Blob ? v : null) }
|
||||||
|
req.onerror = () => { db.close(); reject(req.error) }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -247,8 +247,8 @@ type SplitEnv = {
|
|||||||
setShape: (s: 'square' | 'circle') => void
|
setShape: (s: 'square' | 'circle') => void
|
||||||
getBg: () => 'plain' | 'glow' | 'photo'
|
getBg: () => 'plain' | 'glow' | 'photo'
|
||||||
setBg: (m: 'plain' | 'glow' | 'photo') => void
|
setBg: (m: 'plain' | 'glow' | 'photo') => void
|
||||||
getPhoto: () => string
|
getPhoto: () => Promise<string>
|
||||||
setPhoto: (dataUrl: string) => void
|
setPhoto: (blob: Blob) => Promise<void>
|
||||||
getPlainHsl: () => { h: number; s: number; l: number }
|
getPlainHsl: () => { h: number; s: number; l: number }
|
||||||
setPlainHsl: (v: { h: number; s: number; l: number }) => void
|
setPlainHsl: (v: { h: number; s: number; l: number }) => void
|
||||||
getGlowHsl: () => { h: number; s: number; l: number }
|
getGlowHsl: () => { h: number; s: number; l: number }
|
||||||
@@ -1037,7 +1037,8 @@ function ConsolePane() {
|
|||||||
const [cols, setColsState] = useState<number>(() => splitEnv?.console?.getCols?.() ?? 3)
|
const [cols, setColsState] = useState<number>(() => splitEnv?.console?.getCols?.() ?? 3)
|
||||||
const [shape, setShapeState] = useState<'square' | 'circle'>(() => splitEnv?.console?.getShape?.() ?? 'square')
|
const [shape, setShapeState] = useState<'square' | 'circle'>(() => splitEnv?.console?.getShape?.() ?? 'square')
|
||||||
const [bg, setBgState] = useState<'plain' | 'glow' | 'photo'>(() => splitEnv?.console?.getBg?.() ?? 'glow')
|
const [bg, setBgState] = useState<'plain' | 'glow' | 'photo'>(() => splitEnv?.console?.getBg?.() ?? 'glow')
|
||||||
const [bgPhoto, setBgPhoto] = useState<string>(() => splitEnv?.console?.getPhoto?.() ?? '')
|
const [bgPhoto, setBgPhoto] = useState<string>('')
|
||||||
|
const photoUrlRef = useRef<string>('')
|
||||||
const [openMenu, setOpenMenu] = useState<string | null>(null)
|
const [openMenu, setOpenMenu] = useState<string | null>(null)
|
||||||
const [bgEdit, setBgEdit] = useState<'plain' | 'glow' | null>(null)
|
const [bgEdit, setBgEdit] = useState<'plain' | 'glow' | null>(null)
|
||||||
const [plainHsl, setPlainHslState] = useState<{ h: number; s: number; l: number }>(() => splitEnv?.console?.getPlainHsl?.() ?? { h: 220, s: 31, l: 6 })
|
const [plainHsl, setPlainHslState] = useState<{ h: number; s: number; l: number }>(() => splitEnv?.console?.getPlainHsl?.() ?? { h: 220, s: 31, l: 6 })
|
||||||
@@ -1064,6 +1065,16 @@ function ConsolePane() {
|
|||||||
setShapeState(s)
|
setShapeState(s)
|
||||||
splitEnv?.console?.setShape?.(s)
|
splitEnv?.console?.setShape?.(s)
|
||||||
}
|
}
|
||||||
|
/** 原图上图:原始 Blob 存 IndexedDB,显示用对象 URL(零压缩全分辨率) */
|
||||||
|
const onFilePhoto = async (file: Blob) => {
|
||||||
|
try { await splitEnv?.console?.setPhoto?.(file) } catch {}
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
if (photoUrlRef.current) { try { URL.revokeObjectURL(photoUrlRef.current) } catch {} }
|
||||||
|
photoUrlRef.current = url
|
||||||
|
setBgPhoto(url)
|
||||||
|
setBgState('photo')
|
||||||
|
splitEnv?.console?.setBg?.('photo')
|
||||||
|
}
|
||||||
const onBg = (m: 'plain' | 'glow' | 'photo') => {
|
const onBg = (m: 'plain' | 'glow' | 'photo') => {
|
||||||
if (m === 'photo') {
|
if (m === 'photo') {
|
||||||
const input = document.createElement('input')
|
const input = document.createElement('input')
|
||||||
@@ -1072,31 +1083,7 @@ function ConsolePane() {
|
|||||||
input.onchange = () => {
|
input.onchange = () => {
|
||||||
const file = input.files?.[0]
|
const file = input.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
const url = URL.createObjectURL(file)
|
onFilePhoto(file)
|
||||||
const img = new Image()
|
|
||||||
img.onload = () => {
|
|
||||||
const maxDim = 2400
|
|
||||||
const scale = Math.min(1, maxDim / Math.max(img.width, img.height))
|
|
||||||
const w = Math.max(1, Math.round(img.width * scale))
|
|
||||||
const h = Math.max(1, Math.round(img.height * scale))
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = w
|
|
||||||
canvas.height = h
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
if (!ctx) { URL.revokeObjectURL(url); return }
|
|
||||||
// 高清重采样:大图缩到 2400px 上限,WebP 0.9(不支持时回退 JPEG 0.9)
|
|
||||||
ctx.imageSmoothingEnabled = true
|
|
||||||
ctx.imageSmoothingQuality = 'high'
|
|
||||||
ctx.drawImage(img, 0, 0, w, h)
|
|
||||||
const webp = canvas.toDataURL('image/webp', 0.9)
|
|
||||||
const dataUrl = webp.indexOf('data:image/webp') === 0 ? webp : canvas.toDataURL('image/jpeg', 0.9)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
setBgPhoto(dataUrl)
|
|
||||||
setBgState('photo')
|
|
||||||
splitEnv?.console?.setBg?.('photo')
|
|
||||||
splitEnv?.console?.setPhoto?.(dataUrl)
|
|
||||||
}
|
|
||||||
img.src = url
|
|
||||||
}
|
}
|
||||||
input.click()
|
input.click()
|
||||||
return
|
return
|
||||||
@@ -1176,6 +1163,20 @@ function ConsolePane() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
splitEnv?.console?.refreshPreviews?.()
|
splitEnv?.console?.refreshPreviews?.()
|
||||||
}, [])
|
}, [])
|
||||||
|
// 背景原图:IndexedDB 异步加载(对象 URL 全分辨率;无记录时回退旧压缩图)
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true
|
||||||
|
splitEnv?.console?.getPhoto?.().then((url) => {
|
||||||
|
if (alive && url && url !== photoUrlRef.current) {
|
||||||
|
if (photoUrlRef.current) { try { URL.revokeObjectURL(photoUrlRef.current) } catch {} }
|
||||||
|
photoUrlRef.current = url
|
||||||
|
setBgPhoto(url)
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
return () => { alive = false }
|
||||||
|
}, [])
|
||||||
|
// 面板卸载时释放对象 URL
|
||||||
|
useEffect(() => () => { if (photoUrlRef.current) { try { URL.revokeObjectURL(photoUrlRef.current) } catch {} } }, [])
|
||||||
// 运行时长的分钟级刷新:每秒重渲染一次(仅控制室开着时存在)
|
// 运行时长的分钟级刷新:每秒重渲染一次(仅控制室开着时存在)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const iv = window.setInterval(() => setNow(Date.now()), 1000)
|
const iv = window.setInterval(() => setNow(Date.now()), 1000)
|
||||||
|
|||||||
Reference in New Issue
Block a user