Files
kanban/components/Toolbar.vue
xiaomai 2384e42933 refactor(app): rewrite application with Nuxt 4, Pinia, and TailwindCSS
This commit replaces the original vanilla JavaScript implementation with a modern frontend stack. The entire application has been rewritten to improve maintainability, developer experience, and leverage modern
web technologies.

Key changes include:
- Replaced plain HTML, CSS, and JS with a Nuxt 4 project structure.
- Migrated state management from a global state object to a Pinia store for better organization and reactivity.
- Rebuilt the UI with Vue 3 components and styled with TailwindCSS.
- Changed the primary persistence mechanism from manual file I/O to automatic LocalStorage saving, while retaining import/export functionality.
- All core features, including drag-and-drop, task management, and the import/merge diffing logic, have been ported to the new architecture.
2025-10-22 17:08:31 +08:00

107 lines
4.6 KiB
Vue
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.
<template>
<div class="flex flex-wrap items-center gap-2 p-3 border-b border-border bg-panel">
<div class="flex items-center gap-2">
<h1 class="text-lg font-semibold">{{ appTitle }}</h1>
<span class="text-xs text-slate-400">{{ fileStatus }}</span>
</div>
<div class="flex-1" />
<div class="flex items-center gap-2">
<label class="px-3 py-1 rounded bg-slate-800/60 border border-border cursor-pointer hover:bg-slate-800">
<input type="file" class="hidden" accept="application/json" @change="onOpen" />
打开
</label>
<button class="px-3 py-1 rounded bg-accent text-slate-900 font-medium hover:brightness-110" @click="onSave">保存/导出</button>
<label class="px-3 py-1 rounded bg-slate-800/60 border border-border cursor-pointer hover:bg-slate-800">
<input type="file" class="hidden" accept="application/json" @change="onImport" />
导入/合并
</label>
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onNew">新建</button>
<span class="w-px h-6 bg-border mx-1" />
<input v-model="actor" placeholder="你的名字(记录历史)" class="px-2 py-1 rounded bg-slate-900 border border-border w-48" />
<span class="w-px h-6 bg-border mx-1" />
<input v-model="q" placeholder="搜索任务…" class="px-2 py-1 rounded bg-slate-900 border border-border w-56" />
<select v-model="cat" class="px-2 py-1 rounded bg-slate-900 border border-border">
<option value="">筛选全部类别</option>
<option v-for="c in store.board.categories" :key="c.uuid" :value="c.uuid">{{ c.title }}</option>
</select>
<span class="w-px h-6 bg-border mx-1" />
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onLoadLocal">从本地读取</button>
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onSaveLocal">保存到本地</button>
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onClearLocal">清空本地</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useBoardStore } from '~/stores/board'
const store = useBoardStore()
const config = useRuntimeConfig().public
const appTitle = computed(() => config.appTitle)
const actor = computed({
get: () => store.board.meta?.actor || '',
set: (v: string) => store.setActor(v)
})
const q = defineModel<string>('query', { required: true })
const cat = defineModel<string | ''>('category', { required: true })
const fileStatus = computed(() => {
const name = store.filename || '(未命名)'
const a = store.board.meta?.actor ? ` | 操作人:${store.board.meta.actor}` : ''
const star = store.dirty ? ' *' : ''
return `${name}${star}${a}`
})
function onOpen(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => {
try {
const json = JSON.parse(String(reader.result))
store.setBoard(json, file.name)
store.log('load-file', { name: file.name, size: file.size })
} catch (err: any) {
alert('解析 JSON 失败:' + err.message)
}
}
reader.readAsText(file)
input.value = ''
}
function onSave() { store.downloadCurrent() }
function onNew() {
if (store.dirty && !confirm('当前更改尚未保存,确定新建吗?')) return
store.setBoard({ categories: [], stages: [], tasks: [], layout: { columns: [] }, meta: { id: 'open-kanban', version: '1.0.0', createdAt: new Date().toISOString(), modifiedAt: new Date().toISOString(), actor: store.board.meta?.actor, history: [] } }, '')
store.log('new-board', {})
}
function onImport(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => {
try {
const data = JSON.parse(String(reader.result))
const diff = store.diffBoards(store.board as any, data)
// emit event for MergeModal
emit('open-merge', data, diff, file.name)
} catch (err: any) {
alert('解析 JSON 失败:' + err.message)
}
}
reader.readAsText(file)
input.value = ''
}
function onLoadLocal() {
if (!store.loadFromLocal()) alert('本地不存在保存的数据')
}
function onSaveLocal() { store.saveToLocal() }
function onClearLocal() {
if (confirm('清空本地保存?')) store.clearLocal()
}
const emit = defineEmits<{ (e: 'open-merge', imported: any, diff: any, name: string): void }>()
</script>