重构: 跨模块样式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:
@@ -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; }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user