style(frontend): 统一文件管理、评估管理、问答管理界面样式

- 评估管理界面:新增统计卡片带 glow 效果、空状态轨道动画、表格布局多选
- 问答管理界面:采用与文件管理一致的渐变标题、统计卡片、空状态动画
- 文件管理:微调样式细节

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-19 10:11:44 +08:00
parent fa7829657f
commit 45b77a44c6
3 changed files with 1632 additions and 228 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -113,6 +113,10 @@
</el-checkbox> </el-checkbox>
</div> </div>
<div class="table-actions" v-if="selectedCount > 0"> <div class="table-actions" v-if="selectedCount > 0">
<el-button type="danger" size="small" plain @click="clearSelection" class="batch-clear-btn">
<el-icon><Close /></el-icon>
<span>清除选择</span>
</el-button>
<el-button type="danger" size="small" plain @click="batchDelete" class="batch-delete-btn"> <el-button type="danger" size="small" plain @click="batchDelete" class="batch-delete-btn">
<el-icon><Delete /></el-icon> <el-icon><Delete /></el-icon>
<span>批量删除</span> <span>批量删除</span>
@@ -132,9 +136,10 @@
'row-animated': isInitialLoad 'row-animated': isInitialLoad
}" }"
:style="{ '--delay': index * 0.04 + 's' }" :style="{ '--delay': index * 0.04 + 's' }"
@click="toggleSelect(file.id)"
> >
<!-- Select Checkbox --> <!-- Select Checkbox -->
<div class="col-select"> <div class="col-select" @click.stop>
<el-checkbox <el-checkbox
:model-value="isSelected(file.id)" :model-value="isSelected(file.id)"
@change="toggleSelect(file.id)" @change="toggleSelect(file.id)"
@@ -175,19 +180,15 @@
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="col-actions"> <div class="col-actions" @click.stop>
<el-tooltip content="预览" placement="top" v-if="file.status === 'completed'"> <el-tooltip content="预览" placement="top" v-if="file.status === 'completed'">
<el-button text size="small" class="action-btn preview" @click="handlePreview(file)"> <el-button text size="small" class="action-btn preview" @click="handlePreview(file)">
<el-icon><View /></el-icon> <el-icon><View /></el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-popconfirm title="确定删除此文件?" @confirm="handleDelete(file)"> <el-button text size="small" class="action-btn delete" @click="openDeleteDialog(file)">
<template #reference> <el-icon><Delete /></el-icon>
<el-button text size="small" class="action-btn delete"> </el-button>
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-popconfirm>
</div> </div>
</div> </div>
</div> </div>
@@ -249,7 +250,7 @@
</el-dialog> </el-dialog>
</div> </div>
<!-- Preview Modal --> <!-- Preview Modal -->
<Teleport to="body"> <Teleport to="body">
<!-- Backdrop --> <!-- Backdrop -->
<Transition name="fade"> <Transition name="fade">
@@ -307,6 +308,17 @@
</div> </div>
</Transition> </Transition>
</Teleport> </Teleport>
<DeleteDialog
v-model:visible="deleteDialogVisible"
title="删除文件"
:item-name="pendingDeleteFile?.filename || ''"
detail-text="该操作会移除原始文件以及关联的处理结果请确认当前项目内不再需要它"
warning-text="删除后不可恢复文件相关的分割结果和后续数据将一并失效"
confirm-text="确认删除文件"
:loading="deletingFile"
@confirm="confirmDeleteFile"
/>
</template> </template>
<script setup> <script setup>
@@ -314,6 +326,7 @@ import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { fileApi } from '@/api' import { fileApi } from '@/api'
import DeleteDialog from '@/components/common/DeleteDialog.vue'
const route = useRoute() const route = useRoute()
const projectId = computed(() => route.params.id) const projectId = computed(() => route.params.id)
@@ -331,6 +344,9 @@ const uploadDialogVisible = ref(false)
const uploading = ref(false) const uploading = ref(false)
const uploadRef = ref(null) const uploadRef = ref(null)
const fileList = ref([]) const fileList = ref([])
const deleteDialogVisible = ref(false)
const pendingDeleteFile = ref(null)
const deletingFile = ref(false)
// Multi-select // Multi-select
const selectedFiles = ref([]) const selectedFiles = ref([])
@@ -357,6 +373,10 @@ const toggleSelect = (fileId) => {
const isSelected = (fileId) => selectedFiles.value.includes(fileId) const isSelected = (fileId) => selectedFiles.value.includes(fileId)
const clearSelection = () => {
selectedFiles.value = []
}
const batchDelete = async () => { const batchDelete = async () => {
if (selectedFiles.value.length === 0) return if (selectedFiles.value.length === 0) return
try { try {
@@ -471,14 +491,29 @@ const submitUpload = async () => {
const handleDelete = async (file) => { const handleDelete = async (file) => {
try { try {
deletingFile.value = true
await fileApi.delete(projectId.value, file.id) await fileApi.delete(projectId.value, file.id)
ElMessage.success('删除成功') ElMessage.success('删除成功')
fetchFiles() fetchFiles()
} catch (error) { } catch (error) {
ElMessage.error('删除失败') ElMessage.error('删除失败')
} finally {
deletingFile.value = false
deleteDialogVisible.value = false
pendingDeleteFile.value = null
} }
} }
const openDeleteDialog = (file) => {
pendingDeleteFile.value = file
deleteDialogVisible.value = true
}
const confirmDeleteFile = async () => {
if (!pendingDeleteFile.value) return
await handleDelete(pendingDeleteFile.value)
}
const handlePreview = async (file) => { const handlePreview = async (file) => {
previewFile.value = file previewFile.value = file
previewVisible.value = true previewVisible.value = true
@@ -955,6 +990,18 @@ onMounted(() => fetchFiles())
color: white !important; color: white !important;
} }
.batch-clear-btn {
background: transparent !important;
border-color: var(--text-muted) !important;
color: var(--text-secondary) !important;
}
.batch-clear-btn:hover {
background: var(--bg-hover) !important;
border-color: var(--text-secondary) !important;
color: #ffffff !important;
}
.files-table { .files-table {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,79 +1,182 @@
<template> <template>
<div class="question-manage"> <div class="question-manage">
<!-- Page Header --> <!-- Header -->
<div class="page-header"> <div class="page-header">
<div class="header-left"> <div class="header-left">
<h2>问答管理</h2> <h2 class="page-title">问答管理</h2>
<p class="header-desc">管理和生成问答数据</p> <p class="page-subtitle">管理和生成问答数据</p>
</div>
<div class="header-actions">
<el-button type="primary" @click="showGenerateDialog = true" class="generate-btn">
<el-icon><Plus /></el-icon>
<span>生成问题</span>
</el-button>
</div> </div>
<el-button type="primary" @click="showGenerateDialog = true" class="generate-btn">
<el-icon><Plus /></el-icon>
生成问题
</el-button>
</div> </div>
<!-- Stats --> <!-- Stats Cards -->
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div
<div class="stat-icon violet"> class="stat-card stat-total"
<el-icon><ChatDotSquare /></el-icon> :class="{ active: filterStatus === '' }"
</div> @click="filterStatus = ''"
<div class="stat-info"> >
<span class="stat-value">{{ questions.length }}</span> <div class="stat-glow"></div>
<span class="stat-label">总问题数</span> <div class="stat-inner">
<div class="stat-icon-wrap">
<el-icon size="24"><ChatDotSquare /></el-icon>
</div>
<div class="stat-info">
<span class="stat-value">{{ questions.length }}</span>
<span class="stat-label">总问题数</span>
</div>
</div> </div>
</div> </div>
<div class="stat-card"> <div
<div class="stat-icon cyan"> class="stat-card stat-completed"
<el-icon><MagicStick /></el-icon> :class="{ active: filterStatus === 'generated' }"
</div> @click="filterStatus = filterStatus === 'generated' ? '' : 'generated'"
<div class="stat-info"> >
<span class="stat-value">{{ generatedCount }}</span> <div class="stat-glow"></div>
<span class="stat-label">AI 生成</span> <div class="stat-inner">
<div class="stat-icon-wrap">
<el-icon size="24"><MagicStick /></el-icon>
</div>
<div class="stat-info">
<span class="stat-value">{{ generatedCount }}</span>
<span class="stat-label">AI 生成</span>
</div>
</div> </div>
</div> </div>
<div class="stat-card"> <div
<div class="stat-icon orange"> class="stat-card stat-processing"
<el-icon><EditPen /></el-icon> :class="{ active: filterStatus === 'manual' }"
@click="filterStatus = filterStatus === 'manual' ? '' : 'manual'"
>
<div class="stat-glow"></div>
<div class="stat-inner">
<div class="stat-icon-wrap">
<el-icon size="24"><EditPen /></el-icon>
</div>
<div class="stat-info">
<span class="stat-value">{{ manualCount }}</span>
<span class="stat-label">手动添加</span>
</div>
</div> </div>
<div class="stat-info"> </div>
<span class="stat-value">{{ manualCount }}</span> <div
<span class="stat-label">手动添加</span> class="stat-card stat-failed"
:class="{ active: filterStatus === 'failed' }"
@click="filterStatus = filterStatus === 'failed' ? '' : 'failed'"
>
<div class="stat-glow"></div>
<div class="stat-inner">
<div class="stat-icon-wrap">
<el-icon size="24"><CircleCloseFilled /></el-icon>
</div>
<div class="stat-info">
<span class="stat-value">{{ failedCount }}</span>
<span class="stat-label">失败</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Question Table --> <!-- Question Container -->
<div class="table-card"> <div class="question-container" v-loading="loading && isInitialLoad">
<el-table :data="questions" v-loading="loading" style="width: 100%"> <!-- Empty State -->
<el-table-column prop="content" label="问题内容" min-width="300"> <div v-if="!loading && !isInitialLoad && filteredQuestions.length === 0" class="empty-state">
<template #default="{ row }"> <div class="empty-illustration">
<div class="question-content">{{ row.content }}</div> <div class="orbit orbit-1"></div>
</template> <div class="orbit orbit-2"></div>
</el-table-column> <div class="orbit orbit-3"></div>
<el-table-column prop="answer" label="答案" min-width="200"> <div class="empty-core">
<template #default="{ row }"> <el-icon size="40"><ChatDotSquare /></el-icon>
<div class="answer-content">{{ row.answer || '-' }}</div> </div>
</template> </div>
</el-table-column> <h3 class="empty-title">暂无问答数据</h3>
<el-table-column prop="question_type" label="类型" width="120"> <p class="empty-desc">生成您的第一个问答数据集</p>
<template #default="{ row }"> <el-button type="primary" @click="showGenerateDialog = true" class="empty-btn">生成问题</el-button>
<el-tag size="small" :type="getTypeColor(row.question_type)" effect="dark"> </div>
{{ getTypeName(row.question_type) }}
</el-tag> <!-- Question Table -->
</template> <div v-else class="question-table-wrapper">
</el-table-column> <!-- Table Header -->
<el-table-column prop="source" label="来源" width="120" /> <div class="table-header">
<el-table-column label="操作" width="100" fixed="right"> <div class="table-select">
<template #default="{ row }"> <el-checkbox
<el-popconfirm title="确定删除此问题?" @confirm="handleDelete(row)"> :model-value="isAllSelected"
<template #reference> @change="toggleSelectAll"
<el-button type="danger" size="small" text>删除</el-button> class="select-all"
</template> >
</el-popconfirm> <span v-if="selectedCount > 0" class="selected-text">已选择 {{ selectedCount }} </span>
</template> <span v-else>全选</span>
</el-table-column> </el-checkbox>
</el-table> </div>
<div class="table-actions" v-if="selectedCount > 0">
<el-button type="danger" size="small" plain @click="clearSelection" class="batch-clear-btn">
<el-icon><Close /></el-icon>
<span>清除选择</span>
</el-button>
<el-button type="danger" size="small" plain @click="batchDelete" class="batch-delete-btn">
<el-icon><Delete /></el-icon>
<span>批量删除</span>
</el-button>
</div>
</div>
<!-- Table Body -->
<div class="question-table">
<div
v-for="(question, index) in filteredQuestions"
:key="question.id"
class="question-row"
:class="{
'is-selected': isSelected(question.id),
'row-animated': isInitialLoad
}"
:style="{ '--delay': index * 0.04 + 's' }"
@click="toggleSelect(question.id)"
>
<!-- Select Checkbox -->
<div class="col-select" @click.stop>
<el-checkbox
:model-value="isSelected(question.id)"
@change="toggleSelect(question.id)"
/>
</div>
<!-- Question Content -->
<div class="col-content">
<div class="question-text">{{ question.content }}</div>
<div class="answer-text" v-if="question.answer">: {{ question.answer }}</div>
</div>
<!-- Type -->
<div class="col-type">
<el-tag size="small" :style="{ '--tag-color': getTypeColor(question.question_type) }" effect="dark">
{{ getTypeName(question.question_type) }}
</el-tag>
</div>
<!-- Source -->
<div class="col-source">
<span class="source-badge" :class="'source-' + question.source">{{ getSourceName(question.source) }}</span>
</div>
<!-- Actions -->
<div class="col-actions" @click.stop>
<el-popconfirm title="确定删除此问题?" @confirm="handleDelete(question)">
<template #reference>
<el-button text size="small" class="action-btn delete">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-popconfirm>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Generate Dialog --> <!-- Generate Dialog -->
@@ -126,10 +229,12 @@ const route = useRoute()
const projectId = computed(() => route.params.id) const projectId = computed(() => route.params.id)
const loading = ref(false) const loading = ref(false)
const isInitialLoad = ref(true)
const generating = ref(false) const generating = ref(false)
const questions = ref([]) const questions = ref([])
const chunks = ref([]) const chunks = ref([])
const showGenerateDialog = ref(false) const showGenerateDialog = ref(false)
const filterStatus = ref('')
const generateConfig = reactive({ const generateConfig = reactive({
chunk_ids: [], chunk_ids: [],
@@ -137,19 +242,72 @@ const generateConfig = reactive({
question_types: ['fact', 'summary'] question_types: ['fact', 'summary']
}) })
// Multi-select
const selectedQuestions = ref([])
const filteredQuestions = computed(() => {
if (!filterStatus.value) return questions.value
return questions.value.filter(q => q.source === filterStatus.value)
})
const generatedCount = computed(() => questions.value.filter(q => q.source === 'generated').length) const generatedCount = computed(() => questions.value.filter(q => q.source === 'generated').length)
const manualCount = computed(() => questions.value.filter(q => q.source === 'manual').length) const manualCount = computed(() => questions.value.filter(q => q.source === 'manual').length)
const failedCount = computed(() => questions.value.filter(q => q.status === 'failed').length)
const isAllSelected = computed(() => filteredQuestions.value.length > 0 && selectedQuestions.value.length === filteredQuestions.value.length)
const selectedCount = computed(() => selectedQuestions.value.length)
const toggleSelectAll = () => {
if (isAllSelected.value) {
selectedQuestions.value = []
} else {
selectedQuestions.value = filteredQuestions.value.map(q => q.id)
}
}
const toggleSelect = (id) => {
const index = selectedQuestions.value.indexOf(id)
if (index === -1) {
selectedQuestions.value.push(id)
} else {
selectedQuestions.value.splice(index, 1)
}
}
const isSelected = (id) => selectedQuestions.value.includes(id)
const clearSelection = () => {
selectedQuestions.value = []
}
const batchDelete = async () => {
if (selectedQuestions.value.length === 0) return
try {
for (const id of selectedQuestions.value) {
await questionApi.delete(projectId.value, id)
}
ElMessage.success(`已删除 ${selectedQuestions.value.length} 个问题`)
selectedQuestions.value = []
fetchQuestions()
} catch (error) {
ElMessage.error('删除失败')
}
}
const fetchQuestions = async () => { const fetchQuestions = async () => {
const wasInitial = isInitialLoad.value
loading.value = true loading.value = true
try { try {
const res = await chunkApi.list(projectId.value, {}) const res = await chunkApi.list(projectId.value, {})
chunks.value = res.data.chunks || [] chunks.value = res.data?.chunks || res.chunks || []
questions.value = chunks.value.flatMap(c => (c.questions || []).map(q => ({ ...q, source: c.name }))) questions.value = chunks.value.flatMap(c => (c.questions || []).map(q => ({ ...q, source: c.name })))
} catch (error) { } catch (error) {
questions.value = [] questions.value = []
} finally { } finally {
loading.value = false loading.value = false
if (wasInitial) {
isInitialLoad.value = false
}
} }
} }
@@ -182,8 +340,8 @@ const handleDelete = async (question) => {
} }
const getTypeColor = (type) => { const getTypeColor = (type) => {
const map = { 'fact': 'success', 'summary': 'primary', 'reasoning': 'warning' } const map = { 'fact': '#22c55e', 'summary': '#818cf8', 'reasoning': '#f59e0b' }
return map[type] || 'info' return map[type] || '#818cf8'
} }
const getTypeName = (type) => { const getTypeName = (type) => {
@@ -191,111 +349,522 @@ const getTypeName = (type) => {
return map[type] || type return map[type] || type
} }
const getSourceName = (source) => {
const map = { 'generated': 'AI生成', 'manual': '手动', 'failed': '失败' }
return map[source] || source
}
onMounted(() => fetchQuestions()) onMounted(() => fetchQuestions())
</script> </script>
<style scoped> <style scoped>
/* ========================
CSS Variables
======================== */
.question-manage { .question-manage {
padding: 32px; --accent-cyan: #00d4ff;
max-width: 1200px; --accent-cyan-dim: rgba(0, 212, 255, 0.15);
--accent-cyan-glow: rgba(0, 212, 255, 0.4);
--bg-elevated: #0f1117;
--bg-card: #161920;
--bg-hover: #1c2029;
--border-subtle: rgba(255, 255, 255, 0.08);
--border-active: rgba(0, 212, 255, 0.3);
--text-secondary: #9ca3af;
--text-muted: #6b7280;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--radius-lg: 12px;
--radius-md: 8px;
--radius-sm: 6px;
padding: 28px 32px;
} }
/* Header */ /* ========================
Header
======================== */
.page-header { .page-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
margin-bottom: 28px; margin-bottom: 28px;
} }
.header-left h2 { .header-left {
font-size: 24px; position: relative;
font-weight: 600;
margin-bottom: 4px;
} }
.header-desc { .page-title {
font-size: 28px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, #ffffff 0%, var(--accent-cyan) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.5px;
}
.page-subtitle {
font-size: 14px; font-size: 14px;
color: var(--text-tertiary); color: var(--text-muted);
margin: 6px 0 0;
}
.header-actions {
display: flex;
gap: 12px;
} }
.generate-btn { .generate-btn {
padding: 10px 20px; background: var(--accent-cyan) !important;
font-weight: 500; border: none !important;
color: #030407 !important;
font-weight: 600;
padding: 10px 22px;
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: 0 0 20px var(--accent-cyan-dim);
transition: all 0.2s ease;
} }
/* Stats Grid */ .generate-btn:hover {
box-shadow: 0 0 35px var(--accent-cyan-glow);
transform: translateY(-1px);
}
/* ========================
Stats Grid
======================== */
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 16px; gap: 16px;
margin-bottom: 24px; margin-bottom: 24px;
} }
.stat-card { .stat-card {
position: relative;
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
overflow: hidden;
transition: all 0.25s ease;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
opacity: 0;
transition: opacity 0.25s ease;
}
.stat-card:hover {
transform: translateY(-2px);
border-color: var(--border-active);
cursor: pointer;
}
.stat-card:hover::before {
opacity: 1;
}
.stat-card.active {
border-color: var(--accent-cyan);
box-shadow: 0 0 20px var(--accent-cyan-dim);
}
.stat-card.active::before {
opacity: 1;
}
.stat-card.active .stat-glow {
opacity: 0.5;
}
.stat-glow {
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.stat-card:hover .stat-glow {
opacity: 0.5;
}
/* Stat variations */
.stat-total {
--stat-color: #818cf8;
}
.stat-total::before {
background: linear-gradient(90deg, #6366f1, #818cf8);
}
.stat-total .stat-glow {
background: radial-gradient(circle at 30% 30%, rgba(99, 102, 241, 0.3), transparent 50%);
}
.stat-completed {
--stat-color: var(--success);
}
.stat-completed::before {
background: linear-gradient(90deg, #16a34a, var(--success));
}
.stat-completed .stat-glow {
background: radial-gradient(circle at 30% 30%, rgba(34, 197, 94, 0.3), transparent 50%);
}
.stat-processing {
--stat-color: var(--warning);
}
.stat-processing::before {
background: linear-gradient(90deg, #d97706, var(--warning));
}
.stat-processing .stat-glow {
background: radial-gradient(circle at 30% 30%, rgba(245, 158, 11, 0.3), transparent 50%);
}
.stat-failed {
--stat-color: var(--danger);
}
.stat-failed::before {
background: linear-gradient(90deg, #dc2626, var(--danger));
}
.stat-failed .stat-glow {
background: radial-gradient(circle at 30% 30%, rgba(239, 68, 68, 0.3), transparent 50%);
}
.stat-inner {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
padding: 20px; padding: 20px 22px;
background: var(--bg-secondary); z-index: 1;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
} }
.stat-icon { .stat-icon-wrap {
width: 48px; width: 48px;
height: 48px; height: 48px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(255, 255, 255, 0.05);
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-size: 22px; color: var(--stat-color);
} }
.stat-icon.violet { background: rgba(124, 58, 237, 0.15); color: var(--accent-secondary); }
.stat-icon.cyan { background: var(--accent-primary-muted); color: var(--accent-primary); }
.stat-icon.orange { background: rgba(251, 191, 36, 0.15); color: var(--warning); }
.stat-info { .stat-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.stat-value { .stat-value {
font-size: 24px; font-size: 28px;
font-weight: 700; font-weight: 700;
color: #ffffff;
font-family: 'SF Mono', 'JetBrains Mono', monospace;
line-height: 1.1;
} }
.stat-label { .stat-label {
font-size: 13px; font-size: 13px;
color: var(--text-tertiary); color: var(--text-muted);
margin-top: 2px;
} }
/* Table Card */ /* ========================
.table-card { Question Container
background: var(--bg-secondary); ======================== */
.question-container {
background: var(--bg-card);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
overflow: hidden; overflow: hidden;
overflow-x: auto;
} }
.question-content { /* ========================
Empty State
======================== */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 40px;
min-height: 450px;
}
.empty-illustration {
position: relative;
width: 140px;
height: 140px;
margin-bottom: 28px;
}
.orbit {
position: absolute;
border-radius: 50%;
border: 1px dashed rgba(0, 212, 255, 0.2);
}
.orbit-1 {
inset: 10px;
animation: orbit-rotate 20s linear infinite;
}
.orbit-2 {
inset: 30px;
border-color: rgba(0, 212, 255, 0.15);
animation: orbit-rotate 15s linear infinite reverse;
}
.orbit-3 {
inset: 50px;
border-color: rgba(0, 212, 255, 0.1);
animation: orbit-rotate 10s linear infinite;
}
@keyframes orbit-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.empty-core {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--bg-hover) 0%, var(--bg-card) 100%);
border: 1px solid var(--border-subtle);
border-radius: 50%;
color: var(--accent-cyan);
box-shadow: 0 0 40px var(--accent-cyan-dim);
}
.empty-title {
font-size: 22px;
font-weight: 600;
color: #ffffff;
margin: 0 0 8px;
}
.empty-desc {
font-size: 14px; font-size: 14px;
color: var(--text-primary); color: var(--text-muted);
line-height: 1.5; margin: 0 0 20px;
text-align: center;
} }
.answer-content { .empty-btn {
font-size: 13px; background: var(--accent-cyan) !important;
color: var(--text-tertiary); border: none !important;
line-height: 1.5; color: #030407 !important;
max-height: 60px; font-weight: 600;
}
/* ========================
Question Table
======================== */
.question-table-wrapper {
display: flex;
flex-direction: column;
min-width: 800px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 20px;
background: var(--bg-elevated);
border-bottom: 1px solid var(--border-subtle);
}
.table-select .select-all {
--el-checkbox-text-color: var(--text-secondary);
}
.selected-text {
color: var(--accent-cyan);
font-weight: 500;
}
.batch-delete-btn {
background: transparent !important;
border-color: var(--danger) !important;
color: var(--danger) !important;
}
.batch-delete-btn:hover {
background: var(--danger) !important;
color: white !important;
}
.batch-clear-btn {
background: transparent !important;
border-color: var(--text-muted) !important;
color: var(--text-secondary) !important;
}
.batch-clear-btn:hover {
background: var(--bg-hover) !important;
border-color: var(--text-secondary) !important;
color: #ffffff !important;
}
.question-table {
display: flex;
flex-direction: column;
}
.question-row {
display: grid;
grid-template-columns: 40px 1fr 90px 80px 60px;
align-items: center;
gap: 16px;
padding: 14px 20px;
border-bottom: 1px solid var(--border-subtle);
transition: background 0.15s ease;
cursor: pointer;
}
.question-row.row-animated {
animation: row-in 0.3s ease backwards;
animation-delay: var(--delay);
}
@keyframes row-in {
from {
opacity: 0;
transform: translateX(-10px);
}
}
.question-row:hover {
background: var(--bg-hover);
}
.question-row.is-selected {
background: rgba(0, 212, 255, 0.05);
}
/* Column styles */
.col-select {
display: flex;
justify-content: center;
}
.col-content {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.question-text {
font-size: 14px;
font-weight: 500;
color: #ffffff;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* Form */ .answer-text {
font-size: 13px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.col-type {
min-width: 80px;
}
.col-type .el-tag {
background: color-mix(in srgb, var(--tag-color) 15%, transparent) !important;
border-color: transparent !important;
color: var(--tag-color) !important;
}
.col-source {
min-width: 70px;
}
.source-badge {
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
color: var(--text-secondary);
}
.source-badge.source-generated {
background: rgba(34, 197, 94, 0.1);
color: var(--success);
}
.source-badge.source-manual {
background: rgba(0, 212, 255, 0.1);
color: var(--accent-cyan);
}
.source-badge.source-failed {
background: rgba(239, 68, 68, 0.1);
color: var(--danger);
}
/* Actions */
.col-actions {
display: flex;
justify-content: flex-end;
gap: 4px;
min-width: 60px;
}
.action-btn {
color: var(--text-muted) !important;
transition: all 0.15s ease;
}
.action-btn.delete:hover {
color: var(--danger) !important;
background: rgba(239, 68, 68, 0.1) !important;
}
/* ========================
Dialog
======================== */
.generate-dialog {
--el-dialog-bg-color: var(--bg-elevated);
--el-dialog-border-radius: var(--radius-xl);
border: 1px solid var(--border-subtle);
}
.form-row { .form-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -307,14 +876,43 @@ onMounted(() => fetchQuestions())
color: var(--text-secondary); color: var(--text-secondary);
} }
/* Responsive */ /* ========================
@media (max-width: 768px) { Responsive
.stats-grid { ======================== */
grid-template-columns: 1fr; @media (max-width: 900px) {
.question-manage {
padding: 20px;
} }
.form-row { .stats-grid {
grid-template-columns: 1fr; grid-template-columns: repeat(2, 1fr);
}
.page-header {
flex-direction: column;
gap: 16px;
}
.header-actions {
width: 100%;
}
.header-actions .el-button {
flex: 1;
}
.question-row {
grid-template-columns: 40px 1fr 60px;
gap: 8px;
}
.col-type,
.col-source {
display: none;
}
.col-actions {
min-width: 50px;
} }
} }
</style> </style>