新增: 任务行 hover 快捷操作菜单(改状态/优先级/删除)

- 任务行 hover 显示齿轮按钮,点击展开快捷菜单
- 快捷改状态(6 态子菜单)/改优先级(4 级)/删除(二次确认)
- 点击外部自动关闭菜单
- 补 i18n confirmDelete 翻译
This commit is contained in:
2026-06-30 22:08:43 +08:00
parent 948d6e644b
commit 27eb0a9f6b
3 changed files with 133 additions and 1 deletions

View File

@@ -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: {

View File

@@ -38,6 +38,7 @@ export default {
unknownProject: '未知项目',
empty: '暂无任务',
},
confirmDelete: '确定删除任务「{title}」吗?此操作不可撤销。',
// 新建任务模态
modal: {

View File

@@ -84,7 +84,27 @@
<span class="task-date">{{ formatRelative(task.updated_at) }}</span>
</div>
</div>
<div class="task-actions">
<span class="status-tag" :class="taskStatusClass(task.status)">{{ $t(statusLabel(task.status)) }}</span>
<button class="task-quick-btn" title="快捷操作" @click.stop="toggleQuickMenu(task.id)"></button>
<div v-if="quickMenuId === task.id" class="quick-menu" @click.stop>
<div class="quick-menu-section">
<div class="quick-menu-label">状态</div>
<button v-for="s in quickStatuses" :key="s.key" class="quick-menu-item" @click="quickAdvance(task.id, s.key)">
<span>{{ s.icon }}</span>{{ s.label }}
</button>
</div>
<div class="quick-menu-divider"></div>
<div class="quick-menu-section">
<div class="quick-menu-label">优先级</div>
<button v-for="p in quickPriorities" :key="p.value" class="quick-menu-item" @click="quickPriority(task.id, p.value)">
<span :class="p.cls"></span>{{ p.label }}
</button>
</div>
<div class="quick-menu-divider"></div>
<button class="quick-menu-item quick-menu-danger" @click="quickDelete(task)">🗑 删除</button>
</div>
</div>
</div>
</div>
</section>
@@ -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<string>())
// 快捷菜单
const quickMenuId = ref<string | null>(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)
})
</script>
@@ -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;