feat: 完善分割工作区导入导出与管理流程
- 新增基于 JWT 当前用户的登录恢复、角色权限、用户管理、审计日志和演示出厂重置后台接口与前端管理页。 - 重串 GT_label 导出和 GT Mask 导入逻辑:导出保留类别真实 maskid,导入仅接受灰度或 RGB 等通道 maskid 图,支持未知 maskid 策略、尺寸最近邻拉伸和导入预览。 - 统一分割结果导出体验:默认当前帧,按项目抽帧顺序和 XhXXmXXsXXXms 时间戳命名 ZIP 与图片,补齐 GT/Pro/Mix/分开 Mask 输出和映射 JSON。 - 调整工作区左侧工具栏:移除创建点/线段入口,新增画笔、橡皮擦及尺寸控制,并按绘制、布尔、导入/AI 工具分组分隔。 - 扩展 Canvas 编辑能力:画笔按语义分类绘制并可自动并入连通选中 mask,橡皮擦对选中区域扣除,优化布尔操作、选区、撤销重做和保存状态联动。 - 优化自动传播时间轴显示:同一蓝色系按传播新旧递进变暗,老传播记录达到阈值后统一旧记录色,并维护范围选择与清空后的历史显示。 - 将 AI 智能分割入口替换为更明确的 AI 元素图标,并同步侧栏、工作区和 AI 页面入口表现。 - 完善模板分类、maskid 工具函数、分类树联动、遮罩透明度、边缘平滑和传播链同步相关前端状态。 - 扩展后端项目、媒体、任务、Dashboard、模板和传播 runner 的用户隔离、任务控制、进度事件与兼容处理。 - 补充前后端测试,覆盖用户管理、GT_label 往返导入导出、GT Mask 校验和预览、画笔/橡皮擦、时间轴传播历史、导出范围、WebSocket 与 API 封装。 - 更新 AGENTS、README 和 doc 文档,记录当前接口契约、实现状态、测试计划、安装说明和 maskid/GT_label 规则。
This commit is contained in:
@@ -8,15 +8,17 @@ describe('useStore', () => {
|
||||
});
|
||||
|
||||
it('stores and clears auth state with localStorage', () => {
|
||||
useStore.getState().login('token-1');
|
||||
useStore.getState().login('token-1', { id: 1, username: 'admin', role: 'admin' });
|
||||
|
||||
expect(useStore.getState().isAuthenticated).toBe(true);
|
||||
expect(useStore.getState().token).toBe('token-1');
|
||||
expect(useStore.getState().currentUser?.username).toBe('admin');
|
||||
expect(localStorage.getItem('token')).toBe('token-1');
|
||||
|
||||
useStore.getState().logout();
|
||||
|
||||
expect(useStore.getState().isAuthenticated).toBe(false);
|
||||
expect(useStore.getState().currentUser).toBeNull();
|
||||
expect(useStore.getState().projects).toEqual([]);
|
||||
expect(useStore.getState().frames).toEqual([]);
|
||||
expect(localStorage.getItem('token')).toBeNull();
|
||||
@@ -32,6 +34,8 @@ describe('useStore', () => {
|
||||
useStore.getState().addMask({ id: 'm1', frameId: 'f1', pathData: 'M 0 0 Z', label: 'mask', color: '#fff' });
|
||||
useStore.getState().setSelectedMaskIds(['m1']);
|
||||
useStore.getState().setMaskPreviewOpacity(35);
|
||||
useStore.getState().setBrushSize(36);
|
||||
useStore.getState().setEraserSize(44);
|
||||
useStore.getState().updateMask('m1', { label: 'updated mask', saveStatus: 'dirty' });
|
||||
useStore.getState().addAnnotation({ id: 'a1', frameId: 'f1', type: 'mask', points: [], label: 'ann', color: '#fff' });
|
||||
useStore.getState().addTemplate({ id: 't1', name: 'Template', classes: [], rules: [] });
|
||||
@@ -44,6 +48,8 @@ describe('useStore', () => {
|
||||
expect(useStore.getState().currentFrameIndex).toBe(0);
|
||||
expect(useStore.getState().selectedMaskIds).toEqual(['m1']);
|
||||
expect(useStore.getState().maskPreviewOpacity).toBe(35);
|
||||
expect(useStore.getState().brushSize).toBe(36);
|
||||
expect(useStore.getState().eraserSize).toBe(44);
|
||||
expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({ label: 'updated mask', saveStatus: 'dirty' }));
|
||||
expect(useStore.getState().annotations).toHaveLength(1);
|
||||
expect(useStore.getState().templates[0].name).toBe('Template 2');
|
||||
|
||||
@@ -24,6 +24,8 @@ export type AiModelId =
|
||||
| 'sam2.1_hiera_large';
|
||||
|
||||
export const DEFAULT_AI_MODEL_ID: AiModelId = 'sam2.1_hiera_tiny';
|
||||
export const DEFAULT_BRUSH_SIZE = 24;
|
||||
export const DEFAULT_ERASER_SIZE = 28;
|
||||
|
||||
export const SAM2_MODEL_OPTIONS: Array<{ id: AiModelId; label: string; shortLabel: string }> = [
|
||||
{ id: 'sam2.1_hiera_tiny', label: 'SAM 2.1 Tiny', shortLabel: 'tiny' },
|
||||
@@ -64,6 +66,7 @@ export interface Mask {
|
||||
classId?: string;
|
||||
className?: string;
|
||||
classZIndex?: number;
|
||||
classMaskId?: number;
|
||||
saveStatus?: 'draft' | 'saved' | 'dirty' | 'saving' | 'error';
|
||||
saved?: boolean;
|
||||
pathData: string;
|
||||
@@ -92,6 +95,7 @@ export interface TemplateClass {
|
||||
name: string;
|
||||
color: string;
|
||||
zIndex: number;
|
||||
maskId?: number;
|
||||
category?: string;
|
||||
description?: string;
|
||||
}
|
||||
@@ -104,11 +108,20 @@ export interface TemplateRule {
|
||||
operation: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: number;
|
||||
username: string;
|
||||
role: string;
|
||||
is_active?: number;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
// Auth
|
||||
isAuthenticated: boolean;
|
||||
token: string | null;
|
||||
login: (token: string) => void;
|
||||
currentUser: UserProfile | null;
|
||||
login: (token: string, user?: UserProfile | null) => void;
|
||||
setCurrentUser: (user: UserProfile | null) => void;
|
||||
logout: () => void;
|
||||
|
||||
// Projects
|
||||
@@ -129,6 +142,8 @@ export interface AppState {
|
||||
masks: Mask[];
|
||||
selectedMaskIds: string[];
|
||||
maskPreviewOpacity: number;
|
||||
brushSize: number;
|
||||
eraserSize: number;
|
||||
maskHistory: Mask[][];
|
||||
maskFuture: Mask[][];
|
||||
setActiveModule: (module: string) => void;
|
||||
@@ -142,6 +157,8 @@ export interface AppState {
|
||||
setMasks: (masks: Mask[]) => void;
|
||||
setSelectedMaskIds: (ids: string[]) => void;
|
||||
setMaskPreviewOpacity: (opacity: number) => void;
|
||||
setBrushSize: (size: number) => void;
|
||||
setEraserSize: (size: number) => void;
|
||||
clearMasks: () => void;
|
||||
undoMasks: () => void;
|
||||
redoMasks: () => void;
|
||||
@@ -169,17 +186,20 @@ export interface AppState {
|
||||
|
||||
export const useStore = create<AppState>((set) => ({
|
||||
// Auth
|
||||
isAuthenticated: false,
|
||||
token: null,
|
||||
login: (token: string) => {
|
||||
isAuthenticated: Boolean(localStorage.getItem('token')),
|
||||
token: localStorage.getItem('token'),
|
||||
currentUser: null,
|
||||
login: (token: string, user: UserProfile | null = null) => {
|
||||
localStorage.setItem('token', token);
|
||||
set({ isAuthenticated: true, token });
|
||||
set({ isAuthenticated: true, token, currentUser: user });
|
||||
},
|
||||
setCurrentUser: (currentUser: UserProfile | null) => set({ currentUser }),
|
||||
logout: () => {
|
||||
localStorage.removeItem('token');
|
||||
set({
|
||||
isAuthenticated: false,
|
||||
token: null,
|
||||
currentUser: null,
|
||||
currentProject: null,
|
||||
projects: [],
|
||||
templates: [],
|
||||
@@ -188,6 +208,8 @@ export const useStore = create<AppState>((set) => ({
|
||||
masks: [],
|
||||
selectedMaskIds: [],
|
||||
maskPreviewOpacity: 50,
|
||||
brushSize: DEFAULT_BRUSH_SIZE,
|
||||
eraserSize: DEFAULT_ERASER_SIZE,
|
||||
maskHistory: [],
|
||||
maskFuture: [],
|
||||
activeTemplateId: null,
|
||||
@@ -218,6 +240,8 @@ export const useStore = create<AppState>((set) => ({
|
||||
masks: [],
|
||||
selectedMaskIds: [],
|
||||
maskPreviewOpacity: 50,
|
||||
brushSize: DEFAULT_BRUSH_SIZE,
|
||||
eraserSize: DEFAULT_ERASER_SIZE,
|
||||
maskHistory: [],
|
||||
maskFuture: [],
|
||||
setActiveModule: (activeModule: string) => set({ activeModule }),
|
||||
@@ -254,6 +278,12 @@ export const useStore = create<AppState>((set) => ({
|
||||
setMaskPreviewOpacity: (maskPreviewOpacity: number) => set({
|
||||
maskPreviewOpacity: Math.min(Math.max(maskPreviewOpacity, 10), 100),
|
||||
}),
|
||||
setBrushSize: (brushSize: number) => set({
|
||||
brushSize: Math.round(Math.min(Math.max(brushSize, 4), 96)),
|
||||
}),
|
||||
setEraserSize: (eraserSize: number) => set({
|
||||
eraserSize: Math.round(Math.min(Math.max(eraserSize, 4), 128)),
|
||||
}),
|
||||
clearMasks: () =>
|
||||
set((state) => ({
|
||||
masks: [],
|
||||
|
||||
Reference in New Issue
Block a user