style(frontend): 统一文件管理、评估管理、问答管理界面样式
- 评估管理界面:新增统计卡片带 glow 效果、空状态轨道动画、表格布局多选 - 问答管理界面:采用与文件管理一致的渐变标题、统计卡片、空状态动画 - 文件管理:微调样式细节 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,10 @@
|
||||
</el-checkbox>
|
||||
</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>
|
||||
@@ -132,9 +136,10 @@
|
||||
'row-animated': isInitialLoad
|
||||
}"
|
||||
:style="{ '--delay': index * 0.04 + 's' }"
|
||||
@click="toggleSelect(file.id)"
|
||||
>
|
||||
<!-- Select Checkbox -->
|
||||
<div class="col-select">
|
||||
<div class="col-select" @click.stop>
|
||||
<el-checkbox
|
||||
:model-value="isSelected(file.id)"
|
||||
@change="toggleSelect(file.id)"
|
||||
@@ -175,19 +180,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="col-actions">
|
||||
<div class="col-actions" @click.stop>
|
||||
<el-tooltip content="预览" placement="top" v-if="file.status === 'completed'">
|
||||
<el-button text size="small" class="action-btn preview" @click="handlePreview(file)">
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-popconfirm title="确定删除此文件?" @confirm="handleDelete(file)">
|
||||
<template #reference>
|
||||
<el-button text size="small" class="action-btn delete">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-button text size="small" class="action-btn delete" @click="openDeleteDialog(file)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,7 +250,7 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<!-- Preview Modal -->
|
||||
<Teleport to="body">
|
||||
<!-- Backdrop -->
|
||||
<Transition name="fade">
|
||||
@@ -307,6 +308,17 @@
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<DeleteDialog
|
||||
v-model:visible="deleteDialogVisible"
|
||||
title="删除文件"
|
||||
:item-name="pendingDeleteFile?.filename || ''"
|
||||
detail-text="该操作会移除原始文件以及关联的处理结果,请确认当前项目内不再需要它。"
|
||||
warning-text="删除后不可恢复,文件相关的分割结果和后续数据将一并失效。"
|
||||
confirm-text="确认删除文件"
|
||||
:loading="deletingFile"
|
||||
@confirm="confirmDeleteFile"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -314,6 +326,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fileApi } from '@/api'
|
||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => route.params.id)
|
||||
@@ -331,6 +344,9 @@ const uploadDialogVisible = ref(false)
|
||||
const uploading = ref(false)
|
||||
const uploadRef = ref(null)
|
||||
const fileList = ref([])
|
||||
const deleteDialogVisible = ref(false)
|
||||
const pendingDeleteFile = ref(null)
|
||||
const deletingFile = ref(false)
|
||||
|
||||
// Multi-select
|
||||
const selectedFiles = ref([])
|
||||
@@ -357,6 +373,10 @@ const toggleSelect = (fileId) => {
|
||||
|
||||
const isSelected = (fileId) => selectedFiles.value.includes(fileId)
|
||||
|
||||
const clearSelection = () => {
|
||||
selectedFiles.value = []
|
||||
}
|
||||
|
||||
const batchDelete = async () => {
|
||||
if (selectedFiles.value.length === 0) return
|
||||
try {
|
||||
@@ -471,14 +491,29 @@ const submitUpload = async () => {
|
||||
|
||||
const handleDelete = async (file) => {
|
||||
try {
|
||||
deletingFile.value = true
|
||||
await fileApi.delete(projectId.value, file.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchFiles()
|
||||
} catch (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) => {
|
||||
previewFile.value = file
|
||||
previewVisible.value = true
|
||||
@@ -955,6 +990,18 @@ onMounted(() => fetchFiles())
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,79 +1,182 @@
|
||||
<template>
|
||||
<div class="question-manage">
|
||||
<!-- Page Header -->
|
||||
<!-- Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h2>问答管理</h2>
|
||||
<p class="header-desc">管理和生成问答数据</p>
|
||||
<h2 class="page-title">问答管理</h2>
|
||||
<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>
|
||||
<el-button type="primary" @click="showGenerateDialog = true" class="generate-btn">
|
||||
<el-icon><Plus /></el-icon>
|
||||
生成问题
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon violet">
|
||||
<el-icon><ChatDotSquare /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ questions.length }}</span>
|
||||
<span class="stat-label">总问题数</span>
|
||||
<div
|
||||
class="stat-card stat-total"
|
||||
:class="{ active: filterStatus === '' }"
|
||||
@click="filterStatus = ''"
|
||||
>
|
||||
<div class="stat-glow"></div>
|
||||
<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 class="stat-card">
|
||||
<div class="stat-icon cyan">
|
||||
<el-icon><MagicStick /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ generatedCount }}</span>
|
||||
<span class="stat-label">AI 生成</span>
|
||||
<div
|
||||
class="stat-card stat-completed"
|
||||
:class="{ active: filterStatus === 'generated' }"
|
||||
@click="filterStatus = filterStatus === 'generated' ? '' : 'generated'"
|
||||
>
|
||||
<div class="stat-glow"></div>
|
||||
<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 class="stat-card">
|
||||
<div class="stat-icon orange">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
<div
|
||||
class="stat-card stat-processing"
|
||||
: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 class="stat-info">
|
||||
<span class="stat-value">{{ manualCount }}</span>
|
||||
<span class="stat-label">手动添加</span>
|
||||
</div>
|
||||
<div
|
||||
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>
|
||||
|
||||
<!-- Question Table -->
|
||||
<div class="table-card">
|
||||
<el-table :data="questions" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="content" label="问题内容" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<div class="question-content">{{ row.content }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="answer" label="答案" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="answer-content">{{ row.answer || '-' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="question_type" label="类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="getTypeColor(row.question_type)" effect="dark">
|
||||
{{ getTypeName(row.question_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="source" label="来源" width="120" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-popconfirm title="确定删除此问题?" @confirm="handleDelete(row)">
|
||||
<template #reference>
|
||||
<el-button type="danger" size="small" text>删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- Question Container -->
|
||||
<div class="question-container" v-loading="loading && isInitialLoad">
|
||||
<!-- Empty State -->
|
||||
<div v-if="!loading && !isInitialLoad && filteredQuestions.length === 0" class="empty-state">
|
||||
<div class="empty-illustration">
|
||||
<div class="orbit orbit-1"></div>
|
||||
<div class="orbit orbit-2"></div>
|
||||
<div class="orbit orbit-3"></div>
|
||||
<div class="empty-core">
|
||||
<el-icon size="40"><ChatDotSquare /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="empty-title">暂无问答数据</h3>
|
||||
<p class="empty-desc">生成您的第一个问答数据集</p>
|
||||
<el-button type="primary" @click="showGenerateDialog = true" class="empty-btn">生成问题</el-button>
|
||||
</div>
|
||||
|
||||
<!-- Question Table -->
|
||||
<div v-else class="question-table-wrapper">
|
||||
<!-- Table Header -->
|
||||
<div class="table-header">
|
||||
<div class="table-select">
|
||||
<el-checkbox
|
||||
:model-value="isAllSelected"
|
||||
@change="toggleSelectAll"
|
||||
class="select-all"
|
||||
>
|
||||
<span v-if="selectedCount > 0" class="selected-text">已选择 {{ selectedCount }} 项</span>
|
||||
<span v-else>全选</span>
|
||||
</el-checkbox>
|
||||
</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>
|
||||
|
||||
<!-- Generate Dialog -->
|
||||
@@ -126,10 +229,12 @@ const route = useRoute()
|
||||
const projectId = computed(() => route.params.id)
|
||||
|
||||
const loading = ref(false)
|
||||
const isInitialLoad = ref(true)
|
||||
const generating = ref(false)
|
||||
const questions = ref([])
|
||||
const chunks = ref([])
|
||||
const showGenerateDialog = ref(false)
|
||||
const filterStatus = ref('')
|
||||
|
||||
const generateConfig = reactive({
|
||||
chunk_ids: [],
|
||||
@@ -137,19 +242,72 @@ const generateConfig = reactive({
|
||||
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 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 wasInitial = isInitialLoad.value
|
||||
loading.value = true
|
||||
try {
|
||||
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 })))
|
||||
} catch (error) {
|
||||
questions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (wasInitial) {
|
||||
isInitialLoad.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +340,8 @@ const handleDelete = async (question) => {
|
||||
}
|
||||
|
||||
const getTypeColor = (type) => {
|
||||
const map = { 'fact': 'success', 'summary': 'primary', 'reasoning': 'warning' }
|
||||
return map[type] || 'info'
|
||||
const map = { 'fact': '#22c55e', 'summary': '#818cf8', 'reasoning': '#f59e0b' }
|
||||
return map[type] || '#818cf8'
|
||||
}
|
||||
|
||||
const getTypeName = (type) => {
|
||||
@@ -191,111 +349,522 @@ const getTypeName = (type) => {
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
const getSourceName = (source) => {
|
||||
const map = { 'generated': 'AI生成', 'manual': '手动', 'failed': '失败' }
|
||||
return map[source] || source
|
||||
}
|
||||
|
||||
onMounted(() => fetchQuestions())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ========================
|
||||
CSS Variables
|
||||
======================== */
|
||||
.question-manage {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
--accent-cyan: #00d4ff;
|
||||
--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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.header-left h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
.header-left {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
color: var(--text-tertiary);
|
||||
color: var(--text-muted);
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
padding: 10px 20px;
|
||||
font-weight: 500;
|
||||
background: var(--accent-cyan) !important;
|
||||
border: none !important;
|
||||
color: #030407 !important;
|
||||
font-weight: 600;
|
||||
padding: 10px 22px;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.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;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px 22px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
.stat-icon-wrap {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
font-family: 'SF Mono', 'JetBrains Mono', monospace;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Table Card */
|
||||
.table-card {
|
||||
background: var(--bg-secondary);
|
||||
/* ========================
|
||||
Question Container
|
||||
======================== */
|
||||
.question-container {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
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;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.answer-content {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.5;
|
||||
max-height: 60px;
|
||||
.empty-btn {
|
||||
background: var(--accent-cyan) !important;
|
||||
border: none !important;
|
||||
color: #030407 !important;
|
||||
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;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -307,14 +876,43 @@ onMounted(() => fetchQuestions())
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
/* ========================
|
||||
Responsive
|
||||
======================== */
|
||||
@media (max-width: 900px) {
|
||||
.question-manage {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
.stats-grid {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user