重构: 跨模块样式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:
@@ -944,7 +944,7 @@ mod tests {
|
|||||||
/// 构造测试用 ToolDefinition(仅 name 有意义,description/parameters 填占位)。
|
/// 构造测试用 ToolDefinition(仅 name 有意义,description/parameters 填占位)。
|
||||||
fn tool_def(name: &str) -> df_ai_core::types::ToolDefinition {
|
fn tool_def(name: &str) -> df_ai_core::types::ToolDefinition {
|
||||||
df_ai_core::types::ToolDefinition {
|
df_ai_core::types::ToolDefinition {
|
||||||
tool_type: "function".to_string(),
|
tool_type: "function".into(),
|
||||||
function: df_ai_core::types::ToolFunction {
|
function: df_ai_core::types::ToolFunction {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
|
|||||||
@@ -387,9 +387,31 @@ function groupIcon(name: string): string {
|
|||||||
border-radius: var(--df-radius-sm);
|
border-radius: var(--df-radius-sm);
|
||||||
background: rgba(255,255,255,0.02);
|
background: rgba(255,255,255,0.02);
|
||||||
}
|
}
|
||||||
.ai-batch-approve .ai-btn--sm {
|
/* .ai-btn 基类:批量审批按钮(历史重构遗漏定义,补全治本) */
|
||||||
font-size: 11px;
|
.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/背景,
|
/* -- 全部展开/收起(U-260618:从 topbar 下沉到分组标题行,移除独立按钮 padding/背景,
|
||||||
高度=文字行高,不撑高分组标题行;融入标题视觉,仅 hover 变色) -- */
|
高度=文字行高,不撑高分组标题行;融入标题视觉,仅 hover 变色) -- */
|
||||||
|
|||||||
@@ -813,123 +813,13 @@ defineExpose({
|
|||||||
transition: border-color 0.15s;
|
transition: border-color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* F-260614-05 Phase 2b: 待发送图片预览行(粘贴/拖拽加入,发送前可移除) */
|
/* F-260614-05 Phase 2b: 图片预览样式已提取到 styles/components.css 全局(ImageInput.vue 共享) */
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-input-wrap:focus-within {
|
.ai-input-wrap:focus-within {
|
||||||
border-color: color-mix(in srgb, var(--df-accent) 40%, transparent);
|
border-color: color-mix(in srgb, var(--df-accent) 40%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 技能 chip / 联想浮层 ── */
|
/* ── 技能 chip / 联想浮层样式已提取到 styles/components.css 全局(SkillMention.vue + MentionPopover.vue 共享)── */
|
||||||
.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; }
|
|
||||||
|
|
||||||
/* ── UX-10 §1.4: @ 实体引用浮层样式已随 MentionPopover.vue 提取 ── */
|
/* ── UX-10 §1.4: @ 实体引用浮层样式已随 MentionPopover.vue 提取 ── */
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- UX-10 §1.4: @ 实体引用浮层(@ 触发,选中插入 [类型:名] 标记,架构复用技能 popover) -->
|
<!-- 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">
|
<div v-if="!mentionItems.length" class="ai-skill-empty">
|
||||||
{{ mentionTotal ? $t('aiChat.mentionNoMatch') : $t('aiChat.mentionEmpty') }}
|
{{ mentionTotal ? $t('aiChat.mentionNoMatch') : $t('aiChat.mentionEmpty') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -65,34 +65,8 @@ const emit = defineEmits<{
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* ── UX-10 §1.4: @ 实体引用浮层(复用 .ai-skill-popover 容器,仅新增组标题/类型标签) ── */
|
/* ── UX-10 §1.4: @ 实体引用浮层特定样式(复用全局 .ai-skill-popover/.ai-skill-item,仅新增 mention 专有) ──
|
||||||
.ai-mention-popover {
|
.ai-skill-popover/.ai-skill-item/.ai-skill-empty 已提取到 styles/components.css 全局 */
|
||||||
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; }
|
|
||||||
.ai-mention-group {
|
.ai-mention-group {
|
||||||
padding: 4px 10px 2px;
|
padding: 4px 10px 2px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<router-link to="/projects" class="df-link">{{ $t('dashboard.viewAll') }}</router-link>
|
<router-link to="/projects" class="df-link">{{ $t('dashboard.viewAll') }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-list">
|
<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-row-top">
|
||||||
<div class="project-identity">
|
<div class="project-identity">
|
||||||
<span class="project-dot" :class="'dot-' + p.stage"></span>
|
<span class="project-dot" :class="'dot-' + p.stage"></span>
|
||||||
@@ -34,57 +34,67 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 活跃项目面板 — 从 Dashboard.vue 抽出(项目列表 + 进度条 + 阶段 chip + 空态)。
|
// 活跃项目面板 — 从 Dashboard.vue 抽出(项目列表 + 进度条 + 阶段 chip + 空态)。
|
||||||
// 共享 project store(全局单例),displayProjects computed 直读 store.projects/tasks,无需父传 props。
|
// 共享 project store(全局单例),displayProjects computed 过滤 active 状态按更新时间倒序取前 6 条。
|
||||||
// getProjectStage 死键语义对齐 constants/project.ts 的 PROJECT_STAGE_INFO。
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
|
import { projectStageInfo } from '@/constants/project'
|
||||||
import { formatRelative } from '@/utils/time'
|
import { formatRelative } from '@/utils/time'
|
||||||
|
|
||||||
const store = useProjectStore()
|
const store = useProjectStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
function getProjectStage(status: string): { stage: string; stageLabelKey: string; progress: number } {
|
// 点击项目行跳转详情(治本:列表项可进详情,与其他列表页交互一致)
|
||||||
// F-09 对齐:project status 死键(in_progress/paused/cancelled)已从后端映射移除,
|
function goDetail(projectId: string) {
|
||||||
// 此处 switch 同步收敛,与 constants/project.ts 的 PROJECT_STAGE_INFO 语义一致。
|
router.push(`/projects/${projectId}`)
|
||||||
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 getProjectTaskCount(projectId: string): number {
|
function getProjectTaskCount(projectId: string): number {
|
||||||
return store.tasks.filter(t => t.project_id === projectId && t.status === 'in_progress').length
|
return store.tasks.filter(t => t.project_id === projectId && t.status === 'in_progress').length
|
||||||
}
|
}
|
||||||
|
|
||||||
// 相对时间复用 utils/time.formatRelative(与 Tasks/AuditLog/MessageList 等同源,根治 NaN)
|
// 阶段 label key:projectStageInfo 返回的 stage 值(coding/testing/release/planning)
|
||||||
// 原 formatLastActivity 是其逐行复制,提取为单一来源。
|
// 直接拼接 i18n key dashboard.stage.<stage>,与 constants/project.ts PROJECT_STAGE_INFO 单一来源。
|
||||||
const formatLastActivity = formatRelative
|
// 面板名为"活跃项目",只显示 active 状态(DB 实际默认值,ProjectStatus union 历史遗留未含
|
||||||
|
// 'active',此处断言绕过;DB 实际无 planning/in_progress 等值产生,详见功能决策记录)。
|
||||||
|
// 按更新时间倒序取前 6 条(completed 归档项目不混入)。
|
||||||
const displayProjects = computed(() =>
|
const displayProjects = computed(() =>
|
||||||
store.projects.map(p => {
|
store.projects
|
||||||
const stageInfo = getProjectStage(p.status)
|
.filter(p => (p.status as string) === 'active')
|
||||||
return {
|
.sort((a, b) => b.updated_at.localeCompare(a.updated_at))
|
||||||
id: p.id,
|
.slice(0, 6)
|
||||||
name: p.name,
|
.map(p => {
|
||||||
...stageInfo,
|
const info = projectStageInfo(p.status)
|
||||||
activeTasks: getProjectTaskCount(p.id),
|
return {
|
||||||
lastActivity: formatLastActivity(p.updated_at),
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 仅迁入项目行自身样式;df-panel/df-link 等通用面板类留 Dashboard.vue(多面板共享) */
|
/* df-panel/df-link 通用面板类已提取到 styles/components.css 全局 */
|
||||||
.project-list { display: flex; flex-direction: column; }
|
.project-list { display: flex; flex-direction: column; }
|
||||||
.project-row {
|
.project-row {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
border-bottom: 0.5px solid var(--df-border);
|
border-bottom: 0.5px solid var(--df-border);
|
||||||
animation: fadeInUp 0.4s var(--df-ease) both;
|
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:last-child { border-bottom: none; }
|
||||||
|
|
||||||
.project-row-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
.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>
|
<router-link to="/ideas" class="df-link">{{ $t('dashboard.viewAll') }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="ideas-stats">
|
<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">
|
<div class="stat-item">
|
||||||
<span class="stat-num">{{ stats.pending }}</span>
|
<span class="stat-num">{{ stats.pending }}</span>
|
||||||
<span class="stat-label">{{ $t('ideas.statsPending') }}</span>
|
<span class="stat-label">{{ $t('ideas.statsPending') }}</span>
|
||||||
@@ -23,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="idea-rows">
|
<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">
|
<div class="idea-score-ring" :class="'ring-' + idea.tier">
|
||||||
<span class="idea-score-num">{{ idea.score }}</span>
|
<span class="idea-score-num">{{ idea.score }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,26 +35,39 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 灵感池面板 — 从 Dashboard.vue 抽出(灵感列表 + 评分环 + tier 着色 + 空态)。
|
// 灵感池面板 — 从 Dashboard.vue 抽出(灵感列表 + 评分环 + tier 着色 + 空态)。
|
||||||
// 共享 project store(全局单例),displayIdeas computed 直读 store.ideas(取前 4 条),无需父传 props。
|
// 共享 project store(全局单例),displayIdeas computed 按分数倒序取前 4 条(精选),无需父传 props。
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
|
import { scoreTier } from '@/utils/ideaEval'
|
||||||
|
|
||||||
const store = useProjectStore()
|
const store = useProjectStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 点击灵感行跳转详情(Ideas.vue 同组件路由,会自动选中)
|
||||||
|
function goDetail(ideaId: string) {
|
||||||
|
router.push(`/ideas/${ideaId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 精选灵感列表:按分数倒序取前 4 条(高分优先,符合"精选"语义;原来 slice(0,4)
|
||||||
|
// 无排序会显示最老的 4 条而非高分灵感)。
|
||||||
const displayIdeas = computed(() =>
|
const displayIdeas = computed(() =>
|
||||||
store.ideas.slice(0, 4).map(i => ({
|
[...store.ideas]
|
||||||
id: i.id,
|
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
||||||
title: i.title,
|
.slice(0, 4)
|
||||||
score: i.score ?? 0,
|
.map(i => ({
|
||||||
tier: (i.score ?? 0) >= 80 ? 'high' : (i.score ?? 0) >= 60 ? 'mid' : 'low',
|
id: i.id,
|
||||||
statusLabel: i.status,
|
title: i.title,
|
||||||
}))
|
score: i.score ?? 0,
|
||||||
|
tier: scoreTier(i.score),
|
||||||
|
statusLabel: i.status,
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
// 灵感池统计概览 — 4 项关键指标(总数/待审/已晋升/平均分)。
|
// 灵感池细分统计(顶部 StatCardRow 已显示总数,此处仅展示待审/已晋升/平均分 3 项细分,
|
||||||
// score 可为 null(未评估),avgScore 仅对已评估灵感求均值,无则显示 '—'。
|
// 删除重复的 total 避免与顶部统计卡信息冗余)。
|
||||||
const stats = computed(() => {
|
const stats = computed(() => {
|
||||||
const ideas = store.ideas
|
const ideas = store.ideas
|
||||||
const scored = ideas.filter(i => i.score != null)
|
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)
|
? Math.round(scored.reduce((sum, i) => sum + (i.score ?? 0), 0) / scored.length)
|
||||||
: null
|
: null
|
||||||
return {
|
return {
|
||||||
total: ideas.length,
|
|
||||||
pending: ideas.filter(i => i.status === 'draft' || i.status === 'pending_review').length,
|
pending: ideas.filter(i => i.status === 'draft' || i.status === 'pending_review').length,
|
||||||
promoted: ideas.filter(i => i.status === 'promoted').length,
|
promoted: ideas.filter(i => i.status === 'promoted').length,
|
||||||
avgScore: avg == null ? '—' : avg,
|
avgScore: avg == null ? '—' : avg,
|
||||||
@@ -79,10 +87,10 @@ function ideaStatusLabel(status: string): string {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 仅迁入灵感行自身样式;df-panel/df-link 等通用面板类留 Dashboard.vue(多面板共享) */
|
/* df-panel/df-link 通用面板类已提取到 styles/components.css 全局 */
|
||||||
.ideas-stats {
|
.ideas-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
@@ -117,6 +125,13 @@ function ideaStatusLabel(status: string): string {
|
|||||||
padding: 7px 0;
|
padding: 7px 0;
|
||||||
border-bottom: 0.5px solid var(--df-border);
|
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-row:last-child { border-bottom: none; }
|
||||||
|
|
||||||
.idea-score-ring {
|
.idea-score-ring {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(stat, i) in stats" :key="stat.key"
|
v-for="(stat, i) in stats" :key="stat.key"
|
||||||
class="stat-card"
|
class="stat-card"
|
||||||
|
:class="{ clickable: stat.link }"
|
||||||
:style="{ animationDelay: `${i * 60}ms` }"
|
: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-bg" :class="'stat-bg--' + stat.key"></div>
|
||||||
<div class="stat-card-inner">
|
<div class="stat-card-inner">
|
||||||
@@ -11,11 +14,6 @@
|
|||||||
<div class="stat-icon-wrap" :style="{ background: stat.iconBg }">
|
<div class="stat-icon-wrap" :style="{ background: stat.iconBg }">
|
||||||
<span class="stat-icon">{{ stat.icon }}</span>
|
<span class="stat-icon">{{ stat.icon }}</span>
|
||||||
</div>
|
</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>
|
||||||
<div class="stat-value">{{ stat.value }}</div>
|
<div class="stat-value">{{ stat.value }}</div>
|
||||||
<div class="stat-label">{{ stat.label }}</div>
|
<div class="stat-label">{{ stat.label }}</div>
|
||||||
@@ -27,22 +25,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 统计卡片行 — 从 Dashboard.vue 抽出(4 张统计卡: ideas/projects/tasks/drafts)。
|
// 统计卡片行 — 从 Dashboard.vue 抽出(4 张统计卡: ideas/projects/tasks/drafts)。
|
||||||
// 共享 project store(全局单例),stats computed 直读 store.stats,无需父传 props。
|
// 共享 project store(全局单例),stats computed 直读 store.stats,无需父传 props。
|
||||||
|
// 卡片可点击跳转对应列表页(ideas/projects/tasks),提升导航效率。
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
|
|
||||||
const store = useProjectStore()
|
const store = useProjectStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
function go(link: string) {
|
||||||
|
router.push(link)
|
||||||
|
}
|
||||||
|
|
||||||
const stats = computed(() => [
|
const stats = computed(() => [
|
||||||
{ key: 'ideas', icon: '\u{1F4A1}', label: t('dashboard.stats.ideas'), value: store.stats.ideas, trend: 0,
|
{ key: 'ideas', icon: '\u{1F4A1}', label: t('dashboard.stats.ideas'), value: store.stats.ideas,
|
||||||
iconBg: 'rgba(123,111,240,0.12)' },
|
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, trend: 0,
|
{ key: 'projects', icon: '\u{1F4C2}', label: t('dashboard.stats.projects'), value: store.stats.projects,
|
||||||
iconBg: 'rgba(94,175,240,0.10)' },
|
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, trend: 0,
|
{ key: 'tasks', icon: '⚡', label: t('dashboard.stats.activeTasks'), value: store.stats.activeTasks,
|
||||||
iconBg: 'rgba(61,219,160,0.10)' },
|
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, trend: 0,
|
{ key: 'drafts', icon: '\u{1F4DD}', label: t('dashboard.stats.drafts'), value: store.stats.drafts,
|
||||||
iconBg: 'rgba(240,199,94,0.10)' },
|
iconBg: 'rgba(240,199,94,0.10)', link: null, title: '' },
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -61,6 +66,14 @@ const stats = computed(() => [
|
|||||||
border: 0.5px solid var(--df-border);
|
border: 0.5px solid var(--df-border);
|
||||||
animation: fadeInUp 0.5s var(--df-ease) both;
|
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 {
|
.stat-card-bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -82,17 +95,6 @@ const stats = computed(() => [
|
|||||||
}
|
}
|
||||||
.stat-icon { font-size: 14px; }
|
.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 {
|
.stat-value {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</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')">
|
<button class="btn-evaluate" :disabled="evaluating" @click="$emit('evaluate')">
|
||||||
{{ evaluating ? $t('ideas.evaluating') : $t('ideas.startEval') }}
|
{{ evaluating ? $t('ideas.evaluating') : $t('ideas.startEval') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -90,8 +90,8 @@
|
|||||||
<!-- 评估历史(版本时间线) -->
|
<!-- 评估历史(版本时间线) -->
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h3>{{ $t('ideas.historyTitle') }}</h3>
|
<h3>{{ $t('ideas.historyTitle') }}</h3>
|
||||||
<div v-if="historyLoading" class="eval-report" style="opacity:0.5">{{ $t('ideas.historyLoading') }}</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" style="opacity:0.5">{{ $t('ideas.historyEmpty') }}</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-else class="eval-history-list">
|
||||||
<div
|
<div
|
||||||
v-for="rec in evalHistory"
|
v-for="rec in evalHistory"
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
<span v-else class="related-chip related-chip-missing">#{{ row.id }}</span>
|
<span v-else class="related-chip related-chip-missing">#{{ row.id }}</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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>
|
<button class="btn btn-ghost btn-sm" @click="startRelateEdit">{{ $t('ideas.relatedManage') }}</button>
|
||||||
</template>
|
</template>
|
||||||
<!-- 编辑态 -->
|
<!-- 编辑态 -->
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
<span>{{ idea.title }}</span>
|
<span>{{ idea.title }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<div class="related-edit-actions">
|
||||||
<button class="btn btn-primary btn-sm" @click="confirmRelate">{{ $t('ideas.relatedConfirm') }}</button>
|
<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>
|
<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">
|
<div class="score-bar-row" v-for="dim in parseScores(idea.scores)" :key="dim.name">
|
||||||
<span class="score-bar-label">{{ dim.name }}</span>
|
<span class="score-bar-label">{{ dim.name }}</span>
|
||||||
<div class="score-bar-track">
|
<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>
|
</div>
|
||||||
<span class="score-bar-value">{{ dim.score }}</span>
|
<span class="score-bar-value">{{ dim.score }}</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 标签 -->
|
<!-- 标签 -->
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
<div class="tag-list" v-if="parseTags(idea.tags).length > 0">
|
<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>
|
<span class="tag" v-for="tag in parseTags(idea.tags)" :key="tag">{{ tag }}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 状态管理 -->
|
<!-- 状态管理 -->
|
||||||
@@ -230,7 +230,7 @@ import { useProjectStore } from '../../stores/project'
|
|||||||
import { useRendered } from '../../composables/useMarkdown'
|
import { useRendered } from '../../composables/useMarkdown'
|
||||||
import { ideaApi } from '../../api'
|
import { ideaApi } from '../../api'
|
||||||
import { formatDate } from '../../utils/time'
|
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'
|
import type { IdeaRecord, IdeaStatus, IdeaEvaluationRecord } from '../../api/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -491,17 +491,10 @@ watch(() => props.idea.id, loadHistory)
|
|||||||
border-left: 3px solid var(--df-accent);
|
border-left: 3px solid var(--df-accent);
|
||||||
border-radius: 0 8px 8px 0;
|
border-radius: 0 8px 8px 0;
|
||||||
}
|
}
|
||||||
|
/* 弱化态(空数据/加载中占位,原 inline opacity:0.5 统一为 class) */
|
||||||
|
.eval-report--muted { opacity: 0.5; }
|
||||||
|
|
||||||
/* ===== 状态标签 ===== */
|
/* ===== 状态标签已提取到 styles/components.css 全局(.status-tag 系列) ===== */
|
||||||
.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); }
|
|
||||||
|
|
||||||
/* ===== 对抗式评估 ===== */
|
/* ===== 对抗式评估 ===== */
|
||||||
.adversarial-eval {
|
.adversarial-eval {
|
||||||
@@ -595,21 +588,7 @@ watch(() => props.idea.id, loadHistory)
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assessment-badge {
|
/* assessment-badge 系列已提取到 styles/components.css 全局 */
|
||||||
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); }
|
|
||||||
|
|
||||||
.final-score {
|
.final-score {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -774,45 +753,7 @@ watch(() => props.idea.id, loadHistory)
|
|||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 多维评分条(div 模拟) ===== */
|
/* ===== 多维评分条已提取到 styles/components.css 全局(.score-bar-* 系列) ===== */
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 标签 ===== */
|
/* ===== 标签 ===== */
|
||||||
.tag-list { display: flex; gap: 8px; flex-wrap: wrap; }
|
.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-item input { cursor: pointer; }
|
||||||
.related-edit-actions { display: flex; gap: 8px; }
|
.related-edit-actions { display: flex; gap: 8px; }
|
||||||
|
|
||||||
/* ===== 按钮 ===== */
|
/* ===== 按钮已提取到 global.css 全局(.btn/.btn-primary/.btn-ghost/.btn-sm) ===== */
|
||||||
.btn {
|
/* IdeaDetail 特有按钮定位 */
|
||||||
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; }
|
|
||||||
.desc-edit-btn { margin-bottom: var(--df-gap-page); margin-top: 4px; }
|
.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; }
|
.desc-edit-actions { display: flex; gap: 8px; margin-bottom: var(--df-gap-page); margin-top: 8px; }
|
||||||
|
|
||||||
|
|||||||
@@ -350,17 +350,7 @@ watch(() => props.detail.knowledge.id, () => {
|
|||||||
.edit-input-title { font-size: 16px; font-weight: 500; }
|
.edit-input-title { font-size: 16px; font-weight: 500; }
|
||||||
.edit-textarea { min-height: 100px; resize: vertical; font-family: inherit; }
|
.edit-textarea { min-height: 100px; resize: vertical; font-family: inherit; }
|
||||||
|
|
||||||
/* ===== 按钮 ===== */
|
/* 按钮基础样式 (.btn / .btn-sm / .btn-primary / .btn-ghost) 已收敛至 global.css */
|
||||||
.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); }
|
|
||||||
|
|
||||||
/* ===== 响应式:窄屏详情标题防竖线化(B-260619)===== */
|
/* ===== 响应式:窄屏详情标题防竖线化(B-260619)===== */
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
|
|||||||
@@ -337,30 +337,9 @@ onBeforeUnmount(() => {
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
/* 按钮基础样式 (.btn / .btn-ghost / .btn-sm / .btn-primary) 已收敛至 global.css */
|
||||||
padding: 4px 12px;
|
/* 依赖图工具栏紧凑,覆 padding/font-size 避免按钮变大 */
|
||||||
border: none;
|
.btn { padding: 4px 12px; font-size: 12px; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加依赖弹窗 */
|
/* 添加依赖弹窗 */
|
||||||
.dep-graph__modal-overlay {
|
.dep-graph__modal-overlay {
|
||||||
@@ -395,7 +374,4 @@ onBeforeUnmount(() => {
|
|||||||
.dep-graph__modal-actions {
|
.dep-graph__modal-actions {
|
||||||
display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -612,39 +612,9 @@ async function onRemoveModule() {
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 按钮 */
|
/* 按钮基础样式 (.btn / .btn-ghost / .btn-sm) 已收敛至 global.css */
|
||||||
.btn {
|
/* FileExplorer 工具栏为紧凑布局,覆 padding/font-size 避免按钮变大 */
|
||||||
padding: 6px 12px;
|
.btn { padding: 6px 12px; font-size: 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主体两栏 — 自适应比例 30% / 70% */
|
/* 主体两栏 — 自适应比例 30% / 70% */
|
||||||
.explorer-body {
|
.explorer-body {
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ export default {
|
|||||||
loading: 'Loading…',
|
loading: 'Loading…',
|
||||||
empty: 'No audit records',
|
empty: 'No audit records',
|
||||||
|
|
||||||
|
filter: {
|
||||||
|
allStatus: 'All status',
|
||||||
|
allRisk: 'All risk',
|
||||||
|
toolPlaceholder: 'Search tool name…',
|
||||||
|
},
|
||||||
|
|
||||||
col: {
|
col: {
|
||||||
time: 'Requested',
|
time: 'Requested',
|
||||||
tool: 'Tool',
|
tool: 'Tool',
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export default {
|
|||||||
activeProjects: 'Active Projects',
|
activeProjects: 'Active Projects',
|
||||||
viewAll: 'View all',
|
viewAll: 'View all',
|
||||||
ideaPool: 'Idea Pool',
|
ideaPool: 'Idea Pool',
|
||||||
|
goProjectDetail: 'View project details',
|
||||||
|
goIdeaDetail: 'View idea details',
|
||||||
recentDecisions: 'Recent Decisions',
|
recentDecisions: 'Recent Decisions',
|
||||||
openAnnotations: 'Open Annotations',
|
openAnnotations: 'Open Annotations',
|
||||||
aiBatchProcess: 'AI Batch Process',
|
aiBatchProcess: 'AI Batch Process',
|
||||||
@@ -39,6 +41,7 @@ export default {
|
|||||||
stage: {
|
stage: {
|
||||||
planning: 'Planning',
|
planning: 'Planning',
|
||||||
coding: 'Coding',
|
coding: 'Coding',
|
||||||
|
release: 'Release',
|
||||||
done: 'Done',
|
done: 'Done',
|
||||||
},
|
},
|
||||||
ideaStatus: {
|
ideaStatus: {
|
||||||
|
|||||||
@@ -91,8 +91,6 @@ export default {
|
|||||||
relatedCancel: 'Cancel',
|
relatedCancel: 'Cancel',
|
||||||
|
|
||||||
// Idea stats panel (used by IdeasPanel)
|
// Idea stats panel (used by IdeasPanel)
|
||||||
statsTitle: '📈 Overview',
|
|
||||||
statsTotal: 'Total',
|
|
||||||
statsPending: 'Pending',
|
statsPending: 'Pending',
|
||||||
statsPromoted: 'Promoted',
|
statsPromoted: 'Promoted',
|
||||||
statsAvgScore: 'Avg score',
|
statsAvgScore: 'Avg score',
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ export default {
|
|||||||
loading: '加载中…',
|
loading: '加载中…',
|
||||||
empty: '暂无审计记录',
|
empty: '暂无审计记录',
|
||||||
|
|
||||||
|
filter: {
|
||||||
|
allStatus: '全部状态',
|
||||||
|
allRisk: '全部风险',
|
||||||
|
toolPlaceholder: '搜索工具名…',
|
||||||
|
},
|
||||||
|
|
||||||
col: {
|
col: {
|
||||||
time: '请求时间',
|
time: '请求时间',
|
||||||
tool: '工具',
|
tool: '工具',
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export default {
|
|||||||
activeProjects: '活跃项目',
|
activeProjects: '活跃项目',
|
||||||
viewAll: '查看全部',
|
viewAll: '查看全部',
|
||||||
ideaPool: '灵感',
|
ideaPool: '灵感',
|
||||||
|
goProjectDetail: '查看项目详情',
|
||||||
|
goIdeaDetail: '查看灵感详情',
|
||||||
recentDecisions: '最近决策',
|
recentDecisions: '最近决策',
|
||||||
openAnnotations: '待处理标注',
|
openAnnotations: '待处理标注',
|
||||||
aiBatchProcess: 'AI 批量处理',
|
aiBatchProcess: 'AI 批量处理',
|
||||||
@@ -39,6 +41,7 @@ export default {
|
|||||||
stage: {
|
stage: {
|
||||||
planning: '规划中',
|
planning: '规划中',
|
||||||
coding: '编码中',
|
coding: '编码中',
|
||||||
|
release: '发布中',
|
||||||
done: '已完成',
|
done: '已完成',
|
||||||
},
|
},
|
||||||
ideaStatus: {
|
ideaStatus: {
|
||||||
|
|||||||
@@ -82,8 +82,6 @@ export default {
|
|||||||
relatedCancel: '取消',
|
relatedCancel: '取消',
|
||||||
|
|
||||||
// 灵感统计看板(IdeasPanel 引用)
|
// 灵感统计看板(IdeasPanel 引用)
|
||||||
statsTitle: '📈 灵感概览',
|
|
||||||
statsTotal: '总数',
|
|
||||||
statsPending: '待评估',
|
statsPending: '待评估',
|
||||||
statsPromoted: '已立项',
|
statsPromoted: '已立项',
|
||||||
statsAvgScore: '平均分',
|
statsAvgScore: '平均分',
|
||||||
|
|||||||
@@ -18,8 +18,100 @@
|
|||||||
← ProjectDetail.vue / TaskDetail.vue / TaskOutputCard.vue 三处复制
|
← ProjectDetail.vue / TaskDetail.vue / TaskOutputCard.vue 三处复制
|
||||||
- .panel / .panel-header ← ProjectDetail.vue / TaskDetail.vue 两处复制
|
- .panel / .panel-header ← ProjectDetail.vue / TaskDetail.vue 两处复制
|
||||||
- .tech-tag / .card-tags ← ProjectCard.vue / Projects.vue 两处复制
|
- .tech-tag / .card-tags ← ProjectCard.vue / Projects.vue 两处复制
|
||||||
|
- .error-banner / .error-text / .error-dismiss ← Dashboard.vue / Knowledge.vue 两处复制
|
||||||
═══════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* — Error Banner:页面级错误提示条(danger 底 + 关闭按钮) —
|
||||||
|
原 Dashboard.vue / Knowledge.vue <style scoped> 两处逐字一致。
|
||||||
|
margin-bottom 由各页自定义(全局不设,避免覆盖页面间距变量)。 */
|
||||||
|
.error-banner {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(255,107,107,0.12); border: 0.5px solid var(--df-danger);
|
||||||
|
border-radius: var(--df-radius-sm); color: var(--df-danger); font-size: 12px;
|
||||||
|
}
|
||||||
|
.error-text { word-break: break-word; }
|
||||||
|
.error-dismiss {
|
||||||
|
flex-shrink: 0; background: transparent; border: none; color: var(--df-danger);
|
||||||
|
font-size: 14px; cursor: pointer; line-height: 1; padding: 0 2px;
|
||||||
|
}
|
||||||
|
.error-dismiss:hover { opacity: 0.7; }
|
||||||
|
|
||||||
|
/* — 灵感状态标签:ideas 列表(Ideas.vue) + 灵感详情(IdeaDetail.vue) 共享 —
|
||||||
|
原 Ideas.vue / IdeaDetail.vue <style scoped> 两处逐字一致(灵感专属 5 态色系,
|
||||||
|
与 Tasks/AuditLog 的 status-tag 语义不同,独立提取避免混淆)。 */
|
||||||
|
.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); }
|
||||||
|
|
||||||
|
/* — 多维评分条:IdeaDetail.vue + ProjectDetail.vue 共享 —
|
||||||
|
原 IdeaDetail.vue L770-808 / ProjectDetail.vue L678-692 两处逐字一致
|
||||||
|
(ProjectDetail 注释自承认"复用 IdeaDetail score-bar-* 样式定义")。
|
||||||
|
差异:score-bar-label min-width 72px(IdeaDetail) vs 64px(ProjectDetail),
|
||||||
|
取 72px 保证较长维度名不换行;row gap 12px(IdeaDetail) vs 10px(ProjectDetail),取 12px 更透气。 */
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* — 评估徽章(assessment badge):IdeaDetail.vue + ProjectDetail.vue 共享 —
|
||||||
|
recommendation → CSS 颜色类映射(对应 .immediate/.soon/.conditional/.revised/.defer/.cancel)。
|
||||||
|
原 IdeaDetail.vue L589-606 / ProjectDetail.vue L657-671 两处逐字重复。 */
|
||||||
|
.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: var(--df-warning-bg); 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); }
|
||||||
|
|
||||||
/* — Info Item:字段同行布局(label 固定宽 + 值占余),描述字段 info-block 保持块状 —
|
/* — Info Item:字段同行布局(label 固定宽 + 值占余),描述字段 info-block 保持块状 —
|
||||||
原 ProjectDetail.vue L439-456 / TaskDetail.vue L560-577 /
|
原 ProjectDetail.vue L439-456 / TaskDetail.vue L560-577 /
|
||||||
TaskOutputCard.vue L93-110 三处逐字一致(后者注释说明 scoped 不渗透故复制)。 */
|
TaskOutputCard.vue L93-110 三处逐字一致(后者注释说明 scoped 不渗透故复制)。 */
|
||||||
@@ -78,6 +170,157 @@
|
|||||||
border: 0.5px solid var(--df-border);
|
border: 0.5px solid var(--df-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* — Dashboard 面板基类:ActiveProjectsPanel / IdeasPanel 共享 —
|
||||||
|
原 Dashboard.vue <style scoped> 定义,但 Vue scoped 不渗透到子组件根元素,
|
||||||
|
导致两个子面板的 .df-panel/.df-panel-head/.df-panel-title/.df-link 实际裸奔
|
||||||
|
(无背景/边框/padding/链接色),暗色主题下视觉割裂。提取全局根治。
|
||||||
|
落地批:Dashboard 视觉一致性修复 */
|
||||||
|
.df-panel {
|
||||||
|
background: var(--df-bg-card);
|
||||||
|
border: 0.5px solid var(--df-border);
|
||||||
|
border-radius: var(--df-radius-md);
|
||||||
|
padding: 14px 16px;
|
||||||
|
animation: fadeInUp 0.5s var(--df-ease) both;
|
||||||
|
}
|
||||||
|
.df-panel-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.df-panel-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--df-text);
|
||||||
|
letter-spacing: -0.1px;
|
||||||
|
}
|
||||||
|
.df-link {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--df-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.df-link:hover { opacity: 1; text-decoration: underline; }
|
||||||
|
|
||||||
|
/* — AI 技能联想浮层 / chip:SkillMention.vue + MentionPopover.vue 共享 —
|
||||||
|
原 ChatInput.vue <style scoped> 定义,但实际使用方是子组件 SkillMention.vue
|
||||||
|
(无自带 style 块)和 MentionPopover.vue(自带 scoped 副本)。ChatInput.vue
|
||||||
|
自身 template 未使用这些类,scoped 定义为死代码且不渗透子组件。
|
||||||
|
提取全局根治,消除三份重复。 */
|
||||||
|
.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; }
|
||||||
|
.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; }
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
/* — AI 图片预览:ImageInput.vue 专用(原 ChatInput.vue scoped 死代码,子组件拿不到) — */
|
||||||
|
.ai-img-preview-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/* — AiChat 图标按钮(共享工具类):TopBar header-actions + ConversationSidebar 新建按钮 —
|
/* — AiChat 图标按钮(共享工具类):TopBar header-actions + ConversationSidebar 新建按钮 —
|
||||||
原 AiChat.vue <style scoped> L1039-1072 抽取(非 scoped 全局,供两个 ai/ 子组件共享)。
|
原 AiChat.vue <style scoped> L1039-1072 抽取(非 scoped 全局,供两个 ai/ 子组件共享)。
|
||||||
复用原因:跨 TopBar + ConversationSidebar,无法归属单一 scoped;
|
复用原因:跨 TopBar + ConversationSidebar,无法归属单一 scoped;
|
||||||
@@ -123,4 +366,3 @@
|
|||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -220,3 +220,43 @@ html, body, #app {
|
|||||||
/* — Empty State — */
|
/* — Empty State — */
|
||||||
.empty-state { text-align: center; padding: 60px 20px; font-size: 14px; color: var(--df-text-dim); }
|
.empty-state { text-align: center; padding: 60px 20px; font-size: 14px; color: var(--df-text-dim); }
|
||||||
.empty-hint { font-size: 13px; color: var(--df-text-dim); padding: 12px 0; }
|
.empty-hint { font-size: 13px; color: var(--df-text-dim); padding: 12px 0; }
|
||||||
|
.empty-icon { font-size: 48px; margin-bottom: 12px; }
|
||||||
|
|
||||||
|
/* — Back Link(详情页返回链接)— ProjectDetail / TaskDetail 共享 */
|
||||||
|
.back-link { font-size: 13px; color: var(--df-accent); text-decoration: none; white-space: nowrap; }
|
||||||
|
.back-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* — Filter Bar(列表页筛选 + 搜索)— Ideas / Tasks 共享 */
|
||||||
|
.filter-bar { display: flex; gap: 8px; margin-bottom: var(--df-gap-page); align-items: center; }
|
||||||
|
.filter-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: 0.5px solid var(--df-border);
|
||||||
|
border-radius: var(--df-radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--df-text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.filter-btn:hover { background: var(--df-bg-card); color: var(--df-text); }
|
||||||
|
.filter-btn.active {
|
||||||
|
background: var(--df-accent);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--df-accent);
|
||||||
|
}
|
||||||
|
.search-box { flex: 1; max-width: 300px; }
|
||||||
|
.search-box input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 0.5px solid var(--df-border);
|
||||||
|
border-radius: var(--df-radius-sm);
|
||||||
|
background: var(--df-bg);
|
||||||
|
color: var(--df-text);
|
||||||
|
font-size: 13px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.search-box input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--df-accent);
|
||||||
|
background: var(--df-bg-raised);
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,22 @@ export interface ScoreDimension {
|
|||||||
score: number
|
score: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 评分三分档类型 */
|
||||||
|
export type ScoreTier = 'high' | 'mid' | 'low'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评分三分档阈值统一(>=80 high / >=60 mid / else low)。
|
||||||
|
* 原 Ideas.vue scoreClass / IdeasPanel.vue tier / IdeaDetail.vue 模板内联 /
|
||||||
|
* ProjectDetail.vue 模板内联 4 处重复同一阈值逻辑,提取单一来源。
|
||||||
|
* 阈值调整只需改这一处。
|
||||||
|
*/
|
||||||
|
export function scoreTier(score: number | null | undefined): ScoreTier {
|
||||||
|
const s = score ?? 0
|
||||||
|
if (s >= 80) return 'high'
|
||||||
|
if (s >= 60) return 'mid'
|
||||||
|
return 'low'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 idea.scores(JSON 对象 {dim: score})为维度数组。
|
* 解析 idea.scores(JSON 对象 {dim: score})为维度数组。
|
||||||
* null / 空 / 非法 / 非对象 → []。非数值分维度记 0。
|
* null / 空 / 非法 / 非对象 → []。非数值分维度记 0。
|
||||||
|
|||||||
@@ -3,16 +3,36 @@
|
|||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<h1>{{ t('auditLog.title') }}</h1>
|
<h1>{{ t('auditLog.title') }}</h1>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button class="btn btn-ghost" @click="reload">{{ t('auditLog.refresh') }}</button>
|
<button class="btn btn-ghost" @click="reload" :disabled="loading">
|
||||||
|
<svg v-if="loading" class="audit-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg>
|
||||||
|
{{ t('auditLog.refresh') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p class="page-desc">{{ t('auditLog.desc') }}</p>
|
<p class="page-desc">{{ t('auditLog.desc') }}</p>
|
||||||
|
|
||||||
|
<!-- 筛选栏 -->
|
||||||
|
<div class="filter-bar audit-filters">
|
||||||
|
<select v-model="statusFilter" class="audit-filter-select">
|
||||||
|
<option value="">{{ t('auditLog.filter.allStatus') }}</option>
|
||||||
|
<option v-for="s in statusKeys" :key="s" :value="s">{{ t(`auditLog.status.${s}`) }}</option>
|
||||||
|
</select>
|
||||||
|
<select v-model="riskFilter" class="audit-filter-select">
|
||||||
|
<option value="">{{ t('auditLog.filter.allRisk') }}</option>
|
||||||
|
<option v-for="r in riskKeys" :key="r" :value="r">{{ t(`auditLog.risk.${r}`) }}</option>
|
||||||
|
</select>
|
||||||
|
<div class="search-box audit-search">
|
||||||
|
<input v-model="toolSearch" type="text" :placeholder="t('auditLog.filter.toolPlaceholder')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 状态条 -->
|
<!-- 状态条 -->
|
||||||
<div v-if="loading" class="empty-state">{{ t('auditLog.loading') }}</div>
|
<div v-if="loading" class="empty-state audit-loading">
|
||||||
<div v-else-if="errorMsg" class="empty-state">{{ errorMsg }}</div>
|
<svg class="audit-spinner audit-spinner--lg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg>
|
||||||
<div v-else-if="records.length === 0" class="empty-state">{{ t('auditLog.empty') }}</div>
|
</div>
|
||||||
|
<div v-else-if="errorMsg" class="empty-state audit-error">⚠ {{ errorMsg }}</div>
|
||||||
|
<div v-else-if="filteredRecords.length === 0" class="empty-state">{{ t('auditLog.empty') }}</div>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<div v-else class="audit-table-wrap">
|
<div v-else class="audit-table-wrap">
|
||||||
@@ -29,40 +49,57 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="r in records" :key="r.id">
|
<template v-for="r in filteredRecords" :key="r.id">
|
||||||
<td class="col-time">
|
<tr class="audit-row" :class="{ expanded: expandedId === r.id }" @click="toggleExpand(r.id)">
|
||||||
<div class="time-rel">{{ formatRelative(r.requested_at) }}</div>
|
<td class="col-time">
|
||||||
<div class="time-abs">{{ formatDate(r.requested_at) }}</div>
|
<div class="time-rel">{{ formatRelative(r.requested_at) }}</div>
|
||||||
</td>
|
<div class="time-abs">{{ formatDate(r.requested_at) }}</div>
|
||||||
<td class="col-tool"><code class="tool-name">{{ r.tool_name }}</code></td>
|
<div v-if="r.executed_at" class="time-dur">⏱ {{ durationLabel(r.requested_at, r.executed_at) }}</div>
|
||||||
<td class="col-risk">
|
</td>
|
||||||
<span class="risk-badge" :class="riskClass(r.risk_level)">{{ riskLabel(r.risk_level) }}</span>
|
<td class="col-tool"><code class="tool-name">{{ r.tool_name }}</code></td>
|
||||||
</td>
|
<td class="col-risk">
|
||||||
<td class="col-status">
|
<span class="risk-badge" :class="riskClass(r.risk_level)">{{ riskLabel(r.risk_level) }}</span>
|
||||||
<span class="status-tag" :class="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
|
</td>
|
||||||
</td>
|
<td class="col-status">
|
||||||
<td class="col-decided">
|
<span class="status-tag" :class="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
|
||||||
<span v-if="r.decided_by" class="decided-tag" :class="decidedClass(r.decided_by)">{{ decidedLabel(r.decided_by) }}</span>
|
</td>
|
||||||
<span v-else class="decided-none">—</span>
|
<td class="col-decided">
|
||||||
</td>
|
<span v-if="r.decided_by" class="decided-tag" :class="decidedClass(r.decided_by)">{{ decidedLabel(r.decided_by) }}</span>
|
||||||
<td class="col-args"><code class="brief">{{ r.arguments_brief || '—' }}</code></td>
|
<span v-else class="decided-none">—</span>
|
||||||
<td class="col-result"><code class="brief">{{ r.result_brief || '—' }}</code></td>
|
</td>
|
||||||
</tr>
|
<td class="col-args"><code class="brief">{{ r.arguments_brief || '—' }}</code></td>
|
||||||
|
<td class="col-result"><code class="brief">{{ r.result_brief || '—' }}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="expandedId === r.id" class="audit-detail-row">
|
||||||
|
<td colspan="7">
|
||||||
|
<div class="audit-detail">
|
||||||
|
<div class="detail-section">
|
||||||
|
<span class="detail-label">{{ t('auditLog.col.args') }}</span>
|
||||||
|
<pre class="detail-pre">{{ r.arguments_brief || '—' }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<span class="detail-label">{{ t('auditLog.col.result') }}</span>
|
||||||
|
<pre class="detail-pre">{{ r.result_brief || '—' }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div v-if="!loading && !errorMsg" class="pager">
|
<div v-if="!loading && !errorMsg" class="pager">
|
||||||
<button class="btn btn-ghost" :disabled="offset === 0" @click="prevPage">{{ t('auditLog.pager.prev') }}</button>
|
<button class="btn btn-ghost btn-sm" :disabled="offset === 0" @click="prevPage">{{ t('auditLog.pager.prev') }}</button>
|
||||||
<span class="pager-info">{{ t('auditLog.pager.page', { n: page }) }}{{ hasMore ? '' : t('auditLog.pager.last') }}</span>
|
<span class="pager-info">{{ t('auditLog.pager.page', { n: page }) }}{{ hasMore ? '' : t('auditLog.pager.last') }}</span>
|
||||||
<button class="btn btn-ghost" :disabled="!hasMore" @click="nextPage">{{ t('auditLog.pager.next') }}</button>
|
<button class="btn btn-ghost btn-sm" :disabled="!hasMore" @click="nextPage">{{ t('auditLog.pager.next') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { formatRelative, formatDate } from '@/utils/time'
|
import { formatRelative, formatDate } from '@/utils/time'
|
||||||
@@ -91,10 +128,50 @@ const offset = ref(0)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
|
|
||||||
|
// 筛选状态(客户端过滤,数据量轻量)
|
||||||
|
const statusFilter = ref('')
|
||||||
|
const riskFilter = ref('')
|
||||||
|
const toolSearch = ref('')
|
||||||
|
|
||||||
|
// 展开行(查看完整 args/result)
|
||||||
|
const expandedId = ref<string | null>(null)
|
||||||
|
|
||||||
|
const statusKeys = ['pending', 'approved', 'rejected', 'executing', 'completed', 'failed']
|
||||||
|
const riskKeys = ['low', 'medium', 'high']
|
||||||
|
|
||||||
const page = computed(() => Math.floor(offset.value / PAGE_SIZE) + 1)
|
const page = computed(() => Math.floor(offset.value / PAGE_SIZE) + 1)
|
||||||
// 下一页存在性:本页满 PAGE_SIZE 视为可能还有更多(hasMore);末页不足时点下一页会拉空,自动回退
|
// 下一页存在性:本页满 PAGE_SIZE 视为可能还有更多(hasMore);末页不足时点下一页会拉空,自动回退
|
||||||
const hasMore = computed(() => records.value.length === PAGE_SIZE)
|
const hasMore = computed(() => records.value.length === PAGE_SIZE)
|
||||||
|
|
||||||
|
// 客户端筛选(状态/风险/工具名)
|
||||||
|
// 后端 list_tool_executions 不支持 WHERE 筛选,数据量小(每页 50),前端 filter 轻量。
|
||||||
|
const filteredRecords = computed(() => {
|
||||||
|
let list = records.value
|
||||||
|
if (statusFilter.value) list = list.filter(r => r.status === statusFilter.value)
|
||||||
|
if (riskFilter.value) list = list.filter(r => r.risk_level === riskFilter.value)
|
||||||
|
const q = toolSearch.value.trim().toLowerCase()
|
||||||
|
if (q) list = list.filter(r => r.tool_name.toLowerCase().includes(q))
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
// 筛选变化时关闭展开行(避免展开行被筛掉后残留)
|
||||||
|
watch([statusFilter, riskFilter, toolSearch], () => { expandedId.value = null })
|
||||||
|
|
||||||
|
function toggleExpand(id: string) {
|
||||||
|
expandedId.value = expandedId.value === id ? null : id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审批→执行 耗时(秒级,毫秒差太精细)
|
||||||
|
function durationLabel(requested: string, executed: string): string {
|
||||||
|
const ms = new Date(executed).getTime() - new Date(requested).getTime()
|
||||||
|
if (isNaN(ms) || ms < 0) return '—'
|
||||||
|
if (ms < 1000) return '<1s'
|
||||||
|
const s = Math.floor(ms / 1000)
|
||||||
|
if (s < 60) return `${s}s`
|
||||||
|
const m = Math.floor(s / 60)
|
||||||
|
return `${m}m${s % 60}s`
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
errorMsg.value = ''
|
errorMsg.value = ''
|
||||||
@@ -117,16 +194,20 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
|
if (loading.value) return
|
||||||
offset.value = 0
|
offset.value = 0
|
||||||
|
expandedId.value = null
|
||||||
void load()
|
void load()
|
||||||
}
|
}
|
||||||
function prevPage() {
|
function prevPage() {
|
||||||
if (offset.value === 0) return
|
if (offset.value === 0) return
|
||||||
offset.value = Math.max(0, offset.value - PAGE_SIZE)
|
offset.value = Math.max(0, offset.value - PAGE_SIZE)
|
||||||
|
expandedId.value = null
|
||||||
void load()
|
void load()
|
||||||
}
|
}
|
||||||
function nextPage() {
|
function nextPage() {
|
||||||
offset.value += PAGE_SIZE
|
offset.value += PAGE_SIZE
|
||||||
|
expandedId.value = null
|
||||||
void load()
|
void load()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,14 +250,7 @@ onMounted(() => {
|
|||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
/* page-header / btn 等已提取到 global.css 全局 */
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
}
|
|
||||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
|
|
||||||
.header-actions { display: flex; gap: 10px; }
|
|
||||||
|
|
||||||
.page-desc {
|
.page-desc {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -184,6 +258,30 @@ onMounted(() => {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 筛选栏 ===== */
|
||||||
|
.audit-filters { gap: 8px; margin-bottom: 16px; }
|
||||||
|
.audit-filter-select {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 0.5px solid var(--df-border);
|
||||||
|
border-radius: var(--df-radius-sm);
|
||||||
|
background: var(--df-bg);
|
||||||
|
color: var(--df-text);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.audit-filter-select:focus { outline: none; border-color: var(--df-accent); }
|
||||||
|
.audit-search { max-width: 200px; }
|
||||||
|
|
||||||
|
/* ===== 加载/错误状态 ===== */
|
||||||
|
.audit-loading { display: flex; align-items: center; justify-content: center; padding: 40px; }
|
||||||
|
.audit-error { color: var(--df-danger); }
|
||||||
|
.audit-spinner { animation: audit-spin 0.9s linear infinite; }
|
||||||
|
.audit-spinner--lg { color: var(--df-text-dim); }
|
||||||
|
@keyframes audit-spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== 表格 ===== */
|
/* ===== 表格 ===== */
|
||||||
.audit-table-wrap {
|
.audit-table-wrap {
|
||||||
background: var(--df-bg-card);
|
background: var(--df-bg-card);
|
||||||
@@ -215,7 +313,9 @@ onMounted(() => {
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
.audit-table tbody tr:last-child td { border-bottom: none; }
|
.audit-table tbody tr:last-child td { border-bottom: none; }
|
||||||
.audit-table tbody tr:hover td { background: var(--df-sidebar-hover, rgba(255,255,255,0.03)); }
|
.audit-table tbody tr:hover td { background: var(--df-bg-hover, rgba(255,255,255,0.03)); }
|
||||||
|
/* 展开/hover 行高亮由 .audit-row:hover/.expanded 管理,避免选择器冲突 */
|
||||||
|
.audit-table tbody tr.audit-row:hover td { background: var(--df-bg-hover, rgba(255,255,255,0.03)); }
|
||||||
|
|
||||||
.col-time { min-width: 120px; }
|
.col-time { min-width: 120px; }
|
||||||
.col-tool { min-width: 130px; }
|
.col-tool { min-width: 130px; }
|
||||||
@@ -225,14 +325,39 @@ onMounted(() => {
|
|||||||
.col-args { min-width: 200px; }
|
.col-args { min-width: 200px; }
|
||||||
.col-result { min-width: 220px; }
|
.col-result { min-width: 220px; }
|
||||||
|
|
||||||
|
/* 可点击行 */
|
||||||
|
.audit-row { cursor: pointer; transition: background 0.12s; }
|
||||||
|
.audit-row:hover td { background: var(--df-bg-hover, rgba(255,255,255,0.03)); }
|
||||||
|
.audit-row.expanded td { background: var(--df-bg-hover, rgba(255,255,255,0.04)); }
|
||||||
|
|
||||||
|
/* 展开详情行 */
|
||||||
|
.audit-detail-row td { padding: 0 !important; border-bottom: 0.5px solid var(--df-border); }
|
||||||
|
.audit-detail { padding: 12px 16px; display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.detail-section { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.detail-label { font-size: 11px; color: var(--df-text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
|
||||||
|
.detail-pre {
|
||||||
|
margin: 0; padding: 8px 10px;
|
||||||
|
background: var(--df-bg);
|
||||||
|
border: 0.5px solid var(--df-border);
|
||||||
|
border-radius: var(--df-radius-sm);
|
||||||
|
font-family: var(--df-font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--df-text-secondary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.time-rel { font-size: 12px; color: var(--df-text-secondary); }
|
.time-rel { font-size: 12px; color: var(--df-text-secondary); }
|
||||||
.time-abs { font-size: 10px; color: var(--df-text-dim); margin-top: 2px; font-family: var(--df-font-mono); }
|
.time-abs { font-size: 10px; color: var(--df-text-dim); margin-top: 2px; font-family: var(--df-font-mono); }
|
||||||
|
.time-dur { font-size: 10px; color: var(--df-text-dim); margin-top: 2px; font-family: var(--df-font-mono); }
|
||||||
|
|
||||||
.tool-name {
|
.tool-name {
|
||||||
font-family: var(--df-font-mono);
|
font-family: var(--df-font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--df-accent);
|
color: var(--df-accent);
|
||||||
background: var(--df-accent-bg, rgba(123,111,240,0.08));
|
background: var(--df-accent-bg);
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: var(--df-radius-xs);
|
border-radius: var(--df-radius-xs);
|
||||||
}
|
}
|
||||||
@@ -254,8 +379,8 @@ onMounted(() => {
|
|||||||
border-radius: var(--df-radius-xs);
|
border-radius: var(--df-radius-xs);
|
||||||
}
|
}
|
||||||
.risk-low { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
.risk-low { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
||||||
.risk-medium { background: rgba(255,152,0,0.2); color: #ff9800; }
|
.risk-medium { background: var(--df-warning-bg); color: var(--df-warning); }
|
||||||
.risk-high { background: rgba(255,107,107,0.2); color: var(--df-danger); }
|
.risk-high { background: var(--df-danger-bg); color: var(--df-danger); }
|
||||||
|
|
||||||
/* 状态 tag */
|
/* 状态 tag */
|
||||||
.status-tag {
|
.status-tag {
|
||||||
@@ -266,12 +391,12 @@ onMounted(() => {
|
|||||||
border-radius: var(--df-radius-sm);
|
border-radius: var(--df-radius-sm);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.status-pending { background: rgba(255,217,61,0.2); color: var(--df-warning); }
|
.status-pending { background: var(--df-warning-bg); color: var(--df-warning); }
|
||||||
.status-approved { background: rgba(76,175,80,0.2); color: var(--df-success, #4caf50); }
|
.status-approved { background: var(--df-success-bg); color: var(--df-success); }
|
||||||
.status-rejected { background: rgba(255,107,107,0.2); color: var(--df-danger); }
|
.status-rejected { background: var(--df-danger-bg); color: var(--df-danger); }
|
||||||
.status-executing { background: rgba(100,181,246,0.2); color: var(--df-info); }
|
.status-executing { background: var(--df-info-bg); color: var(--df-info); }
|
||||||
.status-completed { background: rgba(76,175,80,0.15); color: var(--df-success, #4caf50); }
|
.status-completed { background: var(--df-success-bg); color: var(--df-success); }
|
||||||
.status-failed { background: rgba(255,107,107,0.2); color: var(--df-danger); }
|
.status-failed { background: var(--df-danger-bg); color: var(--df-danger); }
|
||||||
|
|
||||||
/* 决策者 tag */
|
/* 决策者 tag */
|
||||||
.decided-tag {
|
.decided-tag {
|
||||||
@@ -281,7 +406,7 @@ onMounted(() => {
|
|||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: var(--df-radius-xs);
|
border-radius: var(--df-radius-xs);
|
||||||
}
|
}
|
||||||
.decided-human { background: rgba(123,111,240,0.2); color: var(--df-accent); }
|
.decided-human { background: var(--df-accent-bg); color: var(--df-accent); }
|
||||||
.decided-auto { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
.decided-auto { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
||||||
.decided-none { color: var(--df-text-dim); }
|
.decided-none { color: var(--df-text-dim); }
|
||||||
|
|
||||||
@@ -299,10 +424,5 @@ onMounted(() => {
|
|||||||
font-family: var(--df-font-mono);
|
font-family: var(--df-font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
/* empty-state 已提取到 global.css 全局 */
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--df-text-dim);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,18 +7,19 @@
|
|||||||
<span class="dash-subtitle">{{ $t('dashboard.subtitle') }}</span>
|
<span class="dash-subtitle">{{ $t('dashboard.subtitle') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dash-header-right">
|
<div class="dash-header-right">
|
||||||
<button class="df-btn df-btn--ghost" @click="refresh" :disabled="loading" :title="$t('dashboard.refresh')">
|
<button class="btn btn-ghost btn-sm dash-refresh-btn" @click="refresh" :disabled="loading" :title="$t('dashboard.refresh')">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
|
<svg v-if="loading" class="dash-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg>
|
||||||
|
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="df-btn df-btn--primary" @click="quickCapture">
|
<button class="btn btn-primary btn-sm" @click="quickCapture">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
{{ $t('dashboard.captureIdea') }}
|
{{ $t('dashboard.captureIdea') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- ═══ 错误条:refresh/onMounted 加载失败对用户可见(对齐 Knowledge error-banner) ═══ -->
|
<!-- ═══ 错误条:refresh/onMounted 加载失败对用户可见(样式全局 .error-banner) ═══ -->
|
||||||
<div v-if="errorMsg" class="error-banner">
|
<div v-if="errorMsg" class="error-banner" style="margin-bottom: 14px">
|
||||||
<span class="error-text">{{ errorMsg }}</span>
|
<span class="error-text">{{ errorMsg }}</span>
|
||||||
<button class="error-dismiss" @click="errorMsg = ''">✕</button>
|
<button class="error-dismiss" @click="errorMsg = ''">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,58 +123,12 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
.dash-header-right { display: flex; gap: 6px; align-items: center; }
|
.dash-header-right { display: flex; gap: 6px; align-items: center; }
|
||||||
|
|
||||||
/* — Buttons — */
|
/* 刷新按钮图标旋转动画(加载中反馈) */
|
||||||
.df-btn {
|
.dash-spinner { animation: dash-spin 0.9s linear infinite; }
|
||||||
display: inline-flex;
|
@keyframes dash-spin {
|
||||||
align-items: center;
|
from { transform: rotate(0deg); }
|
||||||
gap: 6px;
|
to { transform: rotate(360deg); }
|
||||||
padding: 9px 14px;
|
|
||||||
line-height: 1;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--df-radius-sm);
|
|
||||||
font-family: var(--df-font-sans);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s var(--df-ease);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.df-btn--primary {
|
|
||||||
background: var(--df-accent);
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
}
|
|
||||||
.df-btn--primary:hover {
|
|
||||||
background: var(--df-accent);
|
|
||||||
filter: brightness(1.1);
|
|
||||||
|
|
||||||
}
|
|
||||||
.df-btn--ghost {
|
|
||||||
background: rgba(255,255,255,0.03);
|
|
||||||
color: var(--df-text-secondary);
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
}
|
|
||||||
.df-btn--ghost:hover {
|
|
||||||
background: rgba(255,255,255,0.06);
|
|
||||||
color: var(--df-text);
|
|
||||||
border-color: var(--df-border-strong);
|
|
||||||
}
|
|
||||||
.df-btn--xs { padding: 4px 10px; font-size: 11px; }
|
|
||||||
.df-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
||||||
|
|
||||||
/* ═══ 错误条(对齐 Knowledge error-banner) ═══ */
|
|
||||||
.error-banner {
|
|
||||||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
||||||
padding: 8px 12px; margin-bottom: 14px;
|
|
||||||
background: rgba(255,107,107,0.12); border: 0.5px solid var(--df-danger);
|
|
||||||
border-radius: var(--df-radius-sm); color: var(--df-danger); font-size: 12px;
|
|
||||||
}
|
|
||||||
.error-text { word-break: break-word; }
|
|
||||||
.error-dismiss {
|
|
||||||
flex-shrink: 0; background: transparent; border: none; color: var(--df-danger);
|
|
||||||
font-size: 14px; cursor: pointer; line-height: 1; padding: 0 2px;
|
|
||||||
}
|
|
||||||
.error-dismiss:hover { opacity: 0.7; }
|
|
||||||
|
|
||||||
/* ═══ Grid Layout ═══ */
|
/* ═══ Grid Layout ═══ */
|
||||||
.dash-grid {
|
.dash-grid {
|
||||||
@@ -183,37 +138,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
.dash-right { display: flex; flex-direction: column; gap: 12px; }
|
.dash-right { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
|
||||||
/* ═══ Panel(子面板 df-panel 共享基类,scoped 下子组件需各自声明同名类样式;
|
|
||||||
此处保留供未来页内直挂面板,子组件已自带同名 .df-panel 样式) ═══ */
|
|
||||||
.df-panel {
|
|
||||||
background: var(--df-bg-card);
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-md);
|
|
||||||
padding: 14px 16px;
|
|
||||||
animation: fadeInUp 0.5s var(--df-ease) both;
|
|
||||||
}
|
|
||||||
.df-panel-head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.df-panel-title {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--df-text);
|
|
||||||
letter-spacing: -0.1px;
|
|
||||||
}
|
|
||||||
.df-link {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--df-accent);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
.df-link:hover { opacity: 1; text-decoration: underline; }
|
|
||||||
|
|
||||||
/* ═══ Responsive ═══ */
|
/* ═══ Responsive ═══ */
|
||||||
|
|
||||||
/* 窄窗口:单栏 */
|
/* 窄窗口:单栏 */
|
||||||
@@ -233,6 +157,5 @@ onMounted(() => {
|
|||||||
@media (min-width: 1600px) {
|
@media (min-width: 1600px) {
|
||||||
.dashboard { padding: 20px 28px 24px; }
|
.dashboard { padding: 20px 28px 24px; }
|
||||||
.dash-grid { grid-template-columns: 1fr 400px; gap: 16px; }
|
.dash-grid { grid-template-columns: 1fr 400px; gap: 16px; }
|
||||||
.df-panel { padding: 16px 18px; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -134,8 +134,8 @@
|
|||||||
<input v-model="newIdeaSource" :placeholder="$t('ideas.sourcePlaceholder')" />
|
<input v-model="newIdeaSource" :placeholder="$t('ideas.sourcePlaceholder')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn-cancel" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button>
|
<button class="btn btn-ghost" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button>
|
||||||
<button class="btn-confirm" :disabled="creating" @click="confirmCapture">
|
<button class="btn btn-primary" :disabled="creating" @click="confirmCapture">
|
||||||
{{ creating ? $t('ideas.creating') : $t('common.confirm') }}
|
{{ creating ? $t('ideas.creating') : $t('common.confirm') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,6 +156,7 @@ import { useProjectStore } from '../stores/project'
|
|||||||
import type { IdeaStatus, IdeaQuery } from '../api/types'
|
import type { IdeaStatus, IdeaQuery } from '../api/types'
|
||||||
import { formatDate } from '../utils/time'
|
import { formatDate } from '../utils/time'
|
||||||
import { stripMd } from '../utils/markdown'
|
import { stripMd } from '../utils/markdown'
|
||||||
|
import { scoreTier } from '../utils/ideaEval'
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||||
import IdeaDetail from '../components/ideas/IdeaDetail.vue'
|
import IdeaDetail from '../components/ideas/IdeaDetail.vue'
|
||||||
import { useConfirm } from '../composables/useConfirm'
|
import { useConfirm } from '../composables/useConfirm'
|
||||||
@@ -286,11 +287,9 @@ const currentIdea = computed(() => {
|
|||||||
|
|
||||||
// B-260615-25:灵感描述 Markdown 渲染已下沉至 IdeaDetail 子组件(共享模块单例渲染器)
|
// B-260615-25:灵感描述 Markdown 渲染已下沉至 IdeaDetail 子组件(共享模块单例渲染器)
|
||||||
|
|
||||||
|
// 评分档位 class(高/中/低 三色标),阈值统一走 scoreTier(单一来源,消除 4 处重复)
|
||||||
function scoreClass(score: number | null) {
|
function scoreClass(score: number | null) {
|
||||||
const s = score ?? 0
|
return 'score-' + scoreTier(score)
|
||||||
if (s >= 80) return 'score-high'
|
|
||||||
if (s >= 60) return 'score-mid'
|
|
||||||
return 'score-low'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date)
|
// formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date)
|
||||||
@@ -418,74 +417,7 @@ watch(() => route.params.id, (id) => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.ideas { padding: 16px 20px 20px; }
|
.ideas { padding: 16px 20px 20px; }
|
||||||
|
|
||||||
.page-header {
|
/* page-header / btn / modal / filter-bar / filter-btn / search-box / empty-state 系列已提取到 global.css 全局 */
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
}
|
|
||||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
|
|
||||||
.header-actions { display: flex; gap: 10px; }
|
|
||||||
|
|
||||||
/* ===== 按钮 ===== */
|
|
||||||
.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); }
|
|
||||||
|
|
||||||
/* ===== 筛选栏 ===== */
|
|
||||||
.filter-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
flex: 1;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-sm);
|
|
||||||
background: var(--df-bg);
|
|
||||||
color: var(--df-text);
|
|
||||||
font-size: 13px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--df-accent);
|
|
||||||
background: var(--df-bg-raised);
|
|
||||||
}
|
|
||||||
.filter-btn {
|
|
||||||
padding: 6px 14px;
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-sm);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--df-text-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
.filter-btn:hover { background: var(--df-bg-card); color: var(--df-text); }
|
|
||||||
.filter-btn.active {
|
|
||||||
background: var(--df-accent);
|
|
||||||
color: #fff;
|
|
||||||
border-color: var(--df-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 排序按钮组(与状态筛选按钮视觉分隔) */
|
/* 排序按钮组(与状态筛选按钮视觉分隔) */
|
||||||
.sort-group {
|
.sort-group {
|
||||||
@@ -547,16 +479,7 @@ watch(() => route.params.id, (id) => {
|
|||||||
}
|
}
|
||||||
.idea-date { font-size: 11px; color: var(--df-text-dim); }
|
.idea-date { font-size: 11px; color: var(--df-text-dim); }
|
||||||
|
|
||||||
/* ===== 状态标签 ===== */
|
/* ===== 状态标签已提取到 styles/components.css 全局(.status-tag 系列) ===== */
|
||||||
.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); }
|
|
||||||
|
|
||||||
/* ===== 右侧详情(详情内容由 IdeaDetail 子组件渲染,父级仅保留未选中空态壳) ===== */
|
/* ===== 右侧详情(详情内容由 IdeaDetail 子组件渲染,父级仅保留未选中空态壳) ===== */
|
||||||
.idea-detail-panel {
|
.idea-detail-panel {
|
||||||
@@ -572,9 +495,7 @@ watch(() => route.params.id, (id) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.empty-state { text-align: center; color: var(--df-text-dim); }
|
/* empty-state / empty-icon 已提取到 global.css 全局 */
|
||||||
.empty-icon { font-size: 48px; margin-bottom: 12px; }
|
|
||||||
.empty-state p { font-size: 14px; }
|
|
||||||
|
|
||||||
/* ===== 响应式 ===== */
|
/* ===== 响应式 ===== */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
@@ -593,13 +514,6 @@ watch(() => route.params.id, (id) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 模态框 ===== */
|
/* ===== 模态框字段样式(modal-overlay/box/field/actions 已提取到 global.css) ===== */
|
||||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
/* Ideas 模态的 select 用 .modal-field select 全局覆盖,无需额外 scoped */
|
||||||
.modal-box { background: var(--df-bg-card); border: 0.5px solid var(--df-border); border-radius: var(--df-radius-lg); padding: 24px; min-width: 360px; max-width: 90vw; }
|
|
||||||
.modal-box h3 { color: var(--df-text); margin-bottom: var(--df-gap-head); }
|
|
||||||
.modal-box input, .modal-box textarea { width: 100%; padding: 8px 12px; border: 0.5px solid var(--df-border); border-radius: var(--df-radius-sm); background: var(--df-bg); color: var(--df-text); font-size: 13px; margin-bottom: 12px; box-sizing: border-box; font-family: inherit; }
|
|
||||||
.modal-box .modal-select { width: 100%; padding: 8px 12px; border: 0.5px solid var(--df-border); border-radius: var(--df-radius-sm); background: var(--df-bg); color: var(--df-text); font-size: 13px; margin-bottom: 12px; box-sizing: border-box; font-family: inherit; }
|
|
||||||
.modal-box .modal-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
|
||||||
.modal-box .btn-cancel { padding: 6px 16px; border: 0.5px solid var(--df-border); border-radius: var(--df-radius-sm); background: transparent; color: var(--df-text-secondary); cursor: pointer; }
|
|
||||||
.modal-box .btn-confirm { padding: 6px 16px; border: none; border-radius: var(--df-radius-sm); background: var(--df-accent); color: #fff; cursor: pointer; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 错误条:消费 store.error(加载/检索/配置失败对用户可见) -->
|
<!-- 错误条:消费 store.error(样式全局 .error-banner,仅间距用页面变量) -->
|
||||||
<div v-if="store.error" class="error-banner">
|
<div v-if="store.error" class="error-banner" style="margin-bottom: var(--df-gap-page)">
|
||||||
<span class="error-text">{{ store.error }}</span>
|
<span class="error-text">{{ store.error }}</span>
|
||||||
<button class="error-dismiss" @click="clearError">✕</button>
|
<button class="error-dismiss" @click="clearError">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -347,26 +347,9 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.knowledge { padding: 16px 20px 20px; height: 100%; display: flex; flex-direction: column; }
|
.knowledge { padding: 16px 20px 20px; height: 100%; display: flex; flex-direction: column; }
|
||||||
|
|
||||||
.page-header {
|
/* page-header / btn / modal 等已提取到 global.css 全局 */
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
}
|
|
||||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
|
|
||||||
.header-actions { display: flex; gap: 8px; }
|
|
||||||
|
|
||||||
/* ===== 错误条(消费 store.error) ===== */
|
/* ===== 错误条样式已提取到 styles/components.css 全局(.error-banner) ===== */
|
||||||
.error-banner {
|
|
||||||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
||||||
padding: 8px 12px; margin-bottom: var(--df-gap-page);
|
|
||||||
background: rgba(255,107,107,0.12); border: 0.5px solid var(--df-danger);
|
|
||||||
border-radius: var(--df-radius-sm); color: var(--df-danger); font-size: 12px;
|
|
||||||
}
|
|
||||||
.error-text { word-break: break-word; }
|
|
||||||
.error-dismiss {
|
|
||||||
flex-shrink: 0; background: transparent; border: none; color: var(--df-danger);
|
|
||||||
font-size: 14px; cursor: pointer; line-height: 1; padding: 0 2px;
|
|
||||||
}
|
|
||||||
.error-dismiss:hover { opacity: 0.7; }
|
|
||||||
|
|
||||||
/* ===== 顶层 Tab ===== */
|
/* ===== 顶层 Tab ===== */
|
||||||
.top-tabs { display: flex; gap: 4px; margin-bottom: var(--df-gap-page); border-bottom: 0.5px solid var(--df-border); }
|
.top-tabs { display: flex; gap: 4px; margin-bottom: var(--df-gap-page); border-bottom: 0.5px solid var(--df-border); }
|
||||||
@@ -446,28 +429,7 @@ onMounted(() => {
|
|||||||
.conf-medium { background: rgba(255,217,61,0.15); color: var(--df-warning); padding: 1px 6px; border-radius: var(--df-radius-xs); }
|
.conf-medium { background: rgba(255,217,61,0.15); color: var(--df-warning); padding: 1px 6px; border-radius: var(--df-radius-xs); }
|
||||||
.conf-low { background: rgba(255,107,107,0.15); color: var(--df-danger); padding: 1px 6px; border-radius: var(--df-radius-xs); }
|
.conf-low { background: rgba(255,107,107,0.15); color: var(--df-danger); padding: 1px 6px; border-radius: var(--df-radius-xs); }
|
||||||
|
|
||||||
/* ===== 按钮 ===== */
|
/* form-row / form-input / form-select / form-textarea 已随新建/编辑弹层随 KnowledgeDetail 子组件迁移,此处仅保留本壳所需特有覆盖。 */
|
||||||
.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); }
|
|
||||||
|
|
||||||
/* ===== 新增对话框 ===== */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed; inset: 0; background: rgba(0,0,0,0.5);
|
|
||||||
display: flex; align-items: center; justify-content: center; z-index: 1000;
|
|
||||||
}
|
|
||||||
.modal-box {
|
|
||||||
background: var(--df-bg-panel, var(--df-bg-card)); border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-lg); padding: 24px; width: 90%; max-width: 520px;
|
|
||||||
}
|
|
||||||
.modal-title { font-size: 16px; font-weight: 500; color: var(--df-text); margin-bottom: 16px; }
|
|
||||||
.form-row { margin-bottom: 12px; }
|
.form-row { margin-bottom: 12px; }
|
||||||
.form-row label { display: block; font-size: 12px; color: var(--df-text-secondary); margin-bottom: 4px; }
|
.form-row label { display: block; font-size: 12px; color: var(--df-text-secondary); margin-bottom: 4px; }
|
||||||
.form-input, .form-select {
|
.form-input, .form-select {
|
||||||
@@ -477,7 +439,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
.form-input:focus, .form-select:focus { border-color: var(--df-accent); }
|
.form-input:focus, .form-select:focus { border-color: var(--df-accent); }
|
||||||
.form-textarea { min-height: 80px; resize: vertical; font-family: inherit; }
|
.form-textarea { min-height: 80px; resize: vertical; font-family: inherit; }
|
||||||
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
||||||
|
|
||||||
/* ===== 响应式:窄屏单列布局(详情窄屏样式随 KnowledgeDetail 子组件迁移) ===== */
|
/* ===== 响应式:窄屏单列布局(详情窄屏样式随 KnowledgeDetail 子组件迁移) ===== */
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
|
|||||||
@@ -559,7 +559,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* 加载/空态(模板顶部使用) */
|
/* 加载/空态(模板顶部使用) */
|
||||||
.loading-state { text-align: center; padding: 40px; color: var(--df-text-dim); }
|
.loading-state { text-align: center; padding: 40px; color: var(--df-text-dim); }
|
||||||
.empty-state { text-align: center; padding: 40px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
/* 覆盖全局 .empty-state:带图标+文字垂直排列,缩短 padding */
|
||||||
|
.empty-state { padding: 40px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||||
|
|
||||||
/* ===== 项目信息样式 ===== */
|
/* ===== 项目信息样式 ===== */
|
||||||
.project-info {
|
.project-info {
|
||||||
@@ -655,39 +656,16 @@ onUnmounted(() => {
|
|||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* assessment badge(复用 IdeaDetail 同名类的样式定义) */
|
/* assessment-badge 系列(基础 + immediate/soon/conditional/revised/defer/cancel)
|
||||||
.assessment-badge {
|
已提取到 styles/components.css 全局 */
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.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); }
|
|
||||||
|
|
||||||
/* 多维评分条(复用 IdeaDetail score-bar-* 样式定义) */
|
/* 多维评分条(.score-bar-* 系列)已提取到 styles/components.css 全局 */
|
||||||
.source-scores {
|
.source-scores {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.score-bar-row { display: flex; align-items: center; gap: 10px; }
|
|
||||||
.score-bar-label { font-size: 12px; color: var(--df-text-secondary); min-width: 64px; 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); }
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -727,16 +705,9 @@ onUnmounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
/* .page-header / .page-header h1 已提取到 styles/global.css 全局 */
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
}
|
|
||||||
.header-left { display: flex; align-items: center; gap: 12px; }
|
.header-left { display: flex; align-items: center; gap: 12px; }
|
||||||
.back-link { font-size: 13px; color: var(--df-accent); text-decoration: none; }
|
/* .back-link / .back-link:hover 已提取到 styles/global.css 全局 */
|
||||||
.back-link:hover { text-decoration: underline; }
|
|
||||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
|
|
||||||
|
|
||||||
.stage-badge {
|
.stage-badge {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -752,24 +723,9 @@ onUnmounted(() => {
|
|||||||
.stage-paused { background: rgba(158,158,158,0.15); color: #9e9e9e; }
|
.stage-paused { background: rgba(158,158,158,0.15); color: #9e9e9e; }
|
||||||
.stage-cancelled { background: rgba(244,67,54,0.15); color: #f44336; }
|
.stage-cancelled { background: rgba(244,67,54,0.15); color: #f44336; }
|
||||||
|
|
||||||
.header-actions { display: flex; gap: 10px; }
|
/* .header-actions 已提取到 styles/global.css 全局 */
|
||||||
|
|
||||||
/* ===== 按钮 ===== */
|
/* .btn 系列(btn/btn-primary/btn-ghost/btn-sm/btn-danger)已提取到 styles/global.css 全局 */
|
||||||
.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 12px; font-size: 12px; }
|
|
||||||
.btn-danger { background: transparent; color: var(--df-danger); border: 0.5px solid rgba(240,101,101,0.4); }
|
|
||||||
.btn-danger:hover { background: rgba(240,101,101,0.12); }
|
|
||||||
|
|
||||||
/* ===== 两栏布局 — 自己滚动,不撑大父级 ===== */
|
/* ===== 两栏布局 — 自己滚动,不撑大父级 ===== */
|
||||||
.detail-grid {
|
.detail-grid {
|
||||||
@@ -856,35 +812,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* ===== 项目信息(左栏 .project-info/.info-item,右栏重复面板已删)===== */
|
/* ===== 项目信息(左栏 .project-info/.info-item,右栏重复面板已删)===== */
|
||||||
|
|
||||||
/* ===== 模态框 ===== */
|
/* .modal-* 系列(modal-overlay/modal-box/modal-title/modal-field/modal-actions)
|
||||||
.modal-overlay {
|
及 .btn:disabled 已提取到 styles/global.css 全局 */
|
||||||
position: fixed; inset: 0;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
.modal-box {
|
|
||||||
background: var(--df-bg-card);
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-lg);
|
|
||||||
padding: 24px;
|
|
||||||
width: 420px;
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
.modal-title { font-size: 18px; font-weight: 500; color: var(--df-text); margin-bottom: 16px; }
|
|
||||||
.modal-field { margin-bottom: 14px; }
|
|
||||||
.modal-field label { display: block; font-size: 12px; color: var(--df-text-dim); margin-bottom: 4px; }
|
|
||||||
.modal-field input, .modal-field textarea {
|
|
||||||
width: 100%; padding: 8px 10px; border-radius: var(--df-radius-sm);
|
|
||||||
border: 0.5px solid var(--df-border); background: var(--df-bg);
|
|
||||||
color: var(--df-text); font-size: 13px; font-family: inherit;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.modal-field input:focus, .modal-field textarea:focus {
|
|
||||||
outline: none; border-color: var(--df-accent);
|
|
||||||
}
|
|
||||||
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 4px; }
|
|
||||||
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
||||||
|
|
||||||
.empty-hint {
|
.empty-hint {
|
||||||
text-align: center; padding: 24px 12px;
|
text-align: center; padding: 24px 12px;
|
||||||
|
|||||||
@@ -432,29 +432,11 @@ async function runImport() {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.projects { padding: 16px 20px 20px; }
|
.projects { padding: 16px 20px 20px; }
|
||||||
|
|
||||||
.page-header {
|
/* page-header / btn / modal 系列已提取到 global.css 全局 */
|
||||||
display: flex;
|
/* 本页特有:.btn-danger(全局尚未提供)、.btn:disabled(全局仅 .btn-primary:disabled) */
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
}
|
|
||||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
|
|
||||||
.header-actions { display: flex; gap: 10px; }
|
|
||||||
|
|
||||||
/* ===== 按钮 ===== */
|
|
||||||
.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-danger { background: transparent; color: var(--df-danger); border: 0.5px solid rgba(240,101,101,0.4); }
|
.btn-danger { background: transparent; color: var(--df-danger); border: 0.5px solid rgba(240,101,101,0.4); }
|
||||||
.btn-danger:hover { background: rgba(240,101,101,0.12); }
|
.btn-danger:hover { background: rgba(240,101,101,0.12); }
|
||||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
|
||||||
/* ===== 卡片网格 ===== */
|
/* ===== 卡片网格 ===== */
|
||||||
.project-grid {
|
.project-grid {
|
||||||
@@ -495,42 +477,10 @@ async function runImport() {
|
|||||||
.scan-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
.scan-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
||||||
|
|
||||||
/* ===== 模态框 ===== */
|
/* ===== 模态框 ===== */
|
||||||
.modal-overlay {
|
/* page-header / btn / modal 系列已提取到 global.css 全局 */
|
||||||
position: fixed; inset: 0;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
.modal-box {
|
|
||||||
background: var(--df-bg-card);
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-lg);
|
|
||||||
padding: 24px;
|
|
||||||
width: 420px;
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
.modal-title { font-size: 18px; font-weight: 500; color: var(--df-text); margin-bottom: 16px; }
|
|
||||||
.modal-field { margin-bottom: 14px; }
|
|
||||||
.modal-field label { display: block; font-size: 12px; color: var(--df-text-dim); margin-bottom: 4px; }
|
|
||||||
.modal-field input, .modal-field textarea {
|
|
||||||
width: 100%; padding: 8px 10px; border-radius: var(--df-radius-sm);
|
|
||||||
border: 0.5px solid var(--df-border); background: var(--df-bg);
|
|
||||||
color: var(--df-text); font-size: 13px; font-family: inherit;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.modal-field input:focus, .modal-field textarea:focus {
|
|
||||||
outline: none; border-color: var(--df-accent);
|
|
||||||
}
|
|
||||||
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 4px; }
|
|
||||||
.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:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
||||||
|
|
||||||
/* ===== 空状态 ===== */
|
/* ===== 空状态 ===== */
|
||||||
.empty-state {
|
/* page-header / btn / modal 系列已提取到 global.css 全局 */
|
||||||
text-align: center; padding: 60px 20px;
|
|
||||||
font-size: 14px; color: var(--df-text-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 错误条(消费 store.error,对齐 Knowledge error-banner) ===== */
|
/* ===== 错误条(消费 store.error,对齐 Knowledge error-banner) ===== */
|
||||||
.error-banner {
|
.error-banner {
|
||||||
|
|||||||
@@ -239,13 +239,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* 确认弹层已替换为 ConfirmDialog 组件(不再需要自建 CSS) */
|
/* 确认弹层已替换为 ConfirmDialog 组件(不再需要自建 CSS) */
|
||||||
|
|
||||||
.page-header {
|
/* page-header / btn 等已提取到 global.css 全局 */
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
}
|
|
||||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
|
|
||||||
|
|
||||||
/* ===== master-detail 布局(阶段3 UX 重构) =====
|
/* ===== master-detail 布局(阶段3 UX 重构) =====
|
||||||
左 SettingsNav(~200px 固定)+ 右 content(flex 占满 + 纵向滚动)。
|
左 SettingsNav(~200px 固定)+ 右 content(flex 占满 + 纵向滚动)。
|
||||||
|
|||||||
@@ -491,37 +491,8 @@ onBeforeUnmount(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.task-detail { padding: 16px 20px 20px; }
|
.task-detail { padding: 16px 20px 20px; }
|
||||||
|
|
||||||
.page-header {
|
/* page-header / btn / back-link 等已提取到 global.css 全局 */
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
}
|
|
||||||
.header-left { display: flex; align-items: center; gap: 12px; }
|
.header-left { display: flex; align-items: center; gap: 12px; }
|
||||||
.back-link { font-size: 13px; color: var(--df-accent); text-decoration: none; white-space: nowrap; }
|
|
||||||
.back-link:hover { text-decoration: underline; }
|
|
||||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
|
|
||||||
|
|
||||||
.header-actions { display: flex; gap: 10px; }
|
|
||||||
|
|
||||||
/* ===== 按钮 ===== */
|
|
||||||
.btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--df-radius-sm);
|
|
||||||
font-size: 13px;
|
|
||||||
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-card); color: var(--df-text); }
|
|
||||||
/* F-05 推进按钮变体(样式跟随 Projects/ProjectDetail/Knowledge 同款 btn-primary/btn-danger/btn-sm) */
|
|
||||||
.btn-primary { background: var(--df-accent); color: #fff; }
|
|
||||||
.btn-primary:hover { background: var(--df-accent-hover); }
|
|
||||||
.btn-danger { background: transparent; color: var(--df-danger); border: 0.5px solid rgba(240,101,101,0.4); }
|
|
||||||
.btn-danger:hover { background: rgba(240,101,101,0.12); }
|
|
||||||
.btn-sm { padding: 5px 12px; font-size: 12px; }
|
|
||||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
||||||
|
|
||||||
/* F-05 推进操作区 */
|
/* F-05 推进操作区 */
|
||||||
.advance-row .advance-actions {
|
.advance-row .advance-actions {
|
||||||
|
|||||||
@@ -124,25 +124,25 @@
|
|||||||
<h3>{{ $t('tasks.modal.title') }}</h3>
|
<h3>{{ $t('tasks.modal.title') }}</h3>
|
||||||
<div class="modal-field">
|
<div class="modal-field">
|
||||||
<label>{{ $t('tasks.modal.project') }}</label>
|
<label>{{ $t('tasks.modal.project') }}</label>
|
||||||
<select v-model="newTaskProjectId" class="modal-input">
|
<select v-model="newTaskProjectId">
|
||||||
<option v-for="p in store.projects" :key="p.id" :value="p.id">{{ p.name }}</option>
|
<option v-for="p in store.projects" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-field">
|
<div class="modal-field">
|
||||||
<label>{{ $t('tasks.modal.titleField') }}</label>
|
<label>{{ $t('tasks.modal.titleField') }}</label>
|
||||||
<input v-model="newTaskTitle" class="modal-input" :placeholder="$t('tasks.modal.titlePlaceholder')" @keyup.enter="confirmCreate" />
|
<input v-model="newTaskTitle" :placeholder="$t('tasks.modal.titlePlaceholder')" @keyup.enter="confirmCreate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-field">
|
<div class="modal-field">
|
||||||
<label>{{ $t('tasks.modal.desc') }}</label>
|
<label>{{ $t('tasks.modal.desc') }}</label>
|
||||||
<input v-model="newTaskDesc" class="modal-input" :placeholder="$t('tasks.modal.descPlaceholder')" />
|
<input v-model="newTaskDesc" :placeholder="$t('tasks.modal.descPlaceholder')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-field">
|
<div class="modal-field">
|
||||||
<label>{{ $t('tasks.modal.branch') }}</label>
|
<label>{{ $t('tasks.modal.branch') }}</label>
|
||||||
<input v-model="newTaskBranch" class="modal-input" :placeholder="$t('tasks.modal.branchPlaceholder')" />
|
<input v-model="newTaskBranch" :placeholder="$t('tasks.modal.branchPlaceholder')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-field">
|
<div class="modal-field">
|
||||||
<label>{{ $t('tasks.modal.priority') }}</label>
|
<label>{{ $t('tasks.modal.priority') }}</label>
|
||||||
<select v-model="newTaskPriority" class="modal-input">
|
<select v-model="newTaskPriority">
|
||||||
<option :value="0">{{ $t('tasks.modal.priorityCritical') }}</option>
|
<option :value="0">{{ $t('tasks.modal.priorityCritical') }}</option>
|
||||||
<option :value="1">{{ $t('tasks.modal.priorityHigh') }}</option>
|
<option :value="1">{{ $t('tasks.modal.priorityHigh') }}</option>
|
||||||
<option :value="2">{{ $t('tasks.modal.priorityMedium') }}</option>
|
<option :value="2">{{ $t('tasks.modal.priorityMedium') }}</option>
|
||||||
@@ -453,15 +453,7 @@ onUnmounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.tasks { padding: 16px 20px 20px; }
|
.tasks { padding: 16px 20px 20px; }
|
||||||
|
|
||||||
.page-header {
|
/* page-header / page-header h1 / header-actions 已提取到全局 global.css */
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); white-space: nowrap; }
|
|
||||||
.header-actions { display: flex; gap: 10px; align-items: center; }
|
|
||||||
|
|
||||||
/* 搜索框 */
|
/* 搜索框 */
|
||||||
.search-input {
|
.search-input {
|
||||||
@@ -478,33 +470,10 @@ onUnmounted(() => {
|
|||||||
.search-input:focus { border-color: var(--df-accent); }
|
.search-input:focus { border-color: var(--df-accent); }
|
||||||
.search-input::placeholder { color: var(--df-text-dim); }
|
.search-input::placeholder { color: var(--df-text-dim); }
|
||||||
|
|
||||||
/* 按钮 */
|
/* btn / btn-primary / btn-ghost / btn-sm 已提取到全局 global.css */
|
||||||
.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 12px; font-size: 12px; }
|
|
||||||
|
|
||||||
/* 筛选栏(紧凑) */
|
/* 筛选栏(紧凑) */
|
||||||
.filter-bar {
|
/* filter-bar 已提取到全局 global.css,下方仅保留 Tasks 特有筛选类 */
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: var(--df-gap-page);
|
|
||||||
padding: 10px 14px;
|
|
||||||
background: var(--df-bg-card);
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-lg);
|
|
||||||
}
|
|
||||||
.filter-group {
|
.filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -702,61 +671,8 @@ onUnmounted(() => {
|
|||||||
.quick-menu-danger { color: var(--df-danger); }
|
.quick-menu-danger { color: var(--df-danger); }
|
||||||
|
|
||||||
/* 空态 */
|
/* 空态 */
|
||||||
.empty-state {
|
/* empty-state 已提取到全局 global.css */
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 48px 0;
|
|
||||||
color: var(--df-text-dim);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.empty-icon { font-size: 32px; opacity: 0.4; }
|
.empty-icon { font-size: 32px; opacity: 0.4; }
|
||||||
|
|
||||||
/* 模态框 */
|
/* 模态框 overlay/box/modal-field/modal-actions 已提取到 global.css 全局 */
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
.modal-box {
|
|
||||||
background: var(--df-bg-card);
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-lg);
|
|
||||||
padding: 20px;
|
|
||||||
min-width: 400px;
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
.modal-box h3 { font-size: 16px; color: var(--df-text); margin-bottom: 16px; }
|
|
||||||
.modal-field {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.modal-field label {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--df-text-secondary);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.modal-input {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-sm);
|
|
||||||
background: var(--df-bg);
|
|
||||||
color: var(--df-text);
|
|
||||||
font-size: 13px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.modal-input:focus { border-color: var(--df-accent); }
|
|
||||||
.modal-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user