diff --git a/src/components/ideas/IdeaDetail.vue b/src/components/ideas/IdeaDetail.vue new file mode 100644 index 0000000..52d55fc --- /dev/null +++ b/src/components/ideas/IdeaDetail.vue @@ -0,0 +1,573 @@ + + + + + diff --git a/src/views/Ideas.vue b/src/views/Ideas.vue index 0ff203e..b2b27f2 100644 --- a/src/views/Ideas.vue +++ b/src/views/Ideas.vue @@ -60,130 +60,19 @@ -
-
-

{{ currentIdea.title }}

- {{ $t(statusLabelKey(currentIdea.status)) }} -
- -

-

- - -
-

{{ $t('ideas.adversarialTitle') }} {{ $t('ideas.evalModeHeuristic') }}

-
- -
-
-

{{ $t('ideas.positive') }}

-
-
-
-
{{ $t('ideas.confidence', { n: (adversarialEval.positive_strength * 100).toFixed(0) }) }}
-

{{ adversarialEval.positive.thesis }}

-
    -
  • • {{ evidence }}
  • -
-
- -
-

{{ $t('ideas.negative') }}

-
-
-
-
{{ $t('ideas.confidence', { n: (adversarialEval.negative_strength * 100).toFixed(0) }) }}
-

{{ adversarialEval.negative.thesis }}

-
    -
  • • {{ evidence }}
  • -
-
-
- - -
-

{{ $t('ideas.analystTitle') }}

-
- {{ assessmentLabel(adversarialEval.recommendation) }} -
-

{{ $t('ideas.finalScore', { score: adversarialEval.final_score.toFixed(1) }) }}

-
- {{ $t('ideas.netSentiment', { tone: adversarialEval.net_sentiment > 0 ? $t('ideas.sentimentPositive') : (adversarialEval.net_sentiment < 0 ? $t('ideas.sentimentNegative') : $t('ideas.sentimentNeutral')), n: (adversarialEval.net_sentiment * 100).toFixed(0) }) }} -
-

{{ adversarialEval.analyst.summary }}

-
- - -
-

{{ $t('ideas.actionTitle') }}

-
    -
  • • {{ action }}
  • -
-
-
-
- -
⚠️ {{ evalError }}
-
-
- - -
-

{{ $t('ideas.multiScoreTitle') }}

-
-
- {{ dim.name }} -
-
-
- {{ dim.score }} -
-
-
{{ $t('ideas.noEval') }}
-
- - -
-

{{ $t('ideas.tagsTitle') }}

-
- {{ tag }} -
-
{{ $t('ideas.noTags') }}
-
- - -
-

{{ $t('ideas.statusTitle') }}

-
- -
-
- - -
-
- - -
-
-
+
@@ -226,9 +115,8 @@ import { parseTags } from '../stores/knowledge' import { formatDate } from '../utils/time' import { stripMd } from '../utils/markdown' import ConfirmDialog from '../components/ConfirmDialog.vue' +import IdeaDetail from '../components/ideas/IdeaDetail.vue' import { useConfirm } from '../composables/useConfirm' -import { useRendered } from '../composables/useMarkdown' -import type { IdeaRecord } from '../api/types' const { t } = useI18n() const router = useRouter() @@ -304,84 +192,7 @@ const currentIdea = computed(() => { return store.ideas.find(i => i.id === selectedId.value) ?? null }) -// B-260615-25:灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例), -// useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热) -const { rendered: renderedDesc, ensureLoaded } = useRendered( - () => currentIdea.value?.description ?? '', -) - -const currentStatus = computed(() => { - return currentIdea.value?.status || 'draft' -}) - -interface ScoreDimension { name: string; score: number } - -function parseScores(idea: IdeaRecord): ScoreDimension[] { - if (!idea.scores) return [] - try { - const parsed = JSON.parse(idea.scores) - if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { - return Object.entries(parsed).map(([name, score]) => ({ - name, - score: typeof score === 'number' ? score : 0, - })) - } - return [] - } catch { - return [] - } -} - -interface AdversarialEval { - positive_strength: number - negative_strength: number - net_sentiment: number - recommendation: string - final_score: number - summary: string - action_items: string[] - positive: { thesis: string; evidence: string[] } - negative: { thesis: string; evidence: string[] } - analyst: { summary: string } -} - -// 对抗式评估数据持久化在 ai_analysis JSON 字段中,前端解析渲染 -const adversarialEval = computed(() => { - const idea = currentIdea.value - if (!idea?.ai_analysis) return null - try { - return JSON.parse(idea.ai_analysis) as AdversarialEval - } catch { - return null - } -}) - -function assessmentClass(recommendation: string) { - // 映射到 CSS 定义的 badge 颜色类(.immediate/.soon/.conditional/.revised/.defer/.cancel) - const map: Record = { - 'immediate action': 'immediate', - 'soon': 'soon', - 'with resources': 'conditional', - 'research more': 'revised', - 'monitor': 'defer', - 'cancel': 'cancel', - } - return map[recommendation.toLowerCase()] ?? 'conditional' -} - -function assessmentLabel(recommendation: string) { - const key = `ideas.assessment.${recommendation}` - // 未命中 i18n key 时回退到原始 recommendation 字符串 - const translated = t(key) - return translated === key ? recommendation : translated -} - -function sentimentClass(sentiment: number) { - // 统一三档(FR-C2: 原 >=0 与模板 >0 矛盾,net_sentiment=0 时文案负面样式 positive) - if (sentiment > 0) return 'positive' - if (sentiment < 0) return 'negative' - return 'neutral' -} +// B-260615-25:灵感描述 Markdown 渲染已下沉至 IdeaDetail 子组件(共享模块单例渲染器) function scoreClass(score: number | null) { const s = score ?? 0 @@ -461,14 +272,12 @@ async function evaluateCurrentIdea() { } } -async function onStatusChange(e: Event) { +async function onStatusChange(newStatus: string) { if (!currentIdea.value) return - const newStatus = (e.target as HTMLSelectElement).value await store.updateIdea(currentIdea.value.id, 'status', newStatus) } onMounted(async () => { - ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞 await store.loadIdeas() // 从路由参数恢复选中灵感(修复 /ideas/:id 直接访问空白页 B-260615-36) if (route.params.id) { @@ -630,199 +439,7 @@ watch(() => route.params.id, (id) => { .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 { - background: var(--df-bg-raised); - border: 0.5px solid var(--df-border); - border-radius: var(--df-radius); - padding: 1rem; - margin-bottom: 1rem; -} - -.debate-container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - margin-bottom: 1.5rem; -} - -.debate-column { - padding: 1rem; - border-radius: var(--df-radius); - position: relative; -} - -.debate-column.positive { - background: rgba(61, 219, 160, 0.08); - border: 0.5px solid rgba(61, 219, 160, 0.2); -} - -.debate-column.negative { - background: rgba(240, 101, 101, 0.08); - border: 0.5px solid rgba(240, 101, 101, 0.2); -} - -.debate-column h4 { - font-size: 14px; - font-weight: 500; - margin: 0 0 0.5rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.confidence-bar { - height: 4px; - background: rgba(255, 255, 255, 0.1); - border-radius: var(--df-radius-xs); - margin-bottom: 0.25rem; - overflow: hidden; -} - -.confidence-fill { - height: 100%; - border-radius: var(--df-radius-xs); - transition: width 0.4s; -} - -.debate-column.positive .confidence-fill { background: var(--df-success); } -.debate-column.negative .confidence-fill { background: var(--df-danger); } - -.confidence-text { - font-size: 11px; - color: var(--df-text-dim); - margin-bottom: 0.5rem; -} - -.debate-column .thesis { - font-size: 12px; - color: var(--df-text); - margin-bottom: 0.5rem; - font-weight: 500; -} - -.debate-column ul { - list-style: none; - padding: 0; - margin: 0; -} - -.debate-column li { - font-size: 11px; - color: var(--df-text-secondary); - margin-bottom: 0.25rem; - padding-left: 0.5rem; -} - -.analyst-conclusion { - background: var(--df-bg-card); - border: 0.5px solid var(--df-border); - border-radius: var(--df-radius); - padding: 1rem; - margin-bottom: 1rem; -} - -.assessment-badge { - display: inline-block; - padding: 4px 12px; - border-radius: 20px; - font-size: 11px; - font-weight: 500; - margin: 0.5rem 0; -} - -.assessment-badge.immediate { background: var(--df-success-bg); color: var(--df-success); } -.assessment-badge.soon { background: var(--df-info-bg); color: var(--df-info); } -.assessment-badge.conditional { background: var(--df-warning-bg); color: var(--df-warning); } -.assessment-badge.revised { background: rgba(255, 217, 61, 0.2); color: var(--df-warning); } -.assessment-badge.defer { background: var(--df-danger-bg); color: var(--df-danger); } -.assessment-badge.cancel { background: var(--df-danger-bg); color: var(--df-danger); } - -.final-score { - font-size: 14px; - font-weight: 500; - color: var(--df-text); - margin: 0.5rem 0; -} - -.net-sentiment { - font-size: 12px; - padding: 4px 8px; - border-radius: var(--df-radius-xs); - display: inline-block; - margin: 0.5rem 0; -} - -.net-sentiment.positive { background: rgba(61, 219, 160, 0.15); color: var(--df-success); } -.net-sentiment.negative { background: rgba(240, 101, 101, 0.15); color: var(--df-danger); } -.net-sentiment.neutral { background: var(--df-bg-raised); color: var(--df-text-secondary); } - -.summary { - font-size: 12px; - color: var(--df-text-secondary); - line-height: 1.5; - margin: 0.5rem 0; -} - -.action-recommendations { - background: var(--df-bg-raised); - border: 0.5px solid var(--df-border); - border-radius: var(--df-radius); - padding: 1rem; -} - -.action-recommendations h4 { - font-size: 14px; - font-weight: 500; - margin: 0 0 0.5rem; -} - -.action-recommendations ul { - list-style: none; - padding: 0; - margin: 0; -} - -.action-recommendations li { - font-size: 12px; - color: var(--df-text-secondary); - margin-bottom: 0.25rem; - padding-left: 0.5rem; -} - -.btn-evaluate { - background: var(--df-accent); - color: #fff; - border: none; - padding: 8px 16px; - border-radius: var(--df-radius); - font-size: 13px; - cursor: pointer; - transition: all 0.15s; -} - -.btn-evaluate:hover { - background: var(--df-accent-hover); -} - -.eval-error { - margin-top: 8px; - font-size: 12px; - color: var(--df-danger); -} - -.eval-mode-tag { - font-size: 11px; - font-weight: 400; - padding: 1px 8px; - border-radius: var(--df-radius-xs); - background: rgba(255, 217, 61, 0.15); - color: var(--df-warning); - margin-left: 6px; - vertical-align: middle; -} - -/* ===== 右侧详情 ===== */ +/* ===== 右侧详情(详情内容由 IdeaDetail 子组件渲染,父级仅保留未选中空态壳) ===== */ .idea-detail-panel { background: var(--df-bg-card); border: 0.5px solid var(--df-border); @@ -840,128 +457,6 @@ watch(() => route.params.id, (id) => { .empty-icon { font-size: 48px; margin-bottom: 12px; } .empty-state p { font-size: 14px; } -.detail-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--df-gap-head); -} -.detail-title { font-size: 20px; font-weight: 500; color: var(--df-text); } - -.detail-desc { - font-size: 14px; - color: var(--df-text-secondary); - line-height: 1.6; - margin-bottom: var(--df-gap-page); -} - -/* ===== 灵感描述 Markdown 渲染(B-260615-25,基础样式收敛至全局 ai-md.css) ===== */ -.detail-desc.ai-md { white-space: normal; color: var(--df-text-secondary); } - -.detail-section { - margin-bottom: var(--df-gap-page); -} -.detail-section h3 { - font-size: 14px; - font-weight: 500; - color: var(--df-text); - margin-bottom: 12px; -} - -.eval-report { - font-size: 13px; - color: var(--df-text-secondary); - line-height: 1.6; - padding: 14px 16px; - background: rgba(108, 99, 255, 0.06); - border-left: 3px solid var(--df-accent); - border-radius: 0 8px 8px 0; -} - -/* ===== 雷达图(div 模拟) ===== */ -.radar-chart { - display: flex; - flex-direction: column; - gap: 10px; -} -.radar-row { - display: flex; - align-items: center; - gap: 12px; -} -.radar-label { - font-size: 12px; - color: var(--df-text-secondary); - min-width: 72px; - text-align: right; -} -.radar-bar-track { - flex: 1; - height: 8px; - background: var(--df-border); - border-radius: var(--df-radius-xs); - overflow: hidden; -} -.radar-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); } -.radar-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 { - font-size: 12px; padding: 4px 10px; - border-radius: var(--df-radius-xs); - background: rgba(108, 99, 255, 0.1); - color: var(--df-accent); -} - -/* ===== 删除按钮 ===== */ -.btn-delete { - padding: 6px 14px; - border: 0.5px solid rgba(255,107,107,0.3); - border-radius: var(--df-radius-sm); - background: transparent; - color: var(--df-danger); - font-size: 12px; - cursor: pointer; - transition: all 0.15s; -} -.btn-delete:hover { background: rgba(255,107,107,0.1); } - -/* ===== 状态管理 ===== */ -.status-controls { - margin-top: 8px; -} - -.status-select { - width: 100%; - 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: 13px; - cursor: pointer; -} - -.status-select:focus { - outline: none; - border-color: var(--df-accent); - background: var(--df-bg-raised); -} - /* ===== 响应式 ===== */ @media (max-width: 900px) { .ideas { padding: 16px; }