Compare commits
3 Commits
fa7829657f
...
a280b4f014
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a280b4f014 | ||
|
|
135f75e6be | ||
|
|
45b77a44c6 |
@@ -17,7 +17,7 @@ from app.core.crud import CRUDBase
|
||||
from app.core.logging import log_success, log_failure
|
||||
from app.models.models import Chunk, File
|
||||
from app.schemas.chunk import ChunkResponse
|
||||
from app.schemas.chunk import ChunkCreateSchema
|
||||
from app.schemas.chunk import ChunkCreateSchema, ChunkUpdateSchema
|
||||
from app.services.text_splitter.splitter import get_splitter
|
||||
from markitdown import MarkItDown
|
||||
|
||||
@@ -209,7 +209,7 @@ async def list_chunks(
|
||||
limit=page_size,
|
||||
filters=filters,
|
||||
order_by="created_at",
|
||||
descending=True
|
||||
descending=False
|
||||
)
|
||||
|
||||
chunk_responses = [ChunkResponse.model_validate(c) for c in chunks]
|
||||
@@ -239,7 +239,7 @@ async def get_chunk(
|
||||
async def update_chunk(
|
||||
project_id: UUID,
|
||||
chunk_id: UUID,
|
||||
chunk: ChunkCreateSchema,
|
||||
chunk: ChunkUpdateSchema,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Update chunk"""
|
||||
|
||||
@@ -158,7 +158,7 @@ async def upload_file(
|
||||
# txt, md 等直接读取
|
||||
text_content = file_path_obj.read_text(encoding='utf-8')
|
||||
|
||||
# 保存到 ready 目录
|
||||
# 保存到 ready 目录,使用 {uuid}.md 格式
|
||||
ready_dir = get_project_ready_dir(str(project_id_val))
|
||||
ready_filename = f"{file_id}.md"
|
||||
ready_path = ready_dir / ready_filename
|
||||
|
||||
@@ -129,43 +129,59 @@ html, body, #app {
|
||||
.mesh-gradient {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.6;
|
||||
animation: float 20s ease-in-out infinite;
|
||||
filter: blur(100px);
|
||||
opacity: 0.5;
|
||||
animation: sciFiFloat 15s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.mesh-1 {
|
||||
width: 800px;
|
||||
height: 800px;
|
||||
background: radial-gradient(circle, rgba(0, 212, 255, 0.4) 0%, rgba(6, 182, 212, 0.2) 40%, transparent 70%);
|
||||
top: -300px;
|
||||
right: -200px;
|
||||
width: 900px;
|
||||
height: 900px;
|
||||
background: radial-gradient(circle, rgba(0, 212, 255, 0.5) 0%, rgba(6, 182, 212, 0.25) 40%, transparent 70%);
|
||||
top: -400px;
|
||||
left: -300px;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 18s;
|
||||
}
|
||||
|
||||
.mesh-2 {
|
||||
width: 700px;
|
||||
height: 700px;
|
||||
background: radial-gradient(circle, rgba(124, 58, 237, 0.4) 0%, rgba(139, 92, 246, 0.2) 40%, transparent 70%);
|
||||
bottom: -250px;
|
||||
left: -200px;
|
||||
animation-delay: -7s;
|
||||
width: 800px;
|
||||
height: 800px;
|
||||
background: radial-gradient(circle, rgba(124, 58, 237, 0.5) 0%, rgba(139, 92, 246, 0.25) 40%, transparent 70%);
|
||||
bottom: -300px;
|
||||
right: -200px;
|
||||
animation-delay: -5s;
|
||||
animation-duration: 20s;
|
||||
}
|
||||
|
||||
.mesh-3 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, rgba(236, 72, 153, 0.3) 0%, rgba(168, 85, 247, 0.15) 40%, transparent 70%);
|
||||
top: 30%;
|
||||
left: 25%;
|
||||
animation-delay: -14s;
|
||||
width: 700px;
|
||||
height: 700px;
|
||||
background: radial-gradient(circle, rgba(236, 72, 153, 0.4) 0%, rgba(168, 85, 247, 0.2) 40%, transparent 70%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation-delay: -10s;
|
||||
animation-duration: 22s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(60px, -50px) scale(1.1); }
|
||||
50% { transform: translate(-40px, 40px) scale(0.95); }
|
||||
75% { transform: translate(-50px, -30px) scale(1.05); }
|
||||
@keyframes sciFiFloat {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1) rotate(0deg);
|
||||
opacity: 0.4;
|
||||
}
|
||||
25% {
|
||||
transform: translate(200px, -150px) scale(1.2) rotate(10deg);
|
||||
opacity: 0.65;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-150px, 100px) scale(0.85) rotate(-5deg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
75% {
|
||||
transform: translate(100px, 150px) scale(1.15) rotate(8deg);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.noise-overlay {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title=""
|
||||
width="420px"
|
||||
width="460px"
|
||||
class="delete-dialog"
|
||||
:show-close="false"
|
||||
align-center
|
||||
@@ -11,16 +11,23 @@
|
||||
>
|
||||
<template #header>
|
||||
<div class="delete-dialog-header">
|
||||
<div class="delete-icon-wrapper">
|
||||
<el-icon size="28"><WarningFilled /></el-icon>
|
||||
<div class="delete-header-top">
|
||||
<div class="delete-icon-wrapper">
|
||||
<el-icon size="26"><WarningFilled /></el-icon>
|
||||
</div>
|
||||
<div class="delete-kicker">Danger Zone</div>
|
||||
</div>
|
||||
<div class="delete-title-group">
|
||||
<h3>{{ title }}</h3>
|
||||
<p v-if="detailText" class="detail-text">{{ detailText }}</p>
|
||||
</div>
|
||||
<h3>{{ title }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div class="delete-dialog-body">
|
||||
<p>
|
||||
确定要删除 <strong>{{ itemName }}</strong> 吗?
|
||||
</p>
|
||||
<div class="delete-target-card">
|
||||
<span class="target-label">目标对象</span>
|
||||
<strong class="target-name">{{ itemName }}</strong>
|
||||
</div>
|
||||
<p class="warning-text">{{ warningText }}</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -38,7 +45,7 @@
|
||||
class="btn-delete"
|
||||
>
|
||||
<el-icon v-if="!loading"><Delete /></el-icon>
|
||||
确认删除
|
||||
{{ confirmText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -63,6 +70,14 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '此操作不可恢复,所有相关数据将被永久删除'
|
||||
},
|
||||
detailText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确认删除'
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -83,18 +98,25 @@ const handleCancel = () => {
|
||||
|
||||
<style scoped>
|
||||
.delete-dialog :deep(.el-dialog) {
|
||||
background: linear-gradient(145deg, #1a1a2e 0%, #16162a 100%);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 30px rgba(239, 68, 68, 0.1);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(239, 68, 68, 0.14), transparent 24%),
|
||||
radial-gradient(circle at bottom right, rgba(251, 146, 60, 0.08), transparent 20%),
|
||||
linear-gradient(180deg, #11151d 0%, #0d1118 100%);
|
||||
border: 1px solid rgba(239, 68, 68, 0.18);
|
||||
border-radius: 22px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 24px 80px rgba(0, 0, 0, 0.5),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.delete-dialog :deep(.el-dialog__header) {
|
||||
padding: 24px 24px 12px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.delete-dialog :deep(.el-dialog__body) {
|
||||
padding: 0 24px 24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.delete-dialog :deep(.el-dialog__footer) {
|
||||
@@ -102,87 +124,138 @@ const handleCancel = () => {
|
||||
}
|
||||
|
||||
.delete-dialog-header {
|
||||
padding: 24px 24px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent);
|
||||
}
|
||||
|
||||
.delete-header-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.delete-icon-wrapper {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(239, 68, 68, 0.1) 100%);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 50%;
|
||||
color: #ef4444;
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.18), rgba(249, 115, 22, 0.08));
|
||||
border: 1px solid rgba(239, 68, 68, 0.24);
|
||||
border-radius: 14px;
|
||||
color: #fb7185;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.delete-kicker {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(251, 113, 133, 0.72);
|
||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
||||
}
|
||||
|
||||
.delete-title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.delete-dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
color: #f8fafc;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
margin: 0;
|
||||
color: rgba(226, 232, 240, 0.72);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.delete-dialog-body {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
padding: 22px 24px 24px;
|
||||
}
|
||||
|
||||
.delete-dialog-body p {
|
||||
.delete-target-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.target-label {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(148, 163, 184, 0.65);
|
||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
||||
}
|
||||
|
||||
.target-name {
|
||||
color: #f8fafc;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
margin: 0;
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.delete-dialog-body p strong {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.delete-dialog-body .warning-text {
|
||||
margin-top: 8px;
|
||||
color: #ef4444;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.delete-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 20px 24px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding: 18px 24px 24px;
|
||||
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.14));
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.btn-cancel-delete {
|
||||
padding: 10px 24px;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
min-width: 108px;
|
||||
padding: 10px 20px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 10px;
|
||||
color: #ccc;
|
||||
color: rgba(226, 232, 240, 0.8);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-cancel-delete:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.16);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
padding: 10px 24px;
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
min-width: 128px;
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 55%, #b91c1c 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 10px 30px rgba(239, 68, 68, 0.18);
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
|
||||
box-shadow: 0 14px 34px rgba(239, 68, 68, 0.28);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,13 +32,6 @@
|
||||
<span class="nav-dot"></span>
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<router-link to="/" class="home-link">
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
<span>返回首页</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
@@ -296,37 +289,6 @@ onMounted(() => fetchProject())
|
||||
}
|
||||
|
||||
/* Sidebar Footer */
|
||||
.sidebar-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.home-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.home-link:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.home-link span {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .home-link span {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
@@ -348,8 +310,7 @@ onMounted(() => fetchProject())
|
||||
}
|
||||
|
||||
.sidebar .project-details,
|
||||
.sidebar .nav-label,
|
||||
.sidebar .home-link span {
|
||||
.sidebar .nav-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user