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