Files
JARVIS/frontend/src/pages/kanban/index.vue
DESKTOP-72TV0V4\caoxiaozhu b024a2bcb5 refactor(frontend): move views into app and pages structure
Reorganize the frontend around app-level routing and page modules so the runtime and feature screens share a clearer navigation and composition layout for future work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 22:13:12 +08:00

479 lines
13 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { taskApi, type Task, type TaskStatus, type TaskPriority } from '@/api/task'
import { Plus, CheckCircle, Circle, Clock, Trash2, Zap } from 'lucide-vue-next'
const tasks = ref<Task[]>([])
const showCreateForm = ref(false)
const newTaskTitle = ref('')
const newTaskPriority = ref<TaskPriority>('medium')
const todoTasks = computed(() => tasks.value.filter((t) => t.status === 'todo'))
const inProgressTasks = computed(() => tasks.value.filter((t) => t.status === 'in_progress'))
const doneTasks = computed(() => tasks.value.filter((t) => t.status === 'done'))
const priorityConfig: Record<TaskPriority, { color: string; label: string; glow: string }> = {
low: { color: '#4b5563', label: 'LOW', glow: 'rgba(75,85,99,0.3)' },
medium: { color: '#60a5fa', label: 'MED', glow: 'rgba(96,165,250,0.3)' },
high: { color: '#fbbf24', label: 'HIGH', glow: 'rgba(251,191,36,0.3)' },
urgent: { color: '#f87171', label: 'CRIT', glow: 'rgba(248,113,113,0.3)' },
}
async function loadTasks() {
try {
const response = await taskApi.list()
tasks.value = response.data
} catch (e) { console.error('加载任务失败:', e) }
}
async function createTask() {
if (!newTaskTitle.value.trim()) return
try {
const response = await taskApi.create({ title: newTaskTitle.value.trim(), priority: newTaskPriority.value })
tasks.value.unshift(response.data)
newTaskTitle.value = ''
showCreateForm.value = false
} catch (e) { console.error('创建任务失败:', e) }
}
async function updateStatus(task: Task, status: TaskStatus) {
try {
const response = await taskApi.update(task.id, { status })
const index = tasks.value.findIndex((t) => t.id === task.id)
if (index !== -1) tasks.value[index] = response.data
} catch (e) { console.error('更新状态失败:', e) }
}
async function deleteTask(id: string) {
try {
await taskApi.delete(id)
tasks.value = tasks.value.filter((t) => t.id !== id)
} catch (e) { console.error('删除失败:', e) }
}
onMounted(() => { loadTasks() })
</script>
<template>
<div class="kanban-view">
<!-- Header -->
<div class="page-header">
<div class="header-left">
<div class="header-icon"><Zap :size="20" /></div>
<div class="header-text">
<h1>TASK BOARD</h1>
<span class="header-sub">{{ tasks.length }} tasks · {{ doneTasks.length }} completed</span>
</div>
</div>
<button class="add-btn" @click="showCreateForm = true">
<Plus :size="14" />
NEW TASK
</button>
</div>
<!-- Create form -->
<div v-if="showCreateForm" class="create-panel">
<div class="create-inner">
<input
v-model="newTaskTitle"
placeholder="Describe the task..."
@keyup.enter="createTask"
autofocus
/>
<select v-model="newTaskPriority" class="priority-select">
<option value="low">LOW</option>
<option value="medium">MEDIUM</option>
<option value="high">HIGH</option>
<option value="urgent">CRITICAL</option>
</select>
<button class="confirm-btn" @click="createTask">CREATE</button>
<button class="cancel-btn" @click="showCreateForm = false">CANCEL</button>
</div>
</div>
<!-- Board -->
<div class="kanban-board">
<!-- TODO -->
<div class="kanban-col">
<div class="col-header">
<div class="col-title">
<Circle :size="14" />
<span>PENDING</span>
<div class="col-count">{{ todoTasks.length }}</div>
</div>
</div>
<div class="col-line" style="--col-color: #60a5fa"></div>
<div class="col-cards">
<div
v-for="task in todoTasks"
:key="task.id"
class="task-card"
@click="updateStatus(task, 'in_progress')"
>
<div class="task-priority-bar" :style="{ background: priorityConfig[task.priority].color, boxShadow: '0 0 6px ' + priorityConfig[task.priority].glow }"></div>
<div class="task-body">
<div class="task-meta">
<span class="task-priority-tag" :style="{ color: priorityConfig[task.priority].color }">
{{ priorityConfig[task.priority].label }}
</span>
</div>
<div class="task-title">{{ task.title }}</div>
</div>
<button class="task-delete" @click.stop="deleteTask(task.id)">
<Trash2 :size="12" />
</button>
</div>
<div v-if="todoTasks.length === 0" class="col-empty">No pending tasks</div>
</div>
</div>
<!-- IN PROGRESS -->
<div class="kanban-col active-col">
<div class="col-header">
<div class="col-title">
<Clock :size="14" />
<span>IN PROGRESS</span>
<div class="col-count active">{{ inProgressTasks.length }}</div>
</div>
</div>
<div class="col-line" style="--col-color: #fbbf24"></div>
<div class="col-cards">
<div
v-for="task in inProgressTasks"
:key="task.id"
class="task-card"
@click="updateStatus(task, 'done')"
>
<div class="task-priority-bar" :style="{ background: priorityConfig[task.priority].color, boxShadow: '0 0 6px ' + priorityConfig[task.priority].glow }"></div>
<div class="task-body">
<div class="task-meta">
<span class="task-priority-tag" :style="{ color: priorityConfig[task.priority].color }">
{{ priorityConfig[task.priority].label }}
</span>
<span class="active-dot"></span>
</div>
<div class="task-title">{{ task.title }}</div>
</div>
<button class="task-delete" @click.stop="deleteTask(task.id)">
<Trash2 :size="12" />
</button>
</div>
<div v-if="inProgressTasks.length === 0" class="col-empty">No active tasks</div>
</div>
</div>
<!-- DONE -->
<div class="kanban-col">
<div class="col-header">
<div class="col-title">
<CheckCircle :size="14" />
<span>COMPLETED</span>
<div class="col-count">{{ doneTasks.length }}</div>
</div>
</div>
<div class="col-line" style="--col-color: #34d399"></div>
<div class="col-cards">
<div
v-for="task in doneTasks"
:key="task.id"
class="task-card done"
@click="updateStatus(task, 'todo')"
>
<div class="task-priority-bar" :style="{ background: priorityConfig[task.priority].color, boxShadow: '0 0 6px ' + priorityConfig[task.priority].glow, opacity: 0.4 }"></div>
<div class="task-body">
<div class="task-meta">
<span class="task-priority-tag" style="color: var(--text-dim)">
{{ priorityConfig[task.priority].label }}
</span>
</div>
<div class="task-title done-title">{{ task.title }}</div>
</div>
<button class="task-delete" @click.stop="deleteTask(task.id)">
<Trash2 :size="12" />
</button>
</div>
<div v-if="doneTasks.length === 0" class="col-empty">No completed tasks</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.kanban-view {
height: 100%;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left { display: flex; align-items: center; gap: 14px; }
.header-icon { color: var(--accent-amber); filter: drop-shadow(0 0 8px var(--accent-amber)); }
h1 {
font-family: var(--font-display);
font-size: 20px;
font-weight: 700;
letter-spacing: 0.15em;
color: var(--text-primary);
margin: 0;
}
.header-sub { font-family: var(--font-mono); font-size: 10px; color: var(--text-dim); letter-spacing: 0.1em; }
.add-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: var(--accent-amber-dim);
border: 1px solid rgba(249, 168, 37, 0.25);
border-radius: var(--radius-md);
color: var(--accent-amber);
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.12em;
transition: all var(--transition-fast);
}
.add-btn:hover {
background: rgba(249, 168, 37, 0.2);
box-shadow: var(--glow-amber);
}
/* Create panel */
.create-panel {
background: var(--bg-card);
border: 1px solid var(--border-mid);
border-radius: var(--radius-lg);
padding: 16px;
animation: fade-in-up 0.2s ease;
}
.create-inner {
display: flex;
align-items: center;
gap: 10px;
}
.create-inner input {
flex: 1;
padding: 10px 14px;
background: var(--bg-panel);
border: 1px solid var(--border-mid);
border-radius: var(--radius-md);
font-size: 13px;
}
.create-inner input:focus {
border-color: var(--accent-amber);
box-shadow: var(--glow-amber);
}
.priority-select {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.1em;
padding: 8px 12px;
background: var(--bg-panel);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
color: var(--text-secondary);
cursor: pointer;
}
.confirm-btn {
padding: 10px 20px;
background: var(--accent-amber-dim);
border: 1px solid rgba(249, 168, 37, 0.3);
border-radius: var(--radius-md);
color: var(--accent-amber);
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.12em;
transition: all var(--transition-fast);
}
.confirm-btn:hover { background: rgba(249, 168, 37, 0.2); box-shadow: var(--glow-amber); }
.cancel-btn {
padding: 10px 16px;
background: transparent;
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
color: var(--text-dim);
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.12em;
transition: all var(--transition-fast);
}
.cancel-btn:hover { border-color: var(--accent-red); color: var(--accent-red); }
/* Board */
.kanban-board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
flex: 1;
min-height: 0;
}
.kanban-col {
background: var(--bg-card);
border: 1px solid var(--border-dim);
border-radius: var(--radius-lg);
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
min-height: 400px;
}
.kanban-col.active-col {
border-color: rgba(251, 191, 36, 0.2);
background: rgba(251, 191, 36, 0.02);
}
.col-header { }
.col-title {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.15em;
color: var(--text-dim);
margin-bottom: 8px;
}
.col-count {
margin-left: auto;
background: var(--bg-panel);
border: 1px solid var(--border-dim);
border-radius: 10px;
padding: 1px 8px;
font-size: 10px;
color: var(--text-secondary);
}
.col-count.active {
background: var(--accent-amber-dim);
border-color: rgba(249, 168, 37, 0.3);
color: var(--accent-amber);
}
.col-line {
height: 1px;
background: linear-gradient(90deg, var(--col-color, var(--accent-cyan)), transparent);
margin-bottom: 4px;
opacity: 0.5;
}
.col-cards {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
overflow-y: auto;
}
.col-empty {
text-align: center;
padding: 32px 16px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.08em;
}
.task-card {
background: var(--bg-panel);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
padding: 10px 12px;
cursor: pointer;
display: flex;
align-items: stretch;
gap: 10px;
transition: all var(--transition-fast);
position: relative;
overflow: hidden;
}
.task-card:hover {
border-color: var(--border-mid);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.task-card.done { opacity: 0.55; }
.task-priority-bar {
width: 3px;
border-radius: 2px;
flex-shrink: 0;
}
.task-body { flex: 1; min-width: 0; }
.task-meta {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.task-priority-tag {
font-family: var(--font-display);
font-size: 8px;
letter-spacing: 0.1em;
}
.active-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--accent-amber);
box-shadow: 0 0 6px var(--accent-amber);
animation: pulse-glow 1.5s ease-in-out infinite;
}
.task-title {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
.done-title {
text-decoration: line-through;
color: var(--text-dim);
}
.task-delete {
background: none;
border: none;
color: var(--text-dim);
cursor: pointer;
padding: 2px;
border-radius: 3px;
opacity: 0;
transition: all var(--transition-fast);
flex-shrink: 0;
align-self: flex-start;
}
.task-card:hover .task-delete { opacity: 1; }
.task-delete:hover { color: var(--accent-red); background: rgba(255,71,87,0.1); }
</style>