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.
212 lines
6.1 KiB
Vue
212 lines
6.1 KiB
Vue
<template>
|
||
<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>
|
||
</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>
|
||
|
||
<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>
|
||
</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>
|
||
</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 toast = useToast()
|
||
|
||
interface StepDraft {
|
||
id: string
|
||
details: string
|
||
done: boolean
|
||
}
|
||
|
||
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)
|
||
toast.add({ color: 'rose', title: '任务已删除' })
|
||
deleteOpen.value = false
|
||
open.value = false
|
||
}
|
||
</script>
|