- TopBar 10 处 icon-only 按钮补 aria-label(复用同 :title 表达式,含三态 title) - ConversationSidebar: 4 icon-only aria-label + 2 分组标题键盘化(role/tabindex/aria-expanded/keydown) - MessageList 2 icon-only; KnowledgeDetail 3 章节折叠键盘化; dashboard 两列表项键盘化 - FileExplorer 面包屑/模块下拉项键盘化; ConflictResolver 冲突文件折叠键盘化 - 约定: 遮罩/点外关闭类(@click.self)非键盘缺口,跳过;零新增 i18n 键 - 验收: bunx vue-tsc --noEmit EXIT 0;看板 P3-2 更新(两批共 23 处)
161 lines
5.3 KiB
Vue
161 lines
5.3 KiB
Vue
<template>
|
|
<section class="df-panel" style="animation-delay: 280ms">
|
|
<div class="df-panel-head">
|
|
<h2 class="df-panel-title">{{ $t('dashboard.ideaPool') }}</h2>
|
|
<router-link to="/ideas" class="df-link">{{ $t('dashboard.viewAll') }}</router-link>
|
|
</div>
|
|
<div class="ideas-stats">
|
|
<div class="stat-item">
|
|
<span class="stat-num">{{ stats.pending }}</span>
|
|
<span class="stat-label">{{ $t('ideas.statsPending') }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-num">{{ stats.promoted }}</span>
|
|
<span class="stat-label">{{ $t('ideas.statsPromoted') }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-num">{{ stats.avgScore }}</span>
|
|
<span class="stat-label">{{ $t('ideas.statsAvgScore') }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="idea-rows">
|
|
<div v-for="idea in displayIdeas" :key="idea.id" class="idea-row clickable" role="button" tabindex="0" @click="goDetail(idea.id)" @keydown.enter.prevent="goDetail(idea.id)" @keydown.space.prevent="goDetail(idea.id)" :title="t('dashboard.goIdeaDetail')">
|
|
<div class="idea-score-ring" :class="'ring-' + idea.tier">
|
|
<span class="idea-score-num">{{ idea.score }}</span>
|
|
</div>
|
|
<div class="idea-body">
|
|
<span class="idea-name">{{ idea.title }}</span>
|
|
<span class="idea-status">{{ ideaStatusLabel(idea.statusLabel) }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-if="displayIdeas.length === 0" class="empty-hint">{{ $t('dashboard.empty.noIdeas') }}</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
// 灵感池面板 — 从 Dashboard.vue 抽出(灵感列表 + 评分环 + tier 着色 + 空态)。
|
|
// 共享 project store(全局单例),displayIdeas computed 按分数倒序取前 4 条(精选),无需父传 props。
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useRouter } from 'vue-router'
|
|
import { useProjectStore } from '@/stores/project'
|
|
import { scoreTier } from '@/utils/ideaEval'
|
|
|
|
const store = useProjectStore()
|
|
const { t } = useI18n()
|
|
const router = useRouter()
|
|
|
|
// 点击灵感行跳转详情(Ideas.vue 同组件路由,会自动选中)
|
|
function goDetail(ideaId: string) {
|
|
router.push(`/ideas/${ideaId}`)
|
|
}
|
|
|
|
// 精选灵感列表:按分数倒序取前 4 条(高分优先,符合"精选"语义;原来 slice(0,4)
|
|
// 无排序会显示最老的 4 条而非高分灵感)。
|
|
const displayIdeas = computed(() =>
|
|
[...store.ideas]
|
|
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
.slice(0, 4)
|
|
.map(i => ({
|
|
id: i.id,
|
|
title: i.title,
|
|
score: i.score ?? 0,
|
|
tier: scoreTier(i.score),
|
|
statusLabel: i.status,
|
|
}))
|
|
)
|
|
|
|
// 灵感池细分统计(顶部 StatCardRow 已显示总数,此处仅展示待审/已晋升/平均分 3 项细分,
|
|
// 删除重复的 total 避免与顶部统计卡信息冗余)。
|
|
const stats = computed(() => {
|
|
const ideas = store.ideas
|
|
const scored = ideas.filter(i => i.score != null)
|
|
const avg = scored.length > 0
|
|
? Math.round(scored.reduce((sum, i) => sum + (i.score ?? 0), 0) / scored.length)
|
|
: null
|
|
return {
|
|
pending: ideas.filter(i => i.status === 'draft' || i.status === 'pending_review').length,
|
|
promoted: ideas.filter(i => i.status === 'promoted').length,
|
|
avgScore: avg == null ? '—' : avg,
|
|
}
|
|
})
|
|
|
|
function ideaStatusLabel(status: string): string {
|
|
return t('dashboard.ideaStatus.' + status)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* df-panel/df-link 通用面板类已提取到 styles/components.css 全局 */
|
|
.ideas-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.stat-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 8px 4px;
|
|
background: var(--df-bg);
|
|
border: 0.5px solid var(--df-border);
|
|
border-radius: var(--df-radius);
|
|
}
|
|
.stat-num {
|
|
font-family: var(--df-font-mono);
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--df-text);
|
|
line-height: 1.1;
|
|
}
|
|
.stat-label {
|
|
font-size: 10px;
|
|
color: var(--df-text-dim);
|
|
margin-top: 3px;
|
|
text-align: center;
|
|
}
|
|
|
|
.idea-rows { display: flex; flex-direction: column; }
|
|
.idea-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 7px 0;
|
|
border-bottom: 0.5px solid var(--df-border);
|
|
}
|
|
.idea-row.clickable {
|
|
cursor: pointer;
|
|
transition: background 0.15s var(--df-ease);
|
|
}
|
|
.idea-row.clickable:hover {
|
|
background: var(--df-bg);
|
|
}
|
|
.idea-row:last-child { border-bottom: none; }
|
|
|
|
.idea-score-ring {
|
|
width: 32px; height: 32px;
|
|
border-radius: var(--df-radius);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.ring-high { background: rgba(61,219,160,0.10); border: 0.5px solid rgba(61,219,160,0.15); }
|
|
.ring-mid { background: rgba(240,199,94,0.08); border: 0.5px solid rgba(240,199,94,0.12); }
|
|
.ring-low { background: rgba(240,101,101,0.08); border: 0.5px solid rgba(240,101,101,0.12); }
|
|
|
|
.idea-score-num {
|
|
font-family: var(--df-font-mono);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
.ring-high .idea-score-num { color: var(--df-success); }
|
|
.ring-mid .idea-score-num { color: var(--df-warning); }
|
|
.ring-low .idea-score-num { color: var(--df-danger); }
|
|
|
|
.idea-name { font-size: 12px; font-weight: 500; color: var(--df-text); display: block; }
|
|
.idea-status { font-size: 10px; color: var(--df-text-dim); margin-top: 1px; display: block; }
|
|
</style>
|