diff --git a/src/i18n/en/tasks.ts b/src/i18n/en/tasks.ts
index 7c07ef6..93c5c5f 100644
--- a/src/i18n/en/tasks.ts
+++ b/src/i18n/en/tasks.ts
@@ -38,6 +38,7 @@ export default {
unknownProject: 'Unknown Project',
empty: 'No tasks yet',
},
+ confirmDelete: 'Delete task "{title}"? This action cannot be undone.',
// New task modal
modal: {
diff --git a/src/i18n/zh-CN/tasks.ts b/src/i18n/zh-CN/tasks.ts
index 4e69b64..a81bbcd 100644
--- a/src/i18n/zh-CN/tasks.ts
+++ b/src/i18n/zh-CN/tasks.ts
@@ -38,6 +38,7 @@ export default {
unknownProject: '未知项目',
empty: '暂无任务',
},
+ confirmDelete: '确定删除任务「{title}」吗?此操作不可撤销。',
// 新建任务模态
modal: {
diff --git a/src/views/Tasks.vue b/src/views/Tasks.vue
index deaab06..dc68a6f 100644
--- a/src/views/Tasks.vue
+++ b/src/views/Tasks.vue
@@ -84,7 +84,27 @@
{{ formatRelative(task.updated_at) }}
- {{ $t(statusLabel(task.status)) }}
+
+ {{ $t(statusLabel(task.status)) }}
+
+
+
@@ -145,6 +165,7 @@ import { useI18n } from 'vue-i18n'
import { useProjectStore } from '@/stores/project'
import { formatRelative } from '@/utils/time'
import { taskStatusLabel as statusLabel, taskStatusClass, priorityLabel, priorityClass } from '../constants/project'
+import { taskApi } from '@/api'
import type { TaskRecord, TaskQuery } from '@/api/types'
import Paginator from '../components/Paginator.vue'
@@ -164,6 +185,49 @@ const pageSize = ref(20)
// 分组折叠状态(localStorage 记忆)
const collapsedGroups = reactive(new Set())
+
+// 快捷菜单
+const quickMenuId = ref(null)
+function toggleQuickMenu(id: string) {
+ quickMenuId.value = quickMenuId.value === id ? null : id
+}
+const quickStatuses = computed(() => [
+ { key: 'todo', icon: '📝', label: t('tasks.statusFilter.todo') },
+ { key: 'in_progress', icon: '🔨', label: t('tasks.statusFilter.in_progress') },
+ { key: 'in_review', icon: '👀', label: t('tasks.statusFilter.in_review') },
+ { key: 'testing', icon: '🧪', label: t('tasks.statusFilter.testing') },
+ { key: 'done', icon: '✅', label: t('tasks.statusFilter.done') },
+ { key: 'blocked', icon: '🚫', label: t('tasks.statusFilter.blocked') },
+])
+const quickPriorities = computed(() => [
+ { value: 0, label: t('tasks.modal.priorityCritical'), cls: 'priority-critical' },
+ { value: 1, label: t('tasks.modal.priorityHigh'), cls: 'priority-high' },
+ { value: 2, label: t('tasks.modal.priorityMedium'), cls: 'priority-medium' },
+ { value: 3, label: t('tasks.modal.priorityLow'), cls: 'priority-low' },
+])
+async function quickAdvance(id: string, target: string) {
+ quickMenuId.value = null
+ try {
+ await taskApi.advance(id, target)
+ await store.loadTasks(buildTaskQuery())
+ } catch (e) { console.error('快捷改状态失败:', e) }
+}
+async function quickPriority(id: string, priority: number) {
+ quickMenuId.value = null
+ try {
+ await store.updateTask(id, 'priority', String(priority))
+ await store.loadTasks(buildTaskQuery())
+ } catch (e) { console.error('快捷改优先级失败:', e) }
+}
+async function quickDelete(task: TaskRecord) {
+ quickMenuId.value = null
+ if (!confirm(t('tasks.confirmDelete', { title: task.title }))) return
+ try {
+ await store.deleteTask(task.id)
+ } catch (e) { console.error('删除失败:', e) }
+}
+// 点击外部关闭快捷菜单
+function closeQuickMenu() { quickMenuId.value = null }
function toggleGroup(name: string) {
if (collapsedGroups.has(name)) {
collapsedGroups.delete(name)
@@ -355,11 +419,13 @@ onMounted(async () => {
loading.value = false
}
window.addEventListener('keydown', onKeydown)
+ document.addEventListener('click', closeQuickMenu)
})
import { onUnmounted } from 'vue'
onUnmounted(() => {
window.removeEventListener('keydown', onKeydown)
+ document.removeEventListener('click', closeQuickMenu)
if (_searchTimer) clearTimeout(_searchTimer)
})
@@ -555,6 +621,70 @@ onUnmounted(() => {
white-space: nowrap;
}
+/* 快捷操作 */
+.task-actions {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ position: relative;
+}
+.task-quick-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 14px;
+ padding: 2px 4px;
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+.task-item:hover .task-quick-btn { opacity: 0.6; }
+.task-quick-btn:hover { opacity: 1; }
+
+.quick-menu {
+ position: absolute;
+ top: calc(100% + 4px);
+ right: 0;
+ min-width: 160px;
+ background: var(--df-bg-card);
+ border: 0.5px solid var(--df-border);
+ border-radius: var(--df-radius-sm);
+ box-shadow: 0 6px 16px rgba(0,0,0,0.25);
+ z-index: 20;
+ padding: 6px 0;
+}
+.quick-menu-section { padding: 4px 0; }
+.quick-menu-label {
+ font-size: 10px;
+ color: var(--df-text-dim);
+ padding: 2px 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+.quick-menu-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ padding: 5px 12px;
+ background: none;
+ border: none;
+ color: var(--df-text);
+ font-size: 12px;
+ cursor: pointer;
+ text-align: left;
+}
+.quick-menu-item:hover { background: rgba(255,255,255,0.06); }
+.quick-menu-divider {
+ height: 0.5px;
+ background: var(--df-border);
+ margin: 4px 0;
+}
+.quick-menu-danger { color: var(--df-danger); }
+.priority-critical { color: var(--df-danger); }
+.priority-high { color: #ff9800; }
+.priority-medium { color: var(--df-info); }
+.priority-low { color: var(--df-text-dim); }
+
/* 空态 */
.empty-state {
display: flex;