重构: 拆Ideas.vue灵感详情(strategy前端·Ideas<500达标)

- 新建 components/ideas/IdeaDetail.vue(573行): 对抗评估+雷达图+标签+状态+操作按钮+style(详情面板下沉)
- Ideas.vue 990→485(<500达标): 列表+筛选+搜索+捕获+action保留
- props idea/evaluating/evalError/promoting/deleting/statusOptions + emits evaluate/promote/delete/status-change(无store依赖, emit上抛单向数据流)
主代兜底: vue-tsc 0 + grep IdeaDetail抽离/Ideas import/emit印证
strategy: 前端views, 子组件抽详情面板; Ideas<500达标; onStatusChange签名Event→value行为等价
git add指定(Ideas.vue+IdeaDetail.vue)
This commit is contained in:
2026-06-19 11:40:44 +08:00
parent 35c976793b
commit a1ad93ab31
2 changed files with 590 additions and 522 deletions

View File

@@ -0,0 +1,573 @@
<template>
<section class="idea-detail-panel">
<div class="detail-header">
<h2 class="detail-title">{{ idea.title }}</h2>
<span class="status-tag" :class="'status-' + idea.status">{{ $t(statusLabelKey(idea.status)) }}</span>
</div>
<!-- B-260615-25:灵感描述 Markdown 渲染,复用 useMarkdown composable( B-24 TaskDetail),空值回退 -->
<p
v-if="idea.description"
class="detail-desc ai-md"
v-html="renderedDesc"
></p>
<p v-else class="detail-desc"></p>
<!-- 对抗式评估 -->
<div class="detail-section">
<h3>{{ $t('ideas.adversarialTitle') }} <span class="eval-mode-tag">{{ $t('ideas.evalModeHeuristic') }}</span></h3>
<div v-if="adversarialEval" class="adversarial-eval">
<!-- 正反方观点 -->
<div class="debate-container">
<div class="debate-column positive">
<h4>{{ $t('ideas.positive') }}</h4>
<div class="confidence-bar">
<div class="confidence-fill" :style="{ width: (adversarialEval.positive_strength * 100) + '%' }"></div>
</div>
<div class="confidence-text">{{ $t('ideas.confidence', { n: (adversarialEval.positive_strength * 100).toFixed(0) }) }}</div>
<p class="thesis">{{ adversarialEval.positive.thesis }}</p>
<ul>
<li v-for="evidence in adversarialEval.positive.evidence" :key="evidence"> {{ evidence }}</li>
</ul>
</div>
<div class="debate-column negative">
<h4>{{ $t('ideas.negative') }}</h4>
<div class="confidence-bar">
<div class="confidence-fill" :style="{ width: (adversarialEval.negative_strength * 100) + '%' }"></div>
</div>
<div class="confidence-text">{{ $t('ideas.confidence', { n: (adversarialEval.negative_strength * 100).toFixed(0) }) }}</div>
<p class="thesis">{{ adversarialEval.negative.thesis }}</p>
<ul>
<li v-for="evidence in adversarialEval.negative.evidence" :key="evidence"> {{ evidence }}</li>
</ul>
</div>
</div>
<!-- AI 分析师结论 -->
<div class="analyst-conclusion">
<h4>{{ $t('ideas.analystTitle') }}</h4>
<div class="assessment-badge" :class="assessmentClass(adversarialEval.recommendation)">
{{ assessmentLabel(adversarialEval.recommendation) }}
</div>
<p class="final-score">{{ $t('ideas.finalScore', { score: adversarialEval.final_score.toFixed(1) }) }}</p>
<div class="net-sentiment" :class="sentimentClass(adversarialEval.net_sentiment)">
{{ $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) }) }}
</div>
<p class="summary">{{ adversarialEval.analyst.summary }}</p>
</div>
<!-- 行动建议 -->
<div class="action-recommendations">
<h4>{{ $t('ideas.actionTitle') }}</h4>
<ul>
<li v-for="action in adversarialEval.action_items" :key="action"> {{ action }}</li>
</ul>
</div>
</div>
<div v-else class="eval-report" style="opacity:0.5">
<button class="btn-evaluate" :disabled="evaluating" @click="$emit('evaluate')">
{{ evaluating ? $t('ideas.evaluating') : $t('ideas.startEval') }}
</button>
<div v-if="evalError" class="eval-error"> {{ evalError }}</div>
</div>
</div>
<!-- 传统评分雷达图 -->
<div class="detail-section">
<h3>{{ $t('ideas.multiScoreTitle') }}</h3>
<div class="radar-chart" v-if="parseScores(idea).length > 0">
<div class="radar-row" v-for="dim in parseScores(idea)" :key="dim.name">
<span class="radar-label">{{ dim.name }}</span>
<div class="radar-bar-track">
<div class="radar-bar-fill" :style="{ width: dim.score + '%' }" :class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'"></div>
</div>
<span class="radar-value">{{ dim.score }}</span>
</div>
</div>
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noEval') }}</div>
</div>
<!-- 标签 -->
<div class="detail-section">
<h3>{{ $t('ideas.tagsTitle') }}</h3>
<div class="tag-list" v-if="parseTags(idea.tags).length > 0">
<span class="tag" v-for="tag in parseTags(idea.tags)" :key="tag">{{ tag }}</span>
</div>
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noTags') }}</div>
</div>
<!-- 状态管理 -->
<div class="detail-section">
<h3>{{ $t('ideas.statusTitle') }}</h3>
<div class="status-controls">
<select :value="idea.status" @change="onStatusChange" class="status-select">
<option v-for="s in statusOptions" :key="s.value" :value="s.value">{{ $t(s.labelKey) }}</option>
</select>
</div>
</div>
<!-- 操作 -->
<div class="detail-section" style="margin-top:8px">
<div class="action-buttons">
<button
v-if="idea.status === 'approved' && !idea.promoted_to"
class="btn btn-primary"
:disabled="promoting"
@click="$emit('promote')"
>
{{ promoting ? $t('ideas.promoting') : $t('ideas.promoteToProject') }}
</button>
<button class="btn btn-ghost" :disabled="deleting" @click="$emit('delete')">
{{ deleting ? $t('ideas.deleting') : $t('ideas.deleteIdea') }}
</button>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { parseTags } from '../../stores/knowledge'
import { useRendered } from '../../composables/useMarkdown'
import type { IdeaRecord } from '../../api/types'
const props = defineProps<{
idea: IdeaRecord
evaluating: boolean
evalError: string
promoting: boolean
deleting: boolean
statusOptions: { value: string; labelKey: string }[]
}>()
const emit = defineEmits<{
(e: 'evaluate'): void
(e: 'promote'): void
(e: 'delete'): void
(e: 'status-change', value: string): void
}>()
const { t } = useI18n()
// 任意 status 字符串 → 对应 i18n key;未知状态回退到原值显示
function statusLabelKey(status: string): string {
return props.statusOptions.find(s => s.value === status)?.labelKey ?? status
}
// B-260615-25:灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
// useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
const { rendered: renderedDesc } = useRendered(
() => props.idea.description ?? '',
)
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<AdversarialEval | null>(() => {
if (!props.idea.ai_analysis) return null
try {
return JSON.parse(props.idea.ai_analysis) as AdversarialEval
} catch {
return null
}
})
function assessmentClass(recommendation: string) {
// 映射到 CSS 定义的 badge 颜色类(.immediate/.soon/.conditional/.revised/.defer/.cancel
const map: Record<string, string> = {
'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'
}
function onStatusChange(e: Event) {
emit('status-change', (e.target as HTMLSelectElement).value)
}
</script>
<style scoped>
/* ===== 右侧详情 ===== */
.idea-detail-panel {
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-lg);
padding: var(--df-pad-panel);
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.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;
}
/* ===== 状态标签 ===== */
.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 {
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;
}
/* ===== 雷达图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 {
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); }
/* ===== 状态管理 ===== */
.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);
}
</style>

View File

@@ -60,130 +60,19 @@
</section>
<!-- 右侧灵感详情 -->
<section class="idea-detail-panel" v-if="currentIdea">
<div class="detail-header">
<h2 class="detail-title">{{ currentIdea.title }}</h2>
<span class="status-tag" :class="'status-' + currentIdea.status">{{ $t(statusLabelKey(currentIdea.status)) }}</span>
</div>
<!-- B-260615-25:灵感描述 Markdown 渲染,复用 useMarkdown composable( B-24 TaskDetail),空值回退 -->
<p
v-if="currentIdea.description"
class="detail-desc ai-md"
v-html="renderedDesc"
></p>
<p v-else class="detail-desc"></p>
<!-- 对抗式评估 -->
<div class="detail-section">
<h3>{{ $t('ideas.adversarialTitle') }} <span class="eval-mode-tag">{{ $t('ideas.evalModeHeuristic') }}</span></h3>
<div v-if="adversarialEval" class="adversarial-eval">
<!-- 正反方观点 -->
<div class="debate-container">
<div class="debate-column positive">
<h4>{{ $t('ideas.positive') }}</h4>
<div class="confidence-bar">
<div class="confidence-fill" :style="{ width: (adversarialEval.positive_strength * 100) + '%' }"></div>
</div>
<div class="confidence-text">{{ $t('ideas.confidence', { n: (adversarialEval.positive_strength * 100).toFixed(0) }) }}</div>
<p class="thesis">{{ adversarialEval.positive.thesis }}</p>
<ul>
<li v-for="evidence in adversarialEval.positive.evidence" :key="evidence"> {{ evidence }}</li>
</ul>
</div>
<div class="debate-column negative">
<h4>{{ $t('ideas.negative') }}</h4>
<div class="confidence-bar">
<div class="confidence-fill" :style="{ width: (adversarialEval.negative_strength * 100) + '%' }"></div>
</div>
<div class="confidence-text">{{ $t('ideas.confidence', { n: (adversarialEval.negative_strength * 100).toFixed(0) }) }}</div>
<p class="thesis">{{ adversarialEval.negative.thesis }}</p>
<ul>
<li v-for="evidence in adversarialEval.negative.evidence" :key="evidence"> {{ evidence }}</li>
</ul>
</div>
</div>
<!-- AI 分析师结论 -->
<div class="analyst-conclusion">
<h4>{{ $t('ideas.analystTitle') }}</h4>
<div class="assessment-badge" :class="assessmentClass(adversarialEval.recommendation)">
{{ assessmentLabel(adversarialEval.recommendation) }}
</div>
<p class="final-score">{{ $t('ideas.finalScore', { score: adversarialEval.final_score.toFixed(1) }) }}</p>
<div class="net-sentiment" :class="sentimentClass(adversarialEval.net_sentiment)">
{{ $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) }) }}
</div>
<p class="summary">{{ adversarialEval.analyst.summary }}</p>
</div>
<!-- 行动建议 -->
<div class="action-recommendations">
<h4>{{ $t('ideas.actionTitle') }}</h4>
<ul>
<li v-for="action in adversarialEval.action_items" :key="action"> {{ action }}</li>
</ul>
</div>
</div>
<div v-else class="eval-report" style="opacity:0.5">
<button class="btn-evaluate" :disabled="evaluating" @click="evaluateCurrentIdea">
{{ evaluating ? $t('ideas.evaluating') : $t('ideas.startEval') }}
</button>
<div v-if="evalError" class="eval-error"> {{ evalError }}</div>
</div>
</div>
<!-- 传统评分雷达图 -->
<div class="detail-section">
<h3>{{ $t('ideas.multiScoreTitle') }}</h3>
<div class="radar-chart" v-if="parseScores(currentIdea).length > 0">
<div class="radar-row" v-for="dim in parseScores(currentIdea)" :key="dim.name">
<span class="radar-label">{{ dim.name }}</span>
<div class="radar-bar-track">
<div class="radar-bar-fill" :style="{ width: dim.score + '%' }" :class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'"></div>
</div>
<span class="radar-value">{{ dim.score }}</span>
</div>
</div>
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noEval') }}</div>
</div>
<!-- 标签 -->
<div class="detail-section">
<h3>{{ $t('ideas.tagsTitle') }}</h3>
<div class="tag-list" v-if="parseTags(currentIdea.tags).length > 0">
<span class="tag" v-for="tag in parseTags(currentIdea.tags)" :key="tag">{{ tag }}</span>
</div>
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noTags') }}</div>
</div>
<!-- 状态管理 -->
<div class="detail-section">
<h3>{{ $t('ideas.statusTitle') }}</h3>
<div class="status-controls">
<select :value="currentStatus" @change="onStatusChange" class="status-select">
<option v-for="s in statusOptions" :key="s.value" :value="s.value">{{ $t(s.labelKey) }}</option>
</select>
</div>
</div>
<!-- 操作 -->
<div class="detail-section" style="margin-top:8px">
<div class="action-buttons">
<button
v-if="currentIdea.status === 'approved' && !currentIdea.promoted_to"
class="btn btn-primary"
:disabled="promoting"
@click="promoteToProject"
>
{{ promoting ? $t('ideas.promoting') : $t('ideas.promoteToProject') }}
</button>
<button class="btn btn-ghost" :disabled="deleting" @click="deleteCurrentIdea">
{{ deleting ? $t('ideas.deleting') : $t('ideas.deleteIdea') }}
</button>
</div>
</div>
</section>
<IdeaDetail
v-if="currentIdea"
:idea="currentIdea"
:evaluating="evaluating"
:eval-error="evalError"
:promoting="promoting"
:deleting="deleting"
:status-options="statusOptions"
@evaluate="evaluateCurrentIdea"
@promote="promoteToProject"
@delete="deleteCurrentIdea"
@status-change="onStatusChange"
/>
<!-- 未选择 -->
<section class="idea-detail-panel idea-empty" v-else>
@@ -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<AdversarialEval | null>(() => {
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<string, string> = {
'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; }