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;