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

@@ -0,0 +1,213 @@
<template>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-tag-20-solid" class="h-5 w-5 text-sky-400" />
<span class="font-semibold">类别管理</span>
</div>
<UBadge :label="categories.length" color="neutral" variant="subtle" />
</div>
</template>
<div class="space-y-4">
<p v-if="!categories.length" class="text-sm text-slate-400">
当前看板还没有类别新建类别以便在任务中进行筛选和标识
</p>
<div v-else class="space-y-3">
<UCard v-for="cat in categories" :key="cat.uuid" variant="soft" class="border-slate-800">
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<span
class="h-3 w-3 rounded-full ring-2 ring-slate-900"
:style="{ backgroundColor: drafts[cat.uuid]?.color || '#64748b' }"
/>
<UInput
v-model="drafts[cat.uuid].title"
size="sm"
placeholder="类别名称"
@blur="saveCategory(cat.uuid)"
@keyup.enter="saveCategory(cat.uuid)"
/>
<UTooltip text="更新类别">
<UButton icon="i-heroicons-check-20-solid" size="sm" color="primary" variant="soft" @click="saveCategory(cat.uuid)" />
</UTooltip>
<UTooltip text="删除类别">
<UButton icon="i-heroicons-trash-20-solid" size="sm" color="rose" variant="ghost" @click="askRemove(cat)" />
</UTooltip>
</div>
<div class="flex items-center gap-2 text-xs text-slate-400">
<span>颜色</span>
<input
:value="drafts[cat.uuid].color"
type="color"
class="h-8 w-14 cursor-pointer rounded border border-slate-800 bg-transparent"
@input="(e) => updateDraftColor(cat.uuid, e)"
@change="saveCategory(cat.uuid)"
/>
<code class="rounded bg-slate-900/60 px-2 py-1">{{ drafts[cat.uuid].color.toUpperCase() }}</code>
</div>
</div>
</UCard>
</div>
<UButton icon="i-heroicons-plus-circle-20-solid" block color="primary" @click="openCreate">
新建类别
</UButton>
</div>
</UCard>
<UModal v-model="createOpen">
<UCard>
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-plus-circle-20-solid" class="h-5 w-5 text-sky-400" />
<span class="font-semibold">添加类别</span>
</div>
</template>
<form class="space-y-4" @submit.prevent="createCategory">
<UFormGroup label="类别名称" name="title">
<UInput v-model="createForm.title" placeholder="例如:优先级、类型…" autofocus />
</UFormGroup>
<UFormGroup label="颜色" name="color">
<div class="flex items-center gap-3">
<input v-model="createForm.color" type="color" class="h-10 w-16 rounded border border-slate-800 bg-transparent" />
<code class="rounded bg-slate-900/60 px-2 py-1">{{ createForm.color.toUpperCase() }}</code>
</div>
</UFormGroup>
<div class="flex justify-end gap-2">
<UButton color="neutral" variant="ghost" @click="createOpen = false">取消</UButton>
<UButton type="submit" color="primary">保存</UButton>
</div>
</form>
</UCard>
</UModal>
<UModal v-model="removeOpen">
<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>
<div class="space-y-4 text-sm">
<p>
确定要删除类别
<span class="font-medium text-slate-100">{{ removeTarget?.title }}</span>
</p>
<p class="text-slate-400">
若该类别被任务引用将无法删除
</p>
</div>
<div class="flex justify-end gap-2">
<UButton color="neutral" variant="ghost" @click="removeOpen = false">取消</UButton>
<UButton color="rose" @click="confirmRemove">立即删除</UButton>
</div>
</UCard>
</UModal>
</template>
<script setup lang="ts">
import { useBoardStore } from '~/stores/board'
import type { Category } from '~/types/schema'
const store = useBoardStore()
const toast = useToast()
const categories = computed(() => store.board.categories)
const drafts = reactive<Record<string, { title: string; color: string }>>({})
watch(
categories,
(cats) => {
cats.forEach((cat) => {
if (!drafts[cat.uuid]) {
drafts[cat.uuid] = {
title: cat.title,
color: `#${(cat.color || '888888').padStart(6, '0')}`
}
} else {
drafts[cat.uuid].title = cat.title
drafts[cat.uuid].color = `#${(cat.color || '888888').padStart(6, '0')}`
}
})
Object.keys(drafts).forEach((id) => {
if (!cats.find((c) => c.uuid === id)) delete drafts[id]
})
},
{ immediate: true, deep: true }
)
const createOpen = ref(false)
const createForm = reactive({
title: '',
color: '#38bdf8'
})
function openCreate() {
createForm.title = ''
createForm.color = '#38bdf8'
createOpen.value = true
}
function createCategory() {
if (!createForm.title.trim()) {
toast.add({ color: 'rose', title: '类别名称不能为空' })
return
}
const color = createForm.color.replace('#', '').slice(0, 6) || '888888'
const created = store.addCategory(createForm.title.trim(), color)
drafts[created.uuid] = {
title: created.title,
color: `#${color.toUpperCase()}`
}
toast.add({ color: 'primary', title: `已创建类别「${created.title}` })
createOpen.value = false
}
function sanitizeDraft(id: string) {
const draft = drafts[id]
if (!draft) return
draft.title = draft.title.trim() || '未命名类别'
draft.color = `#${draft.color.replace('#', '').slice(0, 6) || '888888'}`
}
function saveCategory(id: string) {
const draft = drafts[id]
if (!draft) return
sanitizeDraft(id)
store.updateCategory(id, {
title: draft.title,
color: draft.color.replace('#', '').slice(0, 6)
})
}
function updateDraftColor(id: string, event: Event) {
const value = (event.target as HTMLInputElement).value
drafts[id].color = value
}
const removeOpen = ref(false)
const removeTarget = ref<Category | null>(null)
function askRemove(cat: Category) {
removeTarget.value = cat
removeOpen.value = true
}
function confirmRemove() {
if (!removeTarget.value) return
const ok = store.removeCategory(removeTarget.value.uuid)
if (!ok) {
toast.add({ color: 'rose', title: '该类别正在被任务引用,无法删除' })
return
}
toast.add({ color: 'neutral', title: `已删除类别「${removeTarget.value.title}` })
removeOpen.value = false
removeTarget.value = null
}
</script>