feat(config): add section-based caching and tunable status interval

This commit is contained in:
hkfires
2025-11-17 12:56:53 +08:00
parent f82bcef990
commit fe5d997398
3 changed files with 403 additions and 43 deletions

View File

@@ -1,81 +1,106 @@
// 设置与开关相关方法模块
// 注意:这些函数依赖于在 CLIProxyManager 实例上提供的 makeRequest/clearCache/showNotification 等基础能力
// 注意:这些函数依赖于在 CLIProxyManager 实例上提供的 makeRequest/clearCache/showNotification/errorHandler 等基础能力
export async function updateDebug(enabled) {
const previousValue = !enabled;
try {
await this.makeRequest('/debug', {
method: 'PUT',
body: JSON.stringify({ value: enabled })
});
this.clearCache(); // 清除缓存
this.clearCache('debug'); // 清除 debug 配置段的缓存
this.showNotification(i18n.t('notification.debug_updated'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
// 恢复原状态
document.getElementById('debug-toggle').checked = !enabled;
this.errorHandler.handleUpdateError(
error,
i18n.t('settings.debug_mode') || '调试模式',
() => document.getElementById('debug-toggle').checked = previousValue
);
}
}
export async function updateProxyUrl() {
const proxyUrl = document.getElementById('proxy-url').value.trim();
const previousValue = document.getElementById('proxy-url').getAttribute('data-previous-value') || '';
try {
await this.makeRequest('/proxy-url', {
method: 'PUT',
body: JSON.stringify({ value: proxyUrl })
});
this.clearCache(); // 清除缓存
this.clearCache('proxy-url'); // 清除 proxy-url 配置段的缓存
document.getElementById('proxy-url').setAttribute('data-previous-value', proxyUrl);
this.showNotification(i18n.t('notification.proxy_updated'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
this.errorHandler.handleUpdateError(
error,
i18n.t('settings.proxy_url') || '代理设置',
() => document.getElementById('proxy-url').value = previousValue
);
}
}
export async function clearProxyUrl() {
const previousValue = document.getElementById('proxy-url').value;
try {
await this.makeRequest('/proxy-url', { method: 'DELETE' });
document.getElementById('proxy-url').value = '';
this.clearCache(); // 清除缓存
document.getElementById('proxy-url').setAttribute('data-previous-value', '');
this.clearCache('proxy-url'); // 仅清除 proxy-url 配置段的缓存
this.showNotification(i18n.t('notification.proxy_cleared'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
this.errorHandler.handleUpdateError(
error,
i18n.t('settings.proxy_url') || '代理设置',
() => document.getElementById('proxy-url').value = previousValue
);
}
}
export async function updateRequestRetry() {
const retryCount = parseInt(document.getElementById('request-retry').value);
const retryInput = document.getElementById('request-retry');
const retryCount = parseInt(retryInput.value);
const previousValue = retryInput.getAttribute('data-previous-value') || '0';
try {
await this.makeRequest('/request-retry', {
method: 'PUT',
body: JSON.stringify({ value: retryCount })
});
this.clearCache(); // 清除缓存
this.clearCache('request-retry'); // 清除 request-retry 配置段的缓存
retryInput.setAttribute('data-previous-value', retryCount.toString());
this.showNotification(i18n.t('notification.retry_updated'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
this.errorHandler.handleUpdateError(
error,
i18n.t('settings.request_retry') || '重试设置',
() => retryInput.value = previousValue
);
}
}
export async function loadDebugSettings() {
try {
const config = await this.getConfig();
if (config.debug !== undefined) {
document.getElementById('debug-toggle').checked = config.debug;
const debugValue = await this.getConfig('debug'); // 仅获取 debug 配置段
if (debugValue !== undefined) {
document.getElementById('debug-toggle').checked = debugValue;
}
} catch (error) {
console.error('加载调试设置失败:', error);
this.errorHandler.handleLoadError(error, i18n.t('settings.debug_mode') || '调试设置');
}
}
export async function loadProxySettings() {
try {
const config = await this.getConfig();
if (config['proxy-url'] !== undefined) {
document.getElementById('proxy-url').value = config['proxy-url'] || '';
const proxyUrl = await this.getConfig('proxy-url'); // 仅获取 proxy-url 配置段
const proxyInput = document.getElementById('proxy-url');
if (proxyUrl !== undefined) {
proxyInput.value = proxyUrl || '';
proxyInput.setAttribute('data-previous-value', proxyUrl || '');
}
} catch (error) {
console.error('加载代理设置失败:', error);
this.errorHandler.handleLoadError(error, i18n.t('settings.proxy_settings') || '代理设置');
}
}

279
src/utils/dom.js Normal file
View File

@@ -0,0 +1,279 @@
/**
* DOM 操作工具函数模块
* 提供高性能的 DOM 操作方法
*/
/**
* 批量渲染列表项,使用 DocumentFragment 减少重绘
* @param {HTMLElement} container - 容器元素
* @param {Array} items - 数据项数组
* @param {Function} renderItemFn - 渲染单个项目的函数,返回 HTML 字符串或 Element
* @param {boolean} append - 是否追加模式(默认 false清空后渲染
*
* @example
* renderList(container, files, (file) => `
* <div class="file-item">${file.name}</div>
* `);
*/
export function renderList(container, items, renderItemFn, append = false) {
if (!container) return;
const fragment = document.createDocumentFragment();
items.forEach((item, index) => {
const rendered = renderItemFn(item, index);
if (typeof rendered === 'string') {
// HTML 字符串,创建临时容器
const temp = document.createElement('div');
temp.innerHTML = rendered;
// 将所有子元素添加到 fragment
while (temp.firstChild) {
fragment.appendChild(temp.firstChild);
}
} else if (rendered instanceof HTMLElement) {
// DOM 元素,直接添加
fragment.appendChild(rendered);
}
});
if (!append) {
container.innerHTML = '';
}
container.appendChild(fragment);
}
/**
* 创建 DOM 元素的快捷方法
* @param {string} tag - 标签名
* @param {Object} attrs - 属性对象
* @param {string|Array<HTMLElement>} content - 内容(文本或子元素数组)
* @returns {HTMLElement}
*
* @example
* const div = createElement('div', { class: 'item', 'data-id': '123' }, 'Hello');
* const ul = createElement('ul', {}, [
* createElement('li', {}, 'Item 1'),
* createElement('li', {}, 'Item 2')
* ]);
*/
export function createElement(tag, attrs = {}, content = null) {
const element = document.createElement(tag);
// 设置属性
Object.keys(attrs).forEach(key => {
if (key === 'class') {
element.className = attrs[key];
} else if (key === 'style' && typeof attrs[key] === 'object') {
Object.assign(element.style, attrs[key]);
} else if (key.startsWith('on') && typeof attrs[key] === 'function') {
const eventName = key.substring(2).toLowerCase();
element.addEventListener(eventName, attrs[key]);
} else {
element.setAttribute(key, attrs[key]);
}
});
// 设置内容
if (content !== null && content !== undefined) {
if (typeof content === 'string') {
element.textContent = content;
} else if (Array.isArray(content)) {
content.forEach(child => {
if (child instanceof HTMLElement) {
element.appendChild(child);
}
});
} else if (content instanceof HTMLElement) {
element.appendChild(content);
}
}
return element;
}
/**
* 批量更新元素属性,减少重绘
* @param {HTMLElement} element - 目标元素
* @param {Object} updates - 更新对象
*
* @example
* batchUpdate(element, {
* className: 'active',
* style: { color: 'red', fontSize: '16px' },
* textContent: 'Updated'
* });
*/
export function batchUpdate(element, updates) {
if (!element) return;
// 使用 requestAnimationFrame 批量更新
requestAnimationFrame(() => {
Object.keys(updates).forEach(key => {
if (key === 'style' && typeof updates[key] === 'object') {
Object.assign(element.style, updates[key]);
} else {
element[key] = updates[key];
}
});
});
}
/**
* 延迟渲染大列表,避免阻塞 UI
* @param {HTMLElement} container - 容器元素
* @param {Array} items - 数据项数组
* @param {Function} renderItemFn - 渲染函数
* @param {number} batchSize - 每批渲染数量
* @returns {Promise} 完成渲染的 Promise
*
* @example
* await renderListAsync(container, largeArray, renderItem, 50);
*/
export function renderListAsync(container, items, renderItemFn, batchSize = 50) {
return new Promise((resolve) => {
if (!container || !items.length) {
resolve();
return;
}
container.innerHTML = '';
let index = 0;
function renderBatch() {
const fragment = document.createDocumentFragment();
const end = Math.min(index + batchSize, items.length);
for (let i = index; i < end; i++) {
const rendered = renderItemFn(items[i], i);
if (typeof rendered === 'string') {
const temp = document.createElement('div');
temp.innerHTML = rendered;
while (temp.firstChild) {
fragment.appendChild(temp.firstChild);
}
} else if (rendered instanceof HTMLElement) {
fragment.appendChild(rendered);
}
}
container.appendChild(fragment);
index = end;
if (index < items.length) {
requestAnimationFrame(renderBatch);
} else {
resolve();
}
}
requestAnimationFrame(renderBatch);
});
}
/**
* 虚拟滚动渲染(仅渲染可见区域)
* @param {Object} config - 配置对象
* @param {HTMLElement} config.container - 容器元素
* @param {Array} config.items - 数据项数组
* @param {Function} config.renderItemFn - 渲染函数
* @param {number} config.itemHeight - 每项高度(像素)
* @param {number} [config.overscan=5] - 额外渲染的项数(上下各)
* @returns {Object} 包含 update 和 destroy 方法的对象
*/
export function createVirtualScroll({ container, items, renderItemFn, itemHeight, overscan = 5 }) {
if (!container) return { update: () => {}, destroy: () => {} };
const totalHeight = items.length * itemHeight;
const viewportHeight = container.clientHeight;
// 创建占位容器
const placeholder = document.createElement('div');
placeholder.style.height = `${totalHeight}px`;
placeholder.style.position = 'relative';
const content = document.createElement('div');
content.style.position = 'absolute';
content.style.top = '0';
content.style.width = '100%';
placeholder.appendChild(content);
container.innerHTML = '';
container.appendChild(placeholder);
function render() {
const scrollTop = container.scrollTop;
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length,
Math.ceil((scrollTop + viewportHeight) / itemHeight) + overscan
);
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex; i++) {
const element = renderItemFn(items[i], i);
if (typeof element === 'string') {
const temp = document.createElement('div');
temp.innerHTML = element;
while (temp.firstChild) {
fragment.appendChild(temp.firstChild);
}
} else if (element instanceof HTMLElement) {
fragment.appendChild(element);
}
}
content.style.top = `${startIndex * itemHeight}px`;
content.innerHTML = '';
content.appendChild(fragment);
}
const handleScroll = () => requestAnimationFrame(render);
container.addEventListener('scroll', handleScroll);
// 初始渲染
render();
return {
update: (newItems) => {
items = newItems;
placeholder.style.height = `${newItems.length * itemHeight}px`;
render();
},
destroy: () => {
container.removeEventListener('scroll', handleScroll);
}
};
}
/**
* 防抖包装器(用于搜索、输入等)
* @param {Function} fn - 要防抖的函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {Function} 防抖后的函数
*/
export function debounce(fn, delay = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
/**
* 节流包装器用于滚动、resize 等)
* @param {Function} fn - 要节流的函数
* @param {number} limit - 限制时间(毫秒)
* @returns {Function} 节流后的函数
*/
export function throttle(fn, limit = 100) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}