更新品牌文案与演示项目名称

- 登录页和侧栏统一使用根目录 logo_square.png,并更新登录系统名称与副标题。

- 更新 Dashboard、项目库和工作区时间轴文案,移除底层时序视频图层说明。

- 演示视频项目显示名改为“演视LC视频序列”,启动时兼容迁移旧 Data_MyVideo_1 名称,恢复出厂设置使用新名。

- 调整侧栏用户管理入口为用户图标,底部当前用户入口为退出图标,并让退出提示不接收鼠标事件。

- 补充前端组件测试、后端演示重置测试和文档说明。
This commit is contained in:
2026-05-07 15:14:53 +08:00
parent f6ee9fa924
commit d583b32221
21 changed files with 65 additions and 30 deletions

View File

@@ -56,7 +56,7 @@ Seg_Server/
├── package.json # npm 依赖与脚本
├── .env.example # AI Studio/Gemini 前端环境变量模板
├── metadata.json # AI Studio 元数据
├── logo_square.png # Sidebar 与 favicon 使用的根目录方形 Logo
├── logo_square.png # 登录页、Sidebar 与 favicon 使用的根目录方形 Logo
├── public/
│ └── logo.png # 旧版保留 Logo 静态资源
├── doc/ # 当前实现审计、接口契约和后续实施文档
@@ -239,7 +239,7 @@ uvicorn main:app --host 0.0.0.0 --port 8000 --reload
## 主要业务流程
1. 登录:`Login.tsx` 调用 `POST /api/auth/login`,后端用 `users` 表和密码哈希校验凭证,默认启动时会种子化唯一管理员 `admin / 123456`;成功后返回签名 JWT`GET /api/auth/me` 可读取当前用户;角色只包括 `admin``annotator`,非默认 admin 的历史管理员或旧 `viewer` 会归一为 `annotator`;写入类业务接口要求 `admin/annotator`,用户管理、审计日志和演示出厂设置后台仅 `admin` 可用。
2. 用户管理:`Sidebar` 仅对 `admin` 显示“用户管理”,`UserAdmin.tsx` 调用 `/api/admin/users` 新增标注员、停用/启用、改密码和删除用户,并调用 `/api/admin/audit-logs` 展示登录和管理操作审计;系统不允许新增第二个管理员,也不再支持观察员角色;演示部署可通过“恢复演示出厂设置”二次确认后调用 `/api/admin/demo-factory-reset`,清空演示数据,只保留默认 admin、演示视频项目和一个已按文件名自然顺序生成帧的演示 DICOM 项目同时按内置权威定义恢复“腹腔镜胆囊切除术”和“头颈部CT分割”系统模板缺失的会重建被修改或删减的语义分类树会覆盖回默认状态。
2. 用户管理:`Sidebar` 仅对 `admin` 显示“用户管理”,该入口使用用户图标;侧栏底部当前用户退出入口使用退出图标,且弹出提示不接收鼠标事件,避免覆盖工作区按钮;`UserAdmin.tsx` 调用 `/api/admin/users` 新增标注员、停用/启用、改密码和删除用户,并调用 `/api/admin/audit-logs` 展示登录和管理操作审计;系统不允许新增第二个管理员,也不再支持观察员角色;演示部署可通过“恢复演示出厂设置”二次确认后调用 `/api/admin/demo-factory-reset`,清空演示数据,只保留默认 admin、名为“演视LC视频序列”的演示视频项目和一个已按文件名自然顺序生成帧的演示 DICOM 项目同时按内置权威定义恢复“腹腔镜胆囊切除术”和“头颈部CT分割”系统模板缺失的会重建被修改或删减的语义分类树会覆盖回默认状态。
3. 项目管理:`ProjectLibrary.tsx` 调用项目 API 创建项目、拉取列表、重命名项目、复制项目和删除项目项目库为所有登录用户共享标注员和管理员在项目创建、导入、解析、标注、AI 推理、任务查看、导出和删除方面能力一致;项目卡片删除按钮旁提供复制入口,复制时可选择“新项目重置”(复制项目媒体和已生成帧序列,但清空标注/mask或“全内容复制”复制项目、帧序列、标注和关联 mask 元数据任务运行历史不复制删除当前项目后会清空工作区当前项目、帧、mask 和选区。
4. 上传资源:视频走 `/api/media/upload`只上传源文件并关联项目不自动拆帧项目库在视频上传期间显示导入进度条、百分比和已上传字节。只有视频项目在尚未生成帧、未处于项目名称编辑状态且未解析中时显示“生成帧”DICOM 项目不显示生成帧入口DICOM 批量走 `/api/media/upload/dicom`,前端和后端都会按文件名自然顺序排序 `.dcm` 文件,避免 `10.dcm` 排在 `2.dcm` 前导致切片错位DICOM 上传期间显示导入进度条、本次有效文件数量和已上传字节,上传完成后轮询解析任务进度直到完成、失败或取消。
5. 生成帧入队:用户在项目库点击“生成帧”,选择目标 FPS 后前端调用 `/api/media/parse`;后端创建 `ProcessingTask` 并投递 Celery接口支持 `parse_fps``max_frames``target_width` 标准帧序列参数;项目库会继续轮询任务进度,解析成功后重新拉取项目列表和当前项目对象,使后端生成的 `thumbnail_url` 立即显示为项目封面;项目库和模板库的成功/失败短反馈使用非阻塞 `TransientNotice`,会自动消失。

View File

@@ -68,6 +68,7 @@ def _seed_default_project_sync() -> None:
from services.demo_media import (
DEMO_DICOM_PROJECT_NAME,
DEMO_VIDEO_PROJECT_NAME,
LEGACY_DEMO_VIDEO_PROJECT_NAMES,
create_parsed_dicom_demo_project,
create_unparsed_video_demo_project,
demo_dicom_files,
@@ -76,6 +77,14 @@ def _seed_default_project_sync() -> None:
db = SessionLocal()
try:
admin = ensure_default_admin(db)
legacy_video = (
db.query(Project)
.filter(Project.name.in_(LEGACY_DEMO_VIDEO_PROJECT_NAMES))
.first()
)
if legacy_video is not None:
legacy_video.name = DEMO_VIDEO_PROJECT_NAME
db.commit()
existing_video = db.query(Project).filter(Project.name == DEMO_VIDEO_PROJECT_NAME).first()
if existing_video is None and os.path.exists(settings.demo_video_path):
video_project = create_unparsed_video_demo_project(

View File

@@ -17,7 +17,8 @@ from statuses import PROJECT_STATUS_PENDING, PROJECT_STATUS_READY
DEMO_DICOM_PROJECT_NAME = "演示DICOM序列"
DEMO_DICOM_PARSE_FPS = 30.0
DEMO_VIDEO_PROJECT_NAME = "Data_MyVideo_1"
DEMO_VIDEO_PROJECT_NAME = "演视LC视频序列"
LEGACY_DEMO_VIDEO_PROJECT_NAMES = {"Data_MyVideo_1"}
def demo_dicom_files(dicom_dir: str) -> list[Path]:

View File

@@ -194,7 +194,7 @@ def test_demo_factory_reset_leaves_admin_and_parsed_demo_dicom(client, db_sessio
assert data["project"]["source_type"] == "dicom"
assert data["project"]["frame_count"] == 3
assert data["project"]["video_path"] == f"uploads/{data['project']['id']}/dicom"
assert [project["name"] for project in data["projects"]] == ["Data_MyVideo_1", "演示DICOM序列"]
assert [project["name"] for project in data["projects"]] == ["演视LC视频序列", "演示DICOM序列"]
assert data["projects"][0]["status"] == "pending"
assert data["projects"][0]["source_type"] == "video"
assert data["projects"][0]["frame_count"] == 0

View File

@@ -24,7 +24,7 @@
- 检查 MinIO bucket。
- 测试 Redis。
- Seed 默认模板。
- 如果存在 `demo_video_path` 和配置的 `demo_dicom_dir` DICOM 序列,创建默认演示视频项目和演示 DICOM 项目DICOM 按文件名自然顺序生成帧。
- 如果存在 `demo_video_path` 和配置的 `demo_dicom_dir` DICOM 序列,创建名为“演视LC视频序列”的默认演示视频项目和演示 DICOM 项目DICOM 按文件名自然顺序生成帧;启动时会把旧显示名 `Data_MyVideo_1` 迁移为新显示名
## 前端模块切换
@@ -72,7 +72,7 @@
2. `UserAdmin.tsx` 调用 `GET/POST/PATCH/DELETE /api/admin/users` 完成标注员新增、停用/启用、改密码和删除用户;不提供观察员或第二个管理员入口。
3. `UserAdmin.tsx` 调用 `GET /api/admin/audit-logs` 展示登录成功/失败以及用户管理操作审计。
4. `UserAdmin.tsx` 危险区“恢复演示出厂设置”需要浏览器确认和输入 `RESET_DEMO_FACTORY`,随后调用 `POST /api/admin/demo-factory-reset`
5. 后端 `backend/routers/admin.py` 会阻止管理员删除、停用、改名或降级自己;项目库已共享,因此删除标注员不会删除或迁移项目;演示出厂重置会清空其它用户、项目帧、标注、任务和私有模板,重新创建演示视频项目和一个已按文件名自然顺序生成帧的演示 DICOM 项目。
5. 后端 `backend/routers/admin.py` 会阻止管理员删除、停用、改名或降级自己;项目库已共享,因此删除标注员不会删除或迁移项目;演示出厂重置会清空其它用户、项目帧、标注、任务和私有模板,重新创建名为“演视LC视频序列”的演示视频项目和一个已按文件名自然顺序生成帧的演示 DICOM 项目。
### 项目与拆帧

View File

@@ -13,7 +13,7 @@
|------|------|------|------|
| 登录拦截 | `App.tsx` | 真实可用 | 未登录显示 `Login`,登录后显示主界面 |
| 模块切换 | `Sidebar.tsx` + `App.tsx` | 真实可用 | 切换 `dashboard/projects/workspace/ai/templates`“AI智能分割”入口使用 Bot + Sparkles 组合图标,强化 AI 语义 |
| Logo | `Sidebar.tsx` | 真实可用 | 侧边栏通过 Vite 资源导入使用根目录 `logo_square.png`favicon 使用 `/logo_square.png`,前端服务会从项目根目录提供该文件 |
| Logo | `Login.tsx` / `Sidebar.tsx` | 真实可用 | 登录页和侧边栏都使用根目录 `logo_square.png`favicon 使用 `/logo_square.png`,前端服务会从项目根目录提供该文件 |
| GPU 状态圆标 | `Sidebar.tsx` | 真实可用 | 通过 `GET /api/ai/models/status` 显示 GPU/CPU 和当前模型可用性 |
## 登录页
@@ -23,7 +23,7 @@
| 用户名/密码输入 | 真实可用 | 默认填入 `admin / 123456`,用户名使用 `autocomplete=username`,密码使用 `autocomplete=current-password` |
| 安全登录按钮 | 真实可用 | 调用 `POST /api/auth/login`,后端校验 `users` 表密码哈希并返回签名 JWT |
| 错误提示 | 真实可用 | 捕获后端错误并显示 |
| 登录态恢复 / 退出 | 真实可用 | 页面刷新后用 `/api/auth/me` 恢复当前用户;侧栏底部显示当前用户名并可退出登录 |
| 登录态恢复 / 退出 | 真实可用 | 页面刷新后用 `/api/auth/me` 恢复当前用户;侧栏底部使用退出图标显示当前用户名并可退出登录,退出提示不接收鼠标事件,避免悬浮到工作区按钮时误弹出 |
| 安全审计说明文字 | 部分可用 | 登录和用户管理操作已有 `audit_logs` 记录;登录页“端到端加密”等安全文案仍是展示性说明,不代表已接入完整企业级安全审计 |
## 管理员用户后台

View File

@@ -24,7 +24,7 @@
| 配置 | `src/lib/config.ts` | 推导 API 和 WebSocket 地址 |
| WebSocket | `src/lib/websocket.ts` | 进度流连接、订阅、连接状态通知、心跳和重连 |
| 模型状态 | `src/components/ModelStatusBadge.tsx` | 展示 GPU 与当前 SAM 模型真实可用状态;左侧 Sidebar 底部使用 compact 形态显示 GPU/CPU 状态,工作区顶栏不再重复显示,具体传播权重只在进入自动传播后由顶栏下拉负责 |
| 登录页 | `src/components/Login.tsx` | 调用登录 API写入 store |
| 登录页 | `src/components/Login.tsx` | 使用根目录 `logo_square.png` 和系统标题文案,调用登录 API写入 store |
| Dashboard | `src/components/Dashboard.tsx` | 展示统计、任务控制、失败详情和 WebSocket 进度消息 |
| 项目库 | `src/components/ProjectLibrary.tsx` | 项目列表、新建、重命名、删除、导入视频/DICOM、显式生成帧 |
| 工作区 | `src/components/VideoWorkspace.tsx` | 加载帧和模板组织工具栏、Canvas、本体面板、时间轴 |
@@ -89,7 +89,7 @@
3. `Template.owner_user_id` 支持用户模板;`owner_user_id IS NULL` 的模板视为系统模板,可作为默认分类体系对用户可见。
4. 角色只分为唯一默认 `admin``annotator``admin/annotator` 可调用写入类业务接口;`/api/admin/*` 仅允许默认 `admin`,用于用户管理、审计日志和演示环境出厂设置。
5. `UserAdmin.tsx` 仅在当前用户角色为 `admin` 时从 `Sidebar` 展示,调用 `/api/admin/users` 完成标注员新增、停用/启用、密码修改和删除用户,调用 `/api/admin/audit-logs` 展示登录和管理操作审计;改密码、删除用户和危险区“恢复演示出厂设置”均使用站内弹窗确认,恢复出厂设置要求输入 `RESET_DEMO_FACTORY` 后调用 `/api/admin/demo-factory-reset`
6. `POST /api/admin/demo-factory-reset` 仅允许 `admin`,会重置默认 admin 密码/角色/启用状态删除其它用户、项目、帧、标注、mask、任务、用户模板和旧审计重新创建 `settings.demo_video_path` 指向的演示视频项目,以及 `settings.demo_dicom_dir` 指向的演示 DICOM 项目DICOM 会按文件名自然顺序上传和生成帧;系统模板保留以保证重置后仍可标注。
6. `POST /api/admin/demo-factory-reset` 仅允许 `admin`,会重置默认 admin 密码/角色/启用状态删除其它用户、项目、帧、标注、mask、任务、用户模板和旧审计重新创建 `settings.demo_video_path` 指向且显示名为“演视LC视频序列”的演示视频项目,以及 `settings.demo_dicom_dir` 指向的演示 DICOM 项目DICOM 会按文件名自然顺序上传和生成帧;系统模板保留以保证重置后仍可标注。
7. 缺失、过期或伪造的 Bearer token 会在业务路由返回 401权限不足返回 403其他用户项目资源对当前用户表现为 404。
### 项目导入与生成帧

View File

@@ -14,7 +14,7 @@
| 需求 | 测试文件 | 覆盖点 |
|------|----------|--------|
| R1 登录与会话 | `src/components/Login.test.tsx`, `src/components/Sidebar.test.tsx`, `src/components/UserAdmin.test.tsx`, `src/store/useStore.test.ts`, `backend/tests/test_auth.py`, `backend/tests/test_admin.py` | 成功登录、JWT/token 写入、当前用户写入、刷新恢复基础状态、失败提示、登录输入 autocomplete、后端 401、`/api/auth/me`、管理员入口、用户 CRUD、唯一 admin/标注员角色权限、审计日志、旧 viewer 归一为标注员、改密码/删除用户站内确认、演示出厂设置站内二次确认和重置结果 |
| R1 登录与会话 | `src/components/Login.test.tsx`, `src/components/Sidebar.test.tsx`, `src/components/UserAdmin.test.tsx`, `src/store/useStore.test.ts`, `backend/tests/test_auth.py`, `backend/tests/test_admin.py` | 登录页 logo 和系统标题文案、成功登录、JWT/token 写入、当前用户写入、刷新恢复基础状态、失败提示、登录输入 autocomplete、后端 401、`/api/auth/me`、管理员入口用户图标、底部退出图标和非交互 tooltip、用户 CRUD、唯一 admin/标注员角色权限、审计日志、旧 viewer 归一为标注员、改密码/删除用户站内确认、演示出厂设置站内二次确认和重置结果 |
| R2 项目管理 | `src/lib/api.test.ts`, `src/components/ProjectLibrary.test.tsx`, `backend/tests/test_projects.py` | 前端字段映射、PATCH 更新、项目卡片复制/删除、修改项目名称时隐藏生成帧、DICOM 项目不显示生成帧、复制项目 reset/full 契约、DELETE 契约、后端 CRUD、删除级联、帧列表、项目按当前 JWT 用户隔离 |
| R3 媒体上传与拆帧 | `src/components/ProjectLibrary.test.tsx`, `src/components/TransientNotice.test.tsx`, `backend/tests/test_media.py`, `backend/tests/test_tasks.py` | 视频导入不自动拆帧、视频/DICOM 上传进度可视化、DICOM 导入显示有效文件数量并在上传后持续显示解析任务进度、显式生成帧 FPS 选择、视频生成帧入队后轮询解析任务并在成功后自动刷新项目封面、项目卡片显示目标 parse_fps 而非原视频 FPS、扩展名校验、自动建项目、关联项目、创建异步任务、非阻塞自动消失操作提示、标准帧序列参数、帧时间戳/源帧号、任务序列元数据、worker 注册帧、取消任务、重试任务、取消后 worker 停止 |
| R4 工作区与帧浏览 | `src/components/VideoWorkspace.test.tsx`, `src/components/FrameTimeline.test.tsx` | 加载帧、无帧项目不自动解析并提示生成帧、工作区短状态自动消失、工作区/AI 画布底图默认居中且保留边距、工作区 mask 透明度、回显已保存标注时保留本地未保存 draft mask、选中 mask 后跨帧自动跟随同一传播链结果、左侧工具栏清空遮罩优先作用于当前帧选中 mask/无选中时作用于当前帧全部 mask、无传播链时直接执行、有传播链时可选取消/只清当前帧/按帧范围选择/清空所有传播帧且按范围清空需最终确认、按范围清空或清空所有传播帧遇到人工/AI 标注帧时二次询问并支持保留人工帧、顶栏不显示重复的清空片段遮罩、传播进度存在时任务 message 只显示在蓝色进度面板内且不重复出现在灰色状态文字里、传播链布尔操作按帧范围选择并二次确认、清空/删除前预检后端 annotation id 并跳过本地陈旧 id、删除单个传播 mask 后空帧不保留传播历史颜色、传播权重下拉深色可读配色、自动传播范围选择时显示传播权重和向前/向后帧数、缩略图/range/视频处理进度条、视频处理进度条点击跳帧、人工/AI 标注帧红色竖线和标识点击跳帧、自动传播帧通过 source/lineage metadata 识别为蓝色区段和标识点击跳帧、最近自动传播历史片段同一蓝色系按新旧递进纯色显示,旧记录第 5 次后统一阈值色、当前帧白色贯穿线、传播/布尔/清空范围边界贯穿线、缩略图红/蓝边框、人工/AI 标注帧叠加传播状态时红框优先保留并显示蓝色内描边、当前人工/AI 标注帧青色外框加红色内描边、普通状态不显示传播范围黄色选区、播放进度条和视频处理进度条选择传播/布尔/清空范围、左右方向键切帧、播放、按项目 FPS 显示当前/总时长 |
@@ -32,7 +32,7 @@
| 需求 | 功能点 | 对应测试 | 当前状态 |
|------|--------|----------|----------|
| R1 | 登录页、唯一默认管理员、JWT 写入、当前用户写入、刷新恢复基础状态、失败提示、后端 401、`/api/auth/me`、管理员用户管理、角色权限、审计日志、演示出厂设置二次确认、重置后只保留 admin、演示视频项目和已生成帧的自然排序演示 DICOM 项目 | `Login.test.tsx`, `Sidebar.test.tsx`, `UserAdmin.test.tsx`, `useStore.test.ts`, `test_auth.py`, `test_admin.py` | 已覆盖 |
| R1 | 登录页 logo 和系统标题文案、唯一默认管理员、JWT 写入、当前用户写入、刷新恢复基础状态、失败提示、后端 401、`/api/auth/me`、管理员用户管理入口图标、底部退出入口图标和 tooltip 命中范围、角色权限、审计日志、演示出厂设置二次确认、重置后只保留 admin、名为“演视LC视频序列”的演示视频项目和已生成帧的自然排序演示 DICOM 项目 | `Login.test.tsx`, `Sidebar.test.tsx`, `UserAdmin.test.tsx`, `useStore.test.ts`, `test_auth.py`, `test_admin.py` | 已覆盖 |
| R2 | 项目列表/创建/选择/重命名/复制、重命名时不触发生成帧、DICOM 不显示生成帧、项目复制 reset/full、项目按用户隔离、视频导入、DICOM 导入、DICOM 前端选择自然排序、后端项目和帧 CRUD | `ProjectLibrary.test.tsx`, `api.test.ts`, `test_projects.py` | 已覆盖 |
| R3 | 文件类型校验、自动/指定项目上传、视频导入与生成帧分离、视频/DICOM 上传进度可视化、DICOM 导入显示有效文件数量并在上传后持续显示解析任务进度、显式 FPS 生成帧、视频生成帧完成后自动刷新项目封面、项目卡片 FPS 徽标显示 `parse_fps`、视频/DICOM 拆帧任务、DICOM 上传/下载/读取自然排序、非阻塞自动消失操作提示、`parse_fps/max_frames/target_width`、标准帧序列 metadata、任务查询、取消、重试、worker 取消停止 | `ProjectLibrary.test.tsx`, `TransientNotice.test.tsx`, `api.test.ts`, `test_media.py`, `test_tasks.py` | 已覆盖 |
| R4 | 工作区加载帧、无帧项目不自动解析、工作区短状态自动消失、后端标注回显保留本地未保存 draft mask、Canvas/AI 底图居中适配且保留边距、工作区 mask 透明度、选中 mask 后跨帧自动跟随同一传播链结果、左侧工具栏当前帧清空优先作用于选中 mask、无传播链时直接执行、有传播链时可选当前帧/传播所有帧/取消、清空人工/AI 标注帧前二次确认、取消确认不删除、仅自动传播帧不确认、删除单个传播 mask 后空帧不保留传播历史颜色、传播权重下拉深色可读配色、缩略图/range/视频处理进度条、视频处理进度条点击跳帧、人工/AI 标注帧红色竖线和标识点击跳帧、自动传播帧蓝色区段和标识点击跳帧、最近自动传播历史片段同一蓝色系按新旧递进显示,旧记录第 5 次后统一阈值色、当前帧白色贯穿线、传播范围洋红/黄绿色边界贯穿线、缩略图红/蓝边框、人工/AI 标注帧叠加传播状态时红框优先保留并显示蓝色内描边、当前人工/AI 标注帧青色外框加红色内描边、普通状态不显示传播范围黄色选区、播放进度条/视频处理进度条拖拽选择传播范围、Canvas/AI 画布拖拽平移回写 position state、左右方向键切帧、播放、按 FPS 显示时间 | `VideoWorkspace.test.tsx`, `FrameTimeline.test.tsx`, `CanvasArea.test.tsx`, `AISegmentation.test.tsx` | 已覆盖 |

View File

@@ -197,6 +197,8 @@ demo_dicom_dir=/home/wkmgc/Desktop/Seg_Server/2024_2_5_王芳/※2F458C45CFAA4C7
EOF
```
`demo_video_path` 仍指向本地源视频文件;系统 seed 和“恢复演示出厂设置”时的项目显示名固定为“演视LC视频序列”。
如果前端通过局域网 IP 访问,例如 `http://192.168.3.11:3000`,需要把该地址加入 `cors_origins`,同时前端也要配置 API 地址。
---
@@ -315,7 +317,7 @@ admin / 123456
首次启动会自动创建默认管理员,密码以哈希形式写入 `users` 表;登录返回签名 JWT业务接口会校验 `Authorization: Bearer <token>`。生产环境必须修改 `jwt_secret_key` 和默认管理员密码。
默认管理员登录后会看到“用户管理”后台,可新增标注员、停用/启用用户、重置密码、删除用户并查看登录与用户管理审计日志。系统只支持唯一默认 `admin``annotator` 两类角色标注员不能新增用户、查看审计日志或恢复演示出厂设置但可以和管理员共享同一项目库并执行项目管理、标注、AI 推理、任务和导出等业务操作。演示部署可在该后台使用“恢复演示出厂设置”,二次确认后只保留默认 admin、演示视频项目和一个已按文件名自然顺序生成帧的演示 DICOM 项目;视频来自 `demo_video_path`DICOM 序列来自 `demo_dicom_dir`
默认管理员登录后会看到“用户管理”后台,可新增标注员、停用/启用用户、重置密码、删除用户并查看登录与用户管理审计日志。系统只支持唯一默认 `admin``annotator` 两类角色标注员不能新增用户、查看审计日志或恢复演示出厂设置但可以和管理员共享同一项目库并执行项目管理、标注、AI 推理、任务和导出等业务操作。演示部署可在该后台使用“恢复演示出厂设置”,二次确认后只保留默认 admin、名为“演视LC视频序列”的演示视频项目和一个已按文件名自然顺序生成帧的演示 DICOM 项目;视频来自 `demo_video_path`DICOM 序列来自 `demo_dicom_dir`
---

View File

@@ -94,6 +94,7 @@ describe('Dashboard', () => {
expect(screen.getByText('已存标注')).toBeInTheDocument();
expect(screen.getByText('真实项目.mp4')).toBeInTheDocument();
expect(screen.getByText('项目状态: pending')).toBeInTheDocument();
expect(screen.getByText('系统全局数据监控')).toBeInTheDocument();
expect(screen.queryByText('City_Driving_Dataset_004.mp4')).not.toBeInTheDocument();
});

View File

@@ -289,7 +289,7 @@ export function Dashboard() {
{isConnected ? 'WebSocket 已连接' : 'WebSocket 断开'}
</div>
</div>
<p className="text-gray-400 text-sm mt-1"></p>
<p className="text-gray-400 text-sm mt-1"></p>
{loadError && <p className="text-red-400 text-xs mt-2">{loadError}</p>}
{taskActionMessage && <p className="text-amber-400 text-xs mt-2">{taskActionMessage}</p>}
</header>

View File

@@ -49,6 +49,7 @@ describe('FrameTimeline', () => {
expect(screen.getAllByText('00:00.10').length).toBeGreaterThan(0);
expect(screen.getAllByText('00:00.20').length).toBeGreaterThan(0);
expect(screen.queryByText('底层时序视频图层截帧导航轴')).not.toBeInTheDocument();
});
it('renders a processing progress bar with red annotation markers and blue propagation segments', () => {

View File

@@ -534,7 +534,6 @@ export function FrameTimeline({
<div className="text-xs font-mono text-cyan-300 mt-1">
{formatTime(currentSeconds)} <span className="text-gray-600">/</span> {formatTime(totalSeconds)}
</div>
<div className="text-[10px] text-gray-500 uppercase tracking-widest mt-1"></div>
</div>
</div>
</div>

View File

@@ -51,4 +51,13 @@ describe('Login', () => {
expect(screen.getByDisplayValue('admin')).toHaveAttribute('autocomplete', 'username');
expect(screen.getByDisplayValue('123456')).toHaveAttribute('autocomplete', 'current-password');
});
it('uses the product logo and updated system title copy', () => {
render(<Login />);
expect(screen.getByAltText('Logo')).toHaveAttribute('src', expect.stringContaining('logo_square'));
expect(screen.getByText('多模态影像及视频智能语义分割与标注系统')).toBeInTheDocument();
expect(screen.getByText('智能语义分割系统')).toBeInTheDocument();
expect(screen.queryByText('欢迎登录协同工作站')).not.toBeInTheDocument();
});
});

View File

@@ -1,5 +1,4 @@
import React, { useState } from 'react';
import { BrainCircuit } from 'lucide-react';
import { cn } from '../lib/utils';
import { useStore } from '../store/useStore';
import { login as loginApi } from '../lib/api';
@@ -34,10 +33,10 @@ export function Login() {
<div className="relative z-10 w-full max-w-md p-8 bg-[#111] border border-white/5 rounded-2xl shadow-2xl scale-in shadow-black/50">
<div className="flex flex-col items-center mb-8">
<div className="w-16 h-16 bg-white rounded-2xl flex items-center justify-center text-cyan-500 shadow-lg shadow-cyan-500/20 mb-4 overflow-hidden border border-white/10">
<BrainCircuit size={32} />
<img src="/logo_square.png" alt="Logo" className="h-full w-full object-contain" />
</div>
<h1 className="text-2xl font-bold text-white tracking-wider mb-2"></h1>
<p className="text-sm text-gray-500">AI </p>
<h1 className="text-center text-2xl font-bold text-white tracking-wider mb-2"></h1>
<p className="text-sm text-gray-500"></p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">

View File

@@ -46,6 +46,7 @@ describe('ProjectLibrary', () => {
fireEvent.click(await screen.findByText('Demo Project'));
expect(useStore.getState().currentProject?.id).toBe('p1');
expect(onProjectSelect).toHaveBeenCalled();
expect(screen.getByText('支持导入视频文件、DICOM序列文件')).toBeInTheDocument();
});
it('shows the generated frame sequence FPS on project cards instead of source FPS', async () => {

View File

@@ -576,7 +576,7 @@ export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) {
<div className="flex justify-between items-end mb-8 border-b border-white/5 pb-6">
<div>
<h1 className="text-3xl font-medium tracking-tight text-white mb-2"></h1>
<p className="text-gray-400 text-sm"></p>
<p className="text-gray-400 text-sm">DICOM序列文件</p>
</div>
<div className="flex items-center gap-3">
<button

View File

@@ -19,7 +19,9 @@ describe('Sidebar', () => {
render(<Sidebar activeModule="dashboard" setActiveModule={setActiveModule} />);
fireEvent.click(screen.getByTitle('用户管理'));
const adminButton = screen.getByTitle('用户管理');
expect(adminButton.querySelector('.lucide-circle-user')).toBeInTheDocument();
fireEvent.click(adminButton);
expect(setActiveModule).toHaveBeenCalledWith('admin');
});
@@ -44,4 +46,15 @@ describe('Sidebar', () => {
expect(screen.getByAltText('Logo')).toHaveAttribute('src', expect.stringContaining('logo_square'));
});
it('uses a logout icon and prevents the logout tooltip from catching workspace hover', () => {
useStore.setState({ currentUser: { id: 1, username: 'admin', role: 'admin' } });
render(<Sidebar activeModule="dashboard" setActiveModule={vi.fn()} />);
const logoutButton = screen.getByTitle('当前用户admin点击退出');
expect(logoutButton.querySelector('.lucide-log-out')).toBeInTheDocument();
expect(screen.getByText('admin / 退出')).toHaveClass('pointer-events-none');
expect(screen.getByText('admin / 退出')).toHaveClass('invisible');
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Home, FolderOpen, Edit3, LayoutTemplate, LogOut, UserCircle, ShieldCheck } from 'lucide-react';
import { Home, FolderOpen, Edit3, LayoutTemplate, LogOut, UserCircle } from 'lucide-react';
import { cn } from '../lib/utils';
import type { ActiveModule } from '../App';
import { ModelStatusBadge } from './ModelStatusBadge';
@@ -20,13 +20,13 @@ export function Sidebar({ activeModule, setActiveModule }: SidebarProps) {
{ id: 'workspace', icon: Edit3, label: '分割工作区' },
{ id: 'ai', icon: AiSegmentationIcon, label: 'AI智能分割' },
{ id: 'templates', icon: LayoutTemplate, label: '模板库' },
...(currentUser?.role === 'admin' ? [{ id: 'admin', icon: ShieldCheck, label: '用户管理' }] : []),
...(currentUser?.role === 'admin' ? [{ id: 'admin', icon: UserCircle, label: '用户管理' }] : []),
] as const;
return (
<aside className="w-16 flex flex-col items-center py-6 bg-[#0d0d0d] border-r border-white/10 z-50 gap-8">
<div className="w-10 h-10 rounded-lg overflow-hidden flex items-center justify-center bg-white">
<img src="/logo_square.png" alt="Logo" className="w-full h-full object-cover" />
<img src="/logo_square.png" alt="Logo" className="h-full w-full object-contain" />
</div>
<nav className="flex flex-col gap-6 w-full px-2">
{navItems.map((item) => {
@@ -60,8 +60,8 @@ export function Sidebar({ activeModule, setActiveModule }: SidebarProps) {
onClick={logout}
className="group relative flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/5 text-gray-400 transition-colors hover:border-red-400/40 hover:bg-red-500/10 hover:text-red-200"
>
{currentUser ? <UserCircle size={20} /> : <LogOut size={20} />}
<span className="absolute left-full ml-2 whitespace-nowrap rounded border border-[#333] bg-[#222] px-2 py-1 text-xs text-gray-200 opacity-0 shadow-xl transition-all group-hover:opacity-100">
<LogOut size={20} />
<span className="pointer-events-none invisible absolute left-full ml-2 whitespace-nowrap rounded border border-[#333] bg-[#222] px-2 py-1 text-xs text-gray-200 opacity-0 shadow-xl transition-all group-hover:visible group-hover:opacity-100">
{currentUser ? `${currentUser.username} / 退出` : '退出登录'}
</span>
</button>

View File

@@ -113,7 +113,7 @@ describe('UserAdmin', () => {
projects: [
{
id: '7',
name: 'Data_MyVideo_1',
name: '演视LC视频序列',
status: 'pending',
frames: 0,
fps: '30FPS',
@@ -156,7 +156,7 @@ describe('UserAdmin', () => {
await waitFor(() => expect(apiMock.resetDemoFactory).toHaveBeenCalledWith('RESET_DEMO_FACTORY'));
expect(await screen.findByText('演示环境已恢复出厂设置')).toBeInTheDocument();
expect(useStore.getState().projects).toEqual([
expect.objectContaining({ name: 'Data_MyVideo_1', source_type: 'video' }),
expect.objectContaining({ name: '演视LC视频序列', source_type: 'video' }),
expect.objectContaining({ name: '演示DICOM序列', source_type: 'dicom' }),
]);
expect(useStore.getState().frames).toEqual([]);

View File

@@ -205,7 +205,7 @@ describe('api client contracts', () => {
admin_user: { id: 1, username: 'admin', role: 'admin', is_active: 1 },
project: { id: 8, name: '演示DICOM序列', status: 'ready', source_type: 'dicom', frame_count: 300, video_path: 'uploads/8/dicom' },
projects: [
{ id: 7, name: 'Data_MyVideo_1', status: 'pending', source_type: 'video', frame_count: 0, video_path: 'uploads/7/Data_MyVideo_1.mp4' },
{ id: 7, name: '演视LC视频序列', status: 'pending', source_type: 'video', frame_count: 0, video_path: 'uploads/7/Data_MyVideo_1.mp4' },
{ id: 8, name: '演示DICOM序列', status: 'ready', source_type: 'dicom', frame_count: 300, video_path: 'uploads/8/dicom' },
],
deleted_counts: { users: 1 },
@@ -216,7 +216,7 @@ describe('api client contracts', () => {
admin_user: expect.objectContaining({ username: 'admin' }),
project: expect.objectContaining({ id: '8', name: '演示DICOM序列', frames: 300, source_type: 'dicom' }),
projects: [
expect.objectContaining({ id: '7', name: 'Data_MyVideo_1', frames: 0, source_type: 'video' }),
expect.objectContaining({ id: '7', name: '演视LC视频序列', frames: 0, source_type: 'video' }),
expect.objectContaining({ id: '8', name: '演示DICOM序列', frames: 300, source_type: 'dicom' }),
],
}));