Integrate the Nuxt UI component library and completely revamp the application's user interface to improve usability, aesthetics, and maintainability. - Replace all custom components and native browser dialogs (`alert`, `prompt`, `confirm`) with Nuxt UI components like `UCard`, `UButton`, `UModal`, and `UNotifications`. - Refactor the main page by extracting the sidebar into dedicated panel components: `CategoryPanel`, `BoardSummaryPanel`, and `HistoryPanel`. - Redesign all major components (Toolbar, Board, Stage, Task) for a cleaner layout and improved information hierarchy. - Implement user-friendly modals for all creation, editing, and deletion flows, providing a more consistent user experience. - Add toast notifications to provide immediate feedback for user actions.
262 lines
7.5 KiB
Vue
262 lines
7.5 KiB
Vue
<template>
|
||
<UCard class="flex h-full flex-col border border-slate-800 bg-slate-950/60">
|
||
<template #header>
|
||
<div class="flex items-start gap-2">
|
||
<div class="min-w-0 flex-1">
|
||
<div v-if="editingTitle" class="space-y-2">
|
||
<UInput
|
||
v-model="titleDraft"
|
||
size="sm"
|
||
placeholder="阶段名称"
|
||
autofocus
|
||
@keyup.enter="saveTitle"
|
||
/>
|
||
<div class="flex items-center gap-2">
|
||
<UButton size="2xs" color="primary" icon="i-heroicons-check-20-solid" @click="saveTitle">
|
||
保存
|
||
</UButton>
|
||
<UButton size="2xs" color="neutral" variant="ghost" @click="cancelEdit">
|
||
取消
|
||
</UButton>
|
||
</div>
|
||
</div>
|
||
<div v-else class="flex flex-wrap items-center gap-2">
|
||
<h3 class="truncate font-semibold">{{ stage?.title || '未命名阶段' }}</h3>
|
||
<UBadge size="xs" color="neutral" variant="soft">{{ tasksCount }} 个任务</UBadge>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<UTooltip text="添加任务">
|
||
<UButton size="xs" color="primary" variant="soft" icon="i-heroicons-plus-20-solid" @click="onAdd" />
|
||
</UTooltip>
|
||
<UDropdown :items="dropdownItems">
|
||
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
|
||
</UDropdown>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div
|
||
class="flex flex-1 flex-col gap-3 overflow-auto rounded-lg border border-dashed border-slate-800 bg-slate-950/50 p-2 transition"
|
||
:data-stage-id="stageId"
|
||
@dragover.prevent="onDragOver"
|
||
@dragleave="over = false"
|
||
@drop.prevent="onDrop"
|
||
:class="{ 'border-sky-400/60 bg-sky-500/10': over }"
|
||
>
|
||
<TaskCard
|
||
v-for="tid in filteredTasks"
|
||
:key="tid"
|
||
:task-id="tid"
|
||
/>
|
||
<div v-if="!filteredTasks.length" class="py-6 text-center text-xs text-slate-500">
|
||
暂无匹配的任务。
|
||
</div>
|
||
</div>
|
||
</UCard>
|
||
|
||
<UModal v-model="moveOpen">
|
||
<UCard>
|
||
<template #header>
|
||
<div class="flex items-center gap-2">
|
||
<UIcon name="i-heroicons-view-columns-20-solid" class="h-5 w-5 text-sky-400" />
|
||
<span class="font-semibold">移动阶段</span>
|
||
</div>
|
||
</template>
|
||
<div class="space-y-4">
|
||
<UFormGroup label="目标列" name="column">
|
||
<USelectMenu
|
||
v-model="moveColumn"
|
||
:options="columnOptions"
|
||
value-attribute="value"
|
||
option-attribute="label"
|
||
/>
|
||
</UFormGroup>
|
||
<div class="flex justify-end gap-2">
|
||
<UButton color="neutral" variant="ghost" @click="moveOpen = false">取消</UButton>
|
||
<UButton color="primary" @click="moveStage">移动</UButton>
|
||
</div>
|
||
</div>
|
||
</UCard>
|
||
</UModal>
|
||
|
||
<UModal v-model="deleteOpen">
|
||
<UCard>
|
||
<template #header>
|
||
<div class="flex items-center gap-2">
|
||
<UIcon name="i-heroicons-exclamation-triangle-20-solid" class="h-5 w-5 text-amber-400" />
|
||
<span class="font-semibold">删除阶段</span>
|
||
</div>
|
||
</template>
|
||
<p class="text-sm text-slate-300">
|
||
确定删除该阶段吗?仅当阶段中没有任务时才能删除。
|
||
</p>
|
||
<div class="mt-6 flex justify-end gap-2">
|
||
<UButton color="neutral" variant="ghost" @click="deleteOpen = false">取消</UButton>
|
||
<UButton color="rose" @click="removeStage">删除</UButton>
|
||
</div>
|
||
</UCard>
|
||
</UModal>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useBoardStore } from '~/stores/board'
|
||
import TaskCard from './TaskCard.vue'
|
||
|
||
const props = defineProps<{ stageId: string; query: string; category: string | ''; columnIndex: number }>()
|
||
const store = useBoardStore()
|
||
const toast = useToast()
|
||
|
||
const stage = computed(() => store.stageById(props.stageId))
|
||
const tasks = computed(() => stage.value?.tasks || [])
|
||
const tasksCount = computed(() => tasks.value.length)
|
||
|
||
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)
|
||
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() {
|
||
const task = store.addTask(props.stageId)
|
||
toast.add({ color: 'primary', title: '已创建新任务', description: task.title })
|
||
}
|
||
|
||
const editingTitle = ref(false)
|
||
const titleDraft = ref('')
|
||
|
||
watch(
|
||
stage,
|
||
(value) => {
|
||
if (!value) return
|
||
titleDraft.value = value.title
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
function startEdit() {
|
||
editingTitle.value = true
|
||
titleDraft.value = stage.value?.title || ''
|
||
}
|
||
|
||
function saveTitle() {
|
||
if (!titleDraft.value.trim()) {
|
||
toast.add({ color: 'rose', title: '阶段名称不能为空' })
|
||
return
|
||
}
|
||
store.renameStage(props.stageId, titleDraft.value.trim())
|
||
editingTitle.value = false
|
||
toast.add({ color: 'neutral', title: '阶段名称已更新' })
|
||
}
|
||
|
||
function cancelEdit() {
|
||
editingTitle.value = false
|
||
titleDraft.value = stage.value?.title || ''
|
||
}
|
||
|
||
const moveOpen = ref(false)
|
||
const moveColumn = ref(props.columnIndex)
|
||
|
||
watch(
|
||
() => props.columnIndex,
|
||
(value) => {
|
||
moveColumn.value = value
|
||
}
|
||
)
|
||
|
||
const columnOptions = computed(() =>
|
||
(store.board.layout?.columns || []).map((_, index) => ({
|
||
label: `列 ${index + 1}`,
|
||
value: index
|
||
}))
|
||
)
|
||
|
||
function moveStage() {
|
||
const cols = store.board.layout.columns
|
||
const target = moveColumn.value
|
||
if (!cols[target]) {
|
||
toast.add({ color: 'rose', title: '目标列不存在' })
|
||
return
|
||
}
|
||
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 })
|
||
toast.add({ color: 'neutral', title: `阶段已移动到第 ${target + 1} 列` })
|
||
moveOpen.value = false
|
||
}
|
||
|
||
const deleteOpen = ref(false)
|
||
|
||
function removeStage() {
|
||
const success = store.deleteStage(props.stageId)
|
||
if (!success) {
|
||
toast.add({ color: 'rose', title: '阶段中仍有任务,无法删除' })
|
||
return
|
||
}
|
||
deleteOpen.value = false
|
||
toast.add({ color: 'neutral', title: '阶段已删除' })
|
||
}
|
||
|
||
const dropdownItems = computed(() => [
|
||
[
|
||
{
|
||
label: '重命名',
|
||
icon: 'i-heroicons-pencil-square-20-solid',
|
||
click: () => startEdit()
|
||
},
|
||
{
|
||
label: '移动到列',
|
||
icon: 'i-heroicons-view-columns-20-solid',
|
||
click: () => {
|
||
moveColumn.value = props.columnIndex
|
||
moveOpen.value = true
|
||
}
|
||
}
|
||
],
|
||
[
|
||
{
|
||
label: '删除阶段',
|
||
icon: 'i-heroicons-trash-20-solid',
|
||
click: () => {
|
||
deleteOpen.value = true
|
||
},
|
||
color: 'rose'
|
||
}
|
||
]
|
||
])
|
||
</script>
|