2026-03-17 14:36:31 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="text-split">
|
2026-03-18 16:08:16 +08:00
|
|
|
<!-- Header -->
|
2026-03-17 14:36:31 +08:00
|
|
|
<div class="page-header">
|
|
|
|
|
<div class="header-left">
|
2026-03-18 16:08:16 +08:00
|
|
|
<h2 class="page-title">分割生成</h2>
|
|
|
|
|
<p class="page-subtitle">选择文件进行智能分割</p>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
|
|
|
|
<div class="header-actions">
|
2026-03-18 16:08:16 +08:00
|
|
|
<el-button @click="refreshFiles" class="refresh-btn">
|
2026-03-17 14:36:31 +08:00
|
|
|
<el-icon><Refresh /></el-icon>
|
2026-03-18 16:08:16 +08:00
|
|
|
<span>刷新</span>
|
2026-03-17 14:36:31 +08:00
|
|
|
</el-button>
|
2026-03-18 16:08:16 +08:00
|
|
|
<el-button
|
|
|
|
|
type="primary"
|
|
|
|
|
@click="handleBatchGenerateQA"
|
|
|
|
|
:disabled="!hasProcessedFiles"
|
|
|
|
|
class="generate-btn"
|
|
|
|
|
>
|
|
|
|
|
<el-icon><ChatDotSquare /></el-icon>
|
|
|
|
|
<span>批量生成问答对</span>
|
2026-03-17 14:36:31 +08:00
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
<!-- Stats Cards -->
|
|
|
|
|
<div class="stats-grid">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-icon total">
|
|
|
|
|
<el-icon><Document /></el-icon>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<span class="stat-value">{{ files.length }}</span>
|
|
|
|
|
<span class="stat-label">总文件</span>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-18 16:08:16 +08:00
|
|
|
<div class="stat-card success">
|
|
|
|
|
<div class="stat-icon">
|
|
|
|
|
<el-icon><CircleCheckFilled /></el-icon>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
2026-03-18 16:08:16 +08:00
|
|
|
<div class="stat-content">
|
|
|
|
|
<span class="stat-value">{{ completedFiles }}</span>
|
|
|
|
|
<span class="stat-label">已分割</span>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
2026-03-18 16:08:16 +08:00
|
|
|
</div>
|
|
|
|
|
<div class="stat-card processing">
|
|
|
|
|
<div class="stat-icon">
|
|
|
|
|
<el-icon><Loading /></el-icon>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
2026-03-18 16:08:16 +08:00
|
|
|
<div class="stat-content">
|
|
|
|
|
<span class="stat-value">{{ processingCount }}</span>
|
|
|
|
|
<span class="stat-label">分割中</span>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-18 16:08:16 +08:00
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-icon chunks">
|
|
|
|
|
<el-icon><List /></el-icon>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<span class="stat-value">{{ totalChunks }}</span>
|
|
|
|
|
<span class="stat-label">总文本块</span>
|
|
|
|
|
</div>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
<!-- File List / Split View -->
|
|
|
|
|
<div class="content-area">
|
|
|
|
|
<!-- Empty State -->
|
|
|
|
|
<div v-if="!loading && files.length === 0" class="empty-state">
|
|
|
|
|
<div class="empty-icon">
|
|
|
|
|
<el-icon size="56"><FolderOpened /></el-icon>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
2026-03-18 16:08:16 +08:00
|
|
|
<h3 class="empty-title">暂无可分割文件</h3>
|
|
|
|
|
<p class="empty-desc">请先在文件管理中上传文档</p>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
<!-- File Table -->
|
|
|
|
|
<div v-else-if="!selectedFile" class="files-table-wrapper">
|
|
|
|
|
<div class="table-header">
|
|
|
|
|
<span class="table-title">文件列表</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="files-list">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(file, index) in files"
|
|
|
|
|
:key="file.id"
|
|
|
|
|
class="file-row"
|
|
|
|
|
:class="{ processing: file.status === 'processing' }"
|
|
|
|
|
:style="{ '--delay': index * 0.03 + 's' }"
|
|
|
|
|
@click="handleFileClick(file)"
|
|
|
|
|
>
|
|
|
|
|
<div class="col-icon">
|
|
|
|
|
<div class="file-type-icon" :style="{ background: getFileBg(file.file_type) }">
|
|
|
|
|
<el-icon size="18" color="white">
|
|
|
|
|
<component :is="getFileIcon(file.file_type)" />
|
|
|
|
|
</el-icon>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-name">
|
|
|
|
|
<span class="file-name">{{ file.filename }}</span>
|
|
|
|
|
<span class="file-meta">{{ formatSize(file.size) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-chunks">
|
|
|
|
|
<span v-if="fileChunks[file.id]" class="chunk-count">
|
|
|
|
|
{{ fileChunks[file.id] }} 块
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else class="chunk-count empty">-</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-status">
|
|
|
|
|
<div v-if="file.status === 'processing'" class="status-badge processing">
|
|
|
|
|
<el-icon class="spin" size="12"><Loading /></el-icon>
|
|
|
|
|
<span>分割中</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else-if="fileChunks[file.id]" class="status-badge success">
|
|
|
|
|
<el-icon size="12"><CircleCheckFilled /></el-icon>
|
|
|
|
|
<span>已完成</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="status-badge pending">
|
|
|
|
|
<el-icon size="12"><Clock /></el-icon>
|
|
|
|
|
<span>待分割</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-action">
|
|
|
|
|
<el-icon><ArrowRight /></el-icon>
|
|
|
|
|
</div>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
<!-- Split Detail View -->
|
|
|
|
|
<div v-else class="split-detail">
|
|
|
|
|
<div class="detail-header">
|
|
|
|
|
<el-button text @click="selectedFile = null" class="back-btn">
|
|
|
|
|
<el-icon><ArrowLeft /></el-icon>
|
|
|
|
|
<span>返回文件列表</span>
|
|
|
|
|
</el-button>
|
|
|
|
|
<div class="file-info">
|
|
|
|
|
<div class="file-type-icon small" :style="{ background: getFileBg(selectedFile.file_type) }">
|
|
|
|
|
<el-icon size="14" color="white">
|
|
|
|
|
<component :is="getFileIcon(selectedFile.file_type)" />
|
|
|
|
|
</el-icon>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="file-name">{{ selectedFile.filename }}</span>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
2026-03-18 16:08:16 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Split Config -->
|
|
|
|
|
<div class="config-card">
|
|
|
|
|
<div class="config-title">分割配置</div>
|
|
|
|
|
<div class="config-form">
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>分割算法</label>
|
|
|
|
|
<el-select v-model="splitConfig.method" placeholder="选择算法">
|
|
|
|
|
<el-option label="Markdown 结构分割" value="markdown_structure" />
|
|
|
|
|
<el-option label="递归字符分割" value="recursive" />
|
|
|
|
|
<el-option label="Token 数量分割" value="token" />
|
|
|
|
|
<el-option label="代码感知分割" value="code" />
|
|
|
|
|
<el-option label="自定义分隔符" value="custom" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row sliders">
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>块大小: {{ splitConfig.chunk_size }}</label>
|
|
|
|
|
<el-slider
|
|
|
|
|
v-model="splitConfig.chunk_size"
|
|
|
|
|
:min="100"
|
|
|
|
|
:max="2000"
|
|
|
|
|
:step="100"
|
|
|
|
|
:marks="{100: '100', 500: '500', 1000: '1k', 2000: '2k'}"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>重叠字符: {{ splitConfig.overlap }}</label>
|
|
|
|
|
<el-slider
|
|
|
|
|
v-model="splitConfig.overlap"
|
|
|
|
|
:min="0"
|
|
|
|
|
:max="500"
|
|
|
|
|
:step="50"
|
|
|
|
|
:marks="{0: '0', 250: '250', 500: '500'}"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-item" v-if="splitConfig.method === 'custom'">
|
|
|
|
|
<label>自定义分隔符</label>
|
|
|
|
|
<el-input v-model="splitConfig.separator" placeholder="例如: \n\n 或 || 或 ---" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="config-actions">
|
|
|
|
|
<el-button type="primary" @click="handleSplit" :loading="splitting" class="split-btn">
|
|
|
|
|
<el-icon><CaretRight /></el-icon>
|
|
|
|
|
开始分割
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Chunks List -->
|
|
|
|
|
<div class="chunks-card" v-if="chunks.length > 0">
|
|
|
|
|
<div class="chunks-header">
|
|
|
|
|
<div class="chunks-title">
|
|
|
|
|
<el-icon><List /></el-icon>
|
|
|
|
|
<span>文本块 ({{ chunks.length }})</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-tag type="primary" effect="dark">
|
|
|
|
|
总计 {{ totalWords }} 字
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="chunks-list" v-loading="loadingChunks">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(chunk, index) in chunks"
|
|
|
|
|
:key="chunk.id"
|
|
|
|
|
class="chunk-item"
|
|
|
|
|
>
|
|
|
|
|
<div class="chunk-header">
|
|
|
|
|
<div class="chunk-badge">{{ index + 1 }}</div>
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="chunk.name"
|
|
|
|
|
class="chunk-name-input"
|
|
|
|
|
placeholder="输入块名称"
|
|
|
|
|
@blur="saveChunkName(chunk)"
|
|
|
|
|
/>
|
|
|
|
|
<span class="chunk-meta">{{ chunk.word_count || 0 }} 字</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="chunk.content"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="4"
|
|
|
|
|
class="chunk-content-input"
|
|
|
|
|
@blur="saveChunkContent(chunk)"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-17 14:36:31 +08:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
2026-03-18 16:08:16 +08:00
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-03-17 14:36:31 +08:00
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
import { fileApi, chunkApi } from '@/api'
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
2026-03-18 16:08:16 +08:00
|
|
|
const router = useRouter()
|
2026-03-17 14:36:31 +08:00
|
|
|
const projectId = computed(() => route.params.id)
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
2026-03-18 16:08:16 +08:00
|
|
|
const loadingChunks = ref(false)
|
2026-03-17 14:36:31 +08:00
|
|
|
const splitting = ref(false)
|
|
|
|
|
const files = ref([])
|
|
|
|
|
const selectedFile = ref(null)
|
2026-03-18 16:08:16 +08:00
|
|
|
const chunks = ref([])
|
|
|
|
|
const fileChunks = ref({})
|
2026-03-17 14:36:31 +08:00
|
|
|
|
|
|
|
|
const splitConfig = reactive({
|
|
|
|
|
method: 'recursive',
|
|
|
|
|
chunk_size: 500,
|
|
|
|
|
overlap: 50,
|
|
|
|
|
separator: '\n\n'
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
const completedFiles = computed(() => {
|
|
|
|
|
return Object.keys(fileChunks.value).length
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const processingCount = computed(() => {
|
|
|
|
|
return files.value.filter(f => f.status === 'processing').length
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const totalChunks = computed(() => {
|
|
|
|
|
return Object.values(fileChunks.value).reduce((sum, count) => sum + count, 0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const hasProcessedFiles = computed(() => {
|
|
|
|
|
return Object.keys(fileChunks.value).length > 0
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-17 14:36:31 +08:00
|
|
|
const totalWords = computed(() => {
|
|
|
|
|
return chunks.value.reduce((sum, c) => sum + (c.word_count || 0), 0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const fetchFiles = async () => {
|
2026-03-18 16:08:16 +08:00
|
|
|
loading.value = true
|
2026-03-17 14:36:31 +08:00
|
|
|
try {
|
|
|
|
|
const res = await fileApi.list(projectId.value)
|
2026-03-18 16:08:16 +08:00
|
|
|
files.value = res.data || []
|
|
|
|
|
// 获取每个文件的 chunk 数量
|
|
|
|
|
await fetchChunksCount()
|
2026-03-17 14:36:31 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error)
|
2026-03-18 16:08:16 +08:00
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fetchChunksCount = async () => {
|
|
|
|
|
const counts = {}
|
|
|
|
|
for (const file of files.value) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await chunkApi.list(projectId.value, { file_id: file.id })
|
|
|
|
|
const chunks = res.data?.chunks || []
|
|
|
|
|
if (chunks.length > 0) {
|
|
|
|
|
counts[file.id] = chunks.length
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e)
|
|
|
|
|
}
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
2026-03-18 16:08:16 +08:00
|
|
|
fileChunks.value = counts
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fetchChunks = async () => {
|
|
|
|
|
if (!selectedFile.value) return
|
2026-03-18 16:08:16 +08:00
|
|
|
loadingChunks.value = true
|
2026-03-17 14:36:31 +08:00
|
|
|
try {
|
|
|
|
|
const res = await chunkApi.list(projectId.value, { file_id: selectedFile.value.id })
|
2026-03-18 16:08:16 +08:00
|
|
|
chunks.value = res.data?.chunks || []
|
2026-03-17 14:36:31 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('获取分割结果失败')
|
|
|
|
|
} finally {
|
2026-03-18 16:08:16 +08:00
|
|
|
loadingChunks.value = false
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
const handleFileClick = (file) => {
|
|
|
|
|
selectedFile.value = file
|
2026-03-17 14:36:31 +08:00
|
|
|
fetchChunks()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSplit = async () => {
|
|
|
|
|
if (!selectedFile.value) {
|
|
|
|
|
ElMessage.warning('请先选择文件')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
splitting.value = true
|
|
|
|
|
try {
|
|
|
|
|
await chunkApi.split(projectId.value, { file_id: selectedFile.value.id, ...splitConfig })
|
|
|
|
|
ElMessage.success('分割任务已启动')
|
2026-03-18 16:08:16 +08:00
|
|
|
// 等待一下再获取结果
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
fetchChunks()
|
|
|
|
|
fetchFiles()
|
|
|
|
|
}, 2000)
|
2026-03-17 14:36:31 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('分割失败')
|
|
|
|
|
} finally {
|
|
|
|
|
splitting.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
const saveChunkName = async (chunk) => {
|
|
|
|
|
try {
|
|
|
|
|
await chunkApi.update(projectId.value, chunk.id, { name: chunk.name })
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-17 14:36:31 +08:00
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
const saveChunkContent = async (chunk) => {
|
|
|
|
|
try {
|
|
|
|
|
await chunkApi.update(projectId.value, chunk.id, { content: chunk.content })
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleBatchGenerateQA = () => {
|
|
|
|
|
if (!hasProcessedFiles.value) {
|
|
|
|
|
ElMessage.warning('请先分割文件')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ElMessage.info('跳转到问答生成页面')
|
|
|
|
|
router.push(`/project/${projectId.value}/questions`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const refreshFiles = () => {
|
2026-03-17 14:36:31 +08:00
|
|
|
fetchFiles()
|
2026-03-18 16:08:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatSize = (bytes) => {
|
|
|
|
|
if (!bytes) return '0 B'
|
|
|
|
|
const units = ['B', 'KB', 'MB', 'GB']
|
|
|
|
|
let i = 0
|
|
|
|
|
while (bytes >= 1024 && i < units.length - 1) {
|
|
|
|
|
bytes /= 1024
|
|
|
|
|
i++
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
2026-03-18 16:08:16 +08:00
|
|
|
return `${bytes.toFixed(1)} ${units[i]}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getFileBg = (type) => {
|
|
|
|
|
const colors = {
|
|
|
|
|
pdf: '#ef4444',
|
|
|
|
|
docx: '#3b82f6',
|
|
|
|
|
xlsx: '#22c55e',
|
|
|
|
|
csv: '#f59e0b',
|
|
|
|
|
md: '#8b5cf6',
|
|
|
|
|
txt: '#6b7280',
|
|
|
|
|
epub: '#ec4899'
|
|
|
|
|
}
|
|
|
|
|
return colors[type] || '#6b7280'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getFileIcon = (type) => {
|
|
|
|
|
const icons = {
|
|
|
|
|
pdf: 'Document',
|
|
|
|
|
docx: 'Document',
|
|
|
|
|
xlsx: 'Grid',
|
|
|
|
|
csv: 'Grid',
|
|
|
|
|
md: 'Document',
|
|
|
|
|
txt: 'Document',
|
|
|
|
|
epub: 'Book'
|
|
|
|
|
}
|
|
|
|
|
return icons[type] || 'Document'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
fetchFiles()
|
2026-03-17 14:36:31 +08:00
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.text-split {
|
2026-03-18 16:08:16 +08:00
|
|
|
--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;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Header */
|
|
|
|
|
.page-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
2026-03-18 16:08:16 +08:00
|
|
|
align-items: flex-start;
|
2026-03-17 14:36:31 +08:00
|
|
|
margin-bottom: 28px;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.header-left {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.page-subtitle {
|
2026-03-17 14:36:31 +08:00
|
|
|
font-size: 14px;
|
2026-03-18 16:08:16 +08:00
|
|
|
color: var(--text-muted);
|
|
|
|
|
margin: 6px 0 0;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.refresh-btn {
|
|
|
|
|
background: var(--bg-card) !important;
|
|
|
|
|
border: 1px solid var(--border-subtle) !important;
|
|
|
|
|
color: var(--text-secondary) !important;
|
|
|
|
|
font-weight: 500;
|
2026-03-17 14:36:31 +08:00
|
|
|
padding: 10px 18px;
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.refresh-btn:hover {
|
|
|
|
|
background: var(--bg-hover) !important;
|
|
|
|
|
border-color: var(--accent-cyan) !important;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.generate-btn {
|
|
|
|
|
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);
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.generate-btn:hover:not(:disabled) {
|
|
|
|
|
box-shadow: 0 0 35px var(--accent-cyan-glow);
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.generate-btn:disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Stats Grid */
|
|
|
|
|
.stats-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
background: var(--bg-card);
|
|
|
|
|
border: 1px solid var(--border-subtle);
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
padding: 20px;
|
2026-03-17 14:36:31 +08:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-03-18 16:08:16 +08:00
|
|
|
gap: 16px;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
transition: all 0.2s ease;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.stat-card::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
height: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card.total::before { background: linear-gradient(90deg, #8b5cf6, #a78bfa); }
|
|
|
|
|
.stat-card.success::before { background: linear-gradient(90deg, #22c55e, #4ade80); }
|
|
|
|
|
.stat-card.processing::before { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
|
|
|
|
|
|
|
|
|
|
.stat-card:hover {
|
|
|
|
|
border-color: var(--border-active);
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
width: 48px;
|
|
|
|
|
height: 48px;
|
2026-03-17 14:36:31 +08:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
border-radius: var(--radius-md);
|
2026-03-18 16:08:16 +08:00
|
|
|
font-size: 22px;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.stat-icon.total { background: rgba(139, 92, 246, 0.15); color: #a78bfa; }
|
|
|
|
|
.stat-icon.success { background: rgba(34, 197, 94, 0.15); color: #4ade80; }
|
|
|
|
|
.stat-icon.processing { background: rgba(245, 158, 11, 0.15); color: #fbbf24; }
|
|
|
|
|
.stat-icon.chunks { background: var(--accent-cyan-dim); color: var(--accent-cyan); }
|
2026-03-17 14:36:31 +08:00
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.stat-content {
|
2026-03-17 14:36:31 +08:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.stat-value {
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
font-family: 'SF Mono', Monaco, monospace;
|
|
|
|
|
color: #fff;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.stat-label {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
margin-top: 2px;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
/* Content Area */
|
|
|
|
|
.content-area {
|
|
|
|
|
min-height: 400px;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Empty State */
|
|
|
|
|
.empty-state {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 80px 20px;
|
2026-03-18 16:08:16 +08:00
|
|
|
background: var(--bg-card);
|
|
|
|
|
border: 2px dashed var(--border-subtle);
|
2026-03-17 14:36:31 +08:00
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.empty-icon {
|
2026-03-17 14:36:31 +08:00
|
|
|
width: 100px;
|
|
|
|
|
height: 100px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-03-18 16:08:16 +08:00
|
|
|
background: var(--bg-hover);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
border: 1px solid var(--border-subtle);
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.empty-icon .el-icon {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-title {
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
margin: 0 0 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-desc {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Files Table */
|
|
|
|
|
.files-table-wrapper {
|
|
|
|
|
background: var(--bg-card);
|
|
|
|
|
border: 1px solid var(--border-subtle);
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-header {
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.files-list {
|
|
|
|
|
max-height: 500px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 50px 1fr 80px 100px 40px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
padding: 14px 20px;
|
|
|
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
animation: slideIn 0.3s ease backwards;
|
|
|
|
|
animation-delay: var(--delay);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-row:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-row:hover {
|
|
|
|
|
background: var(--bg-hover);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-row.processing {
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes slideIn {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(-10px);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.col-icon {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-type-icon {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.col-name {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-name {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #fff;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-meta {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.col-chunks {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunk-count {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunk-count.empty {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.col-status {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-badge {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-badge.processing {
|
|
|
|
|
background: rgba(245, 158, 11, 0.15);
|
|
|
|
|
color: #fbbf24;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-badge.success {
|
|
|
|
|
background: rgba(34, 197, 94, 0.15);
|
|
|
|
|
color: #4ade80;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-badge.pending {
|
|
|
|
|
background: rgba(107, 114, 128, 0.15);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.col-action {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-row:hover .col-action {
|
|
|
|
|
color: var(--accent-cyan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spin {
|
|
|
|
|
animation: spin 1s linear infinite;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
from { transform: rotate(0deg); }
|
|
|
|
|
to { transform: rotate(360deg); }
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
/* Split Detail */
|
|
|
|
|
.split-detail {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.back-btn {
|
|
|
|
|
color: var(--text-secondary) !important;
|
|
|
|
|
font-weight: 500;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.back-btn:hover {
|
|
|
|
|
color: var(--accent-cyan) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-info {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-type-icon.small {
|
|
|
|
|
width: 28px;
|
|
|
|
|
height: 28px;
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-info .file-name {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Config Card */
|
|
|
|
|
.config-card {
|
|
|
|
|
background: var(--bg-card);
|
|
|
|
|
border: 1px solid var(--border-subtle);
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
padding: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.config-title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.config-form {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
|
|
gap: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-item label {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.config-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
border-top: 1px solid var(--border-subtle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.split-btn {
|
|
|
|
|
background: var(--accent-cyan) !important;
|
|
|
|
|
border: none !important;
|
|
|
|
|
color: #030407 !important;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
padding: 12px 28px;
|
|
|
|
|
border-radius: var(--radius-md);
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Chunks Card */
|
|
|
|
|
.chunks-card {
|
2026-03-18 16:08:16 +08:00
|
|
|
background: var(--bg-card);
|
2026-03-17 14:36:31 +08:00
|
|
|
border: 1px solid var(--border-subtle);
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunks-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunks-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.chunks-list {
|
|
|
|
|
max-height: 500px;
|
2026-03-17 14:36:31 +08:00
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunk-item {
|
2026-03-18 16:08:16 +08:00
|
|
|
padding: 16px 20px;
|
2026-03-17 14:36:31 +08:00
|
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunk-item:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunk-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunk-badge {
|
|
|
|
|
width: 28px;
|
|
|
|
|
height: 28px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-03-18 16:08:16 +08:00
|
|
|
background: linear-gradient(135deg, var(--accent-cyan), #06b6d4);
|
2026-03-17 14:36:31 +08:00
|
|
|
color: white;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
2026-03-18 16:08:16 +08:00
|
|
|
flex-shrink: 0;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.chunk-name-input {
|
2026-03-17 14:36:31 +08:00
|
|
|
flex: 1;
|
2026-03-18 16:08:16 +08:00
|
|
|
max-width: 300px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunk-name-input :deep(.el-input__wrapper) {
|
|
|
|
|
background: var(--bg-hover);
|
|
|
|
|
border: 1px solid var(--border-subtle);
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunk-name-input :deep(.el-input__inner) {
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-weight: 500;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chunk-meta {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-muted);
|
2026-03-18 16:08:16 +08:00
|
|
|
flex-shrink: 0;
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.chunk-content-input :deep(.el-textarea__inner) {
|
|
|
|
|
background: var(--bg-hover);
|
|
|
|
|
border: 1px solid var(--border-subtle);
|
|
|
|
|
box-shadow: none;
|
2026-03-17 14:36:31 +08:00
|
|
|
color: var(--text-secondary);
|
2026-03-18 16:08:16 +08:00
|
|
|
font-size: 13px;
|
2026-03-17 14:36:31 +08:00
|
|
|
line-height: 1.7;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:08:16 +08:00
|
|
|
.chunk-content-input :deep(.el-textarea__inner:focus) {
|
|
|
|
|
border-color: var(--accent-cyan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Responsive */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.stats-grid {
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-row {
|
|
|
|
|
grid-template-columns: 40px 1fr 60px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.col-chunks,
|
|
|
|
|
.col-status,
|
|
|
|
|
.col-action {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
2026-03-17 14:36:31 +08:00
|
|
|
}
|
|
|
|
|
</style>
|