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:
143
pages/index.vue
143
pages/index.vue
@@ -1,106 +1,95 @@
|
||||
<template>
|
||||
<div class="grid" style="grid-template-rows: auto 1fr; height: calc(100vh);">
|
||||
<UContainer class="mx-auto max-w-7xl space-y-6 py-8">
|
||||
<Toolbar v-model:query="query" v-model:category="category" @open-merge="openMerge" />
|
||||
<div class="grid" style="grid-template-columns: 280px 1fr;">
|
||||
<aside class="overflow-auto p-3 border-r border-border bg-panel">
|
||||
<section class="mb-4">
|
||||
<h3 class="text-sm font-semibold text-slate-400 mb-2">类别</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="c in store.board.categories" :key="c.uuid" class="flex items-center gap-2 p-2 rounded border border-border bg-slate-900">
|
||||
<span class="w-4 h-4 rounded" :style="{ background: '#'+(c.color||'888888') }" />
|
||||
<input v-model="c.title" class="flex-1 px-2 py-1 rounded bg-slate-950 border border-border" @change="onCatChange(c)" />
|
||||
<input v-model="c.color" placeholder="hex" class="w-24 px-2 py-1 rounded bg-slate-950 border border-border" @change="onCatChange(c)" />
|
||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="delCat(c.uuid)">删除</button>
|
||||
</div>
|
||||
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="addCat">添加类别</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mb-4 text-sm text-slate-400">
|
||||
<h3 class="text-sm font-semibold text-slate-400 mb-2">布局</h3>
|
||||
<div>列数:{{ store.board.layout.columns.length }}</div>
|
||||
<div>阶段总数:{{ store.board.stages.length }}</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3 class="text-sm font-semibold text-slate-400 mb-2">历史</h3>
|
||||
<div class="flex flex-col gap-2 text-xs max-h-[40vh] overflow-auto">
|
||||
<div v-for="h in (store.board.meta?.history||[]).slice().reverse().slice(0,200)" :key="h.id" class="p-2 rounded border border-border bg-slate-900">{{ h.ts }} {{ h.actor?('@'+h.actor):'' }} {{ h.type }}</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="clearHistory">清空历史(写入本地)</button>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
<main>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[320px_1fr]">
|
||||
<div class="space-y-6">
|
||||
<CategoryPanel />
|
||||
<BoardSummaryPanel />
|
||||
<HistoryPanel />
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<Board :query="query" :category="category" />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Merge modal -->
|
||||
<MergeModal v-model:open="mergeOpen" v-model:imported="imported" v-model:diff="diff" v-model:name="name" />
|
||||
</div>
|
||||
<MergeModal
|
||||
v-model:open="mergeOpen"
|
||||
v-model:imported="imported"
|
||||
v-model:diff="diff"
|
||||
v-model:name="importName"
|
||||
/>
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Toolbar from '~/components/Toolbar.vue'
|
||||
import Board from '~/components/Board.vue'
|
||||
import MergeModal from '~/components/MergeModal.vue'
|
||||
import CategoryPanel from '~/components/panels/CategoryPanel.vue'
|
||||
import BoardSummaryPanel from '~/components/panels/BoardSummaryPanel.vue'
|
||||
import HistoryPanel from '~/components/panels/HistoryPanel.vue'
|
||||
import { useBoardStore } from '~/stores/board'
|
||||
|
||||
const store = useBoardStore()
|
||||
const config = useRuntimeConfig().public
|
||||
const toast = useToast()
|
||||
|
||||
// Try load from query or config
|
||||
onMounted(async () => {
|
||||
const qp = new URL(window.location.href).searchParams.get('file')
|
||||
const query = ref('')
|
||||
const category = ref('')
|
||||
|
||||
const mergeOpen = ref(false)
|
||||
const imported = ref<any>({})
|
||||
const diff = ref<any>({})
|
||||
const importName = ref('导入文件')
|
||||
|
||||
function openMerge(data: any, d: any, name: string) {
|
||||
imported.value = data
|
||||
diff.value = d
|
||||
importName.value = name
|
||||
mergeOpen.value = true
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const currentUrl = typeof window !== 'undefined' ? window.location.href : ''
|
||||
const qp = currentUrl ? new URL(currentUrl).searchParams.get('file') : null
|
||||
const fileToLoad = qp || config.autoLoadFile || ''
|
||||
if (fileToLoad) {
|
||||
try {
|
||||
const res = await fetch(fileToLoad, { cache: 'no-store' })
|
||||
if (res.ok) {
|
||||
store.setBoard(await res.json(), fileToLoad)
|
||||
const data = await res.json()
|
||||
store.setBoard(data, fileToLoad)
|
||||
store.log('load-file', { name: fileToLoad, via: qp ? 'query' : 'config' })
|
||||
toast.add({ color: 'primary', title: `已自动加载 ${fileToLoad}` })
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (error: any) {
|
||||
toast.add({ color: 'rose', title: '自动加载失败', description: error?.message })
|
||||
}
|
||||
}
|
||||
// else try local storage
|
||||
if (!store.loadFromLocal()) {
|
||||
// else set empty
|
||||
store.setBoard({ categories: [], stages: [], tasks: [], layout: { columns: [] }, meta: { id: 'open-kanban', version: '1.0.0', createdAt: new Date().toISOString(), modifiedAt: new Date().toISOString(), actor: '', history: [] } }, '')
|
||||
store.setBoard(
|
||||
{
|
||||
categories: [],
|
||||
stages: [],
|
||||
tasks: [],
|
||||
layout: { columns: [] },
|
||||
meta: {
|
||||
id: 'open-kanban',
|
||||
version: '1.0.0',
|
||||
createdAt: new Date().toISOString(),
|
||||
modifiedAt: new Date().toISOString(),
|
||||
actor: '',
|
||||
history: []
|
||||
}
|
||||
},
|
||||
''
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
bootstrap()
|
||||
})
|
||||
|
||||
const query = ref('')
|
||||
const category = ref('')
|
||||
|
||||
function addCat(){
|
||||
const title = prompt('类别名称?'); if (!title) return
|
||||
const color = prompt('颜色 hex(不含 #)?', '70bafa') || '70bafa'
|
||||
store.board.categories.push({ uuid: crypto.randomUUID?.() || Math.random().toString(36).slice(2), title: title.trim(), color: color.replace(/#/g,'').slice(0,6) })
|
||||
store.log('category-add', {})
|
||||
}
|
||||
function delCat(id: string){
|
||||
// prevent delete if used
|
||||
const used = store.board.tasks.some(t => t.category === id)
|
||||
if (used) return alert('该类别已被任务引用,无法删除')
|
||||
store.board.categories = store.board.categories.filter(c => c.uuid !== id)
|
||||
store.log('category-delete', { id })
|
||||
}
|
||||
function onCatChange(c: any){ store.log('category-update', { id: c.uuid }) }
|
||||
function clearHistory(){
|
||||
if (!store.board.meta) return
|
||||
if (confirm('清空历史记录?将写入本地。')){
|
||||
store.board.meta.history = []
|
||||
store.board.meta.modifiedAt = new Date().toISOString()
|
||||
store.log('history-clear', {})
|
||||
}
|
||||
}
|
||||
|
||||
// Merge modal state
|
||||
const mergeOpen = ref(false)
|
||||
const imported = ref<any>({})
|
||||
const diff = ref<any>({})
|
||||
const name = ref('导入文件')
|
||||
function openMerge(data: any, d: any, n: string){ imported.value = data; diff.value = d; name.value = n; mergeOpen.value = true }
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user