feat(frontend): update API clients and Kanban components with enhanced UI

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-11 08:48:22 +08:00
parent 39a9058de1
commit c70e7e7253
6 changed files with 2208 additions and 710 deletions

View File

@@ -25,6 +25,8 @@ export interface ChatStreamHandlers {
onError?: (message: string) => void
}
export type ChatRuntime = 'jarvis' | 'hermes'
export interface MessageAttachment {
id: string
name: string
@@ -84,12 +86,13 @@ export const conversationApi = {
return api.delete(`/api/conversations/${conversationId}`)
},
chat(message: string, conversationId?: string, fileIds: string[] = [], modelName?: string) {
chat(message: string, conversationId?: string, fileIds: string[] = [], modelName?: string, runtime?: ChatRuntime) {
return api.post('/api/conversations/chat', {
message,
conversation_id: conversationId,
file_ids: fileIds,
model_name: modelName,
runtime,
})
},
@@ -98,6 +101,7 @@ export const conversationApi = {
conversationId?: string,
fileIds: string[] = [],
modelName?: string,
runtime: ChatRuntime = 'jarvis',
handlers: ChatStreamHandlers = {},
) {
const token = localStorage.getItem('access_token')
@@ -113,6 +117,7 @@ export const conversationApi = {
conversation_id: conversationId,
file_ids: fileIds,
model_name: modelName,
runtime,
}),
})

View File

@@ -50,6 +50,25 @@ export interface UploadResponse {
indexed_at?: string | null
}
export interface RAGSource {
id: string
title: string
file_type: string
similarity?: number
chunk_content?: string
}
export interface RAGChatRequest {
query: string
top_k?: number
mode?: 'hybrid' | 'semantic' | 'keyword'
}
export interface RAGChatResponse {
answer: string
sources: RAGSource[]
}
export const documentApi = {
list(folderId?: string | null) {
return api.get<Document[]>('/api/documents', {
@@ -93,4 +112,8 @@ export const documentApi = {
getContent(id: string) {
return api.get<string>(`/api/documents/${id}/content`)
},
ragChat(payload: RAGChatRequest) {
return api.post<RAGChatResponse>('/api/documents/rag', payload)
},
}

View File

@@ -1,7 +1,7 @@
import api from './index'
import type { Goal } from './goal'
import type { Reminder } from './reminder'
import type { Task } from './task'
import type { Task, TaskAssigneeType, TaskDispatchStatus, TaskPriority, TaskQuadrant, TaskStatus } from './task'
import type { Todo } from './todo'
export interface ScheduleCenterDaySummary {
@@ -14,6 +14,47 @@ export interface ScheduleCenterDaySummary {
goal_total: number
}
export interface ScheduleCenterFocusTask {
id: string
title: string
status: TaskStatus
priority: TaskPriority
quadrant?: TaskQuadrant | null
assignee_type?: TaskAssigneeType | null
assignee_id?: string | null
dispatch_status: TaskDispatchStatus
due_date?: string | null
}
export interface ScheduleCenterQuadrantTask {
id: string
title: string
status: TaskStatus
priority: TaskPriority
dispatch_status: TaskDispatchStatus
assignee_type?: TaskAssigneeType | null
assignee_id?: string | null
}
export interface ScheduleCenterQuadrant {
id: TaskQuadrant
title: string
subtitle: string
color: string
glow_color: string
icon: string
tasks: ScheduleCenterQuadrantTask[]
}
export interface ScheduleCenterCommanderSummary {
total: number
queued: number
running: number
completed: number
failed: number
overall_status: string
}
export interface ScheduleCenterMonthResponse {
month: string
days: ScheduleCenterDaySummary[]
@@ -26,6 +67,9 @@ export interface ScheduleCenterDateResponse {
reminders: Reminder[]
goals: Goal[]
summary: ScheduleCenterDaySummary
focus_tasks: ScheduleCenterFocusTask[]
quadrants: ScheduleCenterQuadrant[]
commander_summary: ScheduleCenterCommanderSummary
generated_at: string
}

View File

@@ -2,34 +2,225 @@ import api from './index'
export type TaskStatus = 'todo' | 'in_progress' | 'done' | 'cancelled'
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent'
export type TaskSource = 'manual' | 'chat' | 'schedule_center' | 'today_status' | 'commander'
export type TaskQuadrant =
| 'urgent-important'
| 'not-urgent-important'
| 'urgent-not-important'
| 'not-urgent-not-important'
export type TaskDispatchStatus = 'idle' | 'queued' | 'running' | 'completed' | 'failed'
export type TaskAssigneeType =
| 'user'
| 'commander'
| 'agent'
| 'planner'
| 'executor'
| 'knowledge'
| 'analyst'
| 'coder'
| 'researcher'
export interface Task {
export interface TaskSubTask {
id: string
task_id: string
title: string
description?: string
description?: string | null
status: TaskStatus
priority: TaskPriority
due_date?: string
completed_at?: string
tags?: string
order_index: number
assignee_type?: TaskAssigneeType | null
assignee_id?: string | null
dispatch_status: TaskDispatchStatus
dispatch_run_id?: string | null
result_summary?: string | null
completed_at?: string | null
created_at: string
updated_at: string
}
export interface TaskHistoryEntry {
id: string
task_id: string
action: string
old_value?: string | null
new_value?: string | null
created_at: string
updated_at: string
}
export interface TaskDispatchSummary {
status: TaskDispatchStatus
run_id?: string | null
result_summary?: string | null
started_at?: string | null
last_synced_at?: string | null
total_subtasks?: number
dispatched_subtasks?: number
subtask_dispatch_statuses?: Record<string, number>
}
export interface Task {
id: string
title: string
description?: string | null
status: TaskStatus
priority: TaskPriority
due_date?: string | null
completed_at?: string | null
tags: string[]
source: TaskSource
conversation_id?: string | null
quadrant?: TaskQuadrant | null
assignee_type?: TaskAssigneeType | null
assignee_id?: string | null
dispatch_status: TaskDispatchStatus
dispatch_run_id?: string | null
result_summary?: string | null
started_at?: string | null
last_synced_at?: string | null
subtask_count?: number
created_at: string
updated_at: string
}
export interface TaskDetail extends Task {
subtasks: TaskSubTask[]
history: TaskHistoryEntry[]
dispatch: TaskDispatchSummary
dispatch_summary: TaskDispatchSummary
}
export interface TaskDispatchResponse {
status: TaskDispatchStatus
run_id?: string | null
task: TaskDetail
payload: Record<string, unknown>
}
export interface TaskCreateInput {
title: string
description?: string
status?: TaskStatus
priority?: TaskPriority
due_date?: string
tags?: string[]
source?: TaskSource
conversation_id?: string
quadrant?: TaskQuadrant
assignee_type?: TaskAssigneeType
assignee_id?: string
subtasks?: Array<{
title: string
description?: string
status?: TaskStatus
order_index?: number
assignee_type?: TaskAssigneeType
assignee_id?: string
}>
dispatch_to_commander?: boolean
}
export interface TaskUpdateInput {
title?: string
description?: string | null
status?: TaskStatus
priority?: TaskPriority
due_date?: string | null
tags?: string[]
source?: TaskSource
conversation_id?: string | null
quadrant?: TaskQuadrant | null
assignee_type?: TaskAssigneeType | null
assignee_id?: string | null
dispatch_status?: TaskDispatchStatus
dispatch_run_id?: string | null
result_summary?: string | null
started_at?: string | null
last_synced_at?: string | null
}
export interface TaskSubTaskCreateInput {
title: string
description?: string
status?: TaskStatus
order_index?: number
assignee_type?: TaskAssigneeType
assignee_id?: string
}
export interface TaskSubTaskUpdateInput {
title?: string
description?: string | null
status?: TaskStatus
order_index?: number
assignee_type?: TaskAssigneeType | null
assignee_id?: string | null
dispatch_status?: TaskDispatchStatus
dispatch_run_id?: string | null
result_summary?: string | null
}
export interface TaskSubTaskReorderInput {
items: Array<{ id: string; order_index: number }>
}
export interface TaskDispatchInput {
target?: 'commander'
conversation_id?: string
assignee_type?: TaskAssigneeType
assignee_id?: string
}
export const taskApi = {
list(filters?: { status?: TaskStatus; due_date?: string; date_from?: string; date_to?: string }) {
list(filters?: {
status?: TaskStatus
due_date?: string
date_from?: string
date_to?: string
quadrant?: TaskQuadrant
assignee_type?: TaskAssigneeType
dispatch_status?: TaskDispatchStatus
conversation_id?: string
}) {
return api.get<Task[]>('/api/tasks', { params: filters ?? {} })
},
create(data: { title: string; description?: string; priority?: TaskPriority; due_date?: string }) {
return api.post<Task>('/api/tasks', data)
create(data: TaskCreateInput) {
return api.post<TaskDetail>('/api/tasks', data)
},
update(id: string, data: Partial<Task>) {
return api.patch<Task>(`/api/tasks/${id}`, data)
detail(id: string) {
return api.get<TaskDetail>(`/api/tasks/${id}`)
},
update(id: string, data: TaskUpdateInput) {
return api.patch<TaskDetail>(`/api/tasks/${id}`, data)
},
delete(id: string) {
return api.delete(`/api/tasks/${id}`)
},
createSubtask(taskId: string, data: TaskSubTaskCreateInput) {
return api.post<TaskSubTask>(`/api/tasks/${taskId}/subtasks`, data)
},
updateSubtask(taskId: string, subtaskId: string, data: TaskSubTaskUpdateInput) {
return api.patch<TaskSubTask>(`/api/tasks/${taskId}/subtasks/${subtaskId}`, data)
},
deleteSubtask(taskId: string, subtaskId: string) {
return api.delete(`/api/tasks/${taskId}/subtasks/${subtaskId}`)
},
reorderSubtasks(taskId: string, data: TaskSubTaskReorderInput) {
return api.post<TaskSubTask[]>(`/api/tasks/${taskId}/subtasks/reorder`, data)
},
dispatch(taskId: string, data: TaskDispatchInput = {}) {
return api.post<TaskDispatchResponse>(`/api/tasks/${taskId}/dispatch`, { target: 'commander', ...data })
},
dispatchSubtask(taskId: string, subtaskId: string, data: TaskDispatchInput = {}) {
return api.post<TaskDispatchResponse>(`/api/tasks/${taskId}/subtasks/${subtaskId}/dispatch`, { target: 'commander', ...data })
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,104 +1,155 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { ScheduleCenterCommanderSummary } from '@/api/scheduleCenter'
import type { TodayStatusQuadrantView } from '@/pages/chat/composables/useSidebarPlan'
interface KanbanTask {
id: string
title: string
completed: boolean
}
interface KanbanQuadrant {
id: string
title: string
color: string
tasks: KanbanTask[]
}
defineProps<{
visible: boolean
}>()
const emit = defineEmits<{
close: []
toggleTask: [taskId: string]
openDetail: [quadrantId: string]
}>()
const quadrants = computed<KanbanQuadrant[]>(() => [
const FALLBACK_QUADRANTS: TodayStatusQuadrantView[] = [
{
id: 'urgent-important',
title: '重要且紧急',
color: '#f56565',
tasks: [
{ id: '1', title: '将2.0的服务部署到83服务器上', completed: true },
{ id: '2', title: '将83上的遗留的ygzy, recommand和langchain部署起来', completed: true },
{ id: '3', title: '本地知识库接口开发', completed: true },
{ id: '4', title: '将"远光智言"中加入本地文档的问答功能', completed: true },
],
subtitle: 'CRITICAL',
color: '#ff4757',
glowColor: 'rgba(255, 71, 87, 0.4)',
icon: '◈',
tasks: [],
},
{
id: 'not-urgent-important',
title: '重要不紧急',
color: '#ecc94b',
tasks: [
{ id: '5', title: '关于X Request的集成', completed: false },
{ id: '6', title: '调研GLM Code', completed: false },
{ id: '7', title: '页面集成llamafactory功能进行开发', completed: false },
{ id: '8', title: '预训练功能开发', completed: false },
{ id: '9', title: '数据生成相关代码', completed: true },
{ id: '10', title: '调研Transformer的一些基础原理', completed: true },
{ id: '11', title: '英文分词并且找寻关键词', completed: false },
{ id: '12', title: '适配多个数据库类型', completed: false },
],
subtitle: 'PLANNED',
color: '#ffd93d',
glowColor: 'rgba(255, 217, 61, 0.4)',
icon: '◇',
tasks: [],
},
{
id: 'urgent-not-important',
title: '紧急不重要',
color: '#42b9f5',
tasks: [
{ id: '13', title: '文件预处理功能', completed: false },
{ id: '14', title: '机器学习 - 西瓜书', completed: false },
],
subtitle: 'DELEGATE',
color: '#00d4ff',
glowColor: 'rgba(0, 212, 255, 0.4)',
icon: '◉',
tasks: [],
},
{
id: 'not-urgent-not-important',
title: '不重要不紧急',
color: '#97c950',
subtitle: 'ELIMINATE',
color: '#6bcf7f',
glowColor: 'rgba(107, 207, 127, 0.4)',
icon: '○',
tasks: [],
},
])
]
const props = withDefaults(defineProps<{
visible: boolean
quadrants?: TodayStatusQuadrantView[]
commanderSummary?: ScheduleCenterCommanderSummary
}>(), {
quadrants: () => [],
commanderSummary: () => ({
total: 0,
queued: 0,
running: 0,
completed: 0,
failed: 0,
overall_status: 'idle',
}),
})
const emit = defineEmits<{
close: []
createTask: [quadrantId: string]
openTask: [taskId: string]
}>()
const displayQuadrants = computed(() => {
const quadrants = props.quadrants ?? []
return quadrants.length > 0 ? quadrants : FALLBACK_QUADRANTS
})
const totalTaskCount = computed(() => displayQuadrants.value.reduce((sum, quadrant) => sum + quadrant.tasks.length, 0))
const completedTaskCount = computed(() => displayQuadrants.value.reduce((sum, quadrant) => sum + quadrant.tasks.filter((task) => task.completed).length, 0))
const completionRate = computed(() => Math.round((completedTaskCount.value / Math.max(1, totalTaskCount.value)) * 100))
function commanderText() {
const summary = props.commanderSummary
if (!summary.total) return 'COMMANDER IDLE'
return `CMD ${summary.overall_status.toUpperCase()} · Q${summary.queued} R${summary.running} C${summary.completed} F${summary.failed}`
}
</script>
<template>
<aside class="kanban-panel" :class="{ visible }">
<div class="kanban-frame">
<!-- Tech Header -->
<div class="kanban-header">
<div class="kanban-title-row">
<span class="kanban-cloud"></span>
<span class="kanban-title">我的待办</span>
<div class="kanban-icon">
<span class="icon-pulse"></span>
<span class="icon-core"></span>
</div>
<div class="kanban-title-group">
<span class="kanban-title">ISSUE STATUS</span>
<span class="kanban-subtitle">PERSISTENT QUADRANT TASK BOARD</span>
</div>
</div>
<div class="kanban-nav">
<a href="#" class="nav-link">🗓 待办日程</a>
<a href="#" class="nav-link">🗹 已完成的任务</a>
<a href="#" class="nav-link"> 我关注的任务</a>
<a href="#" class="nav-link"> 周报/日报</a>
<div class="kanban-tech-lines">
<span class="tech-line"></span>
<span class="tech-dot"></span>
<span class="tech-line"></span>
</div>
<button class="kanban-close" type="button" aria-label="Close kanban" @click="emit('close')">
×
<span class="close-line line-1"></span>
<span class="close-line line-2"></span>
</button>
</div>
<!-- Quadrants Grid -->
<div class="kanban-quadrants">
<div
v-for="quadrant in quadrants"
v-for="quadrant in displayQuadrants"
:key="quadrant.id"
class="kanban-quadrant"
:style="{ '--quadrant-color': quadrant.color }"
:style="{
'--quadrant-color': quadrant.color,
'--quadrant-glow': quadrant.glowColor
}"
>
<!-- Quadrant Glow Effect -->
<div class="quadrant-glow"></div>
<!-- Quadrant Header -->
<div class="quadrant-header">
<span class="quadrant-title"> {{ quadrant.title }}</span>
<button class="quadrant-check" type="button" @click="emit('openDetail', quadrant.id)"></button>
<div class="quadrant-header-left">
<span class="quadrant-icon">{{ quadrant.icon }}</span>
<div class="quadrant-title-group">
<span class="quadrant-title">{{ quadrant.title }}</span>
<span class="quadrant-subtitle">{{ quadrant.subtitle }}</span>
</div>
</div>
<div class="quadrant-actions">
<button
class="quadrant-add"
type="button"
@click.stop="emit('createTask', quadrant.id)"
title="添加任务"
>
<span class="add-icon">+</span>
</button>
<div class="quadrant-stats">
<span class="stat-value">{{ quadrant.tasks.filter(t => t.completed).length }}</span>
<span class="stat-separator">/</span>
<span class="stat-total">{{ quadrant.tasks.length }}</span>
</div>
</div>
</div>
<!-- Scan Line Animation -->
<div class="quadrant-scan"></div>
<!-- Task List -->
<div class="quadrant-content">
<template v-if="quadrant.tasks.length > 0">
<div
@@ -106,18 +157,48 @@ const quadrants = computed<KanbanQuadrant[]>(() => [
:key="task.id"
class="task-item"
:class="{ completed: task.completed }"
@click="emit('toggleTask', task.id)"
@click="emit('openTask', task.id)"
:title="task.assigneeLabel"
>
<input type="checkbox" :checked="task.completed" disabled />
<div class="task-checkbox">
<div class="checkbox-box" :class="{ checked: task.completed }">
<span v-if="task.completed" class="check-mark"></span>
</div>
</div>
<span class="task-title">{{ task.title }}</span>
<div class="task-indicator" :class="{ done: task.completed }"></div>
</div>
</template>
<div v-else class="empty-state">
恭喜你已完成了所有待办
<div class="empty-icon"></div>
<span class="empty-text">任务队列已清空</span>
<div class="empty-pulse"></div>
</div>
</div>
</div>
</div>
<!-- Footer Status Bar -->
<div class="kanban-footer">
<div class="footer-stat">
<span class="footer-label">总任务</span>
<span class="footer-value">{{ totalTaskCount }}</span>
</div>
<div class="footer-divider"></div>
<div class="footer-stat">
<span class="footer-label">已完成</span>
<span class="footer-value completed">{{ completedTaskCount }}</span>
</div>
<div class="footer-divider"></div>
<div class="footer-stat">
<span class="footer-label">完成率</span>
<span class="footer-value rate">{{ completionRate }}%</span>
</div>
<div class="footer-time">
<span class="time-dot"></span>
<span class="time-text">{{ commanderText() }}</span>
</div>
</div>
</div>
</aside>
</template>
@@ -139,186 +220,608 @@ const quadrants = computed<KanbanQuadrant[]>(() => [
.kanban-frame {
height: 100%;
border-radius: 18px;
border: 1px solid rgba(34, 211, 238, 0.18);
background: linear-gradient(180deg, rgba(8, 14, 28, 0.96), rgba(6, 10, 20, 0.92));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 0 20px rgba(34, 211, 238, 0.08);
border-radius: 16px;
border: 1px solid rgba(0, 245, 212, 0.25);
background:
linear-gradient(135deg, rgba(8, 18, 36, 0.95) 0%, rgba(5, 10, 20, 0.98) 100%),
repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 245, 212, 0.02) 2px,
rgba(0, 245, 212, 0.02) 4px
);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.05),
0 0 40px rgba(0, 245, 212, 0.1),
0 0 80px rgba(0, 245, 212, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* Tech grid overlay */
.kanban-frame::before {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(0, 245, 212, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 245, 212, 0.03) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
z-index: 0;
}
/* ── Header ── */
.kanban-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid rgba(0, 243, 255, 0.1);
padding: 16px 20px;
border-bottom: 1px solid rgba(0, 245, 212, 0.15);
flex-shrink: 0;
position: relative;
z-index: 1;
background: linear-gradient(180deg, rgba(0, 245, 212, 0.08) 0%, transparent 100%);
}
.kanban-title-row {
display: flex;
align-items: center;
gap: 6px;
gap: 12px;
}
.kanban-cloud {
font-size: 16px;
.kanban-icon {
position: relative;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-core {
font-size: 20px;
color: var(--accent-cyan);
text-shadow: 0 0 10px var(--accent-cyan);
z-index: 1;
}
.icon-pulse {
position: absolute;
inset: 0;
border: 1px solid var(--accent-cyan);
border-radius: 50%;
animation: icon-pulse 2s ease-in-out infinite;
}
@keyframes icon-pulse {
0%, 100% { transform: scale(1); opacity: 0.8; }
50% { transform: scale(1.3); opacity: 0; }
}
.kanban-title-group {
display: flex;
flex-direction: column;
gap: 2px;
}
.kanban-title {
font-family: var(--font-display);
font-size: 14px;
font-weight: 600;
color: #e0f7ff;
letter-spacing: 0.08em;
}
.kanban-nav {
display: flex;
gap: 20px;
font-size: 14px;
}
.kanban-nav .nav-link {
color: var(--text-secondary);
text-decoration: none;
}
.kanban-nav .nav-link:hover {
font-size: 16px;
font-weight: 700;
color: var(--accent-cyan);
letter-spacing: 0.15em;
text-shadow: 0 0 20px rgba(0, 245, 212, 0.5);
}
.kanban-subtitle {
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-dim);
letter-spacing: 0.1em;
}
.kanban-tech-lines {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 0 20px;
}
.tech-line {
height: 1px;
flex: 1;
background: linear-gradient(90deg, transparent, rgba(0, 245, 212, 0.3), transparent);
}
.tech-dot {
width: 4px;
height: 4px;
background: var(--accent-cyan);
border-radius: 50%;
box-shadow: 0 0 8px var(--accent-cyan);
}
.kanban-close {
margin-left: 12px;
width: 28px;
height: 28px;
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid rgba(148, 163, 184, 0.16);
background: rgba(8, 14, 26, 0.86);
border: 1px solid rgba(148, 163, 184, 0.2);
background: rgba(8, 14, 26, 0.8);
color: var(--text-dim);
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all var(--transition-fast);
position: relative;
overflow: hidden;
}
.kanban-close:hover {
border-color: rgba(34, 211, 238, 0.4);
color: var(--accent-cyan);
background: rgba(8, 20, 36, 0.96);
border-color: rgba(255, 71, 87, 0.5);
color: #ff4757;
background: rgba(255, 71, 87, 0.1);
box-shadow: 0 0 15px rgba(255, 71, 87, 0.2);
}
.close-line {
position: absolute;
width: 14px;
height: 1.5px;
background: currentColor;
transition: all var(--transition-fast);
}
.line-1 { transform: rotate(45deg); }
.line-2 { transform: rotate(-45deg); }
.kanban-close:hover .line-1 { transform: rotate(135deg); }
.kanban-close:hover .line-2 { transform: rotate(45deg); }
/* ── Quadrants ── */
.kanban-quadrants {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 10px;
gap: 12px;
padding: 12px;
min-height: 0;
overflow: hidden;
position: relative;
z-index: 1;
}
.kanban-quadrant {
background: rgba(0, 20, 40, 0.4);
border: 1px solid rgba(255, 255, 255, 0.06);
background:
linear-gradient(180deg, rgba(0, 20, 40, 0.6) 0%, rgba(0, 10, 20, 0.8) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
transition: all 0.3s ease;
}
.kanban-quadrant:hover {
border-color: var(--quadrant-color);
box-shadow:
0 0 20px var(--quadrant-glow),
inset 0 0 30px rgba(0, 0, 0, 0.3);
transform: scale(1.01);
}
.kanban-quadrant:hover .quadrant-glow {
opacity: 1;
}
.quadrant-glow {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 60%;
background: radial-gradient(ellipse at top, var(--quadrant-glow) 0%, transparent 70%);
opacity: 0.3;
transition: opacity 0.3s ease;
pointer-events: none;
}
.quadrant-scan {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--quadrant-color), transparent);
animation: quadrant-scan 3s linear infinite;
opacity: 0.6;
}
@keyframes quadrant-scan {
0% { top: 0; opacity: 0.6; }
50% { opacity: 0.3; }
100% { top: 100%; opacity: 0; }
}
.quadrant-header {
padding: 8px 10px;
color: #fff;
font-weight: 600;
font-size: 11px;
padding: 12px 14px;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--quadrant-color);
flex-shrink: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, transparent 100%);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
position: relative;
z-index: 1;
}
.quadrant-header-left {
display: flex;
align-items: center;
gap: 10px;
}
.quadrant-icon {
font-size: 18px;
color: var(--quadrant-color);
text-shadow: 0 0 10px var(--quadrant-glow);
}
.quadrant-title-group {
display: flex;
flex-direction: column;
gap: 1px;
}
.quadrant-title {
letter-spacing: 0.05em;
font-family: var(--font-display);
font-size: 12px;
font-weight: 600;
color: #fff;
letter-spacing: 0.08em;
}
.quadrant-check {
.quadrant-subtitle {
font-family: var(--font-mono);
font-size: 8px;
color: var(--quadrant-color);
letter-spacing: 0.15em;
opacity: 0.8;
background: none;
border: none;
color: inherit;
font-size: inherit;
}
.quadrant-actions {
display: flex;
align-items: center;
gap: 10px;
}
.quadrant-add {
width: 22px;
height: 22px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(0, 0, 0, 0.3);
color: var(--text-dim);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all var(--transition-fast);
padding: 0;
}
.quadrant-add:hover {
border-color: var(--quadrant-color);
color: var(--quadrant-color);
background: rgba(255, 255, 255, 0.05);
box-shadow: 0 0 10px var(--quadrant-glow);
}
.quadrant-add .add-icon {
font-size: 14px;
font-weight: 600;
line-height: 1;
}
.quadrant-check:hover {
opacity: 1;
.quadrant-stats {
display: flex;
align-items: baseline;
gap: 2px;
font-family: var(--font-mono);
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: var(--quadrant-color);
text-shadow: 0 0 10px var(--quadrant-glow);
}
.stat-separator {
font-size: 12px;
color: var(--text-dim);
margin: 0 2px;
}
.stat-total {
font-size: 11px;
color: var(--text-dim);
}
.quadrant-content {
flex: 1;
padding: 6px 8px;
padding: 8px 10px;
overflow-y: auto;
min-height: 0;
position: relative;
z-index: 1;
}
/* ── Tasks ── */
.task-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 4px;
gap: 10px;
padding: 8px 6px;
font-size: 11px;
color: #e0f7ff;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
color: var(--text-secondary);
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
cursor: pointer;
transition: background-color 0.2s;
transition: all 0.2s ease;
position: relative;
}
.task-item:hover {
background-color: rgba(0, 243, 255, 0.08);
background: rgba(0, 245, 212, 0.05);
padding-left: 10px;
}
.task-item input[type="checkbox"] {
width: 12px;
height: 12px;
cursor: pointer;
flex-shrink: 0;
.task-item:hover .task-indicator {
opacity: 1;
transform: scaleY(1);
}
.task-item.completed {
color: var(--text-dim);
}
.task-item.completed .task-title {
text-decoration: line-through;
color: rgba(224, 247, 255, 0.5);
background-color: rgba(0, 243, 255, 0.06);
opacity: 0.5;
}
.task-checkbox {
flex-shrink: 0;
}
.checkbox-box {
width: 16px;
height: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
background: rgba(0, 0, 0, 0.3);
}
.checkbox-box.checked {
background: var(--quadrant-color);
border-color: var(--quadrant-color);
box-shadow: 0 0 8px var(--quadrant-glow);
}
.check-mark {
font-size: 10px;
color: #000;
font-weight: bold;
}
.task-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: all 0.2s ease;
}
.task-indicator {
width: 3px;
height: 12px;
background: var(--quadrant-color);
border-radius: 2px;
opacity: 0;
transform: scaleY(0);
transition: all 0.2s ease;
box-shadow: 0 0 6px var(--quadrant-glow);
}
.task-indicator.done {
opacity: 1;
transform: scaleY(1);
background: var(--accent-green);
box-shadow: 0 0 6px rgba(107, 207, 127, 0.4);
}
/* ── Empty State ── */
.empty-state {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: rgba(224, 247, 255, 0.4);
font-size: 12px;
text-align: center;
padding: 20px;
gap: 8px;
position: relative;
}
/* Scrollbar styling */
.quadrant-content::-webkit-scrollbar {
.empty-icon {
font-size: 28px;
color: var(--quadrant-color);
opacity: 0.4;
}
.empty-text {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-dim);
letter-spacing: 0.1em;
}
.empty-pulse {
position: absolute;
width: 60px;
height: 60px;
border: 1px solid var(--quadrant-color);
border-radius: 50%;
opacity: 0.2;
animation: empty-pulse 2s ease-in-out infinite;
}
@keyframes empty-pulse {
0%, 100% { transform: scale(1); opacity: 0.2; }
50% { transform: scale(1.3); opacity: 0; }
}
/* ── Add Task Buttons ── */
.empty-add-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
margin-top: 10px;
border: 1px dashed rgba(255, 255, 255, 0.2);
border-radius: 8px;
background: rgba(0, 0, 0, 0.3);
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 10px;
cursor: pointer;
transition: all 0.2s ease;
}
.empty-add-btn:hover {
border-color: var(--quadrant-color);
color: var(--quadrant-color);
background: rgba(255, 255, 255, 0.05);
box-shadow: 0 0 15px var(--quadrant-glow);
}
.inline-add-btn {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 8px 6px;
margin-top: 6px;
border: 1px dashed rgba(255, 255, 255, 0.1);
border-radius: 6px;
background: transparent;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 10px;
cursor: pointer;
transition: all 0.2s ease;
opacity: 0.6;
}
.inline-add-btn:hover {
opacity: 1;
border-color: var(--quadrant-color);
color: var(--quadrant-color);
background: rgba(255, 255, 255, 0.03);
}
.add-icon-small {
font-size: 12px;
font-weight: 600;
}
/* ── Footer ── */
.kanban-footer {
display: flex;
align-items: center;
padding: 12px 20px;
border-top: 1px solid rgba(0, 245, 212, 0.1);
background: linear-gradient(180deg, transparent 0%, rgba(0, 245, 212, 0.05) 100%);
position: relative;
z-index: 1;
gap: 16px;
}
.footer-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.footer-label {
font-family: var(--font-mono);
font-size: 8px;
color: var(--text-dim);
letter-spacing: 0.1em;
}
.footer-value {
font-family: var(--font-display);
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.footer-value.completed {
color: var(--accent-green);
text-shadow: 0 0 10px rgba(107, 207, 127, 0.5);
}
.footer-value.rate {
color: var(--accent-cyan);
text-shadow: 0 0 10px rgba(0, 245, 212, 0.5);
}
.footer-divider {
width: 1px;
height: 24px;
background: rgba(0, 245, 212, 0.2);
}
.footer-time {
margin-left: auto;
display: flex;
align-items: center;
gap: 6px;
}
.time-dot {
width: 6px;
height: 6px;
background: var(--accent-cyan);
border-radius: 50%;
animation: time-blink 1.5s ease-in-out infinite;
}
@keyframes time-blink {
0%, 100% { opacity: 1; box-shadow: 0 0 6px var(--accent-cyan); }
50% { opacity: 0.4; box-shadow: none; }
}
.time-text {
font-family: var(--font-mono);
font-size: 8px;
color: var(--accent-cyan);
letter-spacing: 0.15em;
opacity: 0.7;
}
/* Scrollbar */
.quadrant-content::-webkit-scrollbar {
width: 4px;
}
.quadrant-content::-webkit-scrollbar-track {
@@ -326,11 +829,11 @@ const quadrants = computed<KanbanQuadrant[]>(() => [
}
.quadrant-content::-webkit-scrollbar-thumb {
background: rgba(0, 243, 255, 0.2);
border-radius: 3px;
background: rgba(0, 245, 212, 0.2);
border-radius: 2px;
}
.quadrant-content::-webkit-scrollbar-thumb:hover {
background: rgba(0, 243, 255, 0.4);
background: rgba(0, 245, 212, 0.4);
}
</style>
</style>