From fd9987546189ea4fc64ef7ff12cd94cfe6149a39 Mon Sep 17 00:00:00 2001 From: xion Date: Mon, 7 Oct 2024 01:53:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9A=82=E5=AD=98=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 + pnpm-lock.yaml | 36 ++++++ src/App.tsx | 5 + src/pages/app/edit/AppVersionList.tsx | 153 ++++++++++++++++++++++++++ src/pages/app/edit/List.tsx | 129 ++++++++++++++++++++++ src/pages/app/index.tsx | 15 +++ src/pages/app/layouts/index.tsx | 5 + src/pages/app/modules/FileUpload.tsx | 56 ++++++++++ src/pages/app/store/app-version.ts | 78 +++++++++++++ src/pages/app/store/index.ts | 2 + src/pages/app/store/user-app.ts | 69 ++++++++++++ src/pages/file/edit/List.tsx | 122 ++++++++++++++++++++ src/pages/file/index.tsx | 13 +++ src/pages/file/layouts/index.tsx | 5 + src/pages/file/store/index.ts | 97 ++++++++++++++++ 15 files changed, 788 insertions(+) create mode 100644 src/pages/app/edit/AppVersionList.tsx create mode 100644 src/pages/app/edit/List.tsx create mode 100644 src/pages/app/index.tsx create mode 100644 src/pages/app/layouts/index.tsx create mode 100644 src/pages/app/modules/FileUpload.tsx create mode 100644 src/pages/app/store/app-version.ts create mode 100644 src/pages/app/store/index.ts create mode 100644 src/pages/app/store/user-app.ts create mode 100644 src/pages/file/edit/List.tsx create mode 100644 src/pages/file/index.tsx create mode 100644 src/pages/file/layouts/index.tsx create mode 100644 src/pages/file/store/index.ts diff --git a/package.json b/package.json index 4a2f879..0b685b4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@types/d3": "^7.4.3", "@types/lodash-es": "^4.17.12", "@types/node": "^22.7.4", + "@types/path-browserify": "^1.0.3", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -57,7 +58,9 @@ "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.12", "globals": "^15.9.0", + "path-browserify": "^1.0.1", "postcss-import": "^16.1.0", + "pretty-bytes": "^6.1.1", "react-is": "^18.3.1", "tailwind-merge": "^2.5.2", "tailwindcss": "^3.4.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c3e6a9..cf3cd35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,12 @@ importers: '@types/node': specifier: ^22.7.4 version: 22.7.4 + '@types/path-browserify': + specifier: ^1.0.3 + version: 1.0.3 + '@types/pretty-bytes': + specifier: ^5.2.0 + version: 5.2.0 '@types/react': specifier: ^18.3.10 version: 18.3.10 @@ -138,9 +144,15 @@ importers: globals: specifier: ^15.9.0 version: 15.9.0 + path-browserify: + specifier: ^1.0.1 + version: 1.0.1 postcss-import: specifier: ^16.1.0 version: 16.1.0(postcss@8.4.47) + pretty-bytes: + specifier: ^6.1.1 + version: 6.1.1 react-is: specifier: ^18.3.1 version: 18.3.1 @@ -907,6 +919,13 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/path-browserify@1.0.3': + resolution: {integrity: sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==} + + '@types/pretty-bytes@5.2.0': + resolution: {integrity: sha512-dJhMFphDp6CE+OAZVyqzha9KsmgeqRMbZN4dIbMSrfObiuzfjucwKdn6zu+ttrjMwmz+Vz71/xXgHx5pO0axhA==} + deprecated: This is a stub types definition. pretty-bytes provides its own type definitions, so you do not need this installed. + '@types/prismjs@1.26.4': resolution: {integrity: sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg==} @@ -1867,6 +1886,9 @@ packages: parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1956,6 +1978,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} @@ -3395,6 +3421,12 @@ snapshots: '@types/parse-json@4.0.2': {} + '@types/path-browserify@1.0.3': {} + + '@types/pretty-bytes@5.2.0': + dependencies: + pretty-bytes: 6.1.1 + '@types/prismjs@1.26.4': {} '@types/prop-types@15.7.13': {} @@ -4490,6 +4522,8 @@ snapshots: dependencies: entities: 4.5.0 + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -4562,6 +4596,8 @@ snapshots: prelude-ls@1.2.1: {} + pretty-bytes@6.1.1: {} + property-information@6.5.0: {} punycode@2.3.1: {} diff --git a/src/App.tsx b/src/App.tsx index 23f563a..8da84c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,8 @@ import { App as AiAgentApp } from './pages/ai-agent'; import { App as UserApp } from './pages/user'; import { App as ChatApp } from './pages/chat-manager'; import { App as GitHubApp } from './pages/github'; +import { App as UserAppApp } from './pages/app'; +import { App as FileApp } from './pages/file'; import '@abearxiong/container/dist/container.css'; @@ -33,6 +35,9 @@ export const App = () => { } /> } /> } /> + } /> + } /> + 404} /> 404} /> diff --git a/src/pages/app/edit/AppVersionList.tsx b/src/pages/app/edit/AppVersionList.tsx new file mode 100644 index 0000000..ad99e19 --- /dev/null +++ b/src/pages/app/edit/AppVersionList.tsx @@ -0,0 +1,153 @@ +import { useParams } from 'react-router'; +import { useAppVersionStore } from '../store'; +import { useShallow } from 'zustand/react/shallow'; +import { useEffect } from 'react'; +import { Button, Form, Input, Modal } from 'antd'; +import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'; +import { isObjectNull } from '@/utils/is-null'; +import { useNavigate } from 'react-router'; +import { FileUpload } from '../modules/FileUpload'; +const FormModal = () => { + const [form] = Form.useForm(); + const containerStore = useAppVersionStore( + useShallow((state) => { + return { + showEdit: state.showEdit, + setShowEdit: state.setShowEdit, + formData: state.formData, + updateData: state.updateData, + }; + }), + ); + useEffect(() => { + const open = containerStore.showEdit; + if (open) { + if (open) { + const isNull = isObjectNull(containerStore.formData); + if (isNull) { + form.setFieldsValue({}); + } else form.setFieldsValue(containerStore.formData); + } + } + }, [containerStore.showEdit]); + const onFinish = async (values: any) => { + containerStore.updateData(values); + }; + const onClose = () => { + containerStore.setShowEdit(false); + form.resetFields(); + }; + const isEdit = containerStore.formData.id; + return ( + containerStore.setShowEdit(false)} + destroyOnClose + footer={false} + width={800} + onCancel={onClose}> +
+ + + + + + + + + + + +
+
+ ); +}; + +export const AppVersionList = () => { + const params = useParams(); + const appKey = params.appKey; + const versionStore = useAppVersionStore( + useShallow((state) => { + return { + list: state.list, + getList: state.getList, + setKey: state.setKey, + setShowEdit: state.setShowEdit, + setFormData: state.setFormData, + deleteData: state.deleteData, + }; + }), + ); + useEffect(() => { + // fetch app version list + if (appKey) { + versionStore.setKey(appKey); + versionStore.getList(); + } + }, []); + return ( +
+
+
+
+ +
+
+
+
+
+ {versionStore.list.map((item, index) => { + return ( +
+
{item.version}
+ +
+ +
+
+ ); + })} +
+
+
+
+ +
+ ); +}; diff --git a/src/pages/app/edit/List.tsx b/src/pages/app/edit/List.tsx new file mode 100644 index 0000000..aa41c79 --- /dev/null +++ b/src/pages/app/edit/List.tsx @@ -0,0 +1,129 @@ +import { useShallow } from 'zustand/react/shallow'; +import { useUserAppStore } from '../store'; +import { useEffect } from 'react'; +import { Button, Form, Input, Modal } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { isObjectNull } from '@/utils/is-null'; +import { useNavigate } from 'react-router'; +import { FileUpload } from '../modules/FileUpload'; +const FormModal = () => { + const [form] = Form.useForm(); + const containerStore = useUserAppStore( + useShallow((state) => { + return { + showEdit: state.showEdit, + setShowEdit: state.setShowEdit, + formData: state.formData, + updateData: state.updateData, + }; + }), + ); + useEffect(() => { + const open = containerStore.showEdit; + if (open) { + if (open) { + const isNull = isObjectNull(containerStore.formData); + if (isNull) { + form.setFieldsValue({}); + } else form.setFieldsValue(containerStore.formData); + } + } + }, [containerStore.showEdit]); + const onFinish = async (values: any) => { + containerStore.updateData(values); + }; + const onClose = () => { + containerStore.setShowEdit(false); + form.resetFields(); + }; + const isEdit = containerStore.formData.id; + return ( + containerStore.setShowEdit(false)} + destroyOnClose + footer={false} + width={800} + onCancel={onClose}> +
+ + + + + + + + + + + + + + +
+
+ ); +}; + +export const List = () => { + const userAppStore = useUserAppStore( + useShallow((state) => { + return { + list: state.list, + getList: state.getList, + setShowEdit: state.setShowEdit, + }; + }), + ); + const navicate = useNavigate(); + useEffect(() => { + userAppStore.getList(); + }, []); + return ( +
+
+ +
+
+
+
+
+ {userAppStore.list.map((item) => { + return ( +
{ + navicate(`/app/${item.key}/verison/list`); + }}> +
{item.title}
+
+ ); + })} +
+
+
+
+ +
+ ); +}; diff --git a/src/pages/app/index.tsx b/src/pages/app/index.tsx new file mode 100644 index 0000000..a2e7a2e --- /dev/null +++ b/src/pages/app/index.tsx @@ -0,0 +1,15 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; +import { Main } from './layouts'; +import { List } from './edit/List'; +import { AppVersionList } from './edit/AppVersionList'; +export const App = () => { + return ( + + }> + }> + } /> + } /> + + + ); +}; diff --git a/src/pages/app/layouts/index.tsx b/src/pages/app/layouts/index.tsx new file mode 100644 index 0000000..acd878c --- /dev/null +++ b/src/pages/app/layouts/index.tsx @@ -0,0 +1,5 @@ +import { LayoutMain } from '@/modules/layout'; + +export const Main = () => { + return ; +}; diff --git a/src/pages/app/modules/FileUpload.tsx b/src/pages/app/modules/FileUpload.tsx new file mode 100644 index 0000000..d16543a --- /dev/null +++ b/src/pages/app/modules/FileUpload.tsx @@ -0,0 +1,56 @@ +export type FileType = { + name: string; + size: number; + lastModified: number; + webkitRelativePath: string; // 包含name +}; + +export const FileUpload = () => { + const onChange = async (e) => { + console.log(e.target.files); + // webkitRelativePath + let files = Array.from(e.target.files) as any[]; + console.log(files); + // 过滤 文件 .DS_Store + files = files.filter((file) => { + if (file.webkitRelativePath.startsWith('__MACOSX')) { + return false; + } + return !file.name.startsWith('.'); + }); + if (files.length === 0) { + console.log('no files'); + return; + } + const root = files[0].webkitRelativePath.split('/')[0]; + const formData = new FormData(); + files.forEach((file) => { + // relativePath 去除第一级 + const webkitRelativePath = file.webkitRelativePath.replace(root + '/', ''); + formData.append('file', file, webkitRelativePath); // 保留文件夹路径 + }); + formData.append('appKey','codeflow'); + formData.append('version', '0.0.2'); + const res = await fetch('/api/app/upload', { + method: 'POST', + body: formData,// + headers: { + Authorization: 'Bearer ' + localStorage.getItem('token'), + }, + }); + console.log(res); + }; + + return ( +
+ 文件上传: + +
+ ); +}; diff --git a/src/pages/app/store/app-version.ts b/src/pages/app/store/app-version.ts new file mode 100644 index 0000000..62252b3 --- /dev/null +++ b/src/pages/app/store/app-version.ts @@ -0,0 +1,78 @@ +import { create } from 'zustand'; +import { query } from '@/modules'; +import { message } from 'antd'; + +type AppVersionStore = { + showEdit: boolean; + setShowEdit: (showEdit: boolean) => void; + formData: any; + setFormData: (formData: any) => void; + loading: boolean; + setLoading: (loading: boolean) => void; + key: string; + setKey: (key: string) => void; + list: any[]; + getList: () => Promise; + updateData: (data: any) => Promise; + deleteData: (id: string) => Promise; +}; +export const useAppVersionStore = create((set, get) => { + return { + showEdit: false, + setShowEdit: (showEdit) => set({ showEdit }), + formData: {}, + setFormData: (formData) => set({ formData }), + loading: false, + setLoading: (loading) => set({ loading }), + key: '', + setKey: (key) => set({ key }), + list: [], + getList: async () => { + set({ loading: true }); + const key = get().key; + + const res = await query.post({ + path: 'app', + key: 'list', + data: { + key, + }, + }); + set({ loading: false }); + if (res.code === 200) { + set({ list: res.data }); + } else { + message.error(res.message || 'Request failed'); + } + }, + updateData: async (data) => { + const { getList } = get(); + const res = await query.post({ + path: 'app', + key: 'update', + data, + }); + if (res.code === 200) { + message.success('Success'); + set({ showEdit: false, formData: res.data }); + getList(); + } else { + message.error(res.message || 'Request failed'); + } + }, + deleteData: async (id) => { + const { getList } = get(); + const res = await query.post({ + path: 'app', + key: 'delete', + id, + }); + if (res.code === 200) { + getList(); + message.success('Success'); + } else { + message.error(res.message || 'Request failed'); + } + }, + }; +}); diff --git a/src/pages/app/store/index.ts b/src/pages/app/store/index.ts new file mode 100644 index 0000000..866e600 --- /dev/null +++ b/src/pages/app/store/index.ts @@ -0,0 +1,2 @@ +export * from './user-app'; +export * from './app-version'; diff --git a/src/pages/app/store/user-app.ts b/src/pages/app/store/user-app.ts new file mode 100644 index 0000000..20b8ab3 --- /dev/null +++ b/src/pages/app/store/user-app.ts @@ -0,0 +1,69 @@ +import { create } from 'zustand'; +import { query } from '@/modules'; +import { message } from 'antd'; +type UserAppStore = { + showEdit: boolean; + setShowEdit: (showEdit: boolean) => void; + formData: any; + setFormData: (formData: any) => void; + loading: boolean; + setLoading: (loading: boolean) => void; + list: any[]; + getList: () => Promise; + updateData: (data: any) => Promise; + deleteData: (id: string) => Promise; +}; +export const useUserAppStore = create((set, get) => { + return { + showEdit: false, + setShowEdit: (showEdit) => set({ showEdit }), + formData: {}, + setFormData: (formData) => set({ formData }), + loading: false, + setLoading: (loading) => set({ loading }), + list: [], + getList: async () => { + set({ loading: true }); + + const res = await query.post({ + path: 'user-app', + key: 'list', + }); + set({ loading: false }); + if (res.code === 200) { + set({ list: res.data }); + } else { + message.error(res.message || 'Request failed'); + } + }, + updateData: async (data) => { + const { getList } = get(); + const res = await query.post({ + path: 'user-app', + key: 'update', + data, + }); + if (res.code === 200) { + message.success('Success'); + set({ showEdit: false, formData: res.data }); + getList(); + } else { + message.error(res.message || 'Request failed'); + } + }, + deleteData: async (id) => { + const { getList } = get(); + const res = await query.post({ + path: 'user-app', + key: 'delete', + id, + }); + if (res.code === 200) { + getList(); + message.success('Success'); + } else { + message.error(res.message || 'Request failed'); + } + }, + }; +}); diff --git a/src/pages/file/edit/List.tsx b/src/pages/file/edit/List.tsx new file mode 100644 index 0000000..d4d6f65 --- /dev/null +++ b/src/pages/file/edit/List.tsx @@ -0,0 +1,122 @@ +import { useShallow } from 'zustand/react/shallow'; +import { useFileStore } from '../store'; +import { useEffect } from 'react'; +import path from 'path-browserify'; +import prettyBytes from 'pretty-bytes'; +import clsx from 'clsx'; +import { isObjectNull } from '@/utils/is-null'; +export const CardPath = ({ children }: any) => { + const userAppStore = useFileStore( + useShallow((state) => { + return { + list: state.list, + getList: state.getList, + setPath: state.setPath, + path: state.path, + }; + }), + ); + const paths = ['root', ...userAppStore.path.split('/').filter((item) => item)]; + const onDirectoryClick = (prefix: string) => { + if (prefix === 'root') { + userAppStore.setPath(''); + userAppStore.getList(); + return; + } + userAppStore.setPath(prefix.replace('root/', '') + '/'); + userAppStore.getList(); + }; + return ( +
+
+
+
+
Path:
+
+ {paths.map((item, index) => { + const isLast = index === paths.length - 1; + return ( +
{ + if (!isLast) { + onDirectoryClick(paths.slice(0, index + 1).join('/')); + } + }}> + {item}/ +
+ ); + })} +
+
+
+
+
{children}
+
+ ); +}; +export const List = () => { + const userAppStore = useFileStore( + useShallow((state) => { + return { + list: state.list, + getList: state.getList, + setPath: state.setPath, + path: state.path, + getFile: state.getFile, + file: state.file, + }; + }), + ); + useEffect(() => { + userAppStore.getList(); + }, []); + const onDirectoryClick = (prefix: string) => { + userAppStore.setPath(prefix); + userAppStore.getList(); + }; + return ( +
+
+ +
+ {userAppStore.list.map((item, index) => { + if (item.prefix) { + let showPrefix = item.prefix.replace(userAppStore.path, ''); + showPrefix = showPrefix.replace('/', ''); + return ( +
{ + onDirectoryClick(item.prefix); + }}> +
Directory:
+
{showPrefix}
+
+ ); + } + const name = path.basename(item.name); + const size = prettyBytes(item.size); + return ( +
{ + userAppStore.getFile(item.name); + }}> +
{name}
+
size: {size}
+
+ ); + })} +
+
+
+
{!isObjectNull(userAppStore.file) && JSON.stringify(userAppStore.file, null, 2)}
+
+
+
+ ); +}; diff --git a/src/pages/file/index.tsx b/src/pages/file/index.tsx new file mode 100644 index 0000000..9dad8a7 --- /dev/null +++ b/src/pages/file/index.tsx @@ -0,0 +1,13 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; +import { Main } from './layouts'; +import { List } from './edit/List'; +export const App = () => { + return ( + + }> + }> + } /> + + + ); +}; diff --git a/src/pages/file/layouts/index.tsx b/src/pages/file/layouts/index.tsx new file mode 100644 index 0000000..724cff7 --- /dev/null +++ b/src/pages/file/layouts/index.tsx @@ -0,0 +1,5 @@ +import { LayoutMain } from '@/modules/layout'; + +export const Main = () => { + return ; +}; diff --git a/src/pages/file/store/index.ts b/src/pages/file/store/index.ts new file mode 100644 index 0000000..ff6c747 --- /dev/null +++ b/src/pages/file/store/index.ts @@ -0,0 +1,97 @@ +import { create } from 'zustand'; +import { query } from '@/modules'; +import { message } from 'antd'; +import { sortBy } from 'lodash-es'; +type FileStore = { + showEdit: boolean; + setShowEdit: (showEdit: boolean) => void; + formData: any; + setFormData: (formData: any) => void; + loading: boolean; + setLoading: (loading: boolean) => void; + path: string; + setPath: (path: string) => void; + list: any[]; + getList: () => Promise; + updateData: (data: any) => Promise; + deleteData: (id: string) => Promise; + getFile: (path: string) => Promise; + file: any; + setFile: (file: any) => void; +}; +export const useFileStore = create((set, get) => { + return { + showEdit: false, + setShowEdit: (showEdit) => set({ showEdit }), + formData: {}, + setFormData: (formData) => set({ formData }), + loading: false, + setLoading: (loading) => set({ loading }), + path: '', + setPath: (path) => set({ path }), + list: [], + getList: async () => { + const { path } = get(); + set({ loading: true }); + + const res = await query.post({ + path: 'file', + key: 'list', + data: { + prefix: path, + }, + }); + set({ loading: false }); + if (res.code === 200) { + const list = res.data; + const sortedList = sortBy(list, [(item) => !item.prefix]); + set({ list: sortedList }); + } else { + message.error(res.message || 'Request failed'); + } + }, + updateData: async (data) => { + const { getList } = get(); + const res = await query.post({ + path: 'file', + key: 'update', + data, + }); + if (res.code === 200) { + message.success('Success'); + set({ showEdit: false, formData: res.data }); + getList(); + } else { + message.error(res.message || 'Request failed'); + } + }, + deleteData: async (id) => { + const { getList } = get(); + const res = await query.post({ + path: 'file', + key: 'delete', + id, + }); + if (res.code === 200) { + getList(); + message.success('Success'); + } else { + message.error(res.message || 'Request failed'); + } + }, + getFile: async (path) => { + const res = await query.post({ + path: 'file', + key: 'stat', + data: { prefix: path }, + }); + if (res.code === 200) { + set({ file: res.data }); + } else { + message.error(res.message || 'Request failed'); + } + }, + file: {}, + setFile: (file) => set({ file }), + }; +});