重构: 跨模块样式DRY收口+审批列表增强+灵感模块优化

- 全局样式收口: page-header/btn/modal/filter-bar/back-link/empty-state
  等通用类从12个视图的scoped重复定义提取到global.css/components.css
- 审批列表增强: 筛选栏(状态/风险/工具搜索)+行展开查看完整参数结果
  +审批耗时显示+刷新spinner+硬编码颜色全部改用CSS变量
- 灵感模块: scoreTier统一评分阈值+score-bar/assessment-badge提取全局
  +statusLabelKey消除重复+i18n状态文案对齐+eval-report--muted统一空态
- Dashboard: df-panel/df-link提取全局(修scoped不渗透)+统计卡可点击跳转
  +活跃项目面板只显示active状态+灵感列表按分数排序+刷新spinner
- 修复: intent.rs tool_type类型对齐(newtype)+ToolCardList补ai-btn定义
  +SkillMention/ImageInput样式不渗透修复(提取全局)
This commit is contained in:
lxy
2026-07-02 23:24:11 +08:00
parent eee0f06e1a
commit 249b3b9ea8
29 changed files with 691 additions and 925 deletions
@@ -5,7 +5,7 @@
<router-link to="/projects" class="df-link">{{ $t('dashboard.viewAll') }}</router-link>
</div>
<div class="project-list">
<div v-for="(p, i) in displayProjects" :key="p.id" class="project-row" :style="{ animationDelay: `${300 + i * 50}ms` }">
<div v-for="(p, i) in displayProjects" :key="p.id" class="project-row clickable" :style="{ animationDelay: `${300 + i * 50}ms` }" @click="goDetail(p.id)" :title="t('dashboard.goProjectDetail')">
<div class="project-row-top">
<div class="project-identity">
<span class="project-dot" :class="'dot-' + p.stage"></span>
@@ -34,57 +34,67 @@
<script setup lang="ts">
// 活跃项目面板 — 从 Dashboard.vue 抽出(项目列表 + 进度条 + 阶段 chip + 空态)。
// 共享 project store(全局单例),displayProjects computed 直读 store.projects/tasks,无需父传 props
// getProjectStage 死键语义对齐 constants/project.ts 的 PROJECT_STAGE_INFO。
// 共享 project store(全局单例),displayProjects computed 过滤 active 状态按更新时间倒序取前 6 条
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useProjectStore } from '@/stores/project'
import { projectStageInfo } from '@/constants/project'
import { formatRelative } from '@/utils/time'
const store = useProjectStore()
const { t } = useI18n()
const router = useRouter()
function getProjectStage(status: string): { stage: string; stageLabelKey: string; progress: number } {
// F-09 对齐:project status 死键(in_progress/paused/cancelled)已从后端映射移除,
// 此处 switch 同步收敛,与 constants/project.ts 的 PROJECT_STAGE_INFO 语义一致。
switch (status) {
case 'planning': return { stage: 'planning', stageLabelKey: 'planning', progress: 20 }
case 'active': return { stage: 'coding', stageLabelKey: 'coding', progress: 55 }
case 'completed': return { stage: 'release', stageLabelKey: 'done', progress: 100 }
default: return { stage: 'planning', stageLabelKey: 'planning', progress: 20 }
}
// 点击项目行跳转详情(治本:列表项可进详情,与其他列表页交互一致)
function goDetail(projectId: string) {
router.push(`/projects/${projectId}`)
}
function getProjectTaskCount(projectId: string): number {
return store.tasks.filter(t => t.project_id === projectId && t.status === 'in_progress').length
}
// 相对时间复用 utils/time.formatRelative(与 Tasks/AuditLog/MessageList 等同源,根治 NaN)
// 原 formatLastActivity 是其逐行复制,提取为单一来源。
const formatLastActivity = formatRelative
// 阶段 label key:projectStageInfo 返回的 stage 值(coding/testing/release/planning)
// 直接拼接 i18n key dashboard.stage.<stage>,与 constants/project.ts PROJECT_STAGE_INFO 单一来源。
// 面板名为"活跃项目",只显示 active 状态(DB 实际默认值,ProjectStatus union 历史遗留未含
// 'active',此处断言绕过;DB 实际无 planning/in_progress 等值产生,详见功能决策记录)。
// 按更新时间倒序取前 6 条(completed 归档项目不混入)。
const displayProjects = computed(() =>
store.projects.map(p => {
const stageInfo = getProjectStage(p.status)
return {
id: p.id,
name: p.name,
...stageInfo,
activeTasks: getProjectTaskCount(p.id),
lastActivity: formatLastActivity(p.updated_at),
}
})
store.projects
.filter(p => (p.status as string) === 'active')
.sort((a, b) => b.updated_at.localeCompare(a.updated_at))
.slice(0, 6)
.map(p => {
const info = projectStageInfo(p.status)
return {
id: p.id,
name: p.name,
stage: info.stage,
stageLabelKey: info.stage,
progress: info.progress,
activeTasks: getProjectTaskCount(p.id),
lastActivity: formatRelative(p.updated_at),
}
})
)
</script>
<style scoped>
/* 仅迁入项目行自身样式;df-panel/df-link 通用面板类留 Dashboard.vue(多面板共享) */
/* df-panel/df-link 通用面板类已提取到 styles/components.css 全局 */
.project-list { display: flex; flex-direction: column; }
.project-row {
padding: 10px 0;
border-bottom: 0.5px solid var(--df-border);
animation: fadeInUp 0.4s var(--df-ease) both;
}
.project-row.clickable {
cursor: pointer;
transition: background 0.15s var(--df-ease);
}
.project-row.clickable:hover {
background: var(--df-bg);
}
.project-row:last-child { border-bottom: none; }
.project-row-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
+33 -18
View File
@@ -5,10 +5,6 @@
<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.total }}</span>
<span class="stat-label">{{ $t('ideas.statsTotal') }}</span>
</div>
<div class="stat-item">
<span class="stat-num">{{ stats.pending }}</span>
<span class="stat-label">{{ $t('ideas.statsPending') }}</span>
@@ -23,7 +19,7 @@
</div>
</div>
<div class="idea-rows">
<div v-for="idea in displayIdeas" :key="idea.id" class="idea-row">
<div v-for="idea in displayIdeas" :key="idea.id" class="idea-row clickable" @click="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>
@@ -39,26 +35,39 @@
<script setup lang="ts">
// 灵感池面板 — 从 Dashboard.vue 抽出(灵感列表 + 评分环 + tier 着色 + 空态)。
// 共享 project store(全局单例),displayIdeas computed 直读 store.ideas(取前 4 条),无需父传 props。
// 共享 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.slice(0, 4).map(i => ({
id: i.id,
title: i.title,
score: i.score ?? 0,
tier: (i.score ?? 0) >= 80 ? 'high' : (i.score ?? 0) >= 60 ? 'mid' : 'low',
statusLabel: i.status,
}))
[...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,
}))
)
// 灵感池统计概览 — 4 项关键指标(总数/待审/已晋升/平均分)。
// score 可为 null(未评估),avgScore 仅对已评估灵感求均值,无则显示 '—'
// 灵感池细分统计(顶部 StatCardRow 已显示总数,此处仅展示待审/已晋升/平均分 3 项细分,
// 删除重复的 total 避免与顶部统计卡信息冗余)
const stats = computed(() => {
const ideas = store.ideas
const scored = ideas.filter(i => i.score != null)
@@ -66,7 +75,6 @@ const stats = computed(() => {
? Math.round(scored.reduce((sum, i) => sum + (i.score ?? 0), 0) / scored.length)
: null
return {
total: ideas.length,
pending: ideas.filter(i => i.status === 'draft' || i.status === 'pending_review').length,
promoted: ideas.filter(i => i.status === 'promoted').length,
avgScore: avg == null ? '—' : avg,
@@ -79,10 +87,10 @@ function ideaStatusLabel(status: string): string {
</script>
<style scoped>
/* 仅迁入灵感行自身样式;df-panel/df-link 通用面板类留 Dashboard.vue(多面板共享) */
/* df-panel/df-link 通用面板类已提取到 styles/components.css 全局 */
.ideas-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 12px;
}
@@ -117,6 +125,13 @@ function ideaStatusLabel(status: string): string {
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 {
+26 -24
View File
@@ -3,7 +3,10 @@
<div
v-for="(stat, i) in stats" :key="stat.key"
class="stat-card"
:class="{ clickable: stat.link }"
:style="{ animationDelay: `${i * 60}ms` }"
@click="stat.link ? go(stat.link) : null"
:title="stat.link ? stat.title : undefined"
>
<div class="stat-card-bg" :class="'stat-bg--' + stat.key"></div>
<div class="stat-card-inner">
@@ -11,11 +14,6 @@
<div class="stat-icon-wrap" :style="{ background: stat.iconBg }">
<span class="stat-icon">{{ stat.icon }}</span>
</div>
<span class="stat-trend" :class="stat.trend > 0 ? 'up' : stat.trend < 0 ? 'down' : 'flat'">
<template v-if="stat.trend > 0">↑</template><template v-else-if="stat.trend < 0"></template>
<template v-else></template>
{{ stat.trend > 0 ? '+' : '' }}{{ stat.trend }}
</span>
</div>
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
@@ -27,22 +25,29 @@
<script setup lang="ts">
// 统计卡片行 — 从 Dashboard.vue 抽出(4 张统计卡: ideas/projects/tasks/drafts)。
// 共享 project store(全局单例),stats computed 直读 store.stats,无需父传 props。
// 卡片可点击跳转对应列表页(ideas/projects/tasks),提升导航效率。
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useProjectStore } from '@/stores/project'
const store = useProjectStore()
const { t } = useI18n()
const router = useRouter()
function go(link: string) {
router.push(link)
}
const stats = computed(() => [
{ key: 'ideas', icon: '\u{1F4A1}', label: t('dashboard.stats.ideas'), value: store.stats.ideas, trend: 0,
iconBg: 'rgba(123,111,240,0.12)' },
{ key: 'projects', icon: '\u{1F4C2}', label: t('dashboard.stats.projects'), value: store.stats.projects, trend: 0,
iconBg: 'rgba(94,175,240,0.10)' },
{ key: 'tasks', icon: '⚡', label: t('dashboard.stats.activeTasks'), value: store.stats.activeTasks, trend: 0,
iconBg: 'rgba(61,219,160,0.10)' },
{ key: 'drafts', icon: '\u{1F4DD}', label: t('dashboard.stats.drafts'), value: store.stats.drafts, trend: 0,
iconBg: 'rgba(240,199,94,0.10)' },
{ key: 'ideas', icon: '\u{1F4A1}', label: t('dashboard.stats.ideas'), value: store.stats.ideas,
iconBg: 'rgba(123,111,240,0.12)', link: '/ideas', title: t('dashboard.viewAll') },
{ key: 'projects', icon: '\u{1F4C2}', label: t('dashboard.stats.projects'), value: store.stats.projects,
iconBg: 'rgba(94,175,240,0.10)', link: '/projects', title: t('dashboard.viewAll') },
{ key: 'tasks', icon: '⚡', label: t('dashboard.stats.activeTasks'), value: store.stats.activeTasks,
iconBg: 'rgba(61,219,160,0.10)', link: '/tasks', title: t('dashboard.viewAll') },
{ key: 'drafts', icon: '\u{1F4DD}', label: t('dashboard.stats.drafts'), value: store.stats.drafts,
iconBg: 'rgba(240,199,94,0.10)', link: null, title: '' },
])
</script>
@@ -61,6 +66,14 @@ const stats = computed(() => [
border: 0.5px solid var(--df-border);
animation: fadeInUp 0.5s var(--df-ease) both;
}
.stat-card.clickable {
cursor: pointer;
transition: border-color 0.15s var(--df-ease), transform 0.15s var(--df-ease);
}
.stat-card.clickable:hover {
border-color: var(--df-accent);
transform: translateY(-1px);
}
.stat-card-bg {
position: absolute;
inset: 0;
@@ -82,17 +95,6 @@ const stats = computed(() => [
}
.stat-icon { font-size: 14px; }
.stat-trend {
font-family: var(--df-font-mono);
font-size: 10px;
font-weight: 500;
padding: 1px 6px;
border-radius: var(--df-radius-sm);
}
.stat-trend.up { color: var(--df-success); background: rgba(61,219,160,0.10); }
.stat-trend.down { color: var(--df-danger); background: rgba(240,101,101,0.10); }
.stat-trend.flat { color: var(--df-text-dim); background: rgba(255,255,255,0.04); }
.stat-value {
font-size: 24px;
font-weight: 500;