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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user