diff --git a/frontend/scripts/regression-dataset-preview.mjs b/frontend/scripts/regression-dataset-preview.mjs index dec2bd8..97954ae 100644 --- a/frontend/scripts/regression-dataset-preview.mjs +++ b/frontend/scripts/regression-dataset-preview.mjs @@ -1,22 +1,67 @@ import fs from 'node:fs' import path from 'node:path' +import ts from 'typescript' const root = process.cwd() const previewPath = path.join(root, 'src/views/dataset/DatasetPreviewView.vue') +const apiPath = path.join(root, 'src/api/modules/dataset.ts') +const adapterPath = path.join(root, 'src/mock/adapter.ts') const mockPath = path.join(root, 'src/mock/data.ts') +const recordsPath = path.join(root, 'src/views/dataset/datasetRecords.ts') +const versionsPath = path.join(root, 'src/mock/datasetVersions.ts') const source = fs.readFileSync(previewPath, 'utf8') +const apiSource = fs.readFileSync(apiPath, 'utf8') +const adapterSource = fs.readFileSync(adapterPath, 'utf8') const mockSource = fs.readFileSync(mockPath, 'utf8') +const recordsSource = fs.readFileSync(recordsPath, 'utf8') +const versionsSource = fs.readFileSync(versionsPath, 'utf8') +const recordsModuleCode = ts.transpileModule(recordsSource, { + compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, +}).outputText +const { parseDatasetRecords, updateDatasetRecord } = await import( + `data:text/javascript;base64,${Buffer.from(recordsModuleCode).toString('base64')}` +) +const versionsModuleCode = ts.transpileModule(versionsSource, { + compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, +}).outputText +const { + activateDatasetVersion, + appendDatasetVersion, + createInitialVersionState, + getActiveDatasetVersion, +} = await import(`data:text/javascript;base64,${Buffer.from(versionsModuleCode).toString('base64')}`) function expect(condition, message) { if (!condition) throw new Error(message) } -expect(source.includes('class="preview-workspace"'), '预览页应使用文件与内容双栏工作区') +expect(source.includes('class="preview-workspace"'), '详情页应提供数据文件工作区') +expect(!source.includes('class="file-pane"'), '单文件数据集详情不应展示文件选择侧栏') expect(source.includes('class="code-line"'), '内容查看器应提供逐行展示') expect(source.includes('class="line-number"'), '内容查看器应显示行号') expect(source.includes('previewLoading'), '切换文件时应提供独立加载状态') expect(source.includes('previewError'), '内容加载失败时应提供错误状态') -expect(source.includes('aria-current'), '文件选中态应向辅助技术暴露') +expect(source.includes('class="records-viewer"'), '详情页应将结构化文件展示为样本列表') +expect(source.includes('class="record-table"'), '样本数据应使用企业级表格展示') +expect(source.includes('fixed="right"'), '逐条编辑操作列应固定在表格右侧') +expect(source.includes('v-for="fieldKey in tableFieldKeys"'), '企业表格应根据数据结构动态生成字段列') +expect(source.includes('openRecordEditor(asDatasetRecord(row))'), '每条样本应提供独立编辑入口') +expect(source.includes('暂存修改'), '单条编辑应先暂存到当前页面') +expect(source.includes('hasPendingVersionChanges && isViewingActiveVersion'), '存在暂存修改时才应显示保存版本按钮') +expect(source.includes('保存版本'), '文件工具栏应提供保存版本按钮') +expect(source.includes('baseVersionContent'), '页面应区分已保存版本与待保存工作副本') +expect(source.includes('class="version-control-bar"'), '详情页应提供独立版本控制区') +expect(source.includes('设为当前版本'), '历史版本应支持显式切换为当前版本') +expect(source.includes('历史版本(只读)'), '历史版本应明确展示只读状态') +expect(source.includes('loadedVersionId.value'), '下载和展示应绑定正在查看的具体版本') +expect(source.includes('versionRequestId'), '快速切换历史版本时应防止旧响应覆盖新内容') +expect(source.includes('v-model:current-page="currentPage"'), '样本列表应支持分页浏览') +expect(!source.includes('class="content-editor"'), '详情页不应继续提供整文件编辑器') +expect(source.includes('saveRecord'), '逐条编辑器应提供单条保存动作') +expect(source.includes('hasUnsavedChanges'), '在线编辑器应跟踪未保存修改') +expect(source.includes('onBeforeRouteLeave'), '离开页面时应保护未保存修改') +expect(source.includes("event.key.toLowerCase() === 's'"), '在线编辑器应支持快捷键保存') +expect(source.includes('loadVersions(selectedFile)'), '版本内容加载失败后应支持重新加载') expect(!source.includes('handleDownloadAll'), '页面不应保留整包下载逻辑') expect(!source.includes('handleDelete'), '页面不应保留删除逻辑') expect(!source.includes('router.back()'), '页面不应保留页头返回逻辑') @@ -25,5 +70,53 @@ expect(mockSource.includes('mockDatasetPreviews'), 'Mock 数据应包含文件 expect(mockSource.includes("'mock-jsonl'"), 'Mock 数据应为通用数据集提供模拟样本') expect(mockSource.includes("'mock-readme'"), 'Mock 数据应为通用数据集提供模拟说明') expect(mockSource.includes('.map((dataset)'), '所有 Mock 数据集都应自动补齐预览文件') +expect(mockSource.includes(': [{ id: `dataset-${dataset.id}-samples`'), '每个 Mock 数据集应只对应一个数据文件') +expect(apiSource.includes('createDatasetFileVersion'), '数据集 API 应提供创建新版本方法') +expect(apiSource.includes('activateDatasetFileVersion'), '数据集 API 应提供切换当前版本方法') +expect(apiSource.includes('getDatasetFileVersionContent'), '数据集 API 应支持读取历史版本内容') +expect(apiSource.includes('expected_current_version_id'), '创建和激活版本应携带乐观锁版本指针') +expect(!apiSource.includes('updateDatasetFileContent'), '数据编辑不应继续覆盖历史版本内容') +expect(adapterSource.includes('mock:dataset-versions:'), 'Mock 版本历史应支持刷新持久化') +expect(adapterSource.includes('当前版本已被其他用户更新'), 'Mock 应拒绝基于过期版本的并发覆盖') +expect(adapterSource.includes('历史版本不可覆盖'), '旧的覆盖保存接口应明确拒绝修改历史版本') +expect(adapterSource.includes('Mock 存储空间不足'), 'Mock 存储失败时应返回明确错误且保留编辑草稿') + +const jsonl = [ + '{"instruction":"第一条","output":"A"}', + '', + '{"instruction":"第二条","output":["B","C"]}', +].join('\n') +const parsed = parseDatasetRecords(jsonl, 'samples.jsonl') +expect(parsed.supported && parsed.records.length === 2, 'JSONL 应跳过空行并解析为逐条样本') +expect(parsed.records[1].sourceIndex === 2, '样本应保留原文件物理行位置') +const updated = updateDatasetRecord(jsonl, 'samples.jsonl', 2, { + instruction: '第二条已修改', + output: ['B', 'C'], +}) +const beforeLines = jsonl.split('\n') +const afterLines = updated.split('\n') +expect(afterLines[0] === beforeLines[0] && afterLines[1] === '', '单条保存不得改写其他样本或空行') +expect(JSON.parse(afterLines[2]).instruction === '第二条已修改', '单条保存应准确更新目标样本') +expect(Array.isArray(JSON.parse(afterLines[2]).output), '单条保存不应丢失数组等非字符串字段类型') +const invalid = parseDatasetRecords('{invalid json}', 'samples.jsonl') +expect(invalid.records[0].kind === 'invalid' && invalid.records[0].error, '非法 JSON 行应被单独标记并给出错误') +const repairedJson = updateDatasetRecord('{invalid json}', 'samples.json', 0, { repaired: true }) +expect(JSON.parse(repairedJson).repaired === true, '非法 JSON 文件应支持按单条方式修复') + +const v1State = createInitialVersionState('A', '2026-07-13T00:00:00Z') +const v2State = appendDatasetVersion(v1State, 'B', '2026-07-13T01:00:00Z', '修改第一条') +expect(v2State !== v1State, '创建版本应返回新的不可变状态') +expect(v1State.versions.length === 1 && v1State.versions[0].content === 'A', '创建新版本不得修改历史版本') +expect(v2State.versions[1].version === 2 && v2State.active_version_id === 'v2', '新版本编号应递增并自动激活') +const switchedToV1 = activateDatasetVersion(v2State, 'v1') +expect(getActiveDatasetVersion(switchedToV1).content === 'A', '切换当前版本后应读取对应历史内容') +expect(v2State.active_version_id === 'v2' && v2State.versions[1].content === 'B', '切换版本不得污染原状态或历史快照') +let invalidVersionRejected = false +try { + activateDatasetVersion(v2State, 'missing') +} catch { + invalidVersionRejected = true +} +expect(invalidVersionRejected, '不存在的版本不得切换为当前版本') console.log('dataset preview regression checks passed') diff --git a/frontend/src/api/modules/dataset.ts b/frontend/src/api/modules/dataset.ts index 5c65a2e..b40eec8 100644 --- a/frontend/src/api/modules/dataset.ts +++ b/frontend/src/api/modules/dataset.ts @@ -1,5 +1,5 @@ import { get, post, put, del } from '../request' -import type { DatasetItem } from '@/types' +import type { DatasetItem, DatasetVersion, DatasetVersionList } from '@/types' /** 数据集列表 */ export const getDatasetList = () => get('/dataset-manage') @@ -29,11 +29,58 @@ export const uploadDatasetFiles = (datasetId: string | number, files: File[]) => /** 预览数据集文件内容 */ export const previewDatasetFile = (fileId: string | number) => - get<{ content: string }>(`/dataset-manage/preview/${fileId}`) + get<{ content: string }>(`/dataset-manage/preview/${encodeURIComponent(fileId)}`) + +/** 数据集文件版本列表 */ +export const getDatasetFileVersions = (fileId: string | number) => + get(`/dataset-manage/versions/${encodeURIComponent(fileId)}`) + +/** 读取指定历史版本内容(不改变当前版本) */ +export const getDatasetFileVersionContent = (fileId: string | number, versionId: string) => + get<{ version: DatasetVersion; content: string }>( + `/dataset-manage/versions/${encodeURIComponent(fileId)}/${encodeURIComponent(versionId)}`, + ) + +/** 保存为新版本,并自动设为当前版本 */ +export const createDatasetFileVersion = ( + fileId: string | number, + content: string, + options: { + description?: string + baseVersionId: string + expectedCurrentVersionId: string + }, +) => + post<{ version: DatasetVersion; content: string }>( + `/dataset-manage/versions/${encodeURIComponent(fileId)}`, + { + content, + description: options.description || '在线编辑', + base_version_id: options.baseVersionId, + expected_current_version_id: options.expectedCurrentVersionId, + }, + ) + +/** 切换当前使用的数据集版本 */ +export const activateDatasetFileVersion = ( + fileId: string | number, + versionId: string, + expectedCurrentVersionId: string, +) => + put<{ version: DatasetVersion; content: string }>( + `/dataset-manage/versions/${encodeURIComponent(fileId)}/active`, + { version_id: versionId, expected_current_version_id: expectedCurrentVersionId }, + ) /** 下载文件 URL */ -export const downloadFileUrl = (datasetId: string | number, fileId: string | number) => - `/api/dataset-manage/download/${datasetId}/${fileId}` +export const downloadFileUrl = ( + datasetId: string | number, + fileId: string | number, + versionId?: string, +) => { + const baseUrl = `/api/dataset-manage/download/${datasetId}/${fileId}` + return versionId ? `${baseUrl}?version_id=${encodeURIComponent(versionId)}` : baseUrl +} /** 打包下载数据集 URL */ export const downloadDatasetUrl = (datasetId: string | number) => diff --git a/frontend/src/mock/adapter.ts b/frontend/src/mock/adapter.ts index 2fad813..c829df5 100644 --- a/frontend/src/mock/adapter.ts +++ b/frontend/src/mock/adapter.ts @@ -22,6 +22,13 @@ import { mockTrainingLogFiles, mockLogContent, } from './data' +import { + activateDatasetVersion, + appendDatasetVersion, + createInitialVersionState, + getActiveDatasetVersion, +} from './datasetVersions' +import type { StoredDatasetVersion, StoredDatasetVersionState } from './datasetVersions' /** 模拟网络延迟 */ function delay(ms = 200): Promise { @@ -57,6 +64,41 @@ function stripBaseURL(url: string): string { return path } +function datasetFallbackContent(fileId: string) { + const fallbackKey = fileId.endsWith('-readme') ? 'mock-readme' : 'mock-jsonl' + return mockDatasetPreviews[fileId] ?? mockDatasetPreviews[fallbackKey] ?? '' +} + +function versionStorageKey(fileId: string) { + return `mock:dataset-versions:${fileId}` +} + +function persistVersionState(fileId: string, state: StoredDatasetVersionState) { + localStorage.setItem(versionStorageKey(fileId), JSON.stringify(state)) +} + +function getVersionState(fileId: string): StoredDatasetVersionState { + const persisted = localStorage.getItem(versionStorageKey(fileId)) + if (persisted) { + try { + const state = JSON.parse(persisted) as StoredDatasetVersionState + if (state.versions?.length && state.active_version_id) return state + } catch { + // 版本数据损坏时回退到初始版本 + } + } + + const legacyContent = localStorage.getItem(`mock:dataset-file:${fileId}`) + const state = createInitialVersionState(legacyContent ?? datasetFallbackContent(fileId)) + persistVersionState(fileId, state) + return state +} + +function versionMetadata(version: StoredDatasetVersion) { + const { content: _content, ...metadata } = version + return metadata +} + /** 通过路径 + method 匹配 mock 响应 */ async function handleMock(config: AxiosRequestConfig) { await delay(150) // 模拟网络延迟 @@ -133,9 +175,77 @@ async function handleMock(config: AxiosRequestConfig) { m = url.match(/^\/dataset-manage\/preview\/([^/]+)$/) if (m && method === 'get') { const fileId = decodeURIComponent(m[1]) - const fallbackKey = fileId.endsWith('-readme') ? 'mock-readme' : 'mock-jsonl' - const content = mockDatasetPreviews[fileId] ?? mockDatasetPreviews[fallbackKey] - return ok({ content }) + return ok({ content: getActiveDatasetVersion(getVersionState(fileId)).content }) + } + if (m && method === 'put') return fail('历史版本不可覆盖,请创建新版本', 405) + + m = url.match(/^\/dataset-manage\/versions\/([^/]+)$/) + if (m && method === 'get') { + const fileId = decodeURIComponent(m[1]) + const state = getVersionState(fileId) + return ok({ + versions: state.versions.map(versionMetadata).sort((a, b) => b.version - a.version), + active_version_id: state.active_version_id, + }) + } + if (m && method === 'post') { + const fileId = decodeURIComponent(m[1]) + if (typeof body.content !== 'string') return fail('文件内容格式不正确', 400) + const currentState = getVersionState(fileId) + if (body.expected_current_version_id !== currentState.active_version_id) { + return fail('当前版本已被其他用户更新,请刷新后重试', 409) + } + if (body.base_version_id !== currentState.active_version_id) { + return fail('只能基于当前版本创建新版本', 409) + } + if (body.content === getActiveDatasetVersion(currentState).content) { + return fail('数据内容没有变化,无需创建新版本', 400) + } + const nextState = appendDatasetVersion( + currentState, + body.content, + new Date().toISOString(), + typeof body.description === 'string' ? body.description : '在线编辑', + ) + try { + persistVersionState(fileId, nextState) + } catch { + return fail('文件内容过大,浏览器 Mock 存储空间不足', 413) + } + const version = getActiveDatasetVersion(nextState) + return ok({ version: versionMetadata(version), content: version.content }) + } + + m = url.match(/^\/dataset-manage\/versions\/([^/]+)\/active$/) + if (m && method === 'put') { + const fileId = decodeURIComponent(m[1]) + const currentState = getVersionState(fileId) + if (body.expected_current_version_id !== currentState.active_version_id) { + return fail('当前版本已被其他用户更新,请刷新后重试', 409) + } + let nextState: StoredDatasetVersionState + try { + nextState = activateDatasetVersion(currentState, String(body.version_id || '')) + } catch (error) { + return fail(error instanceof Error ? error.message : '版本不存在', 404) + } + try { + persistVersionState(fileId, nextState) + } catch { + return fail('浏览器 Mock 存储空间不足', 413) + } + const version = getActiveDatasetVersion(nextState) + return ok({ version: versionMetadata(version), content: version.content }) + } + + m = url.match(/^\/dataset-manage\/versions\/([^/]+)\/([^/]+)$/) + if (m && method === 'get') { + const fileId = decodeURIComponent(m[1]) + const versionId = decodeURIComponent(m[2]) + const version = getVersionState(fileId).versions.find((item) => item.id === versionId) + return version + ? ok({ version: versionMetadata(version), content: version.content }) + : fail('数据集版本不存在', 404) } // ==================== 训练任务 ==================== diff --git a/frontend/src/mock/data.ts b/frontend/src/mock/data.ts index 63ab3c0..ad9edce 100644 --- a/frontend/src/mock/data.ts +++ b/frontend/src/mock/data.ts @@ -30,10 +30,14 @@ export const mockHealth: HealthMetrics = { } export const mockSystemInfo: SystemInfo = { + timestamp: '2026-07-12T10:30:00+08:00', cpu: { percent: 32, cores: 8, percents: [25, 38, 42, 30, 28, 36, 40, 18], + model: 'Intel Xeon Gold 6330', + frequency_mhz: 2500, + load_1m: 2.56, }, memory: { used_gb: 9.2, @@ -46,20 +50,166 @@ export const mockSystemInfo: SystemInfo = { used_gb: 230, total_gb: 512, percent: 45, + read_mb_s: 86.4, + write_mb_s: 34.8, }, gpu: [ - { name: 'NVIDIA A800', gpu_percent: 0, memory_used_gb: 0.0, memory_total_gb: 80, temperature: 32, power_w: 38, fan_speed: 0, clock_mhz: 1410, driver_version: '535.86.10' }, - { name: 'NVIDIA A800', gpu_percent: 28, memory_used_gb: 22.5, memory_total_gb: 80, temperature: 52, power_w: 165, fan_speed: 32, clock_mhz: 1410, driver_version: '535.86.10' }, - { name: 'NVIDIA A800', gpu_percent: 73, memory_used_gb: 58.7, memory_total_gb: 80, temperature: 68, power_w: 320, fan_speed: 58, clock_mhz: 1410, driver_version: '535.86.10' }, - { name: 'NVIDIA A800', gpu_percent: 0, memory_used_gb: 0.0, memory_total_gb: 80, temperature: 34, power_w: 42, fan_speed: 0, clock_mhz: 1410, driver_version: '535.86.10' }, - { name: 'NVIDIA A800', gpu_percent: 46, memory_used_gb: 36.8, memory_total_gb: 80, temperature: 60, power_w: 210, fan_speed: 42, clock_mhz: 1410, driver_version: '535.86.10' }, - { name: 'NVIDIA A800', gpu_percent: 0, memory_used_gb: 0.0, memory_total_gb: 80, temperature: 33, power_w: 40, fan_speed: 0, clock_mhz: 1410, driver_version: '535.86.10' }, - { name: 'NVIDIA A800', gpu_percent: 85, memory_used_gb: 68.2, memory_total_gb: 80, temperature: 73, power_w: 365, fan_speed: 62, clock_mhz: 1410, driver_version: '535.86.10' }, - { name: 'NVIDIA A800', gpu_percent: 15, memory_used_gb: 12.0, memory_total_gb: 80, temperature: 45, power_w: 95, fan_speed: 22, clock_mhz: 1410, driver_version: '535.86.10' }, + { + id: 0, + uuid: 'GPU-MOCK-A800-00', + name: 'NVIDIA A800', + status: 'idle', + gpu_percent: 0, + memory_used_gb: 0, + memory_total_gb: 80, + memory_percent: 0, + temperature: 32, + power_w: 38, + power_limit_w: 400, + fan_speed: 0, + clock_mhz: 1410, + driver_version: '535.86.10', + processes: [], + }, + { + id: 1, + uuid: 'GPU-MOCK-A800-01', + name: 'NVIDIA A800', + status: 'busy', + gpu_percent: 28, + memory_used_gb: 22.5, + memory_total_gb: 80, + memory_percent: 28.1, + temperature: 52, + power_w: 165, + power_limit_w: 400, + fan_speed: 32, + clock_mhz: 1410, + driver_version: '535.86.10', + processes: [ + { pid: 18421, name: 'python', task_name: '指令微调任务', user: 'trainer', memory_used_gb: 18.6 }, + { pid: 18503, name: 'python', task_name: '训练指标采集', user: 'trainer', memory_used_gb: 3.9 }, + ], + }, + { + id: 2, + uuid: 'GPU-MOCK-A800-02', + name: 'NVIDIA A800', + status: 'busy', + gpu_percent: 73, + memory_used_gb: 58.7, + memory_total_gb: 80, + memory_percent: 73.4, + temperature: 68, + power_w: 320, + power_limit_w: 400, + fan_speed: 58, + clock_mhz: 1410, + driver_version: '535.86.10', + processes: [ + { pid: 20118, name: 'python', task_name: '大模型推理服务', user: 'inference', memory_used_gb: 56.4 }, + { pid: 20207, name: 'python', task_name: '推理指标采集', user: 'inference', memory_used_gb: 2.3 }, + ], + }, + { + id: 3, + uuid: 'GPU-MOCK-A800-03', + name: 'NVIDIA A800', + status: 'idle', + gpu_percent: 0, + memory_used_gb: 0, + memory_total_gb: 80, + memory_percent: 0, + temperature: 34, + power_w: 42, + power_limit_w: 400, + fan_speed: 0, + clock_mhz: 1410, + driver_version: '535.86.10', + processes: [], + }, + { + id: 4, + uuid: 'GPU-MOCK-A800-04', + name: 'NVIDIA A800', + status: 'busy', + gpu_percent: 46, + memory_used_gb: 36.8, + memory_total_gb: 80, + memory_percent: 46, + temperature: 60, + power_w: 210, + power_limit_w: 400, + fan_speed: 42, + clock_mhz: 1410, + driver_version: '535.86.10', + processes: [ + { pid: 22680, name: 'python', task_name: '批量评测任务', user: 'evaluation', memory_used_gb: 32 }, + { pid: 22814, name: 'python', task_name: '模型裁判服务', user: 'evaluation', memory_used_gb: 4.8 }, + ], + }, + { + id: 5, + uuid: 'GPU-MOCK-A800-05', + name: 'NVIDIA A800', + status: 'idle', + gpu_percent: 0, + memory_used_gb: 0, + memory_total_gb: 80, + memory_percent: 0, + temperature: 33, + power_w: 40, + power_limit_w: 400, + fan_speed: 0, + clock_mhz: 1410, + driver_version: '535.86.10', + processes: [], + }, + { + id: 6, + uuid: 'GPU-MOCK-A800-06', + name: 'NVIDIA A800', + status: 'warning', + gpu_percent: 85, + memory_used_gb: 68.2, + memory_total_gb: 80, + memory_percent: 85.3, + temperature: 73, + power_w: 365, + power_limit_w: 400, + fan_speed: 62, + clock_mhz: 1410, + driver_version: '535.86.10', + processes: [ + { pid: 24129, name: 'python', task_name: '全参数训练任务', user: 'trainer', memory_used_gb: 63.5 }, + { pid: 24211, name: 'python', task_name: '训练检查点保存', user: 'trainer', memory_used_gb: 4.7 }, + ], + }, + { + id: 7, + uuid: 'GPU-MOCK-A800-07', + name: 'NVIDIA A800', + status: 'busy', + gpu_percent: 15, + memory_used_gb: 12, + memory_total_gb: 80, + memory_percent: 15, + temperature: 45, + power_w: 95, + power_limit_w: 400, + fan_speed: 22, + clock_mhz: 1410, + driver_version: '535.86.10', + processes: [ + { pid: 25367, name: 'python', task_name: '在线推理服务', user: 'inference', memory_used_gb: 12 }, + ], + }, ], network: { download_mb: 1024, upload_mb: 256, + download_mb_s: 42.6, + upload_mb_s: 12.4, }, system: { uptime_seconds: 86400 * 3 + 3600 * 7 + 1800, @@ -112,8 +262,6 @@ export const mockDatasets: DatasetItem[] = [ create_time: '2025-12-20T08:00:00Z', files: [ { id: 'finance-train-jsonl', name: 'finance_train.jsonl', size: '86.4 MB' }, - { id: 'finance-validation-jsonl', name: 'finance_validation.jsonl', size: '21.8 MB' }, - { id: 'dataset-readme', name: 'README.md', size: '4.2 KB' }, ], }, { id: 2, name: '法律文书-训练集', type: 'train', storage_type: 'local', source: 'upload', size: '256 MB', count: 15230, description: '法律文书数据集', create_time: '2025-12-25T10:30:00Z' }, @@ -129,10 +277,7 @@ export const mockDatasets: DatasetItem[] = [ ...dataset, files: dataset.files?.length ? dataset.files - : [ - { id: `dataset-${dataset.id}-samples`, name: 'dataset_samples.jsonl', size: dataset.size || '12.8 MB' }, - { id: `dataset-${dataset.id}-readme`, name: 'README.md', size: '3.8 KB' }, - ], + : [{ id: `dataset-${dataset.id}-samples`, name: 'dataset_samples.jsonl', size: dataset.size || '12.8 MB' }], })) export const mockDatasetPreviews: Record = { diff --git a/frontend/src/mock/datasetVersions.ts b/frontend/src/mock/datasetVersions.ts new file mode 100644 index 0000000..88a8472 --- /dev/null +++ b/frontend/src/mock/datasetVersions.ts @@ -0,0 +1,64 @@ +export interface StoredDatasetVersion { + id: string + version: number + create_time: string + description?: string + content: string +} + +export interface StoredDatasetVersionState { + versions: StoredDatasetVersion[] + active_version_id: string +} + +export function createInitialVersionState( + content: string, + createTime = '2026-07-13T00:00:00Z', +): StoredDatasetVersionState { + const version: StoredDatasetVersion = { + id: 'v1', + version: 1, + create_time: createTime, + description: '初始版本', + content, + } + return { versions: [version], active_version_id: version.id } +} + +/** 追加不可变版本快照,并自动把新版本设为当前版本。 */ +export function appendDatasetVersion( + state: StoredDatasetVersionState, + content: string, + createTime: string, + description = '在线编辑', +): StoredDatasetVersionState { + const nextNumber = Math.max(0, ...state.versions.map((item) => item.version)) + 1 + const version: StoredDatasetVersion = { + id: `v${nextNumber}`, + version: nextNumber, + create_time: createTime, + description, + content, + } + return { + versions: [...state.versions, version], + active_version_id: version.id, + } +} + +/** 仅移动当前版本指针,不修改任何历史快照。 */ +export function activateDatasetVersion( + state: StoredDatasetVersionState, + versionId: string, +): StoredDatasetVersionState { + if (!state.versions.some((item) => item.id === versionId)) { + throw new Error('数据集版本不存在') + } + return { ...state, active_version_id: versionId } +} + +export function getActiveDatasetVersion(state: StoredDatasetVersionState) { + const version = state.versions.find((item) => item.id === state.active_version_id) + if (!version) throw new Error('当前数据集版本不存在') + return version +} diff --git a/frontend/src/views/dataset/DatasetListView.vue b/frontend/src/views/dataset/DatasetListView.vue index 7d4e421..ed7ca5e 100644 --- a/frontend/src/views/dataset/DatasetListView.vue +++ b/frontend/src/views/dataset/DatasetListView.vue @@ -118,7 +118,7 @@ onMounted(loadData)