Files
kanban/components/panels/CategoryPanel.vue
xiaomai 485d75820b 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.
2025-10-22 17:52:17 +08:00

214 lines
7.0 KiB
Vue
Raw Permalink 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>
<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>