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:
@@ -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,
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user