重构: 跨模块样式DRY收口+审批列表增强+灵感模块优化

- 全局样式收口: page-header/btn/modal/filter-bar/back-link/empty-state
  等通用类从12个视图的scoped重复定义提取到global.css/components.css
- 审批列表增强: 筛选栏(状态/风险/工具搜索)+行展开查看完整参数结果
  +审批耗时显示+刷新spinner+硬编码颜色全部改用CSS变量
- 灵感模块: scoreTier统一评分阈值+score-bar/assessment-badge提取全局
  +statusLabelKey消除重复+i18n状态文案对齐+eval-report--muted统一空态
- Dashboard: df-panel/df-link提取全局(修scoped不渗透)+统计卡可点击跳转
  +活跃项目面板只显示active状态+灵感列表按分数排序+刷新spinner
- 修复: intent.rs tool_type类型对齐(newtype)+ToolCardList补ai-btn定义
  +SkillMention/ImageInput样式不渗透修复(提取全局)
This commit is contained in:
lxy
2026-07-02 23:24:11 +08:00
parent eee0f06e1a
commit 249b3b9ea8
29 changed files with 691 additions and 925 deletions
+171 -51
View File
@@ -3,16 +3,36 @@
<header class="page-header">
<h1>{{ t('auditLog.title') }}</h1>
<div class="header-actions">
<button class="btn btn-ghost" @click="reload">{{ t('auditLog.refresh') }}</button>
<button class="btn btn-ghost" @click="reload" :disabled="loading">
<svg v-if="loading" class="audit-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg>
{{ t('auditLog.refresh') }}
</button>
</div>
</header>
<p class="page-desc">{{ t('auditLog.desc') }}</p>
<!-- 筛选栏 -->
<div class="filter-bar audit-filters">
<select v-model="statusFilter" class="audit-filter-select">
<option value="">{{ t('auditLog.filter.allStatus') }}</option>
<option v-for="s in statusKeys" :key="s" :value="s">{{ t(`auditLog.status.${s}`) }}</option>
</select>
<select v-model="riskFilter" class="audit-filter-select">
<option value="">{{ t('auditLog.filter.allRisk') }}</option>
<option v-for="r in riskKeys" :key="r" :value="r">{{ t(`auditLog.risk.${r}`) }}</option>
</select>
<div class="search-box audit-search">
<input v-model="toolSearch" type="text" :placeholder="t('auditLog.filter.toolPlaceholder')" />
</div>
</div>
<!-- 状态条 -->
<div v-if="loading" class="empty-state">{{ t('auditLog.loading') }}</div>
<div v-else-if="errorMsg" class="empty-state">{{ errorMsg }}</div>
<div v-else-if="records.length === 0" class="empty-state">{{ t('auditLog.empty') }}</div>
<div v-if="loading" class="empty-state audit-loading">
<svg class="audit-spinner audit-spinner--lg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg>
</div>
<div v-else-if="errorMsg" class="empty-state audit-error">⚠ {{ errorMsg }}</div>
<div v-else-if="filteredRecords.length === 0" class="empty-state">{{ t('auditLog.empty') }}</div>
<!-- 表格 -->
<div v-else class="audit-table-wrap">
@@ -29,40 +49,57 @@
</tr>
</thead>
<tbody>
<tr v-for="r in records" :key="r.id">
<td class="col-time">
<div class="time-rel">{{ formatRelative(r.requested_at) }}</div>
<div class="time-abs">{{ formatDate(r.requested_at) }}</div>
</td>
<td class="col-tool"><code class="tool-name">{{ r.tool_name }}</code></td>
<td class="col-risk">
<span class="risk-badge" :class="riskClass(r.risk_level)">{{ riskLabel(r.risk_level) }}</span>
</td>
<td class="col-status">
<span class="status-tag" :class="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
</td>
<td class="col-decided">
<span v-if="r.decided_by" class="decided-tag" :class="decidedClass(r.decided_by)">{{ decidedLabel(r.decided_by) }}</span>
<span v-else class="decided-none">—</span>
</td>
<td class="col-args"><code class="brief">{{ r.arguments_brief || '—' }}</code></td>
<td class="col-result"><code class="brief">{{ r.result_brief || '—' }}</code></td>
</tr>
<template v-for="r in filteredRecords" :key="r.id">
<tr class="audit-row" :class="{ expanded: expandedId === r.id }" @click="toggleExpand(r.id)">
<td class="col-time">
<div class="time-rel">{{ formatRelative(r.requested_at) }}</div>
<div class="time-abs">{{ formatDate(r.requested_at) }}</div>
<div v-if="r.executed_at" class="time-dur">⏱ {{ durationLabel(r.requested_at, r.executed_at) }}</div>
</td>
<td class="col-tool"><code class="tool-name">{{ r.tool_name }}</code></td>
<td class="col-risk">
<span class="risk-badge" :class="riskClass(r.risk_level)">{{ riskLabel(r.risk_level) }}</span>
</td>
<td class="col-status">
<span class="status-tag" :class="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
</td>
<td class="col-decided">
<span v-if="r.decided_by" class="decided-tag" :class="decidedClass(r.decided_by)">{{ decidedLabel(r.decided_by) }}</span>
<span v-else class="decided-none">—</span>
</td>
<td class="col-args"><code class="brief">{{ r.arguments_brief || '—' }}</code></td>
<td class="col-result"><code class="brief">{{ r.result_brief || '—' }}</code></td>
</tr>
<tr v-if="expandedId === r.id" class="audit-detail-row">
<td colspan="7">
<div class="audit-detail">
<div class="detail-section">
<span class="detail-label">{{ t('auditLog.col.args') }}</span>
<pre class="detail-pre">{{ r.arguments_brief || '—' }}</pre>
</div>
<div class="detail-section">
<span class="detail-label">{{ t('auditLog.col.result') }}</span>
<pre class="detail-pre">{{ r.result_brief || '—' }}</pre>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- 分页 -->
<div v-if="!loading && !errorMsg" class="pager">
<button class="btn btn-ghost" :disabled="offset === 0" @click="prevPage">{{ t('auditLog.pager.prev') }}</button>
<button class="btn btn-ghost btn-sm" :disabled="offset === 0" @click="prevPage">{{ t('auditLog.pager.prev') }}</button>
<span class="pager-info">{{ t('auditLog.pager.page', { n: page }) }}{{ hasMore ? '' : t('auditLog.pager.last') }}</span>
<button class="btn btn-ghost" :disabled="!hasMore" @click="nextPage">{{ t('auditLog.pager.next') }}</button>
<button class="btn btn-ghost btn-sm" :disabled="!hasMore" @click="nextPage">{{ t('auditLog.pager.next') }}</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { invoke } from '@tauri-apps/api/core'
import { formatRelative, formatDate } from '@/utils/time'
@@ -91,10 +128,50 @@ const offset = ref(0)
const loading = ref(false)
const errorMsg = ref('')
// 筛选状态(客户端过滤,数据量轻量)
const statusFilter = ref('')
const riskFilter = ref('')
const toolSearch = ref('')
// 展开行(查看完整 args/result)
const expandedId = ref<string | null>(null)
const statusKeys = ['pending', 'approved', 'rejected', 'executing', 'completed', 'failed']
const riskKeys = ['low', 'medium', 'high']
const page = computed(() => Math.floor(offset.value / PAGE_SIZE) + 1)
// 下一页存在性:本页满 PAGE_SIZE 视为可能还有更多(hasMore);末页不足时点下一页会拉空,自动回退
const hasMore = computed(() => records.value.length === PAGE_SIZE)
// 客户端筛选(状态/风险/工具名)
// 后端 list_tool_executions 不支持 WHERE 筛选,数据量小(每页 50),前端 filter 轻量。
const filteredRecords = computed(() => {
let list = records.value
if (statusFilter.value) list = list.filter(r => r.status === statusFilter.value)
if (riskFilter.value) list = list.filter(r => r.risk_level === riskFilter.value)
const q = toolSearch.value.trim().toLowerCase()
if (q) list = list.filter(r => r.tool_name.toLowerCase().includes(q))
return list
})
// 筛选变化时关闭展开行(避免展开行被筛掉后残留)
watch([statusFilter, riskFilter, toolSearch], () => { expandedId.value = null })
function toggleExpand(id: string) {
expandedId.value = expandedId.value === id ? null : id
}
// 审批→执行 耗时(秒级,毫秒差太精细)
function durationLabel(requested: string, executed: string): string {
const ms = new Date(executed).getTime() - new Date(requested).getTime()
if (isNaN(ms) || ms < 0) return '—'
if (ms < 1000) return '<1s'
const s = Math.floor(ms / 1000)
if (s < 60) return `${s}s`
const m = Math.floor(s / 60)
return `${m}m${s % 60}s`
}
async function load() {
loading.value = true
errorMsg.value = ''
@@ -117,16 +194,20 @@ async function load() {
}
function reload() {
if (loading.value) return
offset.value = 0
expandedId.value = null
void load()
}
function prevPage() {
if (offset.value === 0) return
offset.value = Math.max(0, offset.value - PAGE_SIZE)
expandedId.value = null
void load()
}
function nextPage() {
offset.value += PAGE_SIZE
expandedId.value = null
void load()
}
@@ -169,14 +250,7 @@ onMounted(() => {
max-width: 1200px;
}
.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; }
/* page-header / btn 等已提取到 global.css 全局 */
.page-desc {
font-size: 12px;
@@ -184,6 +258,30 @@ onMounted(() => {
margin-bottom: 16px;
}
/* ===== 筛选栏 ===== */
.audit-filters { gap: 8px; margin-bottom: 16px; }
.audit-filter-select {
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: 12px;
cursor: pointer;
}
.audit-filter-select:focus { outline: none; border-color: var(--df-accent); }
.audit-search { max-width: 200px; }
/* ===== 加载/错误状态 ===== */
.audit-loading { display: flex; align-items: center; justify-content: center; padding: 40px; }
.audit-error { color: var(--df-danger); }
.audit-spinner { animation: audit-spin 0.9s linear infinite; }
.audit-spinner--lg { color: var(--df-text-dim); }
@keyframes audit-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* ===== 表格 ===== */
.audit-table-wrap {
background: var(--df-bg-card);
@@ -215,7 +313,9 @@ onMounted(() => {
vertical-align: top;
}
.audit-table tbody tr:last-child td { border-bottom: none; }
.audit-table tbody tr:hover td { background: var(--df-sidebar-hover, rgba(255,255,255,0.03)); }
.audit-table tbody tr:hover td { background: var(--df-bg-hover, rgba(255,255,255,0.03)); }
/* 展开/hover 行高亮由 .audit-row:hover/.expanded 管理,避免选择器冲突 */
.audit-table tbody tr.audit-row:hover td { background: var(--df-bg-hover, rgba(255,255,255,0.03)); }
.col-time { min-width: 120px; }
.col-tool { min-width: 130px; }
@@ -225,14 +325,39 @@ onMounted(() => {
.col-args { min-width: 200px; }
.col-result { min-width: 220px; }
/* 可点击行 */
.audit-row { cursor: pointer; transition: background 0.12s; }
.audit-row:hover td { background: var(--df-bg-hover, rgba(255,255,255,0.03)); }
.audit-row.expanded td { background: var(--df-bg-hover, rgba(255,255,255,0.04)); }
/* 展开详情行 */
.audit-detail-row td { padding: 0 !important; border-bottom: 0.5px solid var(--df-border); }
.audit-detail { padding: 12px 16px; display: flex; flex-direction: column; gap: 10px; }
.detail-section { display: flex; flex-direction: column; gap: 4px; }
.detail-label { font-size: 11px; color: var(--df-text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
.detail-pre {
margin: 0; padding: 8px 10px;
background: var(--df-bg);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-sm);
font-family: var(--df-font-mono);
font-size: 11px;
color: var(--df-text-secondary);
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
.time-rel { font-size: 12px; color: var(--df-text-secondary); }
.time-abs { font-size: 10px; color: var(--df-text-dim); margin-top: 2px; font-family: var(--df-font-mono); }
.time-dur { font-size: 10px; color: var(--df-text-dim); margin-top: 2px; font-family: var(--df-font-mono); }
.tool-name {
font-family: var(--df-font-mono);
font-size: 11px;
color: var(--df-accent);
background: var(--df-accent-bg, rgba(123,111,240,0.08));
background: var(--df-accent-bg);
padding: 1px 6px;
border-radius: var(--df-radius-xs);
}
@@ -254,8 +379,8 @@ onMounted(() => {
border-radius: var(--df-radius-xs);
}
.risk-low { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
.risk-medium { background: rgba(255,152,0,0.2); color: #ff9800; }
.risk-high { background: rgba(255,107,107,0.2); color: var(--df-danger); }
.risk-medium { background: var(--df-warning-bg); color: var(--df-warning); }
.risk-high { background: var(--df-danger-bg); color: var(--df-danger); }
/* 状态 tag */
.status-tag {
@@ -266,12 +391,12 @@ onMounted(() => {
border-radius: var(--df-radius-sm);
white-space: nowrap;
}
.status-pending { background: rgba(255,217,61,0.2); color: var(--df-warning); }
.status-approved { background: rgba(76,175,80,0.2); color: var(--df-success, #4caf50); }
.status-rejected { background: rgba(255,107,107,0.2); color: var(--df-danger); }
.status-executing { background: rgba(100,181,246,0.2); color: var(--df-info); }
.status-completed { background: rgba(76,175,80,0.15); color: var(--df-success, #4caf50); }
.status-failed { background: rgba(255,107,107,0.2); color: var(--df-danger); }
.status-pending { background: var(--df-warning-bg); color: var(--df-warning); }
.status-approved { background: var(--df-success-bg); color: var(--df-success); }
.status-rejected { background: var(--df-danger-bg); color: var(--df-danger); }
.status-executing { background: var(--df-info-bg); color: var(--df-info); }
.status-completed { background: var(--df-success-bg); color: var(--df-success); }
.status-failed { background: var(--df-danger-bg); color: var(--df-danger); }
/* 决策者 tag */
.decided-tag {
@@ -281,7 +406,7 @@ onMounted(() => {
padding: 2px 8px;
border-radius: var(--df-radius-xs);
}
.decided-human { background: rgba(123,111,240,0.2); color: var(--df-accent); }
.decided-human { background: var(--df-accent-bg); color: var(--df-accent); }
.decided-auto { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
.decided-none { color: var(--df-text-dim); }
@@ -299,10 +424,5 @@ onMounted(() => {
font-family: var(--df-font-mono);
}
.empty-state {
text-align: center;
padding: 60px 20px;
font-size: 14px;
color: var(--df-text-dim);
}
/* empty-state 已提取到 global.css 全局 */
</style>
+11 -88
View File
@@ -7,18 +7,19 @@
<span class="dash-subtitle">{{ $t('dashboard.subtitle') }}</span>
</div>
<div class="dash-header-right">
<button class="df-btn df-btn--ghost" @click="refresh" :disabled="loading" :title="$t('dashboard.refresh')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
<button class="btn btn-ghost btn-sm dash-refresh-btn" @click="refresh" :disabled="loading" :title="$t('dashboard.refresh')">
<svg v-if="loading" class="dash-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg>
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
</button>
<button class="df-btn df-btn--primary" @click="quickCapture">
<button class="btn btn-primary btn-sm" @click="quickCapture">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
{{ $t('dashboard.captureIdea') }}
</button>
</div>
</header>
<!-- ═══ 错误条:refresh/onMounted 加载失败对用户可见(对齐 Knowledge error-banner) ═══ -->
<div v-if="errorMsg" class="error-banner">
<!-- ═══ 错误条:refresh/onMounted 加载失败对用户可见(样式全局 .error-banner) ═══ -->
<div v-if="errorMsg" class="error-banner" style="margin-bottom: 14px">
<span class="error-text">{{ errorMsg }}</span>
<button class="error-dismiss" @click="errorMsg = ''">✕</button>
</div>
@@ -122,58 +123,12 @@ onMounted(() => {
}
.dash-header-right { display: flex; gap: 6px; align-items: center; }
/* — Buttons — */
.df-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 9px 14px;
line-height: 1;
border: none;
border-radius: var(--df-radius-sm);
font-family: var(--df-font-sans);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s var(--df-ease);
white-space: nowrap;
/* 刷新按钮图标旋转动画(加载中反馈) */
.dash-spinner { animation: dash-spin 0.9s linear infinite; }
@keyframes dash-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.df-btn--primary {
background: var(--df-accent);
color: #fff;
}
.df-btn--primary:hover {
background: var(--df-accent);
filter: brightness(1.1);
}
.df-btn--ghost {
background: rgba(255,255,255,0.03);
color: var(--df-text-secondary);
border: 0.5px solid var(--df-border);
}
.df-btn--ghost:hover {
background: rgba(255,255,255,0.06);
color: var(--df-text);
border-color: var(--df-border-strong);
}
.df-btn--xs { padding: 4px 10px; font-size: 11px; }
.df-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* ═══ 错误条(对齐 Knowledge error-banner) ═══ */
.error-banner {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 8px 12px; margin-bottom: 14px;
background: rgba(255,107,107,0.12); border: 0.5px solid var(--df-danger);
border-radius: var(--df-radius-sm); color: var(--df-danger); font-size: 12px;
}
.error-text { word-break: break-word; }
.error-dismiss {
flex-shrink: 0; background: transparent; border: none; color: var(--df-danger);
font-size: 14px; cursor: pointer; line-height: 1; padding: 0 2px;
}
.error-dismiss:hover { opacity: 0.7; }
/* ═══ Grid Layout ═══ */
.dash-grid {
@@ -183,37 +138,6 @@ onMounted(() => {
}
.dash-right { display: flex; flex-direction: column; gap: 12px; }
/* ═══ Panel(子面板 df-panel 共享基类,scoped 下子组件需各自声明同名类样式;
此处保留供未来页内直挂面板,子组件已自带同名 .df-panel 样式) ═══ */
.df-panel {
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-md);
padding: 14px 16px;
animation: fadeInUp 0.5s var(--df-ease) both;
}
.df-panel-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.df-panel-title {
font-size: 12px;
font-weight: 500;
color: var(--df-text);
letter-spacing: -0.1px;
}
.df-link {
font-size: 11px;
color: var(--df-accent);
text-decoration: none;
font-weight: 500;
opacity: 0.8;
transition: opacity 0.15s;
}
.df-link:hover { opacity: 1; text-decoration: underline; }
/* ═══ Responsive ═══ */
/* 窄窗口:单栏 */
@@ -233,6 +157,5 @@ onMounted(() => {
@media (min-width: 1600px) {
.dashboard { padding: 20px 28px 24px; }
.dash-grid { grid-template-columns: 1fr 400px; gap: 16px; }
.df-panel { padding: 16px 18px; }
}
</style>
+10 -96
View File
@@ -134,8 +134,8 @@
<input v-model="newIdeaSource" :placeholder="$t('ideas.sourcePlaceholder')" />
</div>
<div class="modal-actions">
<button class="btn-cancel" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button>
<button class="btn-confirm" :disabled="creating" @click="confirmCapture">
<button class="btn btn-ghost" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button>
<button class="btn btn-primary" :disabled="creating" @click="confirmCapture">
{{ creating ? $t('ideas.creating') : $t('common.confirm') }}
</button>
</div>
@@ -156,6 +156,7 @@ import { useProjectStore } from '../stores/project'
import type { IdeaStatus, IdeaQuery } from '../api/types'
import { formatDate } from '../utils/time'
import { stripMd } from '../utils/markdown'
import { scoreTier } from '../utils/ideaEval'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import IdeaDetail from '../components/ideas/IdeaDetail.vue'
import { useConfirm } from '../composables/useConfirm'
@@ -286,11 +287,9 @@ const currentIdea = computed(() => {
// B-260615-25:灵感描述 Markdown 渲染已下沉至 IdeaDetail 子组件(共享模块单例渲染器)
// 评分档位 class(高/中/低 三色标),阈值统一走 scoreTier(单一来源,消除 4 处重复)
function scoreClass(score: number | null) {
const s = score ?? 0
if (s >= 80) return 'score-high'
if (s >= 60) return 'score-mid'
return 'score-low'
return 'score-' + scoreTier(score)
}
// formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date)
@@ -418,74 +417,7 @@ watch(() => route.params.id, (id) => {
<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);
}
/* page-header / btn / modal / filter-bar / filter-btn / search-box / empty-state 系列已提取到 global.css 全局 */
/* 排序按钮组(与状态筛选按钮视觉分隔) */
.sort-group {
@@ -547,16 +479,7 @@ watch(() => route.params.id, (id) => {
}
.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); }
/* ===== 状态标签已提取到 styles/components.css 全局(.status-tag 系列) ===== */
/* ===== 右侧详情(详情内容由 IdeaDetail 子组件渲染,父级仅保留未选中空态壳) ===== */
.idea-detail-panel {
@@ -572,9 +495,7 @@ watch(() => route.params.id, (id) => {
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; }
/* empty-state / empty-icon 已提取到 global.css 全局 */
/* ===== 响应式 ===== */
@media (max-width: 900px) {
@@ -593,13 +514,6 @@ watch(() => route.params.id, (id) => {
}
}
/* ===== 模态框 ===== */
.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-select { 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; }
/* ===== 模态框字段样式(modal-overlay/box/field/actions 已提取到 global.css) ===== */
/* Ideas 模态的 select 用 .modal-field select 全局覆盖,无需额外 scoped */
</style>
+5 -44
View File
@@ -8,8 +8,8 @@
</div>
</header>
<!-- 错误条:消费 store.error(加载/检索/配置失败对用户可见) -->
<div v-if="store.error" class="error-banner">
<!-- 错误条:消费 store.error(样式全局 .error-banner,仅间距用页面变量) -->
<div v-if="store.error" class="error-banner" style="margin-bottom: var(--df-gap-page)">
<span class="error-text">{{ store.error }}</span>
<button class="error-dismiss" @click="clearError">✕</button>
</div>
@@ -347,26 +347,9 @@ onMounted(() => {
<style scoped>
.knowledge { padding: 16px 20px 20px; height: 100%; display: flex; flex-direction: column; }
.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: 8px; }
/* page-header / btn / modal 等已提取到 global.css 全局 */
/* ===== 错误条(消费 store.error) ===== */
.error-banner {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 8px 12px; margin-bottom: var(--df-gap-page);
background: rgba(255,107,107,0.12); border: 0.5px solid var(--df-danger);
border-radius: var(--df-radius-sm); color: var(--df-danger); font-size: 12px;
}
.error-text { word-break: break-word; }
.error-dismiss {
flex-shrink: 0; background: transparent; border: none; color: var(--df-danger);
font-size: 14px; cursor: pointer; line-height: 1; padding: 0 2px;
}
.error-dismiss:hover { opacity: 0.7; }
/* ===== 错误条样式已提取到 styles/components.css 全局(.error-banner) ===== */
/* ===== 顶层 Tab ===== */
.top-tabs { display: flex; gap: 4px; margin-bottom: var(--df-gap-page); border-bottom: 0.5px solid var(--df-border); }
@@ -446,28 +429,7 @@ onMounted(() => {
.conf-medium { background: rgba(255,217,61,0.15); color: var(--df-warning); padding: 1px 6px; border-radius: var(--df-radius-xs); }
.conf-low { background: rgba(255,107,107,0.15); color: var(--df-danger); padding: 1px 6px; border-radius: var(--df-radius-xs); }
/* ===== 按钮 ===== */
.btn {
padding: 8px 16px; border: none; border-radius: var(--df-radius-sm);
font-size: 13px; cursor: pointer; transition: all 0.15s;
}
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-primary { background: var(--df-accent); color: #fff; }
.btn-primary:hover { background: var(--df-accent-hover); }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
.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); }
/* ===== 新增对话框 ===== */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.5);
display: flex; align-items: center; justify-content: center; z-index: 1000;
}
.modal-box {
background: var(--df-bg-panel, var(--df-bg-card)); border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-lg); padding: 24px; width: 90%; max-width: 520px;
}
.modal-title { font-size: 16px; font-weight: 500; color: var(--df-text); margin-bottom: 16px; }
/* form-row / form-input / form-select / form-textarea 已随新建/编辑弹层随 KnowledgeDetail 子组件迁移,此处仅保留本壳所需特有覆盖。 */
.form-row { margin-bottom: 12px; }
.form-row label { display: block; font-size: 12px; color: var(--df-text-secondary); margin-bottom: 4px; }
.form-input, .form-select {
@@ -477,7 +439,6 @@ onMounted(() => {
}
.form-input:focus, .form-select:focus { border-color: var(--df-accent); }
.form-textarea { min-height: 80px; resize: vertical; font-family: inherit; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
/* ===== 响应式:窄屏单列布局(详情窄屏样式随 KnowledgeDetail 子组件迁移) ===== */
@media (max-width: 760px) {
+11 -82
View File
@@ -559,7 +559,8 @@ onUnmounted(() => {
/* 加载/空态(模板顶部使用) */
.loading-state { text-align: center; padding: 40px; color: var(--df-text-dim); }
.empty-state { text-align: center; padding: 40px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
/* 覆盖全局 .empty-state:带图标+文字垂直排列,缩短 padding */
.empty-state { padding: 40px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
/* ===== 项目信息样式 ===== */
.project-info {
@@ -655,39 +656,16 @@ onUnmounted(() => {
margin: 6px 0;
}
/* assessment badge(复用 IdeaDetail 同名类的样式定义) */
.assessment-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 500;
}
.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); }
/* assessment-badge 系列(基础 + immediate/soon/conditional/revised/defer/cancel)
已提取到 styles/components.css 全局 */
/* 多维评分条(复用 IdeaDetail score-bar-* 样式定义) */
/* 多维评分条(.score-bar-* 系列)已提取到 styles/components.css 全局 */
.source-scores {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.score-bar-row { display: flex; align-items: center; gap: 10px; }
.score-bar-label { font-size: 12px; color: var(--df-text-secondary); min-width: 64px; text-align: right; }
.score-bar-track {
flex: 1; height: 8px; background: var(--df-border);
border-radius: var(--df-radius-xs); overflow: hidden;
}
.score-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); }
.score-bar-value { font-size: 12px; font-weight: 500; min-width: 28px; text-align: right; color: var(--df-text); }
</style>
@@ -727,16 +705,9 @@ onUnmounted(() => {
overflow: hidden;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--df-gap-page);
}
/* .page-header / .page-header h1 已提取到 styles/global.css 全局 */
.header-left { display: flex; align-items: center; gap: 12px; }
.back-link { font-size: 13px; color: var(--df-accent); text-decoration: none; }
.back-link:hover { text-decoration: underline; }
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
/* .back-link / .back-link:hover 已提取到 styles/global.css 全局 */
.stage-badge {
font-size: 12px;
@@ -752,24 +723,9 @@ onUnmounted(() => {
.stage-paused { background: rgba(158,158,158,0.15); color: #9e9e9e; }
.stage-cancelled { background: rgba(244,67,54,0.15); color: #f44336; }
.header-actions { display: flex; gap: 10px; }
/* .header-actions 已提取到 styles/global.css 全局 */
/* ===== 按钮 ===== */
.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); }
.btn-sm { padding: 4px 12px; font-size: 12px; }
.btn-danger { background: transparent; color: var(--df-danger); border: 0.5px solid rgba(240,101,101,0.4); }
.btn-danger:hover { background: rgba(240,101,101,0.12); }
/* .btn 系列(btn/btn-primary/btn-ghost/btn-sm/btn-danger)已提取到 styles/global.css 全局 */
/* ===== 两栏布局 — 自己滚动,不撑大父级 ===== */
.detail-grid {
@@ -856,35 +812,8 @@ onUnmounted(() => {
/* ===== 项目信息(左栏 .project-info/.info-item,右栏重复面板已删)===== */
/* ===== 模态框 ===== */
.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;
width: 420px;
max-width: 90vw;
}
.modal-title { font-size: 18px; font-weight: 500; color: var(--df-text); margin-bottom: 16px; }
.modal-field { margin-bottom: 14px; }
.modal-field label { display: block; font-size: 12px; color: var(--df-text-dim); margin-bottom: 4px; }
.modal-field input, .modal-field textarea {
width: 100%; padding: 8px 10px; border-radius: var(--df-radius-sm);
border: 0.5px solid var(--df-border); background: var(--df-bg);
color: var(--df-text); font-size: 13px; font-family: inherit;
box-sizing: border-box;
}
.modal-field input:focus, .modal-field textarea:focus {
outline: none; border-color: var(--df-accent);
}
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 4px; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* .modal-* 系列(modal-overlay/modal-box/modal-title/modal-field/modal-actions)
及 .btn:disabled 已提取到 styles/global.css 全局 */
.empty-hint {
text-align: center; padding: 24px 12px;
+5 -55
View File
@@ -432,29 +432,11 @@ async function runImport() {
<style scoped>
.projects { 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); }
/* page-header / btn / modal 系列已提取到 global.css 全局 */
/* 本页特有:.btn-danger(全局尚未提供)、.btn:disabled(全局仅 .btn-primary:disabled) */
.btn-danger { background: transparent; color: var(--df-danger); border: 0.5px solid rgba(240,101,101,0.4); }
.btn-danger:hover { background: rgba(240,101,101,0.12); }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* ===== 卡片网格 ===== */
.project-grid {
@@ -495,42 +477,10 @@ async function runImport() {
.scan-row { display: flex; gap: 8px; align-items: center; margin-top: 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;
width: 420px;
max-width: 90vw;
}
.modal-title { font-size: 18px; font-weight: 500; color: var(--df-text); margin-bottom: 16px; }
.modal-field { margin-bottom: 14px; }
.modal-field label { display: block; font-size: 12px; color: var(--df-text-dim); margin-bottom: 4px; }
.modal-field input, .modal-field textarea {
width: 100%; padding: 8px 10px; border-radius: var(--df-radius-sm);
border: 0.5px solid var(--df-border); background: var(--df-bg);
color: var(--df-text); font-size: 13px; font-family: inherit;
box-sizing: border-box;
}
.modal-field input:focus, .modal-field textarea:focus {
outline: none; border-color: var(--df-accent);
}
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 4px; }
.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:disabled { opacity: 0.4; cursor: not-allowed; }
/* page-header / btn / modal 系列已提取到 global.css 全局 */
/* ===== 空状态 ===== */
.empty-state {
text-align: center; padding: 60px 20px;
font-size: 14px; color: var(--df-text-dim);
}
/* page-header / btn / modal 系列已提取到 global.css 全局 */
/* ===== 错误条(消费 store.error,对齐 Knowledge error-banner) ===== */
.error-banner {
+1 -7
View File
@@ -239,13 +239,7 @@ onUnmounted(() => {
/* 确认弹层已替换为 ConfirmDialog 组件(不再需要自建 CSS) */
.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); }
/* page-header / btn 等已提取到 global.css 全局 */
/* ===== master-detail 布局(阶段3 UX 重构) =====
左 SettingsNav(~200px 固定)+ 右 content(flex 占满 + 纵向滚动)。
+1 -30
View File
@@ -491,37 +491,8 @@ onBeforeUnmount(() => {
<style scoped>
.task-detail { padding: 16px 20px 20px; }
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--df-gap-page);
}
/* page-header / btn / back-link 等已提取到 global.css 全局 */
.header-left { display: flex; align-items: center; gap: 12px; }
.back-link { font-size: 13px; color: var(--df-accent); text-decoration: none; white-space: nowrap; }
.back-link:hover { text-decoration: underline; }
.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-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); }
/* F-05 推进按钮变体(样式跟随 Projects/ProjectDetail/Knowledge 同款 btn-primary/btn-danger/btn-sm) */
.btn-primary { background: var(--df-accent); color: #fff; }
.btn-primary:hover { background: var(--df-accent-hover); }
.btn-danger { background: transparent; color: var(--df-danger); border: 0.5px solid rgba(240,101,101,0.4); }
.btn-danger:hover { background: rgba(240,101,101,0.12); }
.btn-sm { padding: 5px 12px; font-size: 12px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* F-05 推进操作区 */
.advance-row .advance-actions {
+10 -94
View File
@@ -124,25 +124,25 @@
<h3>{{ $t('tasks.modal.title') }}</h3>
<div class="modal-field">
<label>{{ $t('tasks.modal.project') }}</label>
<select v-model="newTaskProjectId" class="modal-input">
<select v-model="newTaskProjectId">
<option v-for="p in store.projects" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</div>
<div class="modal-field">
<label>{{ $t('tasks.modal.titleField') }}</label>
<input v-model="newTaskTitle" class="modal-input" :placeholder="$t('tasks.modal.titlePlaceholder')" @keyup.enter="confirmCreate" />
<input v-model="newTaskTitle" :placeholder="$t('tasks.modal.titlePlaceholder')" @keyup.enter="confirmCreate" />
</div>
<div class="modal-field">
<label>{{ $t('tasks.modal.desc') }}</label>
<input v-model="newTaskDesc" class="modal-input" :placeholder="$t('tasks.modal.descPlaceholder')" />
<input v-model="newTaskDesc" :placeholder="$t('tasks.modal.descPlaceholder')" />
</div>
<div class="modal-field">
<label>{{ $t('tasks.modal.branch') }}</label>
<input v-model="newTaskBranch" class="modal-input" :placeholder="$t('tasks.modal.branchPlaceholder')" />
<input v-model="newTaskBranch" :placeholder="$t('tasks.modal.branchPlaceholder')" />
</div>
<div class="modal-field">
<label>{{ $t('tasks.modal.priority') }}</label>
<select v-model="newTaskPriority" class="modal-input">
<select v-model="newTaskPriority">
<option :value="0">{{ $t('tasks.modal.priorityCritical') }}</option>
<option :value="1">{{ $t('tasks.modal.priorityHigh') }}</option>
<option :value="2">{{ $t('tasks.modal.priorityMedium') }}</option>
@@ -453,15 +453,7 @@ onUnmounted(() => {
<style scoped>
.tasks { padding: 16px 20px 20px; }
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--df-gap-page);
gap: 12px;
}
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); white-space: nowrap; }
.header-actions { display: flex; gap: 10px; align-items: center; }
/* page-header / page-header h1 / header-actions 已提取到全局 global.css */
/* 搜索框 */
.search-input {
@@ -478,33 +470,10 @@ onUnmounted(() => {
.search-input:focus { border-color: var(--df-accent); }
.search-input::placeholder { color: var(--df-text-dim); }
/* 按钮 */
.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); }
.btn-sm { padding: 4px 12px; font-size: 12px; }
/* btn / btn-primary / btn-ghost / btn-sm 已提取到全局 global.css */
/* 筛选栏(紧凑) */
.filter-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: var(--df-gap-page);
padding: 10px 14px;
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-lg);
}
/* filter-bar 已提取到全局 global.css,下方仅保留 Tasks 特有筛选类 */
.filter-group {
display: flex;
align-items: center;
@@ -702,61 +671,8 @@ onUnmounted(() => {
.quick-menu-danger { color: var(--df-danger); }
/* 空态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 48px 0;
color: var(--df-text-dim);
font-size: 13px;
}
/* empty-state 已提取到全局 global.css */
.empty-icon { font-size: 32px; opacity: 0.4; }
/* 模态框 */
.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: 20px;
min-width: 400px;
max-width: 480px;
}
.modal-box h3 { font-size: 16px; color: var(--df-text); margin-bottom: 16px; }
.modal-field {
margin-bottom: 12px;
}
.modal-field label {
display: block;
font-size: 12px;
color: var(--df-text-secondary);
margin-bottom: 4px;
}
.modal-input {
width: 100%;
box-sizing: border-box;
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;
outline: none;
}
.modal-input:focus { border-color: var(--df-accent); }
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
/* 模态框 overlay/box/modal-field/modal-actions 已提取到 global.css 全局 */
</style>