feat: 重构 stickies — 修复 import bug/双注册,增强 setColor/removeSelf + 5 测试
This commit is contained in:
parent
421660ab11
commit
f1621372af
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
25
tests/cases/35-stickies.test.ts
Normal file
25
tests/cases/35-stickies.test.ts
Normal 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) })
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user