From 27eb0a9f6bd4e535e7b110de194a615d219da853 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com>
Date: Tue, 30 Jun 2026 22:08:43 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E4=BB=BB=E5=8A=A1?=
=?UTF-8?q?=E8=A1=8C=20hover=20=E5=BF=AB=E6=8D=B7=E6=93=8D=E4=BD=9C?=
=?UTF-8?q?=E8=8F=9C=E5=8D=95(=E6=94=B9=E7=8A=B6=E6=80=81/=E4=BC=98?=
=?UTF-8?q?=E5=85=88=E7=BA=A7/=E5=88=A0=E9=99=A4)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 任务行 hover 显示齿轮按钮,点击展开快捷菜单
- 快捷改状态(6 态子菜单)/改优先级(4 级)/删除(二次确认)
- 点击外部自动关闭菜单
- 补 i18n confirmDelete 翻译
---
src/i18n/en/tasks.ts | 1 +
src/i18n/zh-CN/tasks.ts | 1 +
src/views/Tasks.vue | 132 +++++++++++++++++++++++++++++++++++++++-
3 files changed, 133 insertions(+), 1 deletion(-)
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;