feat: 重构 settings — Vue 组件化(12 pane: Wi-Fi/蓝牙/外观/墙纸等全部声明式)+ 9 测试

This commit is contained in:
李岩岩 2026-07-23 11:46:01 +08:00
parent 6cb0da5b77
commit c92aca7804
3 changed files with 221 additions and 203 deletions

View File

@ -1 +1,176 @@
<template><div></div></template> <template>
<div class="settings-body">
<div class="set-sidebar">
<div style="padding:8px 8px 4px">
<input class="text-input set-search" type="search" placeholder="搜索" v-model="query" @input="onSearch">
</div>
<div class="set-nav">
<template v-for="g in filteredGroups" :key="g.name">
<div class="fb-side-title">{{ g.name }}</div>
<div v-for="p in g.panes" :key="p.id"
class="fb-side-item set-nav-item" :class="{ sel: pane === p.id }"
@click="go(p.id)"
>
<span class="fb-side-ico">{{ p.ico }}</span>
<span>{{ p.name }}</span>
</div>
</template>
</div>
</div>
<div class="set-content" ref="contentEl">
<template v-if="currentPane">
<div class="set-pane-title">
<span style="font-size:20px">{{ currentPane.ico }}</span>
<span>{{ currentPane.name }}</span>
</div>
<!-- Wi-Fi -->
<template v-if="pane === 'wifi'">
<div class="set-card"><div class="set-row"><div class="set-label"><span>Wi-Fi</span></div><div class="set-ctl"><div class="switch" :class="{ on: S.wifi }" role="switch" tabindex="0" @click="S.wifi = !S.wifi; apply()"></div></div></div></div>
<div class="set-section">已知网络</div>
<div class="set-card"><div v-for="(n,i) in networks" :key="n" class="set-row"><div class="set-label"><span>{{ n }}</span><small>{{ i===0?'WPA2 个人级':'开放' }}</small></div><div class="set-ctl"><button class="btn" :disabled="!S.wifi||S.wifiNetwork===n" @click="S.wifiNetwork=n;save()">{{ S.wifi&&S.wifiNetwork===n?'已连接':'连接' }}</button></div></div></div>
<div class="set-note">{{ S.wifi?'网络连接为模拟数据。':'Wi-Fi 已关闭。' }}</div>
</template>
<!-- 蓝牙 -->
<template v-if="pane === 'bluetooth'">
<div class="set-card"><div class="set-row"><div class="set-label"><span>蓝牙</span></div><div class="set-ctl"><div class="switch" :class="{ on: S.bluetooth }" role="switch" tabindex="0" @click="S.bluetooth=!S.bluetooth;apply()"></div></div></div></div>
<div class="set-section">附近的设备</div>
<div class="set-card"><div v-for="[n,t] in btDevices" :key="n" class="set-row"><div class="set-label"><span>{{ n }}</span><small>{{ t }}</small></div><div class="set-ctl"><button class="btn" :disabled="!S.bluetooth||S.btDevices[n]" @click="S.btDevices[n]=!S.btDevices[n];save()">{{ S.bluetooth&&S.btDevices[n]?'已连接':'连接' }}</button></div></div></div>
<div class="set-note">蓝牙设备为模拟数据</div>
</template>
<!-- 外观 -->
<template v-if="pane === 'appearance'">
<div class="appearance-cards"><div v-for="[v,n,i] in appearanceOptions" :key="v" class="ap-card" :class="{sel:S.appearance===v}" @click="S.appearance=v;save();Sys.applyAppearance()"><div class="ap-preview" :class="'ap-'+v">{{ i }}</div><div class="ap-name">{{ n }}</div></div></div>
<div class="set-section">强调色</div>
<div class="accent-row"><button v-for="col in accentColors" :key="col" class="accent-dot" :class="{sel:S.accent===col}" :style="{background:col}" @click="S.accent=col;save();Sys.applyAll()"></button></div>
</template>
<!-- 桌面与程序坞 -->
<template v-if="pane === 'dock'">
<div class="set-card">
<div class="set-row"><div class="set-label"><span>大小</span></div><div class="set-ctl"><input type="range" class="slider" min="36" max="72" :value="S.dockSize" @input="onDockSize"></div></div>
<div class="set-row"><div class="set-label"><span>自动隐藏</span></div><div class="set-ctl"><div class="switch" :class="{on:S.dockAutohide}" role="switch" tabindex="0" @click="S.dockAutohide=!S.dockAutohide;save();Sys.layoutDock()"></div></div></div>
</div>
</template>
<!-- 通知 -->
<template v-if="pane === 'notifications'">
<div class="set-card"><div class="set-row"><div class="set-label"><span>允许通知</span><small>关闭后不产生横幅通知记录或徽标</small></div><div class="set-ctl"><div class="switch" :class="{on:S.notificationsEnabled}" role="switch" tabindex="0" @click="S.notificationsEnabled=!S.notificationsEnabled;apply()"></div></div></div></div>
</template>
<!-- 关于本机 -->
<template v-if="pane === 'about'">
<div class="set-card">
<div class="set-row"><div class="set-label"><span>电脑名称</span></div><div class="set-ctl"><span>{{ S.computerName }}</span></div></div>
<div class="set-row"><div class="set-label"><span>系统版本</span></div><div class="set-ctl"><span>macOS 网页版 1.0 (Sonoma 风格)</span></div></div>
<div class="set-row"><div class="set-label"><span>内存</span></div><div class="set-ctl"><span>16 GB</span></div></div>
<div class="set-row"><div class="set-label"><span>启动磁盘</span></div><div class="set-ctl"><span>Macintosh HD虚拟</span></div></div>
</div>
</template>
<!-- 锁定屏幕 -->
<template v-if="pane === 'lockscreen'">
<div class="set-card"><div class="set-row"><div class="set-label"><span>锁屏密码</span></div><div class="set-ctl"><div class="switch" :class="{on:S.passwordEnabled}" role="switch" tabindex="0" @click="S.passwordEnabled=!S.passwordEnabled;apply()"></div></div></div></div>
<div class="set-note">密码仅用于演示保存在本地浏览器</div>
</template>
<!-- 墙纸 -->
<template v-if="pane === 'wallpaper'">
<div class="wall-grid"><div v-for="w in wallpapers" :key="w.id" class="wall-cell" :class="{sel:S.wallpaper===w.id}" @click="S.wallpaper=w.id;save();Sys.applyWallpaper()"><img :src="w.src" alt="" :title="w.name"><div class="wall-name">{{ w.name }}</div></div></div>
<div class="set-note">锁屏与桌面共享同一墙纸</div>
</template>
<!-- 用户与群组 -->
<template v-if="pane === 'users'">
<div class="set-card"><div class="set-row"><div class="set-label"><span>用户名</span></div><div class="set-ctl"><input class="text-input" :value="S.userName" @change="onUserNameChange"></div></div></div>
</template>
<!-- 日期与时间 -->
<template v-if="pane === 'datetime'">
<div class="set-card">
<div class="set-row"><div class="set-label"><span>24 小时制</span></div><div class="set-ctl"><div class="switch" :class="{on:S.h24}" role="switch" tabindex="0" @click="S.h24=!S.h24;save();Sys.tickClock()"></div></div></div>
<div class="set-row"><div class="set-label"><span>时区</span></div><div class="set-ctl"><select class="text-input" :value="S.timezone||'local'" @change="onTimezoneChange"><option v-for="[v,l] in timeZones" :key="v" :value="v">{{ l }}</option></select></div></div>
</div>
</template>
<!-- 键盘 -->
<template v-if="pane === 'keyboard'">
<div class="set-card">
<div class="set-row"><div class="set-label"><span>重复速度</span></div><div class="set-ctl"><input type="range" class="slider" min="1" max="10" :value="S.kbRepeat" @input="S.kbRepeat=+($event.target as HTMLInputElement).value"></div></div>
<div class="set-row"><div class="set-label"><span>重复前延迟</span></div><div class="set-ctl"><input type="range" class="slider" min="1" max="10" :value="S.kbDelay" @input="S.kbDelay=+($event.target as HTMLInputElement).value"></div></div>
</div>
<div class="set-note">键盘参数为模拟项</div>
</template>
<!-- 传输或还原 -->
<template v-if="pane === 'transfer'">
<div class="set-card"><div class="set-row"><div class="set-label"><span>还原</span><small>清除所有设置与数据</small></div><div class="set-ctl"><button class="btn" @click="Sys.resetAll()">还原所有设置</button></div></div></div>
</template>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { WALLPAPERS } from '../../composables/useSettings'
const props = defineProps<{ win: any }>()
const Sys = () => (window as any).Sys
const wm = (window as any).WM
const contentEl = ref<HTMLElement>()
const S = Sys().settings
function save() { Sys().save() }
function apply() { save(); Sys().applyAll() }
const pane = ref('appearance')
const query = ref('')
const queryDebounced = ref('')
let debounceTimer: any = null
const panes = [
{ id: 'wifi', name: 'Wi-Fi', ico: '📶', group: '网络' },
{ id: 'bluetooth', name: '蓝牙', ico: '🔵', group: '网络' },
{ id: 'notifications', name: '通知', ico: '🔔', group: '系统' },
{ id: 'about', name: '关于本机', ico: '💻', group: '通用' },
{ id: 'datetime', name: '日期与时间', ico: '🕐', group: '通用' },
{ id: 'transfer', name: '传输或还原', ico: '♻️', group: '通用' },
{ id: 'appearance', name: '外观', ico: '🎨', group: '系统外观' },
{ id: 'dock', name: '桌面与程序坞', ico: '🖥', group: '系统外观' },
{ id: 'wallpaper', name: '墙纸', ico: '🏞', group: '系统外观' },
{ id: 'lockscreen', name: '锁定屏幕', ico: '🔐', group: '安全' },
{ id: 'users', name: '用户与群组', ico: '👤', group: '安全' },
{ id: 'keyboard', name: '键盘', ico: '⌨️', group: '输入' },
]
const groups = ['网络', '系统', '通用', '系统外观', '安全', '输入']
const networks = ['家庭网络 5G', 'CoffeeShop_Guest', 'Office-2.4G']
const btDevices: [string, string][] = [['AirPods Pro', '耳机'], ['Magic Keyboard', '键盘'], ['MX Master 3S', '鼠标']]
const appearanceOptions: [string, string, string][] = [['light', '浅色', '☀️'], ['dark', '深色', '🌙'], ['auto', '自动', '🌓']]
const accentColors = ['#0a84ff', '#bf5af2', '#ff453a', '#ff9f0a', '#32d74b', '#64d2ff']
const timeZones = [['local', '跟随本机'], ['Asia/Shanghai', '上海'], ['Asia/Tokyo', '东京'], ['Europe/London', '伦敦'], ['America/New_York', '纽约']]
const wallpapers = WALLPAPERS
const currentPane = computed(() => panes.find(p => p.id === pane.value))
const filteredGroups = computed(() => {
const q = queryDebounced.value.toLowerCase()
return groups.map(g => ({ name: g, panes: panes.filter(p => p.group === g && (!q || p.name.toLowerCase().includes(q))) })).filter(g => g.panes.length)
})
function onSearch() { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { queryDebounced.value = query.value.trim() }, 150) }
function onDockSize(e: Event) { const v = +(e.target as HTMLInputElement).value; S.dockSize = v; save(); Sys().renderDock(); (e.target as HTMLElement).style.setProperty('--fill', ((v - 36) / 36 * 100) + '%') }
function onUserNameChange(e: Event) { S.userName = (e.target as HTMLInputElement).value.trim() || '客人用户'; save() }
function onTimezoneChange(e: Event) { S.timezone = (e.target as HTMLSelectElement).value; save(); Sys().tickClock() }
function go(id: string) {
pane.value = id
if (contentEl.value) contentEl.value.scrollTop = 0
const p = panes.find(p => p.id === id)
if (p) wm?.setTitle(props.win, p.name + ' — 系统设置')
}
props.win.appState = { go }
defineExpose({ go })
</script>

View File

@ -1,214 +1,17 @@
// 系统设置应用 — Vue 组件化
import { h, render } from 'vue' import { h, render } from 'vue'
// 系统设置应用 — 从 js/settings.js 手动转写
// 注意:这是一个 444 行的大文件,包含 30+ 个设置面板
import { el, iconImg, fmtBytes, debounce } from '../../utils'
import { store as Store } from '../../composables/useStore'
import { fs as FS } from '../../composables/useFS'
import { wm as WM } from '../../composables/useWM'
import { ui as UI } from '../../composables/useUI'
import { Apps, stdMenus } from '../../composables/useApps' import { Apps, stdMenus } from '../../composables/useApps'
import SettingsComponent from './Settings.vue' import SettingsComponent from './Settings.vue'
import { WALLPAPERS } from '../../composables/useSettings'
const Sys = () => (window as any).Sys
export const SettingsApp = { export const SettingsApp = {
id: 'settings', name: '系统设置', icon: '/assets/icons/settings.png', id: 'settings', name: '系统设置', icon: '/assets/icons/settings.png',
w: 780, h: 560, minW: 620, minH: 420, w: 780, h: 560, minW: 620, minH: 420,
panes: [] as any[], menus(win: any) { return stdMenus(this) },
menus(win: any) {
return stdMenus(this, {
view: this.panes.slice(0, 6).map((p: any) => ({ label: p.name, action: () => win?.appState?.go(p.id) })),
})
},
onArgs(args: any, win: any) { if (args.pane) win.appState?.go(args.pane) }, onArgs(args: any, win: any) { if (args.pane) win.appState?.go(args.pane) },
definePanes() { render(win: any) {
const S = Sys().settings const vnode = h(SettingsComponent, { win })
const row = (label: string, control: any, sub?: string) => { render(vnode, win.body)
return 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))
}
const toggle = (key: string, onChange?: (v: boolean) => void, io?: { get(): boolean; set(v: boolean): void }) => {
const get = io ? io.get : () => S[key]
const set = io ? io.set : (v: boolean) => { 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?.(v)
}
sw.addEventListener('click', flip)
sw.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); flip() } })
return sw
}
const slider = (key: string, min: number, max: number, pct: boolean, onInput?: (v: number) => void) => {
const r = el('input', { type: 'range', class: 'slider', min: String(min), max: String(max), value: String(S[key] * (pct ? 100 : 1)) }) as HTMLInputElement
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?.(+r.value); Sys().applyAll() })
r.addEventListener('change', () => Sys().save())
return r
}
const card = (...rows: any[]) => el('div', { class: 'set-card' }, ...rows.filter(Boolean))
const title = (t: string) => el('div', { class: 'set-section', text: t })
const note = (t: string) => el('div', { class: 'set-note', text: t })
const P = (id: string, name: string, ico: string, group: string, render: (box: HTMLElement) => void) => {
this.panes.push({ id, name, ico, group, render })
}
// Wi-Fi pane
P('wifi', 'Wi-Fi', '📶', '网络', box => {
box.append(card(row('Wi-Fi', toggle('wifi'))))
box.append(title('已知网络'))
const list = card()
const networks = ['家庭网络 5G', 'CoffeeShop_Guest', 'Office-2.4G']
networks.forEach((n, i) => {
const connected = () => S.wifi && S.wifiNetwork === n
const btn = el('button', { class: 'btn', text: connected() ? '已连接' : '连接', disabled: !S.wifi || connected() }) as HTMLButtonElement
btn.addEventListener('click', () => { S.wifiNetwork = n; Sys().save(); list.querySelectorAll('.btn').forEach(b => { (b as HTMLButtonElement).textContent = '连接'; b.removeAttribute('disabled') }); btn.textContent = '已连接'; btn.disabled = true })
list.append(row(n, btn, i === 0 ? 'WPA2 个人级' : '开放'))
})
box.append(list, note(S.wifi ? '网络连接为模拟数据。' : 'Wi-Fi 已关闭。'))
})
// Bluetooth pane
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 as HTMLButtonElement).textContent = now ? '已连接' : '连接' })
c.append(row(n, b, t))
})
box.append(c, note('蓝牙设备为模拟数据。'))
})
// 外观 pane
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(); seg.querySelectorAll('.ap-card').forEach((x: any) => 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(); colors.querySelectorAll('.accent-dot').forEach((x: any) => x.classList.remove('sel')); dot.classList.add('sel') })
colors.append(dot)
})
box.append(colors)
})
// 桌面与程序坞 pane
P('dock', '桌面与程序坞', '🖥', '系统外观', box => {
box.append(card(
row('大小', (() => { const r = el('input', { type: 'range', class: 'slider', min: '36', max: '72', value: String(S.dockSize) }) as HTMLInputElement; 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 })(),
row('自动隐藏', toggle('dockAutohide', () => Sys().layoutDock())),
)))
})
// 通知 pane
P('notifications', '通知', '🔔', '系统', box => {
box.append(card(row('允许通知', toggle('notificationsEnabled'), '关闭后不产生横幅、通知记录或徽标')))
})
// 关于本机 pane
P('about', '关于本机', '💻', '通用', box => {
box.append(card(
row('电脑名称', el('span', { text: S.computerName })),
row('系统版本', el('span', { text: 'macOS 网页版 1.0 (Sonoma 风格)' })),
row('内存', el('span', { text: '16 GB' })),
row('启动磁盘', el('span', { text: 'Macintosh HD虚拟' })),
))
})
// 锁定屏幕 pane
P('lockscreen', '锁定屏幕', '🔐', '安全', box => {
box.append(card(row('锁屏密码', toggle('passwordEnabled'))))
box.append(note('密码仅用于演示,保存在本地浏览器。'))
})
// 墙纸 pane
P('wallpaper', '墙纸', '🏞', '系统外观', box => {
const grid = el('div', { class: 'wall-grid' })
WALLPAPERS.forEach((w: any) => {
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(); grid.querySelectorAll('.wall-cell').forEach((x: any) => x.classList.remove('sel')); cell.classList.add('sel') })
grid.append(cell)
})
box.append(grid, note('锁屏与桌面共享同一墙纸。'))
})
// 用户与群组
P('users', '用户与群组', '👤', '安全', box => {
const nameIn = el('input', { class: 'text-input', value: S.userName }) as HTMLInputElement
nameIn.addEventListener('change', () => { S.userName = nameIn.value.trim() || '客人用户'; Sys().save() })
box.append(card(row('用户名', nameIn)))
})
// 日期与时间
P('datetime', '日期与时间', '🕐', '通用', box => {
const ZONES = [['local', '跟随本机'], ['Asia/Shanghai', '上海'], ['Asia/Tokyo', '东京'], ['Europe/London', '伦敦'], ['America/New_York', '纽约']]
const s = el('select', { class: 'text-input' }, ...ZONES.map(([v, l]) => el('option', { text: l, value: v }))) as HTMLSelectElement
s.value = S.timezone || 'local'
s.addEventListener('change', () => { S.timezone = s.value; Sys().save(); Sys().tickClock() })
box.append(card(row('24 小时制', toggle('h24', () => Sys().tickClock())), row('时区', s)))
})
// 键盘
P('keyboard', '键盘', '⌨️', '输入', box => {
box.append(card(row('重复速度', slider('kbRepeat', 1, 10, false)), row('重复前延迟', slider('kbDelay', 1, 10, false))))
box.append(note('键盘参数为模拟项。'))
})
// 传输或还原
P('transfer', '传输或还原', '♻️', '通用', box => {
const b = el('button', { class: 'btn', text: '还原所有设置…' })
b.addEventListener('click', () => Sys().resetAll())
box.append(card(row('还原', b, '清除所有设置与数据')))
})
}, },
render(win: any, args: any) {
if (!this.panes.length) this.definePanes()
const st = win.appState = { pane: null as string | null, q: '' }
win.body.classList.add('settings-body')
const search = el('input', { class: 'text-input set-search', type: 'search', placeholder: '搜索' }) as HTMLInputElement
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 = ['网络', '系统', '通用', '系统外观', '安全', '输入']
const renderNav = () => {
nav.innerHTML = ''
for (const g of groups) {
const items = this.panes.filter((p: any) => 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: string) => {
const p = this.panes.find((p: any) => 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) Apps.register(SettingsApp)
;(window as any).SettingsApp = SettingsApp ;(window as any).SettingsApp = SettingsApp

View File

@ -0,0 +1,40 @@
/**
* 39-settings: Settings Vue
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { h, render, nextTick } from 'vue'
import SettingsComponent from '../../src/apps/settings/Settings.vue'
import { setupDOM } from '../helpers'
describe('39-settings — Settings Vue 组件化', () => {
let el: HTMLElement, body: HTMLElement, win: any
beforeEach(() => {
localStorage.clear()
setupDOM()
el = document.createElement('div'); el.className = 'window'
document.getElementById('window-layer')?.appendChild(el)
body = document.createElement('div'); body.className = 'win-body'
el.appendChild(body)
win = { el, body, timers: [], data: {} };
(window as any).Sys = {
settings: { wifi: true, wifiNetwork: '', bluetooth: false, btDevices: {} as any, appearance: 'auto', accent: '#0a84ff', dockSize: 48, dockAutohide: false, notificationsEnabled: true, computerName: 'Mac', passwordEnabled: false, wallpaper: 'sonoma', h24: true, timezone: 'local', kbRepeat: 5, kbDelay: 3, userName: '客人用户' },
save() {}, applyAll() {}, applyAppearance() {}, applyWallpaper() {}, renderDock() {}, layoutDock() {}, tickClock() {}, resetAll() {},
};
(window as any).WM = { setTitle() {} }
})
afterEach(() => { render(null, body); el.remove() })
function mount() { const v = h(SettingsComponent, { win }); render(v, body) }
it('显示导航栏', () => { mount(); expect(body.querySelector('.set-sidebar')).toBeTruthy() })
it('显示内容区', () => { mount(); expect(body.querySelector('.set-content')).toBeTruthy() })
it('搜索框存在', () => { mount(); expect(body.querySelector('.set-search')).toBeTruthy() })
it('外观 pane 显示浅色/深色/自动', () => { mount(); expect(body.querySelectorAll('.ap-card').length).toBe(3) })
it('默认选中外观', () => { mount(); expect(body.querySelector('.set-nav-item.sel')?.textContent).toContain('外观') })
it('切换 pane', async () => { mount(); const items = body.querySelectorAll('.set-nav-item'); (items[1] as HTMLElement)?.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(items[1].classList.contains('sel')).toBe(true) })
it('Wi-Fi toggle 存在', async () => { mount(); const items = body.querySelectorAll('.set-nav-item'); const wifiItem = [...items].find(i => i.textContent?.includes('Wi-Fi')) as HTMLElement; wifiItem?.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(body.querySelector('.switch')).toBeTruthy() })
it('强调色按钮存在', () => { mount(); expect(body.querySelectorAll('.accent-dot').length).toBe(6) })
it('卸载', () => { mount(); render(null, body); expect(body.children.length).toBe(0) })
})