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.
This commit is contained in:
106
components/Toolbar.vue
Normal file
106
components/Toolbar.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user