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:
75
components/StageColumn.vue
Normal file
75
components/StageColumn.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="rounded-lg border border-border bg-panel">
|
||||
<div class="flex items-center justify-between px-3 py-2 border-b border-border">
|
||||
<div class="font-semibold">{{ stage?.title || '未命名阶段' }}</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onAdd">添加任务</button>
|
||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onRename">重命名</button>
|
||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onMoveCol">移列</button>
|
||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onDelete">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 flex flex-col gap-2 min-h-[60px]" :data-stage-id="stageId" @dragover.prevent="onDragOver" @dragleave="over=false" @drop.prevent="onDrop" :class="{ 'outline outline-2 outline-accent/60 outline-offset-[-4px] rounded-md': over }">
|
||||
<TaskCard v-for="tid in filteredTasks" :key="tid" :task-id="tid" @dragging="(v) => dragging.value = v" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBoardStore } from '~/stores/board'
|
||||
import TaskCard from './TaskCard.vue'
|
||||
|
||||
const store = useBoardStore()
|
||||
const props = defineProps<{ stageId: string; query: string; category: string | '' }>()
|
||||
const stage = computed(() => store.stageById(props.stageId))
|
||||
const tasks = computed(() => stage.value?.tasks || [])
|
||||
const filteredTasks = computed(() => {
|
||||
const q = (props.query || '').toLowerCase()
|
||||
const c = props.category
|
||||
return tasks.value.filter((tid) => {
|
||||
const t = store.taskById(tid)
|
||||
if (!t) return false
|
||||
const hitText = !q || t.title.toLowerCase().includes(q) || (t.description || '').toLowerCase().includes(q)
|
||||
const hitCat = !c || t.category === c
|
||||
return hitText && hitCat
|
||||
})
|
||||
})
|
||||
|
||||
const over = ref(false)
|
||||
const dragging = ref(false)
|
||||
function onDragOver(e: DragEvent) {
|
||||
e.dataTransfer!.dropEffect = 'move'
|
||||
over.value = true
|
||||
}
|
||||
function onDrop(e: DragEvent) {
|
||||
over.value = false
|
||||
const taskId = e.dataTransfer?.getData('text/task'); if (!taskId) return
|
||||
const list = (e.currentTarget as HTMLElement).querySelectorAll('[data-task]')
|
||||
const y = e.clientY
|
||||
let index = list.length
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const rect = (list[i] as HTMLElement).getBoundingClientRect()
|
||||
if (y < rect.top + rect.height / 2) { index = i; break }
|
||||
}
|
||||
store.moveTask(taskId, props.stageId, index)
|
||||
}
|
||||
function onAdd() { store.addTask(props.stageId) }
|
||||
function onRename() {
|
||||
const title = prompt('新的阶段名称?', stage.value?.title || '')
|
||||
if (!title) return
|
||||
store.renameStage(props.stageId, title.trim())
|
||||
}
|
||||
function onDelete() {
|
||||
if (!confirm('删除该阶段?仅空阶段可删除')) return
|
||||
if (!store.deleteStage(props.stageId)) alert('阶段非空或不存在')
|
||||
}
|
||||
function onMoveCol() {
|
||||
const cols = store.board.layout.columns
|
||||
if (!cols.length) return alert('暂无列')
|
||||
const ans = prompt(`移动到第几列(1 - ${cols.length})?`, '1')
|
||||
const target = Math.max(1, Math.min(Number(ans) || 1, cols.length)) - 1
|
||||
cols.forEach((col) => { const i = col.indexOf(props.stageId); if (i !== -1) col.splice(i, 1) })
|
||||
cols[target].push(props.stageId)
|
||||
store.log('stage-move-column', { id: props.stageId, to: target })
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user