重构: 拆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:
573
src/components/ideas/IdeaDetail.vue
Normal file
573
src/components/ideas/IdeaDetail.vue
Normal 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>
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user