重构: 跨模块样式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
+24 -2
View File
@@ -387,9 +387,31 @@ function groupIcon(name: string): string {
border-radius: var(--df-radius-sm);
background: rgba(255,255,255,0.02);
}
.ai-batch-approve .ai-btn--sm {
font-size: 11px;
/* .ai-btn 基类:批量审批按钮(历史重构遗漏定义,补全治本) */
.ai-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 6px 12px;
border: none;
border-radius: var(--df-radius-sm);
background: var(--df-accent);
color: #fff;
font-family: var(--df-font-sans);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: filter 0.15s, background 0.15s;
white-space: nowrap;
}
.ai-btn:hover { filter: brightness(1.1); }
.ai-btn--sm { padding: 4px 10px; font-size: 11px; }
.ai-btn--danger {
background: var(--df-danger);
color: #fff;
}
.ai-btn--danger:hover { filter: brightness(1.1); }
/* -- 全部展开/收起(U-260618:从 topbar 下沉到分组标题行,移除独立按钮 padding/背景,
高度=文字行高,不撑高分组标题行;融入标题视觉,仅 hover 变色) -- */
+2 -112
View File
@@ -813,123 +813,13 @@ defineExpose({
transition: border-color 0.15s;
}
/* F-260614-05 Phase 2b: 待发送图片预览行(粘贴/拖拽加入,发送前可移除) */
.ai-img-preview-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
width: 100%; /* 全宽强制后续 textarea+按钮换行 */
padding-bottom: 4px;
border-bottom: 0.5px solid var(--df-border);
}
.ai-img-preview {
position: relative;
width: 56px;
height: 56px;
border-radius: var(--df-radius);
overflow: hidden;
border: 0.5px solid var(--df-border);
background: var(--df-bg);
}
.ai-img-preview-thumb {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.ai-img-preview-x {
position: absolute;
top: 2px;
right: 2px;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: color-mix(in srgb, var(--df-bg) 80%, transparent);
color: var(--df-text-dim);
font-size: 14px;
line-height: 1;
cursor: pointer;
padding: 0;
transition: background 0.15s, color 0.15s;
}
.ai-img-preview-x:hover {
background: var(--df-danger);
color: #fff;
}
/* F-260614-05 Phase 2b: 图片预览样式已提取到 styles/components.css 全局(ImageInput.vue 共享) */
.ai-input-wrap:focus-within {
border-color: color-mix(in srgb, var(--df-accent) 40%, transparent);
}
/* ── 技能 chip / 联想浮层 ── */
.ai-skill-chip {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
padding: 3px 8px;
background: color-mix(in srgb, var(--df-accent) 16%, transparent);
border: 0.5px solid color-mix(in srgb, var(--df-accent) 40%, transparent);
border-radius: var(--df-radius);
font-size: 12px;
}
.ai-skill-chip-name { color: var(--df-accent); font-weight: 600; flex-shrink: 0; }
.ai-skill-chip-desc { color: var(--df-text-dim); flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 参数格式提示(技能自带 argument_hint 数据,不进 i18n) */
.ai-skill-chip-main { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; flex-wrap: wrap; }
.ai-skill-chip-hint {
color: var(--df-text-dim);
font-family: var(--df-font-mono, ui-monospace, monospace);
font-size: 11px;
background: color-mix(in srgb, var(--df-text-dim) 14%, transparent);
padding: 1px 5px;
border-radius: 3px;
}
.ai-skill-chip-x { background: none; border: none; color: var(--df-text-dim); cursor: pointer; font-size: 14px; line-height: 1; padding: 0 2px; flex-shrink: 0; }
.ai-skill-chip-x:hover { color: var(--df-text); }
.ai-skill-popover {
position: absolute;
bottom: calc(100% + 4px);
left: 0;
right: 0;
max-height: 240px;
overflow-y: auto;
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
z-index: 20;
}
.ai-skill-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 10px;
cursor: pointer;
font-size: 12px;
}
.ai-skill-item-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ai-skill-item--active,
.ai-skill-item:hover {
background: color-mix(in srgb, var(--df-accent) 14%, transparent);
}
.ai-skill-item-name { color: var(--df-accent); font-weight: 600; min-width: 90px; }
.ai-skill-item-desc { color: var(--df-text-dim); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-skill-item-src { color: var(--df-text-dim); opacity: 0.6; font-size: 10px; text-transform: uppercase; }
/* 浮层内参数格式提示(等宽 dim 小字,仅在 skill.argument_hint 有值时显) */
.ai-skill-item-hint {
color: var(--df-text-dim);
font-family: var(--df-font-mono, ui-monospace, monospace);
font-size: 11px;
opacity: 0.85;
word-break: break-all;
}
.ai-skill-empty { padding: 8px 12px; color: var(--df-text-dim); opacity: 0.7; font-size: 12px; }
/* ── 技能 chip / 联想浮层样式已提取到 styles/components.css 全局(SkillMention.vue + MentionPopover.vue 共享)── */
/* ── UX-10 §1.4: @ 实体引用浮层样式已随 MentionPopover.vue 提取 ── */
+3 -29
View File
@@ -38,7 +38,7 @@ const emit = defineEmits<{
<template>
<!-- UX-10 §1.4: @ 实体引用浮层(@ 触发,选中插入 [类型:] 标记,架构复用技能 popover) -->
<div class="ai-skill-popover ai-mention-popover">
<div class="ai-skill-popover">
<div v-if="!mentionItems.length" class="ai-skill-empty">
{{ mentionTotal ? $t('aiChat.mentionNoMatch') : $t('aiChat.mentionEmpty') }}
</div>
@@ -65,34 +65,8 @@ const emit = defineEmits<{
</template>
<style scoped>
/* ── UX-10 §1.4: @ 实体引用浮层(复用 .ai-skill-popover 容器,仅新增组标题/类型标签) ── */
.ai-mention-popover {
position: absolute;
bottom: calc(100% + 4px);
left: 0;
right: 0;
max-height: 240px;
overflow-y: auto;
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
z-index: 20;
}
.ai-skill-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 10px;
cursor: pointer;
font-size: 12px;
}
.ai-skill-item-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ai-skill-item--active,
.ai-skill-item:hover {
background: color-mix(in srgb, var(--df-accent) 14%, transparent);
}
.ai-skill-empty { padding: 8px 12px; color: var(--df-text-dim); opacity: 0.7; font-size: 12px; }
/* ── UX-10 §1.4: @ 实体引用浮层特定样式(复用全局 .ai-skill-popover/.ai-skill-item,仅新增 mention 专有) ──
.ai-skill-popover/.ai-skill-item/.ai-skill-empty 已提取到 styles/components.css 全局 */
.ai-mention-group {
padding: 4px 10px 2px;
font-size: 10px;
@@ -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;
+16 -89
View File
@@ -74,7 +74,7 @@
</ul>
</div>
</div>
<div v-else class="eval-report" style="opacity:0.5">
<div v-else class="eval-report eval-report--muted">
<button class="btn-evaluate" :disabled="evaluating" @click="$emit('evaluate')">
{{ evaluating ? $t('ideas.evaluating') : $t('ideas.startEval') }}
</button>
@@ -90,8 +90,8 @@
<!-- 评估历史(版本时间线) -->
<div class="detail-section">
<h3>{{ $t('ideas.historyTitle') }}</h3>
<div v-if="historyLoading" class="eval-report" style="opacity:0.5">{{ $t('ideas.historyLoading') }}</div>
<div v-else-if="evalHistory.length === 0" class="eval-report" style="opacity:0.5">{{ $t('ideas.historyEmpty') }}</div>
<div v-if="historyLoading" class="eval-report eval-report--muted">{{ $t('ideas.historyLoading') }}</div>
<div v-else-if="evalHistory.length === 0" class="eval-report eval-report--muted">{{ $t('ideas.historyEmpty') }}</div>
<div v-else class="eval-history-list">
<div
v-for="rec in evalHistory"
@@ -135,7 +135,7 @@
<span v-else class="related-chip related-chip-missing">#{{ row.id }}</span>
</template>
</div>
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.relatedEmpty') }}</div>
<div v-else class="eval-report eval-report--muted">{{ $t('ideas.relatedEmpty') }}</div>
<button class="btn btn-ghost btn-sm" @click="startRelateEdit">{{ $t('ideas.relatedManage') }}</button>
</template>
<!-- 编辑态 -->
@@ -154,7 +154,7 @@
<span>{{ idea.title }}</span>
</label>
</div>
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.relatedEmpty') }}</div>
<div v-else class="eval-report eval-report--muted">{{ $t('ideas.relatedEmpty') }}</div>
<div class="related-edit-actions">
<button class="btn btn-primary btn-sm" @click="confirmRelate">{{ $t('ideas.relatedConfirm') }}</button>
<button class="btn btn-ghost btn-sm" @click="cancelRelate">{{ $t('ideas.relatedCancel') }}</button>
@@ -169,12 +169,12 @@
<div class="score-bar-row" v-for="dim in parseScores(idea.scores)" :key="dim.name">
<span class="score-bar-label">{{ dim.name }}</span>
<div class="score-bar-track">
<div class="score-bar-fill" :style="{ width: dim.score + '%' }" :class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'"></div>
<div class="score-bar-fill" :style="{ width: dim.score + '%' }" :class="'fill-' + scoreTier(dim.score)"></div>
</div>
<span class="score-bar-value">{{ dim.score }}</span>
</div>
</div>
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noEval') }}</div>
<div v-else class="eval-report eval-report--muted">{{ $t('ideas.noEval') }}</div>
</div>
<!-- 标签 -->
@@ -183,7 +183,7 @@
<div class="tag-list" v-if="parseTags(idea.tags).length > 0">
<span class="tag" v-for="tag in parseTags(idea.tags)" :key="tag">{{ tag }}</span>
</div>
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noTags') }}</div>
<div v-else class="eval-report eval-report--muted">{{ $t('ideas.noTags') }}</div>
</div>
<!-- 状态管理 -->
@@ -230,7 +230,7 @@ import { useProjectStore } from '../../stores/project'
import { useRendered } from '../../composables/useMarkdown'
import { ideaApi } from '../../api'
import { formatDate } from '../../utils/time'
import { parseScores, assessmentClass, assessmentLabel } from '../../utils/ideaEval'
import { parseScores, assessmentClass, assessmentLabel, scoreTier } from '../../utils/ideaEval'
import type { IdeaRecord, IdeaStatus, IdeaEvaluationRecord } from '../../api/types'
const props = defineProps<{
@@ -491,17 +491,10 @@ watch(() => props.idea.id, loadHistory)
border-left: 3px solid var(--df-accent);
border-radius: 0 8px 8px 0;
}
/* 弱化态(空数据/加载中占位,原 inline opacity:0.5 统一为 class) */
.eval-report--muted { opacity: 0.5; }
/* ===== 状态标签 ===== */
.status-tag {
font-size: 11px; font-weight: 500; padding: 2px 8px;
border-radius: var(--df-radius-xs);
}
.status-draft { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
.status-pending_review { background: rgba(100,181,246,0.2); color: var(--df-info); }
.status-approved { background: rgba(100,255,218,0.15); color: var(--df-success); }
.status-promoted { background: rgba(108,99,255,0.2); color: var(--df-accent); }
.status-rejected { background: rgba(255,107,107,0.2); color: var(--df-danger); }
/* ===== 状态标签已提取到 styles/components.css 全局(.status-tag 系列) ===== */
/* ===== 对抗式评估 ===== */
.adversarial-eval {
@@ -595,21 +588,7 @@ watch(() => props.idea.id, loadHistory)
margin-bottom: 1rem;
}
.assessment-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 500;
margin: 0.5rem 0;
}
.assessment-badge.immediate { background: var(--df-success-bg); color: var(--df-success); }
.assessment-badge.soon { background: var(--df-info-bg); color: var(--df-info); }
.assessment-badge.conditional { background: var(--df-warning-bg); color: var(--df-warning); }
.assessment-badge.revised { background: rgba(255, 217, 61, 0.2); color: var(--df-warning); }
.assessment-badge.defer { background: var(--df-danger-bg); color: var(--df-danger); }
.assessment-badge.cancel { background: var(--df-danger-bg); color: var(--df-danger); }
/* assessment-badge 系列已提取到 styles/components.css 全局 */
.final-score {
font-size: 14px;
@@ -774,45 +753,7 @@ watch(() => props.idea.id, loadHistory)
margin: 6px 0 0;
}
/* ===== 多维评分条div 模拟) ===== */
.score-bar-chart {
display: flex;
flex-direction: column;
gap: 10px;
}
.score-bar-row {
display: flex;
align-items: center;
gap: 12px;
}
.score-bar-label {
font-size: 12px;
color: var(--df-text-secondary);
min-width: 72px;
text-align: right;
}
.score-bar-track {
flex: 1;
height: 8px;
background: var(--df-border);
border-radius: var(--df-radius-xs);
overflow: hidden;
}
.score-bar-fill {
height: 100%;
border-radius: var(--df-radius-xs);
transition: width 0.4s;
}
.fill-high { background: var(--df-success); }
.fill-mid { background: var(--df-warning); }
.fill-low { background: var(--df-danger); }
.score-bar-value {
font-size: 12px;
font-weight: 500;
min-width: 28px;
text-align: right;
color: var(--df-text);
}
/* ===== 多维评分条已提取到 styles/components.css 全局(.score-bar-* 系列) ===== */
/* ===== 标签 ===== */
.tag-list { display: flex; gap: 8px; flex-wrap: wrap; }
@@ -854,22 +795,8 @@ watch(() => props.idea.id, loadHistory)
.related-edit-item input { cursor: pointer; }
.related-edit-actions { display: flex; gap: 8px; }
/* ===== 按钮 ===== */
.btn {
padding: 8px 16px;
border: none;
border-radius: var(--df-radius-sm);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.btn-primary { background: var(--df-accent); color: #fff; }
.btn-primary:hover { background: var(--df-accent-hover); }
.btn-ghost { background: transparent; color: var(--df-text-secondary); border: 0.5px solid var(--df-border); }
.btn-ghost:hover { background: var(--df-bg-card); color: var(--df-text); }
/* 小尺寸按钮(描述编辑/重试等) */
.btn-sm { padding: 4px 10px; font-size: 12px; }
/* ===== 按钮已提取到 global.css 全局(.btn/.btn-primary/.btn-ghost/.btn-sm) ===== */
/* IdeaDetail 特有按钮定位 */
.desc-edit-btn { margin-bottom: var(--df-gap-page); margin-top: 4px; }
.desc-edit-actions { display: flex; gap: 8px; margin-bottom: var(--df-gap-page); margin-top: 8px; }
+1 -11
View File
@@ -350,17 +350,7 @@ watch(() => props.detail.knowledge.id, () => {
.edit-input-title { font-size: 16px; font-weight: 500; }
.edit-textarea { min-height: 100px; resize: vertical; font-family: inherit; }
/* ===== 按钮 ===== */
.btn {
padding: 8px 16px; border: none; border-radius: var(--df-radius-sm);
font-size: 13px; cursor: pointer; transition: all 0.15s;
}
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-primary { background: var(--df-accent); color: #fff; }
.btn-primary:hover { background: var(--df-accent-hover); }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-ghost { background: transparent; color: var(--df-text-secondary); border: 0.5px solid var(--df-border); }
.btn-ghost:hover { background: var(--df-bg-card); color: var(--df-text); }
/* 按钮基础样式 (.btn / .btn-sm / .btn-primary / .btn-ghost) 已收敛至 global.css */
/* ===== 响应式:窄屏详情标题防竖线化(B-260619)===== */
@media (max-width: 760px) {
+3 -27
View File
@@ -337,30 +337,9 @@ onBeforeUnmount(() => {
opacity: 0.4;
}
.btn {
padding: 4px 12px;
border: none;
border-radius: var(--df-radius-sm, 4px);
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.btn-ghost {
background: transparent;
color: var(--df-text-secondary);
border: 0.5px solid var(--df-border);
}
.btn-ghost:hover {
background: var(--df-bg);
color: var(--df-text);
}
.btn-sm {
padding: 4px 10px;
font-size: 11px;
}
/* 按钮基础样式 (.btn / .btn-ghost / .btn-sm / .btn-primary) 已收敛至 global.css */
/* 依赖图工具栏紧凑,覆 padding/font-size 避免按钮变大 */
.btn { padding: 4px 12px; font-size: 12px; }
/* 添加依赖弹窗 */
.dep-graph__modal-overlay {
@@ -395,7 +374,4 @@ onBeforeUnmount(() => {
.dep-graph__modal-actions {
display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px;
}
.btn-primary { background: var(--df-accent); color: #fff; }
.btn-primary:hover { filter: brightness(1.1); }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
</style>
+3 -33
View File
@@ -612,39 +612,9 @@ async function onRemoveModule() {
opacity: 0.6;
}
/* 按钮 */
.btn {
padding: 6px 12px;
border: none;
border-radius: var(--df-radius-sm, 4px);
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-ghost {
background: transparent;
color: var(--df-text-secondary);
border: 0.5px solid var(--df-border);
}
.btn-ghost:hover:not(:disabled) {
background: var(--df-bg);
color: var(--df-text);
}
.btn-ghost:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-sm {
padding: 4px 10px;
font-size: 11px;
}
/* 按钮基础样式 (.btn / .btn-ghost / .btn-sm) 已收敛至 global.css */
/* FileExplorer 工具栏为紧凑布局,覆 padding/font-size 避免按钮变大 */
.btn { padding: 6px 12px; font-size: 12px; }
/* 主体两栏 — 自适应比例 30% / 70% */
.explorer-body {