This commit is contained in:
熊潇 2025-05-29 22:16:18 +08:00
parent 60ea5abe45
commit a43cfb4b5f
11 changed files with 472 additions and 1 deletions

View File

@ -19,6 +19,10 @@ let proxy = {
target: `${target}`,
secure: false,
},
'/root/resources/': {
target: `${target}`,
secure: false,
},
'/user/login/': {
target: `${target}`,
secure: false,
@ -30,6 +34,7 @@ let proxy = {
export default defineConfig({
// ...
// site: 'https://kevisual.xiongxiao.me/root/astro',
// base: isDev ? undefined : pkgs.basename,
base: isDev ? undefined : pkgs.basename,
integrations: [
mdx(),

View File

@ -53,6 +53,7 @@
"react-draggable": "^4.4.6",
"react-hook-form": "^7.56.4",
"react-i18next": "^15.5.2",
"react-resizable-panels": "^3.0.2",
"react-sortablejs": "^6.1.4",
"react-toastify": "^11.0.5",
"sortablejs": "^1.15.6",

45
pnpm-lock.yaml generated
View File

@ -116,6 +116,9 @@ importers:
react-i18next:
specifier: ^15.5.2
version: 15.5.2(i18next@25.2.0(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
react-resizable-panels:
specifier: ^3.0.2
version: 3.0.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-sortablejs:
specifier: ^6.1.4
version: 6.1.4(@types/sortablejs@1.15.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sortablejs@1.15.6)
@ -635,67 +638,79 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@ -1190,56 +1205,67 @@ packages:
resolution: {integrity: sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.40.1':
resolution: {integrity: sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.40.1':
resolution: {integrity: sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.40.1':
resolution: {integrity: sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.40.1':
resolution: {integrity: sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.40.1':
resolution: {integrity: sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.40.1':
resolution: {integrity: sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.40.1':
resolution: {integrity: sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.40.1':
resolution: {integrity: sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.40.1':
resolution: {integrity: sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.40.1':
resolution: {integrity: sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.40.1':
resolution: {integrity: sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==}
@ -1318,24 +1344,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.7':
resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.7':
resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.7':
resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.7':
resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==}
@ -2325,24 +2355,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.1:
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.1:
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.1:
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.1:
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
@ -2901,6 +2935,12 @@ packages:
'@types/react':
optional: true
react-resizable-panels@3.0.2:
resolution: {integrity: sha512-j4RNII75fnHkLnbsTb5G5YsDvJsSEZrJK2XSF2z0Tc2jIonYlIVir/Yh/5LvcUFCfs1HqrMAoiBFmIrRjC4XnA==}
peerDependencies:
react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-sortablejs@6.1.4:
resolution: {integrity: sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==}
peerDependencies:
@ -6499,6 +6539,11 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.5
react-resizable-panels@3.0.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-sortablejs@6.1.4(@types/sortablejs@1.15.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sortablejs@1.15.6):
dependencies:
'@types/sortablejs': 1.15.8

View File

@ -0,0 +1,37 @@
import { useShallow } from 'zustand/shallow';
import { getCurrentContent, useMenuStore } from './store/menu';
import { useEffect, useRef, useState } from 'react';
import { checkText } from './modules/get-content-type';
// import { TextEditor } from '@kevisual/markdown-editor/tiptap/editor.ts';
export const Content: React.FC = () => {
const { currentPath } = useMenuStore(
useShallow((state) => ({
currentPath: state.currentPath,
})),
);
const [content, setContent] = useState<string | null>(null);
useEffect(() => {
const fetchContent = async () => {
if (currentPath) {
const isText = checkText(currentPath);
if (!isText.isText) {
setContent('This file type is not supported for viewing.');
return;
}
setContent('Loading content...');
const content = await getCurrentContent(currentPath);
setContent(content);
}
};
fetchContent();
}, [currentPath]);
const ref = useRef<HTMLDivElement>(null);
return (
<div className='h-full w-full overflow-y-auto scrollbar px-2'>
<pre>{content}</pre>
</div>
);
};

View File

@ -1,4 +1,7 @@
import { ToastProvider } from '@/modules/toast/Provider';
import Sidebar from './sidebar';
import { Content } from './content';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
export const App = () => {
return (
@ -8,11 +11,33 @@ export const App = () => {
);
};
export const SidebarApp = () => {
return (
<ToastProvider>
<Sidebar />
</ToastProvider>
);
};
export const AIEditor = () => {
return (
<div className='flex h-full w-full flex-col items-center justify-center'>
<div className='text-2xl font-bold'>AI Editor</div>
<div className='mt-4 text-gray-500'>This is a placeholder for the AI Editor application.</div>
<div style={{ height: 'calc(100vh - 50px)' }} className='flex w-full'>
<div className='flex w-full'>
<PanelGroup direction='horizontal'>
<Panel minSize={20} defaultSize={30}>
<Sidebar />
</Panel>
<PanelResizeHandle />
<Panel minSize={30}>
<div className='w-full h-full overflow-hidden'>
<Content />
</div>
</Panel>
</PanelGroup>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,28 @@
export const checkText = (filepath: string) => {
const textExtensions = [
'.txt',
'.md',
'.json',
'.js',
'.ts',
'.html',
'.css',
'.xml',
'.csv',
'.tsx',
'.jsx',
'.mjs',
'.cjs',
'.vue',
'.yaml',
'.yml',
'.log',
'.conf',
'.env',
'.example', //
];
const extension = filepath.split('.').pop()?.toLowerCase();
return { isText: textExtensions.includes(`.${extension}`) };
};

View File

@ -0,0 +1,135 @@
import React, { useState, useMemo, useEffect } from 'react';
import { File, Folder, ChevronRight, ChevronDown, Loader2 } from 'lucide-react';
import { useMenuStore, init } from './store/menu.ts';
import type { MenuItem } from './store/menu.ts';
// 辅助函数:获取目录的直接子项
const getDirectChildren = (path: string, menu: MenuItem[]): MenuItem[] => {
const pathPrefix = path ? path + '/' : '';
return menu.filter((item) => {
if (!item.path.startsWith(pathPrefix)) return false;
if (item.path === path) return false;
const relativePath = item.path.slice(pathPrefix.length);
return !relativePath.includes('/');
});
};
interface DirectoryItemProps {
item: MenuItem;
level: number;
prefix?: string;
}
const DirectoryItem: React.FC<DirectoryItemProps> = ({ item, level, prefix }) => {
const [expanded, setExpanded] = useState(false);
const { menu } = useMenuStore();
const children = useMemo(() => {
const children = getDirectChildren(item.path, menu);
return children.sort((a, b) => {
if (a.type === 'directory' && b.type === 'file') return -1; // 目录排在前面
if (a.type === 'file' && b.type === 'directory') return 1; // 文件排在后面
return a.path.localeCompare(b.path); // 同类型按路径排序
});
}, [menu, item.path]);
const directory = item.path.replace(prefix || '', '').replace(/\/$/, '') || '/'; // 去掉前缀和末尾斜杠
const newPrefix = item.path + '/';
return (
<div>
<div
className='flex items-center cursor-pointer py-1 px-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded group'
style={{ paddingLeft: `${level * 12}px` }}
onClick={() => setExpanded(!expanded)}>
{children.length > 0 ? (
expanded ? (
<ChevronDown className='w-4 h-4 mr-1 text-gray-500' />
) : (
<ChevronRight className='w-4 h-4 mr-1 text-gray-500' />
)
) : (
<div className='w-4 h-4 mr-1' />
)}
<Folder className='w-4 h-4 mr-2 text-yellow-500 shrink-0' />
<span className='text-sm truncate'>{directory}</span>
</div>
{expanded && children.length > 0 && (
<div className='ml-2'>
{children.map((child) =>
child.type === 'directory' ? (
<DirectoryItem key={child.path} item={child} prefix={newPrefix} level={level + 1} />
) : (
<FileItem key={child.path} item={child} prefix={newPrefix} level={level + 1} />
),
)}
</div>
)}
</div>
);
};
interface FileItemProps {
item: MenuItem;
level: number;
prefix?: string;
}
const FileItem: React.FC<FileItemProps> = ({ item, level, prefix }) => {
const { currentPath, setCurrentPath } = useMenuStore();
return (
<div
className={`flex items-center cursor-pointer py-1 px-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded ${
currentPath === item.path ? 'bg-blue-100 dark:bg-blue-900' : ''
}`}
style={{ paddingLeft: `${level * 12}px` }}
onClick={() => setCurrentPath(item.path)}>
<div className='w-4 h-4 mr-1' /> {/* 占位,保持对齐 */}
<File className='w-4 h-4 mr-2 text-blue-500 shrink-0' />
<span className='text-sm truncate'>{item.path.replace(prefix || '', '')}</span>
</div>
);
};
export const Sidebar: React.FC = () => {
const { menu, isLoading } = useMenuStore();
useEffect(() => {
// 初始化菜单数据
init();
}, []);
// 获取顶层项(根目录下的文件和文件夹)
const rootItems = useMemo(() => {
// return menu.filter((item) => !item.path.includes('/'));
const _menu = menu.filter((item) => {
return !item.path.includes('/');
});
return _menu.sort((a, b) => {
if (a.type === 'directory' && b.type === 'file') return -1; // 目录排在前面
if (a.type === 'file' && b.type === 'directory') return 1; // 文件排在后面
return a.path.localeCompare(b.path); // 同类型按路径排序
});
}, [menu]);
return (
<div className='border-r w-full border-gray-200 dark:border-gray-700 min-h-screen p-2 bg-white dark:bg-gray-900 h-full'>
<div className='flex items-center justify-between mb-4 px-2'>
{/* <h2 className='text-lg font-semibold'>文件</h2> */}
{isLoading && <Loader2 className='w-4 h-4 animate-spin text-gray-500' />}
</div>
<div className='overflow-y-auto scrollbar' style={{ maxHeight: 'calc(100% - 58px)' }}>
{menu.length === 0 && !isLoading ? (
<div className='text-sm text-gray-500 px-2'></div>
) : (
<div className='space-y-1'>
{rootItems.map((item) =>
item.type === 'directory' ? <DirectoryItem key={item.path} item={item} level={1} /> : <FileItem key={item.path} item={item} level={1} />,
)}
</div>
)}
</div>
</div>
);
};
export default Sidebar;

View File

@ -0,0 +1,104 @@
import { create } from 'zustand';
import { query } from '@/modules/query';
import { QueryLoginBrowser } from '@/query/query-login/query-login-browser';
import { QueryResources } from '@/query/query-resources/index';
export const queryLogin = new QueryLoginBrowser({ query });
export const queryResources = new QueryResources({
prefix: '/root/resources/',
});
export type MenuItem = {
type: 'file' | 'directory';
name?: string; // type === 'file'
prefix?: string; // type === 'directory'
etag?: string;
lastModified?: string; // ISO date string
size: number;
url: string;
path: string; // a/b/file.md
pathname: string;
};
export type Menu = MenuItem[];
interface MenuState {
menu: Menu;
currentPath: string | null;
isLoading: boolean;
setMenu: (menu: Menu) => void;
setCurrentPath: (path: string | null) => void;
setLoading: (status: boolean) => void;
}
export const useMenuStore = create<MenuState>((set) => ({
menu: [],
currentPath: null,
isLoading: false,
setMenu: (menu) => set({ menu }),
setCurrentPath: (currentPath) => set({ currentPath }),
setLoading: (isLoading) => set({ isLoading }),
}));
class Status {
isInitialized = false;
username = '';
}
const status = new Status();
export const init = async (prefix: string = '') => {
const { setMenu, setCurrentPath, setLoading } = useMenuStore.getState();
let me = await queryLogin.checkLocalUser();
const isInitialized = status.isInitialized;
if (!isInitialized && me) {
status.isInitialized = true;
status.username = me.username!;
queryResources.setUsername(status.username);
}
let recursive = true;
const data = {};
if (recursive) {
data['recursive'] = recursive;
}
const res = await queryResources.getList(prefix, data);
if (res.code === 200) {
const menu = res.data!.map((item: any) => {
if (item.prefix) {
item.type = 'directory';
} else {
item.type = 'file';
}
return item;
});
console.log('init menu', menu);
if (recursive) {
const obj: Record<string, any> = {};
menu.forEach((item) => {
const parts = item.path.split('/');
const dirParts = parts.slice(0, -1);
for (let i = 0; i < dirParts.length; i++) {
const dir = dirParts.slice(0, i + 1).join('/');
if (!dir) continue; // skip root
obj[dir] = obj[dir] || { type: 'directory', path: dir };
}
});
Object.keys(obj).forEach((key) => {
const item = obj[key];
menu.push(item);
});
}
setMenu(menu);
setCurrentPath('');
}
};
export const getCurrentContent = async (currentPath: string): Promise<string | null> => {
if (!currentPath) return null;
const res = await queryResources.fetchFile(currentPath);
if (res.success) {
return res.data;
} else {
console.error('Error fetching content:', res.message);
return null;
}
};

View File

@ -0,0 +1,10 @@
---
import '@/styles/global.css';
import '@/styles/theme.css';
import Blank from '@/components/html/blank.astro';
import { App } from '@/apps/ai-editor';
---
<Blank>
<App client:only />
</Blank>

View File

@ -0,0 +1,10 @@
---
import '@/styles/global.css';
import '@/styles/theme.css';
import Blank from '@/components/html/blank.astro';
import { SidebarApp } from '@/apps/ai-editor';
---
<Blank>
<SidebarApp client:only />
</Blank>

View File

@ -0,0 +1,71 @@
import { adapter, DataOpts, Result } from '@kevisual/query';
type QueryResourcesOptions = {
prefix?: string;
storage?: Storage;
username?: string;
[key: string]: any;
};
export class QueryResources {
prefix: string; // root/resources
storage: Storage;
constructor(opts: QueryResourcesOptions) {
if (opts.username) {
this.prefix = `/${opts.username}/resources/`;
} else {
this.prefix = opts.prefix || '';
}
this.storage = opts.storage || localStorage;
}
setUsername(username: string) {
this.prefix = `/${username}/resources/`;
}
header(headers?: Record<string, string>, json = true): Record<string, string> {
const token = this.storage.getItem('token');
const _headers: Record<string, string> = {
'Content-Type': 'application/json',
...headers,
};
if (!json) {
delete _headers['Content-Type'];
}
if (!token) {
return _headers;
}
return {
..._headers,
Authorization: `Bearer ${token}`,
};
}
async get(data: any, opts: DataOpts): Promise<any> {
return adapter({
url: opts.url!,
method: 'GET',
body: data,
...opts,
headers: this.header(opts?.headers),
});
}
async getList(prefix: string, data?: { recursive?: boolean }, opts?: DataOpts): Promise<Result<any[]>> {
return this.get(data, {
url: `${this.prefix}${prefix}`,
body: data,
...opts,
});
}
async fetchFile(filepath: string, opts?: DataOpts): Promise<Result<any>> {
return fetch(`${this.prefix}${filepath}`, {
method: 'GET',
headers: this.header(opts?.headers, false),
}).then(async (res) => {
if (!res.ok) {
return {
code: 500,
success: false,
message: `Failed to fetch file: ${res.status} ${res.statusText}`,
} as Result<any>;
}
return { code: 200, data: await res.text(), success: true } as Result<any>;
});
}
}