feat: 完善前端页面与用户引导

This commit is contained in:
caoxiaozhu
2026-07-21 16:14:32 +08:00
parent b27c5f2cd6
commit a5fbe5130a
22 changed files with 3232 additions and 214 deletions

View File

@@ -0,0 +1,26 @@
import { existsSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(fileURLToPath(new URL('..', import.meta.url)))
const loginPath = resolve(root, 'src/views/login/LoginView.vue')
const heroAssetPath = resolve(root, 'src/assets/login-hero-flow.jpg')
const login = readFileSync(loginPath, 'utf8')
function assert(condition, message) {
if (!condition) throw new Error(message)
}
assert(login.includes('class="login-visual"'), '登录页缺少左侧品牌视觉区域')
assert(login.includes('class="login-panel"'), '登录页缺少右侧登录区域')
assert(login.includes('远光软件微调平台'), '登录页缺少平台标题')
assert(login.includes('大模型微调、评测与推理的一体化工作台'), '登录页缺少平台定位文案')
assert(login.includes('login-hero-flow.jpg'), '登录页未使用压缩后的流线主视觉资源')
assert(login.includes('@media (max-width: 1440px)'), '登录页缺少笔记本宽度适配规则')
assert(login.includes('@media (max-height: 820px) and (min-width: 901px)'), '登录页缺少笔记本短屏适配规则')
assert(login.includes('@media (max-width: 900px)'), '登录页缺少窄屏响应式规则')
assert(/\.login-page\s*\{[^}]*height:\s*100dvh;[^}]*overflow:\s*hidden;/.test(login), '桌面端登录页不应产生页面滚动')
assert(/@media \(max-width: 900px\)[\s\S]*?\.login-page\s*\{[^}]*height:\s*auto;[^}]*overflow:\s*auto;/.test(login), '窄屏登录页应保留内容滚动能力')
assert(existsSync(heroAssetPath), '登录页流线主视觉资源不存在')
console.log('login-layout regression checks passed')

View File

@@ -76,6 +76,7 @@ const selfSurfaceRoutes = [
'data-process',
'data-process/create',
'dataset',
'compute',
'user-settings',
]
for (const routePath of selfSurfaceRoutes) {

View File

@@ -0,0 +1,87 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const sourceRoot = path.resolve(scriptDir, '../src')
const publicRoot = path.resolve(scriptDir, '../public')
const [headerSource, routerSource, guideSource, guideContentSource] = await Promise.all([
readFile(path.join(sourceRoot, 'components/AppHeader.vue'), 'utf8'),
readFile(path.join(sourceRoot, 'router/index.ts'), 'utf8'),
readFile(path.join(sourceRoot, 'views/guide/GuideView.vue'), 'utf8'),
readFile(path.join(sourceRoot, 'views/guide/guideContent.ts'), 'utf8'),
])
assert.match(headerSource, /fa-book/, '顶部栏缺少书状使用文档图标')
assert.match(headerSource, /window\.open\([\s\S]*?['"]_blank['"]/, '使用文档应在浏览器新标签页打开')
assert.match(headerSource, /aria-label=["']打开使用文档["']/, '使用文档图标缺少无障碍名称')
assert.match(routerSource, /path:\s*['"]\/guide['"][\s\S]*?GuideView\.vue/, '缺少独立使用文档路由')
assert.match(routerSource, /name:\s*['"]guide['"][\s\S]*?skipPermission:\s*true/, '使用文档应允许所有已登录用户访问')
assert.ok(routerSource.indexOf("path: '/guide'") < routerSource.indexOf("path: '/'"), '使用文档不应嵌入主应用布局')
assert.match(guideSource, /class="docs-shell"/, '使用文档缺少独立文档站布局')
assert.match(guideSource, /class="docs-sidebar"/, '使用文档缺少左侧分级导航')
assert.match(guideSource, /class="article-toc"/, '使用文档缺少右侧页内目录')
assert.match(guideSource, /v-model="searchQuery"/, '使用文档缺少全局搜索能力')
assert.match(guideSource, /scrollIntoView/, '使用文档目录缺少锚点导航能力')
assert.match(guideSource, /class="architecture-flow"/, '使用文档缺少架构或流程图')
assert.match(guideSource, /articlePresentation\.stepsTitle/, '文档步骤标题应根据页面类型动态展示')
assert.match(guideSource, /查看型页面/, '文档缺少查看型页面说明')
assert.match(guideSource, /排查型页面/, '文档缺少排查型页面说明')
assert.match(guideSource, /class="step-screenshot"/, '使用文档缺少步骤内联页面截图')
assert.match(guideSource, /loading="lazy"/, '页面截图应使用懒加载')
assert.match(guideSource, /class="image-preview-overlay"/, '页面截图缺少大图预览')
assert.match(guideSource, /aria-modal="true"/, '截图预览缺少无障碍对话框语义')
assert.match(guideSource, /:inert="previewImage \? true : undefined"/, '截图预览打开时应禁用背景交互')
assert.match(guideSource, /event\.key === 'Tab'/, '截图预览缺少键盘焦点循环')
assert.match(guideSource, /:width="step\.screenshot\.width"/, '页面截图应使用真实固有尺寸')
for (const moduleName of [
'服务看板',
'模型训练',
'模型评测',
'模型推理',
'模型管理',
'数据集管理',
'数据处理',
'数据类型转换',
'用户设置',
'平台性能',
'查看日志',
]) {
assert.ok(guideContentSource.includes(moduleName), `使用文档缺少模块:${moduleName}`)
}
assert.match(guideSource, /操作步骤/, '使用文档缺少分步骤教程')
assert.match(guideContentSource, /title:\s*'快速开始'/, '使用文档缺少完整快速开始教程')
assert.match(guideContentSource, /architecture:/, '使用文档内容缺少架构流程数据')
assert.match(guideContentSource, /steps:/, '使用文档内容缺少模块操作步骤')
assert.match(guideContentSource, /id:\s*'dashboard'[\s\S]*?mode:\s*'overview'/, '服务看板应标记为查看型页面')
assert.match(guideContentSource, /id:\s*'hardware'[\s\S]*?mode:\s*'overview'/, '平台性能应标记为查看型页面')
assert.match(guideContentSource, /id:\s*'logs'[\s\S]*?mode:\s*'troubleshooting'/, '查看日志应标记为排查型页面')
assert.match(guideContentSource, /screenshot:/, '使用文档步骤缺少真实页面截图引用')
assert.match(guideContentSource, /status:\s*'建设中'/, '使用文档应如实标记未开放功能')
assert.match(guideSource, /@media \(max-width: 900px\)/, '使用文档缺少窄屏响应式布局')
for (const screenshotName of [
'service-dashboard.png',
'fine-tune-create.jpg',
'training-detail.jpg',
'model-edit.jpg',
'data-process-create.jpg',
'data-process-preview.jpg',
]) {
const screenshotPath = path.join(publicRoot, 'guide/screenshots', screenshotName)
const screenshot = await readFile(screenshotPath)
assert.ok(screenshot.length > 10_000, `页面截图不存在或内容异常:${screenshotName}`)
const isValidSignature = screenshotName.endsWith('.png')
? screenshot.subarray(1, 4).toString() === 'PNG'
: screenshot[0] === 0xff && screenshot[1] === 0xd8 && screenshot[2] === 0xff
assert.ok(isValidSignature, `页面截图扩展名与文件格式不一致:${screenshotName}`)
assert.ok(
guideContentSource.includes(`/guide/screenshots/${screenshotName}`),
`使用文档未引用页面截图:${screenshotName}`,
)
}
console.log('使用文档回归检查通过')

View File

@@ -0,0 +1,116 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import ts from 'typescript'
import { parse as parseSfc } from '@vue/compiler-sfc'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const sourceRoot = path.resolve(scriptDir, '../src')
const [viewSource, createSource, permissionSource, sidebarSource, routerSource, apiSource, adapterSource, authSource, usersSource] = await Promise.all([
readFile(path.join(sourceRoot, 'views/system/UserSettingsView.vue'), 'utf8'),
readFile(path.join(sourceRoot, 'views/system/UserCreateView.vue'), 'utf8'),
readFile(path.join(sourceRoot, 'views/system/UserPermissionView.vue'), 'utf8'),
readFile(path.join(sourceRoot, 'components/AppSidebar.vue'), 'utf8'),
readFile(path.join(sourceRoot, 'router/index.ts'), 'utf8'),
readFile(path.join(sourceRoot, 'api/modules/system.ts'), 'utf8'),
readFile(path.join(sourceRoot, 'mock/adapter.ts'), 'utf8'),
readFile(path.join(sourceRoot, 'stores/auth.ts'), 'utf8'),
readFile(path.join(sourceRoot, 'mock/users.ts'), 'utf8'),
])
const userFeatureSource = [viewSource, createSource, permissionSource].join('\n')
const descriptor = parseSfc(viewSource).descriptor
assert.ok(descriptor.template?.content, '用户设置页面缺少模板')
assert.ok(descriptor.scriptSetup?.content, '用户设置页面缺少 script setup')
assert.match(sidebarSource, /label:\s*'用户设置'/, '系统设置菜单缺少用户设置入口')
assert.match(sidebarSource, /to:\s*'\/user-settings'/, '用户设置菜单路由不正确')
assert.match(sidebarSource, /visibleMenuGroups/, '侧边栏没有根据权限过滤菜单')
assert.match(routerSource, /path:\s*'user-settings'/, '缺少用户设置路由')
assert.match(routerSource, /permission:\s*'user-settings'/, '用户设置路由缺少权限声明')
assert.match(routerSource, /permission-denied/, '缺少无权限访问反馈页面')
assert.match(authSource, /hasPermission/, '认证状态缺少权限检查能力')
for (const marker of [
'创建用户',
'用户权限设置',
'功能权限',
'权限设置',
'删除',
'AppConfirmDialog',
"tone: 'danger'",
]) {
assert.match(userFeatureSource, new RegExp(marker), `用户设置页面缺少关键交互:${marker}`)
}
assert.match(createSource, /createRules/, '创建用户缺少表单校验')
assert.match(viewSource, /deleteDisabledReason/, '删除操作缺少当前用户和内置用户保护')
assert.match(viewSource, /prefers-reduced-motion/, '页面未适配减少动态效果偏好')
for (const apiName of ['getUsers', 'createUser', 'deleteUser', 'updateUserAccess']) {
assert.match(apiSource, new RegExp(`export const ${apiName}`), `用户 API 缺少 ${apiName}`)
}
assert.match(adapterSource, /url === '\/users' && method === 'get'/, 'Mock 缺少用户列表路由')
assert.match(adapterSource, /url === '\/users' && method === 'post'/, 'Mock 缺少创建用户路由')
assert.match(adapterSource, /userMatch && method === 'put'/, 'Mock 缺少权限更新路由')
assert.match(adapterSource, /userMatch && method === 'delete'/, 'Mock 缺少删除用户路由')
assert.match(usersSource, /mock:system-users:v1/, 'Mock 用户没有持久化存储')
const usersModuleCode = ts.transpileModule(usersSource, {
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
}).outputText
const usersModule = await import(
`data:text/javascript;base64,${Buffer.from(usersModuleCode).toString('base64')}`
)
const initialUsers = usersModule.listMockUsers()
assert.equal(initialUsers.length, 2, '初始状态应包含内置管理员和操作员')
assert.equal(initialUsers[0].username, 'admin', '默认管理员账号应为 admin')
assert.equal(initialUsers[0].permissions.length, 12, '默认管理员应拥有全部模块权限')
assert.equal(initialUsers[1].username, 'operator', '默认操作员账号应为 operator')
assert.ok(initialUsers[1].permissions.includes('compute'), '默认操作员应拥有算力节点权限')
assert.ok(!initialUsers[1].permissions.includes('user-settings'), '默认操作员不应拥有用户管理权限')
assert.ok(!('password' in initialUsers[0]), '用户列表不能向页面返回密码')
const operator = usersModule.createMockUser({
username: 'operator01',
display_name: '测试操作员',
password: '123456',
role: 'operator',
status: 'active',
})
assert.equal(usersModule.listMockUsers().length, 3, '创建用户后列表数量应增加')
assert.ok(!operator.permissions.includes('user-settings'), '操作员默认不应拥有用户管理权限')
assert.throws(
() => usersModule.createMockUser({ username: 'OPERATOR01', display_name: '重复账号', password: '123456', role: 'viewer' }),
/用户名已存在/,
'用户名唯一校验应忽略大小写',
)
const updated = usersModule.updateMockUserAccess(operator.id, {
role: 'viewer',
permissions: ['dashboard', 'dataset'],
})
assert.deepEqual(updated.permissions, ['dashboard', 'dataset'], '权限更新应准确保存勾选结果')
assert.equal(updated.role, 'viewer', '权限设置应同步保存用户角色')
const loginResult = usersModule.authenticateMockUser('operator01', '123456')
assert.equal(loginResult.user.id, operator.id, '新建用户应可使用初始密码登录')
assert.ok(loginResult.user.last_login, '登录后应记录最近登录时间')
assert.throws(
() => usersModule.deleteMockUser(operator.id, 'operator01'),
/不能删除当前登录用户/,
'不能删除当前登录账号',
)
assert.throws(
() => usersModule.deleteMockUser(initialUsers[0].id, 'other-user'),
/系统内置管理员不能删除/,
'不能删除系统内置管理员',
)
usersModule.deleteMockUser(operator.id, 'admin')
assert.equal(usersModule.listMockUsers().length, 2, '确认删除后用户应从列表移除')
console.log('用户设置页回归检查通过')