rebuild: 第五版重构 - 添加pinia
This commit is contained in:
parent
a708b4cdee
commit
d3c89d2c09
1874
REFACTORING_GUIDE.md
Normal file
1874
REFACTORING_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,8 @@
|
|||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"pinia": "^4.0.2",
|
||||||
"vue": "^3.5.0"
|
"vue": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
1946
pnpm-lock.yaml
generated
Normal file
1946
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
src/composables/useEventBus.ts
Normal file
28
src/composables/useEventBus.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* useEventBus — mitt 类型安全事件总线
|
||||||
|
* 替代原 useBus.ts,所有事件必须在此声明类型
|
||||||
|
*/
|
||||||
|
import mitt from 'mitt'
|
||||||
|
import type { WinState } from '@/types/window'
|
||||||
|
|
||||||
|
type Events = {
|
||||||
|
// 窗口事件
|
||||||
|
'wm:focus': WinState | null
|
||||||
|
'wm:changed': void
|
||||||
|
'wm:closed': { winId: string }
|
||||||
|
|
||||||
|
// 文件系统事件
|
||||||
|
'fs:changed': { op?: string; paths: string[]; dirty?: boolean }
|
||||||
|
|
||||||
|
// 系统事件
|
||||||
|
'apps:ready': void
|
||||||
|
'volume:changed': number
|
||||||
|
'trash:changed': void
|
||||||
|
'unlocked': void
|
||||||
|
'locked': void
|
||||||
|
|
||||||
|
// 通知事件
|
||||||
|
'notify:badges-updated': void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emitter = mitt<Events>()
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import { initPinia } from './stores'
|
||||||
|
|
||||||
// Import all CSS
|
// Import all CSS
|
||||||
import './styles/base.css'
|
import './styles/base.css'
|
||||||
@ -9,4 +10,5 @@ import './styles/apps.css'
|
|||||||
import './styles/apps2.css'
|
import './styles/apps2.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
app.use(initPinia())
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
55
src/stores/apps.ts
Normal file
55
src/stores/apps.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* appStore — 应用注册表
|
||||||
|
* 替代 useApps.ts 中的注册表部分
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useFSStore } from './fs'
|
||||||
|
import type { AppDefinition } from '@/types/app'
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('apps', () => {
|
||||||
|
const registry = reactive<Record<string, AppDefinition>>({})
|
||||||
|
|
||||||
|
// 待打开的应用(由 AppLoader 消费)
|
||||||
|
const pendingOpen = ref<{ id: string; args?: any } | null>(null)
|
||||||
|
|
||||||
|
function register(def: AppDefinition) {
|
||||||
|
def.w = def.w || 720
|
||||||
|
def.h = def.h || 480
|
||||||
|
def.minW = def.minW || 320
|
||||||
|
def.minH = def.minH || 240
|
||||||
|
def.about = def.about || `${def.name} — macOS 网页版内置应用`
|
||||||
|
registry[def.id] = def
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(id: string): AppDefinition | undefined {
|
||||||
|
return registry[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(id: string, args?: any) {
|
||||||
|
pendingOpen.value = { id, args }
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPath(path: string) {
|
||||||
|
const fsStore = useFSStore()
|
||||||
|
const n = fsStore.node(path)
|
||||||
|
if (!n) {
|
||||||
|
// ui.alert 稍后接入
|
||||||
|
console.warn('[apps] 找不到项目:', path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (n.t === 'd') return open('finder', { path })
|
||||||
|
if (n.t === 'a') return open(n.app!)
|
||||||
|
const ext = (path.split('.').pop() || '').toLowerCase()
|
||||||
|
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'pdf'].includes(ext))
|
||||||
|
return open('preview', { path })
|
||||||
|
return open('textedit', { path })
|
||||||
|
}
|
||||||
|
|
||||||
|
function quit(id: string) {
|
||||||
|
// 由外部 wm 处理实际关闭
|
||||||
|
pendingOpen.value = { id, args: { quit: true } }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { registry, pendingOpen, register, get, open, openPath, quit }
|
||||||
|
})
|
||||||
315
src/stores/fs.ts
Normal file
315
src/stores/fs.ts
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
/**
|
||||||
|
* fsStore — 虚拟文件系统
|
||||||
|
* 替代 useFS.ts,数据用 Pinia 管理
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { emitter } from '@/composables/useEventBus'
|
||||||
|
import type { FSNode, FSEntry } from '@/types/fs'
|
||||||
|
|
||||||
|
// ---- 文件扩展名 → MIME 映射 ----
|
||||||
|
function mimeOf(name: string): string {
|
||||||
|
const ext = name.split('.').pop()?.toLowerCase() || ''
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
txt: 'text/plain', md: 'text/markdown', html: 'text/html', css: 'text/css', js: 'text/javascript',
|
||||||
|
json: 'application/json', xml: 'text/xml', svg: 'image/svg+xml', csv: 'text/csv',
|
||||||
|
}
|
||||||
|
return map[ext] || 'text/plain'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 文件扩展名 → 图标映射 ----
|
||||||
|
export function iconFor(path: string): string {
|
||||||
|
const segs = path.split('/')
|
||||||
|
const name = segs[segs.length - 1] || ''
|
||||||
|
const ext = name.split('.').pop()?.toLowerCase() || ''
|
||||||
|
|
||||||
|
const extIcons: Record<string, string> = {
|
||||||
|
txt: '/assets/icons/file-text.svg', md: '/assets/icons/file-text.svg',
|
||||||
|
html: '/assets/icons/file-code.svg', css: '/assets/icons/file-code.svg',
|
||||||
|
js: '/assets/icons/file-code.svg', ts: '/assets/icons/file-code.svg',
|
||||||
|
json: '/assets/icons/file-code.svg', xml: '/assets/icons/file-code.svg',
|
||||||
|
png: '/assets/icons/file-image.svg', jpg: '/assets/icons/file-image.svg',
|
||||||
|
jpeg: '/assets/icons/file-image.svg', gif: '/assets/icons/file-image.svg',
|
||||||
|
webp: '/assets/icons/file-image.svg', svg: '/assets/icons/file-image.svg',
|
||||||
|
pdf: '/assets/icons/file-pdf.svg',
|
||||||
|
mp3: '/assets/icons/file-audio.svg', wav: '/assets/icons/file-audio.svg', ogg: '/assets/icons/file-audio.svg',
|
||||||
|
mp4: '/assets/icons/file-video.svg', mov: '/assets/icons/file-video.svg',
|
||||||
|
zip: '/assets/icons/file-archive.svg', tar: '/assets/icons/file-archive.svg', gz: '/assets/icons/file-archive.svg',
|
||||||
|
app: '/assets/icons/app-default.svg',
|
||||||
|
}
|
||||||
|
if (extIcons[ext]) return extIcons[ext]
|
||||||
|
return '/assets/icons/file-generic.svg'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFSStore = defineStore('fs', () => {
|
||||||
|
const HOME = '/Users/guest'
|
||||||
|
const root = ref<FSNode | null>(null)
|
||||||
|
|
||||||
|
// ---- 懒引用(由外部注入,打破循环依赖) ----
|
||||||
|
let _Apps: any = null
|
||||||
|
let _AppStoreApp: any = null
|
||||||
|
|
||||||
|
function setAppsRef(apps: any, appStore: any) {
|
||||||
|
_Apps = apps
|
||||||
|
_AppStoreApp = appStore
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAppStoreRef(a: any) { _AppStoreApp = a }
|
||||||
|
|
||||||
|
const TRASH = computed(() => HOME + '/.Trash')
|
||||||
|
|
||||||
|
// ---- 路径工具 ----
|
||||||
|
function normalize(p: string): string {
|
||||||
|
if (!p) return '/'
|
||||||
|
const parts: string[] = []
|
||||||
|
for (const seg of String(p).split('/')) {
|
||||||
|
if (!seg || seg === '.') continue
|
||||||
|
if (seg === '..') parts.pop()
|
||||||
|
else parts.push(seg)
|
||||||
|
}
|
||||||
|
return '/' + parts.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function join(...segs: string[]) { return normalize(segs.join('/')) }
|
||||||
|
function baseName(p: string) { p = normalize(p); return p === '/' ? '/' : p.slice(p.lastIndexOf('/') + 1) }
|
||||||
|
function dirName(p: string) { p = normalize(p); const i = p.lastIndexOf('/'); return i <= 0 ? '/' : p.slice(0, i) }
|
||||||
|
|
||||||
|
function node(p: string): FSNode | null {
|
||||||
|
p = normalize(p)
|
||||||
|
if (p === '/') return root.value
|
||||||
|
let cur: FSNode | null = root.value
|
||||||
|
for (const seg of p.slice(1).split('/')) {
|
||||||
|
if (!cur || cur.t !== 'd' || !cur.c![seg]) return null
|
||||||
|
cur = cur.c![seg]
|
||||||
|
}
|
||||||
|
return cur
|
||||||
|
}
|
||||||
|
|
||||||
|
function exists(p: string) { return !!node(p) }
|
||||||
|
function isDir(p: string) { return node(p)?.t === 'd' }
|
||||||
|
|
||||||
|
function parent(p: string): [FSNode | null, string, string] {
|
||||||
|
p = normalize(p)
|
||||||
|
const dir = dirName(p)
|
||||||
|
const name = baseName(p)
|
||||||
|
const par = node(dir)
|
||||||
|
return par && par.t === 'd' ? [par, name, dir] : [null, name, dir]
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertDir(p: string): FSNode {
|
||||||
|
const n = node(p)
|
||||||
|
if (!n || n.t !== 'd') throw new Error('不是文件夹: ' + p)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 持久化 ----
|
||||||
|
function loadFromStorage(): FSNode | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('macos-web:fs')
|
||||||
|
if (!raw) return null
|
||||||
|
const v = JSON.parse(raw)
|
||||||
|
return v && v.t === 'd' ? v : null
|
||||||
|
} catch { return null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
try { localStorage.setItem('macos-web:fs', JSON.stringify(root.value)) }
|
||||||
|
catch (e) { console.warn('[fs] 保存失败:', e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 初始化 & 种子数据 ----
|
||||||
|
function seed() {
|
||||||
|
const dirs = ['Desktop', 'Documents', 'Downloads', 'Pictures', 'Music', 'Applications', '.Trash']
|
||||||
|
dirs.forEach(d => mkdir(HOME + '/' + d, { recursive: true, silent: true }))
|
||||||
|
|
||||||
|
write(HOME + '/Desktop/welcome.txt', [
|
||||||
|
'欢迎使用 macOS 网页版!', '',
|
||||||
|
'这是一套在浏览器中运行的桌面模拟器。', '你可以:',
|
||||||
|
'· 双击打开「Sample Folder」和各个应用', '· 在访达、终端、文本编辑之间管理同一套虚拟文件',
|
||||||
|
'· 通过 Apple 菜单锁定、重启或关机', '· 在系统设置中更换壁纸、切换深色模式', '',
|
||||||
|
'所有数据都保存在浏览器本地,刷新后依然存在。'
|
||||||
|
].join('\n'), { silent: true })
|
||||||
|
|
||||||
|
mkdir(HOME + '/Desktop/Sample Folder', { silent: true })
|
||||||
|
write(HOME + '/Desktop/Sample Folder/会议纪要.txt', '周会纪要\n\n1. 桌面端体验优化\n2. 虚拟文件系统联调\n3. 下周发布预览版', { silent: true })
|
||||||
|
write(HOME + '/Desktop/Sample Folder/待办.txt', '- [x] 搭建窗口管理器\n- [x] 接入通知中心\n- [ ] 完善离线回退', { silent: true })
|
||||||
|
write(HOME + '/Documents/购物清单.txt', '牛奶\n鸡蛋\n全麦面包\n咖啡豆\n牛油果\n', { silent: true })
|
||||||
|
write(HOME + '/Documents/Ideas.txt', '想法收集\n\n· 给屏保加上天气\n· 终端支持管道\n· 地图离线瓦片\n', { silent: true })
|
||||||
|
write(HOME + '/Documents/旅行清单.txt', '京都 4 日行\n\nD1 清水寺 / 二年坂\nD2 岚山竹林 / 渡月桥\nD3 伏见稻荷大社\nD4 锦市场采购\n', { silent: true })
|
||||||
|
write(HOME + '/Documents/关于本系统.txt', 'macOS 网页版 v1.0\n\n纯 HTML/CSS/JavaScript 实现,无需构建。\n数据存储于 localStorage,离线可用。', { silent: true })
|
||||||
|
write(HOME + '/Downloads/说明.txt', '此目录用于存放下载的文件。', { silent: true })
|
||||||
|
write(HOME + '/Pictures/壁纸说明.txt', '系统内置多张壁纸,可在「系统设置 › 墙纸」中切换。', { silent: true })
|
||||||
|
write(HOME + '/Music/曲目说明.txt', '音乐 App 已内置 6 首 Kevin MacLeod (CC-BY) 曲目。', { silent: true })
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
root.value = loadFromStorage()
|
||||||
|
if (!root.value || root.value.t !== 'd') {
|
||||||
|
root.value = { t: 'd', c: {}, mtime: Date.now() }
|
||||||
|
seed()
|
||||||
|
}
|
||||||
|
emitter.on('apps:ready', () => syncApps())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 应用同步 ----
|
||||||
|
function syncApps() {
|
||||||
|
if (!_Apps) return
|
||||||
|
const dir = node(HOME + '/Applications')
|
||||||
|
if (!dir) return
|
||||||
|
const installed = (id: string) => _AppStoreApp && _AppStoreApp.isInstalled(id)
|
||||||
|
const want = new Set<string>()
|
||||||
|
for (const app of Object.values(_Apps.registry) as any[]) {
|
||||||
|
if ((app as any).storeApp && !installed((app as any).id)) continue
|
||||||
|
want.add((app as any).id)
|
||||||
|
const name = (app as any).name + '.app'
|
||||||
|
if (!dir.c![name]) dir.c![name] = { t: 'a', app: (app as any).id, mtime: Date.now() }
|
||||||
|
}
|
||||||
|
let dirty = false
|
||||||
|
for (const [name, n] of Object.entries(dir.c!)) {
|
||||||
|
if ((n as FSNode).t === 'a' && (n as FSNode).app && !want.has((n as FSNode).app!)) {
|
||||||
|
const reg = _Apps.registry[(n as FSNode).app!]
|
||||||
|
if (reg && reg.storeApp) { delete dir.c![name]; dirty = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save()
|
||||||
|
emitter.emit('fs:changed', { op: 'sync', paths: [HOME + '/Applications'], dirty })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CRUD 操作 ----
|
||||||
|
function list(p: string, opts: { showHidden?: boolean } = {}): FSEntry[] {
|
||||||
|
const n = assertDir(p)
|
||||||
|
return Object.entries(n.c!)
|
||||||
|
.filter(([name]) => opts.showHidden || !name.startsWith('.'))
|
||||||
|
.map(([name, nd]) => ({ name, path: join(p, name), node: nd }))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function read(p: string): string {
|
||||||
|
const n = node(p)
|
||||||
|
if (!n) throw new Error('文件不存在: ' + p)
|
||||||
|
if (n.t !== 'f') throw new Error('不是文本文件: ' + p)
|
||||||
|
return n.data ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(p: string, data: string, opts: { mime?: string; silent?: boolean } = {}) {
|
||||||
|
const [par, name] = parent(p)
|
||||||
|
if (!par) throw new Error('父目录不存在: ' + p)
|
||||||
|
const now = Date.now()
|
||||||
|
if (par.c![name] && par.c![name].t === 'd') throw new Error('同名文件夹已存在: ' + name)
|
||||||
|
par.c![name] = { t: 'f', data: String(data ?? ''), mime: opts.mime || mimeOf(name), mtime: now }
|
||||||
|
save()
|
||||||
|
if (!opts.silent) emitter.emit('fs:changed', { op: 'write', paths: [normalize(p), dirName(p)] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkdir(p: string, opts: { recursive?: boolean; silent?: boolean } = {}) {
|
||||||
|
p = normalize(p)
|
||||||
|
if (exists(p)) { if (!opts.recursive) throw new Error('已存在: ' + p); return }
|
||||||
|
if (opts.recursive) {
|
||||||
|
let cur = root.value!
|
||||||
|
let curPath = ''
|
||||||
|
for (const seg of p.slice(1).split('/')) {
|
||||||
|
curPath += '/' + seg
|
||||||
|
if (!cur.c![seg]) cur.c![seg] = { t: 'd', c: {}, mtime: Date.now() }
|
||||||
|
if (cur.c![seg].t !== 'd') throw new Error('路径冲突: ' + curPath)
|
||||||
|
cur = cur.c![seg]
|
||||||
|
}
|
||||||
|
save()
|
||||||
|
if (!opts.silent) emitter.emit('fs:changed', { op: 'mkdir', paths: [p, dirName(p)] })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [par, name] = parent(p)
|
||||||
|
if (!par) throw new Error('父目录不存在: ' + p)
|
||||||
|
par.c![name] = { t: 'd', c: {}, mtime: Date.now() }
|
||||||
|
save()
|
||||||
|
if (!opts.silent) emitter.emit('fs:changed', { op: 'mkdir', paths: [p, dirName(p)] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameItem(p: string, newName: string): string {
|
||||||
|
p = normalize(p); newName = String(newName || '').trim()
|
||||||
|
if (!newName || newName.includes('/')) throw new Error('名称无效')
|
||||||
|
const [par, name, dir] = parent(p)
|
||||||
|
if (!par || !par.c![name]) throw new Error('不存在: ' + p)
|
||||||
|
if (name === newName) return p
|
||||||
|
if (par.c![newName]) throw new Error('已存在同名项目: ' + newName)
|
||||||
|
par.c![newName] = par.c![name]; delete par.c![name]
|
||||||
|
par.c![newName].mtime = Date.now()
|
||||||
|
const np = join(dir, newName)
|
||||||
|
save(); emitter.emit('fs:changed', { op: 'rename', paths: [p, np, dir] })
|
||||||
|
return np
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueName(dir: string, base: string): string {
|
||||||
|
const n = assertDir(dir)
|
||||||
|
if (!n.c![base]) return base
|
||||||
|
const dot = base.lastIndexOf('.')
|
||||||
|
const stem = dot > 0 ? base.slice(0, dot) : base
|
||||||
|
const ext = dot > 0 ? base.slice(dot) : ''
|
||||||
|
for (let i = 2; ; i++) { const cand = `${stem} ${i}${ext}`; if (!n.c![cand]) return cand }
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy(src: string, dstDir: string): string {
|
||||||
|
src = normalize(src); dstDir = normalize(dstDir)
|
||||||
|
const sn = node(src); if (!sn) throw new Error('不存在: ' + src)
|
||||||
|
const dd = assertDir(dstDir)
|
||||||
|
const name = uniqueName(dstDir, baseName(src))
|
||||||
|
dd.c![name] = structuredClone(sn); dd.c![name].mtime = Date.now()
|
||||||
|
delete (dd.c![name] as any).origPath
|
||||||
|
save(); emitter.emit('fs:changed', { op: 'copy', paths: [src, join(dstDir, name), dstDir] })
|
||||||
|
return join(dstDir, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function move(src: string, dstDir: string): string {
|
||||||
|
src = normalize(src); dstDir = normalize(dstDir)
|
||||||
|
if (src === dstDir) throw new Error('不能移动到自身')
|
||||||
|
if (dstDir === src || dstDir.startsWith(src + '/')) throw new Error('不能把文件夹移动到它自己内部')
|
||||||
|
const [par, name] = parent(src)
|
||||||
|
if (!par || !par.c![name]) throw new Error('不存在: ' + src)
|
||||||
|
const dd = assertDir(dstDir)
|
||||||
|
let final = name
|
||||||
|
if (dd.c![final]) final = uniqueName(dstDir, name)
|
||||||
|
dd.c![final] = par.c![name]; delete par.c![name]
|
||||||
|
dd.c![final].mtime = Date.now()
|
||||||
|
const np = join(dstDir, final)
|
||||||
|
save(); emitter.emit('fs:changed', { op: 'move', paths: [src, np, dirName(src), dstDir] })
|
||||||
|
return np
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(p: string) {
|
||||||
|
p = normalize(p)
|
||||||
|
if (p === '/' || p === TRASH.value) throw new Error('不能删除该项目')
|
||||||
|
const [par, name, dir] = parent(p)
|
||||||
|
if (!par || !par.c![name]) throw new Error('不存在: ' + p)
|
||||||
|
delete par.c![name]
|
||||||
|
save(); emitter.emit('fs:changed', { op: 'remove', paths: [p, dir] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function trash(p: string): string {
|
||||||
|
p = normalize(p)
|
||||||
|
if (p.startsWith(TRASH.value + '/')) { remove(p); return p }
|
||||||
|
const n = node(p); if (!n) throw new Error('不存在: ' + p)
|
||||||
|
;(n as any).origPath = p
|
||||||
|
const np = move(p, TRASH.value)
|
||||||
|
save()
|
||||||
|
emitter.emit('trash:changed')
|
||||||
|
return np
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyTrash() {
|
||||||
|
const t = node(TRASH.value)
|
||||||
|
if (t && t.c) Object.keys(t.c).forEach(k => delete t.c![k])
|
||||||
|
save()
|
||||||
|
emitter.emit('trash:changed')
|
||||||
|
emitter.emit('fs:changed', { op: 'trash:empty', paths: [TRASH.value] })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
HOME, TRASH, root,
|
||||||
|
setAppsRef, setAppStoreRef,
|
||||||
|
init, seed, save, syncApps,
|
||||||
|
normalize, join, baseName, dirName,
|
||||||
|
node, exists, isDir, assertDir,
|
||||||
|
list, read, write, mkdir, renameItem, uniqueName,
|
||||||
|
copy, move, remove, trash, emptyTrash,
|
||||||
|
}
|
||||||
|
})
|
||||||
11
src/stores/index.ts
Normal file
11
src/stores/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Pinia 实例创建 & 插件注册
|
||||||
|
*/
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { persistPlugin } from './plugins/persist'
|
||||||
|
|
||||||
|
export function initPinia() {
|
||||||
|
const pinia = createPinia()
|
||||||
|
pinia.use(persistPlugin)
|
||||||
|
return pinia
|
||||||
|
}
|
||||||
64
src/stores/notify.ts
Normal file
64
src/stores/notify.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* notifyStore — 通知数据管理
|
||||||
|
* 替代 useNotify.ts 中的数据部分(通知列表/持久化/徽标计数)
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { Notification } from '@/types/notify'
|
||||||
|
|
||||||
|
export const useNotifyStore = defineStore('notify', () => {
|
||||||
|
const notifications = ref<Notification[]>([])
|
||||||
|
|
||||||
|
// ---- 持久化 ----
|
||||||
|
function loadFromStorage(): Notification[] {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('macos-web:notify')
|
||||||
|
return raw ? JSON.parse(raw) : []
|
||||||
|
} catch { return [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('macos-web:notify', JSON.stringify(notifications.value.slice(-60)))
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
notifications.value = loadFromStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 操作 ----
|
||||||
|
function send(n: Notification) {
|
||||||
|
notifications.value.push(n)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
function markRead(id: string) {
|
||||||
|
const n = notifications.value.find(x => x.id === id)
|
||||||
|
if (n && !n.read) { n.read = true; save() }
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(id: string) {
|
||||||
|
notifications.value = notifications.value.filter(x => x.id !== id)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
notifications.value = []
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
function badgeCount(appId: string): number {
|
||||||
|
return notifications.value.filter(n => n.appId === appId && !n.read).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function unreadCount(): number {
|
||||||
|
return notifications.value.filter(n => !n.read).length
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
notifications, init, save,
|
||||||
|
send, markRead, remove, clearAll,
|
||||||
|
badgeCount, unreadCount,
|
||||||
|
}
|
||||||
|
})
|
||||||
37
src/stores/plugins/persist.ts
Normal file
37
src/stores/plugins/persist.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Pinia persist 插件
|
||||||
|
* 自动将指定 store 的状态持久化到 localStorage
|
||||||
|
*/
|
||||||
|
import type { PiniaPluginContext } from 'pinia'
|
||||||
|
|
||||||
|
const PREFIX = 'macos-web:'
|
||||||
|
|
||||||
|
/** 需要持久化的 store id 列表 */
|
||||||
|
const PERSISTED = ['settings', 'fs', 'notify']
|
||||||
|
|
||||||
|
export function persistPlugin({ store }: PiniaPluginContext) {
|
||||||
|
if (!PERSISTED.includes(store.$id)) return
|
||||||
|
|
||||||
|
// 1. 初始化:从 localStorage 恢复
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(PREFIX + store.$id)
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
if (parsed) store.$patch(parsed)
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
// 2. 自动保存
|
||||||
|
store.$subscribe((_, state) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(PREFIX + store.$id, JSON.stringify(state))
|
||||||
|
} catch (e) { console.warn(`[persist] ${store.$id} 保存失败:`, e) }
|
||||||
|
}, { detached: true, deep: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清除所有持久化数据 */
|
||||||
|
export function clearAllPersisted() {
|
||||||
|
Object.keys(localStorage)
|
||||||
|
.filter(k => k.startsWith(PREFIX))
|
||||||
|
.forEach(k => localStorage.removeItem(k))
|
||||||
|
}
|
||||||
118
src/stores/settings.ts
Normal file
118
src/stores/settings.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* settingsStore — 系统设置管理
|
||||||
|
* 替代 useSettings.ts + useStore.ts 的设置部分
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { reactive, computed } from 'vue'
|
||||||
|
import type { SystemSettings, Wallpaper } from '@/types/settings'
|
||||||
|
|
||||||
|
export const WALLPAPERS: Wallpaper[] = [
|
||||||
|
{ id: 'monterey', name: 'Monterey 抽象', src: '/assets/wallpapers/monterey.jpg' },
|
||||||
|
{ id: 'sonoma', name: 'Sonoma 落日', src: '/assets/wallpapers/sonoma.jpg' },
|
||||||
|
{ id: 'ventura', name: 'Ventura 霞光', src: '/assets/wallpapers/ventura.jpg' },
|
||||||
|
{ id: 'bigsur', name: 'Big Sur 海岸', src: '/assets/wallpapers/big-sur-day.jpg', dark: '/assets/wallpapers/big-sur-night.jpg' },
|
||||||
|
{ id: 'sequoia', name: 'Sequoia 山谷', src: '/assets/wallpapers/sequoia.jpg' },
|
||||||
|
{ id: 'galaxy', name: '银河', src: '/assets/wallpapers/galaxy.jpg' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: SystemSettings = {
|
||||||
|
appearance: 'light', wallpaper: 'monterey', accent: '#0a84ff',
|
||||||
|
reduceTransparency: false, reduceMotion: false, increaseContrast: false,
|
||||||
|
dockSize: 54, dockMagnify: true, dockMagnifyLevel: 1.6, dockPosition: 'bottom', dockAutohide: false,
|
||||||
|
brightness: 1, nightShift: false, nightShiftStrength: 0.4,
|
||||||
|
volume: 0.6, muted: false,
|
||||||
|
wifi: true, bluetooth: true, airdrop: false, focus: false, vpn: false,
|
||||||
|
userName: '客人用户', avatar: '/assets/icons/avatar.svg',
|
||||||
|
passwordEnabled: false, password: '',
|
||||||
|
screensaverType: 'off', screensaverDelay: 5,
|
||||||
|
h24: false, language: 'zh-Hans', region: '中国', firstDayMonday: true,
|
||||||
|
searchEngine: 'bing', computerName: 'MacBook Pro',
|
||||||
|
siriApps: true, siriFiles: true, siriSettings: true,
|
||||||
|
loginItems: {}, notifAllow: {},
|
||||||
|
notificationsEnabled: true,
|
||||||
|
timezone: 'local',
|
||||||
|
wifiNetwork: '家庭网络 5G',
|
||||||
|
btDevices: {},
|
||||||
|
kbRepeat: 7, kbDelay: 4, mouseSpeed: 5,
|
||||||
|
finderView: 'icon', finderSort: 'name',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
|
// ---- 状态 ----
|
||||||
|
const settings = reactive<SystemSettings>(structuredClone(DEFAULT_SETTINGS))
|
||||||
|
|
||||||
|
// ---- 计算属性 ----
|
||||||
|
const isDark = computed(() => {
|
||||||
|
const a = settings.appearance
|
||||||
|
if (a === 'dark') return true
|
||||||
|
if (a === 'auto') return matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const wallpaperSrc = computed(() => {
|
||||||
|
const w = WALLPAPERS.find(wp => wp.id === settings.wallpaper) || WALLPAPERS[0]
|
||||||
|
return (isDark.value && w.dark) ? w.dark : w.src
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- 初始化 ----
|
||||||
|
function init(saved?: Partial<SystemSettings>) {
|
||||||
|
if (saved) Object.assign(settings, structuredClone(DEFAULT_SETTINGS), saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 方法 ----
|
||||||
|
function set<K extends keyof SystemSettings>(key: K, value: SystemSettings[K]) {
|
||||||
|
settings[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAppearance() {
|
||||||
|
const b = document.body
|
||||||
|
b.classList.toggle('dark', isDark.value)
|
||||||
|
b.classList.toggle('reduce-transparency', !!settings.reduceTransparency)
|
||||||
|
b.classList.toggle('reduce-motion', !!settings.reduceMotion)
|
||||||
|
b.classList.toggle('increase-contrast', !!settings.increaseContrast)
|
||||||
|
applyWallpaper()
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWallpaper() {
|
||||||
|
const src = wallpaperSrc.value
|
||||||
|
const desk = document.getElementById('desktop')
|
||||||
|
const lock = document.getElementById('lockscreen-bg')
|
||||||
|
if (desk) desk.style.backgroundImage = `url("${src}")`
|
||||||
|
if (lock) lock.style.backgroundImage = `url("${src}")`
|
||||||
|
// 壁纸预加载错误回退
|
||||||
|
const probe = new Image()
|
||||||
|
probe.onerror = () => {
|
||||||
|
const grad = 'linear-gradient(160deg,#4a6fa5 0%,#c86b85 45%,#f0a35e 100%)'
|
||||||
|
if (desk) desk.style.backgroundImage = grad
|
||||||
|
if (lock) lock.style.backgroundImage = grad
|
||||||
|
}
|
||||||
|
probe.src = src
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBrightness() {
|
||||||
|
const el = document.getElementById('overlay-brightness')
|
||||||
|
if (el) el.style.opacity = String((1 - settings.brightness) * 0.55)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyNightShift() {
|
||||||
|
const el = document.getElementById('overlay-nightshift')
|
||||||
|
if (el) el.style.opacity = settings.nightShift ? String(settings.nightShiftStrength * 0.32) : '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAccent() {
|
||||||
|
document.documentElement.style.setProperty('--accent', settings.accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAll() {
|
||||||
|
applyAppearance()
|
||||||
|
applyBrightness()
|
||||||
|
applyNightShift()
|
||||||
|
applyAccent()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings, isDark, wallpaperSrc,
|
||||||
|
init, set,
|
||||||
|
applyAppearance, applyWallpaper, applyBrightness, applyNightShift, applyAccent, applyAll,
|
||||||
|
}
|
||||||
|
})
|
||||||
89
src/stores/ui.ts
Normal file
89
src/stores/ui.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* uiStore — 菜单/对话框状态管理
|
||||||
|
* 替代 useUI.ts 中的状态管理部分
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import type { MenuItem } from '@/types/app'
|
||||||
|
import type { DialogConfig } from '@/types/ui'
|
||||||
|
|
||||||
|
export const useUIStore = defineStore('ui', () => {
|
||||||
|
// ---- 弹出菜单 ----
|
||||||
|
const menuVisible = ref(false)
|
||||||
|
const menuItems = ref<MenuItem[]>([])
|
||||||
|
const menuPosition = ref({ x: 0, y: 0 })
|
||||||
|
const menuSub = ref(false)
|
||||||
|
let menuOnClose: (() => void) | null = null
|
||||||
|
|
||||||
|
function showMenu(items: MenuItem[], x: number, y: number, opts: { sub?: boolean; onClose?: () => void } = {}) {
|
||||||
|
hideMenu()
|
||||||
|
menuItems.value = items
|
||||||
|
menuPosition.value = { x, y }
|
||||||
|
menuSub.value = !!opts.sub
|
||||||
|
menuVisible.value = true
|
||||||
|
menuOnClose = opts.onClose ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideMenu() {
|
||||||
|
menuOnClose?.()
|
||||||
|
menuOnClose = null
|
||||||
|
menuVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function showContextMenu(items: MenuItem[], e: MouseEvent) {
|
||||||
|
e.preventDefault(); e.stopPropagation()
|
||||||
|
showMenu(items, e.clientX, e.clientY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 对话框 ----
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogConfig = ref<DialogConfig>({ title: '', msg: '' })
|
||||||
|
let dialogResolve: ((v: string | boolean) => void) | null = null
|
||||||
|
|
||||||
|
function showDialog(config: DialogConfig): Promise<string | boolean> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
dialogConfig.value = config
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogResolve = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDialog(value: string | boolean) {
|
||||||
|
dialogVisible.value = false
|
||||||
|
dialogResolve?.(value)
|
||||||
|
dialogResolve = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function alert(title: string, msg: string, icon?: string): Promise<boolean> {
|
||||||
|
return showDialog({ icon, title, msg, buttons: ['好'] }).then(() => true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirm(
|
||||||
|
title: string, msg: string,
|
||||||
|
opts: { ok?: string; danger?: boolean; icon?: string } = {}
|
||||||
|
): Promise<boolean> {
|
||||||
|
const result = await showDialog({
|
||||||
|
icon: opts.icon, title, msg,
|
||||||
|
ok: opts.ok || '确定', danger: opts.danger,
|
||||||
|
})
|
||||||
|
return result === 'ok' || result === true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prompt(title: string, msg: string, defaultValue?: string): Promise<string | null> {
|
||||||
|
const result = await showDialog({
|
||||||
|
title, msg: msg + (defaultValue ? `\n\n默认值:${defaultValue}` : ''),
|
||||||
|
ok: '确定',
|
||||||
|
})
|
||||||
|
return typeof result === 'string' ? result : null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// menu
|
||||||
|
menuVisible, menuItems, menuPosition, menuSub,
|
||||||
|
showMenu, hideMenu, showContextMenu,
|
||||||
|
// dialog
|
||||||
|
dialogVisible, dialogConfig,
|
||||||
|
showDialog, resolveDialog,
|
||||||
|
alert, confirm, prompt,
|
||||||
|
}
|
||||||
|
})
|
||||||
135
src/stores/wm.ts
Normal file
135
src/stores/wm.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* wmStore — 窗口管理器状态
|
||||||
|
* 替代 useWM.ts 中的状态管理部分(windows / activeWin / zTop / cascade)
|
||||||
|
* 注意:窗口 DOM 创建/拖拽/缩放逻辑保留在 useWM.ts 中
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { emitter } from '@/composables/useEventBus'
|
||||||
|
import { useSettingsStore } from './settings'
|
||||||
|
import type { Rect, WinState, WinStateType } from '@/types/window'
|
||||||
|
|
||||||
|
export const useWMStore = defineStore('wm', () => {
|
||||||
|
const windows: WinState[] = reactive([])
|
||||||
|
const activeWin = ref<WinState | null>(null)
|
||||||
|
const zTop = ref(100)
|
||||||
|
const cascadeCount = ref(0)
|
||||||
|
|
||||||
|
// ---- 辅助 ----
|
||||||
|
function usableRect(): Rect {
|
||||||
|
const s = useSettingsStore().settings
|
||||||
|
const r: Rect = { x: 0, y: 30, w: window.innerWidth, h: window.innerHeight - 30 }
|
||||||
|
if (!s.dockAutohide) {
|
||||||
|
const dockH = (s.dockSize || 48) + 26
|
||||||
|
if (s.dockPosition === 'bottom') r.h -= dockH
|
||||||
|
else if (s.dockPosition === 'left') { r.x += dockH; r.w -= dockH }
|
||||||
|
else r.w -= dockH
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowsForApp(appId: string): WinState[] {
|
||||||
|
return windows.filter(w => w.appId === appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 窗口操作 ----
|
||||||
|
function addWindow(win: WinState) {
|
||||||
|
windows.push(win)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWindow(winId: string) {
|
||||||
|
const idx = windows.findIndex(w => w.id === winId)
|
||||||
|
if (idx >= 0) windows.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus(winId: string) {
|
||||||
|
const win = windows.find(w => w.id === winId)
|
||||||
|
if (!win) return
|
||||||
|
if (win.state === 'minimized') restore(winId)
|
||||||
|
if (activeWin.value?.id === winId) {
|
||||||
|
if (win.el) win.el.style.zIndex = String(++zTop.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (activeWin.value) activeWin.value.el?.classList.add('inactive')
|
||||||
|
activeWin.value = win
|
||||||
|
win.el?.classList.remove('inactive')
|
||||||
|
if (win.el) win.el.style.zIndex = String(++zTop.value)
|
||||||
|
emitter.emit('wm:focus', win)
|
||||||
|
emitter.emit('wm:changed')
|
||||||
|
}
|
||||||
|
|
||||||
|
function minimize(winId: string) {
|
||||||
|
const win = windows.find(w => w.id === winId)
|
||||||
|
if (!win || win.state === 'fullscreen') return
|
||||||
|
;(win as any).stateBeforeMin = win.state
|
||||||
|
win.state = 'minimized'
|
||||||
|
win.el?.classList.add('minimized')
|
||||||
|
if (activeWin.value?.id === winId) {
|
||||||
|
const rest = windows.filter(w => w.state !== 'minimized')
|
||||||
|
activeWin.value = null
|
||||||
|
if (rest.length) focus(rest[rest.length - 1].id)
|
||||||
|
else emitter.emit('wm:focus', null)
|
||||||
|
}
|
||||||
|
emitter.emit('wm:changed')
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore(winId: string) {
|
||||||
|
const win = windows.find(w => w.id === winId)
|
||||||
|
if (!win) return
|
||||||
|
win.state = ((win.stateBeforeMin && win.stateBeforeMin !== 'minimized') ? win.stateBeforeMin : 'normal') as WinStateType
|
||||||
|
win.el?.classList.remove('minimized')
|
||||||
|
focus(winId)
|
||||||
|
emitter.emit('wm:changed')
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRect(win: WinState) {
|
||||||
|
const r = win.rect
|
||||||
|
if (win.el) Object.assign(win.el.style, {
|
||||||
|
left: r.x + 'px', top: r.y + 'px',
|
||||||
|
width: r.w + 'px', height: r.h + 'px',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleZoom(winId: string) {
|
||||||
|
const win = windows.find(w => w.id === winId)
|
||||||
|
if (!win || win.state === 'fullscreen') return
|
||||||
|
if (win.state === 'minimized') restore(winId)
|
||||||
|
if (win.state === 'zoomed') {
|
||||||
|
win.rect = { ...win.prevRect! }
|
||||||
|
win.state = 'normal'
|
||||||
|
} else {
|
||||||
|
win.prevRect = { ...win.rect }
|
||||||
|
const u = usableRect()
|
||||||
|
win.rect = { x: u.x + 4, y: u.y + 4, w: u.w - 8, h: u.h - 8 }
|
||||||
|
win.state = 'zoomed'
|
||||||
|
}
|
||||||
|
applyRect(win)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTitle(winId: string, title: string) {
|
||||||
|
const win = windows.find(w => w.id === winId)
|
||||||
|
if (!win) return
|
||||||
|
win.title = title
|
||||||
|
if (win.titleEl) {
|
||||||
|
const t = win.titleEl.querySelector('.t')
|
||||||
|
if (t) t.textContent = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampAll() {
|
||||||
|
const u = usableRect()
|
||||||
|
for (const w of windows) {
|
||||||
|
if (w.state === 'fullscreen' || w.state === 'minimized') continue
|
||||||
|
w.rect.x = Math.max(u.x, Math.min(w.rect.x, u.x + u.w - 80))
|
||||||
|
w.rect.y = Math.max(u.y, Math.min(w.rect.y, u.y + u.h - 40))
|
||||||
|
applyRect(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
windows, activeWin, zTop, cascadeCount,
|
||||||
|
usableRect, windowsForApp,
|
||||||
|
addWindow, removeWindow, focus, minimize, restore,
|
||||||
|
applyRect, toggleZoom, setTitle, clampAll,
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -161,20 +161,5 @@ export function initSystem() {
|
|||||||
Sys.boot()
|
Sys.boot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose globals for compatibility
|
// 各模块通过 store/composable 访问,不再挂载到 window 全局
|
||||||
;(window as any).Sys = Sys
|
// 如需在其他位置使用,请通过 import { useXxxStore } from '@/stores/xxx' 访问
|
||||||
;(window as any).Notify = Notify
|
|
||||||
;(window as any).Spotlight = Spotlight
|
|
||||||
;(window as any).WM = wm
|
|
||||||
;(window as any).UI = ui
|
|
||||||
;(window as any).FS = fs
|
|
||||||
;(window as any).Apps = Apps
|
|
||||||
;(window as any).Bus = bus
|
|
||||||
;(window as any).Store = store
|
|
||||||
;(window as any).WALLPAPERS = WALLPAPERS
|
|
||||||
;(window as any).$ = $
|
|
||||||
;(window as any).$$ = $$
|
|
||||||
;(window as any).el = el
|
|
||||||
;(window as any).esc = esc
|
|
||||||
;(window as any).clamp = clamp
|
|
||||||
;(window as any).iconImg = iconImg
|
|
||||||
|
|||||||
29
src/types/app.ts
Normal file
29
src/types/app.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// ============ 应用 ============
|
||||||
|
|
||||||
|
export interface MenuItem {
|
||||||
|
label?: string
|
||||||
|
key?: string
|
||||||
|
checked?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
submenu?: MenuItem[]
|
||||||
|
action?: () => void
|
||||||
|
sep?: boolean
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppDefinition {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
w?: number
|
||||||
|
h?: number
|
||||||
|
minW?: number
|
||||||
|
minH?: number
|
||||||
|
singleton?: boolean
|
||||||
|
about?: string
|
||||||
|
noResize?: boolean
|
||||||
|
storeApp?: boolean
|
||||||
|
onArgs?: (args: any, win: any) => void
|
||||||
|
menus?: (win: any) => MenuItem[]
|
||||||
|
render?: (win: any, args?: any) => void
|
||||||
|
}
|
||||||
17
src/types/fs.ts
Normal file
17
src/types/fs.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// ============ 虚拟文件系统 ============
|
||||||
|
|
||||||
|
export interface FSNode {
|
||||||
|
t: 'd' | 'f' | 'a'
|
||||||
|
c?: Record<string, FSNode>
|
||||||
|
data?: string
|
||||||
|
mtime: number
|
||||||
|
mime?: string
|
||||||
|
app?: string
|
||||||
|
origPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FSEntry {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
node: FSNode
|
||||||
|
}
|
||||||
11
src/types/notify.ts
Normal file
11
src/types/notify.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// ============ 通知 ============
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
id: string
|
||||||
|
appId: string
|
||||||
|
title: string
|
||||||
|
body: string
|
||||||
|
ts: number
|
||||||
|
read: boolean
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
30
src/types/settings.ts
Normal file
30
src/types/settings.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// ============ 系统设置 ============
|
||||||
|
|
||||||
|
export interface Wallpaper {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
src: string
|
||||||
|
dark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemSettings {
|
||||||
|
appearance: string; wallpaper: string; accent: string
|
||||||
|
reduceTransparency: boolean; reduceMotion: boolean; increaseContrast: boolean
|
||||||
|
dockSize: number; dockMagnify: boolean; dockMagnifyLevel: number; dockPosition: string; dockAutohide: boolean
|
||||||
|
brightness: number; nightShift: boolean; nightShiftStrength: number
|
||||||
|
volume: number; muted: boolean
|
||||||
|
wifi: boolean; bluetooth: boolean; airdrop: boolean; focus: boolean; vpn: boolean
|
||||||
|
userName: string; avatar: string
|
||||||
|
passwordEnabled: boolean; password: string
|
||||||
|
screensaverType: string; screensaverDelay: number
|
||||||
|
h24: boolean; language: string; region: string; firstDayMonday: boolean
|
||||||
|
searchEngine: string; computerName: string
|
||||||
|
siriApps: boolean; siriFiles: boolean; siriSettings: boolean
|
||||||
|
loginItems: Record<string, boolean>; notifAllow: Record<string, boolean>
|
||||||
|
notificationsEnabled: boolean
|
||||||
|
timezone: string
|
||||||
|
wifiNetwork: string
|
||||||
|
btDevices: Record<string, any>
|
||||||
|
kbRepeat: number; kbDelay: number; mouseSpeed: number
|
||||||
|
finderView: string; finderSort: string
|
||||||
|
}
|
||||||
10
src/types/ui.ts
Normal file
10
src/types/ui.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// ============ 对话框 ============
|
||||||
|
|
||||||
|
export interface DialogConfig {
|
||||||
|
icon?: string
|
||||||
|
title: string
|
||||||
|
msg: string
|
||||||
|
buttons?: string[]
|
||||||
|
ok?: string
|
||||||
|
danger?: boolean
|
||||||
|
}
|
||||||
36
src/types/window.ts
Normal file
36
src/types/window.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// ============ 窗口状态 ============
|
||||||
|
|
||||||
|
export interface Rect {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WinStateType = 'normal' | 'minimized' | 'zoomed' | 'fullscreen'
|
||||||
|
|
||||||
|
export interface WinState {
|
||||||
|
id: string
|
||||||
|
appId: string
|
||||||
|
app: any
|
||||||
|
title: string
|
||||||
|
icon: string
|
||||||
|
rect: Rect
|
||||||
|
prevRect: Rect | null
|
||||||
|
minW: number
|
||||||
|
minH: number
|
||||||
|
state: WinStateType
|
||||||
|
onClose: (() => void) | null
|
||||||
|
data: any
|
||||||
|
noResize: boolean
|
||||||
|
el: HTMLElement | null
|
||||||
|
body: HTMLElement | null
|
||||||
|
titleEl: HTMLElement | null
|
||||||
|
timers: ReturnType<typeof setInterval | typeof setTimeout>[]
|
||||||
|
appState?: any
|
||||||
|
confirmClose?: (done: () => void, cancel: () => void) => void
|
||||||
|
closePromise?: Promise<string> | null
|
||||||
|
_closed?: boolean
|
||||||
|
stateBeforeMin?: string
|
||||||
|
stateBeforeFs?: string
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user