Files
kanban/components/StageColumn.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

76 lines
3.4 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="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>