新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理
后端: - 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展 - 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通 - 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦 - AI planner/plan_hint/intent:aichat B 路线并行多轮基础 - patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环 - 压缩超时兜底(F-15 卡死根治) - F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本 - 知识注入 DRY/skills/audit 扩展 清理: - aichat 技术债(误报 allow/死导入/过时注释 30 项) - URGENT.md 删除(11 项加急全解决/迁 todo) - 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
This commit is contained in:
@@ -5,16 +5,26 @@
|
||||
<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>
|
||||
<template v-if="editing">
|
||||
<textarea v-model="editDesc" class="detail-desc-edit" rows="4"></textarea>
|
||||
<div class="desc-edit-actions">
|
||||
<button class="btn btn-primary btn-sm" @click="saveEdit">{{ $t('ideas.saveDesc') }}</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="cancelEdit">{{ $t('ideas.cancelEdit') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p
|
||||
v-if="idea.description"
|
||||
class="detail-desc ai-md"
|
||||
v-html="renderedDesc"
|
||||
></p>
|
||||
<p v-else class="detail-desc">—</p>
|
||||
<button class="btn btn-ghost btn-sm desc-edit-btn" @click="startEdit">{{ $t('ideas.editDesc') }}</button>
|
||||
</template>
|
||||
|
||||
<!-- 对抗式评估 -->
|
||||
<div class="detail-section">
|
||||
<h3>{{ $t('ideas.adversarialTitle') }} <span class="eval-mode-tag">{{ $t('ideas.evalModeHeuristic') }}</span></h3>
|
||||
<h3>{{ $t('ideas.adversarialTitle') }} <span class="eval-mode-tag">{{ $t(evalModeLabelKey()) }}</span></h3>
|
||||
<div v-if="adversarialEval" class="adversarial-eval">
|
||||
<!-- 正反方观点 -->
|
||||
<div class="debate-container">
|
||||
@@ -47,7 +57,7 @@
|
||||
<div class="analyst-conclusion">
|
||||
<h4>{{ $t('ideas.analystTitle') }}</h4>
|
||||
<div class="assessment-badge" :class="assessmentClass(adversarialEval.recommendation)">
|
||||
{{ assessmentLabel(adversarialEval.recommendation) }}
|
||||
{{ localAssessmentLabel(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)">
|
||||
@@ -68,15 +78,95 @@
|
||||
<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 v-if="evalError" class="eval-error">
|
||||
⚠️ {{ evalError }}
|
||||
<button class="btn-evaluate btn-retry" :disabled="evaluating" @click="$emit('evaluate')">
|
||||
{{ $t('ideas.retryEval') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评估历史(版本时间线) -->
|
||||
<div class="detail-section">
|
||||
<h3>{{ $t('ideas.historyTitle') }}</h3>
|
||||
<div v-if="historyLoading" class="eval-report" style="opacity:0.5">{{ $t('ideas.historyLoading') }}</div>
|
||||
<div v-else-if="evalHistory.length === 0" class="eval-report" style="opacity:0.5">{{ $t('ideas.historyEmpty') }}</div>
|
||||
<div v-else class="eval-history-list">
|
||||
<div
|
||||
v-for="rec in evalHistory"
|
||||
:key="rec.id"
|
||||
class="eval-history-item"
|
||||
:class="{ expanded: expandedVersion === rec.version }"
|
||||
@click="toggleVersion(rec.version)"
|
||||
>
|
||||
<div class="history-row-main">
|
||||
<span class="version-badge">{{ $t('ideas.historyVersion') }} v{{ rec.version }}</span>
|
||||
<span v-if="rec.version === latestVersion" class="latest-tag">{{ $t('ideas.historyLatest') }}</span>
|
||||
<span class="history-eval-mode">{{ $t(evalModeLabelKeyFromStr(rec.evaluated_by)) }}</span>
|
||||
<span class="history-score">{{ rec.score == null ? '—' : rec.score.toFixed(1) }}</span>
|
||||
<span class="history-date">{{ formatDate(rec.evaluated_at) }}</span>
|
||||
</div>
|
||||
<div v-if="expandedVersion === rec.version" class="history-detail">
|
||||
<template v-if="historySummary(rec.ai_analysis)">
|
||||
<p class="history-summary">{{ historySummary(rec.ai_analysis)?.summary ?? '—' }}</p>
|
||||
<p v-if="historySummary(rec.ai_analysis)?.recommendation" class="history-rec">
|
||||
{{ historySummary(rec.ai_analysis)?.recommendation }}
|
||||
</p>
|
||||
</template>
|
||||
<p v-else class="history-summary">—</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联灵感 -->
|
||||
<div class="detail-section">
|
||||
<h3>{{ $t('ideas.relatedTitle') }}</h3>
|
||||
<!-- 展示态 -->
|
||||
<template v-if="!relatedEditing">
|
||||
<div v-if="relatedIds.length > 0" class="related-list">
|
||||
<template v-for="row in relatedRows" :key="row.id">
|
||||
<router-link
|
||||
v-if="row.title"
|
||||
class="related-chip"
|
||||
:to="`/ideas/${row.id}`"
|
||||
>{{ row.title }}</router-link>
|
||||
<span v-else class="related-chip related-chip-missing">#{{ row.id }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.relatedEmpty') }}</div>
|
||||
<button class="btn btn-ghost btn-sm" @click="startRelateEdit">{{ $t('ideas.relatedManage') }}</button>
|
||||
</template>
|
||||
<!-- 编辑态 -->
|
||||
<template v-else>
|
||||
<div v-if="candidateIdeas.length > 0" class="related-edit-list">
|
||||
<label
|
||||
v-for="idea in candidateIdeas"
|
||||
:key="idea.id"
|
||||
class="related-edit-item"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="relatedSelected.includes(idea.id)"
|
||||
@change="toggleSelect(idea.id)"
|
||||
/>
|
||||
<span>{{ idea.title }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.relatedEmpty') }}</div>
|
||||
<div class="related-edit-actions">
|
||||
<button class="btn btn-primary btn-sm" @click="confirmRelate">{{ $t('ideas.relatedConfirm') }}</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="cancelRelate">{{ $t('ideas.relatedCancel') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
<div class="radar-chart" v-if="parseScores(idea.scores).length > 0">
|
||||
<div class="radar-row" v-for="dim in parseScores(idea.scores)" :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>
|
||||
@@ -117,6 +207,13 @@
|
||||
>
|
||||
{{ promoting ? $t('ideas.promoting') : $t('ideas.promoteToProject') }}
|
||||
</button>
|
||||
<router-link
|
||||
v-if="idea.promoted_to"
|
||||
class="btn btn-primary"
|
||||
:to="`/projects/${idea.promoted_to}`"
|
||||
>
|
||||
🚀 {{ $t('ideas.promotedProject') }} →
|
||||
</router-link>
|
||||
<button class="btn btn-ghost" :disabled="deleting" @click="$emit('delete')">
|
||||
{{ deleting ? $t('ideas.deleting') : $t('ideas.deleteIdea') }}
|
||||
</button>
|
||||
@@ -126,11 +223,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { parseTags } from '../../stores/knowledge'
|
||||
import { useProjectStore } from '../../stores/project'
|
||||
import { useRendered } from '../../composables/useMarkdown'
|
||||
import type { IdeaRecord, IdeaStatus } from '../../api/types'
|
||||
import { ideaApi } from '../../api'
|
||||
import { formatDate } from '../../utils/time'
|
||||
import { parseScores, assessmentClass, assessmentLabel } from '../../utils/ideaEval'
|
||||
import type { IdeaRecord, IdeaStatus, IdeaEvaluationRecord } from '../../api/types'
|
||||
|
||||
const props = defineProps<{
|
||||
idea: IdeaRecord
|
||||
@@ -146,9 +247,82 @@ const emit = defineEmits<{
|
||||
(e: 'promote'): void
|
||||
(e: 'delete'): void
|
||||
(e: 'status-change', value: IdeaStatus): void
|
||||
(e: 'update-desc', value: string): void
|
||||
(e: 'update-related', ids: string[]): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const store = useProjectStore()
|
||||
|
||||
// 描述可编辑模式
|
||||
const editing = ref(false)
|
||||
const editDesc = ref('')
|
||||
|
||||
function startEdit() {
|
||||
editDesc.value = props.idea.description ?? ''
|
||||
editing.value = true
|
||||
}
|
||||
|
||||
function saveEdit() {
|
||||
emit('update-desc', editDesc.value)
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
// ===== 关联灵感(related_ids JSON 数组字符串)=====
|
||||
// 解析 props.idea.related_ids(JSON 字符串数组,null/空/非法 → [])
|
||||
const relatedIds = computed<string[]>(() => {
|
||||
const raw = props.idea.related_ids
|
||||
if (!raw) return []
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
return Array.isArray(parsed) ? parsed.filter((x): x is string => typeof x === 'string') : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
// 反查标题:relatedIds 在 store.ideas 中找对应灵感(找不到留待展示态显 #id)。
|
||||
// 项3 DRY/性能:原模板 v-for 内对每个 id 调 relatedIdeas.find 两次(O(n)×2);
|
||||
// 改为一次性构建 {id, title|null} 数组(保留原始顺序含缺失项),模板直接 v-for 对象,O(1)。
|
||||
const relatedRows = computed<{ id: string; title: string | null }[]>(() => {
|
||||
// store.ideas → Map 一次构建,O(1) 查找,避免对每个 relatedId 线性扫 store.ideas
|
||||
const byId = new Map(store.ideas.map(i => [i.id, i.title] as const))
|
||||
return relatedIds.value.map(id => ({ id, title: byId.get(id) ?? null }))
|
||||
})
|
||||
|
||||
// 编辑态
|
||||
const relatedEditing = ref(false)
|
||||
const relatedSelected = ref<string[]>([])
|
||||
|
||||
// 可选关联的其他灵感(排除当前灵感自身)
|
||||
const candidateIdeas = computed(() => store.ideas.filter(i => i.id !== props.idea.id))
|
||||
|
||||
function startRelateEdit() {
|
||||
relatedSelected.value = [...relatedIds.value]
|
||||
relatedEditing.value = true
|
||||
}
|
||||
|
||||
function confirmRelate() {
|
||||
emit('update-related', relatedSelected.value)
|
||||
relatedEditing.value = false
|
||||
}
|
||||
|
||||
function cancelRelate() {
|
||||
relatedEditing.value = false
|
||||
}
|
||||
|
||||
function toggleSelect(id: string) {
|
||||
const idx = relatedSelected.value.indexOf(id)
|
||||
if (idx >= 0) {
|
||||
relatedSelected.value.splice(idx, 1)
|
||||
} else {
|
||||
relatedSelected.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 任意 status 字符串 → 对应 i18n key;未知状态回退到原值显示
|
||||
function statusLabelKey(status: IdeaStatus): string {
|
||||
@@ -161,23 +335,7 @@ 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 []
|
||||
}
|
||||
}
|
||||
// parseScores / assessmentClass / assessmentLabel 抽到 ../../utils/ideaEval 复用(消除与 ProjectDetail 的 DRY 重复)。
|
||||
|
||||
interface AdversarialEval {
|
||||
positive_strength: number
|
||||
@@ -190,6 +348,7 @@ interface AdversarialEval {
|
||||
positive: { thesis: string; evidence: string[] }
|
||||
negative: { thesis: string; evidence: string[] }
|
||||
analyst: { summary: string }
|
||||
evaluated_by?: string
|
||||
}
|
||||
|
||||
// 对抗式评估数据持久化在 ai_analysis JSON 字段中,前端解析渲染
|
||||
@@ -202,25 +361,18 @@ const adversarialEval = computed<AdversarialEval | 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'
|
||||
// 评估深度标签动态化(P0):根据 evaluated_by 字段映射 i18n key
|
||||
// 'Llm' → LLM 深度评估;'HeuristicFallback' → 启发式降级;
|
||||
// 其他(undefined / 'Heuristic' / 老数据无此字段)→ 启发式
|
||||
// 映射逻辑与 evalModeLabelKeyFromStr 同,这里直接透传避免重复(项2 DRY)
|
||||
function evalModeLabelKey(): string {
|
||||
return evalModeLabelKeyFromStr(adversarialEval.value?.evaluated_by)
|
||||
}
|
||||
|
||||
function assessmentLabel(recommendation: string) {
|
||||
const key = `ideas.assessment.${recommendation}`
|
||||
// 未命中 i18n key 时回退到原始 recommendation 字符串
|
||||
const translated = t(key)
|
||||
return translated === key ? recommendation : translated
|
||||
}
|
||||
// assessmentClass 直接复用 ../../utils/ideaEval(无 i18n 依赖,模板可直调)。
|
||||
// assessmentLabel 需要 t 注入,这里留一个绑定 t 的薄包装,消除映射逻辑重复(项1 DRY)。
|
||||
const localAssessmentLabel = (recommendation: string): string =>
|
||||
assessmentLabel(t, recommendation)
|
||||
|
||||
function sentimentClass(sentiment: number) {
|
||||
// 统一三档(FR-C2: 原 >=0 与模板 >0 矛盾,net_sentiment=0 时文案负面样式 positive)
|
||||
@@ -232,6 +384,63 @@ function sentimentClass(sentiment: number) {
|
||||
function onStatusChange(e: Event) {
|
||||
emit('status-change', (e.target as HTMLSelectElement).value as IdeaStatus)
|
||||
}
|
||||
|
||||
// ===== 评估历史(版本时间线)=====
|
||||
const evalHistory = ref<IdeaEvaluationRecord[]>([])
|
||||
const historyLoading = ref(false)
|
||||
const expandedVersion = ref<number | null>(null)
|
||||
|
||||
async function loadHistory() {
|
||||
historyLoading.value = true
|
||||
try {
|
||||
evalHistory.value = await ideaApi.listEvaluations(props.idea.id)
|
||||
} catch {
|
||||
evalHistory.value = []
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 最新版本 = version 最大者(后端按 DESC 返回,首项即最新;这里用 max 兜底)
|
||||
const latestVersion = computed(() =>
|
||||
evalHistory.value.reduce((max, r) => Math.max(max, r.version), -1),
|
||||
)
|
||||
|
||||
function toggleVersion(v: number) {
|
||||
expandedVersion.value = expandedVersion.value === v ? null : v
|
||||
}
|
||||
|
||||
// 针对字符串 evaluated_by 映射 i18n key(与 evalModeLabelKey 同映射规则,接收裸字符串)
|
||||
function evalModeLabelKeyFromStr(source: string | null | undefined): string {
|
||||
if (source === 'Llm') return 'ideas.evalModeLlm'
|
||||
if (source === 'HeuristicFallback') return 'ideas.evalModeFallback'
|
||||
return 'ideas.evalModeHeuristic'
|
||||
}
|
||||
|
||||
// 解析历史 ai_analysis JSON,取 summary / recommendation(对抗式评估格式)。
|
||||
// 老数据或非 LLM 评估可能无该结构 → 返回 null。
|
||||
interface HistorySummary { summary: string | null; recommendation: string | null }
|
||||
function historySummary(aiAnalysis: string | null | undefined): HistorySummary | null {
|
||||
if (!aiAnalysis) return null
|
||||
try {
|
||||
const parsed = JSON.parse(aiAnalysis)
|
||||
if (typeof parsed !== 'object' || parsed === null) return null
|
||||
const obj = parsed as Record<string, unknown>
|
||||
const summary = typeof obj.summary === 'string' ? obj.summary
|
||||
: (typeof obj.analyst === 'object' && obj.analyst !== null
|
||||
&& typeof (obj.analyst as Record<string, unknown>).summary === 'string'
|
||||
? ((obj.analyst as Record<string, unknown>).summary as string)
|
||||
: null)
|
||||
const recommendation = typeof obj.recommendation === 'string' ? obj.recommendation : null
|
||||
if (!summary && !recommendation) return null
|
||||
return { summary, recommendation }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadHistory)
|
||||
watch(() => props.idea.id, loadHistory)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -486,6 +695,85 @@ function onStatusChange(e: Event) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ===== 评估历史(版本时间线)===== */
|
||||
.eval-history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.eval-history-item {
|
||||
background: var(--df-bg-raised);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.eval-history-item:hover {
|
||||
background: var(--df-bg-card);
|
||||
border-color: var(--df-accent);
|
||||
}
|
||||
.eval-history-item.expanded {
|
||||
border-color: var(--df-accent);
|
||||
}
|
||||
.history-row-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
.version-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(108, 99, 255, 0.12);
|
||||
color: var(--df-accent);
|
||||
}
|
||||
.latest-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(100, 255, 218, 0.15);
|
||||
color: var(--df-success);
|
||||
}
|
||||
.history-eval-mode {
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(255, 217, 61, 0.12);
|
||||
color: var(--df-warning);
|
||||
}
|
||||
.history-score {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--df-text);
|
||||
min-width: 32px;
|
||||
}
|
||||
.history-date {
|
||||
font-size: 11px;
|
||||
color: var(--df-text-dim);
|
||||
margin-left: auto;
|
||||
}
|
||||
.history-detail {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
}
|
||||
.history-summary {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-secondary);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
.history-rec {
|
||||
font-size: 11px;
|
||||
color: var(--df-accent);
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
/* ===== 雷达图(div 模拟) ===== */
|
||||
.radar-chart {
|
||||
display: flex;
|
||||
@@ -535,6 +823,37 @@ function onStatusChange(e: Event) {
|
||||
color: var(--df-accent);
|
||||
}
|
||||
|
||||
/* ===== 关联灵感 ===== */
|
||||
.related-list { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.related-chip {
|
||||
font-size: 12px; padding: 4px 10px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(108, 99, 255, 0.1);
|
||||
color: var(--df-accent);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.related-chip:hover { background: rgba(108, 99, 255, 0.2); }
|
||||
.related-chip-missing { cursor: default; opacity: 0.6; }
|
||||
|
||||
.related-edit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.related-edit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--df-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
.related-edit-item input { cursor: pointer; }
|
||||
.related-edit-actions { display: flex; gap: 8px; }
|
||||
|
||||
/* ===== 按钮 ===== */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
@@ -549,6 +868,40 @@ function onStatusChange(e: Event) {
|
||||
.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); }
|
||||
|
||||
/* 小尺寸按钮(描述编辑/重试等) */
|
||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
.desc-edit-btn { margin-bottom: var(--df-gap-page); margin-top: 4px; }
|
||||
.desc-edit-actions { display: flex; gap: 8px; margin-bottom: var(--df-gap-page); margin-top: 8px; }
|
||||
|
||||
/* 描述编辑 textarea */
|
||||
.detail-desc-edit {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
background: var(--df-bg);
|
||||
color: var(--df-text);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
margin-bottom: var(--df-gap-page);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.detail-desc-edit:focus {
|
||||
outline: none;
|
||||
border-color: var(--df-accent);
|
||||
background: var(--df-bg-raised);
|
||||
}
|
||||
|
||||
/* 评估失败重试按钮(内联于错误提示) */
|
||||
.btn-retry {
|
||||
margin-left: 8px;
|
||||
padding: 2px 10px;
|
||||
font-size: 11px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ===== 状态管理 ===== */
|
||||
.status-controls {
|
||||
margin-top: 8px;
|
||||
|
||||
Reference in New Issue
Block a user