拆分 DatasetVersionBar、DatasetRecordTable、DatasetRawPreview、DatasetRecordEditorDialog 子组件,DatasetPreviewView 聚焦编排,预览类型独立到 preview/types.ts,回归脚本适配。
768 lines
21 KiB
Vue
768 lines
21 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
import { onBeforeRouteLeave, useRoute } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import PageCard from '@/components/PageCard.vue'
|
|
import {
|
|
activateDatasetFileVersion,
|
|
createDatasetFileVersion,
|
|
downloadFileUrl,
|
|
getDataset,
|
|
getDatasetFileVersionContent,
|
|
getDatasetFileVersions,
|
|
} from '@/api/modules/dataset'
|
|
import { DATASET_TYPE_MAP, STORAGE_MAP } from '@/constants'
|
|
import { parseDatasetRecords, updateDatasetRecord } from './datasetRecords'
|
|
import type { DatasetRecord } from './datasetRecords'
|
|
import DatasetVersionBar from './preview/DatasetVersionBar.vue'
|
|
import DatasetRecordTable from './preview/DatasetRecordTable.vue'
|
|
import DatasetRecordEditorDialog from './preview/DatasetRecordEditorDialog.vue'
|
|
import DatasetRawPreview from './preview/DatasetRawPreview.vue'
|
|
import type { EditField } from './preview/types'
|
|
import type { DatasetFile, DatasetItem, DatasetVersion } from '@/types'
|
|
|
|
const route = useRoute()
|
|
const datasetId = route.params.id as string
|
|
|
|
const loading = ref(false)
|
|
const previewLoading = ref(false)
|
|
const dataset = ref<DatasetItem | null>(null)
|
|
const selectedFileId = ref('')
|
|
const previewContent = ref('')
|
|
const baseVersionContent = ref('')
|
|
const previewError = ref('')
|
|
const editorVisible = ref(false)
|
|
const editingRecord = ref<DatasetRecord | null>(null)
|
|
const editFields = ref<EditField[]>([])
|
|
const rawDraft = ref('')
|
|
const originalDraft = ref('')
|
|
const savingRecord = ref(false)
|
|
const savingVersion = ref(false)
|
|
const versions = ref<DatasetVersion[]>([])
|
|
const selectedVersionId = ref('')
|
|
const activeVersionId = ref('')
|
|
const loadedVersionId = ref('')
|
|
const versionLoading = ref(false)
|
|
const activatingVersion = ref(false)
|
|
let versionRequestId = 0
|
|
|
|
const files = computed<DatasetFile[]>(() => dataset.value?.files || [])
|
|
const selectedFile = computed(() => files.value.find((file) => fileKey(file) === selectedFileId.value))
|
|
const totalLines = computed(() => previewContent.value ? previewContent.value.split('\n').length : 0)
|
|
const recordResult = computed(() => parseDatasetRecords(previewContent.value, selectedFile.value?.name || ''))
|
|
const isRecordFile = computed(() => recordResult.value.supported)
|
|
const records = computed(() => recordResult.value.records)
|
|
const viewedVersion = computed(() => versions.value.find((item) => item.id === loadedVersionId.value))
|
|
const isViewingActiveVersion = computed(() => Boolean(loadedVersionId.value) && loadedVersionId.value === activeVersionId.value)
|
|
const nextVersionNumber = computed(() => Math.max(0, ...versions.value.map((item) => item.version)) + 1)
|
|
const currentDraft = computed(() => editingRecord.value?.kind === 'object'
|
|
? JSON.stringify(editFields.value)
|
|
: rawDraft.value)
|
|
const hasEditorDraftChanges = computed(() => editorVisible.value && currentDraft.value !== originalDraft.value)
|
|
const hasPendingVersionChanges = computed(() => previewContent.value !== baseVersionContent.value)
|
|
const hasUnsavedChanges = computed(() => hasEditorDraftChanges.value || hasPendingVersionChanges.value)
|
|
const datasetType = computed(() => {
|
|
const value = String(dataset.value?.type || '').toLowerCase()
|
|
return DATASET_TYPE_MAP[value] || dataset.value?.type || '-'
|
|
})
|
|
const storageType = computed(() => {
|
|
const value = dataset.value?.storage_type || ''
|
|
return STORAGE_MAP[value] || value || '-'
|
|
})
|
|
const createdAt = computed(() => dataset.value?.create_time
|
|
? new Date(dataset.value.create_time).toLocaleString('zh-CN')
|
|
: '-')
|
|
|
|
function fileKey(file: DatasetFile) {
|
|
return String(file.id || file.name)
|
|
}
|
|
|
|
async function loadDataset() {
|
|
loading.value = true
|
|
try {
|
|
dataset.value = await getDataset(datasetId)
|
|
const firstFile = files.value[0]
|
|
if (firstFile) {
|
|
selectedFileId.value = fileKey(firstFile)
|
|
await loadVersions(firstFile)
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadVersions(file: DatasetFile) {
|
|
previewLoading.value = true
|
|
previewError.value = ''
|
|
const requestId = ++versionRequestId
|
|
try {
|
|
const result = await getDatasetFileVersions(fileKey(file))
|
|
const activeContent = await getDatasetFileVersionContent(fileKey(file), result.active_version_id)
|
|
if (requestId !== versionRequestId) return
|
|
versions.value = result.versions
|
|
activeVersionId.value = result.active_version_id
|
|
selectedVersionId.value = result.active_version_id
|
|
loadedVersionId.value = result.active_version_id
|
|
previewContent.value = activeContent.content
|
|
baseVersionContent.value = activeContent.content
|
|
resetRecordEditor()
|
|
} catch {
|
|
if (requestId === versionRequestId) previewError.value = '版本内容加载失败,请稍后重试'
|
|
} finally {
|
|
if (requestId === versionRequestId) previewLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function handleViewedVersionChange(nextVersionId: string) {
|
|
if (!selectedFile.value || nextVersionId === loadedVersionId.value) return
|
|
const previousVersionId = loadedVersionId.value
|
|
if (!await confirmDiscardChanges('切换版本后,当前样本的未保存修改将丢失。')) {
|
|
selectedVersionId.value = previousVersionId
|
|
return
|
|
}
|
|
|
|
versionLoading.value = true
|
|
const requestId = ++versionRequestId
|
|
try {
|
|
const result = await getDatasetFileVersionContent(fileKey(selectedFile.value), nextVersionId)
|
|
if (requestId !== versionRequestId) return
|
|
previewContent.value = result.content
|
|
baseVersionContent.value = result.content
|
|
loadedVersionId.value = result.version.id
|
|
resetRecordEditor()
|
|
} catch {
|
|
if (requestId === versionRequestId) selectedVersionId.value = previousVersionId
|
|
} finally {
|
|
if (requestId === versionRequestId) versionLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function activateViewedVersion() {
|
|
if (!selectedFile.value || !viewedVersion.value || isViewingActiveVersion.value) return
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
`确定将 V${viewedVersion.value.version} 设为当前版本吗?后续任务将使用该版本。`,
|
|
'切换当前版本',
|
|
{
|
|
type: 'warning',
|
|
confirmButtonText: '设为当前版本',
|
|
cancelButtonText: '取消',
|
|
},
|
|
)
|
|
} catch {
|
|
return
|
|
}
|
|
|
|
activatingVersion.value = true
|
|
try {
|
|
const result = await activateDatasetFileVersion(
|
|
fileKey(selectedFile.value),
|
|
viewedVersion.value.id,
|
|
activeVersionId.value,
|
|
)
|
|
activeVersionId.value = result.version.id
|
|
previewContent.value = result.content
|
|
baseVersionContent.value = result.content
|
|
ElMessage.success(`已将 V${result.version.version} 设为当前版本`)
|
|
} finally {
|
|
activatingVersion.value = false
|
|
}
|
|
}
|
|
|
|
function openRecordEditor(record: DatasetRecord) {
|
|
if (!isViewingActiveVersion.value) {
|
|
ElMessage.warning('历史版本为只读,请先设为当前版本')
|
|
return
|
|
}
|
|
editingRecord.value = record
|
|
if (record.kind === 'object') {
|
|
editFields.value = Object.entries(record.value as Record<string, unknown>).map(([key, value]) => ({
|
|
key,
|
|
value: typeof value === 'string' ? value : JSON.stringify(value, null, 2),
|
|
isJson: typeof value !== 'string',
|
|
}))
|
|
rawDraft.value = ''
|
|
} else {
|
|
editFields.value = []
|
|
rawDraft.value = record.raw
|
|
}
|
|
editorVisible.value = true
|
|
originalDraft.value = record.kind === 'object' ? JSON.stringify(editFields.value) : rawDraft.value
|
|
}
|
|
|
|
function resetRecordEditor() {
|
|
editorVisible.value = false
|
|
editingRecord.value = null
|
|
editFields.value = []
|
|
rawDraft.value = ''
|
|
originalDraft.value = ''
|
|
}
|
|
|
|
function updateEditField(index: number, value: string) {
|
|
const field = editFields.value[index]
|
|
if (field) field.value = value
|
|
}
|
|
|
|
async function confirmDiscardChanges(message = '当前存在尚未保存为版本的修改,放弃后将无法恢复。') {
|
|
if (!hasUnsavedChanges.value) return true
|
|
try {
|
|
await ElMessageBox.confirm(message, '确认放弃修改?', {
|
|
type: 'warning',
|
|
confirmButtonText: '放弃修改',
|
|
cancelButtonText: '继续编辑',
|
|
})
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function confirmEditorDiscardChanges() {
|
|
if (!hasEditorDraftChanges.value) return true
|
|
try {
|
|
await ElMessageBox.confirm('当前样本存在未暂存修改,放弃后将无法恢复。', '确认放弃修改?', {
|
|
type: 'warning',
|
|
confirmButtonText: '放弃修改',
|
|
cancelButtonText: '继续编辑',
|
|
})
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function closeRecordEditor(done?: () => void) {
|
|
if (savingRecord.value) return
|
|
if (!await confirmEditorDiscardChanges()) return
|
|
resetRecordEditor()
|
|
done?.()
|
|
}
|
|
|
|
function buildNextRecordValue() {
|
|
if (editingRecord.value?.kind !== 'object') return JSON.parse(rawDraft.value)
|
|
const nextValue: Record<string, unknown> = {}
|
|
for (const field of editFields.value) {
|
|
nextValue[field.key] = field.isJson ? JSON.parse(field.value) : field.value
|
|
}
|
|
return nextValue
|
|
}
|
|
|
|
async function saveRecord() {
|
|
if (!selectedFile.value || !editingRecord.value || savingRecord.value) return
|
|
const record = editingRecord.value
|
|
let nextValue: unknown
|
|
try {
|
|
nextValue = buildNextRecordValue()
|
|
} catch (error) {
|
|
ElMessage.error(`JSON 格式校验失败:${error instanceof Error ? error.message : '内容格式不正确'}`)
|
|
return
|
|
}
|
|
|
|
savingRecord.value = true
|
|
try {
|
|
const nextContent = updateDatasetRecord(
|
|
previewContent.value,
|
|
selectedFile.value.name,
|
|
record.sourceIndex,
|
|
nextValue,
|
|
)
|
|
previewContent.value = nextContent
|
|
resetRecordEditor()
|
|
ElMessage.success(`第 ${record.displayIndex} 条修改已暂存`)
|
|
} finally {
|
|
savingRecord.value = false
|
|
}
|
|
}
|
|
|
|
async function saveVersion() {
|
|
if (
|
|
!selectedFile.value
|
|
|| !hasPendingVersionChanges.value
|
|
|| !isViewingActiveVersion.value
|
|
|| savingVersion.value
|
|
) return
|
|
|
|
savingVersion.value = true
|
|
try {
|
|
const result = await createDatasetFileVersion(
|
|
fileKey(selectedFile.value),
|
|
previewContent.value,
|
|
{
|
|
description: '在线编辑数据样本',
|
|
baseVersionId: loadedVersionId.value,
|
|
expectedCurrentVersionId: activeVersionId.value,
|
|
},
|
|
)
|
|
previewContent.value = result.content
|
|
baseVersionContent.value = result.content
|
|
versions.value = [result.version, ...versions.value]
|
|
activeVersionId.value = result.version.id
|
|
selectedVersionId.value = result.version.id
|
|
loadedVersionId.value = result.version.id
|
|
ElMessage.success(`V${result.version.version} 创建成功,已设为当前版本`)
|
|
} finally {
|
|
savingVersion.value = false
|
|
}
|
|
}
|
|
|
|
function handleBeforeUnload(event: BeforeUnloadEvent) {
|
|
if (!hasUnsavedChanges.value) return
|
|
event.preventDefault()
|
|
event.returnValue = ''
|
|
}
|
|
|
|
function handleDownloadFile(file: DatasetFile) {
|
|
window.open(downloadFileUrl(datasetId, fileKey(file), loadedVersionId.value), '_blank')
|
|
}
|
|
|
|
onBeforeRouteLeave(() => confirmDiscardChanges())
|
|
onMounted(() => {
|
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
|
loadDataset()
|
|
})
|
|
onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnload))
|
|
</script>
|
|
|
|
<template>
|
|
<PageCard class="dataset-preview-page" v-loading="loading">
|
|
<template #header>
|
|
<div class="page-header">
|
|
<div class="page-heading">
|
|
<div class="heading-icon" aria-hidden="true"><i class="fa fa-database" /></div>
|
|
<div>
|
|
<div class="heading-row">
|
|
<h1>{{ dataset?.name || '数据集详情' }}</h1>
|
|
<el-tag size="small" type="primary">{{ datasetType }}</el-tag>
|
|
<el-tag size="small" type="success">{{ storageType }}</el-tag>
|
|
</div>
|
|
<p>{{ dataset?.description || '逐条查看和修改数据样本' }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<section class="dataset-overview" aria-label="数据集概览">
|
|
<div class="overview-item"><span>数据条数</span><strong>{{ (dataset?.count || 0).toLocaleString() }}</strong></div>
|
|
<div class="overview-item"><span>数据大小</span><strong>{{ dataset?.size || '-' }}</strong></div>
|
|
<div class="overview-item overview-item-wide"><span>创建时间</span><strong>{{ createdAt }}</strong></div>
|
|
</section>
|
|
|
|
<DatasetVersionBar
|
|
v-model="selectedVersionId"
|
|
:versions="versions"
|
|
:viewed-version="viewedVersion"
|
|
:active-version-id="activeVersionId"
|
|
:is-viewing-active-version="isViewingActiveVersion"
|
|
:has-pending-version-changes="hasPendingVersionChanges"
|
|
:version-loading="versionLoading"
|
|
:saving-record="savingRecord"
|
|
:activating-version="activatingVersion"
|
|
@change="handleViewedVersionChange"
|
|
@activate="activateViewedVersion"
|
|
/>
|
|
|
|
<el-alert
|
|
v-if="viewedVersion && !isViewingActiveVersion"
|
|
class="historical-version-alert"
|
|
type="info"
|
|
:closable="false"
|
|
show-icon
|
|
title="你正在查看历史版本,数据默认只读;设为当前版本后才可编辑。"
|
|
/>
|
|
|
|
<section v-if="files.length" class="preview-workspace" aria-label="数据集文件详情">
|
|
<div class="content-pane">
|
|
<div class="content-toolbar">
|
|
<div class="content-title">
|
|
<span class="file-icon" aria-hidden="true"><i class="fa fa-file-text-o" /></span>
|
|
<div>
|
|
<h2>{{ selectedFile?.name }}</h2>
|
|
<p>
|
|
{{ selectedFile?.size || '未知大小' }} <span aria-hidden="true">·</span>
|
|
<template v-if="isRecordFile">{{ records.length.toLocaleString() }} 条记录</template>
|
|
<template v-else>{{ totalLines.toLocaleString() }} 行</template>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="toolbar-actions">
|
|
<el-button
|
|
v-if="hasPendingVersionChanges && isViewingActiveVersion"
|
|
type="primary"
|
|
:loading="savingVersion"
|
|
@click="saveVersion"
|
|
>
|
|
<i class="fa fa-save" />保存版本
|
|
</el-button>
|
|
<el-tooltip content="下载当前文件" placement="top">
|
|
<el-button class="download-button" aria-label="下载当前文件" :disabled="!selectedFile" @click="selectedFile && handleDownloadFile(selectedFile)">
|
|
<i class="fa fa-download" />下载文件
|
|
</el-button>
|
|
</el-tooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="viewer-shell" v-loading="versionLoading" :aria-busy="previewLoading || versionLoading">
|
|
<div v-if="previewLoading" class="viewer-state"><i class="fa fa-circle-o-notch fa-spin" aria-hidden="true" /><span>正在加载文件内容…</span></div>
|
|
<div v-else-if="previewError" class="viewer-state viewer-state-error" role="alert">
|
|
<i class="fa fa-exclamation-circle" aria-hidden="true" /><strong>{{ previewError }}</strong>
|
|
<el-button size="small" @click="selectedFile && loadVersions(selectedFile)">重新加载</el-button>
|
|
</div>
|
|
<div v-else-if="!previewContent" class="viewer-state"><i class="fa fa-file-o" aria-hidden="true" /><span>当前文件暂无可预览内容</span></div>
|
|
|
|
<DatasetRecordTable
|
|
v-else-if="isRecordFile"
|
|
:records="records"
|
|
:is-viewing-active-version="isViewingActiveVersion"
|
|
:version-id="loadedVersionId"
|
|
@edit="openRecordEditor"
|
|
/>
|
|
|
|
<DatasetRawPreview
|
|
v-else
|
|
:content="previewContent"
|
|
:file-name="selectedFile?.name"
|
|
/>
|
|
</div>
|
|
|
|
<div class="viewer-footer"><span v-if="isRecordFile"><i class="fa fa-list-alt" aria-hidden="true" /> 逐条查看与编辑</span><span v-else><i class="fa fa-eye" aria-hidden="true" /> 文本预览</span><span>UTF-8</span></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-else-if="!loading" class="empty-files" aria-label="空文件列表">
|
|
<span class="empty-icon" aria-hidden="true"><i class="fa fa-folder-open-o" /></span><h2>暂无数据文件</h2><p>该数据集还没有可供预览的文件。</p>
|
|
</section>
|
|
|
|
<DatasetRecordEditorDialog
|
|
:model-value="editorVisible"
|
|
:record="editingRecord"
|
|
:edit-fields="editFields"
|
|
:raw-draft="rawDraft"
|
|
:saving-record="savingRecord"
|
|
:has-draft-changes="hasEditorDraftChanges"
|
|
:next-version-number="nextVersionNumber"
|
|
@close="closeRecordEditor"
|
|
@save="saveRecord"
|
|
@update:field="updateEditField"
|
|
@update:raw-draft="rawDraft = $event"
|
|
/>
|
|
</PageCard>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.dataset-preview-page {
|
|
min-width: 0;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 20px;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
.page-heading {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.heading-icon {
|
|
display: grid;
|
|
flex: 0 0 44px;
|
|
width: 44px;
|
|
height: 44px;
|
|
place-items: center;
|
|
border-radius: 12px;
|
|
background: linear-gradient(145deg, #eef2ff, #e0e7ff);
|
|
color: var(--primary-color);
|
|
font-size: 18px;
|
|
}
|
|
|
|
.heading-row {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.heading-row h1 {
|
|
margin: 0 4px 0 0;
|
|
color: #172033;
|
|
font-size: 20px;
|
|
font-weight: 650;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.page-heading p {
|
|
margin: 4px 0 0;
|
|
color: #7c8799;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.dataset-overview {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
margin-bottom: 20px;
|
|
padding: 16px 20px;
|
|
border: 1px solid #edf0f5;
|
|
border-radius: 12px;
|
|
background: #fbfcfe;
|
|
}
|
|
|
|
.overview-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
min-width: 0;
|
|
padding: 0 20px;
|
|
border-right: 1px solid #e9edf4;
|
|
}
|
|
|
|
.overview-item:first-child {
|
|
padding-left: 0;
|
|
}
|
|
|
|
.overview-item:last-child {
|
|
padding-right: 0;
|
|
border-right: 0;
|
|
}
|
|
|
|
.overview-item span {
|
|
color: #8a94a6;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.overview-item strong {
|
|
overflow: hidden;
|
|
color: #283347;
|
|
font-size: 15px;
|
|
font-variant-numeric: tabular-nums;
|
|
font-weight: 600;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.historical-version-alert {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.preview-workspace {
|
|
min-height: 520px;
|
|
overflow: hidden;
|
|
border: 1px solid #e5eaf2;
|
|
border-radius: 12px;
|
|
background: #fff;
|
|
}
|
|
|
|
.content-title h2 {
|
|
margin: 0;
|
|
color: #263146;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.content-pane {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
background: #fff;
|
|
}
|
|
|
|
.content-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
min-height: 68px;
|
|
padding: 10px 16px;
|
|
border-bottom: 1px solid #e5eaf2;
|
|
}
|
|
|
|
.content-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.content-title > div {
|
|
min-width: 0;
|
|
}
|
|
|
|
.content-title h2 {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.content-title p {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin: 3px 0 0;
|
|
color: #8a94a6;
|
|
font-size: 11px;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.file-icon {
|
|
display: grid;
|
|
flex: 0 0 34px;
|
|
height: 34px;
|
|
place-items: center;
|
|
border-radius: 8px;
|
|
background: #f1f5f9;
|
|
color: #69768b;
|
|
}
|
|
|
|
.download-button {
|
|
min-height: 36px;
|
|
}
|
|
|
|
.download-button i {
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.toolbar-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.toolbar-actions i {
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.viewer-shell {
|
|
position: relative;
|
|
flex: 1;
|
|
min-height: 400px;
|
|
overflow: hidden;
|
|
background: #fbfcfe;
|
|
}
|
|
|
|
.viewer-state {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
height: 100%;
|
|
min-height: 400px;
|
|
color: #8b96a8;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.viewer-state > i {
|
|
color: #aeb7c5;
|
|
font-size: 24px;
|
|
}
|
|
|
|
.viewer-state-error > i,
|
|
.viewer-state-error strong {
|
|
color: #d14343;
|
|
}
|
|
|
|
.viewer-state-error strong {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.viewer-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
min-height: 34px;
|
|
padding: 6px 14px;
|
|
border-top: 1px solid #e5eaf2;
|
|
background: #f8fafc;
|
|
color: #8a94a6;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.viewer-footer i {
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.empty-files {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
min-height: 420px;
|
|
border: 1px dashed #dce2eb;
|
|
border-radius: 12px;
|
|
background: #fbfcfe;
|
|
text-align: center;
|
|
}
|
|
|
|
.empty-icon {
|
|
display: grid;
|
|
width: 54px;
|
|
height: 54px;
|
|
margin-bottom: 14px;
|
|
place-items: center;
|
|
border-radius: 14px;
|
|
background: #eef2f7;
|
|
color: #8a96a8;
|
|
font-size: 22px;
|
|
}
|
|
|
|
.empty-files h2 {
|
|
margin: 0;
|
|
color: #354056;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.empty-files p {
|
|
margin: 6px 0 0;
|
|
color: #929daf;
|
|
font-size: 13px;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.dataset-overview {
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
@media (max-width: 680px) {
|
|
.page-header {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.dataset-overview {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.overview-item {
|
|
padding: 0 0 12px;
|
|
border-right: 0;
|
|
border-bottom: 1px solid #e9edf4;
|
|
}
|
|
|
|
.overview-item:last-child {
|
|
padding-bottom: 0;
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.content-toolbar {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.download-button {
|
|
width: 44px;
|
|
padding: 8px;
|
|
font-size: 0;
|
|
}
|
|
|
|
.download-button i {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
</style>
|