优化: 图像模型兜底防御+任务UI(状态多选+跳详情)+token popover遮挡
This commit is contained in:
@@ -1080,6 +1080,7 @@ defineExpose({
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
flex-direction: row-reverse; /* 头像在右:DOM 仍 avatar 在先,视觉反转成【气泡】【头像】靠右 */
|
||||
margin-top: -3px; /* user 与上方消息间距略收(全局 gap 14 → 视觉 11),更紧凑 */
|
||||
}
|
||||
.ai-msg-ai {
|
||||
position: relative;
|
||||
@@ -1087,6 +1088,9 @@ defineExpose({
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
/* token 详情 popover 展开时,提升本消息 z-index 盖住后续消息气泡
|
||||
(后续 .ai-msg-user 也是 relative 同级 context,DOM 在后会遮挡 popover) */
|
||||
.ai-msg-ai:has(.ai-token-popover) { z-index: 30; }
|
||||
.ai-msg-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -295,8 +295,24 @@
|
||||
<h2>{{ $t('projectDetail.taskListTitle') }}</h2>
|
||||
<span class="task-count">{{ $t('projectDetail.taskCount', { n: projectTasks.length }) }}</span>
|
||||
</div>
|
||||
<div class="task-filter-chips">
|
||||
<button
|
||||
v-for="s in taskStatusFilters"
|
||||
:key="s.key"
|
||||
class="filter-chip"
|
||||
:class="{ 'is-active': activeTaskStatuses.length === 0 || activeTaskStatuses.includes(s.key) }"
|
||||
@click="toggleTaskStatus(s.key)"
|
||||
>
|
||||
{{ s.icon }} {{ s.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="task-list">
|
||||
<div class="task-card" v-for="task in projectTasks" :key="task.id">
|
||||
<div
|
||||
class="task-card"
|
||||
v-for="task in filteredProjectTasks"
|
||||
:key="task.id"
|
||||
@click="router.push('/tasks/' + task.id)"
|
||||
>
|
||||
<div class="task-top">
|
||||
<span class="task-title">{{ task.title }}</span>
|
||||
<span class="task-status" :class="taskStatusClass(task.status)">{{ $t(taskStatusLabel(task.status)) }}</span>
|
||||
@@ -572,6 +588,30 @@ async function loadProjectTasks() {
|
||||
}
|
||||
// taskStatusLabel / taskStatusClass 由 ../constants/project 提供
|
||||
|
||||
// 任务状态多选过滤
|
||||
const taskStatusFilters = 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') },
|
||||
{ key: 'cancelled', icon: '🗑️', label: t('tasks.statusFilter.cancelled') },
|
||||
])
|
||||
const activeTaskStatuses = ref<string[]>([])
|
||||
function toggleTaskStatus(key: string) {
|
||||
const idx = activeTaskStatuses.value.indexOf(key)
|
||||
if (idx >= 0) {
|
||||
activeTaskStatuses.value.splice(idx, 1)
|
||||
} else {
|
||||
activeTaskStatuses.value.push(key)
|
||||
}
|
||||
}
|
||||
const filteredProjectTasks = computed(() => {
|
||||
if (activeTaskStatuses.value.length === 0) return projectTasks.value
|
||||
return projectTasks.value.filter(t => activeTaskStatuses.value.includes(t.status))
|
||||
})
|
||||
|
||||
// ── 工程列表(概览内 CRUD)──
|
||||
// 多工程(monorepo/多模块)在概览内增删改 + 扫描;复用 moduleApi + ConfirmDialog(删除确认)
|
||||
// + arco Message(反馈)。切到文件 Tab 时 FileExplorer 因 v-if 重挂载自动重拉,无需手动同步。
|
||||
@@ -1330,6 +1370,36 @@ onUnmounted(() => {
|
||||
/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 B-260619),
|
||||
此处仅保留本组件特有 .task-count。 */
|
||||
.task-count { font-size: 12px; color: var(--df-text-dim); }
|
||||
.task-filter-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.task-filter-chips .filter-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px 8px;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--df-text-dim);
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.task-filter-chips .filter-chip:hover {
|
||||
background: var(--df-bg-card);
|
||||
color: var(--df-text);
|
||||
border-color: var(--df-accent);
|
||||
}
|
||||
.task-filter-chips .filter-chip.is-active {
|
||||
background: var(--df-accent-soft);
|
||||
color: var(--df-accent);
|
||||
border-color: var(--df-accent);
|
||||
}
|
||||
|
||||
/* A2[PD-P1-3]:工作流日志「全局事件」标注(次要小字标签) */
|
||||
.global-event-tag {
|
||||
@@ -1346,7 +1416,13 @@ onUnmounted(() => {
|
||||
.task-card {
|
||||
padding: 14px 0;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
margin: 0 -12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--df-radius);
|
||||
}
|
||||
.task-card:hover { background: var(--df-bg-card-hover); }
|
||||
.task-card:last-child { border-bottom: none; }
|
||||
.task-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
||||
.task-title { font-size: 14px; font-weight: 500; color: var(--df-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
+58
-10
@@ -31,11 +31,17 @@
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">{{ $t('tasks.filter.status') }}</span>
|
||||
<select v-model="activeStatus" class="filter-select">
|
||||
<option v-for="s in statusFilters" :key="s.key" :value="s.key">
|
||||
<div class="filter-chip-group">
|
||||
<button
|
||||
v-for="s in statusFilters"
|
||||
:key="s.key"
|
||||
class="filter-chip"
|
||||
:class="{ 'is-active': activeStatuses.length === 0 || activeStatuses.includes(s.key) }"
|
||||
@click="toggleStatus(s.key)"
|
||||
>
|
||||
{{ s.icon }} {{ s.label }}
|
||||
</option>
|
||||
</select>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">{{ $t('tasks.sort.label') }}</span>
|
||||
@@ -311,7 +317,17 @@ const { toast, showToast } = useToast()
|
||||
// 列表筛选/分页状态持久化到 localStorage(key 前缀 'tasks.'),
|
||||
// 刷新页面后保留用户上次选择。collapsedGroups 单独走 df-tasks-collapsed(沿用既有实现)。
|
||||
const activeProject = usePersistedRef('tasks.activeProject', 'all')
|
||||
const activeStatus = usePersistedRef('tasks.activeStatus', 'all')
|
||||
const activeStatuses = usePersistedRef<string[]>('tasks.activeStatuses', [])
|
||||
|
||||
// 状态多选切换
|
||||
function toggleStatus(key: string) {
|
||||
const idx = activeStatuses.value.indexOf(key)
|
||||
if (idx >= 0) {
|
||||
activeStatuses.value.splice(idx, 1)
|
||||
} else {
|
||||
activeStatuses.value.push(key)
|
||||
}
|
||||
}
|
||||
const sortBy = usePersistedRef('tasks.sortBy', 'updated_at')
|
||||
const searchKeyword = usePersistedRef('tasks.searchKeyword', '')
|
||||
const loading = ref(false)
|
||||
@@ -526,8 +542,12 @@ function parentPct(row: TaskRow): number {
|
||||
|
||||
const filteredGroups = computed(() => {
|
||||
// F-260805 D5:store.tasks 是一次性拉取的筛选全量,前端已组装树(taskRows),直接按项目分组
|
||||
// 状态多选:未选(全)或选中状态匹配
|
||||
const statusFilter = (row: TaskRow) =>
|
||||
activeStatuses.value.length === 0 || activeStatuses.value.includes(row.task.status)
|
||||
const groupMap = new Map<string, TaskRow[]>()
|
||||
for (const row of taskRows.value) {
|
||||
if (!statusFilter(row)) continue
|
||||
if (!groupMap.has(row.task.project_id)) {
|
||||
groupMap.set(row.task.project_id, [])
|
||||
}
|
||||
@@ -548,11 +568,10 @@ const filteredGroups = computed(() => {
|
||||
|
||||
function buildTaskQuery(): TaskQuery | undefined {
|
||||
const projAll = activeProject.value === 'all'
|
||||
const statusAll = activeStatus.value === 'all'
|
||||
const kw = searchKeyword.value.trim()
|
||||
const query: TaskQuery = { order_by: sortBy.value }
|
||||
if (!projAll) query.project_id = activeProject.value
|
||||
if (!statusAll) query.status = activeStatus.value
|
||||
// 状态多选走前端过滤(后端 status 单值,不给后端传使全量拉取,前端 filteredGroups 过滤)
|
||||
if (kw) query.keyword = kw
|
||||
// F-260805 D5:一次拉当前筛选全量(limit 放大 500 钳制上限,offset 恒 0),
|
||||
// 树形需要完整父子关系,分页会割裂父/子;全量已在前端,父进度也前端计算
|
||||
@@ -608,8 +627,8 @@ watch(activeProject, () => {
|
||||
store.loadTasks(buildTaskQuery()).finally(() => { loading.value = false })
|
||||
})
|
||||
|
||||
watch(activeStatus, () => {
|
||||
store.setActiveTaskStatus(activeStatus.value)
|
||||
watch(activeStatuses, () => {
|
||||
store.setActiveTaskStatus(activeStatuses.value.length === 0 ? 'all' : activeStatuses.value.join(','))
|
||||
loading.value = true
|
||||
store.loadTasks(buildTaskQuery()).finally(() => { loading.value = false })
|
||||
})
|
||||
@@ -632,7 +651,7 @@ function onKeydown(e: KeyboardEvent) {
|
||||
|
||||
onMounted(async () => {
|
||||
store.setActiveTaskProject(activeProject.value)
|
||||
store.setActiveTaskStatus(activeStatus.value)
|
||||
store.setActiveTaskStatus(activeStatuses.value.length === 0 ? 'all' : activeStatuses.value.join(','))
|
||||
store.setActiveTaskKeyword(searchKeyword.value.trim() || undefined)
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -705,6 +724,35 @@ onUnmounted(() => {
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.filter-select:hover { border-color: var(--df-accent); }
|
||||
.filter-chip-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.filter-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px 8px;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--df-text-dim);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.filter-chip:hover {
|
||||
background: var(--df-bg-card);
|
||||
color: var(--df-text);
|
||||
border-color: var(--df-accent);
|
||||
}
|
||||
.filter-chip.is-active {
|
||||
background: var(--df-accent-soft);
|
||||
color: var(--df-accent);
|
||||
border-color: var(--df-accent);
|
||||
}
|
||||
.filter-count {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user