feat(frontend): redesign KanbanDetail modal - remove sidebar, add editable title, subtasks with drag-drop

This commit is contained in:
2026-04-07 13:16:34 +08:00
parent 7aef898bf5
commit 536c541a5b

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Trash2, Pencil } from 'lucide-vue-next'
interface Task {
id: string
@@ -24,6 +25,8 @@ const emit = defineEmits<{
const activeTab = ref<'comments' | 'history'>('comments')
const commentInput = ref('')
const isEditingTitle = ref(false)
const editingTitle = ref('')
const mockTask: Task = {
id: '1',
@@ -34,32 +37,138 @@ const mockTask: Task = {
priority: '重要且紧急 (P1)',
createdAt: '2024-03-21 14:49:35',
}
const description = ref('')
const agents = [
{ id: 'planner', name: '规划师', icon: '📋' },
{ id: 'executor', name: '执行者', icon: '⚡' },
{ id: 'knowledge', name: '知识管理员', icon: '🧠' },
{ id: 'analyst', name: '分析师', icon: '🔍' },
{ id: 'coder', name: '程序员', icon: '💻' },
{ id: 'researcher', name: '研究员', icon: '🔬' },
]
const selectedAgent = ref(agents[0].id)
const priorities = [
{ id: 'urgent-important', name: '重要且紧急', color: '#f56565' },
{ id: 'not-urgent-important', name: '重要不紧急', color: '#ecc94b' },
{ id: 'urgent-not-important', name: '紧急不重要', color: '#42b9f5' },
{ id: 'not-urgent-not-important', name: '不重要不紧急', color: '#97c950' },
]
const selectedPriority = ref('urgent-important')
const taskStatuses = [
{ id: 'pending', name: '未完成' },
{ id: 'in_progress', name: '进行中' },
{ id: 'completed', name: '已完成' },
]
const selectedStatus = ref('completed')
interface SubTask {
id: number
title: string
completed: boolean
subAgent: string
}
const subtasks = ref<SubTask[]>([])
let subtaskIdCounter = 0
// Drag and drop state
const draggedIndex = ref<number | null>(null)
const dragOverIndex = ref<number | null>(null)
function onDragStart(index: number) {
draggedIndex.value = index
}
function onDragOver(event: DragEvent, index: number) {
event.preventDefault()
dragOverIndex.value = index
}
function onDragLeave() {
dragOverIndex.value = null
}
function onDrop(event: DragEvent, targetIndex: number) {
event.preventDefault()
if (draggedIndex.value !== null && draggedIndex.value !== targetIndex) {
const item = subtasks.value.splice(draggedIndex.value, 1)[0]
subtasks.value.splice(targetIndex, 0, item)
}
draggedIndex.value = null
dragOverIndex.value = null
}
function onDragEnd() {
draggedIndex.value = null
dragOverIndex.value = null
}
function formatDate() {
return new Date().toLocaleString('zh-CN')
}
function startEditTitle() {
editingTitle.value = mockTask.title
isEditingTitle.value = true
}
function saveTitle() {
if (editingTitle.value.trim()) {
mockTask.title = editingTitle.value.trim()
}
isEditingTitle.value = false
}
function addSubtask() {
subtasks.value.push({
id: ++subtaskIdCounter,
title: '',
completed: false,
subAgent: '',
})
}
function removeSubtask(index: number) {
subtasks.value.splice(index, 1)
}
</script>
<template>
<div class="kanban-detail-overlay" :class="{ visible }" @click.self="emit('close')">
<div class="kanban-detail-panel">
<button class="detail-close" type="button" aria-label="Close detail" @click="emit('close')">
×
</button>
<div class="detail-header">
<div class="task-title-section">
<div class="task-title">
<span class="check-icon"></span>
{{ mockTask.title }}
<input
v-if="isEditingTitle"
v-model="editingTitle"
class="title-input"
maxlength="20"
@blur="saveTitle"
@keyup.enter="saveTitle"
@keyup.escape="isEditingTitle = false"
autofocus
/>
<span v-else class="title-text">{{ mockTask.title }}</span>
<button class="btn-edit-title" type="button" title="修改标题" @click="startEditTitle">
<Pencil :size="14" />
</button>
</div>
<div class="task-meta">
<span class="avatar">C</span>
{{ mockTask.assignee }} 创建于{{ mockTask.createdAt }}
创建于{{ mockTask.createdAt }}
</div>
</div>
<div class="status-badge" :style="{ background: quadrantColor + '20', color: quadrantColor }">
<div class="header-actions">
<button class="btn-delete-icon" type="button" title="删除任务">
<Trash2 :size="16" />
</button>
</div>
</div>
@@ -70,25 +179,40 @@ function formatDate() {
<span class="icon"></span>
描述
</div>
<div class="description-box">
添加详细描述...
</div>
<textarea
v-model="description"
class="description-box"
placeholder="添加详细描述..."
></textarea>
</div>
<div class="task-info-list">
<div class="info-item">
<span class="icon"></span>
负责<span class="avatar">C</span>{{ mockTask.assignname }}
负责指挥官
<select v-model="selectedAgent" class="agent-select">
<option v-for="agent in agents" :key="agent.id" :value="agent.id">
{{ agent.icon }} {{ agent.name }}
</option>
</select>
</div>
<div class="info-item">
<span class="icon"></span>
优先级<span class="priority-high">{{ mockTask.priority }}</span>
优先级
<select v-model="selectedPriority" class="priority-select">
<option v-for="p in priorities" :key="p.id" :value="p.id">
{{ p.name }}
</option>
</select>
</div>
<div class="info-item">
<span class="icon">📅</span>
任务状态<span :class="{ 'status-completed': mockTask.completed }">
{{ mockTask.completed ? '已完成' : '进行中' }}
</span>
任务状态
<select v-model="selectedStatus" class="status-select">
<option v-for="s in taskStatuses" :key="s.id" :value="s.id">
{{ s.name }}
</option>
</select>
</div>
</div>
@@ -98,9 +222,44 @@ function formatDate() {
<span class="icon"></span>
子任务
</span>
<button class="btn-add-subtask" type="button">添加子任务</button>
<button class="btn-add-subtask" type="button" @click="addSubtask">添加子任务</button>
</div>
<div v-if="subtasks.length === 0" class="subtask-empty">暂无子任务</div>
<div v-else class="subtask-list">
<div
v-for="(subtask, index) in subtasks"
:key="subtask.id"
class="subtask-item"
:class="{ 'drag-over': dragOverIndex === index }"
draggable="true"
@dragstart="onDragStart(index)"
@dragover="onDragOver($event, index)"
@dragleave="onDragLeave"
@drop="onDrop($event, index)"
@dragend="onDragEnd"
>
<button class="subtask-drag" type="button" title="拖动排序"></button>
<input
type="checkbox"
v-model="subtask.completed"
class="subtask-checkbox"
/>
<input
v-model="subtask.title"
class="subtask-input"
:class="{ completed: subtask.completed }"
placeholder="输入子任务名称..."
@keyup.enter="addSubtask"
/>
<select v-model="subtask.subAgent" class="subtask-agent">
<option value="">子指挥官</option>
<option v-for="agent in agents" :key="agent.id" :value="agent.id">
{{ agent.icon }} {{ agent.name }}
</option>
</select>
<button class="subtask-delete" type="button" @click="removeSubtask(index)">×</button>
</div>
</div>
<div class="subtask-empty">暂无子任务</div>
</div>
<div class="section">
@@ -120,41 +279,6 @@ function formatDate() {
</div>
</div>
</div>
<div class="sidebar">
<button class="sidebar-btn" type="button">
<span class="icon"></span>
标记未完成
</button>
<button class="sidebar-btn" type="button">
<span class="icon"></span>
优先级
</button>
<button class="sidebar-btn" type="button">
<span class="icon">👤</span>
负责人
</button>
<button class="sidebar-btn" type="button">
<span class="icon">📆</span>
计划时间
</button>
<button class="sidebar-btn" type="button">
<span class="icon">📎</span>
添加附件
</button>
<button class="sidebar-btn" type="button">
<span class="icon">📧</span>
关注人
</button>
<button class="sidebar-btn" type="button">
<span class="icon">📁</span>
归档
</button>
<button class="sidebar-btn btn-delete" type="button">
<span class="icon">🗑</span>
删除
</button>
</div>
</div>
</div>
</div>
@@ -200,23 +324,6 @@ function formatDate() {
transform: scale(1);
}
.detail-close {
position: absolute;
top: 16px;
right: 20px;
font-size: 28px;
color: #ff4d4f;
background: none;
border: none;
cursor: pointer;
line-height: 1;
z-index: 10;
}
.detail-close:hover {
color: #ff7875;
}
.detail-header {
display: flex;
justify-content: space-between;
@@ -225,8 +332,38 @@ function formatDate() {
border-bottom: 1px solid rgba(0, 243, 255, 0.1);
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.btn-delete-icon {
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid rgba(255, 77, 79, 0.3);
background: rgba(255, 77, 79, 0.08);
color: #ff4d4f;
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.btn-delete-icon:hover {
background: rgba(255, 77, 79, 0.2);
border-color: #ff4d4f;
}
.btn-delete-icon :deep(svg) {
stroke: currentColor;
}
.task-title-section {
flex: 1;
min-width: 0;
}
.task-title {
@@ -234,13 +371,71 @@ function formatDate() {
font-weight: 600;
color: #e0f7ff;
margin-bottom: 10px;
display: flex;
display: inline-flex;
align-items: center;
gap: 8px;
max-width: 100%;
}
.check-icon {
color: #52c41a;
flex-shrink: 0;
}
.title-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 400px;
}
.title-input {
font-size: 22px;
font-weight: 600;
font-family: inherit;
color: #e0f7ff;
background: rgba(0, 243, 255, 0.08);
border: 1px solid rgba(0, 243, 255, 0.3);
border-radius: 6px;
padding: 4px 10px;
outline: none;
min-width: 200px;
max-width: 400px;
}
.title-input:focus {
border-color: rgba(0, 243, 255, 0.6);
box-shadow: 0 0 12px rgba(0, 245, 212, 0.2);
}
.btn-edit-title {
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid transparent;
background: transparent;
color: var(--text-dim);
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
flex-shrink: 0;
}
.task-title:hover .btn-edit-title {
opacity: 1;
}
.btn-edit-title:hover {
background: rgba(0, 243, 255, 0.1);
border-color: rgba(0, 243, 255, 0.2);
color: var(--accent-cyan);
}
.btn-edit-title :deep(svg) {
stroke: currentColor;
}
.task-meta {
@@ -262,20 +457,7 @@ function formatDate() {
margin-right: 6px;
}
.status-badge {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
}
.detail-content {
display: grid;
grid-template-columns: 1fr 240px;
gap: 20px;
padding: 20px 24px;
flex: 1;
overflow-y: auto;
@@ -320,13 +502,27 @@ function formatDate() {
}
.description-box {
width: 100%;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 243, 255, 0.1);
border-radius: 8px;
padding: 16px;
min-height: 60px;
min-height: 100px;
color: var(--text-dim);
font-size: 14px;
font-family: inherit;
resize: vertical;
outline: none;
transition: border-color var(--transition-fast);
box-sizing: border-box;
}
.description-box:focus {
border-color: rgba(0, 243, 255, 0.3);
}
.description-box::placeholder {
color: var(--text-dim);
}
.task-info-list {
@@ -350,6 +546,80 @@ function formatDate() {
text-align: center;
}
.agent-select {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 243, 255, 0.2);
border-radius: 6px;
color: #e0f7ff;
font-size: 13px;
font-family: inherit;
padding: 6px 10px;
cursor: pointer;
outline: none;
transition: border-color var(--transition-fast);
}
.agent-select:hover {
border-color: rgba(0, 243, 255, 0.4);
}
.agent-select:focus {
border-color: rgba(0, 243, 255, 0.5);
}
.agent-select option {
background: #0a0f1a;
color: #e0f7ff;
}
.priority-select {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 243, 255, 0.2);
border-radius: 6px;
color: #e0f7ff;
font-size: 13px;
font-family: inherit;
padding: 6px 10px;
cursor: pointer;
outline: none;
transition: border-color var(--transition-fast);
}
.priority-select:hover {
border-color: rgba(0, 243, 255, 0.4);
}
.priority-select:focus {
border-color: rgba(0, 243, 255, 0.5);
}
.priority-select option,
.status-select option {
background: #0a0f1a;
color: #e0f7ff;
}
.status-select {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 243, 255, 0.2);
border-radius: 6px;
color: #e0f7ff;
font-size: 13px;
font-family: inherit;
padding: 6px 10px;
cursor: pointer;
outline: none;
transition: border-color var(--transition-fast);
}
.status-select:hover {
border-color: rgba(0, 243, 255, 0.4);
}
.status-select:focus {
border-color: rgba(0, 243, 255, 0.5);
}
.priority-high {
color: #ff4d4f;
font-weight: 600;
@@ -385,6 +655,124 @@ function formatDate() {
font-size: 14px;
}
.subtask-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.subtask-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(0, 243, 255, 0.08);
border-radius: 8px;
transition: border-color var(--transition-fast), opacity var(--transition-fast);
}
.subtask-item.drag-over {
border-color: rgba(0, 243, 255, 0.5);
border-style: dashed;
}
.subtask-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
flex-shrink: 0;
}
.subtask-input {
flex: 1;
background: transparent;
border: none;
color: #e0f7ff;
font-size: 13px;
font-family: inherit;
outline: none;
}
.subtask-input.completed {
text-decoration: line-through;
color: var(--text-dim);
}
.subtask-input::placeholder {
color: var(--text-dim);
}
.subtask-delete {
width: 24px;
height: 24px;
border: none;
background: transparent;
color: var(--text-dim);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.subtask-delete:hover {
background: rgba(255, 77, 79, 0.2);
color: #ff4d4f;
}
.subtask-drag {
width: 24px;
height: 24px;
border: none;
background: transparent;
color: var(--text-dim);
font-size: 14px;
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.subtask-drag:hover {
background: rgba(0, 243, 255, 0.1);
color: var(--accent-cyan);
}
.subtask-agent {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 243, 255, 0.15);
border-radius: 4px;
color: #e0f7ff;
font-size: 11px;
font-family: inherit;
padding: 4px 6px;
cursor: pointer;
outline: none;
transition: border-color var(--transition-fast);
flex-shrink: 0;
min-width: 80px;
}
.subtask-agent:hover {
border-color: rgba(0, 243, 255, 0.3);
}
.subtask-agent:focus {
border-color: rgba(0, 243, 255, 0.4);
}
.subtask-agent option {
background: #0a0f1a;
color: #e0f7ff;
}
.comment-empty {
color: var(--text-dim);
font-size: 14px;
@@ -436,46 +824,6 @@ function formatDate() {
background: rgba(0, 243, 255, 0.2);
}
.sidebar {
display: flex;
flex-direction: column;
gap: 8px;
}
.sidebar-btn {
padding: 10px 14px;
border: 1px solid rgba(0, 243, 255, 0.15);
border-radius: 6px;
background: rgba(0, 0, 0, 0.2);
color: #e0f7ff;
font-size: 13px;
text-align: left;
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-btn:hover {
border-color: rgba(0, 243, 255, 0.4);
background: rgba(0, 243, 255, 0.08);
}
.sidebar-btn .icon {
font-size: 14px;
}
.sidebar-btn.btn-delete {
border-color: rgba(255, 77, 79, 0.3);
color: #ff4d4f;
}
.sidebar-btn.btn-delete:hover {
background: rgba(255, 77, 79, 0.1);
border-color: #ff4d4f;
}
/* Scrollbar styling */
.detail-content::-webkit-scrollbar {
width: 8px;