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.
107 lines
4.8 KiB
Vue
107 lines
4.8 KiB
Vue
<template>
|
||
<div class="grid" style="grid-template-rows: auto 1fr; height: calc(100vh);">
|
||
<Toolbar v-model:query="query" v-model:category="category" @open-merge="openMerge" />
|
||
<div class="grid" style="grid-template-columns: 280px 1fr;">
|
||
<aside class="overflow-auto p-3 border-r border-border bg-panel">
|
||
<section class="mb-4">
|
||
<h3 class="text-sm font-semibold text-slate-400 mb-2">类别</h3>
|
||
<div class="flex flex-col gap-2">
|
||
<div v-for="c in store.board.categories" :key="c.uuid" class="flex items-center gap-2 p-2 rounded border border-border bg-slate-900">
|
||
<span class="w-4 h-4 rounded" :style="{ background: '#'+(c.color||'888888') }" />
|
||
<input v-model="c.title" class="flex-1 px-2 py-1 rounded bg-slate-950 border border-border" @change="onCatChange(c)" />
|
||
<input v-model="c.color" placeholder="hex" class="w-24 px-2 py-1 rounded bg-slate-950 border border-border" @change="onCatChange(c)" />
|
||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="delCat(c.uuid)">删除</button>
|
||
</div>
|
||
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="addCat">添加类别</button>
|
||
</div>
|
||
</section>
|
||
<section class="mb-4 text-sm text-slate-400">
|
||
<h3 class="text-sm font-semibold text-slate-400 mb-2">布局</h3>
|
||
<div>列数:{{ store.board.layout.columns.length }}</div>
|
||
<div>阶段总数:{{ store.board.stages.length }}</div>
|
||
</section>
|
||
<section>
|
||
<h3 class="text-sm font-semibold text-slate-400 mb-2">历史</h3>
|
||
<div class="flex flex-col gap-2 text-xs max-h-[40vh] overflow-auto">
|
||
<div v-for="h in (store.board.meta?.history||[]).slice().reverse().slice(0,200)" :key="h.id" class="p-2 rounded border border-border bg-slate-900">{{ h.ts }} {{ h.actor?('@'+h.actor):'' }} {{ h.type }}</div>
|
||
</div>
|
||
<div class="mt-2">
|
||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="clearHistory">清空历史(写入本地)</button>
|
||
</div>
|
||
</section>
|
||
</aside>
|
||
<main>
|
||
<Board :query="query" :category="category" />
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Merge modal -->
|
||
<MergeModal v-model:open="mergeOpen" v-model:imported="imported" v-model:diff="diff" v-model:name="name" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import Toolbar from '~/components/Toolbar.vue'
|
||
import Board from '~/components/Board.vue'
|
||
import MergeModal from '~/components/MergeModal.vue'
|
||
import { useBoardStore } from '~/stores/board'
|
||
|
||
const store = useBoardStore()
|
||
const config = useRuntimeConfig().public
|
||
|
||
// Try load from query or config
|
||
onMounted(async () => {
|
||
const qp = new URL(window.location.href).searchParams.get('file')
|
||
const fileToLoad = qp || config.autoLoadFile || ''
|
||
if (fileToLoad) {
|
||
try {
|
||
const res = await fetch(fileToLoad, { cache: 'no-store' })
|
||
if (res.ok) {
|
||
store.setBoard(await res.json(), fileToLoad)
|
||
store.log('load-file', { name: fileToLoad, via: qp ? 'query' : 'config' })
|
||
return
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
// else try local storage
|
||
if (!store.loadFromLocal()) {
|
||
// else set empty
|
||
store.setBoard({ categories: [], stages: [], tasks: [], layout: { columns: [] }, meta: { id: 'open-kanban', version: '1.0.0', createdAt: new Date().toISOString(), modifiedAt: new Date().toISOString(), actor: '', history: [] } }, '')
|
||
}
|
||
})
|
||
|
||
const query = ref('')
|
||
const category = ref('')
|
||
|
||
function addCat(){
|
||
const title = prompt('类别名称?'); if (!title) return
|
||
const color = prompt('颜色 hex(不含 #)?', '70bafa') || '70bafa'
|
||
store.board.categories.push({ uuid: crypto.randomUUID?.() || Math.random().toString(36).slice(2), title: title.trim(), color: color.replace(/#/g,'').slice(0,6) })
|
||
store.log('category-add', {})
|
||
}
|
||
function delCat(id: string){
|
||
// prevent delete if used
|
||
const used = store.board.tasks.some(t => t.category === id)
|
||
if (used) return alert('该类别已被任务引用,无法删除')
|
||
store.board.categories = store.board.categories.filter(c => c.uuid !== id)
|
||
store.log('category-delete', { id })
|
||
}
|
||
function onCatChange(c: any){ store.log('category-update', { id: c.uuid }) }
|
||
function clearHistory(){
|
||
if (!store.board.meta) return
|
||
if (confirm('清空历史记录?将写入本地。')){
|
||
store.board.meta.history = []
|
||
store.board.meta.modifiedAt = new Date().toISOString()
|
||
store.log('history-clear', {})
|
||
}
|
||
}
|
||
|
||
// Merge modal state
|
||
const mergeOpen = ref(false)
|
||
const imported = ref<any>({})
|
||
const diff = ref<any>({})
|
||
const name = ref('导入文件')
|
||
function openMerge(data: any, d: any, n: string){ imported.value = data; diff.value = d; name.value = n; mergeOpen.value = true }
|
||
</script>
|
||
|