Files
DevFlow/src/views/Ideas.vue
绝尘 80b9243528 重构: 前端 composable 拆分+详情页 Markdown 渲染+流式 memo+状态同步+bug 修复
- composables/useMarkdown 抽(AiChat 复用)+ai/* 流式核心+useAiSend catch 回滚 user msg(B-21)+useAiStream 超时文案(B-03)+useAiConversations 切换 token(FR-R1)
- views/TaskDetail 新建(F-02)+描述 md+mdReady 响应式+字段同行布局(B-24/31)
- views/ProjectDetail 多选审批 UI(F-01)+描述 md+mdReady+字段同行(B-25/31)
- views/Ideas+Knowledge 描述 md+mdReady(B-25)
- views/Tasks 项目切换联动(B-29)+task 详情跳转
- components/AiChat toast 失败提示(B-20)+流式块级 memo(ARC-08)+clean UI(AR-7)+stop 兜底(AR-5)
- components/ToolCard 审批卡片可读化(AR-3)
- stores useAiStore barrel(ARC-04)+del settings mock(ARC-01)+approve options(R-PD-5)
- i18n aiTool/nav 中英对称(AR-3/9)+删 nav.decisions 死链(ARC-02)
- router /tasks/:id(F-02)+global.css selection 可见(R-260615-01)+api task detail+App 删 decisions nav
2026-06-15 05:14:56 +08:00

1005 lines
31 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="ideas">
<header class="page-header">
<h1>{{ $t('ideas.title') }}</h1>
<div class="header-actions">
<button class="btn btn-ghost" @click="refresh">{{ $t('ideas.refresh') }}</button>
<button class="btn btn-primary" @click="openCaptureModal">{{ $t('ideas.capture') }}</button>
</div>
</header>
<!-- 搜索和筛选栏 -->
<div class="filter-bar">
<div class="search-box">
<input
v-model="searchQuery"
type="text"
:placeholder="$t('ideas.searchPlaceholder')"
/>
</div>
<button
v-for="f in filters"
:key="f.key"
class="filter-btn"
:class="{ active: activeFilter === f.key }"
@click="activeFilter = f.key"
>
{{ f.icon }} {{ $t(f.labelKey) }}
</button>
</div>
<!-- 两栏布局 -->
<div class="ideas-layout">
<!-- 左侧灵感列表 -->
<section class="idea-list-panel">
<div class="idea-list">
<div
v-for="idea in filteredIdeas"
:key="idea.id"
class="idea-card"
:class="{ selected: selectedId === idea.id }"
@click="selectedId = idea.id"
>
<div class="idea-card-header">
<span class="idea-title">{{ idea.title }}</span>
<span class="idea-score" :class="scoreClass(idea.score)">{{ idea.score ?? '-' }}</span>
</div>
<p class="idea-desc-preview">{{ idea.description?.slice(0, 60) ?? '' }}{{ idea.description && idea.description.length > 60 ? '...' : '' }}</p>
<div class="idea-card-footer">
<span class="status-tag" :class="'status-' + idea.status">{{ $t(statusLabelKey(idea.status)) }}</span>
<span class="idea-date">{{ formatDate(idea.created_at) }}</span>
</div>
</div>
</div>
</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).length > 0">
<span class="tag" v-for="tag in parseTags(currentIdea)" :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"
@click="promoteToProject"
>
{{ $t('ideas.promoteToProject') }}
</button>
<button class="btn btn-ghost" @click="deleteCurrentIdea">{{ $t('ideas.deleteIdea') }}</button>
</div>
</div>
</section>
<!-- 未选择 -->
<section class="idea-detail-panel idea-empty" v-else>
<div class="empty-state">
<div class="empty-icon">💡</div>
<p>{{ $t('ideas.emptyState') }}</p>
</div>
</section>
</div>
<!-- 捕捉灵感模态框 -->
<div class="modal-overlay" v-if="showCaptureModal" @click.self="showCaptureModal = false">
<div class="modal-box">
<h3>{{ $t('ideas.captureTitle') }}</h3>
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldTitle') }}</label>
<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">{{ $t('ideas.fieldDesc') }}</label>
<textarea v-model="newIdeaDesc" :placeholder="$t('ideas.descPlaceholder')" rows="3" style="resize:vertical"></textarea>
<div class="modal-actions">
<button class="btn-cancel" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button>
<button class="btn-confirm" @click="confirmCapture">{{ $t('common.confirm') }}</button>
</div>
</div>
</div>
<!-- 确认弹层删除灵感替代原生 window.confirm -->
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Message } from '@arco-design/web-vue'
import { useProjectStore } from '../stores/project'
import { formatDate } from '../utils/time'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import { useConfirm } from '../composables/useConfirm'
import { useMarkdown } from '../composables/useMarkdown'
import type { IdeaRecord } from '../api/types'
// B-260615-25:灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例)
const { renderMd, loadMarkdown, mdReady } = useMarkdown()
// 描述渲染依赖 mdReady 响应式:loadMarkdown 完成前 escapeFallback 纯文本,完成后 mdReady 翻转
// 触发 computed 重算(B-25 漏响应式致 v-html=renderMd 首次纯文本后不重算,代码块不渲染)
const renderedDesc = computed(() => {
void mdReady.value
return renderMd(currentIdea.value?.description ?? '')
})
const { t } = useI18n()
const router = useRouter()
const store = useProjectStore()
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
type FilterKey = 'all' | 'hot' | 'pending' | 'promoted'
const activeFilter = ref<FilterKey>('all')
const selectedId = ref<string | null>(null)
const searchQuery = ref('')
// ── 新建灵感模态框 ──
const showCaptureModal = ref(false)
const newIdeaTitle = ref('')
const newIdeaDesc = ref('')
const filters: { key: FilterKey; labelKey: string; icon: string }[] = [
{ key: 'all', labelKey: 'ideas.filter.all', icon: '📋' },
{ key: 'hot', labelKey: 'ideas.filter.hot', icon: '🔥' },
{ key: 'pending', labelKey: 'ideas.filter.pending', 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(() => {
let ideas = store.ideas
// 应用状态过滤
switch (activeFilter.value) {
case 'hot': ideas = ideas.filter(i => (i.score ?? 0) >= 80); break
case 'pending': ideas = ideas.filter(i => i.status === 'pending_review' || i.status === 'draft'); break
case 'promoted': ideas = ideas.filter(i => i.status === 'promoted'); break
default: break // 不过滤
}
// 应用搜索过滤
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase().trim()
ideas = ideas.filter(i =>
i.title.toLowerCase().includes(query) ||
(i.description && i.description.toLowerCase().includes(query)) ||
(i.tags && JSON.parse(i.tags || '[]').some((tag: string) => tag.toLowerCase().includes(query)))
)
}
return ideas
})
const currentIdea = computed(() => {
if (!selectedId.value) return null
return store.ideas.find(i => i.id === selectedId.value) ?? null
})
const currentStatus = computed(() => {
return currentIdea.value?.status || 'draft'
})
function parseTags(idea: IdeaRecord): string[] {
if (!idea.tags) return []
try {
const parsed = JSON.parse(idea.tags)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
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'
}
function scoreClass(score: number | null) {
const s = score ?? 0
if (s >= 80) return 'score-high'
if (s >= 60) return 'score-mid'
return 'score-low'
}
// formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date
function openCaptureModal() {
newIdeaTitle.value = ''
newIdeaDesc.value = ''
showCaptureModal.value = true
}
async function confirmCapture() {
if (!newIdeaTitle.value.trim()) return
await store.createIdea({
title: newIdeaTitle.value.trim(),
description: newIdeaDesc.value.trim() || undefined,
})
showCaptureModal.value = false
}
async function deleteCurrentIdea() {
if (!currentIdea.value) return
if (!await confirmDialog(t('ideas.confirmDelete', { title: currentIdea.value.title }))) return
await store.deleteIdea(currentIdea.value.id)
selectedId.value = null
}
async function promoteToProject() {
if (!currentIdea.value) return
try {
const res = await store.promoteIdea(currentIdea.value.id)
router.push(`/projects/${res.project_id}`)
} catch (e: any) {
const msg = e?.toString() ?? t('ideas.promoteFailed')
console.error(t('ideas.promoteFailed'), e)
Message.error(msg)
}
}
async function refresh() {
await store.loadIdeas()
}
const evaluating = ref(false)
const evalError = ref('')
async function evaluateCurrentIdea() {
if (!currentIdea.value || evaluating.value) return
evaluating.value = true
evalError.value = ''
try {
await store.evaluateIdea(currentIdea.value.id)
} catch (e: any) {
evalError.value = e?.toString() ?? t('ideas.evalFailed')
console.error(t('ideas.evalFailed'), e)
} finally {
evaluating.value = false
}
}
async function onStatusChange(e: Event) {
if (!currentIdea.value) return
const newStatus = (e.target as HTMLSelectElement).value
await store.updateIdea(currentIdea.value.id, 'status', newStatus)
}
onMounted(async () => {
loadMarkdown() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
await store.loadIdeas()
})
</script>
<style scoped>
.ideas { padding: 16px 20px 20px; }
.page-header {
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);
}
/* ===== 两栏布局 ===== */
.ideas-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: var(--df-gap-grid);
}
/* ===== 左侧列表 ===== */
.idea-list-panel {
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-lg);
padding: 6px;
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.idea-card {
padding: 14px;
border-radius: var(--df-radius);
cursor: pointer;
transition: all 0.15s;
margin-bottom: 4px;
}
.idea-card:hover { background: rgba(108, 99, 255, 0.06); }
.idea-card.selected { background: rgba(108, 99, 255, 0.12); border: 0.5px solid var(--df-accent); }
.idea-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.idea-title { font-size: 14px; font-weight: 500; color: var(--df-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.idea-score {
font-size: 13px; font-weight: 500;
min-width: 32px; height: 26px;
display: flex; align-items: center; justify-content: center;
border-radius: var(--df-radius-sm);
}
.score-high { background: rgba(100,255,218,0.15); color: var(--df-success); }
.score-mid { background: rgba(255,217,61,0.15); color: var(--df-warning); }
.score-low { background: rgba(255,107,107,0.15); color: var(--df-danger); }
.idea-desc-preview { font-size: 12px; color: var(--df-text-dim); margin-bottom: 8px; line-height: 1.4; }
.idea-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.idea-date { font-size: 11px; color: var(--df-text-dim); }
/* ===== 状态标签 ===== */
.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); }
.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;
}
/* ===== 右侧详情 ===== */
.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;
}
.idea-empty {
display: flex;
align-items: center;
justify-content: center;
}
.empty-state { text-align: center; color: var(--df-text-dim); }
.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,样式同 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 {
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; }
.ideas-layout {
grid-template-columns: 1fr;
}
.filter-bar {
flex-wrap: wrap;
}
.search-box {
max-width: 100%;
order: -1;
width: 100%;
margin-bottom: 8px;
}
}
/* ===== 模态框 ===== */
.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: 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-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>