macos-web/js/settings.js
2026-07-17 16:52:13 +08:00

445 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============ 系统设置 ============ */
'use strict';
const SettingsApp = {
id: 'settings', name: '系统设置', icon: 'assets/icons/settings.png',
w: 780, h: 560, minW: 620, minH: 420,
panes: [],
menus(win) {
return stdMenus(this, {
view: this.panes.slice(0, 6).map(p => ({ label: p.name, action: () => win?.appState?.go(p.id) })),
});
},
onArgs(args, win) { if (args.pane) win.appState?.go(args.pane); },
definePanes() {
const S = Sys.settings;
const row = (label, control, sub) => {
const r = el('div', { class: 'set-row' },
el('div', { class: 'set-label' }, el('span', { text: label }), sub ? el('small', { text: sub }) : null),
el('div', { class: 'set-ctl' }, control));
return r;
};
/* toggle完整 ariarole/aria-checked/tabindex/键盘)。
* io 可选:{ get(), set(v) } 用于嵌套或计算状态(如 notifAllow、Wi-Fi 网络)。 */
const toggle = (key, onChange, io) => {
const get = io ? io.get : () => S[key];
const set = io ? io.set : v => { S[key] = v; };
const sw = el('div', { class: 'switch' + (get() ? ' on' : ''), role: 'switch', 'aria-checked': String(!!get()), tabindex: '0' });
const flip = () => {
const v = !get(); set(v);
sw.classList.toggle('on', v); sw.setAttribute('aria-checked', String(v));
Sys.save(); Sys.applyAll(); onChange && onChange(v);
};
sw.addEventListener('click', flip);
sw.addEventListener('keydown', e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); flip(); } });
return sw;
};
const slider = (key, min, max, pct, onInput) => {
const r = el('input', { type: 'range', class: 'slider', min: String(min), max: String(max), value: String(S[key] * (pct ? 100 : 1)) });
r.style.setProperty('--fill', (r.value / (pct ? 1 : max)) * (pct ? 1 : 1) + '%');
const sync = () => r.style.setProperty('--fill', ((r.value - min) / (max - min)) * 100 + '%');
sync();
r.addEventListener('input', () => { S[key] = pct ? r.value / 100 : +r.value; sync(); onInput && onInput(+r.value); Sys.applyAll(); });
r.addEventListener('change', () => Sys.save());
return r;
};
const card = (...rows) => el('div', { class: 'set-card' }, ...rows.filter(Boolean));
const title = t => el('div', { class: 'set-section', text: t });
const note = t => el('div', { class: 'set-note', text: t });
const P = (id, name, ico, group, render) => this.panes.push({ id, name, ico, group, render });
P('wifi', 'Wi-Fi', '📶', '网络', box => {
box.append(card(row('Wi-Fi', toggle('wifi', on => { Sys._mbWifi.innerHTML = ''; Sys._mbWifi.append(Sys.wifiSvg()); }))));
box.append(title('已知网络'));
const list = card();
['家庭网络 5G', 'CoffeeShop_Guest', 'Office-2.4G'].forEach((n, i) => {
const connected = () => S.wifi && S.wifiNetwork === n;
const btn = el('button', { class: 'btn', text: connected() ? '已连接' : '连接', disabled: !S.wifi || connected() });
btn.addEventListener('click', () => {
S.wifiNetwork = n; Sys.save();
$$('.set-card .btn', list).forEach(b => { b.textContent = '连接'; b.disabled = false; });
btn.textContent = '已连接'; btn.disabled = true;
});
list.append(row(n, btn, i === 0 ? 'WPA2 个人级' : '开放'));
});
box.append(list, note(S.wifi ? '网络连接为模拟数据;连接状态会保存在本机并在重新打开后恢复。' : 'Wi-Fi 已关闭,打开后可加入网络。'));
});
P('bluetooth', '蓝牙', '🔵', '网络', box => {
box.append(card(row('蓝牙', toggle('bluetooth'))));
box.append(title('附近的设备'));
const c = card();
[['AirPods Pro', '耳机'], ['Magic Keyboard', '键盘'], ['MX Master 3S', '鼠标']].forEach(([n, t]) => {
const connected = () => S.bluetooth && S.btDevices[n] === true;
const b = el('button', { class: 'btn', text: connected() ? '已连接' : '连接', disabled: !S.bluetooth || connected() });
b.addEventListener('click', () => {
if (!S.bluetooth) return;
const now = !(S.btDevices[n] === true);
S.btDevices[n] = now; Sys.save();
b.textContent = now ? '已连接' : '连接';
});
c.append(row(n, b, t));
});
box.append(c, note('蓝牙设备为模拟数据;连接状态会保存在本机并在重新打开后恢复。'));
});
P('network', '网络', '🌐', '网络', box => {
box.append(card(
row('状态', el('span', { class: S.wifi ? 'set-ok' : 'set-bad', text: S.wifi ? '● 已连接Wi-Fi' : '● 未连接' })),
row('IP 地址', el('span', { text: '192.168.1.23' })),
row('路由器', el('span', { text: '192.168.1.1' })),
row('DNS', el('span', { text: '223.5.5.5, 8.8.8.8' }))));
box.append(note('网络信息为本地模拟数据。'));
});
P('vpn', 'VPN', '🛡', '网络', box => {
box.append(card(row('VPN', toggle('vpn'), S.vpn ? '已连接到 公司内网' : '未连接')));
box.append(title('配置'));
box.append(card(row('公司内网', el('span', { text: 'IKEv2 · vpn.example.com' }))));
box.append(note('VPN 为模拟项:开关状态会保存,但不产生真实网络连接。'));
});
P('notifications', '通知', '🔔', '系统', box => {
box.append(card(row('允许通知', toggle('notificationsEnabled'), '关闭后所有应用不再产生横幅、通知中心记录或 Dock 徽标')));
box.append(title('应用通知'));
const c = card();
['mail', 'messages', 'calendar', 'reminders', 'weather', 'appstore', 'clock'].forEach(id => {
const app = Apps.get(id); if (!app) return;
const io = { get: () => S.notifAllow[id] !== false, set: v => { S.notifAllow[id] = v; } };
c.append(row(app.name, toggle(null, null, io), '横幅与通知中心'));
});
box.append(c);
});
P('sound', '声音', '🔊', '系统', box => {
box.append(card(
row('输出音量', slider('volume', 0, 100, true, () => Sys.applyVolume())),
row('静音', toggle('muted', () => Sys.applyVolume()))));
box.append(title('提示音'));
const c = card();
['玻璃', '水滴', '风铃'].forEach((n, i) => {
const key = 'alertSound';
if (!S[key]) S[key] = '玻璃';
const r = el('input', { type: 'radio', name: 'alert-sound' });
r.checked = S[key] === n;
r.addEventListener('change', () => { S[key] = n; Sys.save(); });
c.append(row(n, r));
});
box.append(c);
});
P('focus', '专注模式', '🌙', '系统', box => {
box.append(card(row('勿扰模式', toggle('focus'), '打开后暂停大多数通知横幅')));
box.append(note('打开专注模式时,通知仍会记录在通知中心,但不会弹出横幅打扰你。'));
});
P('about', '关于本机', '💻', '通用', box => {
const head = el('div', { class: 'about-head' },
iconImg('assets/icons/settings.png', 'about-icon'),
el('div', null,
el('div', { class: 'about-name', text: S.computerName }),
el('div', { class: 'about-sub', text: 'macOS 网页版 1.0 (Sonoma 风格)' })));
box.append(head, card(
row('芯片', el('span', { text: 'Apple M4 Web' })),
row('内存', el('span', { text: '16 GB' })),
row('启动磁盘', el('span', { text: 'Macintosh HD虚拟' })),
row('序列号', el('span', { text: 'C02WEB2026MAC' }))));
const upd = el('button', { class: 'btn', text: '软件更新…' });
upd.addEventListener('click', () => this.panes.find(p => p.id === 'update') && win_go('update'));
box.append(el('div', { style: { marginTop: '12px' } }, upd));
});
P('update', '软件更新', '🔄', '通用', box => {
const st = el('div', { class: 'empty-state', style: { height: '200px' } }, el('div', { class: 'es-icon', text: '⏳' }), el('div', { text: '正在检查更新…' }));
box.append(st);
setTimeout(() => {
if (!document.body.contains(st)) return;
st.innerHTML = '';
st.append(el('div', { class: 'es-icon', text: '✅' }), el('div', { text: '你的 Mac 已是最新版本' }), el('div', { class: 'set-note', text: 'macOS 网页版 1.0' }));
}, 1600);
});
P('storage', '储存空间', '💾', '通用', box => {
const fsSize = FS.size('/');
const total = 64 * 1024 * 1024;
const used = 12 * 1024 * 1024 + fsSize * 8; // 模拟系统占用 + 用户数据
const pct = Math.min(96, Math.round(used / total * 100));
const bar = el('div', { class: 'storage-bar' },
el('div', { class: 'storage-seg sys', style: { width: pct - 8 + '%' } }),
el('div', { class: 'storage-seg doc', style: { width: '8%' } }));
box.append(card(row('Macintosh HD', el('span', { text: `${fmtBytes(used)} / ${fmtBytes(total)} 已使用` }))), bar,
card(
row('系统数据', el('span', { text: fmtBytes(used - fsSize * 8) })),
row('文稿与文件', el('span', { text: fmtBytes(fsSize * 8) })),
row('可用', el('span', { text: fmtBytes(total - used) }))));
});
P('datetime', '日期与时间', '🕐', '通用', box => {
box.append(card(
row('当前时间', el('span', { text: new Date().toLocaleString('zh-CN') })),
row('24 小时制', toggle('h24', () => Sys.tickClock())),
row('时区', (() => {
// 保存到 settings.timezone并实时影响菜单栏时钟、锁屏时钟与相关显示
const ZONES = [
['local', '跟随本机(浏览器时区)'],
['Asia/Shanghai', '亚洲/上海'], ['Asia/Tokyo', '亚洲/东京'], ['Asia/Singapore', '亚洲/新加坡'],
['Asia/Dubai', '亚洲/迪拜'], ['Europe/London', '欧洲/伦敦'], ['Europe/Paris', '欧洲/巴黎'],
['America/New_York', '美洲/纽约'], ['America/Los_Angeles', '美洲/洛杉矶'], ['Australia/Sydney', '澳洲/悉尼'],
];
const s = el('select', { class: 'text-input' }, ...ZONES.map(([v, label]) => el('option', { text: label, value: v })));
s.value = S.timezone || 'local';
s.addEventListener('change', () => {
S.timezone = s.value; Sys.save();
Sys.tickClock(); Sys.tickLockClock();
});
return s;
})())));
box.append(note('菜单栏与锁屏时钟会立即应用 24 小时制设置。'));
});
P('language', '语言与地区', '🌍', '通用', box => {
box.append(card(
row('首选语言', el('span', { text: '简体中文' })),
row('地区', (() => { const s = el('select', { class: 'text-input' }, ...['中国', '中国香港', '中国台湾', '新加坡'].map(z => el('option', { text: z, selected: z === S.region }))); s.addEventListener('change', () => { S.region = s.value; Sys.save(); }); return s; })(),
row('每周第一天', (() => { const s = el('select', { class: 'text-input' }, el('option', { text: '星期一', value: '1', selected: S.firstDayMonday }), el('option', { text: '星期日', value: '0', selected: !S.firstDayMonday })); s.addEventListener('change', () => { S.firstDayMonday = s.value === '1'; Sys.save(); }); return s; })()))));
});
P('loginitems', '登录项', '🔑', '通用', box => {
box.append(title('登录时打开'));
const c = card();
['mail', 'messages', 'notes', 'calendar', 'music'].forEach(id => {
const app = Apps.get(id); if (!app) return;
const io = { get: () => S.loginItems[id] === true, set: v => { S.loginItems[id] = v; } };
c.append(row(app.name, toggle(null, null, io)));
});
box.append(c, note('勾选的 App 会在解锁进入桌面时自动打开。'));
});
P('sharing', '共享', '📡', '通用', box => {
const inp = el('input', { class: 'text-input', value: S.computerName });
inp.addEventListener('change', () => { S.computerName = inp.value.trim() || 'MacBook Pro'; Sys.save(); });
box.append(card(row('电脑名称', inp)), card(row('屏幕共享', toggle('_shareScreen')), row('文件共享', toggle('_shareFile'))));
box.append(note('共享开关为模拟项:状态会保存,但不会开启真实共享服务。'));
});
P('transfer', '传输或还原', '♻️', '通用', box => {
const b1 = el('button', { class: 'btn', text: '还原所有设置…' });
b1.addEventListener('click', () => Sys.resetAll());
const b2 = el('button', { class: 'btn danger', text: '抹掉所有内容和设置…' });
b2.addEventListener('click', async () => {
if (await UI.confirm('抹掉所有内容和设置?', '将删除此浏览器中保存的全部 macOS 网页版数据并恢复到初始状态。', { ok: '抹掉', danger: true })) { Store.clearAll(); location.reload(); }
});
box.append(card(row('还原', b1, '清除所有设置与数据并恢复默认值')), card(row('抹掉', b2, '删除全部本地数据')));
});
P('appearance', '外观', '🎨', '系统外观', box => {
const seg = el('div', { class: 'appearance-cards' });
[['light', '浅色', '☀️'], ['dark', '深色', '🌙'], ['auto', '自动', '🌓']].forEach(([v, n, i]) => {
const c = el('div', { class: 'ap-card' + (S.appearance === v ? ' sel' : '') },
el('div', { class: 'ap-preview ap-' + v, text: i }),
el('div', { class: 'ap-name', text: n }));
c.addEventListener('click', () => { S.appearance = v; Sys.save(); Sys.applyAppearance(); $$('.ap-card', seg).forEach(x => x.classList.remove('sel')); c.classList.add('sel'); });
seg.append(c);
});
box.append(seg);
box.append(title('强调色'));
const colors = el('div', { class: 'accent-row' });
['#0a84ff', '#bf5af2', '#ff453a', '#ff9f0a', '#32d74b', '#64d2ff'].forEach(col => {
const dot = el('button', { class: 'accent-dot' + (S.accent === col ? ' sel' : ''), style: { background: col }, 'aria-label': '强调色 ' + col });
dot.addEventListener('click', () => { S.accent = col; Sys.save(); Sys.applyAll(); $$('.accent-dot', colors).forEach(x => x.classList.remove('sel')); dot.classList.add('sel'); });
colors.append(dot);
});
box.append(colors);
});
P('accessibility', '辅助功能', '♿', '系统外观', box => {
box.append(card(
row('减少透明度', toggle('reduceTransparency'), '将磨砂玻璃替换为实色背景'),
row('增强对比度', toggle('increaseContrast'), '提高边框与分隔线对比度'),
row('减少动态效果', toggle('reduceMotion'), '显著削弱窗口与界面动画')));
});
P('controlcenter', '控制中心', '🎛', '系统外观', box => {
box.append(card(
row('Wi-Fi 模块', el('span', { text: '始终显示' })),
row('专注模式模块', el('span', { text: '始终显示' })),
row('亮度与声音', el('span', { text: '始终显示' }))));
box.append(note('模块布局为固定展示(模拟);通过菜单栏右上角图标随时打开控制中心。'));
});
P('siri', 'Siri 与聚焦', '🔍', '系统外观', box => {
box.append(card(
row('在聚焦中搜索应用', toggle('siriApps')),
row('在聚焦中搜索文件', toggle('siriFiles')),
row('在聚焦中搜索设置', toggle('siriSettings'))));
box.append(note('快捷键Ctrl + 空格 或 ⌘ + 空格。支持输入算式即时计算。'));
});
P('privacy', '隐私与安全性', '🔒', '系统外观', box => {
box.append(card(row('防火墙', toggle('_firewall'), '阻止未授权的传入连接(模拟,不影响真实网络)')));
box.append(title('App 权限'));
box.append(card(
row('邮件 — 通知', el('span', { class: 'set-ok', text: '已允许' })),
row('信息 — 通知', el('span', { class: 'set-ok', text: '已允许' })),
row('天气 — 位置', el('span', { text: '使用期间允许' }))));
});
P('dock', '桌面与程序坞', '🖥', '系统外观', box => {
const sizeRow = row('大小', (() => {
const r = el('input', { type: 'range', class: 'slider', min: '36', max: '72', value: String(S.dockSize) });
const sync = () => r.style.setProperty('--fill', ((r.value - 36) / 36 * 100) + '%');
sync();
r.addEventListener('input', () => { S.dockSize = +r.value; sync(); Sys.save(); Sys.renderDock(); });
return r;
})());
const magRow = row('放大', (() => {
const wrap = el('div', { style: { display: 'flex', alignItems: 'center', gap: '10px', flex: '1' } });
const r = el('input', { type: 'range', class: 'slider', min: '10', max: '25', value: String(Math.round(S.dockMagnifyLevel * 10)) });
const sync = () => r.style.setProperty('--fill', ((r.value - 10) / 15 * 100) + '%');
sync();
r.addEventListener('input', () => { S.dockMagnifyLevel = r.value / 10; sync(); Sys.save(); });
wrap.append(toggle('dockMagnify'), r);
return wrap;
})());
box.append(card(sizeRow, magRow));
box.append(title('置于屏幕上的位置'));
const seg = el('div', { class: 'segmented' }, ...[['left', '左边'], ['bottom', '底部'], ['right', '右边']].map(([v, n]) => {
const b = el('button', { text: n, class: S.dockPosition === v ? 'on' : '' });
b.addEventListener('click', () => { S.dockPosition = v; Sys.save(); Sys.layoutDock(); $$('button', seg).forEach(x => x.classList.remove('on')); b.classList.add('on'); });
return b;
}));
box.append(el('div', { style: { marginBottom: '12px' } }, seg));
box.append(card(row('自动隐藏和显示程序坞', toggle('dockAutohide', () => Sys.layoutDock()))));
});
P('displays', '显示器', '🖵', '系统外观', box => {
box.append(card(
row('亮度', slider('brightness', 20, 100, true, () => Sys.applyBrightness())),
row('夜览', toggle('nightShift', () => Sys.applyNightShift()), '日落后自动将屏幕调暖'),
row('夜览强度', slider('nightShiftStrength', 0, 100, true, () => Sys.applyNightShift()))));
box.append(card(row('分辨率', el('span', { text: `${innerWidth} × ${innerHeight}` })), row('刷新率', el('span', { text: '60 Hz' }))));
});
P('screensaver', '屏幕保护程序', '🌌', '系统外观', box => {
const seg = el('div', { class: 'segmented' }, ...[['off', '关闭'], ['clock', '数字时钟'], ['slides', '壁纸幻灯片']].map(([v, n]) => {
const b = el('button', { text: n, class: S.screensaverType === v ? 'on' : '' });
b.addEventListener('click', () => { S.screensaverType = v; Sys.save(); Sys.resetIdle(); $$('button', seg).forEach(x => x.classList.remove('on')); b.classList.add('on'); });
return b;
}));
box.append(el('div', { style: { marginBottom: '12px' } }, seg));
const sel = el('select', { class: 'text-input' }, ...[1, 3, 5].map(m => el('option', { value: String(m), text: `${m} 分钟`, selected: S.screensaverDelay === m })));
sel.addEventListener('change', () => { S.screensaverDelay = +sel.value; Sys.save(); Sys.resetIdle(); });
box.append(card(row('闲置多久后启动', sel)));
const pv = el('button', { class: 'btn', text: '预览屏保' });
pv.addEventListener('click', () => { if (S.screensaverType === 'off') UI.alert('屏保已关闭', '请先选择一种屏保类型。', this.icon); else Sys.showScreensaver(); });
box.append(el('div', { style: { marginTop: '10px' } }, pv));
});
P('wallpaper', '墙纸', '🏞', '系统外观', box => {
const grid = el('div', { class: 'wall-grid' });
WALLPAPERS.forEach(w => {
const cell = el('div', { class: 'wall-cell' + (S.wallpaper === w.id ? ' sel' : '') },
iconImg(w.src, '', w.name), el('div', { class: 'wall-name', text: w.name }));
cell.addEventListener('click', () => { S.wallpaper = w.id; Sys.save(); Sys.applyWallpaper(); $$('.wall-cell', grid).forEach(x => x.classList.remove('sel')); cell.classList.add('sel'); });
grid.append(cell);
});
box.append(grid, note('锁屏与桌面共享同一墙纸;深色模式下部分墙纸会自动切换为暗色版本。'));
});
P('lockscreen', '锁定屏幕', '🔐', '安全', box => {
const sw = el('div', { class: 'switch' + (S.passwordEnabled ? ' on' : ''), role: 'switch', 'aria-checked': String(!!S.passwordEnabled), tabindex: '0' });
const flip = async () => {
if (!S.passwordEnabled) {
const p1 = await UI.prompt('设置密码', '请输入锁屏密码(本地保存,仅用于演示):', '');
if (p1 == null) return;
const p2 = await UI.prompt('确认密码', '请再次输入密码:', '');
if (p1 !== p2) { UI.alert('两次输入不一致', '请重试。', this.icon); return; }
S.password = p1; S.passwordEnabled = true;
} else { S.passwordEnabled = false; S.password = ''; }
sw.classList.toggle('on', S.passwordEnabled);
sw.setAttribute('aria-checked', String(S.passwordEnabled));
Sys.save();
UI.alert('已更新', S.passwordEnabled ? '锁屏密码已启用。' : '锁屏密码已关闭。', this.icon);
};
sw.addEventListener('click', flip);
sw.addEventListener('keydown', e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); flip(); } });
box.append(card(row('锁屏密码', sw, S.passwordEnabled ? '已启用' : '未启用')));
box.append(note('启用后,解锁时需要输入密码。'));
});
P('touchid', '触控 ID 与密码', '👆', '安全', box => {
box.append(el('div', { class: 'empty-state', style: { height: '140px' } },
el('div', { class: 'es-icon', text: '👆' }),
el('div', { text: '此 Mac 不支持触控 ID' }),
el('div', { class: 'set-note', text: '可以在「锁定屏幕」中管理锁屏密码。' })));
const b = el('button', { class: 'btn', text: '前往锁定屏幕设置' });
b.addEventListener('click', () => win_go('lockscreen'));
box.append(el('div', { style: { textAlign: 'center' } }, b));
});
P('users', '用户与群组', '👤', '安全', box => {
const avRow = el('div', { class: 'avatar-row' });
['assets/icons/avatar.svg', 'assets/icons/bear.png', 'assets/icons/facetime.png'].forEach(src => {
const im = iconImg(src, 'avatar-choice' + (S.avatar === src ? ' sel' : ''));
im.addEventListener('click', () => { S.avatar = src; Sys.save(); $$('.avatar-choice', avRow).forEach(x => x.classList.remove('sel')); im.classList.add('sel'); });
avRow.append(im);
});
const nameIn = el('input', { class: 'text-input', value: S.userName });
nameIn.addEventListener('change', () => { S.userName = nameIn.value.trim() || '客人用户'; Sys.save(); });
box.append(card(row('当前用户', el('div', { style: { display: 'flex', gap: '10px', alignItems: 'center' } }, avRow)), row('用户名', nameIn)));
box.append(note('头像与用户名会显示在锁屏界面。'));
});
P('keyboard', '键盘', '⌨️', '输入', box => {
box.append(card(
row('按键重复速度', slider('kbRepeat', 1, 10, false)),
row('重复前延迟', slider('kbDelay', 1, 10, false))));
box.append(card(row('输入法', el('span', { text: '简体拼音' })), row('键盘亮度', el('span', { text: '自动' }))));
box.append(note('键盘参数为模拟项:值会保存在本模拟器中,不会影响真实系统。'));
});
P('mouse', '鼠标', '🖱', '输入', box => {
box.append(card(
row('跟踪速度', slider('mouseSpeed', 1, 10, false)),
row('自然滚动', toggle('_naturalScroll'))));
box.append(note('鼠标参数为模拟项:值会保存在本模拟器中,不会影响真实系统。'));
});
P('trackpad', '触控板', '⬜', '输入', box => {
box.append(card(
row('轻点来点按', toggle('_tapClick')),
row('双指滚动', toggle('_twoFingerScroll')),
row('三指拖移', toggle('_threeDrag'))));
box.append(note('触控板手势为模拟展示,不会影响真实系统。'));
});
P('printers', '打印机与扫描仪', '🖨', '输入', box => {
box.append(el('div', { class: 'empty-state', style: { height: '160px' } },
el('div', { class: 'es-icon', text: '🖨' }), el('div', { text: '没有打印机' })));
const b = el('button', { class: 'btn', text: '添加打印机…' });
b.addEventListener('click', () => UI.alert('未找到打印机', '网络上没有可用的打印机。', this.icon));
box.append(el('div', { style: { textAlign: 'center' } }, b));
});
P('safari', 'Safari 浏览器', '🧭', 'App', box => {
const sel = el('select', { class: 'text-input' },
el('option', { value: 'bing', text: 'Bing', selected: S.searchEngine === 'bing' }),
el('option', { value: 'baidu', text: '百度', selected: S.searchEngine === 'baidu' }));
sel.addEventListener('change', () => { S.searchEngine = sel.value; Sys.save(); });
box.append(card(row('搜索引擎', sel)));
box.append(note('离线环境下,外部网页会显示设计好的回退页,可选择在系统浏览器新标签页打开。'));
});
function win_go(id) { WM.windowsForApp('settings')[0]?.appState?.go(id); }
},
render(win, args) {
if (!this.panes.length) this.definePanes();
const st = win.appState = { pane: null, q: '' };
win.body.classList.add('settings-body');
const search = el('input', { class: 'text-input set-search', type: 'search', placeholder: '搜索' });
const nav = el('div', { class: 'set-nav' });
const side = el('div', { class: 'set-sidebar' }, el('div', { style: { padding: '8px 8px 4px' } }, search), nav);
const content = el('div', { class: 'set-content' });
win.body.append(side, content);
const groups = ['网络', '系统', '通用', '系统外观', '安全', '输入', 'App'];
const renderNav = () => {
nav.innerHTML = '';
for (const g of groups) {
const items = this.panes.filter(p => p.group === g && (!st.q || p.name.toLowerCase().includes(st.q.toLowerCase())));
if (!items.length) continue;
nav.append(el('div', { class: 'fb-side-title', text: g }));
for (const p of items) {
const rowEl = el('div', { class: 'fb-side-item set-nav-item' + (st.pane === p.id ? ' sel' : '') },
el('span', { class: 'fb-side-ico', text: p.ico }), el('span', { text: p.name }));
rowEl.addEventListener('click', () => st.go(p.id));
nav.append(rowEl);
}
}
};
st.go = id => {
const p = this.panes.find(p => p.id === id) || this.panes[0];
st.pane = p.id;
renderNav();
content.innerHTML = '';
content.scrollTop = 0;
WM.setTitle(win, p.name + ' — 系统设置');
content.append(el('div', { class: 'set-pane-title' }, el('span', { text: p.ico, style: { fontSize: '20px' } }), el('span', { text: p.name })));
try { p.render(content); } catch (e) { console.error('[settings]', e); content.append(el('div', { class: 'empty-state', text: '此设置页加载失败' })); }
};
search.addEventListener('input', debounce(() => { st.q = search.value.trim(); renderNav(); }, 150));
renderNav();
st.go(args.pane || 'appearance');
}
};
Apps.register(SettingsApp);
window.SettingsApp = SettingsApp;