generated from template/astro-template
temp
This commit is contained in:
parent
60ea5abe45
commit
a43cfb4b5f
@ -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(),
|
||||
|
@ -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
45
pnpm-lock.yaml
generated
@ -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
|
||||
|
37
src/apps/ai-editor/content.tsx
Normal file
37
src/apps/ai-editor/content.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
28
src/apps/ai-editor/modules/get-content-type.ts
Normal file
28
src/apps/ai-editor/modules/get-content-type.ts
Normal 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}`) };
|
||||
};
|
135
src/apps/ai-editor/sidebar.tsx
Normal file
135
src/apps/ai-editor/sidebar.tsx
Normal 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;
|
104
src/apps/ai-editor/store/menu.ts
Normal file
104
src/apps/ai-editor/store/menu.ts
Normal 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;
|
||||
}
|
||||
};
|
10
src/pages/ai-editor/index.astro
Normal file
10
src/pages/ai-editor/index.astro
Normal 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>
|
10
src/pages/ai-editor/sidebar.astro
Normal file
10
src/pages/ai-editor/sidebar.astro
Normal 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>
|
71
src/query/query-resources/index.ts
Normal file
71
src/query/query-resources/index.ts
Normal 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>;
|
||||
});
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user