新增: 初始化 DevFlow 项目仓库
Tauri 2 + Vue 3 + Vite 6 桌面应用,Rust workspace 含 13 个 crate (df-ai / df-storage / df-workflow / df-core / df-execute 等)。 核心能力:AI 聊天 agentic 循环(工具调用+人工审批)、工作流引擎、 任务/想法/项目/阶段管理、可追溯性,及配套前端组件。
This commit is contained in:
388
src/views/Tasks.vue
Normal file
388
src/views/Tasks.vue
Normal file
@@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<div class="tasks">
|
||||
<header class="page-header">
|
||||
<h1>🔀 任务队列</h1>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-ghost" @click="refresh">🔄 刷新</button>
|
||||
<button class="btn btn-primary" @click="openCreateModal">+ 新建任务</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">项目:</span>
|
||||
<button
|
||||
v-for="p in projectFilters"
|
||||
:key="p.key"
|
||||
class="filter-btn"
|
||||
:class="{ active: activeProject === p.key }"
|
||||
@click="activeProject = p.key"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">状态:</span>
|
||||
<button
|
||||
v-for="s in statusFilters"
|
||||
:key="s.key"
|
||||
class="filter-btn"
|
||||
:class="{ active: activeStatus === s.key }"
|
||||
@click="activeStatus = s.key"
|
||||
>
|
||||
{{ s.icon }} {{ s.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按项目分组 -->
|
||||
<div class="task-groups">
|
||||
<section
|
||||
v-for="group in filteredGroups"
|
||||
:key="group.projectName"
|
||||
class="task-group"
|
||||
>
|
||||
<div class="group-header">
|
||||
<span class="group-icon">{{ group.icon }}</span>
|
||||
<h2 class="group-name">{{ group.projectName }}</h2>
|
||||
<span class="group-count">{{ group.tasks.length }} 个任务</span>
|
||||
</div>
|
||||
<div class="task-list">
|
||||
<div class="task-item" v-for="task in group.tasks" :key="task.id">
|
||||
<div class="task-main">
|
||||
<div class="task-title-row">
|
||||
<span class="task-title">{{ task.title }}</span>
|
||||
<span class="priority-badge" :class="priorityClass(task.priority)">{{ priorityLabel(task.priority) }}</span>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="branch-tag" v-if="task.branch_name">
|
||||
<span class="branch-icon">⑂</span>
|
||||
{{ task.branch_name }}
|
||||
</span>
|
||||
<span class="task-date">{{ formatTime(task.created_at) }}</span>
|
||||
<span class="task-time">{{ formatTime(task.updated_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="status-tag" :class="'status-' + task.status">{{ statusLabel(task.status) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 新建任务模态框 -->
|
||||
<div class="modal-overlay" v-if="showCreateModal" @click.self="showCreateModal = false">
|
||||
<div class="modal-box">
|
||||
<h3>新建任务</h3>
|
||||
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">项目</label>
|
||||
<select v-model="newTaskProjectId" style="width:100%;padding:8px 12px;border: 0.5px solid var(--df-border);border-radius: var(--df-radius-sm);background:var(--df-bg);color:var(--df-text);font-size:13px;margin-bottom:12px">
|
||||
<option v-for="p in store.projects" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">标题</label>
|
||||
<input v-model="newTaskTitle" placeholder="输入任务标题..." @keyup.enter="confirmCreate" />
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" @click="showCreateModal = false">取消</button>
|
||||
<button class="btn-confirm" @click="confirmCreate">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import { formatRelativeZh } from '../utils/time'
|
||||
import type { TaskRecord } from '../api/types'
|
||||
|
||||
const store = useProjectStore()
|
||||
|
||||
const activeProject = ref('all')
|
||||
const activeStatus = ref('all')
|
||||
|
||||
// ── 新建任务模态框 ──
|
||||
const showCreateModal = ref(false)
|
||||
const newTaskProjectId = ref('')
|
||||
const newTaskTitle = ref('')
|
||||
|
||||
interface TaskGroup {
|
||||
projectName: string
|
||||
icon: string
|
||||
tasks: TaskRecord[]
|
||||
}
|
||||
|
||||
const projectFilters = computed(() => [
|
||||
{ key: 'all', label: '全部' },
|
||||
...store.projects.map(p => ({ key: p.id, label: p.name })),
|
||||
])
|
||||
|
||||
const statusFilters: { key: string; label: string; icon: string }[] = [
|
||||
{ key: 'all', label: '全部', icon: '📋' },
|
||||
{ key: 'todo', label: '待开始', icon: '📝' },
|
||||
{ key: 'in_progress', label: '进行中', icon: '🔨' },
|
||||
{ key: 'review_ready', label: '待审查', icon: '👀' },
|
||||
{ key: 'merged', label: '已合并', icon: '✅' },
|
||||
{ key: 'abandoned', label: '已废弃', icon: '🗑️' },
|
||||
]
|
||||
|
||||
function getProjectName(projectId: string): string {
|
||||
return store.projects.find(p => p.id === projectId)?.name ?? '未知项目'
|
||||
}
|
||||
|
||||
const projectIcons: Record<string, string> = {
|
||||
'u-ask': '🤖',
|
||||
'u-img': '🖼️',
|
||||
'flux': '⚡',
|
||||
}
|
||||
|
||||
const filteredGroups = computed(() => {
|
||||
let filtered = store.tasks
|
||||
if (activeProject.value !== 'all') {
|
||||
filtered = filtered.filter(t => t.project_id === activeProject.value)
|
||||
}
|
||||
if (activeStatus.value !== 'all') {
|
||||
filtered = filtered.filter(t => t.status === activeStatus.value)
|
||||
}
|
||||
|
||||
const groupMap = new Map<string, TaskRecord[]>()
|
||||
for (const task of filtered) {
|
||||
if (!groupMap.has(task.project_id)) {
|
||||
groupMap.set(task.project_id, [])
|
||||
}
|
||||
groupMap.get(task.project_id)!.push(task)
|
||||
}
|
||||
|
||||
const result: TaskGroup[] = []
|
||||
for (const [projectId, groupTasks] of groupMap) {
|
||||
const name = getProjectName(projectId)
|
||||
result.push({
|
||||
projectName: name,
|
||||
icon: projectIcons[name] || '📂',
|
||||
tasks: groupTasks,
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
function statusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
todo: '📝 待开始',
|
||||
in_progress: '🔨 进行中',
|
||||
review_ready: '👀 待审查',
|
||||
merged: '✅ 已合并',
|
||||
abandoned: '🗑️ 已废弃',
|
||||
}
|
||||
return map[status] ?? status
|
||||
}
|
||||
|
||||
function priorityLabel(priority: number) {
|
||||
const map: Record<number, string> = { 0: 'P0', 1: 'P1', 2: 'P2', 3: 'P3' }
|
||||
return map[priority] ?? `P${priority}`
|
||||
}
|
||||
|
||||
function priorityClass(priority: number): string {
|
||||
const map: Record<number, string> = { 0: 'priority-critical', 1: 'priority-high', 2: 'priority-medium', 3: 'priority-low' }
|
||||
return map[priority] ?? 'priority-low'
|
||||
}
|
||||
|
||||
function formatTime(timestamp: string | number): string {
|
||||
return formatRelativeZh(timestamp)
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await store.loadTasks()
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
newTaskProjectId.value = store.projects.length > 0 ? store.projects[0].id : ''
|
||||
newTaskTitle.value = ''
|
||||
showCreateModal.value = true
|
||||
}
|
||||
|
||||
async function confirmCreate() {
|
||||
if (!newTaskTitle.value.trim() || !newTaskProjectId.value) return
|
||||
await store.createTask({
|
||||
project_id: newTaskProjectId.value,
|
||||
title: newTaskTitle.value.trim(),
|
||||
})
|
||||
showCreateModal.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([store.loadProjects(), store.loadTasks()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tasks { padding: 16px 20px 20px; }
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--df-gap-page);
|
||||
}
|
||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
|
||||
.header-actions { display: flex; gap: 10px; }
|
||||
|
||||
/* ===== 按钮 ===== */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: var(--df-radius-sm);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.btn-primary { background: var(--df-accent); color: #fff; }
|
||||
.btn-primary:hover { background: var(--df-accent-hover); }
|
||||
.btn-ghost { background: transparent; color: var(--df-text-secondary); border: 0.5px solid var(--df-border); }
|
||||
.btn-ghost:hover { background: var(--df-bg-card); color: var(--df-text); }
|
||||
|
||||
/* ===== 筛选栏 ===== */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: var(--df-gap-page);
|
||||
padding: var(--df-pad-panel);
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
}
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.filter-label {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.filter-btn {
|
||||
padding: 4px 12px;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--df-text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.filter-btn:hover { background: rgba(108, 99, 255, 0.06); color: var(--df-text); }
|
||||
.filter-btn.active {
|
||||
background: var(--df-accent);
|
||||
color: #fff;
|
||||
border-color: var(--df-accent);
|
||||
}
|
||||
|
||||
/* ===== 任务分组 ===== */
|
||||
.task-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--df-gap-grid);
|
||||
}
|
||||
|
||||
.task-group {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
padding: var(--df-pad-panel);
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--df-gap-head);
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
}
|
||||
.group-icon { font-size: 18px; }
|
||||
.group-name { font-size: 15px; font-weight: 500; color: var(--df-text); }
|
||||
.group-count { font-size: 12px; color: var(--df-text-dim); margin-left: 8px; }
|
||||
|
||||
/* ===== 任务条目 ===== */
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.task-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--df-radius);
|
||||
transition: background 0.1s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.task-item:hover { background: rgba(108, 99, 255, 0.04); }
|
||||
|
||||
.task-main { flex: 1; min-width: 0; }
|
||||
|
||||
.task-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.task-title { font-size: 14px; font-weight: 500; color: var(--df-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.priority-badge {
|
||||
font-size: 10px; font-weight: 500;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
}
|
||||
.priority-critical { background: rgba(255,107,107,0.2); color: var(--df-danger); }
|
||||
.priority-high { background: rgba(255,152,0,0.2); color: #ff9800; }
|
||||
.priority-medium { background: rgba(100,181,246,0.2); color: var(--df-info); }
|
||||
.priority-low { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.branch-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 12px;
|
||||
color: var(--df-text-secondary);
|
||||
background: rgba(108, 99, 255, 0.06);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
font-family: monospace;
|
||||
}
|
||||
.branch-icon { font-size: 12px; color: var(--df-accent); }
|
||||
.task-date { font-size: 11px; color: var(--df-text-dim); }
|
||||
.task-time {
|
||||
font-size: 11px;
|
||||
color: var(--df-text-dim);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ===== 状态标签 ===== */
|
||||
.status-tag {
|
||||
font-size: 11px; font-weight: 500;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-created { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
||||
.status-in_progress { background: rgba(100,181,246,0.2); color: var(--df-info); }
|
||||
.status-review_ready { background: rgba(255,217,61,0.2); color: var(--df-warning); }
|
||||
.status-merged { background: rgba(100,255,218,0.15); color: var(--df-success); }
|
||||
.status-abandoned { background: rgba(255,107,107,0.2); color: var(--df-danger); }
|
||||
.status-todo { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
||||
|
||||
/* ===== 模态框 ===== */
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
||||
.modal-box { background: var(--df-bg-card); border: 0.5px solid var(--df-border); border-radius: var(--df-radius-lg); padding: 24px; min-width: 360px; max-width: 90vw; }
|
||||
.modal-box h3 { color: var(--df-text); margin-bottom: 16px; }
|
||||
.modal-box input, .modal-box select { width: 100%; padding: 8px 12px; border: 0.5px solid var(--df-border); border-radius: var(--df-radius-sm); background: var(--df-bg); color: var(--df-text); font-size: 13px; margin-bottom: 12px; box-sizing: border-box; }
|
||||
.modal-box .modal-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
||||
.modal-box .btn-cancel { padding: 6px 16px; border: 0.5px solid var(--df-border); border-radius: var(--df-radius-sm); background: transparent; color: var(--df-text-secondary); cursor: pointer; }
|
||||
.modal-box .btn-confirm { padding: 6px 16px; border: none; border-radius: var(--df-radius-sm); background: var(--df-accent); color: #fff; cursor: pointer; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user