修复: 前端UX一致性(Dashboard状态映射/状态徽章全局/Knowledge下一条与失败反馈/Ideas筛选与错误条/ConfirmDialog安全/快捷菜单状态机/分页越界) + 后端校验(queue/count-list一致/软删拒改/父聚合/promote CAS/MCP状态机收口/update白名单剔除id/created_at) + 销账
This commit is contained in:
+35
-8
@@ -8,6 +8,12 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 错误条:消费 store.error(对齐 Knowledge/Projects error-banner,不整表替换列表) -->
|
||||
<div v-if="store.error" class="error-banner" style="margin-bottom: var(--df-gap-page)">
|
||||
<span class="error-text">{{ store.error }}</span>
|
||||
<button class="error-dismiss" @click="store.clearError()">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<div class="search-box">
|
||||
@@ -46,9 +52,8 @@
|
||||
<!-- 左侧:灵感列表 -->
|
||||
<section class="idea-list-panel">
|
||||
<div class="idea-list">
|
||||
<!-- 三态:加载/错误/空(对齐 Tasks.vue:43-45) -->
|
||||
<!-- 三态:加载/空(错误改走顶部 error-banner,WK-7 不整表替换列表) -->
|
||||
<div v-if="store.loading" class="empty-state">{{ $t('common.loading') }}</div>
|
||||
<div v-else-if="store.error" class="empty-state">{{ store.error }}</div>
|
||||
<div v-else-if="filteredIdeas.length === 0" class="empty-state">{{ $t('ideas.listEmpty') }}</div>
|
||||
<template v-else>
|
||||
<div
|
||||
@@ -244,6 +249,13 @@ function buildIdeaQuery(): IdeaQuery {
|
||||
return q
|
||||
}
|
||||
|
||||
// WK-4:按当前视图查询加载(登记 setActiveIdeaQuery 供 df-data-changed 联动刷新复用同一筛选,
|
||||
// 避免 promote/relate/监听触发无参 loadIdeas 全量冲掉当前筛选视图)
|
||||
function loadCurrentView() {
|
||||
store.setActiveIdeaQuery(buildIdeaQuery())
|
||||
return store.loadIdeas(buildIdeaQuery())
|
||||
}
|
||||
|
||||
// 按当前筛选/搜索/排序触发后端加载(防抖仅对 keyword 输入,筛选/排序即时)。
|
||||
// 让 store.loading 驱动「加载中」空态显示(对齐 Tasks.vue 三态)。
|
||||
// G5.3:keyword 防抖 timer 用 useTimerOwnership 每实例管理 + onUnmounted 自动清理,
|
||||
@@ -255,7 +267,7 @@ async function reloadIdeas(immediate = false) {
|
||||
clearOwn(keywordDebounce)
|
||||
keywordDebounce = null
|
||||
}
|
||||
const run = () => store.loadIdeas(buildIdeaQuery())
|
||||
const run = () => loadCurrentView()
|
||||
if (immediate) {
|
||||
await run()
|
||||
} else {
|
||||
@@ -319,6 +331,13 @@ watch([filteredIdeas, () => selectedId.value], () => {
|
||||
if (targetPage !== page.value) page.value = targetPage
|
||||
})
|
||||
|
||||
// WK-12:分页越界自愈(删除/筛选变化后 page 超界 → 钳制到最后一页,防空白页)
|
||||
watch([filteredIdeas, pageSize], () => {
|
||||
if (pageSize.value <= 0) return
|
||||
const maxPage = Math.max(1, Math.ceil(filteredIdeas.value.length / pageSize.value))
|
||||
if (page.value > maxPage) page.value = maxPage
|
||||
})
|
||||
|
||||
// B-260615-25:灵感描述 Markdown 渲染已下沉至 IdeaDetail 子组件(共享模块单例渲染器)
|
||||
|
||||
// 评分档位 class(高/中/低 三色标),阈值统一走 scoreTier(单一来源,消除 4 处重复)
|
||||
@@ -361,8 +380,9 @@ async function deleteCurrentIdea() {
|
||||
if (!await confirmDialog(t('ideas.confirmDelete', { title: currentIdea.value.title }))) return
|
||||
deleting.value = true
|
||||
try {
|
||||
await store.deleteIdea(currentIdea.value.id)
|
||||
selectedId.value = null
|
||||
// WK-7:删除失败不清理选中态(错误走 error-banner),仅成功时清空详情
|
||||
const ok = await store.deleteIdea(currentIdea.value.id)
|
||||
if (ok) selectedId.value = null
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
@@ -373,6 +393,8 @@ async function promoteToProject() {
|
||||
promoting.value = true
|
||||
try {
|
||||
const res = await store.promoteIdea(currentIdea.value.id)
|
||||
// WK-4:promote 后按当前视图重载(store 不再内部全量 loadIdeas 冲掉筛选)
|
||||
await loadCurrentView()
|
||||
router.push(`/projects/${res.project_id}`)
|
||||
} catch (e: any) {
|
||||
const msg = e?.toString() ?? t('ideas.promoteFailed')
|
||||
@@ -384,7 +406,7 @@ async function promoteToProject() {
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await store.loadIdeas(buildIdeaQuery())
|
||||
await loadCurrentView()
|
||||
}
|
||||
|
||||
const evaluating = ref(false)
|
||||
@@ -407,10 +429,13 @@ async function evaluateCurrentIdea() {
|
||||
|
||||
async function onStatusChange(newStatus: IdeaStatus) {
|
||||
if (!currentIdea.value) return
|
||||
// WK-5:promoted 走独立立项流程(promoteToProject),状态菜单/下拉已排除,此处兜底禁止直接置
|
||||
if (newStatus === 'promoted') return
|
||||
// ⑥ 状态切换确认:落到 promoted/rejected/archived 等终态(或离开 approved 失去立项入口)
|
||||
// 需用户确认,避免误点。中性流转(draft↔pending_review↔approved)直接落库。
|
||||
// (WK-5:上方已拦截 promoted,此处不再重复比对,TS 收窄后 'promoted' 无交集)
|
||||
const from = currentIdea.value.status
|
||||
const needsConfirm = newStatus === 'promoted' || newStatus === 'rejected' || newStatus === 'archived'
|
||||
const needsConfirm = newStatus === 'rejected' || newStatus === 'archived'
|
||||
|| (from === 'approved' && newStatus !== 'approved')
|
||||
if (needsConfirm) {
|
||||
const msg = t('ideas.confirmStatus', {
|
||||
@@ -433,6 +458,8 @@ async function onUpdateDesc(desc: string) {
|
||||
async function onUpdateRelated(ids: string[]) {
|
||||
if (!currentIdea.value) return
|
||||
await store.relateIdeas(currentIdea.value.id, ids)
|
||||
// WK-4:relate 后端原子更新受影响灵感,按当前视图重载(store 不再内部全量 loadIdeas 冲筛选)
|
||||
await loadCurrentView()
|
||||
}
|
||||
|
||||
// 接收 IdeaDetail 子组件 emit 的 'update-tags'(标签编辑保存)
|
||||
@@ -442,7 +469,7 @@ async function onUpdateTags(tags: string) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadIdeas(buildIdeaQuery())
|
||||
await loadCurrentView()
|
||||
// 从路由参数恢复选中灵感(修复 /ideas/:id 直接访问空白页 B-260615-36)
|
||||
if (route.params.id) {
|
||||
const id = route.params.id as string
|
||||
|
||||
+26
-11
@@ -286,13 +286,20 @@ async function onDetailSave(payload: { id: string; data: { title: string; conten
|
||||
|
||||
// ===== 审核操作(收件箱) =====
|
||||
// P0-②:发布/拒绝/归档后自动选中下一条(而非清空详情),减少审核者来回点击
|
||||
// WK-2:index 须在 store 操作前快照(操作后 candidates 已 filter 移除 currentId,findIndex 恒 -1)
|
||||
// WK-3:操作失败(updateStatus/archive 返 false)toast 报错 + 中止推进(不 selectNext,不清详情)
|
||||
async function publishCurrent() {
|
||||
if (!detail.value) return
|
||||
const currentId = detail.value.knowledge.id
|
||||
const idx = store.candidates.findIndex(c => c.id === currentId)
|
||||
submitting.value = true
|
||||
try {
|
||||
await store.updateStatus(currentId, 'published')
|
||||
await selectNextCandidate(currentId)
|
||||
const ok = await store.updateStatus(currentId, 'published')
|
||||
if (!ok) {
|
||||
showToast(t('knowledge.err.updateStatusFailed'), 'error')
|
||||
return
|
||||
}
|
||||
await selectNextCandidate(idx)
|
||||
showToast(t('knowledge.publishedToast'), 'success')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
@@ -302,10 +309,15 @@ async function publishCurrent() {
|
||||
async function rejectCurrent() {
|
||||
if (!detail.value) return
|
||||
const currentId = detail.value.knowledge.id
|
||||
const idx = store.candidates.findIndex(c => c.id === currentId)
|
||||
submitting.value = true
|
||||
try {
|
||||
await store.archive(currentId)
|
||||
await selectNextCandidate(currentId)
|
||||
const ok = await store.archive(currentId)
|
||||
if (!ok) {
|
||||
showToast(t('knowledge.err.archiveFailed'), 'error')
|
||||
return
|
||||
}
|
||||
await selectNextCandidate(idx)
|
||||
showToast(t('knowledge.rejectedToast'), 'info')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
@@ -315,12 +327,17 @@ async function rejectCurrent() {
|
||||
async function archiveCurrent() {
|
||||
if (!detail.value) return
|
||||
const currentId = detail.value.knowledge.id
|
||||
const idx = store.candidates.findIndex(c => c.id === currentId)
|
||||
submitting.value = true
|
||||
try {
|
||||
await store.archive(currentId)
|
||||
const ok = await store.archive(currentId)
|
||||
if (!ok) {
|
||||
showToast(t('knowledge.err.archiveFailed'), 'error')
|
||||
return
|
||||
}
|
||||
// 知识库 tab 无「下一条」语义(library 不限 candidate),归档后清空详情
|
||||
if (topTab.value === 'inbox') {
|
||||
await selectNextCandidate(currentId)
|
||||
await selectNextCandidate(idx)
|
||||
} else {
|
||||
selectedId.value = null
|
||||
detail.value = null
|
||||
@@ -338,11 +355,9 @@ function pickNextCandidate(remaining: KnowledgeRecord[], currentIndex: number):
|
||||
return remaining[currentIndex] ?? remaining[currentIndex - 1] ?? null
|
||||
}
|
||||
|
||||
async function selectNextCandidate(currentId: string) {
|
||||
const before = store.candidates
|
||||
const idx = before.findIndex(c => c.id === currentId)
|
||||
const remaining = store.candidates // store 操作后已 filter 移除 currentId
|
||||
const next = idx >= 0 ? pickNextCandidate(remaining, idx) : null
|
||||
async function selectNextCandidate(currentIndex: number) {
|
||||
const remaining = store.candidates // 操作后已 filter 移除 currentId
|
||||
const next = currentIndex >= 0 ? pickNextCandidate(remaining, currentIndex) : null
|
||||
if (next) {
|
||||
await selectKnowledge(next.id)
|
||||
} else {
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
@@ -219,6 +219,13 @@ const pagedProjects = computed(() => {
|
||||
return store.projects.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
// WK-12:分页越界自愈(删除/回收后 store.projects 数量变化,page 超界 → 钳制到最后一页,防空白页)
|
||||
watch([() => store.projects, pageSize], () => {
|
||||
if (pageSize.value <= 0) return
|
||||
const maxPage = Math.max(1, Math.ceil(store.projects.length / pageSize.value))
|
||||
if (page.value > maxPage) page.value = maxPage
|
||||
})
|
||||
|
||||
// 状态映射统一走 ../constants/project(与 ProjectDetail/Tasks/Dashboard 一致)
|
||||
// formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date)
|
||||
|
||||
|
||||
+62
-21
@@ -14,6 +14,12 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 错误条:消费 store.error(对齐 Knowledge/Projects error-banner,不整表替换列表) -->
|
||||
<div v-if="store.error" class="error-banner" style="margin-bottom: var(--df-gap-page)">
|
||||
<span class="error-text">{{ store.error }}</span>
|
||||
<button class="error-dismiss" @click="store.clearError()">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏(紧凑下拉) -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-group">
|
||||
@@ -46,7 +52,6 @@
|
||||
<!-- 任务分组列表 -->
|
||||
<div class="task-groups">
|
||||
<div v-if="loading" class="empty-state">{{ $t('common.loading') }}</div>
|
||||
<div v-else-if="store.error" class="empty-state">{{ store.error }}</div>
|
||||
<div v-else-if="filteredGroups.length === 0" class="empty-state">
|
||||
<div class="empty-icon">📋</div>
|
||||
<div>{{ $t('tasks.group.empty') }}</div>
|
||||
@@ -85,16 +90,16 @@
|
||||
<span class="task-parent-icon">📑</span>
|
||||
<span class="task-title">{{ row.task.title }}</span>
|
||||
<span class="priority-badge" :class="priorityClass(row.task.priority)">{{ priorityLabel(row.task.priority) }}</span>
|
||||
<!-- 子进度徽章(done+cancelled / total) -->
|
||||
<span class="sub-progress-badge" :title="$t('tasks.tree.progress')">{{ row.progress!.done }}/{{ row.progress!.total }}</span>
|
||||
<!-- 子进度徽章(done+cancelled / total);WK-11:搜索态隐藏(树形进度失真) -->
|
||||
<span v-if="!isSearching" class="sub-progress-badge" :title="$t('tasks.tree.progress')">{{ row.progress!.done }}/{{ row.progress!.total }}</span>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="branch-tag" v-if="row.task.branch_name">
|
||||
<span class="branch-icon">⑂</span>{{ row.task.branch_name }}
|
||||
</span>
|
||||
<span class="task-date">{{ formatRelative(row.task.updated_at) }}</span>
|
||||
<!-- 迷你进度条(渐变填充,宽度=完成子任务百分比) -->
|
||||
<div class="mini-progress" :title="$t('tasks.tree.progress')">
|
||||
<!-- 迷你进度条(渐变填充,宽度=完成子任务百分比);WK-11:搜索态隐藏 -->
|
||||
<div v-if="!isSearching" class="mini-progress" :title="$t('tasks.tree.progress')">
|
||||
<div class="mini-progress-fill" :style="{ width: parentPct(row) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,7 +110,7 @@
|
||||
<div v-if="quickMenuId === row.task.id" class="quick-menu" @click.stop>
|
||||
<div class="quick-menu-section">
|
||||
<div class="quick-menu-label">{{ $t('tasks.quickStatus') }}</div>
|
||||
<button v-for="s in quickStatuses" :key="s.key" class="quick-menu-item" @click="quickAdvance(row.task.id, s.key)">
|
||||
<button v-for="s in quickStatusesFor(row.task)" :key="s.key" class="quick-menu-item" @click="quickAdvance(row.task.id, s.key)">
|
||||
<span>{{ s.icon }}</span>{{ s.label }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -155,7 +160,7 @@
|
||||
<div v-if="quickMenuId === child.id" class="quick-menu" @click.stop>
|
||||
<div class="quick-menu-section">
|
||||
<div class="quick-menu-label">{{ $t('tasks.quickStatus') }}</div>
|
||||
<button v-for="s in quickStatuses" :key="s.key" class="quick-menu-item" @click="quickAdvance(child.id, s.key)">
|
||||
<button v-for="s in quickStatusesFor(child)" :key="s.key" class="quick-menu-item" @click="quickAdvance(child.id, s.key)">
|
||||
<span>{{ s.icon }}</span>{{ s.label }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -196,7 +201,7 @@
|
||||
<div v-if="quickMenuId === row.task.id" class="quick-menu" @click.stop>
|
||||
<div class="quick-menu-section">
|
||||
<div class="quick-menu-label">{{ $t('tasks.quickStatus') }}</div>
|
||||
<button v-for="s in quickStatuses" :key="s.key" class="quick-menu-item" @click="quickAdvance(row.task.id, s.key)">
|
||||
<button v-for="s in quickStatusesFor(row.task)" :key="s.key" class="quick-menu-item" @click="quickAdvance(row.task.id, s.key)">
|
||||
<span>{{ s.icon }}</span>{{ s.label }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -274,6 +279,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷操作失败 toast(WK-13,对齐 Projects.vue 操作反馈) -->
|
||||
<div v-if="toast.visible" class="toast" :class="'toast-' + toast.type">{{ toast.msg }}</div>
|
||||
|
||||
<!-- 确认弹层(删除任务,替代原生 window.confirm) -->
|
||||
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
|
||||
</div>
|
||||
@@ -285,17 +293,20 @@ import { useRouter } from 'vue-router'
|
||||
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 { taskStatusLabel as statusLabel, taskStatusClass, priorityLabel, priorityClass, TASK_STATUS_TRANSITIONS } from '../constants/project'
|
||||
import { taskApi } from '@/api'
|
||||
import type { TaskRecord, TaskQuery, ProjectId } from '@/api/types'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { usePersistedRef } from '@/composables/usePersistedRef'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useProjectStore()
|
||||
const { t } = useI18n()
|
||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
// WK-13:快捷操作失败 toast 反馈(对齐 Projects.vue useToast 模式)
|
||||
const { toast, showToast } = useToast()
|
||||
|
||||
// 列表筛选/分页状态持久化到 localStorage(key 前缀 'tasks.'),
|
||||
// 刷新页面后保留用户上次选择。collapsedGroups 单独走 df-tasks-collapsed(沿用既有实现)。
|
||||
@@ -304,6 +315,8 @@ const activeStatus = usePersistedRef('tasks.activeStatus', 'all')
|
||||
const sortBy = usePersistedRef('tasks.sortBy', 'updated_at')
|
||||
const searchKeyword = usePersistedRef('tasks.searchKeyword', '')
|
||||
const loading = ref(false)
|
||||
// WK-11:搜索态(keyword 过滤可能截断父子关系,子进度徽章/迷你条语义失真 → 隐藏)
|
||||
const isSearching = computed(() => searchKeyword.value.trim().length > 0)
|
||||
|
||||
// F-260805 D5:列表页一次性加载 + 前端组装树,移除真分页(page/pageSize/totalTasks 已删)。
|
||||
// 分组折叠状态(localStorage 记忆)
|
||||
@@ -339,7 +352,13 @@ const quickStatuses = computed(() => [
|
||||
{ key: 'testing', icon: '🧪', label: t('tasks.statusFilter.testing') },
|
||||
{ key: 'done', icon: '✅', label: t('tasks.statusFilter.done') },
|
||||
{ key: 'blocked', icon: '🚫', label: t('tasks.statusFilter.blocked') },
|
||||
{ key: 'cancelled', icon: '🗑️', label: t('tasks.statusFilter.cancelled') },
|
||||
])
|
||||
/** WK-8:快捷状态目标 = 状态机合法流转目标(TASK_STATUS_TRANSITIONS 过滤,对齐 TaskDetail ADVANCE_MAP) */
|
||||
function quickStatusesFor(task: TaskRecord): { key: string; icon: string; label: string }[] {
|
||||
const valid = new Set(TASK_STATUS_TRANSITIONS[task.status] ?? [])
|
||||
return quickStatuses.value.filter(s => valid.has(s.key))
|
||||
}
|
||||
const quickPriorities = computed(() => [
|
||||
{ value: 0, label: t('tasks.modal.priorityCritical'), cls: 'priority-critical' },
|
||||
{ value: 1, label: t('tasks.modal.priorityHigh'), cls: 'priority-high' },
|
||||
@@ -351,14 +370,20 @@ async function quickAdvance(id: string, target: string) {
|
||||
try {
|
||||
await taskApi.advance(id, target)
|
||||
await store.loadTasks(buildTaskQuery())
|
||||
} catch (e) { console.error('快捷改状态失败:', e) }
|
||||
} catch (e: any) {
|
||||
// WK-13:快捷操作失败 toast 反馈(原 console.error 静默,用户无感知)
|
||||
showToast(e?.toString() ?? t('common.unknownError'), 'error')
|
||||
}
|
||||
}
|
||||
async function quickPriority(id: string, priority: number) {
|
||||
quickMenuId.value = null
|
||||
try {
|
||||
await store.updateTask(id, 'priority', String(priority))
|
||||
// WK-13:改用 taskApi 直调(不走 store.updateTask,避免失败置 store.error 触发整表错误态)
|
||||
await taskApi.update(id, 'priority', String(priority))
|
||||
await store.loadTasks(buildTaskQuery())
|
||||
} catch (e) { console.error('快捷改优先级失败:', e) }
|
||||
} catch (e: any) {
|
||||
showToast(e?.toString() ?? t('common.unknownError'), 'error')
|
||||
}
|
||||
}
|
||||
async function quickDelete(task: TaskRecord, childCount = 0) {
|
||||
quickMenuId.value = null
|
||||
@@ -368,7 +393,9 @@ async function quickDelete(task: TaskRecord, childCount = 0) {
|
||||
: t('tasks.confirmDelete', { title: task.title })
|
||||
if (!await confirmDialog(msg)) return
|
||||
try {
|
||||
await store.deleteTask(task.id)
|
||||
// WK-13:deleteTask 返成功布尔,失败 toast(store.error 同时走 error-banner)
|
||||
const ok = await store.deleteTask(task.id)
|
||||
if (!ok) showToast(t('tasks.err.deleteFailed'), 'error')
|
||||
} catch (e) { console.error('删除失败:', e) }
|
||||
}
|
||||
// 点击外部关闭快捷菜单
|
||||
@@ -392,6 +419,8 @@ try {
|
||||
// 搜索防抖
|
||||
let _searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
watch(searchKeyword, () => {
|
||||
// WK-10:登记当前关键字到 barrel(数据变更联动刷新复用同一筛选,不冲掉搜索结果)
|
||||
store.setActiveTaskKeyword(searchKeyword.value.trim() || undefined)
|
||||
if (_searchTimer) clearTimeout(_searchTimer)
|
||||
_searchTimer = setTimeout(() => {
|
||||
loading.value = true
|
||||
@@ -604,6 +633,7 @@ function onKeydown(e: KeyboardEvent) {
|
||||
onMounted(async () => {
|
||||
store.setActiveTaskProject(activeProject.value)
|
||||
store.setActiveTaskStatus(activeStatus.value)
|
||||
store.setActiveTaskKeyword(searchKeyword.value.trim() || undefined)
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
@@ -692,7 +722,8 @@ onUnmounted(() => {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
overflow: hidden;
|
||||
/* WK-9:原 overflow:hidden 会把快捷菜单(.quick-menu 绝对定位向下溢出)裁切。
|
||||
改为组自身不裁切,圆角裁剪下放给 header(顶角)与末行(底角,见下),菜单可正常浮出。 */
|
||||
}
|
||||
|
||||
.group-header {
|
||||
@@ -704,6 +735,9 @@ onUnmounted(() => {
|
||||
user-select: none;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
transition: background 0.1s;
|
||||
/* WK-9:承接原 .task-group overflow:hidden 的顶角圆角裁剪(自身 overflow,组不再裁切快捷菜单) */
|
||||
border-radius: var(--df-radius-lg) var(--df-radius-lg) 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.group-header:hover { background: var(--df-accent-bg); }
|
||||
.task-group.collapsed .group-header { border-bottom: none; }
|
||||
@@ -733,6 +767,8 @@ onUnmounted(() => {
|
||||
cursor: pointer;
|
||||
}
|
||||
.task-item:last-child { border-bottom: none; }
|
||||
/* WK-9:末行承接组底角圆角裁剪(替代原 .task-group overflow:hidden) */
|
||||
.task-item:last-child { border-radius: 0 0 var(--df-radius-lg) var(--df-radius-lg); }
|
||||
.task-item:hover { background: var(--df-accent-bg); }
|
||||
|
||||
.task-main { flex: 1; min-width: 0; }
|
||||
@@ -781,13 +817,7 @@ onUnmounted(() => {
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* status-tag 尺寸/圆角 + status-* 7 态色已提取到全局 components.css(WK-6 对齐三视图),此处不再重复定义 */
|
||||
|
||||
/* 快捷操作 */
|
||||
.task-actions {
|
||||
@@ -909,6 +939,17 @@ onUnmounted(() => {
|
||||
}
|
||||
.quick-menu-danger { color: var(--df-danger); }
|
||||
|
||||
/* ===== 快捷操作失败 toast(WK-13,对齐 Projects.vue 操作反馈) ===== */
|
||||
.toast {
|
||||
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
||||
padding: 10px 18px; border-radius: var(--df-radius-sm);
|
||||
font-size: 13px; z-index: 200; max-width: 80vw;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
.toast-info { background: var(--df-accent); color: #fff; }
|
||||
.toast-error { background: var(--df-danger); color: #fff; }
|
||||
.toast-success { background: var(--df-success); color: #fff; }
|
||||
|
||||
/* 空态 */
|
||||
/* empty-state 已提取到全局 global.css */
|
||||
.empty-icon { font-size: 32px; opacity: 0.4; }
|
||||
|
||||
Reference in New Issue
Block a user