233 lines
5.8 KiB
TypeScript
233 lines
5.8 KiB
TypeScript
import { defineStore } from 'pinia';
|
|
|
|
const FILE_STORE_DEBUG_LOGS = false;
|
|
function fileDebugLog(...args: unknown[]) {
|
|
if (!FILE_STORE_DEBUG_LOGS) {
|
|
return;
|
|
}
|
|
console.log(...args);
|
|
}
|
|
|
|
interface FileNode {
|
|
type: 'folder' | 'file';
|
|
name: string;
|
|
path: string;
|
|
annotation?: string;
|
|
children?: FileNode[];
|
|
}
|
|
|
|
interface TodoTask {
|
|
index: number;
|
|
title: string;
|
|
status: string;
|
|
}
|
|
|
|
interface TodoList {
|
|
instruction?: string;
|
|
tasks?: TodoTask[];
|
|
}
|
|
|
|
interface ContextMenuState {
|
|
visible: boolean;
|
|
x: number;
|
|
y: number;
|
|
node: FileNode | null;
|
|
}
|
|
|
|
interface FileState {
|
|
fileTree: FileNode[];
|
|
expandedFolders: Record<string, boolean>;
|
|
todoList: TodoList | null;
|
|
fileTreeUnavailable: boolean;
|
|
fileTreeMessage: string;
|
|
contextMenu: ContextMenuState;
|
|
}
|
|
|
|
function buildNodes(treeMap: Record<string, any> | undefined): FileNode[] {
|
|
if (!treeMap) {
|
|
return [];
|
|
}
|
|
const entries = Object.keys(treeMap).map(name => {
|
|
const node = treeMap[name] || {};
|
|
if (node.type === 'folder') {
|
|
return {
|
|
type: 'folder' as const,
|
|
name,
|
|
path: node.path || name,
|
|
children: buildNodes(node.children)
|
|
};
|
|
}
|
|
return {
|
|
type: 'file' as const,
|
|
name,
|
|
path: node.path || name,
|
|
annotation: node.annotation || ''
|
|
};
|
|
});
|
|
entries.sort((a, b) => {
|
|
if (a.type !== b.type) {
|
|
return a.type === 'folder' ? -1 : 1;
|
|
}
|
|
return a.name.localeCompare(b.name, 'zh-CN');
|
|
});
|
|
return entries;
|
|
}
|
|
|
|
export const useFileStore = defineStore('file', {
|
|
state: (): FileState => ({
|
|
fileTree: [],
|
|
expandedFolders: {},
|
|
todoList: null,
|
|
fileTreeUnavailable: false,
|
|
fileTreeMessage: '',
|
|
contextMenu: {
|
|
visible: false,
|
|
x: 0,
|
|
y: 0,
|
|
node: null
|
|
}
|
|
}),
|
|
actions: {
|
|
async fetchFileTree() {
|
|
try {
|
|
const response = await fetch('/api/files');
|
|
const data = await response.json();
|
|
fileDebugLog('[FileTree] fetch result', data);
|
|
this.setFileTreeFromResponse(data);
|
|
} catch (error) {
|
|
console.error('获取文件树失败:', error);
|
|
}
|
|
},
|
|
markFileTreeUnavailable(message: string) {
|
|
this.fileTreeUnavailable = true;
|
|
this.fileTreeMessage = message || '宿主机模式下文件树不可用';
|
|
this.fileTree = [];
|
|
this.expandedFolders = {};
|
|
},
|
|
setFileTreeFromResponse(payload: any) {
|
|
if (!payload) {
|
|
console.warn('[FileTree] 空 payload');
|
|
return;
|
|
}
|
|
if (payload.unavailable) {
|
|
this.markFileTreeUnavailable(payload.message || payload.error);
|
|
return;
|
|
}
|
|
const structure = payload.structure || payload;
|
|
if (structure && structure.unavailable) {
|
|
this.markFileTreeUnavailable(structure.message || structure.error);
|
|
return;
|
|
}
|
|
if (!structure || !structure.tree) {
|
|
console.warn('[FileTree] 缺少 structure.tree', structure);
|
|
return;
|
|
}
|
|
this.fileTreeUnavailable = false;
|
|
this.fileTreeMessage = '';
|
|
const nodes = buildNodes(structure.tree);
|
|
const expanded = { ...this.expandedFolders };
|
|
const validFolderPaths = new Set<string>();
|
|
|
|
const ensureExpansion = (list: FileNode[]) => {
|
|
list.forEach(item => {
|
|
if (item.type === 'folder') {
|
|
validFolderPaths.add(item.path);
|
|
if (expanded[item.path] === undefined) {
|
|
expanded[item.path] = false;
|
|
}
|
|
ensureExpansion(item.children || []);
|
|
}
|
|
});
|
|
};
|
|
|
|
ensureExpansion(nodes);
|
|
Object.keys(expanded).forEach(path => {
|
|
if (!validFolderPaths.has(path)) {
|
|
delete expanded[path];
|
|
}
|
|
});
|
|
|
|
this.expandedFolders = expanded;
|
|
this.fileTree = nodes;
|
|
},
|
|
toggleFolder(path: string) {
|
|
if (!path) {
|
|
return;
|
|
}
|
|
const current = !!this.expandedFolders[path];
|
|
fileDebugLog('[FileTree] toggle folder', path, '=>', !current);
|
|
this.expandedFolders = {
|
|
...this.expandedFolders,
|
|
[path]: !current
|
|
};
|
|
},
|
|
async fetchTodoList() {
|
|
try {
|
|
const response = await fetch('/api/todo-list');
|
|
const data = await response.json();
|
|
if (data && data.success) {
|
|
this.todoList = data.data || null;
|
|
}
|
|
} catch (error) {
|
|
console.error('获取待办列表失败:', error);
|
|
}
|
|
},
|
|
setTodoList(payload: TodoList | null) {
|
|
this.todoList = payload;
|
|
},
|
|
showContextMenu(payload: { node: FileNode; event: MouseEvent }) {
|
|
if (!payload || !payload.node) {
|
|
return;
|
|
}
|
|
const { node, event } = payload;
|
|
if (!node.path && node.path !== '') {
|
|
this.hideContextMenu();
|
|
return;
|
|
}
|
|
if (node.type !== 'file' && node.type !== 'folder') {
|
|
this.hideContextMenu();
|
|
return;
|
|
}
|
|
|
|
if (event && typeof event.preventDefault === 'function') {
|
|
event.preventDefault();
|
|
}
|
|
if (event && typeof event.stopPropagation === 'function') {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
const menuWidth = 200;
|
|
const menuHeight = 50;
|
|
const viewportWidth = window.innerWidth;
|
|
const viewportHeight = window.innerHeight;
|
|
let x = (event && event.clientX) || 0;
|
|
let y = (event && event.clientY) || 0;
|
|
|
|
if (x + menuWidth > viewportWidth) {
|
|
x = viewportWidth - menuWidth - 8;
|
|
}
|
|
if (y + menuHeight > viewportHeight) {
|
|
y = viewportHeight - menuHeight - 8;
|
|
}
|
|
|
|
this.contextMenu = {
|
|
visible: true,
|
|
x: Math.max(8, x),
|
|
y: Math.max(8, y),
|
|
node
|
|
};
|
|
},
|
|
hideContextMenu() {
|
|
if (!this.contextMenu.visible) {
|
|
return;
|
|
}
|
|
this.contextMenu = {
|
|
visible: false,
|
|
x: 0,
|
|
y: 0,
|
|
node: null
|
|
};
|
|
}
|
|
}
|
|
});
|