mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-01-23 22:43:28 +00:00
统一data model
This commit is contained in:
226
src/ts/schema.ts
Normal file
226
src/ts/schema.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
export const CURRENT_SCHEMA_VERSION = '1.0.0';
|
||||
|
||||
export interface Transform {
|
||||
SCALE_X: number;
|
||||
SCALE_Y: number;
|
||||
TRANSLATE_X: number;
|
||||
TRANSLATE_Y: number;
|
||||
}
|
||||
|
||||
export interface NodeStyle {
|
||||
// Size and transform
|
||||
width: number;
|
||||
height: number;
|
||||
rotate?: number; // deg
|
||||
|
||||
// Shape/appearance
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth?: number;
|
||||
radius?: number | [number, number, number, number];
|
||||
opacity?: number; // 0..1
|
||||
|
||||
// Shadow
|
||||
shadow?: {
|
||||
color?: string;
|
||||
blur?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
};
|
||||
|
||||
// Text style for text node or nodes with text
|
||||
textStyle?: {
|
||||
color?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontWeight?: number | string;
|
||||
lineHeight?: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
verticalAlign?: 'top' | 'middle' | 'bottom';
|
||||
letterSpacing?: number;
|
||||
padding?: [number, number, number, number];
|
||||
background?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NodeMeta {
|
||||
z?: number;
|
||||
locked?: boolean;
|
||||
visible?: boolean;
|
||||
groupId?: string;
|
||||
name?: string;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
}
|
||||
|
||||
export interface NodeProperties {
|
||||
style: NodeStyle;
|
||||
meta?: NodeMeta;
|
||||
image?: { url: string; fit?: 'fill'|'contain'|'cover' };
|
||||
text?: { content: string; rich?: boolean };
|
||||
vector?: { kind: 'path'|'rect'|'ellipse'|'polygon'; path?: string; points?: Array<[number, number]> };
|
||||
shikigami?: { name: string; avatar: string; rarity: string };
|
||||
yuhun?: { name: string; type: string; avatar: string; shortName?: string };
|
||||
property?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
type: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
properties: NodeProperties;
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
id: string;
|
||||
type?: string;
|
||||
sourceNodeId: string;
|
||||
targetNodeId: string;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GraphDocument { nodes: GraphNode[]; edges: GraphEdge[]; }
|
||||
|
||||
export interface FlowFile {
|
||||
label: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
type: string; // 'FLOW'
|
||||
graphRawData: GraphDocument;
|
||||
transform: Transform;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface RootDocument {
|
||||
schemaVersion: string;
|
||||
fileList: FlowFile[];
|
||||
activeFile: string;
|
||||
}
|
||||
|
||||
export const DefaultNodeStyle: NodeStyle = {
|
||||
width: 180,
|
||||
height: 120,
|
||||
rotate: 0,
|
||||
fill: '#ffffff',
|
||||
stroke: '#dcdfe6',
|
||||
strokeWidth: 1,
|
||||
radius: 4,
|
||||
opacity: 1,
|
||||
shadow: { color: 'rgba(0,0,0,0.1)', blur: 4, offsetX: 0, offsetY: 2 },
|
||||
textStyle: {
|
||||
color: '#303133',
|
||||
fontFamily: 'system-ui',
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.4,
|
||||
align: 'left',
|
||||
verticalAlign: 'top',
|
||||
letterSpacing: 0,
|
||||
padding: [8, 8, 8, 8],
|
||||
},
|
||||
};
|
||||
|
||||
function ensureTransform(t?: Partial<Transform>): Transform {
|
||||
return {
|
||||
SCALE_X: t?.SCALE_X ?? 1,
|
||||
SCALE_Y: t?.SCALE_Y ?? 1,
|
||||
TRANSLATE_X: t?.TRANSLATE_X ?? 0,
|
||||
TRANSLATE_Y: t?.TRANSLATE_Y ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Migration to v1 root document
|
||||
export function migrateToV1(input: any): RootDocument {
|
||||
const now = Date.now();
|
||||
|
||||
// Normalize a single node into the v1 shape (properties.style + meta, width/height mirrored)
|
||||
const migrateNode = (node: any): GraphNode => {
|
||||
const n: any = { ...node };
|
||||
const props: any = n.properties ?? {};
|
||||
const style: any = props.style ?? {};
|
||||
const meta: any = props.meta ?? {};
|
||||
|
||||
// Prefer explicit style width/height; otherwise fall back to scattered fields
|
||||
const propWidth = props.width ?? props.w;
|
||||
const propHeight = props.height ?? props.h;
|
||||
|
||||
if (style.width == null) {
|
||||
if (propWidth != null) {
|
||||
style.width = propWidth;
|
||||
} else if (n.width != null) {
|
||||
style.width = n.width;
|
||||
}
|
||||
}
|
||||
if (style.height == null) {
|
||||
if (propHeight != null) {
|
||||
style.height = propHeight;
|
||||
} else if (n.height != null) {
|
||||
style.height = n.height;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure meta defaults
|
||||
if (meta.visible == null) meta.visible = true;
|
||||
if (meta.locked == null) meta.locked = false;
|
||||
|
||||
props.style = style;
|
||||
props.meta = meta;
|
||||
n.properties = props;
|
||||
|
||||
// Mirror back to node width/height for render engines that still read from the node itself
|
||||
if (style.width != null) n.width = style.width;
|
||||
if (style.height != null) n.height = style.height;
|
||||
|
||||
return n as GraphNode;
|
||||
};
|
||||
|
||||
const ensureGraphDocument = (f: any): GraphDocument => {
|
||||
const raw = (f?.graphRawData && typeof f.graphRawData === 'object')
|
||||
? f.graphRawData
|
||||
: { nodes: [], edges: [] };
|
||||
const nodes = Array.isArray(raw.nodes) ? raw.nodes.map(migrateNode) : [];
|
||||
const edges = Array.isArray(raw.edges) ? raw.edges : [];
|
||||
return { nodes, edges };
|
||||
};
|
||||
|
||||
const wrap = (files: any[], active?: string): RootDocument => ({
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
fileList: files.map((f, i) => ({
|
||||
label: f?.label ?? `File ${i + 1}`,
|
||||
name: f?.name ?? `File ${i + 1}`,
|
||||
visible: f?.visible ?? true,
|
||||
type: f?.type ?? 'FLOW',
|
||||
graphRawData: ensureGraphDocument(f),
|
||||
transform: ensureTransform(f?.transform),
|
||||
createdAt: f?.createdAt ?? now,
|
||||
updatedAt: f?.updatedAt ?? now,
|
||||
id: f?.id,
|
||||
})),
|
||||
activeFile: active ?? (files[0]?.name ?? 'File 1'),
|
||||
});
|
||||
|
||||
if (!input) {
|
||||
return wrap([{ label: 'File 1', name: 'File 1', visible: true, type: 'FLOW' }]);
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
return wrap(input);
|
||||
}
|
||||
|
||||
if (typeof input === 'object' && 'fileList' in input) {
|
||||
const active = (input as any).activeFile;
|
||||
const files = (input as any).fileList ?? [];
|
||||
const root = wrap(files, active);
|
||||
// Preserve version if present
|
||||
root.schemaVersion = (input as any).schemaVersion || CURRENT_SCHEMA_VERSION;
|
||||
return root;
|
||||
}
|
||||
|
||||
// Oldest shape: treat input as groups array and wrap
|
||||
return wrap([{ label: 'File 1', name: 'File 1', visible: true, type: 'FLOW', groups: input }]);
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {defineStore} from 'pinia';
|
||||
import {ref, computed} from 'vue';
|
||||
// import type { Edge, Node, ViewportTransform } from '@vue-flow/core';
|
||||
import {ElMessageBox} from "element-plus";
|
||||
import {useGlobalMessage} from "./useGlobalMessage";
|
||||
import {getLogicFlowInstance} from "./useLogicFlow";
|
||||
import {CURRENT_SCHEMA_VERSION, migrateToV1, RootDocument} from "./schema";
|
||||
|
||||
const {showMessage} = useGlobalMessage();
|
||||
|
||||
@@ -11,6 +13,11 @@ const {showMessage} = useGlobalMessage();
|
||||
let localStorageDebounceTimer: NodeJS.Timeout | null = null;
|
||||
const LOCALSTORAGE_DEBOUNCE_DELAY = 1000; // 1秒防抖
|
||||
|
||||
type PersistedRoot = RootDocument & {
|
||||
activeFileId?: string;
|
||||
activeFile?: string;
|
||||
};
|
||||
|
||||
interface FlowFile {
|
||||
id: string; // stable identity, do not rely on name for selection
|
||||
label: string;
|
||||
@@ -130,40 +137,25 @@ export const useFilesStore = defineStore('files', () => {
|
||||
// 导入数据(兼容旧格式 activeFile/name)
|
||||
const importData = (data: any) => {
|
||||
try {
|
||||
let incoming: any[] = [];
|
||||
if (data && Array.isArray(data.fileList)) {
|
||||
incoming = data.fileList;
|
||||
} else if (Array.isArray(data)) {
|
||||
incoming = data; // old shape: file array directly
|
||||
} else {
|
||||
// older: only groups array -> wrap as one file
|
||||
const index = fileList.value.length + 1;
|
||||
const newFile: FlowFile = {
|
||||
id: genId(),
|
||||
label: `File ${index}`,
|
||||
name: `File ${index}`,
|
||||
visible: true,
|
||||
type: 'FLOW',
|
||||
graphRawData: { nodes: [], edges: [] },
|
||||
transform: { SCALE_X: 1, SCALE_Y: 1, TRANSLATE_X: 0, TRANSLATE_Y: 0 },
|
||||
};
|
||||
fileList.value.push(newFile);
|
||||
activeFileId.value = newFile.id;
|
||||
showMessage('success', '数据导入成功');
|
||||
return;
|
||||
}
|
||||
// 如果已有 schemaVersion,则视为 v1 RootDocument;否则通过迁移器补齐
|
||||
const root: PersistedRoot = (data && typeof data === 'object' && (data as any).schemaVersion)
|
||||
? (data as PersistedRoot)
|
||||
: migrateToV1(data) as PersistedRoot;
|
||||
|
||||
const normalized = normalizeList(incoming);
|
||||
const normalized = normalizeList(root.fileList || []);
|
||||
fileList.value = normalized;
|
||||
|
||||
// 选中逻辑:优先 activeFileId -> 其次 activeFile(name) -> 首个
|
||||
let nextActiveId: string | undefined = undefined;
|
||||
const idFromData = (data as any).activeFileId;
|
||||
const idFromData = (data as any).activeFileId ?? root.activeFileId;
|
||||
if (idFromData && normalized.some(f => f.id === idFromData)) {
|
||||
nextActiveId = idFromData;
|
||||
} else if ((data as any).activeFile) {
|
||||
const byName = normalized.find(f => f.name === (data as any).activeFile);
|
||||
nextActiveId = byName?.id;
|
||||
} else {
|
||||
const nameFromData = (data as any).activeFile ?? root.activeFile;
|
||||
if (nameFromData) {
|
||||
const byName = normalized.find(f => f.name === nameFromData);
|
||||
nextActiveId = byName?.id;
|
||||
}
|
||||
}
|
||||
activeFileId.value = nextActiveId || normalized[0]?.id || '';
|
||||
|
||||
@@ -179,6 +171,7 @@ export const useFilesStore = defineStore('files', () => {
|
||||
try {
|
||||
const activeName = findById(activeFileId.value)?.name || '';
|
||||
const dataStr = JSON.stringify({
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
fileList: fileList.value,
|
||||
activeFileId: activeFileId.value,
|
||||
activeFile: activeName,
|
||||
@@ -199,16 +192,27 @@ export const useFilesStore = defineStore('files', () => {
|
||||
|
||||
// 启动自动恢复;如有保存的数据则直接恢复;否则用默认
|
||||
const initializeWithPrompt = () => {
|
||||
const savedState = loadStateFromLocalStorage();
|
||||
const savedStateRaw = loadStateFromLocalStorage();
|
||||
const defaultState = getDefaultState();
|
||||
|
||||
if (savedState && savedState.fileList) {
|
||||
const normalized = normalizeList(savedState.fileList || []);
|
||||
if (savedStateRaw && (savedStateRaw as any).fileList) {
|
||||
// 若已有 schemaVersion,则视为 v1;否则通过迁移器补齐到 RootDocument 形态
|
||||
const root: PersistedRoot = ((savedStateRaw as any).schemaVersion)
|
||||
? (savedStateRaw as PersistedRoot)
|
||||
: migrateToV1(savedStateRaw) as PersistedRoot;
|
||||
|
||||
const normalized = normalizeList(root.fileList || []);
|
||||
fileList.value = normalized;
|
||||
const id = savedState.activeFileId;
|
||||
let next = (id && normalized.find(f => f.id === id)?.id) || undefined;
|
||||
if (!next && savedState.activeFile) {
|
||||
next = normalized.find(f => f.name === savedState.activeFile)?.id;
|
||||
|
||||
let next: string | undefined;
|
||||
const idFromData = (savedStateRaw as any).activeFileId ?? root.activeFileId;
|
||||
if (idFromData && normalized.some(f => f.id === idFromData)) {
|
||||
next = idFromData;
|
||||
} else {
|
||||
const nameFromData = (savedStateRaw as any).activeFile ?? root.activeFile;
|
||||
if (nameFromData) {
|
||||
next = normalized.find(f => f.name === nameFromData)?.id;
|
||||
}
|
||||
}
|
||||
activeFileId.value = next || normalized[0]?.id || '';
|
||||
showMessage('success', '已恢复上次工作区');
|
||||
@@ -290,8 +294,9 @@ export const useFilesStore = defineStore('files', () => {
|
||||
syncLogicFlowDataToStore(targetId);
|
||||
|
||||
// 再保存到 localStorage(带防抖)
|
||||
const state = {
|
||||
fileList: fileList.value,
|
||||
const state: PersistedRoot = {
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
fileList: fileList.value as any,
|
||||
activeFileId: activeFileId.value,
|
||||
activeFile: findById(activeFileId.value)?.name || ''
|
||||
};
|
||||
@@ -351,4 +356,4 @@ export const useFilesStore = defineStore('files', () => {
|
||||
activeFileId,
|
||||
visibleFiles,
|
||||
};
|
||||
});;;
|
||||
});;;
|
||||
|
||||
Reference in New Issue
Block a user