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,68 +1,211 @@
<template>
<div class="fixed inset-0 bg-black/60 flex items-center justify-center p-4">
<div class="w-[720px] max-w-[95vw] max-h-[90vh] rounded-lg border border-border bg-panel flex flex-col">
<div class="flex items-center justify-between px-3 py-2 border-b border-border">
<h2 class="font-semibold">编辑任务</h2>
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="$emit('close')"></button>
</div>
<div class="p-3 overflow-auto space-y-3">
<div class="grid grid-cols-[90px_1fr] gap-3 items-start">
<label class="pt-1 text-sm text-slate-400">标题</label>
<input v-model="title" class="px-2 py-1 rounded bg-slate-900 border border-border" />
<UModal v-model="open" :ui="{ width: 'max-w-2xl' }">
<UCard>
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-pencil-square-20-solid" class="h-5 w-5 text-sky-400" />
<span class="font-semibold">编辑任务</span>
</div>
<div class="grid grid-cols-[90px_1fr] gap-3 items-start">
<label class="pt-1 text-sm text-slate-400">类别</label>
<select v-model="category" class="px-2 py-1 rounded bg-slate-900 border border-border">
<option value="">未分类</option>
<option v-for="c in store.board.categories" :key="c.uuid" :value="c.uuid">{{ c.title }}</option>
</select>
</template>
<form class="space-y-5" @submit.prevent="save">
<UFormGroup label="标题" name="title">
<UInput v-model="form.title" placeholder="任务标题" autofocus />
</UFormGroup>
<div class="grid gap-4 sm:grid-cols-2">
<UFormGroup label="类别" name="category">
<USelectMenu
v-model="form.category"
:options="categoryOptions"
value-attribute="value"
option-attribute="label"
searchable
placeholder="选择类别"
/>
</UFormGroup>
<UFormGroup label="摘要" name="summary">
<UInput v-model="summary" placeholder="用于步骤的快速概览(可选)" />
</UFormGroup>
</div>
<div class="grid grid-cols-[90px_1fr] gap-3 items-start">
<label class="pt-1 text-sm text-slate-400">描述</label>
<textarea v-model="desc" rows="6" class="px-2 py-1 rounded bg-slate-900 border border-border"></textarea>
</div>
<div class="grid grid-cols-[90px_1fr] gap-3 items-start">
<label class="pt-1 text-sm text-slate-400">步骤</label>
<div class="space-y-2">
<div v-for="(s, i) in steps" :key="i" class="grid grid-cols-[24px_1fr_24px] items-center gap-2">
<input type="checkbox" v-model="s.done" />
<input v-model="s.details" class="px-2 py-1 rounded bg-slate-900 border border-border" />
<button class="text-sm" @click="steps.splice(i,1)">🗑</button>
<UFormGroup label="详细描述" name="description">
<UTextarea
v-model="form.description"
placeholder="补充任务背景、验收标准等……"
:rows="6"
/>
</UFormGroup>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-slate-200">步骤清单</span>
<UButton size="xs" color="primary" variant="soft" icon="i-heroicons-plus-20-solid" @click="addStep">
添加步骤
</UButton>
</div>
<div v-if="!form.steps.length" class="rounded border border-slate-800 bg-slate-900/70 px-3 py-4 text-sm text-slate-400">
暂无步骤添加步骤帮助团队追踪完成情况
</div>
<div v-else class="space-y-3">
<div
v-for="(step, index) in form.steps"
:key="step.id"
class="flex items-start gap-3 rounded border border-slate-800 bg-slate-900/80 p-3"
>
<UCheckbox v-model="step.done" class="mt-1.5" />
<div class="min-w-0 flex-1 space-y-2">
<UInput
v-model="step.details"
placeholder="步骤内容"
/>
<p class="text-[11px] text-slate-500">步骤 {{ index + 1 }}</p>
</div>
<UTooltip text="删除步骤">
<UButton
size="xs"
color="neutral"
variant="ghost"
icon="i-heroicons-trash-20-solid"
@click="removeStep(index)"
/>
</UTooltip>
</div>
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="steps.push({details:'新步骤', done:false})">添加步骤</button>
</div>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<UButton
color="rose"
variant="soft"
icon="i-heroicons-trash-20-solid"
@click.prevent="deleteOpen = true"
>
删除任务
</UButton>
<div class="flex items-center gap-2">
<UButton color="neutral" variant="ghost" @click="open = false">取消</UButton>
<UButton type="submit" color="primary" icon="i-heroicons-check-20-solid">保存</UButton>
</div>
</div>
</form>
</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="removeTask">永久删除</UButton>
</div>
<div class="flex items-center gap-2 px-3 py-2 border-t border-border">
<button class="px-3 py-1 rounded bg-red-500/90 text-white hover:brightness-110" @click="onDelete">删除</button>
<div class="flex-1" />
<button class="px-3 py-1 rounded bg-accent text-slate-900 font-medium hover:brightness-110" @click="onSave">保存</button>
</div>
</div>
</div>
</UCard>
</UModal>
</template>
<script setup lang="ts">
import { useBoardStore } from '~/stores/board'
import { uuid } from '~/utils/uuid'
const props = defineProps<{ taskId: string }>()
const open = defineModel<boolean>('open', { default: false })
const store = useBoardStore()
const props = defineProps<{ taskId: string }>()
const t = computed(() => store.taskById(props.taskId))
const title = ref(t.value?.title || '')
const desc = ref(t.value?.description || '')
const category = ref<string | ''>((t.value?.category as string) || '')
const steps = ref(JSON.parse(JSON.stringify(t.value?.steps || [])))
const toast = useToast()
function onSave() {
store.editTask(props.taskId, { title: title.value.trim() || t.value?.title, description: desc.value, category: category.value || null, steps: steps.value })
emit('close')
interface StepDraft {
id: string
details: string
done: boolean
}
function onDelete() {
if (!confirm('删除该任务?此操作不可撤销')) return
const form = reactive({
title: '',
category: '' as string | '',
description: '',
steps: [] as StepDraft[]
})
const summary = ref('')
const categoryOptions = computed(() => [
{ label: '未分类', value: '' },
...store.board.categories.map((c) => ({ label: c.title, value: c.uuid }))
])
watch(
() => open.value,
(value) => {
if (value) hydrate()
}
)
watch(
() => props.taskId,
() => {
if (open.value) hydrate()
}
)
function hydrate() {
const task = store.taskById(props.taskId)
if (!task) return
form.title = task.title
form.category = (task.category as string) || ''
form.description = task.description || ''
summary.value = ''
form.steps = (task.steps || []).map((step) => ({
id: uuid(),
details: step.details,
done: step.done
}))
}
function addStep() {
form.steps.push({
id: uuid(),
details: summary.value ? summary.value : `步骤 ${form.steps.length + 1}`,
done: false
})
summary.value = ''
}
function removeStep(index: number) {
form.steps.splice(index, 1)
}
function save() {
if (!form.title.trim()) {
toast.add({ color: 'rose', title: '任务标题不能为空' })
return
}
store.editTask(props.taskId, {
title: form.title.trim(),
description: form.description.trim(),
category: form.category || null,
steps: form.steps.map((step) => ({
details: step.details.trim(),
done: step.done
}))
})
toast.add({ color: 'primary', title: '任务已保存' })
open.value = false
}
const deleteOpen = ref(false)
function removeTask() {
store.removeTask(props.taskId)
emit('close')
toast.add({ color: 'rose', title: '任务已删除' })
deleteOpen.value = false
open.value = false
}
const emit = defineEmits(['close'])
</script>