feat(ui): overhaul interface with Nuxt UI

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.
This commit is contained in:
xiaomai
2025-10-22 17:52:17 +08:00
parent 2384e42933
commit 485d75820b
19 changed files with 1823 additions and 318 deletions

View File

@@ -1,16 +1,54 @@
<template>
<div class="grid grid-cols-[20px_1fr_auto] items-center gap-2 p-2 rounded-lg bg-card border border-border shadow" draggable="true" data-task :data-id="taskId" @dragstart="onDragStart">
<div class="opacity-70 select-none cursor-grab"></div>
<div>
<div class="font-semibold">{{ task?.title || '(无标题)' }}</div>
<div class="text-xs text-slate-400">步骤: {{ stat }}</div>
<UCard
class="group cursor-grab select-none border border-slate-800 bg-slate-900/80 transition hover:border-sky-500/40"
data-task
:data-id="taskId"
draggable="true"
@dragstart="onDragStart"
>
<div class="flex items-start gap-3">
<div class="flex h-6 w-6 items-center justify-center rounded-md bg-slate-800/80 text-xs text-slate-400">
<UIcon name="i-heroicons-arrows-up-down-20-solid" class="h-4 w-4" />
</div>
<div class="min-w-0 flex-1 space-y-2">
<div class="flex flex-wrap items-center gap-2">
<h4 class="truncate font-semibold text-slate-100">{{ task?.title || '未命名任务' }}</h4>
<UBadge size="xs" color="neutral" variant="subtle">{{ stat }}</UBadge>
</div>
<div v-if="hasSteps" class="space-y-1">
<UProgress :value="progress" color="primary" class="h-1.5" />
<p class="text-[11px] text-slate-400">步骤完成度 {{ progress }}%</p>
</div>
<p v-if="task?.description" class="line-clamp-2 text-xs text-slate-400">
{{ task.description }}
</p>
</div>
<div class="flex flex-col items-end gap-2">
<UBadge
size="xs"
variant="soft"
:style="{
backgroundColor: `${categoryColor}1A`,
color: categoryColor,
borderColor: `${categoryColor}33`
}"
>
{{ categoryTitle }}
</UBadge>
<UButton
size="2xs"
color="primary"
variant="soft"
icon="i-heroicons-pencil-square-20-solid"
@click.stop="modalOpen = true"
>
详情
</UButton>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-[11px] px-2 py-0.5 rounded-full border border-black/30 whitespace-nowrap" :style="chipStyle">{{ categoryTitle }}</span>
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="openModal">编辑</button>
</div>
</div>
<TaskModal v-if="open" :task-id="taskId" @close="open=false" />
</UCard>
<TaskModal v-model:open="modalOpen" :task-id="taskId" />
</template>
<script setup lang="ts">
@@ -19,21 +57,35 @@ import TaskModal from './TaskModal.vue'
const store = useBoardStore()
const props = defineProps<{ taskId: string }>()
const task = computed(() => store.taskById(props.taskId))
const category = computed(() => store.categoryById(task.value?.category as string))
const categoryTitle = computed(() => category.value?.title || '未分类')
const chipStyle = computed(() => ({ background: `#${(category.value?.color||'888888')}33`, borderColor: `#${(category.value?.color||'888888')}` }))
const stat = computed(() => {
const steps = task.value?.steps || []
const done = steps.filter(s => s.done).length
return `${done}/${steps.length}`
const category = computed(() => {
const id = task.value?.category
return id ? store.categoryById(id) : null
})
const open = ref(false)
function openModal(){ open.value = true }
const categoryTitle = computed(() => category.value?.title || '未分类')
const categoryColor = computed(() => `#${(category.value?.color || '64748b').padStart(6, '0')}`)
const steps = computed(() => task.value?.steps || [])
const hasSteps = computed(() => steps.value.length > 0)
const stat = computed(() => {
const done = steps.value.filter((s) => s.done).length
return `${done}/${steps.value.length}`
})
const progress = computed(() => {
if (!steps.value.length) return 0
const done = steps.value.filter((s) => s.done).length
return Math.round((done / steps.value.length) * 100)
})
const modalOpen = ref(false)
function onDragStart(e: DragEvent) {
e.dataTransfer?.setData('text/task', props.taskId)
e.dataTransfer?.setDragImage(new Image(), 0, 0)
e.dataTransfer!.effectAllowed = 'move'
emit('dragging', true)
}
const emit = defineEmits<{ (e:'dragging', v:boolean): void }>()
</script>