修复一些已知问题
This commit is contained in:
1 parent
4a11e448cf
commit
b59db6879c
31 files changed
+1316
-772
No files matched your search
+74
-1
@@ -59,6 +59,12 @@ function isPreviewableHtml(name) {
|
||||
return (name || '').toLowerCase().endsWith('.html') || (name || '').toLowerCase().endsWith('.htm');
|
||||
}
|
||||
|
||||
/** 二进制表格(XLSX/XLS):由后端 /api/table/text 提取为 TSV 后按列着色预览 */
|
||||
function isPreviewableTable(name) {
|
||||
const ext = (name || '').toLowerCase();
|
||||
return ext.endsWith('.xlsx') || ext.endsWith('.xls');
|
||||
}
|
||||
|
||||
const BINARY_EXTS = new Set([
|
||||
'.exe','.dll','.so','.dylib','.bin','.dat','.zip','.rar','.7z','.tar','.gz','.bz2','.xz',
|
||||
'.png','.jpg','.jpeg','.gif','.bmp','.ico','.webp','.tiff','.tif','.psd','.ai','.eps',
|
||||
@@ -78,7 +84,19 @@ function isBinaryFile(name) {
|
||||
}
|
||||
|
||||
// ===== 模式切换 =====
|
||||
let _modeSwitchCooldown = false;
|
||||
|
||||
export function switchToMode(mode) {
|
||||
// 防连点:短时间内禁止重复切换
|
||||
if (_modeSwitchCooldown) return;
|
||||
_modeSwitchCooldown = true;
|
||||
setTimeout(() => { _modeSwitchCooldown = false; }, 300);
|
||||
|
||||
// Agent 运行中禁止切换到 cron/edit 模式,避免 chat 区域被替换导致工具调用和思考历史消失
|
||||
if (state.sending && mode !== "chat") {
|
||||
showToast("Agent 正在运行中,请等待完成后再切换模式");
|
||||
return;
|
||||
}
|
||||
state.mode = mode;
|
||||
if (mode === "edit") {
|
||||
state.editMode = true;
|
||||
@@ -155,6 +173,15 @@ export async function openFolder(folderPath) {
|
||||
}
|
||||
if (!rootPath) return;
|
||||
|
||||
// 先校验目录存在并列出文件;文件夹被删除/不可访问时给出明确提示,避免把失效路径写入状态
|
||||
let listRes;
|
||||
try {
|
||||
listRes = await listDir(rootPath, 10);
|
||||
} catch (e) {
|
||||
showToast("打开文件夹失败: " + (e.message || e));
|
||||
return;
|
||||
}
|
||||
|
||||
state._docRootPath = rootPath;
|
||||
state._docRootName = rootPath.split(/[/\\]/).pop() || rootPath;
|
||||
|
||||
@@ -166,7 +193,6 @@ export async function openFolder(folderPath) {
|
||||
if (!state._docTreeFolders) state._docTreeFolders = {};
|
||||
state._docTreeFolders[state._docRootName] = true;
|
||||
|
||||
const listRes = await listDir(rootPath, 10);
|
||||
const files = listRes.files || [];
|
||||
const supportedFiles = files.filter((f) => !f.is_dir && isSupportedFile(f.name));
|
||||
// 去重:按 path 去重
|
||||
@@ -257,6 +283,19 @@ export async function loadDocContent(filePath) {
|
||||
openFilePreview(filePath, fileName, res.content || '',
|
||||
{ status, onDownload: () => downloadFile(filePath, fileName) });
|
||||
}
|
||||
} else if (isPreviewableTable(fileName)) {
|
||||
// XLSX/XLS:后端提取为 TSV 文本,走与 CSV/TSV 一致的按列着色预览
|
||||
const absPath = (/^[A-Za-z]:[\\/]/.test(filePath) || filePath.startsWith('/') || filePath.startsWith('\\'))
|
||||
? filePath
|
||||
: state._docRootPath.replace(/[\\/]+$/, '') + '/' + filePath.replace(/^[\\/]+/, '');
|
||||
try {
|
||||
const tsv = await api('/api/table/text?path=' + encodeURIComponent(absPath));
|
||||
openFilePreview(absPath, fileName, (tsv && tsv.content) || '',
|
||||
{ status, onDownload: () => downloadFile(absPath, fileName) });
|
||||
} catch (e) {
|
||||
openFilePreview(absPath, fileName, '(加载失败: ' + (e.message || e) + ')',
|
||||
{ status, onDownload: () => downloadFile(absPath, fileName) });
|
||||
}
|
||||
} else if (isBinaryFile(fileName)) {
|
||||
showToast('二进制文件不支持在线查看', 'warning');
|
||||
} else {
|
||||
@@ -309,6 +348,20 @@ async function downloadFile(filePath, fileName) {
|
||||
a.href = url; a.download = fileName;
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
}
|
||||
} else if (isBinaryFile(fileName)) {
|
||||
// 二进制文件(xlsx/docx/pdf 等):按 base64 还原字节后下载
|
||||
const res = await readBinaryFile(filePath, state._docRootPath);
|
||||
if (res && res.status === 'ok' && res.data) {
|
||||
const bin = atob(res.data);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
const blob = new Blob([bytes], { type: res.mime || 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = fileName;
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} else {
|
||||
const res = await readTextFile(filePath, state._docRootPath);
|
||||
if (res && res.status === 'ok') {
|
||||
@@ -737,17 +790,37 @@ function setupInputDropHandler() {
|
||||
const target = composerInput || $('textInput');
|
||||
if (!target) return;
|
||||
|
||||
// 子元素间移动会成对触发 dragenter/dragleave,用计数器避免状态误清除
|
||||
let dragDepth = 0;
|
||||
const setDragState = (on) => target.classList.toggle('is-drag-attach', on);
|
||||
const isAcceptedDrag = (types) => types.includes('application/doc-path') || types.includes('Files');
|
||||
|
||||
target.addEventListener('dragenter', (e) => {
|
||||
if (isAcceptedDrag(Array.from(e.dataTransfer.types))) {
|
||||
dragDepth++;
|
||||
setDragState(true);
|
||||
}
|
||||
});
|
||||
|
||||
target.addEventListener('dragover', (e) => {
|
||||
const types = Array.from(e.dataTransfer.types);
|
||||
// 接受应用内文件/文件夹引用拖拽 或 外部文件拖拽
|
||||
if (types.includes('application/doc-path') || types.includes('Files')) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
setDragState(true);
|
||||
}
|
||||
});
|
||||
|
||||
target.addEventListener('dragleave', () => {
|
||||
dragDepth = Math.max(0, dragDepth - 1);
|
||||
if (dragDepth === 0) setDragState(false);
|
||||
});
|
||||
|
||||
target.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
dragDepth = 0;
|
||||
setDragState(false);
|
||||
|
||||
// 1. 内部拖拽(doc-tree 文件/文件夹引用)
|
||||
// app.js 检测到 application/doc-path 也会 return,无需 stopPropagation
|
||||
|
||||
+101
-1
@@ -6,8 +6,9 @@ import { $, escapeHtml } from './utils.js';
|
||||
const IMG_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.ico']);
|
||||
const HTML_EXTS = new Set(['.html', '.htm']);
|
||||
const TEXT_EXTS = new Set(['.txt', '.json', '.md', '.py', '.js', '.ts', '.css', '.xml', '.yaml', '.yml', '.toml',
|
||||
'.csv', '.log', '.env', '.ini', '.cfg', '.sh', '.bat', '.ps1', '.sql', '.r', '.go', '.rs', '.rb',
|
||||
'.csv', '.tsv', '.log', '.env', '.ini', '.cfg', '.sh', '.bat', '.ps1', '.sql', '.r', '.go', '.rs', '.rb',
|
||||
'.php', '.swift', '.kt', '.scala', '.cpp', '.c', '.h', '.java', '.m', '.svg']);
|
||||
const TABLE_EXTS = new Set(['.csv', '.tsv', '.xlsx', '.xls']);
|
||||
|
||||
let _currentFilePath = null;
|
||||
let _currentContent = null;
|
||||
@@ -33,6 +34,13 @@ function isTextFile(fileName) {
|
||||
return dot >= 0 && TEXT_EXTS.has(ext.substring(dot));
|
||||
}
|
||||
|
||||
/** 判断表格文件(CSV/TSV 文本按列着色;XLSX/XLS 由后端提取为 TSV 后同样按列着色) */
|
||||
function isTableFile(fileName) {
|
||||
const ext = (fileName || '').toLowerCase();
|
||||
const dot = ext.lastIndexOf('.');
|
||||
return dot >= 0 && TABLE_EXTS.has(ext.substring(dot));
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开文件预览浮窗
|
||||
* @param {string} filePath - 文件路径
|
||||
@@ -84,6 +92,8 @@ export function openFilePreview(filePath, fileName, content, opts = {}) {
|
||||
_renderImage(body, content, fileName);
|
||||
} else if (isHtml(fileName) && !opts.asText) {
|
||||
_renderPlainText(body, content);
|
||||
} else if (isTableFile(fileName)) {
|
||||
_renderTable(body, content, fileName);
|
||||
} else if (isTextFile(fileName)) {
|
||||
_renderPlainText(body, content);
|
||||
} else if (content && content.startsWith && content.startsWith('data:')) {
|
||||
@@ -162,6 +172,96 @@ function _renderPlainText(body, content) {
|
||||
codeLines.addEventListener('contextmenu', (e) => _onCodeContextMenu(e));
|
||||
}
|
||||
|
||||
// ===== 表格预览(复用 plaintext 预览 UI,不同列不同颜色) =====
|
||||
|
||||
/** 按主题区分的列色板(暗色 / 亮色各一套,保证可读性) */
|
||||
const TABLE_COLUMN_COLORS = {
|
||||
dark: ['#e06c75', '#61afef', '#98c379', '#e5c07b', '#c678dd', '#56b6c2', '#d19a66', '#f472b6', '#7f9cf5', '#4ade80'],
|
||||
light: ['#c0392b', '#2471a3', '#1e8449', '#b7950b', '#7d3c98', '#117a65', '#ca6f1e', '#a93226', '#2e86c1', '#229954'],
|
||||
};
|
||||
|
||||
/** 判断分隔符:TSV 用 tab,CSV 用逗号,XLSX/XLS(后端已提取为 TSV)用 tab,其他按首行内容猜测 */
|
||||
function _getDelimiter(fileName, content) {
|
||||
const ext = (fileName || '').toLowerCase();
|
||||
if (ext.endsWith('.tsv') || ext.endsWith('.xlsx') || ext.endsWith('.xls')) return '\t';
|
||||
if (ext.endsWith('.csv')) return ',';
|
||||
const firstLine = (content || '').split('\n')[0] || '';
|
||||
return firstLine.includes('\t') ? '\t' : ',';
|
||||
}
|
||||
|
||||
/** 解析单行分隔符文本(支持引号包裹的字段,如 "a,b") */
|
||||
function _splitDelimited(line, delim) {
|
||||
const cells = [];
|
||||
let cur = '';
|
||||
let inQuotes = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const ch = line[i];
|
||||
if (inQuotes) {
|
||||
if (ch === '"') {
|
||||
if (line[i + 1] === '"') { cur += '"'; i++; }
|
||||
else inQuotes = false;
|
||||
} else cur += ch;
|
||||
} else if (ch === '"') {
|
||||
inQuotes = true;
|
||||
} else if (ch === delim) {
|
||||
cells.push(cur);
|
||||
cur = '';
|
||||
} else {
|
||||
cur += ch;
|
||||
}
|
||||
}
|
||||
cells.push(cur);
|
||||
return cells;
|
||||
}
|
||||
|
||||
/** 表格预览:保留行号/选中引用交互,单元格按列着色 */
|
||||
function _renderTable(body, content, fileName) {
|
||||
const text = content || '';
|
||||
const delim = _getDelimiter(fileName, text);
|
||||
const lines = text.split('\n');
|
||||
const colors = document.body.classList.contains('theme-dark') ? TABLE_COLUMN_COLORS.dark : TABLE_COLUMN_COLORS.light;
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'fp-code-wrap fp-table-wrap';
|
||||
|
||||
// 行号列
|
||||
const lineNos = document.createElement('div');
|
||||
lineNos.className = 'fp-line-nos';
|
||||
for (let i = 1; i <= lines.length; i++) {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = i;
|
||||
lineNos.appendChild(span);
|
||||
}
|
||||
|
||||
// 数据列:每行一个 span,单元格按列序号着色,首行作为表头加粗
|
||||
const codeLines = document.createElement('div');
|
||||
codeLines.className = 'fp-code-lines';
|
||||
lines.forEach((line, i) => {
|
||||
const row = document.createElement('span');
|
||||
row.setAttribute('data-line', i + 1);
|
||||
const cells = _splitDelimited(line, delim);
|
||||
if (cells.length === 1 && !line.trim()) {
|
||||
row.textContent = '\u00A0';
|
||||
} else {
|
||||
cells.forEach((cell, ci) => {
|
||||
const cellEl = document.createElement('span');
|
||||
cellEl.className = 'fp-tbl-cell' + (i === 0 ? ' fp-tbl-cell--header' : '');
|
||||
cellEl.textContent = cell;
|
||||
cellEl.style.color = colors[ci % colors.length];
|
||||
row.appendChild(cellEl);
|
||||
});
|
||||
}
|
||||
codeLines.appendChild(row);
|
||||
});
|
||||
|
||||
wrap.appendChild(lineNos);
|
||||
wrap.appendChild(codeLines);
|
||||
body.appendChild(wrap);
|
||||
|
||||
// 右键菜单:引用选中行
|
||||
codeLines.addEventListener('contextmenu', (e) => _onCodeContextMenu(e));
|
||||
}
|
||||
|
||||
function _onCodeContextMenu(e) {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
|
||||
|
||||
@@ -211,12 +211,6 @@ function buildPendingCard(pending, isSubAgent) {
|
||||
inputRow.appendChild(sendBtn);
|
||||
|
||||
cardBody.appendChild(inputRow);
|
||||
|
||||
// 跳过按钮
|
||||
const btnRow = document.createElement("div");
|
||||
btnRow.className = "pending-overlay-btn-row";
|
||||
btnRow.appendChild(makeDecisionBtn("跳过", "skip", "", () => submitSingle(pending.id, "skip", "", card)));
|
||||
cardBody.appendChild(btnRow);
|
||||
} else if (interactionType === "selection") {
|
||||
if (pending.questions && Array.isArray(pending.questions) && pending.questions.length > 0) {
|
||||
pending.questions.forEach((q) => {
|
||||
@@ -289,12 +283,6 @@ function buildPendingCard(pending, isSubAgent) {
|
||||
});
|
||||
cardBody.appendChild(pillsRow);
|
||||
}
|
||||
|
||||
// 跳过按钮
|
||||
const btnRow = document.createElement("div");
|
||||
btnRow.className = "pending-overlay-btn-row";
|
||||
btnRow.appendChild(makeDecisionBtn("跳过", "skip", "", () => submitSingle(pending.id, "skip", "", card)));
|
||||
cardBody.appendChild(btnRow);
|
||||
}
|
||||
|
||||
card.appendChild(cardBody);
|
||||
|
||||
+45
-9
@@ -139,16 +139,19 @@ function buildAgentCard(agent, idx) {
|
||||
header.className = "agent-card-header";
|
||||
const h4 = document.createElement("h4");
|
||||
h4.textContent = `Agent: ${agent.name || "(\u672A\u547D\u540D)"}`;
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.className = "agent-card-remove";
|
||||
removeBtn.textContent = "\u2715";
|
||||
removeBtn.title = "\u5220\u9664\u6B64 Agent";
|
||||
removeBtn.addEventListener("click", () => {
|
||||
state.cachedAgentConfigs.splice(idx, 1);
|
||||
renderAgentConfigs();
|
||||
});
|
||||
header.appendChild(h4);
|
||||
header.appendChild(removeBtn);
|
||||
// 主 Agent 不可删除
|
||||
if (agent.role !== "main") {
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.className = "agent-card-remove";
|
||||
removeBtn.textContent = "\u2715";
|
||||
removeBtn.title = "\u5220\u9664\u6B64 Agent";
|
||||
removeBtn.addEventListener("click", () => {
|
||||
state.cachedAgentConfigs.splice(idx, 1);
|
||||
renderAgentConfigs();
|
||||
});
|
||||
header.appendChild(removeBtn);
|
||||
}
|
||||
|
||||
const nameField = makeField("\u540D\u79F0", "text", agent.name || "", (v) => { agent.name = v; });
|
||||
const descField = makeField("\u80FD\u529B\u63CF\u8FF0 (description)", "text", agent.description || "", (v) => { agent.description = v; });
|
||||
@@ -1135,6 +1138,8 @@ function renderThemePanel() {
|
||||
const bgApplyBtn = $("bgImageApplyBtn");
|
||||
const bgResetBtn = $("bgImageResetBtn");
|
||||
const bgStatus = $("bgPreviewStatus");
|
||||
const bgPickerBtn = $("bgImagePickerBtn");
|
||||
const bgFileInput = $("bgImageFileInput");
|
||||
if (bgInput) {
|
||||
bgInput.value = getBgImage();
|
||||
const applyBg = () => {
|
||||
@@ -1154,6 +1159,37 @@ function renderThemePanel() {
|
||||
}
|
||||
// 回车也应用
|
||||
bgInput.onkeydown = (e) => { if (e.key === 'Enter') applyBg(); };
|
||||
|
||||
// 图片选择器按钮点击事件
|
||||
if (bgPickerBtn && bgFileInput) {
|
||||
bgPickerBtn.onclick = () => {
|
||||
bgFileInput.click();
|
||||
};
|
||||
|
||||
// 文件选择事件
|
||||
bgFileInput.onchange = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// 使用文件路径(Electron 环境)或创建临时 URL(Web 环境)
|
||||
if (window.electronAPI && window.electronAPI.getFilePath) {
|
||||
// Electron 环境:获取文件路径
|
||||
const filePath = window.electronAPI.getFilePath(file);
|
||||
bgInput.value = filePath;
|
||||
applyBg();
|
||||
} else {
|
||||
// Web 环境:创建临时 URL
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
bgInput.value = event.target?.result || "";
|
||||
applyBg();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
// 清空 input 以便重复选择同一文件
|
||||
bgFileInput.value = "";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 背景模糊度
|
||||
|
||||
+95
-45
@@ -583,12 +583,40 @@ export const THEMES = {
|
||||
};
|
||||
|
||||
const DEFAULT_THEME = "vscode-dark";
|
||||
const STORAGE_KEY = "minor_theme";
|
||||
const STORAGE_BG_KEY = "minor_bg_image";
|
||||
const STORAGE_FONT_KEY = "minor_font";
|
||||
const STORAGE_FONT_SIZE_KEY = "minor_font_size";
|
||||
const STORAGE_BLUR_KEY = "minor_bg_blur";
|
||||
const STORAGE_VIGNETTE_KEY = "minor_bg_vignette";
|
||||
|
||||
// 主题设置统一内存态:全部落盘到服务端 theme_config.json(不使用 localStorage)
|
||||
let _settings = {
|
||||
theme: DEFAULT_THEME,
|
||||
bg_image: "",
|
||||
font_family: "",
|
||||
font_size: "",
|
||||
bg_blur: "",
|
||||
bg_vignette: "",
|
||||
};
|
||||
let _loadingTheme = false; // 从服务端加载期间不写回,避免覆盖已有配置
|
||||
let _persistTimer = null;
|
||||
|
||||
/** 防抖写回服务端 theme_config.json(仅保存用户主动修改,加载期间跳过) */
|
||||
function _persistThemeConfig() {
|
||||
if (_loadingTheme) return;
|
||||
if (_persistTimer) clearTimeout(_persistTimer);
|
||||
_persistTimer = setTimeout(() => {
|
||||
_persistTimer = null;
|
||||
try {
|
||||
api("/api/config/theme", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
theme: _settings.theme,
|
||||
bg_image: _settings.bg_image,
|
||||
font_family: _settings.font_family,
|
||||
font_size: _settings.font_size,
|
||||
bg_blur: _settings.bg_blur,
|
||||
bg_vignette: _settings.bg_vignette,
|
||||
}),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 字体预设
|
||||
export const FONT_PRESETS = {
|
||||
@@ -611,14 +639,14 @@ export const FONT_SIZE_PRESETS = {
|
||||
"20px": "20px",
|
||||
};
|
||||
|
||||
const getFontKey = (k) => ({ family: STORAGE_FONT_KEY, size: STORAGE_FONT_SIZE_KEY }[k]);
|
||||
|
||||
export function getFontSetting(key) {
|
||||
try { return localStorage.getItem(getFontKey(key)) || ""; } catch { return ""; }
|
||||
return key === "family" ? _settings.font_family : key === "size" ? _settings.font_size : "";
|
||||
}
|
||||
export function setFontSetting(key, val) {
|
||||
try { localStorage.setItem(getFontKey(key), val || ""); } catch {}
|
||||
if (key === "family") _settings.font_family = val || "";
|
||||
else if (key === "size") _settings.font_size = val || "";
|
||||
applyFontSetting(key, val);
|
||||
_persistThemeConfig();
|
||||
}
|
||||
export function applyFontSetting(key, val) {
|
||||
const r = document.documentElement;
|
||||
@@ -638,15 +666,12 @@ function localPathToUrl(localPath) {
|
||||
|
||||
/** 获取/设置自定义背景图 */
|
||||
export function getBgImage() {
|
||||
try { return localStorage.getItem(STORAGE_BG_KEY) || ""; }
|
||||
catch { return ""; }
|
||||
return _settings.bg_image;
|
||||
}
|
||||
export function setBgImage(url) {
|
||||
try { localStorage.setItem(STORAGE_BG_KEY, url || ""); }
|
||||
catch {}
|
||||
applyBgImage(url);
|
||||
// 异步同步到服务端
|
||||
try { api("/api/config/theme", { method: "POST", body: JSON.stringify({ bg_image: url || "" }) }); } catch {}
|
||||
_settings.bg_image = url || "";
|
||||
applyBgImage(_settings.bg_image);
|
||||
_persistThemeConfig();
|
||||
}
|
||||
export function applyBgImage(url) {
|
||||
const r = document.documentElement;
|
||||
@@ -666,8 +691,7 @@ export function applyBgImage(url) {
|
||||
|
||||
/** 获取当前保存的主题 */
|
||||
export function getCurrentTheme() {
|
||||
try { return localStorage.getItem(STORAGE_KEY) || DEFAULT_THEME; }
|
||||
catch { return DEFAULT_THEME; }
|
||||
return _settings.theme;
|
||||
}
|
||||
|
||||
/** 应用主题到页面 */
|
||||
@@ -685,8 +709,9 @@ export function applyTheme(themeId) {
|
||||
Object.entries(theme.edit).forEach(([k, v]) => r.style.setProperty(k, v));
|
||||
}
|
||||
|
||||
// 持久化
|
||||
try { localStorage.setItem(STORAGE_KEY, themeId); } catch {}
|
||||
// 更新内存态并落盘(加载期间跳过,避免覆盖服务端已有配置)
|
||||
_settings.theme = THEMES[themeId] ? themeId : DEFAULT_THEME;
|
||||
_persistThemeConfig();
|
||||
|
||||
// 触发 body 类名切换(用于全局暗/亮模式判断)
|
||||
// 优先使用主题对象的 dark 字段;缺失时回退到 id 字符串推断
|
||||
@@ -695,28 +720,27 @@ export function applyTheme(themeId) {
|
||||
document.body.classList.toggle("theme-light", !isDark);
|
||||
}
|
||||
|
||||
/** 初始化:从 localStorage 恢复主题,并从服务端同步背景图 */
|
||||
/** 初始化:先用默认主题渲染,再从服务端 theme_config.json 恢复全部设置 */
|
||||
export function initTheme() {
|
||||
const id = getCurrentTheme();
|
||||
applyTheme(id);
|
||||
// 先从 localStorage 快读,再从服务端同步(异步)
|
||||
applyBgImage(getBgImage());
|
||||
applyFontSetting('family', getFontSetting('family'));
|
||||
applyFontSetting('size', getFontSetting('size'));
|
||||
applyBgBlur(getBgBlur());
|
||||
applyBgVignette(getBgVignette());
|
||||
// 异步从服务端同步主题配置
|
||||
loadThemeConfig();
|
||||
return id;
|
||||
_loadingTheme = true;
|
||||
applyTheme(DEFAULT_THEME);
|
||||
applyBgImage("");
|
||||
applyFontSetting("family", "");
|
||||
applyFontSetting("size", "");
|
||||
applyBgBlur("");
|
||||
applyBgVignette("");
|
||||
loadThemeConfig(); // 完成后在 finally 中解除 _loadingTheme
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
|
||||
// ===== 背景模糊度 =====
|
||||
export function getBgBlur() {
|
||||
try { return localStorage.getItem(STORAGE_BLUR_KEY) || ""; } catch { return ""; }
|
||||
return _settings.bg_blur;
|
||||
}
|
||||
export function setBgBlur(val) {
|
||||
try { localStorage.setItem(STORAGE_BLUR_KEY, val || ""); } catch {}
|
||||
applyBgBlur(val);
|
||||
_settings.bg_blur = val || "";
|
||||
applyBgBlur(_settings.bg_blur);
|
||||
_persistThemeConfig();
|
||||
}
|
||||
export function applyBgBlur(val) {
|
||||
// val 为空字符串时使用主题默认值(由 applyTheme 设置),否则覆盖
|
||||
@@ -730,11 +754,12 @@ export function applyBgBlur(val) {
|
||||
|
||||
// ===== 背景暗角 =====
|
||||
export function getBgVignette() {
|
||||
try { return localStorage.getItem(STORAGE_VIGNETTE_KEY) || ""; } catch { return ""; }
|
||||
return _settings.bg_vignette;
|
||||
}
|
||||
export function setBgVignette(val) {
|
||||
try { localStorage.setItem(STORAGE_VIGNETTE_KEY, val || ""); } catch {}
|
||||
applyBgVignette(val);
|
||||
_settings.bg_vignette = val || "";
|
||||
applyBgVignette(_settings.bg_vignette);
|
||||
_persistThemeConfig();
|
||||
}
|
||||
export function applyBgVignette(val) {
|
||||
const r = document.documentElement;
|
||||
@@ -745,14 +770,39 @@ export function applyBgVignette(val) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 从服务端加载主题配置(覆盖 localStorage 缓存) */
|
||||
/** 从服务端 theme_config.json 加载主题配置并应用(不使用 localStorage) */
|
||||
export async function loadThemeConfig() {
|
||||
try {
|
||||
const res = await api("/api/config/theme");
|
||||
if (res.bg_image !== undefined) {
|
||||
const img = res.bg_image || "";
|
||||
try { localStorage.setItem(STORAGE_BG_KEY, img); } catch {}
|
||||
applyBgImage(img);
|
||||
_loadingTheme = true;
|
||||
if (res.theme && THEMES[res.theme]) {
|
||||
applyTheme(res.theme);
|
||||
}
|
||||
} catch {}
|
||||
if (res.bg_image !== undefined) {
|
||||
_settings.bg_image = res.bg_image || "";
|
||||
applyBgImage(_settings.bg_image);
|
||||
}
|
||||
if (res.font_family !== undefined) {
|
||||
// 过滤旧版默认占位值(msjh 等非真实字体)
|
||||
const fam = (res.font_family || "").trim();
|
||||
_settings.font_family = (fam && fam !== "msjh" && fam !== "msjhbd") ? fam : "";
|
||||
applyFontSetting("family", _settings.font_family);
|
||||
}
|
||||
if (res.font_size !== undefined) {
|
||||
_settings.font_size = res.font_size || "";
|
||||
applyFontSetting("size", _settings.font_size);
|
||||
}
|
||||
if (res.bg_blur !== undefined) {
|
||||
_settings.bg_blur = res.bg_blur || "";
|
||||
applyBgBlur(_settings.bg_blur);
|
||||
}
|
||||
if (res.bg_vignette !== undefined) {
|
||||
_settings.bg_vignette = res.bg_vignette || "";
|
||||
applyBgVignette(_settings.bg_vignette);
|
||||
}
|
||||
} catch {
|
||||
// 服务端不可达:保持默认设置
|
||||
} finally {
|
||||
_loadingTheme = false;
|
||||
}
|
||||
}
|
||||
@@ -626,7 +626,9 @@ function renderToolCallRow(call, options = {}) {
|
||||
icon.title = isRunning ? "点击折叠" : "点击展开工具返回值";
|
||||
icon.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
row.classList.toggle("tool-call-row--expanded");
|
||||
const nowExpanded = row.classList.toggle("tool-call-row--expanded");
|
||||
// 展开后若详情在可视区外,滚动到可见位置(block:nearest 已可见时不滚动)
|
||||
if (nowExpanded) detailEl.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user