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:
24
app.config.ts
Normal file
24
app.config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
primary: 'sky',
|
||||||
|
gray: 'slate',
|
||||||
|
notifications: {
|
||||||
|
position: 'bottom-right'
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
default: {
|
||||||
|
color: 'neutral'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
default: {
|
||||||
|
header: {
|
||||||
|
padding: 'px-4 py-3'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: 'px-4 py-4'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
17
app.vue
17
app.vue
@@ -1,11 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-full bg-slate-950 text-slate-100">
|
<UApp>
|
||||||
|
<NuxtLayout>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</NuxtLayout>
|
||||||
|
<UNotifications />
|
||||||
|
</UApp>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const config = useRuntimeConfig().public
|
const config = useRuntimeConfig().public;
|
||||||
useHead({ title: config.appTitle })
|
useHead({
|
||||||
|
titleTemplate: (title) => {
|
||||||
|
if (!title || title === config.appTitle) return config.appTitle;
|
||||||
|
return `${title} · ${config.appTitle}`;
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,95 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-3 overflow-auto grid gap-3" :style="gridStyle">
|
<UCard class="flex h-full flex-col">
|
||||||
<div v-for="(col, colIndex) in columns" :key="colIndex" class="flex flex-col gap-3 min-w-[320px]">
|
<template #header>
|
||||||
<StageColumn v-for="sid in col" :key="sid" :stage-id="sid" :query="query" :category="category" />
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm font-medium text-slate-200">看板阶段</span>
|
||||||
|
<span class="text-xs text-slate-400">
|
||||||
|
共 {{ totalStages }} 个阶段,分布在 {{ columns.length }} 列
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-1" />
|
||||||
|
<UTooltip text="新增阶段">
|
||||||
|
<UButton color="primary" icon="i-heroicons-plus-circle-20-solid" @click="openStageModal()">
|
||||||
|
添加阶段
|
||||||
|
</UButton>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="新增列(用于分组阶段)">
|
||||||
|
<UButton color="neutral" variant="soft" icon="i-heroicons-view-columns-20-solid" @click="addColumn">
|
||||||
|
添加列
|
||||||
|
</UButton>
|
||||||
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!columns.length || !columns[0]?.length" class="text-center text-slate-400 py-10">
|
</template>
|
||||||
暂无阶段。点击左侧“添加阶段”创建。
|
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<div class="flex h-full gap-4 overflow-x-auto pb-6 pr-2">
|
||||||
|
<div
|
||||||
|
v-for="(col, colIndex) in columns"
|
||||||
|
:key="`column-${colIndex}`"
|
||||||
|
class="flex min-w-[320px] flex-col gap-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between text-xs uppercase tracking-wide text-slate-400">
|
||||||
|
<span>列 {{ colIndex + 1 }}</span>
|
||||||
|
<UBadge color="neutral" variant="soft" :label="`${col.length} 个阶段`" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-3 pb-3">
|
<StageColumn
|
||||||
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="addColumn">添加列</button>
|
v-for="sid in col"
|
||||||
|
:key="sid"
|
||||||
|
:stage-id="sid"
|
||||||
|
:query="query"
|
||||||
|
:category="category"
|
||||||
|
:column-index="colIndex"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-3 pb-3">
|
|
||||||
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="addStage">添加阶段</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!columns.length || !totalStages"
|
||||||
|
class="absolute inset-0 flex flex-col items-center justify-center gap-4 text-slate-400"
|
||||||
|
>
|
||||||
|
<UIcon name="i-heroicons-queue-list-20-solid" class="h-10 w-10 text-slate-600" />
|
||||||
|
<div class="text-sm">暂无阶段。点击“添加阶段”开始构建看板。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<StageModal v-model:open="stageModalOpen" :default-column="stageModalColumn" @submit="createStage" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useBoardStore } from '~/stores/board'
|
import { useBoardStore } from '~/stores/board'
|
||||||
import StageColumn from './StageColumn.vue'
|
import StageColumn from './StageColumn.vue'
|
||||||
|
import StageModal from '~/components/dialogs/StageModal.vue'
|
||||||
|
|
||||||
const store = useBoardStore()
|
const store = useBoardStore()
|
||||||
const props = defineProps<{ query: string; category: string | '' }>()
|
|
||||||
const columns = computed(() => store.board.layout?.columns || [])
|
|
||||||
|
|
||||||
const gridStyle = computed(() => ({
|
const props = defineProps<{ query: string; category: string | '' }>()
|
||||||
gridAutoFlow: 'column',
|
|
||||||
gridAutoRows: '1fr'
|
const toast = useToast()
|
||||||
}))
|
|
||||||
|
const columns = computed(() => store.board.layout?.columns || [])
|
||||||
|
const totalStages = computed(() => store.board.stages.length)
|
||||||
|
|
||||||
|
const stageModalOpen = ref(false)
|
||||||
|
const stageModalColumn = ref(0)
|
||||||
|
|
||||||
|
function openStageModal(column = 0) {
|
||||||
|
stageModalColumn.value = column
|
||||||
|
stageModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStage(payload: { title: string; column: number }) {
|
||||||
|
stageModalOpen.value = false
|
||||||
|
const column = Math.max(0, Math.min(payload.column, columns.value.length ? columns.value.length - 1 : 0))
|
||||||
|
store.addStage(payload.title.trim(), column)
|
||||||
|
toast.add({ color: 'primary', title: `已创建阶段「${payload.title}」` })
|
||||||
|
}
|
||||||
|
|
||||||
function addColumn() {
|
function addColumn() {
|
||||||
if (!store.board.layout.columns) store.board.layout.columns = []
|
if (!store.board.layout.columns) store.board.layout.columns = []
|
||||||
store.board.layout.columns.push([])
|
store.board.layout.columns.push([])
|
||||||
store.log('column-add', { count: store.board.layout.columns.length })
|
store.log('column-add', { count: store.board.layout.columns.length })
|
||||||
}
|
toast.add({ color: 'neutral', title: `新增列,当前共有 ${store.board.layout.columns.length} 列` })
|
||||||
function addStage() {
|
|
||||||
const title = prompt('阶段名称?')
|
|
||||||
if (!title) return
|
|
||||||
const colIndex = store.board.layout.columns.length ? 0 : 0
|
|
||||||
store.addStage(title.trim(), colIndex)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,84 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="open" class="fixed inset-0 bg-black/60 flex items-center justify-center p-4">
|
<UModal v-model="open" :ui="{ width: 'max-w-3xl' }">
|
||||||
<div class="w-[1000px] max-w-[95vw] max-h-[90vh] rounded-lg border border-border bg-panel flex flex-col">
|
<UCard>
|
||||||
<div class="flex items-center justify-between px-3 py-2 border-b border-border">
|
<template #header>
|
||||||
<h2 class="font-semibold">导入/合并 预览</h2>
|
<div class="flex items-center justify-between">
|
||||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="close">✕</button>
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-arrow-down-tray-20-solid" class="h-5 w-5 text-sky-400" />
|
||||||
|
<span class="font-semibold">导入/合并预览</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3 overflow-auto space-y-3">
|
<UBadge color="neutral" variant="soft">来源文件:{{ name || '未命名' }}</UBadge>
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div>
|
|
||||||
<label class="text-sm text-slate-400 mr-2">策略</label>
|
|
||||||
<select v-model="policy" class="px-2 py-1 rounded bg-slate-900 border border-border">
|
|
||||||
<option value="prefer-import">冲突优先:导入文件</option>
|
|
||||||
<option value="prefer-current">冲突优先:当前看板</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<label class="text-sm flex items-center gap-2">
|
</template>
|
||||||
<input type="checkbox" v-model="removeMissing" /> 同步删除在导入文件中不存在的任务/阶段
|
|
||||||
</label>
|
<div class="space-y-6">
|
||||||
|
<UAlert
|
||||||
|
color="primary"
|
||||||
|
variant="soft"
|
||||||
|
title="合并前请确认策略"
|
||||||
|
description="导入文件将与当前看板数据对比。请选择冲突策略及是否删除缺失项目。"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
<UFormGroup label="冲突策略" name="policy" class="w-64">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="policy"
|
||||||
|
:options="policies"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
<UCheckbox v-model="removeMissing">
|
||||||
|
同步删除在导入文件中不存在的任务/阶段
|
||||||
|
</UCheckbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
|
||||||
<div class="rounded border border-border bg-slate-900 p-3">
|
<div class="grid gap-3 md:grid-cols-3">
|
||||||
<div class="text-sm text-slate-400">任务新增</div>
|
<UCard variant="soft" class="border border-emerald-500/20 bg-emerald-500/5 text-emerald-200">
|
||||||
<div class="text-2xl font-bold">{{ diff?.tasks?.added?.length || 0 }}</div>
|
<p class="text-xs uppercase tracking-wide opacity-70">任务新增</p>
|
||||||
|
<p class="text-3xl font-semibold">{{ diff?.tasks?.added?.length || 0 }}</p>
|
||||||
|
</UCard>
|
||||||
|
<UCard variant="soft" class="border border-rose-500/20 bg-rose-500/5 text-rose-200">
|
||||||
|
<p class="text-xs uppercase tracking-wide opacity-70">任务删除</p>
|
||||||
|
<p class="text-3xl font-semibold">{{ diff?.tasks?.removed?.length || 0 }}</p>
|
||||||
|
</UCard>
|
||||||
|
<UCard variant="soft" class="border border-amber-500/20 bg-amber-500/5 text-amber-200">
|
||||||
|
<p class="text-xs uppercase tracking-wide opacity-70">任务修改</p>
|
||||||
|
<p class="text-3xl font-semibold">{{ diff?.tasks?.modified?.length || 0 }}</p>
|
||||||
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded border border-border bg-slate-900 p-3">
|
|
||||||
<div class="text-sm text-slate-400">任务删除</div>
|
<UCard variant="soft" class="border border-slate-800 bg-slate-900/70 text-xs text-slate-300">
|
||||||
<div class="text-2xl font-bold">{{ diff?.tasks?.removed?.length || 0 }}</div>
|
<div class="space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-document-duplicate-20-solid" class="h-4 w-4 text-slate-500" />
|
||||||
|
<span>Tasks: +{{ diff?.tasks?.added?.length || 0 }} / -{{ diff?.tasks?.removed?.length || 0 }} / ~{{ diff?.tasks?.modified?.length || 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded border border-border bg-slate-900 p-3">
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-sm text-slate-400">任务修改</div>
|
<UIcon name="i-heroicons-queue-list-20-solid" class="h-4 w-4 text-slate-500" />
|
||||||
<div class="text-2xl font-bold">{{ diff?.tasks?.modified?.length || 0 }}</div>
|
<span>Stages: +{{ diff?.stages?.added?.length || 0 }} / -{{ diff?.stages?.removed?.length || 0 }} / ~{{ diff?.stages?.modified?.length || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-view-columns-20-solid" class="h-4 w-4 text-slate-500" />
|
||||||
|
<span>布局是否变化:{{ diff?.layout?.changed ? '是' : '否' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded border border-border bg-slate-900 p-3 text-xs whitespace-pre-wrap">
|
</UCard>
|
||||||
<div>文件:{{ name }}</div>
|
|
||||||
<div>Tasks: +{{ diff?.tasks?.added?.length || 0 }} -{{ diff?.tasks?.removed?.length || 0 }} ~{{ diff?.tasks?.modified?.length || 0 }}</div>
|
<div class="flex justify-end gap-2">
|
||||||
<div>Stages: +{{ diff?.stages?.added?.length || 0 }} -{{ diff?.stages?.removed?.length || 0 }} ~{{ diff?.stages?.modified?.length || 0 }}</div>
|
<UButton color="neutral" variant="ghost" @click="open = false">取消</UButton>
|
||||||
<div>Layout changed: {{ diff?.layout?.changed ? 'Yes' : 'No' }}</div>
|
<UButton color="primary" icon="i-heroicons-arrow-up-tray-20-solid" @click="apply">应用合并</UButton>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 px-3 py-2 border-t border-border">
|
|
||||||
<button class="px-3 py-1 rounded bg-accent text-slate-900 font-medium hover:brightness-110" @click="apply">应用合并</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useBoardStore } from '~/stores/board'
|
import { useBoardStore } from '~/stores/board'
|
||||||
|
|
||||||
const store = useBoardStore()
|
const store = useBoardStore()
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { required: true })
|
const open = defineModel<boolean>('open', { required: true })
|
||||||
const imported = defineModel<any>('imported', { required: true })
|
const imported = defineModel<any>('imported', { required: true })
|
||||||
const diff = defineModel<any>('diff', { required: true })
|
const diff = defineModel<any>('diff', { required: true })
|
||||||
@@ -57,10 +86,14 @@ const name = defineModel<string>('name', { required: true })
|
|||||||
|
|
||||||
const policy = ref<'prefer-import' | 'prefer-current'>('prefer-import')
|
const policy = ref<'prefer-import' | 'prefer-current'>('prefer-import')
|
||||||
const removeMissing = ref(false)
|
const removeMissing = ref(false)
|
||||||
function close(){ open.value = false }
|
|
||||||
function apply(){
|
const policies = [
|
||||||
|
{ label: '冲突优先:导入文件', value: 'prefer-import' },
|
||||||
|
{ label: '冲突优先:当前看板', value: 'prefer-current' }
|
||||||
|
]
|
||||||
|
|
||||||
|
function apply() {
|
||||||
store.applyMerge(imported.value, policy.value, removeMissing.value)
|
store.applyMerge(imported.value, policy.value, removeMissing.value)
|
||||||
close()
|
open.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,75 +1,261 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-lg border border-border bg-panel">
|
<UCard class="flex h-full flex-col border border-slate-800 bg-slate-950/60">
|
||||||
<div class="flex items-center justify-between px-3 py-2 border-b border-border">
|
<template #header>
|
||||||
<div class="font-semibold">{{ stage?.title || '未命名阶段' }}</div>
|
<div class="flex items-start gap-2">
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="min-w-0 flex-1">
|
||||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onAdd">添加任务</button>
|
<div v-if="editingTitle" class="space-y-2">
|
||||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onRename">重命名</button>
|
<UInput
|
||||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onMoveCol">移列</button>
|
v-model="titleDraft"
|
||||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onDelete">删除</button>
|
size="sm"
|
||||||
|
placeholder="阶段名称"
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="saveTitle"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UButton size="2xs" color="primary" icon="i-heroicons-check-20-solid" @click="saveTitle">
|
||||||
|
保存
|
||||||
|
</UButton>
|
||||||
|
<UButton size="2xs" color="neutral" variant="ghost" @click="cancelEdit">
|
||||||
|
取消
|
||||||
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-2 flex flex-col gap-2 min-h-[60px]" :data-stage-id="stageId" @dragover.prevent="onDragOver" @dragleave="over=false" @drop.prevent="onDrop" :class="{ 'outline outline-2 outline-accent/60 outline-offset-[-4px] rounded-md': over }">
|
<div v-else class="flex flex-wrap items-center gap-2">
|
||||||
<TaskCard v-for="tid in filteredTasks" :key="tid" :task-id="tid" @dragging="(v) => dragging.value = v" />
|
<h3 class="truncate font-semibold">{{ stage?.title || '未命名阶段' }}</h3>
|
||||||
|
<UBadge size="xs" color="neutral" variant="soft">{{ tasksCount }} 个任务</UBadge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<UTooltip text="添加任务">
|
||||||
|
<UButton size="xs" color="primary" variant="soft" icon="i-heroicons-plus-20-solid" @click="onAdd" />
|
||||||
|
</UTooltip>
|
||||||
|
<UDropdown :items="dropdownItems">
|
||||||
|
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
|
||||||
|
</UDropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-1 flex-col gap-3 overflow-auto rounded-lg border border-dashed border-slate-800 bg-slate-950/50 p-2 transition"
|
||||||
|
:data-stage-id="stageId"
|
||||||
|
@dragover.prevent="onDragOver"
|
||||||
|
@dragleave="over = false"
|
||||||
|
@drop.prevent="onDrop"
|
||||||
|
:class="{ 'border-sky-400/60 bg-sky-500/10': over }"
|
||||||
|
>
|
||||||
|
<TaskCard
|
||||||
|
v-for="tid in filteredTasks"
|
||||||
|
:key="tid"
|
||||||
|
:task-id="tid"
|
||||||
|
/>
|
||||||
|
<div v-if="!filteredTasks.length" class="py-6 text-center text-xs text-slate-500">
|
||||||
|
暂无匹配的任务。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UModal v-model="moveOpen">
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-view-columns-20-solid" class="h-5 w-5 text-sky-400" />
|
||||||
|
<span class="font-semibold">移动阶段</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<UFormGroup label="目标列" name="column">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="moveColumn"
|
||||||
|
:options="columnOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<UButton color="neutral" variant="ghost" @click="moveOpen = false">取消</UButton>
|
||||||
|
<UButton color="primary" @click="moveStage">移动</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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="removeStage">删除</UButton>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useBoardStore } from '~/stores/board'
|
import { useBoardStore } from '~/stores/board'
|
||||||
import TaskCard from './TaskCard.vue'
|
import TaskCard from './TaskCard.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{ stageId: string; query: string; category: string | ''; columnIndex: number }>()
|
||||||
const store = useBoardStore()
|
const store = useBoardStore()
|
||||||
const props = defineProps<{ stageId: string; query: string; category: string | '' }>()
|
const toast = useToast()
|
||||||
|
|
||||||
const stage = computed(() => store.stageById(props.stageId))
|
const stage = computed(() => store.stageById(props.stageId))
|
||||||
const tasks = computed(() => stage.value?.tasks || [])
|
const tasks = computed(() => stage.value?.tasks || [])
|
||||||
|
const tasksCount = computed(() => tasks.value.length)
|
||||||
|
|
||||||
const filteredTasks = computed(() => {
|
const filteredTasks = computed(() => {
|
||||||
const q = (props.query || '').toLowerCase()
|
const q = (props.query || '').toLowerCase()
|
||||||
const c = props.category
|
const c = props.category
|
||||||
return tasks.value.filter((tid) => {
|
return tasks.value.filter((tid) => {
|
||||||
const t = store.taskById(tid)
|
const t = store.taskById(tid)
|
||||||
if (!t) return false
|
if (!t) return false
|
||||||
const hitText = !q || t.title.toLowerCase().includes(q) || (t.description || '').toLowerCase().includes(q)
|
const hitText =
|
||||||
|
!q ||
|
||||||
|
t.title.toLowerCase().includes(q) ||
|
||||||
|
(t.description || '').toLowerCase().includes(q)
|
||||||
const hitCat = !c || t.category === c
|
const hitCat = !c || t.category === c
|
||||||
return hitText && hitCat
|
return hitText && hitCat
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const over = ref(false)
|
const over = ref(false)
|
||||||
const dragging = ref(false)
|
|
||||||
function onDragOver(e: DragEvent) {
|
function onDragOver(e: DragEvent) {
|
||||||
e.dataTransfer!.dropEffect = 'move'
|
e.dataTransfer!.dropEffect = 'move'
|
||||||
over.value = true
|
over.value = true
|
||||||
}
|
}
|
||||||
function onDrop(e: DragEvent) {
|
function onDrop(e: DragEvent) {
|
||||||
over.value = false
|
over.value = false
|
||||||
const taskId = e.dataTransfer?.getData('text/task'); if (!taskId) return
|
const taskId = e.dataTransfer?.getData('text/task')
|
||||||
|
if (!taskId) return
|
||||||
const list = (e.currentTarget as HTMLElement).querySelectorAll('[data-task]')
|
const list = (e.currentTarget as HTMLElement).querySelectorAll('[data-task]')
|
||||||
const y = e.clientY
|
const y = e.clientY
|
||||||
let index = list.length
|
let index = list.length
|
||||||
for (let i = 0; i < list.length; i++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
const rect = (list[i] as HTMLElement).getBoundingClientRect()
|
const rect = (list[i] as HTMLElement).getBoundingClientRect()
|
||||||
if (y < rect.top + rect.height / 2) { index = i; break }
|
if (y < rect.top + rect.height / 2) {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
store.moveTask(taskId, props.stageId, index)
|
store.moveTask(taskId, props.stageId, index)
|
||||||
}
|
}
|
||||||
function onAdd() { store.addTask(props.stageId) }
|
|
||||||
function onRename() {
|
function onAdd() {
|
||||||
const title = prompt('新的阶段名称?', stage.value?.title || '')
|
const task = store.addTask(props.stageId)
|
||||||
if (!title) return
|
toast.add({ color: 'primary', title: '已创建新任务', description: task.title })
|
||||||
store.renameStage(props.stageId, title.trim())
|
|
||||||
}
|
}
|
||||||
function onDelete() {
|
|
||||||
if (!confirm('删除该阶段?仅空阶段可删除')) return
|
const editingTitle = ref(false)
|
||||||
if (!store.deleteStage(props.stageId)) alert('阶段非空或不存在')
|
const titleDraft = ref('')
|
||||||
|
|
||||||
|
watch(
|
||||||
|
stage,
|
||||||
|
(value) => {
|
||||||
|
if (!value) return
|
||||||
|
titleDraft.value = value.title
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function startEdit() {
|
||||||
|
editingTitle.value = true
|
||||||
|
titleDraft.value = stage.value?.title || ''
|
||||||
}
|
}
|
||||||
function onMoveCol() {
|
|
||||||
|
function saveTitle() {
|
||||||
|
if (!titleDraft.value.trim()) {
|
||||||
|
toast.add({ color: 'rose', title: '阶段名称不能为空' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.renameStage(props.stageId, titleDraft.value.trim())
|
||||||
|
editingTitle.value = false
|
||||||
|
toast.add({ color: 'neutral', title: '阶段名称已更新' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
editingTitle.value = false
|
||||||
|
titleDraft.value = stage.value?.title || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveOpen = ref(false)
|
||||||
|
const moveColumn = ref(props.columnIndex)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.columnIndex,
|
||||||
|
(value) => {
|
||||||
|
moveColumn.value = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const columnOptions = computed(() =>
|
||||||
|
(store.board.layout?.columns || []).map((_, index) => ({
|
||||||
|
label: `列 ${index + 1}`,
|
||||||
|
value: index
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
function moveStage() {
|
||||||
const cols = store.board.layout.columns
|
const cols = store.board.layout.columns
|
||||||
if (!cols.length) return alert('暂无列')
|
const target = moveColumn.value
|
||||||
const ans = prompt(`移动到第几列(1 - ${cols.length})?`, '1')
|
if (!cols[target]) {
|
||||||
const target = Math.max(1, Math.min(Number(ans) || 1, cols.length)) - 1
|
toast.add({ color: 'rose', title: '目标列不存在' })
|
||||||
cols.forEach((col) => { const i = col.indexOf(props.stageId); if (i !== -1) col.splice(i, 1) })
|
return
|
||||||
|
}
|
||||||
|
cols.forEach((col) => {
|
||||||
|
const i = col.indexOf(props.stageId)
|
||||||
|
if (i !== -1) col.splice(i, 1)
|
||||||
|
})
|
||||||
cols[target].push(props.stageId)
|
cols[target].push(props.stageId)
|
||||||
store.log('stage-move-column', { id: props.stageId, to: target })
|
store.log('stage-move-column', { id: props.stageId, to: target })
|
||||||
|
toast.add({ color: 'neutral', title: `阶段已移动到第 ${target + 1} 列` })
|
||||||
|
moveOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteOpen = ref(false)
|
||||||
|
|
||||||
|
function removeStage() {
|
||||||
|
const success = store.deleteStage(props.stageId)
|
||||||
|
if (!success) {
|
||||||
|
toast.add({ color: 'rose', title: '阶段中仍有任务,无法删除' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deleteOpen.value = false
|
||||||
|
toast.add({ color: 'neutral', title: '阶段已删除' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdownItems = computed(() => [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: '重命名',
|
||||||
|
icon: 'i-heroicons-pencil-square-20-solid',
|
||||||
|
click: () => startEdit()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '移动到列',
|
||||||
|
icon: 'i-heroicons-view-columns-20-solid',
|
||||||
|
click: () => {
|
||||||
|
moveColumn.value = props.columnIndex
|
||||||
|
moveOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: '删除阶段',
|
||||||
|
icon: 'i-heroicons-trash-20-solid',
|
||||||
|
click: () => {
|
||||||
|
deleteOpen.value = true
|
||||||
|
},
|
||||||
|
color: 'rose'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,16 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-[20px_1fr_auto] items-center gap-2 p-2 rounded-lg bg-card border border-border shadow" draggable="true" data-task :data-id="taskId" @dragstart="onDragStart">
|
<UCard
|
||||||
<div class="opacity-70 select-none cursor-grab">⋮⋮</div>
|
class="group cursor-grab select-none border border-slate-800 bg-slate-900/80 transition hover:border-sky-500/40"
|
||||||
<div>
|
data-task
|
||||||
<div class="font-semibold">{{ task?.title || '(无标题)' }}</div>
|
:data-id="taskId"
|
||||||
<div class="text-xs text-slate-400">步骤: {{ stat }}</div>
|
draggable="true"
|
||||||
|
@dragstart="onDragStart"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="flex h-6 w-6 items-center justify-center rounded-md bg-slate-800/80 text-xs text-slate-400">
|
||||||
|
<UIcon name="i-heroicons-arrows-up-down-20-solid" class="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="min-w-0 flex-1 space-y-2">
|
||||||
<span class="text-[11px] px-2 py-0.5 rounded-full border border-black/30 whitespace-nowrap" :style="chipStyle">{{ categoryTitle }}</span>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="openModal">编辑</button>
|
<h4 class="truncate font-semibold text-slate-100">{{ task?.title || '未命名任务' }}</h4>
|
||||||
|
<UBadge size="xs" color="neutral" variant="subtle">{{ stat }}</UBadge>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasSteps" class="space-y-1">
|
||||||
|
<UProgress :value="progress" color="primary" class="h-1.5" />
|
||||||
|
<p class="text-[11px] text-slate-400">步骤完成度 {{ progress }}%</p>
|
||||||
|
</div>
|
||||||
|
<p v-if="task?.description" class="line-clamp-2 text-xs text-slate-400">
|
||||||
|
{{ task.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end gap-2">
|
||||||
|
<UBadge
|
||||||
|
size="xs"
|
||||||
|
variant="soft"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: `${categoryColor}1A`,
|
||||||
|
color: categoryColor,
|
||||||
|
borderColor: `${categoryColor}33`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ categoryTitle }}
|
||||||
|
</UBadge>
|
||||||
|
<UButton
|
||||||
|
size="2xs"
|
||||||
|
color="primary"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-pencil-square-20-solid"
|
||||||
|
@click.stop="modalOpen = true"
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TaskModal v-if="open" :task-id="taskId" @close="open=false" />
|
</UCard>
|
||||||
|
|
||||||
|
<TaskModal v-model:open="modalOpen" :task-id="taskId" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -19,21 +57,35 @@ import TaskModal from './TaskModal.vue'
|
|||||||
|
|
||||||
const store = useBoardStore()
|
const store = useBoardStore()
|
||||||
const props = defineProps<{ taskId: string }>()
|
const props = defineProps<{ taskId: string }>()
|
||||||
|
|
||||||
const task = computed(() => store.taskById(props.taskId))
|
const task = computed(() => store.taskById(props.taskId))
|
||||||
const category = computed(() => store.categoryById(task.value?.category as string))
|
const category = computed(() => {
|
||||||
const categoryTitle = computed(() => category.value?.title || '未分类')
|
const id = task.value?.category
|
||||||
const chipStyle = computed(() => ({ background: `#${(category.value?.color||'888888')}33`, borderColor: `#${(category.value?.color||'888888')}` }))
|
return id ? store.categoryById(id) : null
|
||||||
const stat = computed(() => {
|
|
||||||
const steps = task.value?.steps || []
|
|
||||||
const done = steps.filter(s => s.done).length
|
|
||||||
return `${done}/${steps.length}`
|
|
||||||
})
|
})
|
||||||
const open = ref(false)
|
|
||||||
function openModal(){ open.value = true }
|
const categoryTitle = computed(() => category.value?.title || '未分类')
|
||||||
|
const categoryColor = computed(() => `#${(category.value?.color || '64748b').padStart(6, '0')}`)
|
||||||
|
|
||||||
|
const steps = computed(() => task.value?.steps || [])
|
||||||
|
const hasSteps = computed(() => steps.value.length > 0)
|
||||||
|
|
||||||
|
const stat = computed(() => {
|
||||||
|
const done = steps.value.filter((s) => s.done).length
|
||||||
|
return `${done}/${steps.value.length}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const progress = computed(() => {
|
||||||
|
if (!steps.value.length) return 0
|
||||||
|
const done = steps.value.filter((s) => s.done).length
|
||||||
|
return Math.round((done / steps.value.length) * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
const modalOpen = ref(false)
|
||||||
|
|
||||||
function onDragStart(e: DragEvent) {
|
function onDragStart(e: DragEvent) {
|
||||||
e.dataTransfer?.setData('text/task', props.taskId)
|
e.dataTransfer?.setData('text/task', props.taskId)
|
||||||
|
e.dataTransfer?.setDragImage(new Image(), 0, 0)
|
||||||
e.dataTransfer!.effectAllowed = 'move'
|
e.dataTransfer!.effectAllowed = 'move'
|
||||||
emit('dragging', true)
|
|
||||||
}
|
}
|
||||||
const emit = defineEmits<{ (e:'dragging', v:boolean): void }>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,68 +1,211 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fixed inset-0 bg-black/60 flex items-center justify-center p-4">
|
<UModal v-model="open" :ui="{ width: 'max-w-2xl' }">
|
||||||
<div class="w-[720px] max-w-[95vw] max-h-[90vh] rounded-lg border border-border bg-panel flex flex-col">
|
<UCard>
|
||||||
<div class="flex items-center justify-between px-3 py-2 border-b border-border">
|
<template #header>
|
||||||
<h2 class="font-semibold">编辑任务</h2>
|
<div class="flex items-center gap-2">
|
||||||
<button class="px-2 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="$emit('close')">✕</button>
|
<UIcon name="i-heroicons-pencil-square-20-solid" class="h-5 w-5 text-sky-400" />
|
||||||
|
<span class="font-semibold">编辑任务</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3 overflow-auto space-y-3">
|
</template>
|
||||||
<div class="grid grid-cols-[90px_1fr] gap-3 items-start">
|
|
||||||
<label class="pt-1 text-sm text-slate-400">标题</label>
|
<form class="space-y-5" @submit.prevent="save">
|
||||||
<input v-model="title" class="px-2 py-1 rounded bg-slate-900 border border-border" />
|
<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>
|
||||||
<div class="grid grid-cols-[90px_1fr] gap-3 items-start">
|
|
||||||
<label class="pt-1 text-sm text-slate-400">类别</label>
|
<UFormGroup label="详细描述" name="description">
|
||||||
<select v-model="category" class="px-2 py-1 rounded bg-slate-900 border border-border">
|
<UTextarea
|
||||||
<option value="">未分类</option>
|
v-model="form.description"
|
||||||
<option v-for="c in store.board.categories" :key="c.uuid" :value="c.uuid">{{ c.title }}</option>
|
placeholder="补充任务背景、验收标准等……"
|
||||||
</select>
|
: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>
|
||||||
<div class="grid grid-cols-[90px_1fr] gap-3 items-start">
|
<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">
|
||||||
<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>
|
||||||
<div class="grid grid-cols-[90px_1fr] gap-3 items-start">
|
<div v-else class="space-y-3">
|
||||||
<label class="pt-1 text-sm text-slate-400">步骤</label>
|
<div
|
||||||
<div class="space-y-2">
|
v-for="(step, index) in form.steps"
|
||||||
<div v-for="(s, i) in steps" :key="i" class="grid grid-cols-[24px_1fr_24px] items-center gap-2">
|
:key="step.id"
|
||||||
<input type="checkbox" v-model="s.done" />
|
class="flex items-start gap-3 rounded border border-slate-800 bg-slate-900/80 p-3"
|
||||||
<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>
|
<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>
|
</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>
|
<UTooltip text="删除步骤">
|
||||||
</div>
|
<UButton
|
||||||
</div>
|
size="xs"
|
||||||
</div>
|
color="neutral"
|
||||||
<div class="flex items-center gap-2 px-3 py-2 border-t border-border">
|
variant="ghost"
|
||||||
<button class="px-3 py-1 rounded bg-red-500/90 text-white hover:brightness-110" @click="onDelete">删除</button>
|
icon="i-heroicons-trash-20-solid"
|
||||||
<div class="flex-1" />
|
@click="removeStep(index)"
|
||||||
<button class="px-3 py-1 rounded bg-accent text-slate-900 font-medium hover:brightness-110" @click="onSave">保存</button>
|
/>
|
||||||
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useBoardStore } from '~/stores/board'
|
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 store = useBoardStore()
|
||||||
const props = defineProps<{ taskId: string }>()
|
const toast = useToast()
|
||||||
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 || [])))
|
|
||||||
|
|
||||||
function onSave() {
|
interface StepDraft {
|
||||||
store.editTask(props.taskId, { title: title.value.trim() || t.value?.title, description: desc.value, category: category.value || null, steps: steps.value })
|
id: string
|
||||||
emit('close')
|
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)
|
store.removeTask(props.taskId)
|
||||||
emit('close')
|
toast.add({ color: 'rose', title: '任务已删除' })
|
||||||
|
deleteOpen.value = false
|
||||||
|
open.value = false
|
||||||
}
|
}
|
||||||
const emit = defineEmits(['close'])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-center gap-2 p-3 border-b border-border bg-panel">
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1 class="text-lg font-semibold">{{ appTitle }}</h1>
|
<UIcon name="i-heroicons-squares-2x2-20-solid" class="h-5 w-5 text-sky-400" />
|
||||||
<span class="text-xs text-slate-400">{{ fileStatus }}</span>
|
<span class="text-lg font-semibold">{{ appTitle }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1" />
|
<UBadge
|
||||||
|
:color="store.dirty ? 'primary' : 'neutral'"
|
||||||
|
variant="soft"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ fileStatus }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<UButton color="neutral" variant="soft" icon="i-heroicons-folder-open-20-solid" @click="triggerOpen">
|
||||||
|
打开文件
|
||||||
|
</UButton>
|
||||||
|
<UButton color="primary" icon="i-heroicons-arrow-down-tray-20-solid" @click="onSave">
|
||||||
|
导出 JSON
|
||||||
|
</UButton>
|
||||||
|
<UButton color="neutral" variant="soft" icon="i-heroicons-arrow-path-rounded-square-20-solid" @click="triggerImport">
|
||||||
|
导入/合并
|
||||||
|
</UButton>
|
||||||
|
<UButton color="neutral" variant="ghost" icon="i-heroicons-plus-circle-20-solid" @click="newOpen = true">
|
||||||
|
新建看板
|
||||||
|
</UButton>
|
||||||
|
<UDropdown :items="localMenu" mode="hover">
|
||||||
|
<UButton color="neutral" variant="ghost" icon="i-heroicons-device-phone-mobile-20-solid">
|
||||||
|
本地存储
|
||||||
|
</UButton>
|
||||||
|
</UDropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid gap-3 lg:grid-cols-4">
|
||||||
|
<UInput
|
||||||
|
v-model="q"
|
||||||
|
icon="i-heroicons-magnifying-glass-20-solid"
|
||||||
|
placeholder="搜索任务(标题或描述)"
|
||||||
|
color="neutral"
|
||||||
|
/>
|
||||||
|
<USelectMenu
|
||||||
|
v-model="cat"
|
||||||
|
:options="categoryOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
searchable
|
||||||
|
placeholder="筛选类别"
|
||||||
|
/>
|
||||||
|
<UInput
|
||||||
|
v-model="actor"
|
||||||
|
icon="i-heroicons-user-circle-20-solid"
|
||||||
|
placeholder="记录操作人名称(写入历史)"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center justify-end text-xs text-slate-400">
|
||||||
|
最近保存于:{{ lastSaved }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<input ref="openInput" type="file" accept="application/json" class="hidden" @change="handleOpen" />
|
||||||
|
<input ref="importInput" type="file" accept="application/json" class="hidden" @change="handleImport" />
|
||||||
|
|
||||||
|
<UModal v-model="newOpen">
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label class="px-3 py-1 rounded bg-slate-800/60 border border-border cursor-pointer hover:bg-slate-800">
|
<UIcon name="i-heroicons-plus-circle-20-solid" class="h-5 w-5 text-sky-400" />
|
||||||
<input type="file" class="hidden" accept="application/json" @change="onOpen" />
|
<span class="font-semibold">新建看板</span>
|
||||||
打开…
|
|
||||||
</label>
|
|
||||||
<button class="px-3 py-1 rounded bg-accent text-slate-900 font-medium hover:brightness-110" @click="onSave">保存/导出</button>
|
|
||||||
<label class="px-3 py-1 rounded bg-slate-800/60 border border-border cursor-pointer hover:bg-slate-800">
|
|
||||||
<input type="file" class="hidden" accept="application/json" @change="onImport" />
|
|
||||||
导入/合并…
|
|
||||||
</label>
|
|
||||||
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onNew">新建</button>
|
|
||||||
<span class="w-px h-6 bg-border mx-1" />
|
|
||||||
<input v-model="actor" placeholder="你的名字(记录历史)" class="px-2 py-1 rounded bg-slate-900 border border-border w-48" />
|
|
||||||
<span class="w-px h-6 bg-border mx-1" />
|
|
||||||
<input v-model="q" placeholder="搜索任务…" class="px-2 py-1 rounded bg-slate-900 border border-border w-56" />
|
|
||||||
<select v-model="cat" 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>
|
|
||||||
<span class="w-px h-6 bg-border mx-1" />
|
|
||||||
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onLoadLocal">从本地读取</button>
|
|
||||||
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onSaveLocal">保存到本地</button>
|
|
||||||
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="onClearLocal">清空本地</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="space-y-4 text-sm text-slate-300">
|
||||||
|
<p>当前更改尚未导出。继续新建将清空现有看板数据。</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-6 flex justify-end gap-2">
|
||||||
|
<UButton color="neutral" variant="ghost" @click="newOpen = false">取消</UButton>
|
||||||
|
<UButton color="primary" @click="createNewBoard">确认新建</UButton>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
|
||||||
|
<UModal v-model="clearLocalOpen">
|
||||||
|
<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="clearLocalOpen = false">取消</UButton>
|
||||||
|
<UButton color="rose" @click="clearLocalData">清空数据</UButton>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -37,6 +108,8 @@ import { useBoardStore } from '~/stores/board'
|
|||||||
|
|
||||||
const store = useBoardStore()
|
const store = useBoardStore()
|
||||||
const config = useRuntimeConfig().public
|
const config = useRuntimeConfig().public
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
const appTitle = computed(() => config.appTitle)
|
const appTitle = computed(() => config.appTitle)
|
||||||
const actor = computed({
|
const actor = computed({
|
||||||
get: () => store.board.meta?.actor || '',
|
get: () => store.board.meta?.actor || '',
|
||||||
@@ -45,15 +118,40 @@ const actor = computed({
|
|||||||
const q = defineModel<string>('query', { required: true })
|
const q = defineModel<string>('query', { required: true })
|
||||||
const cat = defineModel<string | ''>('category', { required: true })
|
const cat = defineModel<string | ''>('category', { required: true })
|
||||||
|
|
||||||
|
const openInput = ref<HTMLInputElement | null>(null)
|
||||||
|
const importInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
const categoryOptions = computed(() => [
|
||||||
|
{ label: '全部类别', value: '' },
|
||||||
|
...store.board.categories.map((c) => ({ label: c.title, value: c.uuid }))
|
||||||
|
])
|
||||||
|
|
||||||
const fileStatus = computed(() => {
|
const fileStatus = computed(() => {
|
||||||
const name = store.filename || '(未命名)'
|
const name = store.filename || '未命名'
|
||||||
const a = store.board.meta?.actor ? ` | 操作人:${store.board.meta.actor}` : ''
|
const indicator = store.dirty ? '(未保存修改)' : '(已同步)'
|
||||||
const star = store.dirty ? ' *' : ''
|
return `${name} ${indicator}`
|
||||||
return `${name}${star}${a}`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function onOpen(e: Event) {
|
const lastSaved = computed(() => {
|
||||||
const input = e.target as HTMLInputElement
|
const ts = store.board.meta?.modifiedAt
|
||||||
|
if (!ts) return '尚未记录'
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(ts))
|
||||||
|
} catch {
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function triggerOpen() {
|
||||||
|
openInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerImport() {
|
||||||
|
importInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpen(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
const file = input.files?.[0]
|
const file = input.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
@@ -62,21 +160,19 @@ function onOpen(e: Event) {
|
|||||||
const json = JSON.parse(String(reader.result))
|
const json = JSON.parse(String(reader.result))
|
||||||
store.setBoard(json, file.name)
|
store.setBoard(json, file.name)
|
||||||
store.log('load-file', { name: file.name, size: file.size })
|
store.log('load-file', { name: file.name, size: file.size })
|
||||||
|
toast.add({ color: 'primary', title: `已载入 ${file.name}` })
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert('解析 JSON 失败:' + err.message)
|
toast.add({ color: 'rose', title: '解析 JSON 失败', description: err?.message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.readAsText(file)
|
reader.readAsText(file)
|
||||||
input.value = ''
|
input.value = ''
|
||||||
}
|
}
|
||||||
function onSave() { store.downloadCurrent() }
|
|
||||||
function onNew() {
|
const emit = defineEmits<{ (e: 'open-merge', imported: any, diff: any, name: string): void }>()
|
||||||
if (store.dirty && !confirm('当前更改尚未保存,确定新建吗?')) return
|
|
||||||
store.setBoard({ categories: [], stages: [], tasks: [], layout: { columns: [] }, meta: { id: 'open-kanban', version: '1.0.0', createdAt: new Date().toISOString(), modifiedAt: new Date().toISOString(), actor: store.board.meta?.actor, history: [] } }, '')
|
function handleImport(event: Event) {
|
||||||
store.log('new-board', {})
|
const input = event.target as HTMLInputElement
|
||||||
}
|
|
||||||
function onImport(e: Event) {
|
|
||||||
const input = e.target as HTMLInputElement
|
|
||||||
const file = input.files?.[0]
|
const file = input.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
@@ -84,23 +180,89 @@ function onImport(e: Event) {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(String(reader.result))
|
const data = JSON.parse(String(reader.result))
|
||||||
const diff = store.diffBoards(store.board as any, data)
|
const diff = store.diffBoards(store.board as any, data)
|
||||||
// emit event for MergeModal
|
|
||||||
emit('open-merge', data, diff, file.name)
|
emit('open-merge', data, diff, file.name)
|
||||||
|
toast.add({ color: 'neutral', title: `已分析 ${file.name}` })
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert('解析 JSON 失败:' + err.message)
|
toast.add({ color: 'rose', title: '解析 JSON 失败', description: err?.message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.readAsText(file)
|
reader.readAsText(file)
|
||||||
input.value = ''
|
input.value = ''
|
||||||
}
|
}
|
||||||
function onLoadLocal() {
|
|
||||||
if (!store.loadFromLocal()) alert('本地不存在保存的数据')
|
function onSave() {
|
||||||
}
|
store.downloadCurrent()
|
||||||
function onSaveLocal() { store.saveToLocal() }
|
toast.add({ color: 'primary', title: '已导出文件' })
|
||||||
function onClearLocal() {
|
|
||||||
if (confirm('清空本地保存?')) store.clearLocal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'open-merge', imported: any, diff: any, name: string): void }>()
|
const newOpen = ref(false)
|
||||||
|
|
||||||
|
function createNewBoard() {
|
||||||
|
store.setBoard(
|
||||||
|
{
|
||||||
|
categories: [],
|
||||||
|
stages: [],
|
||||||
|
tasks: [],
|
||||||
|
layout: { columns: [] },
|
||||||
|
meta: {
|
||||||
|
id: 'open-kanban',
|
||||||
|
version: '1.0.0',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
modifiedAt: new Date().toISOString(),
|
||||||
|
actor: store.board.meta?.actor,
|
||||||
|
history: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
''
|
||||||
|
)
|
||||||
|
store.log('new-board', {})
|
||||||
|
toast.add({ color: 'neutral', title: '已创建新的空白看板' })
|
||||||
|
newOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLocal() {
|
||||||
|
if (!store.loadFromLocal()) {
|
||||||
|
toast.add({ color: 'rose', title: '本地没有保存的数据' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.add({ color: 'neutral', title: '已从本地加载看板' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLocal() {
|
||||||
|
store.saveToLocal()
|
||||||
|
toast.add({ color: 'primary', title: '已同步至本地存储' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearLocalOpen = ref(false)
|
||||||
|
|
||||||
|
function clearLocalData() {
|
||||||
|
store.clearLocal()
|
||||||
|
toast.add({ color: 'neutral', title: '本地数据已清空' })
|
||||||
|
clearLocalOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const localMenu = computed(() => [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: '保存至本地',
|
||||||
|
icon: 'i-heroicons-arrow-down-on-square-20-solid',
|
||||||
|
click: () => saveLocal()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '从本地恢复',
|
||||||
|
icon: 'i-heroicons-arrow-up-on-square-20-solid',
|
||||||
|
click: () => loadLocal()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: '清空本地缓存',
|
||||||
|
icon: 'i-heroicons-trash-20-solid',
|
||||||
|
click: () => {
|
||||||
|
clearLocalOpen.value = true
|
||||||
|
},
|
||||||
|
color: 'rose'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
76
components/dialogs/StageModal.vue
Normal file
76
components/dialogs/StageModal.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<UModal v-model="open">
|
||||||
|
<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="handleSubmit">
|
||||||
|
<UFormGroup label="阶段名称" name="title">
|
||||||
|
<UInput v-model="form.title" placeholder="例如:待办、进行中、已完成…" autofocus />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup v-if="columnOptions.length" label="放置到列" name="column">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="form.column"
|
||||||
|
:options="columnOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
<p v-else class="text-xs text-slate-400">
|
||||||
|
尚未创建列,新增阶段时系统会自动建立第一列。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<UButton color="neutral" variant="ghost" @click="open = false">取消</UButton>
|
||||||
|
<UButton type="submit" color="primary">创建阶段</UButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBoardStore } from '~/stores/board'
|
||||||
|
|
||||||
|
const store = useBoardStore()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const props = defineProps<{ defaultColumn?: number }>()
|
||||||
|
const open = defineModel<boolean>('open', { required: true })
|
||||||
|
const emit = defineEmits<{ (e: 'submit', payload: { title: string; column: number }): void }>()
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
title: '',
|
||||||
|
column: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const columnOptions = computed(() =>
|
||||||
|
(store.board.layout?.columns || []).map((_, index) => ({
|
||||||
|
label: `列 ${index + 1}`,
|
||||||
|
value: index
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => open.value,
|
||||||
|
(value) => {
|
||||||
|
if (!value) return
|
||||||
|
form.title = ''
|
||||||
|
const defaultColumn = props.defaultColumn ?? 0
|
||||||
|
form.column = columnOptions.value[defaultColumn] ? defaultColumn : 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (!form.title.trim()) {
|
||||||
|
toast.add({ color: 'rose', title: '请输入阶段名称' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('submit', { title: form.title.trim(), column: form.column })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
73
components/panels/BoardSummaryPanel.vue
Normal file
73
components/panels/BoardSummaryPanel.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-chart-bar-20-solid" class="h-5 w-5 text-emerald-400" />
|
||||||
|
<span class="font-semibold">看板概览</span>
|
||||||
|
</div>
|
||||||
|
<UBadge
|
||||||
|
v-if="lastModified"
|
||||||
|
:label="`更新于:${lastModified}`"
|
||||||
|
color="neutral"
|
||||||
|
variant="soft"
|
||||||
|
class="text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 gap-3 text-sm">
|
||||||
|
<UCard variant="soft" class="border border-emerald-500/10 bg-emerald-500/5 text-emerald-200">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs uppercase tracking-wide opacity-80">任务</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ tasksCount }}</p>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
<UCard variant="soft" class="border border-sky-500/10 bg-sky-500/5 text-sky-200">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs uppercase tracking-wide opacity-80">阶段</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ stagesCount }}</p>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
<UCard variant="soft" class="border border-violet-500/10 bg-violet-500/5 text-violet-200">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs uppercase tracking-wide opacity-80">列</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ columnsCount }}</p>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 space-y-2 text-xs text-slate-400">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-user-circle-20-solid" class="h-4 w-4 text-slate-500" />
|
||||||
|
<span>当前操作人:{{ actor || '未设置' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-document-duplicate-20-solid" class="h-4 w-4 text-slate-500" />
|
||||||
|
<span>文件来源:{{ filename }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBoardStore } from '~/stores/board'
|
||||||
|
|
||||||
|
const store = useBoardStore()
|
||||||
|
|
||||||
|
const columnsCount = computed(() => store.board.layout?.columns?.length || 0)
|
||||||
|
const stagesCount = computed(() => store.board.stages.length)
|
||||||
|
const tasksCount = computed(() => store.board.tasks.length)
|
||||||
|
const actor = computed(() => store.board.meta?.actor || '')
|
||||||
|
const filename = computed(() => store.filename || '未命名')
|
||||||
|
|
||||||
|
const lastModified = computed(() => {
|
||||||
|
const ts = store.board.meta?.modifiedAt
|
||||||
|
if (!ts) return ''
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(ts))
|
||||||
|
} catch {
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
213
components/panels/CategoryPanel.vue
Normal file
213
components/panels/CategoryPanel.vue
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<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>
|
||||||
92
components/panels/HistoryPanel.vue
Normal file
92
components/panels/HistoryPanel.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-clock-20-solid" class="h-5 w-5 text-amber-400" />
|
||||||
|
<span class="font-semibold">历史记录</span>
|
||||||
|
</div>
|
||||||
|
<UButton
|
||||||
|
size="2xs"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-trash-20-solid"
|
||||||
|
@click="clearOpen = true"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-if="!entries.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="max-h-72 space-y-2 overflow-auto pr-1 text-xs">
|
||||||
|
<div
|
||||||
|
v-for="entry in entries"
|
||||||
|
:key="entry.id"
|
||||||
|
class="rounded border border-slate-800 bg-slate-900/70 px-3 py-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<span class="font-medium text-slate-200">{{ entry.type }}</span>
|
||||||
|
<span class="text-[10px] text-slate-500">{{ formatTime(entry.ts) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 flex items-center gap-2 text-[11px] text-slate-400">
|
||||||
|
<UIcon name="i-heroicons-user-20-solid" class="h-3.5 w-3.5" />
|
||||||
|
<span>{{ entry.actor || '系统' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UModal v-model="clearOpen">
|
||||||
|
<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="clearOpen = false">取消</UButton>
|
||||||
|
<UButton color="rose" @click="clearHistory">确认清空</UButton>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBoardStore } from '~/stores/board'
|
||||||
|
|
||||||
|
const store = useBoardStore()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const entries = computed(() => {
|
||||||
|
const list = store.board.meta?.history || []
|
||||||
|
return list.slice(-200).reverse()
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatTime(ts: string) {
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(ts))
|
||||||
|
} catch {
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearOpen = ref(false)
|
||||||
|
|
||||||
|
function clearHistory() {
|
||||||
|
clearOpen.value = false
|
||||||
|
if (!store.board.meta) return
|
||||||
|
store.board.meta.history = []
|
||||||
|
store.board.meta.modifiedAt = new Date().toISOString()
|
||||||
|
store.log('history-clear', {})
|
||||||
|
toast.add({ color: 'neutral', title: '历史记录已清空' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
5
layouts/default.vue
Normal file
5
layouts/default.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-slate-950 text-slate-100">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// Nuxt 4 configuration
|
// Nuxt 4 configuration
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
modules: ['@pinia/nuxt', '@nuxtjs/tailwindcss'],
|
modules: ['@nuxt/ui', '@pinia/nuxt', '@nuxtjs/tailwindcss'],
|
||||||
css: ['~/assets/css/tailwind.css'],
|
css: ['~/assets/css/tailwind.css'],
|
||||||
typescript: {
|
typescript: {
|
||||||
strict: true
|
strict: true
|
||||||
@@ -10,6 +10,11 @@ export default defineNuxtConfig({
|
|||||||
title: 'Kanban'
|
title: 'Kanban'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
ui: {
|
||||||
|
global: true,
|
||||||
|
primary: 'sky',
|
||||||
|
gray: 'slate'
|
||||||
|
},
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
viewer: false
|
viewer: false
|
||||||
},
|
},
|
||||||
@@ -22,4 +27,3 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"@types/node": "^20.11.30"
|
"@types/node": "^20.11.30",
|
||||||
|
"@nuxt/ui": "^2.15.1",
|
||||||
|
"@nuxt/icon": "^1.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
141
pages/index.vue
141
pages/index.vue
@@ -1,106 +1,95 @@
|
|||||||
<template>
|
<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" />
|
<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">
|
<div class="grid gap-6 xl:grid-cols-[320px_1fr]">
|
||||||
<section class="mb-4">
|
<div class="space-y-6">
|
||||||
<h3 class="text-sm font-semibold text-slate-400 mb-2">类别</h3>
|
<CategoryPanel />
|
||||||
<div class="flex flex-col gap-2">
|
<BoardSummaryPanel />
|
||||||
<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">
|
<HistoryPanel />
|
||||||
<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>
|
</div>
|
||||||
<button class="px-3 py-1 rounded bg-slate-800/60 border border-border hover:bg-slate-800" @click="addCat">添加类别</button>
|
<div class="space-y-6">
|
||||||
</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>
|
|
||||||
<Board :query="query" :category="category" />
|
<Board :query="query" :category="category" />
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Merge modal -->
|
<MergeModal
|
||||||
<MergeModal v-model:open="mergeOpen" v-model:imported="imported" v-model:diff="diff" v-model:name="name" />
|
v-model:open="mergeOpen"
|
||||||
</div>
|
v-model:imported="imported"
|
||||||
|
v-model:diff="diff"
|
||||||
|
v-model:name="importName"
|
||||||
|
/>
|
||||||
|
</UContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Toolbar from '~/components/Toolbar.vue'
|
import Toolbar from '~/components/Toolbar.vue'
|
||||||
import Board from '~/components/Board.vue'
|
import Board from '~/components/Board.vue'
|
||||||
import MergeModal from '~/components/MergeModal.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'
|
import { useBoardStore } from '~/stores/board'
|
||||||
|
|
||||||
const store = useBoardStore()
|
const store = useBoardStore()
|
||||||
const config = useRuntimeConfig().public
|
const config = useRuntimeConfig().public
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
// Try load from query or config
|
const query = ref('')
|
||||||
onMounted(async () => {
|
const category = ref('')
|
||||||
const qp = new URL(window.location.href).searchParams.get('file')
|
|
||||||
|
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 || ''
|
const fileToLoad = qp || config.autoLoadFile || ''
|
||||||
if (fileToLoad) {
|
if (fileToLoad) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(fileToLoad, { cache: 'no-store' })
|
const res = await fetch(fileToLoad, { cache: 'no-store' })
|
||||||
if (res.ok) {
|
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' })
|
store.log('load-file', { name: fileToLoad, via: qp ? 'query' : 'config' })
|
||||||
|
toast.add({ color: 'primary', title: `已自动加载 ${fileToLoad}` })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (error: any) {
|
||||||
|
toast.add({ color: 'rose', title: '自动加载失败', description: error?.message })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// else try local storage
|
|
||||||
if (!store.loadFromLocal()) {
|
if (!store.loadFromLocal()) {
|
||||||
// else set empty
|
store.setBoard(
|
||||||
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: [] } }, '')
|
{
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
373
pnpm-lock.yaml
generated
373
pnpm-lock.yaml
generated
@@ -24,6 +24,12 @@ importers:
|
|||||||
specifier: ^3.23.8
|
specifier: ^3.23.8
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@nuxt/icon':
|
||||||
|
specifier: ^1.5.0
|
||||||
|
version: 1.15.0(magicast@0.3.5)(vite@7.1.11(@types/node@20.19.23)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||||
|
'@nuxt/ui':
|
||||||
|
specifier: ^2.15.1
|
||||||
|
version: 2.22.3(magicast@0.3.5)(vite@7.1.11(@types/node@20.19.23)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)(zod@3.25.76)
|
||||||
'@nuxtjs/tailwindcss':
|
'@nuxtjs/tailwindcss':
|
||||||
specifier: ^6.11.4
|
specifier: ^6.11.4
|
||||||
version: 6.14.0(magicast@0.3.5)(yaml@2.8.1)
|
version: 6.14.0(magicast@0.3.5)(yaml@2.8.1)
|
||||||
@@ -49,6 +55,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
'@antfu/install-pkg@1.1.0':
|
||||||
|
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
|
||||||
|
|
||||||
|
'@antfu/utils@8.1.1':
|
||||||
|
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
|
||||||
|
|
||||||
'@babel/code-frame@7.27.1':
|
'@babel/code-frame@7.27.1':
|
||||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -347,6 +359,35 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@headlessui/tailwindcss@0.2.2':
|
||||||
|
resolution: {integrity: sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: ^3.0 || ^4.0
|
||||||
|
|
||||||
|
'@headlessui/vue@1.7.23':
|
||||||
|
resolution: {integrity: sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.2.0
|
||||||
|
|
||||||
|
'@iconify-json/heroicons@1.2.3':
|
||||||
|
resolution: {integrity: sha512-n+vmCEgTesRsOpp5AB5ILB6srsgsYK+bieoQBNlafvoEhjVXLq8nIGN4B0v/s4DUfa0dOrjwE/cKJgIKdJXOEg==}
|
||||||
|
|
||||||
|
'@iconify/collections@1.0.608':
|
||||||
|
resolution: {integrity: sha512-uMbaErE6TzDb04peWVFYjc9cweBD+j1nFBHi5EEcA1u1mXJAyePF01VzH6dimurrhivvU+nRmuYfiC8GPDyG6g==}
|
||||||
|
|
||||||
|
'@iconify/types@2.0.0':
|
||||||
|
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||||
|
|
||||||
|
'@iconify/utils@2.3.0':
|
||||||
|
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
||||||
|
|
||||||
|
'@iconify/vue@5.0.0':
|
||||||
|
resolution: {integrity: sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3'
|
||||||
|
|
||||||
'@ioredis/commands@1.4.0':
|
'@ioredis/commands@1.4.0':
|
||||||
resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==}
|
resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==}
|
||||||
|
|
||||||
@@ -430,6 +471,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: '>=6.0'
|
vite: '>=6.0'
|
||||||
|
|
||||||
|
'@nuxt/icon@1.15.0':
|
||||||
|
resolution: {integrity: sha512-kA0rxqr1B601zNJNcOXera8CyYcxUCEcT7dXEC7rwAz71PRCN5emf7G656eKEQgtqrD4JSj6NQqWDgrmFcf/GQ==}
|
||||||
|
|
||||||
'@nuxt/kit@3.19.3':
|
'@nuxt/kit@3.19.3':
|
||||||
resolution: {integrity: sha512-ze46EW5xW+UxDvinvPkYt2MzR355Az1lA3bpX8KDialgnCwr+IbkBij/udbUEC6ZFbidPkfK1eKl4ESN7gMY+w==}
|
resolution: {integrity: sha512-ze46EW5xW+UxDvinvPkYt2MzR355Az1lA3bpX8KDialgnCwr+IbkBij/udbUEC6ZFbidPkfK1eKl4ESN7gMY+w==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
@@ -447,6 +491,26 @@ packages:
|
|||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@nuxt/ui@2.22.3':
|
||||||
|
resolution: {integrity: sha512-895SAzqCCT5JAc1JQ8nAmmpwdKCJqArY8ifL/PNtD681FKSdXiSPxODGnpqpovM/ws6bvoRwglA7BtwAJ5ySBg==}
|
||||||
|
peerDependencies:
|
||||||
|
joi: ^17.13.0
|
||||||
|
superstruct: ^2.0.0
|
||||||
|
valibot: ^1.0.0
|
||||||
|
yup: ^1.6.0
|
||||||
|
zod: ^3.24.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
joi:
|
||||||
|
optional: true
|
||||||
|
superstruct:
|
||||||
|
optional: true
|
||||||
|
valibot:
|
||||||
|
optional: true
|
||||||
|
yup:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@nuxt/vite-builder@4.1.3':
|
'@nuxt/vite-builder@4.1.3':
|
||||||
resolution: {integrity: sha512-yrblLSpGW6h9k+sDZa+vtevQz/6JLrPAj3n97HrEmVa6qB+4sE4HWtkMNUtWsOPe60sAm9usRsjDUkkiHZ0DpA==}
|
resolution: {integrity: sha512-yrblLSpGW6h9k+sDZa+vtevQz/6JLrPAj3n97HrEmVa6qB+4sE4HWtkMNUtWsOPe60sAm9usRsjDUkkiHZ0DpA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -457,6 +521,9 @@ packages:
|
|||||||
rolldown:
|
rolldown:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nuxtjs/color-mode@3.5.2':
|
||||||
|
resolution: {integrity: sha512-cC6RfgZh3guHBMLLjrBB2Uti5eUoGM9KyauOaYS9ETmxNWBMTvpgjvSiSJp1OFljIXPIqVTJ3xtJpSNZiO3ZaA==}
|
||||||
|
|
||||||
'@nuxtjs/tailwindcss@6.14.0':
|
'@nuxtjs/tailwindcss@6.14.0':
|
||||||
resolution: {integrity: sha512-30RyDK++LrUVRgc2A85MktGWIZoRQgeQKjE4CjjD64OXNozyl+4ScHnnYgqVToMM6Ch2ZG2W4wV2J0EN6F0zkQ==}
|
resolution: {integrity: sha512-30RyDK++LrUVRgc2A85MktGWIZoRQgeQKjE4CjjD64OXNozyl+4ScHnnYgqVToMM6Ch2ZG2W4wV2J0EN6F0zkQ==}
|
||||||
|
|
||||||
@@ -828,6 +895,9 @@ packages:
|
|||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
|
'@popperjs/core@2.11.8':
|
||||||
|
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||||
|
|
||||||
'@poppinss/colors@4.1.5':
|
'@poppinss/colors@4.1.5':
|
||||||
resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==}
|
resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==}
|
||||||
|
|
||||||
@@ -1039,6 +1109,37 @@ packages:
|
|||||||
'@speed-highlight/core@1.2.7':
|
'@speed-highlight/core@1.2.7':
|
||||||
resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==}
|
resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.0.0':
|
||||||
|
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||||
|
|
||||||
|
'@tailwindcss/aspect-ratio@0.4.2':
|
||||||
|
resolution: {integrity: sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
|
||||||
|
|
||||||
|
'@tailwindcss/container-queries@0.1.1':
|
||||||
|
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: '>=3.2.0'
|
||||||
|
|
||||||
|
'@tailwindcss/forms@0.5.10':
|
||||||
|
resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1'
|
||||||
|
|
||||||
|
'@tailwindcss/typography@0.5.19':
|
||||||
|
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.12':
|
||||||
|
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
|
||||||
|
|
||||||
|
'@tanstack/vue-virtual@3.13.12':
|
||||||
|
resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^2.7.0 || ^3.0.0
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||||
|
|
||||||
@@ -1058,6 +1159,9 @@ packages:
|
|||||||
'@types/web-bluetooth@0.0.20':
|
'@types/web-bluetooth@0.0.20':
|
||||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21':
|
||||||
|
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||||
|
|
||||||
'@unhead/vue@2.0.19':
|
'@unhead/vue@2.0.19':
|
||||||
resolution: {integrity: sha512-7BYjHfOaoZ9+ARJkT10Q2TjnTUqDXmMpfakIAsD/hXiuff1oqWg1xeXT5+MomhNcC15HbiABpbbBmITLSHxdKg==}
|
resolution: {integrity: sha512-7BYjHfOaoZ9+ARJkT10Q2TjnTUqDXmMpfakIAsD/hXiuff1oqWg1xeXT5+MomhNcC15HbiABpbbBmITLSHxdKg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1167,12 +1271,72 @@ packages:
|
|||||||
'@vueuse/core@11.3.0':
|
'@vueuse/core@11.3.0':
|
||||||
resolution: {integrity: sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==}
|
resolution: {integrity: sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==}
|
||||||
|
|
||||||
|
'@vueuse/core@13.9.0':
|
||||||
|
resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
|
'@vueuse/integrations@13.9.0':
|
||||||
|
resolution: {integrity: sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==}
|
||||||
|
peerDependencies:
|
||||||
|
async-validator: ^4
|
||||||
|
axios: ^1
|
||||||
|
change-case: ^5
|
||||||
|
drauu: ^0.4
|
||||||
|
focus-trap: ^7
|
||||||
|
fuse.js: ^7
|
||||||
|
idb-keyval: ^6
|
||||||
|
jwt-decode: ^4
|
||||||
|
nprogress: ^0.2
|
||||||
|
qrcode: ^1.5
|
||||||
|
sortablejs: ^1
|
||||||
|
universal-cookie: ^7 || ^8
|
||||||
|
vue: ^3.5.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
async-validator:
|
||||||
|
optional: true
|
||||||
|
axios:
|
||||||
|
optional: true
|
||||||
|
change-case:
|
||||||
|
optional: true
|
||||||
|
drauu:
|
||||||
|
optional: true
|
||||||
|
focus-trap:
|
||||||
|
optional: true
|
||||||
|
fuse.js:
|
||||||
|
optional: true
|
||||||
|
idb-keyval:
|
||||||
|
optional: true
|
||||||
|
jwt-decode:
|
||||||
|
optional: true
|
||||||
|
nprogress:
|
||||||
|
optional: true
|
||||||
|
qrcode:
|
||||||
|
optional: true
|
||||||
|
sortablejs:
|
||||||
|
optional: true
|
||||||
|
universal-cookie:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vueuse/math@13.9.0':
|
||||||
|
resolution: {integrity: sha512-Qk2jqlaEGKwwe2/MBGtUd8nPpzoQPSQTfm2d30NPywjpYdpbI+WqOAE99MuSq9kIRoU7Xq3IYBtxMaLTy6lpsA==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
'@vueuse/metadata@11.3.0':
|
'@vueuse/metadata@11.3.0':
|
||||||
resolution: {integrity: sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==}
|
resolution: {integrity: sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==}
|
||||||
|
|
||||||
|
'@vueuse/metadata@13.9.0':
|
||||||
|
resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
|
||||||
|
|
||||||
'@vueuse/shared@11.3.0':
|
'@vueuse/shared@11.3.0':
|
||||||
resolution: {integrity: sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==}
|
resolution: {integrity: sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==}
|
||||||
|
|
||||||
|
'@vueuse/shared@13.9.0':
|
||||||
|
resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
abbrev@3.0.1:
|
abbrev@3.0.1:
|
||||||
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
@@ -1923,6 +2087,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
|
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
globals@15.15.0:
|
||||||
|
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
globby@15.0.0:
|
globby@15.0.0:
|
||||||
resolution: {integrity: sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==}
|
resolution: {integrity: sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -2211,6 +2379,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==}
|
resolution: {integrity: sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==}
|
||||||
engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4}
|
engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4}
|
||||||
|
|
||||||
|
kolorist@1.8.0:
|
||||||
|
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
||||||
|
|
||||||
launch-editor@2.11.1:
|
launch-editor@2.11.1:
|
||||||
resolution: {integrity: sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==}
|
resolution: {integrity: sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==}
|
||||||
|
|
||||||
@@ -2326,6 +2497,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
mini-svg-data-uri@1.4.4:
|
||||||
|
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
|
|
||||||
@@ -2818,6 +2993,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
postcss: ^8.4.32
|
postcss: ^8.4.32
|
||||||
|
|
||||||
|
postcss-selector-parser@6.0.10:
|
||||||
|
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
postcss-selector-parser@6.1.2:
|
postcss-selector-parser@6.1.2:
|
||||||
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3179,6 +3358,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: 1 || 2 || 2.0.1-compat || 3
|
tailwindcss: 1 || 2 || 2.0.1-compat || 3
|
||||||
|
|
||||||
|
tailwind-merge@2.6.0:
|
||||||
|
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
|
||||||
|
|
||||||
tailwindcss@3.4.18:
|
tailwindcss@3.4.18:
|
||||||
resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==}
|
resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -3640,6 +3822,13 @@ snapshots:
|
|||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
|
'@antfu/install-pkg@1.1.0':
|
||||||
|
dependencies:
|
||||||
|
package-manager-detector: 1.5.0
|
||||||
|
tinyexec: 1.0.1
|
||||||
|
|
||||||
|
'@antfu/utils@8.1.1': {}
|
||||||
|
|
||||||
'@babel/code-frame@7.27.1':
|
'@babel/code-frame@7.27.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.27.1
|
'@babel/helper-validator-identifier': 7.27.1
|
||||||
@@ -3913,6 +4102,43 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.25.11':
|
'@esbuild/win32-x64@0.25.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@headlessui/tailwindcss@0.2.2(tailwindcss@3.4.18(yaml@2.8.1))':
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 3.4.18(yaml@2.8.1)
|
||||||
|
|
||||||
|
'@headlessui/vue@1.7.23(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/vue-virtual': 3.13.12(vue@3.5.22(typescript@5.9.3))
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@iconify-json/heroicons@1.2.3':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
|
'@iconify/collections@1.0.608':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
|
'@iconify/types@2.0.0': {}
|
||||||
|
|
||||||
|
'@iconify/utils@2.3.0':
|
||||||
|
dependencies:
|
||||||
|
'@antfu/install-pkg': 1.1.0
|
||||||
|
'@antfu/utils': 8.1.1
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
debug: 4.4.3
|
||||||
|
globals: 15.15.0
|
||||||
|
kolorist: 1.8.0
|
||||||
|
local-pkg: 1.1.2
|
||||||
|
mlly: 1.8.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@iconify/vue@5.0.0(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
'@ioredis/commands@1.4.0': {}
|
'@ioredis/commands@1.4.0': {}
|
||||||
|
|
||||||
'@isaacs/cliui@8.0.2':
|
'@isaacs/cliui@8.0.2':
|
||||||
@@ -4096,6 +4322,28 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@nuxt/icon@1.15.0(magicast@0.3.5)(vite@7.1.11(@types/node@20.19.23)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/collections': 1.0.608
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
'@iconify/utils': 2.3.0
|
||||||
|
'@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
'@nuxt/devtools-kit': 2.6.5(magicast@0.3.5)(vite@7.1.11(@types/node@20.19.23)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
|
||||||
|
'@nuxt/kit': 3.19.3(magicast@0.3.5)
|
||||||
|
consola: 3.4.2
|
||||||
|
local-pkg: 1.1.2
|
||||||
|
mlly: 1.8.0
|
||||||
|
ohash: 2.0.11
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.3
|
||||||
|
std-env: 3.10.0
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- magicast
|
||||||
|
- supports-color
|
||||||
|
- vite
|
||||||
|
- vue
|
||||||
|
|
||||||
'@nuxt/kit@3.19.3(magicast@0.3.5)':
|
'@nuxt/kit@3.19.3(magicast@0.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
c12: 3.3.1(magicast@0.3.5)
|
c12: 3.3.1(magicast@0.3.5)
|
||||||
@@ -4178,6 +4426,52 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
|
'@nuxt/ui@2.22.3(magicast@0.3.5)(vite@7.1.11(@types/node@20.19.23)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)(zod@3.25.76)':
|
||||||
|
dependencies:
|
||||||
|
'@headlessui/tailwindcss': 0.2.2(tailwindcss@3.4.18(yaml@2.8.1))
|
||||||
|
'@headlessui/vue': 1.7.23(vue@3.5.22(typescript@5.9.3))
|
||||||
|
'@iconify-json/heroicons': 1.2.3
|
||||||
|
'@nuxt/icon': 1.15.0(magicast@0.3.5)(vite@7.1.11(@types/node@20.19.23)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||||
|
'@nuxt/kit': 4.1.3(magicast@0.3.5)
|
||||||
|
'@nuxtjs/color-mode': 3.5.2(magicast@0.3.5)
|
||||||
|
'@nuxtjs/tailwindcss': 6.14.0(magicast@0.3.5)(yaml@2.8.1)
|
||||||
|
'@popperjs/core': 2.11.8
|
||||||
|
'@standard-schema/spec': 1.0.0
|
||||||
|
'@tailwindcss/aspect-ratio': 0.4.2(tailwindcss@3.4.18(yaml@2.8.1))
|
||||||
|
'@tailwindcss/container-queries': 0.1.1(tailwindcss@3.4.18(yaml@2.8.1))
|
||||||
|
'@tailwindcss/forms': 0.5.10(tailwindcss@3.4.18(yaml@2.8.1))
|
||||||
|
'@tailwindcss/typography': 0.5.19(tailwindcss@3.4.18(yaml@2.8.1))
|
||||||
|
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
'@vueuse/integrations': 13.9.0(fuse.js@7.1.0)(vue@3.5.22(typescript@5.9.3))
|
||||||
|
'@vueuse/math': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
defu: 6.1.4
|
||||||
|
fuse.js: 7.1.0
|
||||||
|
ohash: 2.0.11
|
||||||
|
pathe: 2.0.3
|
||||||
|
scule: 1.3.0
|
||||||
|
tailwind-merge: 2.6.0
|
||||||
|
tailwindcss: 3.4.18(yaml@2.8.1)
|
||||||
|
optionalDependencies:
|
||||||
|
zod: 3.25.76
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- async-validator
|
||||||
|
- axios
|
||||||
|
- change-case
|
||||||
|
- drauu
|
||||||
|
- focus-trap
|
||||||
|
- idb-keyval
|
||||||
|
- jwt-decode
|
||||||
|
- magicast
|
||||||
|
- nprogress
|
||||||
|
- qrcode
|
||||||
|
- sortablejs
|
||||||
|
- supports-color
|
||||||
|
- tsx
|
||||||
|
- universal-cookie
|
||||||
|
- vite
|
||||||
|
- vue
|
||||||
|
- yaml
|
||||||
|
|
||||||
'@nuxt/vite-builder@4.1.3(@types/node@20.19.23)(magicast@0.3.5)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)':
|
'@nuxt/vite-builder@4.1.3(@types/node@20.19.23)(magicast@0.3.5)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 4.1.3(magicast@0.3.5)
|
'@nuxt/kit': 4.1.3(magicast@0.3.5)
|
||||||
@@ -4235,6 +4529,15 @@ snapshots:
|
|||||||
- vue-tsc
|
- vue-tsc
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
'@nuxtjs/color-mode@3.5.2(magicast@0.3.5)':
|
||||||
|
dependencies:
|
||||||
|
'@nuxt/kit': 3.19.3(magicast@0.3.5)
|
||||||
|
pathe: 1.1.2
|
||||||
|
pkg-types: 1.3.1
|
||||||
|
semver: 7.7.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- magicast
|
||||||
|
|
||||||
'@nuxtjs/tailwindcss@6.14.0(magicast@0.3.5)(yaml@2.8.1)':
|
'@nuxtjs/tailwindcss@6.14.0(magicast@0.3.5)(yaml@2.8.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.19.3(magicast@0.3.5)
|
'@nuxt/kit': 3.19.3(magicast@0.3.5)
|
||||||
@@ -4482,6 +4785,8 @@ snapshots:
|
|||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
|
'@popperjs/core@2.11.8': {}
|
||||||
|
|
||||||
'@poppinss/colors@4.1.5':
|
'@poppinss/colors@4.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
kleur: 4.1.5
|
kleur: 4.1.5
|
||||||
@@ -4635,6 +4940,33 @@ snapshots:
|
|||||||
|
|
||||||
'@speed-highlight/core@1.2.7': {}
|
'@speed-highlight/core@1.2.7': {}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.0.0': {}
|
||||||
|
|
||||||
|
'@tailwindcss/aspect-ratio@0.4.2(tailwindcss@3.4.18(yaml@2.8.1))':
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 3.4.18(yaml@2.8.1)
|
||||||
|
|
||||||
|
'@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.18(yaml@2.8.1))':
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 3.4.18(yaml@2.8.1)
|
||||||
|
|
||||||
|
'@tailwindcss/forms@0.5.10(tailwindcss@3.4.18(yaml@2.8.1))':
|
||||||
|
dependencies:
|
||||||
|
mini-svg-data-uri: 1.4.4
|
||||||
|
tailwindcss: 3.4.18(yaml@2.8.1)
|
||||||
|
|
||||||
|
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(yaml@2.8.1))':
|
||||||
|
dependencies:
|
||||||
|
postcss-selector-parser: 6.0.10
|
||||||
|
tailwindcss: 3.4.18(yaml@2.8.1)
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.12': {}
|
||||||
|
|
||||||
|
'@tanstack/vue-virtual@3.13.12(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/virtual-core': 3.13.12
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -4654,6 +4986,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/web-bluetooth@0.0.20': {}
|
'@types/web-bluetooth@0.0.20': {}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21': {}
|
||||||
|
|
||||||
'@unhead/vue@2.0.19(vue@3.5.22(typescript@5.9.3))':
|
'@unhead/vue@2.0.19(vue@3.5.22(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
hookable: 5.5.3
|
hookable: 5.5.3
|
||||||
@@ -4846,8 +5180,30 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@types/web-bluetooth': 0.0.21
|
||||||
|
'@vueuse/metadata': 13.9.0
|
||||||
|
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@vueuse/integrations@13.9.0(fuse.js@7.1.0)(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
optionalDependencies:
|
||||||
|
fuse.js: 7.1.0
|
||||||
|
|
||||||
|
'@vueuse/math@13.9.0(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
'@vueuse/metadata@11.3.0': {}
|
'@vueuse/metadata@11.3.0': {}
|
||||||
|
|
||||||
|
'@vueuse/metadata@13.9.0': {}
|
||||||
|
|
||||||
'@vueuse/shared@11.3.0(vue@3.5.22(typescript@5.9.3))':
|
'@vueuse/shared@11.3.0(vue@3.5.22(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vue-demi: 0.14.10(vue@3.5.22(typescript@5.9.3))
|
vue-demi: 0.14.10(vue@3.5.22(typescript@5.9.3))
|
||||||
@@ -4855,6 +5211,10 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/shared@13.9.0(vue@3.5.22(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
abbrev@3.0.1: {}
|
abbrev@3.0.1: {}
|
||||||
|
|
||||||
abort-controller@3.0.0:
|
abort-controller@3.0.0:
|
||||||
@@ -5586,6 +5946,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ini: 4.1.1
|
ini: 4.1.1
|
||||||
|
|
||||||
|
globals@15.15.0: {}
|
||||||
|
|
||||||
globby@15.0.0:
|
globby@15.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sindresorhus/merge-streams': 4.0.0
|
'@sindresorhus/merge-streams': 4.0.0
|
||||||
@@ -5889,6 +6251,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
kolorist@1.8.0: {}
|
||||||
|
|
||||||
launch-editor@2.11.1:
|
launch-editor@2.11.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
@@ -6006,6 +6370,8 @@ snapshots:
|
|||||||
|
|
||||||
mimic-fn@4.0.0: {}
|
mimic-fn@4.0.0: {}
|
||||||
|
|
||||||
|
mini-svg-data-uri@1.4.4: {}
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
@@ -6688,6 +7054,11 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-value-parser: 4.2.0
|
postcss-value-parser: 4.2.0
|
||||||
|
|
||||||
|
postcss-selector-parser@6.0.10:
|
||||||
|
dependencies:
|
||||||
|
cssesc: 3.0.0
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
postcss-selector-parser@6.1.2:
|
postcss-selector-parser@6.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cssesc: 3.0.0
|
cssesc: 3.0.0
|
||||||
@@ -7072,6 +7443,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
tailwind-merge@2.6.0: {}
|
||||||
|
|
||||||
tailwindcss@3.4.18(yaml@2.8.1):
|
tailwindcss@3.4.18(yaml@2.8.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
|
|||||||
@@ -113,6 +113,28 @@ export const useBoardStore = defineStore('board', () => {
|
|||||||
const stageById = (id: string) => board.value.stages.find(s => s.uuid === id)
|
const stageById = (id: string) => board.value.stages.find(s => s.uuid === id)
|
||||||
const categoryById = (id: string) => board.value.categories.find(c => c.uuid === id)
|
const categoryById = (id: string) => board.value.categories.find(c => c.uuid === id)
|
||||||
|
|
||||||
|
function addCategory(title: string, color: string) {
|
||||||
|
const c: Category = { uuid: uuid(), title, color }
|
||||||
|
board.value.categories.push(c)
|
||||||
|
log('category-add', { id: c.uuid, title, color })
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
function updateCategory(id: string, patch: Partial<Category>) {
|
||||||
|
const c = categoryById(id)
|
||||||
|
if (!c) return false
|
||||||
|
const before = JSON.parse(JSON.stringify(c))
|
||||||
|
Object.assign(c, patch)
|
||||||
|
log('category-update', { id, before, after: c })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
function removeCategory(id: string) {
|
||||||
|
const used = board.value.tasks.some(t => t.category === id)
|
||||||
|
if (used) return false
|
||||||
|
board.value.categories = board.value.categories.filter(c => c.uuid !== id)
|
||||||
|
log('category-delete', { id })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function addStage(title: string, colIndex = 0) {
|
function addStage(title: string, colIndex = 0) {
|
||||||
const s: Stage = { uuid: uuid(), title, tasks: [] }
|
const s: Stage = { uuid: uuid(), title, tasks: [] }
|
||||||
board.value.stages.push(s)
|
board.value.stages.push(s)
|
||||||
@@ -188,8 +210,8 @@ export const useBoardStore = defineStore('board', () => {
|
|||||||
setBoard, setActor, log, saveToLocal, loadFromLocal, clearLocal,
|
setBoard, setActor, log, saveToLocal, loadFromLocal, clearLocal,
|
||||||
toJSON, downloadCurrent, applyMerge,
|
toJSON, downloadCurrent, applyMerge,
|
||||||
addStage, renameStage, deleteStage, addTask, removeTask, editTask, moveTask,
|
addStage, renameStage, deleteStage, addTask, removeTask, editTask, moveTask,
|
||||||
|
addCategory, updateCategory, removeCategory,
|
||||||
taskById, stageById, categoryById,
|
taskById, stageById, categoryById,
|
||||||
diffBoards
|
diffBoards
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,5 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: []
|
plugins: [],
|
||||||
} satisfies Config
|
} satisfies Config
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user