Files
metona-ollama-desktop/js/components/settings-modal.js
T
thzxx 38edaee0f0 refactor: 模块化架构重构
将单文件巨型 IIFE (1608行) 拆分为 14 个 ES Module:

核心层:
- js/state.js          - 轻量级响应式状态管理 (发布-订阅)
- js/utils.js          - 通用工具函数
- js/sanitizer.js      - HTML XSS 净化器
- js/marked-config.js  - Markdown 渲染器配置

组件层:
- js/components/chat-area.js      - 消息渲染/流式更新/导出
- js/components/input-area.js     - 文本输入/图片上传/发送
- js/components/history-modal.js  - 历史记录面板/搜索/分页
- js/components/settings-modal.js - 设置面板/数据管理
- js/components/header.js         - 顶部导航/连接状态
- js/components/model-bar.js      - 模型选择栏
- js/components/toast.js          - Toast 通知
- js/components/lightbox.js       - 图片预览

入口层:
- js/app.js            - 主入口/初始化协调

index.html: 1608行 → 204行纯模板
2026-04-03 11:34:24 +08:00

170 lines
6.0 KiB
JavaScript

/**
* SettingsModal - 设置面板组件
*/
import { state, KEYS } from '../state.js';
import { debounce, formatSize } from '../utils.js';
import { updateConnectionInfo, updateRunningModels } from './header.js';
import { loadModels } from './model-bar.js';
import { showToast } from './toast.js';
import { OllamaAPI } from '../../ollama-api.js';
let settingsModalEl;
export function initSettingsModal() {
settingsModalEl = document.querySelector('#settingsModal');
// 打开/关闭
document.querySelector('#btnSettings').addEventListener('click', openSettingsModal);
document.querySelector('#btnCloseSettings').addEventListener('click', closeSettingsModal);
settingsModalEl.addEventListener('click', (e) => {
if (e.target === settingsModalEl) closeSettingsModal();
});
// 保存服务器地址
const saveServerUrl = debounce(async () => {
const url = document.querySelector('#inputServerUrl').value.trim();
if (!url) return;
const db = state.get(KEYS.DB);
const api = new OllamaAPI(url);
state.set(KEYS.API, api);
if (db) await db.saveSetting('serverUrl', url);
updateConnectionInfo();
loadModels();
}, 500);
document.querySelector('#inputServerUrl').addEventListener('input', saveServerUrl);
// 系统提示词开关
document.querySelector('#toggleSystemPrompt').addEventListener('change', async (e) => {
const db = state.get(KEYS.DB);
state.set(KEYS.SYSTEM_PROMPT_ENABLED, e.target.checked);
document.querySelector('#inputSystemPrompt').disabled = !e.target.checked;
if (db) await db.saveSetting('systemPromptEnabled', e.target.checked);
});
// 保存系统提示词
const saveSystemPrompt = debounce(async () => {
const db = state.get(KEYS.DB);
const val = document.querySelector('#inputSystemPrompt').value.trim();
state.set(KEYS.SYSTEM_PROMPT, val);
if (db) await db.saveSetting('systemPrompt', val);
}, 500);
document.querySelector('#inputSystemPrompt').addEventListener('input', saveSystemPrompt);
// 保存上下文长度
const saveNumCtx = debounce(async () => {
const db = state.get(KEYS.DB);
const val = document.querySelector('#inputNumCtx').value.trim();
const numCtx = val ? parseInt(val) : 24576;
state.set(KEYS.NUM_CTX, numCtx);
if (db) await db.saveSetting('numCtx', numCtx);
}, 500);
document.querySelector('#inputNumCtx').addEventListener('input', saveNumCtx);
// 释放显存
document.querySelector('#btnReleaseVRAM').addEventListener('click', async () => {
const api = state.get(KEYS.API);
const model = document.querySelector('#modelSelect').value;
try {
await api.chat({ model: model || 'any', messages: [], keep_alive: 0 });
showToast('显存已释放', 'success');
updateRunningModels();
} catch (err) {
showToast(`释放失败: ${err.message}`, 'error');
}
});
// 清空所有历史
document.querySelector('#btnClearAllHistory').addEventListener('click', async () => {
const db = state.get(KEYS.DB);
if (confirm('确定清空所有历史记录?此操作不可恢复!')) {
await db.clearAll();
// 触发新会话
document.querySelector('#btnNewChat').click();
showToast('已清空所有历史记录', 'success');
}
});
// 导出全部会话
document.querySelector('#btnExportAll').addEventListener('click', exportAllSessions);
// 导入会话
const importFileInput = document.querySelector('#importFileInput');
document.querySelector('#btnImportSessions').addEventListener('click', () => importFileInput.click());
importFileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
importSessions(e.target.files[0]);
e.target.value = '';
}
});
}
export function openSettingsModal() {
settingsModalEl.style.display = '';
document.querySelector('#inputServerUrl').value = state.get(KEYS.API)?.baseUrl || '';
updateConnectionInfo();
updateRunningModels();
}
export function closeSettingsModal() {
settingsModalEl.style.display = 'none';
}
async function exportAllSessions() {
const db = state.get(KEYS.DB);
if (!db) return;
const sessions = await db.getAllSessions();
if (sessions.length === 0) {
showToast('没有可导出的会话', 'warning');
return;
}
const backup = {
app: 'Metona Ollama Client',
version: 1,
exportedAt: new Date().toISOString(),
count: sessions.length,
sessions
};
const json = JSON.stringify(backup, null, 2);
const ts = new Date().toISOString().slice(0, 10);
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([json], { type: 'application/json;charset=utf-8' }));
a.download = `metona-backup-${ts}.json`;
a.click();
showToast(`已导出 ${sessions.length} 个会话`, 'success');
}
async function importSessions(file) {
const db = state.get(KEYS.DB);
if (!db) return;
try {
const text = await file.text();
let data;
try {
data = JSON.parse(text);
} catch {
showToast('文件格式错误:不是有效的 JSON', 'error');
return;
}
let sessions;
if (Array.isArray(data)) {
sessions = data;
} else if (data.sessions && Array.isArray(data.sessions)) {
sessions = data.sessions;
} else {
showToast('文件格式错误:未找到会话数据', 'error');
return;
}
const result = await db.importSessions(sessions);
showToast(`导入完成:${result.imported} 个会话${result.skipped > 0 ? `,跳过 ${result.skipped} 个` : ''}`, 'success', 4000);
} catch (err) {
console.error('[Settings] 导入失败:', err);
showToast(`导入失败: ${err.message}`, 'error');
}
}