修复导入问题

This commit is contained in:
2025-12-24 14:04:57 +08:00
parent 651d82a20f
commit 919bd08ca1
2 changed files with 164 additions and 114 deletions

View File

@@ -2,7 +2,7 @@
import Toolbar from './components/Toolbar.vue'; import Toolbar from './components/Toolbar.vue';
import ProjectExplorer from './components/ProjectExplorer.vue'; import ProjectExplorer from './components/ProjectExplorer.vue';
import ComponentsPanel from './components/flow/ComponentsPanel.vue'; import ComponentsPanel from './components/flow/ComponentsPanel.vue';
import {computed, ref, onMounted, onUnmounted, onBeforeUpdate, reactive, provide, inject, watch} from "vue"; import {computed, ref, onMounted, onUnmounted, reactive, watch} from "vue";
import {useFilesStore} from "@/ts/useStore"; import {useFilesStore} from "@/ts/useStore";
import Vue3DraggableResizable from 'vue3-draggable-resizable'; import Vue3DraggableResizable from 'vue3-draggable-resizable';
import {TabPaneName, TabsPaneContext} from "element-plus"; import {TabPaneName, TabsPaneContext} from "element-plus";
@@ -10,12 +10,10 @@ import FlowEditor from './components/flow/FlowEditor.vue';
import ShikigamiSelect from './components/flow/nodes/yys/ShikigamiSelect.vue'; import ShikigamiSelect from './components/flow/nodes/yys/ShikigamiSelect.vue';
import YuhunSelect from './components/flow/nodes/yys/YuhunSelect.vue'; import YuhunSelect from './components/flow/nodes/yys/YuhunSelect.vue';
import PropertySelect from './components/flow/nodes/yys/PropertySelect.vue'; import PropertySelect from './components/flow/nodes/yys/PropertySelect.vue';
// import { useVueFlow } from '@vue-flow/core';
import DialogManager from './components/DialogManager.vue'; import DialogManager from './components/DialogManager.vue';
import {getLogicFlowInstance} from "@/ts/useLogicFlow"; import {getLogicFlowInstance} from "@/ts/useLogicFlow";
const filesStore = useFilesStore(); const filesStore = useFilesStore();
// const { updateNode,toObject,fromObject } = useVueFlow();
const width = ref('100%'); const width = ref('100%');
const height = ref('100vh'); const height = ref('100vh');
@@ -44,30 +42,54 @@ onUnmounted(() => {
}); });
// 1) 切换激活文件:仅当 id 变化时保存旧数据并渲染新数据
watch( watch(
() => filesStore.activeFile, () => filesStore.activeFileId,
async (newVal, oldVal) => { async (newId, oldId) => {
// 保存旧 tab 数据 if (oldId && newId !== oldId) {
if (oldVal) { filesStore.updateTab(oldId);
filesStore.updateTab(oldVal);
} }
// 渲染新 tab 数据 if (newId) {
if (newVal) {
const logicFlowInstance = getLogicFlowInstance(); const logicFlowInstance = getLogicFlowInstance();
const currentTab = filesStore.getTab(newVal); const currentTab = filesStore.getTab(newId);
if (logicFlowInstance && currentTab?.graphRawData) { if (logicFlowInstance && currentTab?.graphRawData) {
try { try {
logicFlowInstance.render(currentTab.graphRawData); logicFlowInstance.render(currentTab.graphRawData);
logicFlowInstance.zoom(currentTab.transform.SCALE_X, [currentTab.transform.TRANSLATE_X, currentTab.transform.TRANSLATE_Y]); logicFlowInstance.zoom(
currentTab.transform?.SCALE_X ?? 1,
[currentTab.transform?.TRANSLATE_X ?? 0, currentTab.transform?.TRANSLATE_Y ?? 0]
);
} catch (error) { } catch (error) {
console.warn('渲染画布数据失败:', error); console.warn('渲染画布数据失败:', error);
} }
} }
} }
},
{ flush: 'post' }
);
// 2) 导入等替换 fileList 引用时,主动按当前 activeFileId 渲染一次,不保存旧数据
watch(
() => filesStore.fileList,
() => {
const logicFlowInstance = getLogicFlowInstance();
const currentTab = filesStore.getTab(filesStore.activeFileId);
if (logicFlowInstance && currentTab?.graphRawData) {
try {
logicFlowInstance.render(currentTab.graphRawData);
logicFlowInstance.zoom(
currentTab.transform?.SCALE_X ?? 1,
[currentTab.transform?.TRANSLATE_X ?? 0, currentTab.transform?.TRANSLATE_Y ?? 0]
);
} catch (error) {
console.warn('渲染画布数据失败:', error);
} }
}
},
{ flush: 'post' }
); );
</script> </script>
@@ -83,7 +105,7 @@ watch(
<!-- 工作区 --> <!-- 工作区 -->
<div class="workspace"> <div class="workspace">
<el-tabs <el-tabs
v-model="filesStore.activeFile" v-model="filesStore.activeFileId"
type="card" type="card"
class="demo-tabs" class="demo-tabs"
editable editable
@@ -91,9 +113,9 @@ watch(
> >
<el-tab-pane <el-tab-pane
v-for="(file, index) in filesStore.visibleFiles" v-for="(file, index) in filesStore.visibleFiles"
:key="`${file.name}-${filesStore.activeFile}`" :key="`${file.id}-${filesStore.activeFileId}`"
:label="file.label" :label="file.label"
:name="file.name.toString()" :name="file.id"
/> />
</el-tabs> </el-tabs>
<div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }"> <div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }">

View File

@@ -12,6 +12,7 @@ let localStorageDebounceTimer: NodeJS.Timeout | null = null;
const LOCALSTORAGE_DEBOUNCE_DELAY = 1000; // 1秒防抖 const LOCALSTORAGE_DEBOUNCE_DELAY = 1000; // 1秒防抖
interface FlowFile { interface FlowFile {
id: string; // stable identity, do not rely on name for selection
label: string; label: string;
name: string; name: string;
visible: boolean; visible: boolean;
@@ -26,9 +27,11 @@ interface FlowFile {
} }
function getDefaultState() { function getDefaultState() {
const id = 'f_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
return { return {
"fileList": [ "fileList": [
{ {
"id": id,
"label": "File 1", "label": "File 1",
"name": "File 1", "name": "File 1",
"visible": true, "visible": true,
@@ -45,8 +48,10 @@ function getDefaultState() {
} }
} }
], ],
"activeFile": "File 1" // legacy: kept for compatibility, actual selection uses activeFileId
}; "activeFile": "File 1",
"activeFileId": id
} as any;
} }
function clearFilesStoreLocalStorage() { function clearFilesStoreLocalStorage() {
@@ -89,63 +94,94 @@ function saveStateToLocalStorage(state: any) {
export const useFilesStore = defineStore('files', () => { export const useFilesStore = defineStore('files', () => {
// 文件列表状态 // Helper: generate stable ids
const genId = () => 'f_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
// 文件列表状态(以 id 作为主键)
const fileList = ref<FlowFile[]>([]); const fileList = ref<FlowFile[]>([]);
const activeFile = ref<string>(''); const activeFileId = ref<string>('');
// 计算属性:获取可见的文件 // 计算属性:获取可见的文件
const visibleFiles = computed(() => { const visibleFiles = computed(() => {
return fileList.value.filter(file => file.visible); return fileList.value.filter(file => file.visible);
}); });
// 导入数据 // 根据传入列表补全缺省字段并补齐 id
const importData = (data: any) => { const normalizeList = (list: any[]): FlowFile[] => {
try { return (list || []).map((f: any, i: number) => ({
if (data.fileList && Array.isArray(data.fileList)) { id: f?.id || genId(),
// 新版本格式:包含 fileList 和 activeFile label: f?.label ?? f?.name ?? `File ${i + 1}`,
fileList.value = data.fileList; name: f?.name ?? f?.label ?? `File ${i + 1}`,
activeFile.value = data.activeFile || data[0]?.name; visible: f?.visible ?? true,
showMessage('success', '数据导入成功'); type: f?.type ?? 'FLOW',
} else if (Array.isArray(data) && data[0]?.visible === true) { graphRawData: (f?.graphRawData && typeof f.graphRawData === 'object') ? f.graphRawData : { nodes: [], edges: [] },
// 兼容旧版本格式:直接是 fileList 数组 transform: f?.transform ?? {
fileList.value = data;
activeFile.value = data[0]?.name || "1";
showMessage('success', '数据导入成功');
} else {
// 兼容更旧版本格式:仅包含 groups 数组
const newFile = {
label: `File ${fileList.value.length + 1}`,
name: String(fileList.value.length + 1),
visible: true,
type: "FLOW",
groups: data,
graphRawData: {
nodes: [],
edges: []
},
transform: {
SCALE_X: 1, SCALE_X: 1,
SCALE_Y: 1, SCALE_Y: 1,
TRANSLATE_X: 0, TRANSLATE_X: 0,
TRANSLATE_Y: 0 TRANSLATE_Y: 0
} },
}));
};
const findById = (id?: string) => fileList.value.find(f => f.id === id);
const findByName = (name?: string) => fileList.value.find(f => f.name === name);
// 导入数据(兼容旧格式 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); fileList.value.push(newFile);
activeFile.value = newFile.name; activeFileId.value = newFile.id;
showMessage('success', '数据导入成功'); showMessage('success', '数据导入成功');
return;
} }
const normalized = normalizeList(incoming);
fileList.value = normalized;
// 选中逻辑:优先 activeFileId -> 其次 activeFile(name) -> 首个
let nextActiveId: string | undefined = undefined;
const idFromData = (data as any).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;
}
activeFileId.value = nextActiveId || normalized[0]?.id || '';
showMessage('success', '数据导入成功');
} catch (error) { } catch (error) {
console.error('Failed to import file', error); console.error('Failed to import file', error);
showMessage('error', '数据导入失败'); showMessage('error', '数据导入失败');
} }
}; };
// 导出数据 // 导出数据(同时携带 activeFile 与 activeFileId 以兼容旧版)
const exportData = () => { const exportData = () => {
try { try {
const activeName = findById(activeFileId.value)?.name || '';
const dataStr = JSON.stringify({ const dataStr = JSON.stringify({
fileList: fileList.value, fileList: fileList.value,
activeFile: activeFile.value activeFileId: activeFileId.value,
activeFile: activeName,
}, null, 2); }, null, 2);
const blob = new Blob([dataStr], {type: 'application/json;charset=utf-8'}); const blob = new Blob([dataStr], {type: 'application/json;charset=utf-8'});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -161,43 +197,36 @@ export const useFilesStore = defineStore('files', () => {
} }
}; };
// 初始化时检查是否有未保存的数据 // 启动自动恢复;如有保存的数据则直接恢复;否则用默认
const initializeWithPrompt = () => { const initializeWithPrompt = () => {
const savedState = loadStateFromLocalStorage(); const savedState = loadStateFromLocalStorage();
const defaultState = getDefaultState(); const defaultState = getDefaultState();
// 如果没有保存的数据,使用默认状态 if (savedState && savedState.fileList) {
if (!savedState) { const normalized = normalizeList(savedState.fileList || []);
fileList.value = defaultState.fileList; fileList.value = normalized;
activeFile.value = defaultState.activeFile; 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;
}
activeFileId.value = next || normalized[0]?.id || '';
showMessage('success', '已恢复上次工作区');
return; return;
} }
const isSame = JSON.stringify(savedState) === JSON.stringify(defaultState); // 无保存数据:使用默认
if (savedState && !isSame) { fileList.value = normalizeList(defaultState.fileList);
ElMessageBox.confirm( activeFileId.value = fileList.value[0]?.id || '';
'检测到有未保存的旧数据,是否恢复?', };
'提示',
{ // 提供重置接口:清空本地并回到默认
confirmButtonText: '恢复', const resetWorkspace = () => {
cancelButtonText: '不恢复',
type: 'warning',
}
).then(() => {
fileList.value = savedState.fileList || [];
activeFile.value = savedState.activeFile || "1";
showMessage('success', '数据已恢复');
}).catch(() => {
clearFilesStoreLocalStorage(); clearFilesStoreLocalStorage();
fileList.value = defaultState.fileList; const def = getDefaultState();
activeFile.value = defaultState.activeFile; fileList.value = normalizeList(def.fileList);
showMessage('info', '选择了不恢复旧数据'); activeFileId.value = fileList.value[0]?.id || '';
}); showMessage('success', '已重置工作区');
} else {
// 如果有保存的数据且与默认状态相同,直接使用保存的数据
fileList.value = savedState.fileList || defaultState.fileList;
activeFile.value = savedState.activeFile || defaultState.activeFile;
}
}; };
// 设置自动更新 // 设置自动更新
@@ -215,7 +244,8 @@ export const useFilesStore = defineStore('files', () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const newFileName = `File ${fileList.value.length + 1}`; const newFileName = `File ${fileList.value.length + 1}`;
const newFile = { const newFile: FlowFile = {
id: genId(),
label: newFileName, label: newFileName,
name: newFileName, name: newFileName,
visible: true, visible: true,
@@ -229,22 +259,22 @@ export const useFilesStore = defineStore('files', () => {
} }
}; };
fileList.value.push(newFile); fileList.value.push(newFile);
activeFile.value = newFileName; activeFileId.value = newFile.id;
}); });
}; };
// 关闭文件标签 // 关闭文件标签
const removeTab = (fileName: string | undefined) => { const removeTab = (fileId: string | undefined) => {
if (!fileName) return; if (!fileId) return;
const index = fileList.value.findIndex(file => file.name === fileName); const index = fileList.value.findIndex(file => file.id === fileId);
if (index === -1) return; if (index === -1) return;
fileList.value.splice(index, 1); fileList.value.splice(index, 1);
// 如果关闭的是当前活动文件,则切换到其他文件 // 如果关闭的是当前活动文件,则切换到其他文件
if (activeFile.value === fileName) { if (activeFileId.value === fileId) {
activeFile.value = fileList.value[Math.max(0, index - 1)]?.name || ''; activeFileId.value = fileList.value[Math.max(0, index - 1)]?.id || '';
} }
// 关闭文件后立即更新 // 关闭文件后立即更新
@@ -252,17 +282,18 @@ export const useFilesStore = defineStore('files', () => {
}; };
// 更新指定 Tab - 内存操作即时localStorage 操作防抖 // 更新指定 Tab - 内存操作即时localStorage 操作防抖
const updateTab = (fileName?: string) => { const updateTab = (fileId?: string) => {
try { try {
const targetFile = fileName || activeFile.value; const targetId = fileId || activeFileId.value;
// 先同步 LogicFlow 数据到内存 // 先同步 LogicFlow 数据到内存
syncLogicFlowDataToStore(targetFile); syncLogicFlowDataToStore(targetId);
// 再保存到 localStorage带防抖 // 再保存到 localStorage带防抖
const state = { const state = {
fileList: fileList.value, fileList: fileList.value,
activeFile: activeFile.value activeFileId: activeFileId.value,
activeFile: findById(activeFileId.value)?.name || ''
}; };
saveStateToLocalStorage(state); saveStateToLocalStorage(state);
} catch (error) { } catch (error) {
@@ -271,18 +302,18 @@ export const useFilesStore = defineStore('files', () => {
} }
}; };
// 获取当前 Tab 数据 // 获取当前 Tab 数据(优先按 id兼容按 name
const getTab = (fileName?: string) => { const getTab = (fileKey?: string) => {
const targetFile = fileName || activeFile.value; const targetId = fileKey || activeFileId.value;
return fileList.value.find(f => f.name === targetFile); return findById(targetId) || findByName(fileKey || '');
}; };
// 同步 LogicFlow 画布数据到 store 的内部方法 // 同步 LogicFlow 画布数据到 store 的内部方法
const syncLogicFlowDataToStore = (fileName?: string) => { const syncLogicFlowDataToStore = (fileId?: string) => {
const logicFlowInstance = getLogicFlowInstance(); const logicFlowInstance = getLogicFlowInstance();
const targetFile = fileName || activeFile.value; const targetId = fileId || activeFileId.value;
if (logicFlowInstance && targetFile) { if (logicFlowInstance && targetId) {
try { try {
// 获取画布最新数据 // 获取画布最新数据
const graphData = logicFlowInstance.getGraphRawData(); const graphData = logicFlowInstance.getGraphRawData();
@@ -290,11 +321,11 @@ export const useFilesStore = defineStore('files', () => {
if (graphData) { if (graphData) {
// 直接保存原始数据到 GraphRawData // 直接保存原始数据到 GraphRawData
const file = fileList.value.find(f => f.name === targetFile); const file = findById(targetId);
if (file) { if (file) {
file.graphRawData = graphData; file.graphRawData = graphData;
file.transform = transform; file.transform = transform;
console.log(`已同步画布数据到文件 "${targetFile}"`); console.log(`已同步画布数据到文件 "${file.name}"(${targetId})`);
} }
} }
} catch (error) { } catch (error) {
@@ -303,15 +334,12 @@ export const useFilesStore = defineStore('files', () => {
} }
}; };
return { return {
importData, importData,
exportData, exportData,
initializeWithPrompt, initializeWithPrompt,
resetWorkspace,
setupAutoSave, setupAutoSave,
addTab, addTab,
@@ -320,7 +348,7 @@ export const useFilesStore = defineStore('files', () => {
getTab, getTab,
fileList, fileList,
activeFile, activeFileId,
visibleFiles, visibleFiles,
}; };
}); });;;