修复: 2196c77 workflow整文件替换回退恢复(AiChat+Ideas 12项功能+头像位置)

根因: 批次1 workflow(2196c77)做 @/ 路径统一时对 AiChat.vue/Ideas.vue
做了整文件替换(非手术式编辑),把 6254d06 之后积累的功能改动全部抹掉。

AiChat.vue 恢复 12 项功能:
- row-reverse 用户头像右侧布局
- groupedActive 对话按今天/昨天/更早分组
- archivedConvs 归档分组(可折叠)
- confirmClearChat 清空对话二次确认
- showBackToBottom 回到底部按钮
- queue UI 生成中消息排队可视化
- 技能联想(/ 触发浮层)
- token-usage 展示
- .ai-conv-group/item-actions/back-to-bottom/queue/skill 样式组
- onBeforeUnmount 补 stopListener() (CR-24 回归修复)

Ideas.vue 恢复:
- i18n 全量($t() 33处)
- useConfirm + ConfirmDialog
- useRendered/useMarkdown 描述渲染
- 过滤器 + 状态选项 + 晋升逻辑

vue-tsc 0 err / cargo check 0 err
This commit is contained in:
2026-06-15 17:15:19 +08:00
parent 2196c7748f
commit b880a976c2
2 changed files with 931 additions and 749 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="ideas"> <div class="ideas">
<header class="page-header"> <header class="page-header">
<h1>💡 灵感</h1> <h1>{{ $t('ideas.title') }}</h1>
<div class="header-actions"> <div class="header-actions">
<button class="btn btn-ghost" @click="refresh">🔄 刷新</button> <button class="btn btn-ghost" @click="refresh">{{ $t('ideas.refresh') }}</button>
<button class="btn btn-primary" @click="openCaptureModal"> 捕捉灵感</button> <button class="btn btn-primary" @click="openCaptureModal">{{ $t('ideas.capture') }}</button>
</div> </div>
</header> </header>
@@ -14,8 +14,7 @@
<input <input
v-model="searchQuery" v-model="searchQuery"
type="text" type="text"
placeholder="搜索灵感..." :placeholder="$t('ideas.searchPlaceholder')"
@input="filterIdeas"
/> />
</div> </div>
<button <button
@@ -25,13 +24,13 @@
:class="{ active: activeFilter === f.key }" :class="{ active: activeFilter === f.key }"
@click="activeFilter = f.key" @click="activeFilter = f.key"
> >
{{ f.icon }} {{ f.label }} {{ f.icon }} {{ $t(f.labelKey) }}
</button> </button>
</div> </div>
<!-- 两栏布局 --> <!-- 两栏布局 -->
<div class="ideas-layout"> <div class="ideas-layout">
<!-- 左侧想法列表 --> <!-- 左侧灵感列表 -->
<section class="idea-list-panel"> <section class="idea-list-panel">
<div class="idea-list"> <div class="idea-list">
<div <div
@@ -47,31 +46,39 @@
</div> </div>
<p class="idea-desc-preview">{{ idea.description?.slice(0, 60) ?? '' }}{{ idea.description && idea.description.length > 60 ? '...' : '' }}</p> <p class="idea-desc-preview">{{ idea.description?.slice(0, 60) ?? '' }}{{ idea.description && idea.description.length > 60 ? '...' : '' }}</p>
<div class="idea-card-footer"> <div class="idea-card-footer">
<span class="status-tag" :class="'status-' + idea.status">{{ statusLabel(idea.status) }}</span> <span class="status-tag" :class="'status-' + idea.status">{{ $t(statusLabelKey(idea.status)) }}</span>
<span class="idea-date">{{ formatDate(idea.created_at) }}</span> <span class="idea-date">{{ formatDate(idea.created_at) }}</span>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- 右侧想法详情 --> <!-- 右侧灵感详情 -->
<section class="idea-detail-panel" v-if="currentIdea"> <section class="idea-detail-panel" v-if="currentIdea">
<div class="detail-header"> <div class="detail-header">
<h2 class="detail-title">{{ currentIdea.title }}</h2> <h2 class="detail-title">{{ currentIdea.title }}</h2>
<span class="status-tag" :class="'status-' + currentIdea.status">{{ statusLabel(currentIdea.status) }}</span> <span class="status-tag" :class="'status-' + currentIdea.status">{{ $t(statusLabelKey(currentIdea.status)) }}</span>
</div> </div>
<p class="detail-desc">{{ currentIdea.description }}</p> <!-- 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"> <div class="detail-section">
<h3> 对抗式评估</h3> <h3>{{ $t('ideas.adversarialTitle') }} <span class="eval-mode-tag">{{ $t('ideas.evalModeHeuristic') }}</span></h3>
<div v-if="adversarialEval" class="adversarial-eval"> <div v-if="adversarialEval" class="adversarial-eval">
<!-- 正反方观点 --> <!-- 正反方观点 -->
<div class="debate-container"> <div class="debate-container">
<div class="debate-column positive"> <div class="debate-column positive">
<h4>📈 正方观点</h4> <h4>{{ $t('ideas.positive') }}</h4>
<div class="confidence-bar" :style="{ width: (adversarialEval.positive_strength * 100) + '%' }"></div> <div class="confidence-bar">
<div class="confidence-text">{{ (adversarialEval.positive_strength * 100).toFixed(0) }}% 置信度</div> <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> <p class="thesis">{{ adversarialEval.positive.thesis }}</p>
<ul> <ul>
<li v-for="evidence in adversarialEval.positive.evidence" :key="evidence"> {{ evidence }}</li> <li v-for="evidence in adversarialEval.positive.evidence" :key="evidence"> {{ evidence }}</li>
@@ -79,9 +86,11 @@
</div> </div>
<div class="debate-column negative"> <div class="debate-column negative">
<h4>📉 反方观点</h4> <h4>{{ $t('ideas.negative') }}</h4>
<div class="confidence-bar" :style="{ width: (adversarialEval.negative_strength * 100) + '%' }"></div> <div class="confidence-bar">
<div class="confidence-text">{{ (adversarialEval.negative_strength * 100).toFixed(0) }}% 置信度</div> <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> <p class="thesis">{{ adversarialEval.negative.thesis }}</p>
<ul> <ul>
<li v-for="evidence in adversarialEval.negative.evidence" :key="evidence"> {{ evidence }}</li> <li v-for="evidence in adversarialEval.negative.evidence" :key="evidence"> {{ evidence }}</li>
@@ -91,34 +100,36 @@
<!-- AI 分析师结论 --> <!-- AI 分析师结论 -->
<div class="analyst-conclusion"> <div class="analyst-conclusion">
<h4>🧠 AI 分析师结论</h4> <h4>{{ $t('ideas.analystTitle') }}</h4>
<div class="assessment-badge" :class="assessmentClass(adversarialEval.recommendation)"> <div class="assessment-badge" :class="assessmentClass(adversarialEval.recommendation)">
{{ assessmentLabel(adversarialEval.recommendation) }} {{ assessmentLabel(adversarialEval.recommendation) }}
</div> </div>
<p class="final-score">综合评分: {{ adversarialEval.final_score.toFixed(1) }}/10</p> <p class="final-score">{{ $t('ideas.finalScore', { score: adversarialEval.final_score.toFixed(1) }) }}</p>
<div class="net-sentiment" :class="sentimentClass(adversarialEval.net_sentiment)"> <div class="net-sentiment" :class="sentimentClass(adversarialEval.net_sentiment)">
整体倾向: {{ adversarialEval.net_sentiment > 0 ? '积极' : '谨慎' }} {{ $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.net_sentiment * 100).toFixed(0) }})
</div> </div>
<p class="summary">{{ adversarialEval.analyst.summary }}</p> <p class="summary">{{ adversarialEval.analyst.summary }}</p>
</div> </div>
<!-- 行动建议 --> <!-- 行动建议 -->
<div class="action-recommendations"> <div class="action-recommendations">
<h4>💡 行动建议</h4> <h4>{{ $t('ideas.actionTitle') }}</h4>
<ul> <ul>
<li v-for="action in adversarialEval.action_items" :key="action"> {{ action }}</li> <li v-for="action in adversarialEval.action_items" :key="action"> {{ action }}</li>
</ul> </ul>
</div> </div>
</div> </div>
<div v-else class="eval-report" style="opacity:0.5"> <div v-else class="eval-report" style="opacity:0.5">
<button class="btn-evaluate" @click="evaluateCurrentIdea">🔍 开始对抗评估</button> <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> </div>
<!-- 传统评分雷达图 --> <!-- 传统评分雷达图 -->
<div class="detail-section"> <div class="detail-section">
<h3>📊 多维评分</h3> <h3>{{ $t('ideas.multiScoreTitle') }}</h3>
<div class="radar-chart" v-if="parseScores(currentIdea).length > 0"> <div class="radar-chart" v-if="parseScores(currentIdea).length > 0">
<div class="radar-row" v-for="dim in parseScores(currentIdea)" :key="dim.name"> <div class="radar-row" v-for="dim in parseScores(currentIdea)" :key="dim.name">
<span class="radar-label">{{ dim.name }}</span> <span class="radar-label">{{ dim.name }}</span>
@@ -128,28 +139,24 @@
<span class="radar-value">{{ dim.score }}</span> <span class="radar-value">{{ dim.score }}</span>
</div> </div>
</div> </div>
<div v-else class="eval-report" style="opacity:0.5">暂无评估</div> <div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noEval') }}</div>
</div> </div>
<!-- 标签 --> <!-- 标签 -->
<div class="detail-section"> <div class="detail-section">
<h3>🏷 标签</h3> <h3>{{ $t('ideas.tagsTitle') }}</h3>
<div class="tag-list" v-if="parseTags(currentIdea).length > 0"> <div class="tag-list" v-if="parseTags(currentIdea).length > 0">
<span class="tag" v-for="tag in parseTags(currentIdea)" :key="tag">{{ tag }}</span> <span class="tag" v-for="tag in parseTags(currentIdea)" :key="tag">{{ tag }}</span>
</div> </div>
<div v-else class="eval-report" style="opacity:0.5">暂无标签</div> <div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noTags') }}</div>
</div> </div>
<!-- 状态管理 --> <!-- 状态管理 -->
<div class="detail-section"> <div class="detail-section">
<h3>📋 状态管理</h3> <h3>{{ $t('ideas.statusTitle') }}</h3>
<div class="status-controls"> <div class="status-controls">
<select v-model="currentStatus" @change="updateIdeaStatus" class="status-select"> <select :value="currentStatus" @change="onStatusChange" class="status-select">
<option value="draft">📝 草稿</option> <option v-for="s in statusOptions" :key="s.value" :value="s.value">{{ $t(s.labelKey) }}</option>
<option value="pending_review"> 待评估</option>
<option value="approved"> 已批准</option>
<option value="promoted">🚀 已立项</option>
<option value="rejected"> 已拒绝</option>
</select> </select>
</div> </div>
</div> </div>
@@ -162,9 +169,9 @@
class="btn btn-primary" class="btn btn-primary"
@click="promoteToProject" @click="promoteToProject"
> >
🚀 立项为项目 {{ $t('ideas.promoteToProject') }}
</button> </button>
<button class="btn btn-ghost" @click="deleteCurrentIdea">🗑 删除想法</button> <button class="btn btn-ghost" @click="deleteCurrentIdea">{{ $t('ideas.deleteIdea') }}</button>
</div> </div>
</div> </div>
</section> </section>
@@ -173,57 +180,82 @@
<section class="idea-detail-panel idea-empty" v-else> <section class="idea-detail-panel idea-empty" v-else>
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon">💡</div> <div class="empty-icon">💡</div>
<p>选择一个想法查看详情</p> <p>{{ $t('ideas.emptyState') }}</p>
</div> </div>
</section> </section>
</div> </div>
<!-- 捕捉想法模态框 --> <!-- 捕捉灵感模态框 -->
<div class="modal-overlay" v-if="showCaptureModal" @click.self="showCaptureModal = false"> <div class="modal-overlay" v-if="showCaptureModal" @click.self="showCaptureModal = false">
<div class="modal-box"> <div class="modal-box">
<h3> 捕捉新想法</h3> <h3>{{ $t('ideas.captureTitle') }}</h3>
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">标题</label> <label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldTitle') }}</label>
<input v-model="newIdeaTitle" placeholder="一句话描述你的想法..." @keyup.enter="confirmCapture" /> <input v-model="newIdeaTitle" :placeholder="$t('ideas.titlePlaceholder')" @keyup.enter="confirmCapture" />
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">描述</label> <label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldDesc') }}</label>
<textarea v-model="newIdeaDesc" placeholder="详细说明(可选)..." rows="3" style="resize:vertical"></textarea> <textarea v-model="newIdeaDesc" :placeholder="$t('ideas.descPlaceholder')" rows="3" style="resize:vertical"></textarea>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn-cancel" @click="showCaptureModal = false">取消</button> <button class="btn-cancel" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button>
<button class="btn-confirm" @click="confirmCapture">确认</button> <button class="btn-confirm" @click="confirmCapture">{{ $t('common.confirm') }}</button>
</div> </div>
</div> </div>
</div> </div>
<!-- 确认弹层删除灵感替代原生 window.confirm -->
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Message } from '@arco-design/web-vue'
import { useProjectStore } from '../stores/project' import { useProjectStore } from '../stores/project'
import { formatDate } from '../utils/time' import { formatDate } from '../utils/time'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import { useConfirm } from '../composables/useConfirm'
import { useRendered } from '../composables/useMarkdown'
import type { IdeaRecord } from '../api/types' import type { IdeaRecord } from '../api/types'
const { t } = useI18n()
const router = useRouter() const router = useRouter()
const route = useRoute()
const store = useProjectStore() const store = useProjectStore()
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
type FilterKey = 'all' | 'hot' | 'pending' | 'promoted' type FilterKey = 'all' | 'hot' | 'pending' | 'promoted'
const activeFilter = ref<FilterKey>('all') const activeFilter = ref<FilterKey>('all')
const selectedId = ref<string | null>(null) const selectedId = ref<string | null>(null)
const searchQuery = ref('') const searchQuery = ref('')
// ── 新建想法模态框 ── // ── 新建灵感模态框 ──
const showCaptureModal = ref(false) const showCaptureModal = ref(false)
const newIdeaTitle = ref('') const newIdeaTitle = ref('')
const newIdeaDesc = ref('') const newIdeaDesc = ref('')
const filters: { key: FilterKey; label: string; icon: string }[] = [ const filters: { key: FilterKey; labelKey: string; icon: string }[] = [
{ key: 'all', label: '全部', icon: '📋' }, { key: 'all', labelKey: 'ideas.filter.all', icon: '📋' },
{ key: 'hot', label: '热门', icon: '🔥' }, { key: 'hot', labelKey: 'ideas.filter.hot', icon: '🔥' },
{ key: 'pending', label: '待评估', icon: '⏳' }, { key: 'pending', labelKey: 'ideas.filter.pending', icon: '⏳' },
{ key: 'promoted', label: '已立项', icon: '🚀' }, { key: 'promoted', labelKey: 'ideas.filter.promoted', icon: '🚀' },
] ]
// ── 状态映射(单一数据源,列表/详情/下拉共用) ──
const statusOptions: { value: string; labelKey: string }[] = [
{ value: 'draft', labelKey: 'ideas.status.draft' },
{ value: 'pending_review', labelKey: 'ideas.status.pending_review' },
{ value: 'approved', labelKey: 'ideas.status.approved' },
{ value: 'promoted', labelKey: 'ideas.status.promoted' },
{ value: 'rejected', labelKey: 'ideas.status.rejected' },
]
// 任意 status 字符串 → 对应 i18n key;未知状态回退到原值显示
function statusLabelKey(status: string): string {
return statusOptions.find(s => s.value === status)?.labelKey ?? status
}
const filteredIdeas = computed(() => { const filteredIdeas = computed(() => {
let ideas = store.ideas let ideas = store.ideas
@@ -248,15 +280,17 @@ const filteredIdeas = computed(() => {
return ideas return ideas
}) })
function filterIdeas() {
// filteredIdeas 是 computed会自动响应变化
}
const currentIdea = computed(() => { const currentIdea = computed(() => {
if (!selectedId.value) return null if (!selectedId.value) return null
return store.ideas.find(i => i.id === selectedId.value) ?? null 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(() => { const currentStatus = computed(() => {
return currentIdea.value?.status || 'draft' return currentIdea.value?.status || 'draft'
}) })
@@ -314,23 +348,30 @@ const adversarialEval = computed<AdversarialEval | null>(() => {
}) })
function assessmentClass(recommendation: string) { function assessmentClass(recommendation: string) {
return recommendation.toLowerCase().replace(/ /g, '-') // 映射到 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) { function assessmentLabel(recommendation: string) {
const map: Record<string, string> = { const key = `ideas.assessment.${recommendation}`
'immediate action': '🚀 立即行动', // 未命中 i18n key 时回退到原始 recommendation 字符串
'soon': '📅 尽快行动', const translated = t(key)
'with resources': '📦 配置资源后行动', return translated === key ? recommendation : translated
'research more': '🔍 需要更多研究',
'monitor': '👁️ 持续监控',
'cancel': '❌ 取消想法'
}
return map[recommendation] ?? recommendation
} }
function sentimentClass(sentiment: number) { function sentimentClass(sentiment: number) {
return sentiment >= 0 ? 'positive' : 'negative' // 统一三档(FR-C2: 原 >=0 与模板 >0 矛盾net_sentiment=0 时文案负面样式 positive)
if (sentiment > 0) return 'positive'
if (sentiment < 0) return 'negative'
return 'neutral'
} }
function scoreClass(score: number | null) { function scoreClass(score: number | null) {
@@ -340,17 +381,6 @@ function scoreClass(score: number | null) {
return 'score-low' return 'score-low'
} }
function statusLabel(status: string) {
const map: Record<string, string> = {
draft: '📝 草稿',
pending_review: '⏳ 待评估',
approved: '✅ 已批准',
promoted: '🚀 已立项',
rejected: '❌ 已拒绝',
}
return map[status] ?? status
}
// formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date // formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date
function openCaptureModal() { function openCaptureModal() {
@@ -370,104 +400,54 @@ async function confirmCapture() {
async function deleteCurrentIdea() { async function deleteCurrentIdea() {
if (!currentIdea.value) return if (!currentIdea.value) return
if (!await confirmDialog(t('ideas.confirmDelete', { title: currentIdea.value.title }))) return
await store.deleteIdea(currentIdea.value.id) await store.deleteIdea(currentIdea.value.id)
selectedId.value = null selectedId.value = null
} }
async function promoteToProject() { async function promoteToProject() {
if (!currentIdea.value) return if (!currentIdea.value) return
try {
// 创建新项目基于想法store.createProject 第 3 参 = idea_id const res = await store.promoteIdea(currentIdea.value.id)
const project = await store.createProject( router.push(`/projects/${res.project_id}`)
currentIdea.value.title, } catch (e: any) {
currentIdea.value.description, const msg = e?.toString() ?? t('ideas.promoteFailed')
currentIdea.value.id, console.error(t('ideas.promoteFailed'), e)
) Message.error(msg)
if (!project) return }
const projectId = project.id
// 更新想法状态和晋升信息store.updateIdea 单字段,分两次调用)
await store.updateIdea(currentIdea.value.id, 'status', 'promoted')
await store.updateIdea(currentIdea.value.id, 'promoted_to', projectId)
// 跳转到项目详情
router.push(`/projects/${projectId}`)
// 刷新想法列表
await store.loadIdeas()
} }
async function refresh() { async function refresh() {
await store.loadIdeas() await store.loadIdeas()
} }
const evaluating = ref(false)
const evalError = ref('')
async function evaluateCurrentIdea() { async function evaluateCurrentIdea() {
if (!currentIdea.value) return if (!currentIdea.value || evaluating.value) return
// 模拟对抗式评估(实际应该调用后端 API evaluating.value = true
// 这里使用模拟数据展示界面 evalError.value = ''
const mockEval: AdversarialEval = { try {
positive_strength: 0.75, await store.evaluateIdea(currentIdea.value.id)
negative_strength: 0.65, } catch (e: any) {
net_sentiment: 0.1, evalError.value = e?.toString() ?? t('ideas.evalFailed')
recommendation: 'with resources', console.error(t('ideas.evalFailed'), e)
final_score: 6.5, } finally {
summary: '该想法整体价值评估中等偏上,建议在有条件的情况下执行。主要价值在于技术创新性较强,需要关注风险控制和资源投入。', evaluating.value = false
action_items: [
'确认资源预算',
'评估ROI',
'制定风险预案'
],
positive: {
thesis: '技术创新性强,潜在回报高',
evidence: ['技术栈成熟', '市场需求明确', '团队有相关经验'],
},
negative: {
thesis: '资源投入大,存在执行风险',
evidence: ['开发周期长', '需要额外人力', '竞品已有类似方案'],
},
analyst: {
summary: '综合正反方观点,建议在资源到位后启动,并设立阶段性验收点控制风险。',
},
} }
// 更新想法的评估结果(实际应该调用 API
await store.updateIdea(currentIdea.value.id, 'ai_analysis', JSON.stringify(mockEval, null, 2))
} }
async function updateIdeaStatus() { async function onStatusChange(e: Event) {
if (!currentIdea.value || !currentStatus.value) return if (!currentIdea.value) return
const newStatus = (e.target as HTMLSelectElement).value
await store.updateIdea(currentIdea.value.id, 'status', currentStatus.value) await store.updateIdea(currentIdea.value.id, 'status', newStatus)
} }
onMounted(async () => { onMounted(async () => {
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
await store.loadIdeas() await store.loadIdeas()
// 支持从 /ideas/:id 路由直接打开指定灵感
const id = route.params.id as string | undefined
if (id) {
const exists = store.ideas.some(i => i.id === id)
if (exists) {
selectedId.value = id
} else {
console.warn(`[Ideas] 路由指定的灵感 id=${id} 不在当前列表中`)
}
}
})
// 支持在同组件内切换 /ideas/:id如从灵感来源链接跳转
watch(() => route.params.id, (newId) => {
const id = newId as string | undefined
if (id) {
const exists = store.ideas.some(i => i.id === id)
if (exists) {
selectedId.value = id
} else {
console.warn(`[Ideas] 路由切换的灵感 id=${id} 不在当前列表中`)
}
} else {
// 回到 /ideas无 id时不清空保留用户选择
}
}) })
</script> </script>
@@ -656,25 +636,14 @@ watch(() => route.params.id, (newId) => {
overflow: hidden; overflow: hidden;
} }
.debate-column.positive .confidence-bar::after { .confidence-fill {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%; height: 100%;
background: currentColor;
border-radius: var(--df-radius-xs); border-radius: var(--df-radius-xs);
transition: width 0.4s;
} }
.debate-column.negative .confidence-bar::after { .debate-column.positive .confidence-fill { background: var(--df-success); }
content: ''; .debate-column.negative .confidence-fill { background: var(--df-danger); }
position: absolute;
left: 0;
top: 0;
height: 100%;
background: currentColor;
border-radius: var(--df-radius-xs);
}
.confidence-text { .confidence-text {
font-size: 11px; font-size: 11px;
@@ -792,6 +761,23 @@ watch(() => route.params.id, (newId) => {
background: var(--df-accent-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;
}
/* ===== 右侧详情 ===== */ /* ===== 右侧详情 ===== */
.idea-detail-panel { .idea-detail-panel {
background: var(--df-bg-card); background: var(--df-bg-card);
@@ -825,6 +811,63 @@ watch(() => route.params.id, (newId) => {
margin-bottom: var(--df-gap-page); margin-bottom: var(--df-gap-page);
} }
/* ===== 灵感描述 Markdown 渲染(B-260615-25,样式同 TaskDetail B-24 .ai-md 收敛) ===== */
.detail-desc.ai-md { white-space: normal; color: var(--df-text-secondary); }
.detail-desc.ai-md :deep(p) { margin: 0 0 6px; }
.detail-desc.ai-md :deep(p:last-child) { margin-bottom: 0; }
.detail-desc.ai-md :deep(ul), .detail-desc.ai-md :deep(ol) { margin: 4px 0; padding-left: 20px; }
.detail-desc.ai-md :deep(li) { margin: 2px 0; line-height: 1.5; }
.detail-desc.ai-md :deep(code) {
font-family: var(--df-font-mono);
font-size: 12px;
padding: 1px 5px;
background: rgba(255,255,255,0.06);
border-radius: var(--df-radius-sm);
color: var(--df-accent);
}
.detail-desc.ai-md :deep(pre) {
margin: 8px 0;
padding: 10px 12px;
background: var(--df-bg);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius);
overflow-x: auto;
}
.detail-desc.ai-md :deep(pre code) {
padding: 0;
background: transparent;
border-radius: 0;
color: var(--df-text-secondary);
font-size: 12px;
line-height: 1.5;
}
.detail-desc.ai-md :deep(blockquote) {
margin: 6px 0;
padding: 4px 12px;
border-left: 2px solid var(--df-accent);
color: var(--df-text-secondary);
}
.detail-desc.ai-md :deep(h1), .detail-desc.ai-md :deep(h2), .detail-desc.ai-md :deep(h3) {
font-weight: 500;
color: var(--df-text);
margin: 8px 0 4px;
}
.detail-desc.ai-md :deep(h1) { font-size: 16px; }
.detail-desc.ai-md :deep(h2) { font-size: 14px; }
.detail-desc.ai-md :deep(h3) { font-size: 13px; }
.detail-desc.ai-md :deep(a) { color: var(--df-accent); text-decoration: none; }
.detail-desc.ai-md :deep(a:hover) { text-decoration: underline; }
.detail-desc.ai-md :deep(strong) { font-weight: 500; color: var(--df-text); }
.detail-desc.ai-md :deep(hr) { border: none; border-top: 0.5px solid var(--df-border); margin: 8px 0; }
.detail-desc.ai-md :deep(table) {
width: 100%; border-collapse: collapse; margin: 6px 0;
font-size: 12px; overflow-x: auto; display: block;
}
.detail-desc.ai-md :deep(th), .detail-desc.ai-md :deep(td) {
padding: 4px 8px; border: 0.5px solid var(--df-border); text-align: left;
}
.detail-desc.ai-md :deep(th) { font-weight: 500; background: var(--df-bg); }
.detail-section { .detail-section {
margin-bottom: var(--df-gap-page); margin-bottom: var(--df-gap-page);
} }