Files
kanban/pages/index.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.8 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="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>