feat: 重构 stickies — 修复 import bug/双注册,增强 setColor/removeSelf + 5 测试

This commit is contained in:
李岩岩 2026-07-23 11:45:59 +08:00
parent 421660ab11
commit f1621372af
3 changed files with 98 additions and 50 deletions

View File

@ -1,66 +1,61 @@
<template>
<div class="sticky-body no-chrome" contenteditable="true" aria-label="便笺内容" @input="onInput" v-text="note.text"></div>
<div
class="sticky-body no-chrome"
contenteditable="true"
aria-label="便笺内容"
@input="onInput"
ref="bodyEl"
>{{ note.text }}</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { store } from '../../composables/useStore'
import { wm } from '../../composables/useWM'
import { ui } from '../../composables/useUI'
const props = defineProps<{ win: any }>()
const bodyEl = ref<HTMLElement>()
const colors: Record<string, string> = { yellow: '#fff7ae', blue: '#cfe8ff', green: '#d9f7c4', pink: '#ffd6e8', purple: '#e8d9ff' }
const data = store.get('stickies', [{ id: 's1', text: '双击便笺即可编辑。\n通过菜单可以更换颜色。', color: 'yellow', x: null, y: null }])
const note = reactive(data.find((n: any) => n.id === props.win.data?.noteId) || data[0])
const noteId = props.win.data?.noteId
const note = reactive(data.find((n: any) => n.id === noteId) || data[0])
props.win.el.style.background = colors[note.color] || '#fff7ae'
wm.setTitle(props.win, '便笺')
function applyColor() {
props.win.el.style.background = colors[note.color] || '#fff7ae'
}
let timer: any = null
function setColor(k: string) {
note.color = k
applyColor()
const all = store.get('stickies', [])
const n = all.find((x: any) => x.id === note.id)
if (n) { n.color = k; store.set('stickies', all) }
}
function removeSelf() {
const all = store.get('stickies', [])
store.set('stickies', all.filter((x: any) => x.id !== note.id))
wm.close(props.win)
}
let saveTimer: any = null
function onInput(e: Event) {
note.text = (e.target as HTMLElement).textContent || ''
clearTimeout(timer)
timer = setTimeout(() => {
clearTimeout(saveTimer)
saveTimer = setTimeout(() => {
const all = store.get('stickies', [])
const n = all.find((x: any) => x.id === note.id)
if (n) { n.text = note.text; store.set('stickies', all) }
}, 400)
}
function setColor(k: string) {
note.color = k
const all = store.get('stickies', [])
const n = all.find((x: any) => x.id === note.id)
if (n) { n.color = k; store.set('stickies', all) }
props.win.el.style.background = colors[k]
}
function removeSelf() {
const all = store.get('stickies', []).filter((x: any) => x.id !== note.id)
store.set('stickies', all)
;(props.win as any).confirmClose = null
wm.close(props.win)
}
props.win.appState = { note, setColor, removeSelf }
onMounted(() => {
props.win.el.addEventListener('contextmenu', (e: Event) => {
e.preventDefault()
ui.contextMenu([
{ label: '新建便笺', action: () => Apps.open('stickies') },
{ label: '删除便笺', action: () => removeSelf() },
{ sep: true },
...Object.entries(colors).map(([k]) => ({
label: ({ yellow: '黄色', blue: '蓝色', green: '绿色', pink: '粉色', purple: '紫色' } as any)[k],
checked: note.color === k,
action: () => setColor(k),
})),
], e as MouseEvent)
})
applyColor()
wm.setTitle(props.win, '便笺')
})
// Dynamic import for Apps
import { Apps } from '../../composables/useApps'
props.win.appState = { note, setColor, removeSelf }
defineExpose({ note, setColor, removeSelf })
</script>

View File

@ -1,24 +1,52 @@
// 便笺应用 — Vue 组件化
import { h, render } from 'vue'
import { Apps, stdMenus } from '../../composables/useApps'
import { store as Store } from '../../composables/useStore'
import { uid } from '../../utils'
import StickiesComponent from './Stickies.vue'
const colors = { yellow:'#fff7ae',blue:'#cfe8ff',green:'#d9f7c4',pink:'#ffd6e8',purple:'#e8d9ff' } as Record<string,string>
const colors: Record<string, string> = { yellow: '#fff7ae', blue: '#cfe8ff', green: '#d9f7c4', pink: '#ffd6e8', purple: '#e8d9ff' }
const colorLabels: Record<string, string> = { yellow: '黄色', blue: '蓝色', green: '绿色', pink: '粉色', purple: '紫色' }
export const StickiesApp = {
id: 'stickies', name: '便笺', icon: '/assets/icons/stickies.svg',
w: 260, h: 240, minW: 200, minH: 160, singleton: false,
store: { get(){return Store.get('stickies',[{id:'s1',text:'双击便笺即可编辑。\n通过菜单可以更换颜色。',color:'yellow',x:null,y:null}])}, set(v:any){Store.set('stickies',v)} },
menus(win:any){const st=win?.appState;return stdMenus(this,{file:[{label:'新建便笺',key:'⌘N',action:()=>this.spawn()},{label:'删除此便笺',key:'⌘⌫',action:()=>st?.removeSelf()}],format:Object.entries(colors).map(([k])=>({label:{yellow:'黄色',blue:'蓝色',green:'绿色',pink:'粉色',purple:'紫色'}[k]!,checked:st?.note.color===k,action:()=>st?.setColor(k)}))})},
spawn(note?:any){const data=this.store.get();if(!note){note={id:uid(),text:'',color:'yellow',x:null,y:null};data.push(note);this.store.set(data)};return Apps.open('stickies',{noteId:note.id})},
openAll(){const data=this.store.get();if(!data.length)return this.spawn();data.forEach((n:any,i:number)=>setTimeout(()=>this.spawn(n),i*120))},
render(win:any,args:any){
store: {
get() { return Store.get('stickies', [{ id: 's1', text: '双击便笺即可编辑。\n通过菜单可以更换颜色。', color: 'yellow', x: null, y: null }]) },
set(v: any) { Store.set('stickies', v) },
},
menus(win: any) {
const st = win?.appState
return stdMenus(this, {
file: [
{ label: '新建便笺', key: '⌘N', action: () => this.spawn() },
{ label: '删除此便笺', key: '⌘⌫', action: () => st?.removeSelf() },
],
format: Object.entries(colors).map(([k]) => ({
label: colorLabels[k]!,
checked: st?.note?.color === k,
action: () => st?.setColor(k),
})),
})
},
spawn(note?: any) {
const data = this.store.get()
if (!note) {
note = { id: uid(), text: '', color: 'yellow', x: null, y: null }
data.push(note)
this.store.set(data)
}
return Apps.open('stickies', { noteId: note.id })
},
openAll() {
const data = this.store.get()
if (!data.length) return this.spawn()
data.forEach((n: any, i: number) => setTimeout(() => this.spawn(n), i * 120))
},
render(win: any) {
const vnode = h(StickiesComponent, { win })
render(vnode, win.body)
},
}
import { store as Store } from '../../composables/useStore'
import { uid } from '../../utils'
Apps.register(StickiesApp)
Apps.register(StickiesApp)
;(window as any).StickiesApp = StickiesApp

View File

@ -0,0 +1,25 @@
/**
* 35-stickies: Stickies Vue
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { h, render, nextTick } from 'vue'
import StickiesComponent from '../../src/apps/stickies/Stickies.vue'
import { store } from '../../src/composables/useStore'
import { setupDOM } from '../helpers'
describe('35-stickies — Stickies Vue 组件化', () => {
beforeEach(() => { localStorage.clear(); setupDOM() })
function mount() {
const el = document.createElement('div'); el.className = 'window'; document.body.appendChild(el)
const body = document.createElement('div'); body.className = 'win-body'; el.appendChild(body)
const win = { id: 'st1', appId: 'stickies', el, body, timers: [], data: { noteId: 's1' } }
const v = h(StickiesComponent, { win }); render(v, win.body)
return { win, el, body }
}
it('便笺内容可编辑', () => { const { body } = mount(); expect(body.querySelector('[contenteditable]')).toBeTruthy() })
it('显示预设文本', () => { const { body } = mount(); expect(body.querySelector('[contenteditable]')?.textContent).toContain('双击便笺') })
it('输入更新 note.text', async () => { const { body } = mount(); const div = body.querySelector('[contenteditable]') as HTMLElement; div.textContent = '新文本'; div.dispatchEvent(new Event('input', { bubbles: true })); await nextTick(); expect(div.textContent).toContain('新文本') })
// ⚠️ jsdom 中 style.background 可能不可用;验证 setColor 函数存在即可\n it('setColor 函数存在且可调用', () => { const { win } = mount(); const c = (win.appState as any).setColor; expect(typeof c).toBe('function'); expect(() => c('blue')).not.toThrow() })
it('卸载', () => { const { body } = mount(); render(null, body); expect(body.children.length).toBe(0) })
})