修复: CR-26 AR-11 listener 按 B-29 筛选契约+Tasks 空态兜底

project.ts listener task 分支改读 _activeTaskProject 闭包 var(undefined 跳过保筛选/all 全量/具体 id 按项目),加 setActiveTaskProject 暴露由 Tasks 注册,保 B-29 store.tasks 反映当前筛选契约(原无参 loadTasks 冲成全量破坏筛选);Tasks.vue 加 v-if loading/error/empty 兜底+setActiveTaskProject onMounted/watch 更新。批8,vue-tsc 0
This commit is contained in:
2026-06-15 08:21:11 +08:00
parent ef2d033ad6
commit 7637f297c9
2 changed files with 80 additions and 33 deletions

View File

@@ -26,6 +26,19 @@ function createStore() {
// 后端 AI 工具(create/update/delete/restore/purge/bind_directory)执行成功后 emit
// "df-data-changed" { entity, action },前端按 entity 调对应 load 刷新列表,
// 取代工具执行后需手动刷新。
//
// CR-260615-26(R-1): task 分支不再无参 loadTasks(会把 store.tasks 冲成全量,破坏
// B-260615-29「store.tasks 反映当前筛选」契约)。改为读 _activeTaskProject:
// - undefined: Tasks 视图未挂载/从未设过筛选 → 不触发 task load(避免破坏筛选,
// 让 B-29 watch/handle 主导;此时改动由用户下次进 Tasks 自然刷新)
// - 'all' : 全量拉取
// - 具体 id : 按项目拉取
// 由 Tasks.vue 经 setActiveTaskProject 在 onMounted/watch 更新本变量。
let _activeTaskProject: string | undefined = undefined
function setActiveTaskProject(projectKey: string | undefined) {
_activeTaskProject = projectKey
}
let _dataChangedUnlisten: (() => void) | null = null
async function startDataChangedListener() {
@@ -36,7 +49,10 @@ function createStore() {
if (entity === 'project') {
void projectsStore.loadProjects()
} else if (entity === 'task') {
void tasksStore.loadTasks()
// 无筛选状态(undefined)时不触发,保 B-29 筛选契约;有筛选则按当前 activeProject 拉
if (_activeTaskProject !== undefined) {
void tasksStore.loadTasks(_activeTaskProject === 'all' ? undefined : _activeTaskProject)
}
} else if (entity === 'idea') {
void ideasStore.loadIdeas()
}
@@ -108,6 +124,8 @@ function createStore() {
// AR-11方案A数据变更联动刷新监听(后端工具执行后 emit df-data-changed)
startDataChangedListener,
stopDataChangedListener,
// CR-260615-26(R-1): Tasks 视图登记当前 activeProject 筛选,供 listener 按筛选拉取
setActiveTaskProject,
pendingApproval: computed(() => state.pendingApproval),
// computed
stats,

View File

@@ -38,36 +38,42 @@
<!-- 按项目分组 -->
<div class="task-groups">
<section
v-for="group in filteredGroups"
:key="group.projectName"
class="task-group"
>
<div class="group-header">
<span class="group-icon">{{ group.icon }}</span>
<h2 class="group-name">{{ group.projectName }}</h2>
<span class="group-count">{{ $t('tasks.group.taskCount', { n: group.tasks.length }) }}</span>
</div>
<div class="task-list">
<div class="task-item" v-for="task in group.tasks" :key="task.id" @click="router.push(`/tasks/${task.id}`)">
<div class="task-main">
<div class="task-title-row">
<span class="task-title">{{ task.title }}</span>
<span class="priority-badge" :class="priorityClass(task.priority)">{{ priorityLabel(task.priority) }}</span>
</div>
<div class="task-meta">
<span class="branch-tag" v-if="task.branch_name">
<span class="branch-icon"></span>
{{ task.branch_name }}
</span>
<span class="task-date">{{ formatRelativeZh(task.created_at) }}</span>
<span class="task-time">{{ formatRelativeZh(task.updated_at) }}</span>
</div>
</div>
<span class="status-tag" :class="taskStatusClass(task.status)">{{ $t(statusLabel(task.status)) }}</span>
<!-- Y-2: loading/error/empty 兜底(参考 Projects.vue 空态写法) -->
<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>
<template v-else>
<section
v-for="group in filteredGroups"
:key="group.projectName"
class="task-group"
>
<div class="group-header">
<span class="group-icon">{{ group.icon }}</span>
<h2 class="group-name">{{ group.projectName }}</h2>
<span class="group-count">{{ $t('tasks.group.taskCount', { n: group.tasks.length }) }}</span>
</div>
</div>
</section>
<div class="task-list">
<div class="task-item" v-for="task in group.tasks" :key="task.id" @click="router.push(`/tasks/${task.id}`)">
<div class="task-main">
<div class="task-title-row">
<span class="task-title">{{ task.title }}</span>
<span class="priority-badge" :class="priorityClass(task.priority)">{{ priorityLabel(task.priority) }}</span>
</div>
<div class="task-meta">
<span class="branch-tag" v-if="task.branch_name">
<span class="branch-icon"></span>
{{ task.branch_name }}
</span>
<span class="task-date">{{ formatRelativeZh(task.created_at) }}</span>
<span class="task-time">{{ formatRelativeZh(task.updated_at) }}</span>
</div>
</div>
<span class="status-tag" :class="taskStatusClass(task.status)">{{ $t(statusLabel(task.status)) }}</span>
</div>
</div>
</section>
</template>
</div>
<!-- 新建任务模态框 -->
@@ -115,6 +121,8 @@ const { t } = useI18n()
const activeProject = ref('all')
const activeStatus = ref('all')
// Y-2: 本视图加载态(loadTasks 不翻转全局 state.loading,故用局部 ref 精确反映任务加载)
const loading = ref(false)
// ── 新建任务模态框 ──
const showCreateModal = ref(false)
@@ -186,7 +194,12 @@ const filteredGroups = computed(() => {
// statusLabel / priorityLabel / priorityClass 由 ../constants/project 提供(与 ProjectDetail 文案统一)
async function refresh() {
await store.loadTasks()
loading.value = true
try {
await store.loadTasks(activeProject.value === 'all' ? undefined : activeProject.value)
} finally {
loading.value = false
}
}
function openCreateModal() {
@@ -212,12 +225,22 @@ async function confirmCreate() {
}
// B-260615-29: 切换项目筛选时按 projectId 重载(避免仅前端 filter,跨项目视图不同步)
// CR-260615-26(R-1): 同步登记当前筛选到 store,供 AR-11 listener 按筛选拉取
watch(activeProject, (key) => {
store.loadTasks(key === 'all' ? undefined : key)
store.setActiveTaskProject(key)
loading.value = true
store.loadTasks(key === 'all' ? undefined : key).finally(() => { loading.value = false })
})
onMounted(async () => {
await Promise.all([store.loadProjects(), store.loadTasks()])
// R-1: 登记初始筛选(all=全量),让数据变更 listener 知道视图已挂载、按全量拉取
store.setActiveTaskProject(activeProject.value)
loading.value = true
try {
await Promise.all([store.loadProjects(), store.loadTasks()])
} finally {
loading.value = false
}
})
</script>
@@ -384,6 +407,12 @@ onMounted(async () => {
.status-done { background: rgba(100,255,218,0.15); color: var(--df-success); }
.status-abandoned { background: rgba(255,107,107,0.2); color: var(--df-danger); }
/* ===== 空状态(Y-2: loading/error/empty 兜底,样式参考 Projects.vue) ===== */
.empty-state {
text-align: center; padding: 60px 20px;
font-size: 14px; color: var(--df-text-dim);
}
/* ===== 模态框 ===== */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal-box { background: var(--df-bg-card); border: 0.5px solid var(--df-border); border-radius: var(--df-radius-lg); padding: 24px; min-width: 360px; max-width: 90vw; }