feat: add layout and org
This commit is contained in:
		| @@ -1,12 +1,13 @@ | ||||
| import { useParams } from 'react-router'; | ||||
| import { useNavigation, 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 { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| import { Button, Form, Input, Modal, Tooltip } from 'antd'; | ||||
| import { CloudUploadOutlined, DeleteOutlined, EditOutlined, FileOutlined, LeftOutlined, PlusOutlined } from '@ant-design/icons'; | ||||
| import { isObjectNull } from '@/utils/is-null'; | ||||
| import { useNavigate } from 'react-router'; | ||||
| import { FileUpload } from '../modules/FileUpload'; | ||||
| import clsx from 'clsx'; | ||||
| const FormModal = () => { | ||||
|   const [form] = Form.useForm(); | ||||
|   const containerStore = useAppVersionStore( | ||||
| @@ -86,13 +87,19 @@ export const AppVersionList = () => { | ||||
|       return { | ||||
|         list: state.list, | ||||
|         getList: state.getList, | ||||
|         key: state.key, | ||||
|         setKey: state.setKey, | ||||
|         setShowEdit: state.setShowEdit, | ||||
|         formData: state.formData, | ||||
|         setFormData: state.setFormData, | ||||
|         deleteData: state.deleteData, | ||||
|         publishVersion: state.publishVersion, | ||||
|         app: state.app, | ||||
|       }; | ||||
|     }), | ||||
|   ); | ||||
|   const navigate = useNavigate(); | ||||
|   const [isUpload, setIsUpload] = useState(false); | ||||
|   useEffect(() => { | ||||
|     // fetch app version list | ||||
|     if (appKey) { | ||||
| @@ -100,6 +107,9 @@ export const AppVersionList = () => { | ||||
|       versionStore.getList(); | ||||
|     } | ||||
|   }, []); | ||||
|   const appVersion = useMemo(() => { | ||||
|     return versionStore.app?.version || ''; | ||||
|   }, [versionStore.app]); | ||||
|   return ( | ||||
|     <div className='w-full h-full flex bg-slate-100'> | ||||
|       <div className='p-2 bg-white'> | ||||
| @@ -111,33 +121,68 @@ export const AppVersionList = () => { | ||||
|           icon={<PlusOutlined />} | ||||
|         /> | ||||
|       </div> | ||||
|       <div> | ||||
|         <FileUpload /> | ||||
|       </div> | ||||
|       <div className='flex-grow h-full'> | ||||
|         <div className='w-full h-full p-4'> | ||||
|  | ||||
|       <div className='flex-grow h-full relative'> | ||||
|         <div className='absolute top-2 left-4'> | ||||
|           <Tooltip title='返回' placement='bottom'> | ||||
|             <Button | ||||
|               onClick={() => { | ||||
|                 navigate('/app/edit/list'); | ||||
|               }} | ||||
|               icon={<LeftOutlined />} | ||||
|             /> | ||||
|           </Tooltip> | ||||
|         </div> | ||||
|  | ||||
|         <div className='w-full h-full p-4 pt-12'> | ||||
|           <div className='w-full h-full rounded-lg bg-white'> | ||||
|             <div className='flex gap-2 flex-wrap p-4'> | ||||
|               {versionStore.list.map((item, index) => { | ||||
|                 const isPublish = item.version === appVersion; | ||||
|                 const color = isPublish ? 'bg-green-500' : ''; | ||||
|                 return ( | ||||
|                   <div className='card border-t' key={index}> | ||||
|                     <div>{item.version}</div> | ||||
|                   <div className='card border-t w-[300px]' key={index}> | ||||
|                     <div className={'flex items-center justify-between'}> | ||||
|                       {item.version} | ||||
|  | ||||
|                     <div> | ||||
|                       <Tooltip title={isPublish ? 'published' : ''}> | ||||
|                         <div className={clsx('ml-4 rounded-full w-4 h-4', color)}></div> | ||||
|                       </Tooltip> | ||||
|                     </div> | ||||
|                     <div className='mt-4'> | ||||
|                       <Button.Group> | ||||
|                         <Button | ||||
|                         {/* <Button | ||||
|                           onClick={() => { | ||||
|                             versionStore.setFormData(item); | ||||
|                             versionStore.setShowEdit(true); | ||||
|                           }} | ||||
|                           icon={<EditOutlined />} | ||||
|                         /> | ||||
|                         <Button | ||||
|                           onClick={() => { | ||||
|                             versionStore.deleteData(item.id); | ||||
|                           }} | ||||
|                           icon={<DeleteOutlined />} | ||||
|                         /> | ||||
|                         /> */} | ||||
|                         <Tooltip title='Delete'> | ||||
|                           <Button | ||||
|                             onClick={() => { | ||||
|                               versionStore.deleteData(item.id); | ||||
|                             }} | ||||
|                             icon={<DeleteOutlined />} | ||||
|                           /> | ||||
|                         </Tooltip> | ||||
|                         <Tooltip title='使用当前版本,发布为此版本'> | ||||
|                           <Button | ||||
|                             icon={<CloudUploadOutlined />} | ||||
|                             onClick={() => { | ||||
|                               versionStore.publishVersion({ id: item.id }); | ||||
|                             }} | ||||
|                           /> | ||||
|                         </Tooltip> | ||||
|                         <Tooltip title='文件管理'> | ||||
|                           <Button | ||||
|                             icon={<FileOutlined />} | ||||
|                             onClick={() => { | ||||
|                               versionStore.setFormData(item); | ||||
|                               setIsUpload(true); | ||||
|                             }} | ||||
|                           /> | ||||
|                         </Tooltip> | ||||
|                       </Button.Group> | ||||
|                     </div> | ||||
|                   </div> | ||||
| @@ -147,7 +192,63 @@ export const AppVersionList = () => { | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className='flex-shrink'> | ||||
|         {isUpload && ( | ||||
|           <div className='bg-white p-2 w-[600px]'> | ||||
|             <div className='header flex items-center gap-2'> | ||||
|               <Tooltip title='返回'> | ||||
|                 <Button | ||||
|                   onClick={() => { | ||||
|                     setIsUpload(false); | ||||
|                   }} | ||||
|                   icon={<LeftOutlined />} | ||||
|                 /> | ||||
|               </Tooltip> | ||||
|               <div className='font-bold'>{versionStore.key}</div> | ||||
|             </div> | ||||
|             <AppVersionFile /> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|       <FormModal /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| export const AppVersionFile = () => { | ||||
|   const versionStore = useAppVersionStore( | ||||
|     useShallow((state) => { | ||||
|       return { | ||||
|         formData: state.formData, | ||||
|       }; | ||||
|     }), | ||||
|   ); | ||||
|   const versionFiles = useMemo(() => { | ||||
|     if (!versionStore.formData?.data) return []; | ||||
|     const files = versionStore.formData.data.files || []; | ||||
|     return files; | ||||
|   }, [versionStore.formData]); | ||||
|   return ( | ||||
|     <> | ||||
|       <div>version: {versionStore.formData.version}</div> | ||||
|       <div className='border rounded-md my-2'> | ||||
|         <div className='flex gap-2 items-center border-b py-2 px-2'> | ||||
|           Files | ||||
|           <FileUpload /> | ||||
|         </div> | ||||
|         <div className='mt-2'> | ||||
|           {versionFiles.map((file, index) => { | ||||
|             const prefix = versionStore.formData.key + '/' + versionStore.formData.version + '/'; | ||||
|             const _path = file.path || ''; | ||||
|             const path = _path.replace(prefix, ''); | ||||
|             return ( | ||||
|               <div className='flex gap-2 px-4 py-2 border-b' key={index}> | ||||
|                 {/* <div className='w-[100px] truncate'>{file.name}</div> */} | ||||
|                 <div>{path}</div> | ||||
|               </div> | ||||
|             ); | ||||
|           })} | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| 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 { Button, Form, Input, Modal, Select, Tooltip } from 'antd'; | ||||
| import { DeleteOutlined, EditOutlined, PlusOutlined, UnorderedListOutlined } from '@ant-design/icons'; | ||||
| import { isObjectNull } from '@/utils/is-null'; | ||||
| import { useNavigate } from 'react-router'; | ||||
| import { FileUpload } from '../modules/FileUpload'; | ||||
| @@ -61,12 +61,23 @@ const FormModal = () => { | ||||
|         <Form.Item name='title' label='title'> | ||||
|           <Input /> | ||||
|         </Form.Item> | ||||
|         <Form.Item name='domain' label='domain'> | ||||
|           <Input /> | ||||
|         </Form.Item> | ||||
|         <Form.Item name='key' label='key'> | ||||
|           <Input /> | ||||
|         </Form.Item> | ||||
|         <Form.Item name='description' label='description'> | ||||
|           <Input.TextArea rows={4} /> | ||||
|         </Form.Item> | ||||
|         <Form.Item name='status' label='status'> | ||||
|           <Select | ||||
|             options={[ | ||||
|               { label: 'Running', value: 'running' }, | ||||
|               { label: 'Stop', value: 'stop' }, | ||||
|             ]} | ||||
|           /> | ||||
|         </Form.Item> | ||||
|         <Form.Item label=' ' colon={false}> | ||||
|           <Button type='primary' htmlType='submit'> | ||||
|             Submit | ||||
| @@ -87,6 +98,9 @@ export const List = () => { | ||||
|         list: state.list, | ||||
|         getList: state.getList, | ||||
|         setShowEdit: state.setShowEdit, | ||||
|         formData: state.formData, | ||||
|         setFormData: state.setFormData, | ||||
|         deleteData: state.deleteData, | ||||
|       }; | ||||
|     }), | ||||
|   ); | ||||
| @@ -109,13 +123,32 @@ export const List = () => { | ||||
|             <div className='flex flex-wrap gap-2'> | ||||
|               {userAppStore.list.map((item) => { | ||||
|                 return ( | ||||
|                   <div | ||||
|                     className='card border-t w-[300px] ' | ||||
|                     key={item.id} | ||||
|                     onClick={() => { | ||||
|                       navicate(`/app/${item.key}/verison/list`); | ||||
|                     }}> | ||||
|                     <div className='card-title'>{item.title}</div> | ||||
|                   <div className='card border-t w-[300px] ' key={item.id}> | ||||
|                     <div className='card-title' onClick={() => {}}> | ||||
|                       {item.title} | ||||
|                     </div> | ||||
|                     <div className='mt-2'> | ||||
|                       <Button.Group> | ||||
|                         <Tooltip title={'Edit'}> | ||||
|                           <Button | ||||
|                             icon={<EditOutlined />} | ||||
|                             onClick={() => { | ||||
|                               userAppStore.setFormData(item); | ||||
|                               userAppStore.setShowEdit(true); | ||||
|                             }}></Button> | ||||
|                         </Tooltip> | ||||
|                         <Tooltip title={'App Version List'}> | ||||
|                           <Button | ||||
|                             icon={<UnorderedListOutlined />} | ||||
|                             onClick={() => { | ||||
|                               navicate(`/app/${item.key}/verison/list`); | ||||
|                             }}></Button> | ||||
|                         </Tooltip> | ||||
|                         <Tooltip title={'Delete'}> | ||||
|                           <Button icon={<DeleteOutlined />} onClick={() => userAppStore.deleteData(item.id)}></Button> | ||||
|                         </Tooltip> | ||||
|                       </Button.Group> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 ); | ||||
|               })} | ||||
|   | ||||
| @@ -1,3 +1,8 @@ | ||||
| import { Button, message } from 'antd'; | ||||
| import { useCallback, useRef } from 'react'; | ||||
| import { useAppVersionStore } from '../store'; | ||||
| import { useShallow } from 'zustand/react/shallow'; | ||||
|  | ||||
| export type FileType = { | ||||
|   name: string; | ||||
|   size: number; | ||||
| @@ -6,51 +11,86 @@ export type FileType = { | ||||
| }; | ||||
|  | ||||
| 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; | ||||
|   const ref = useRef<HTMLInputElement | null>(null); | ||||
|   const appVersionStore = useAppVersionStore( | ||||
|     useShallow((state) => { | ||||
|       return { | ||||
|         formData: state.formData, | ||||
|         setFormData: state.setFormData, | ||||
|         updateByFromData: state.updateByFromData, | ||||
|       }; | ||||
|     }), | ||||
|   ); | ||||
|   const onChange = useCallback( | ||||
|     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; | ||||
|       } | ||||
|       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); | ||||
|   }; | ||||
|       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); // 保留文件夹路径 | ||||
|       }); | ||||
|       const key = appVersionStore.formData.key; | ||||
|       const version = appVersionStore.formData.version; | ||||
|       formData.append('appKey', key); | ||||
|       formData.append('version', version); | ||||
|       const res = await fetch('/api/app/upload', { | ||||
|         method: 'POST', | ||||
|         body: formData, // | ||||
|         headers: { | ||||
|           Authorization: 'Bearer ' + localStorage.getItem('token'), | ||||
|         }, | ||||
|       }).then((res) => res.json()); | ||||
|       if (res?.code === 200) { | ||||
|         appVersionStore.setFormData(res.data); | ||||
|         appVersionStore.updateByFromData(); | ||||
|       } else { | ||||
|         message.error(res.message || 'Request failed'); | ||||
|       } | ||||
|       // 清理之前上传的文件 | ||||
|       e.target.value = ''; | ||||
|     }, | ||||
|     [appVersionStore.formData], | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <div> | ||||
|       文件上传: | ||||
|       <input | ||||
|         className='hidden' | ||||
|         ref={ref} | ||||
|         type='file' | ||||
|         // @ts-ignore | ||||
|         webkitdirectory='true' | ||||
|         multiple | ||||
|         onChange={onChange} | ||||
|       /> | ||||
|       <Button | ||||
|         onClick={() => { | ||||
|           const key = appVersionStore.formData.key; | ||||
|           const version = appVersionStore.formData.version; | ||||
|           if (!key || !version) { | ||||
|             message.error('请先选择应用和版本'); | ||||
|             return; | ||||
|           } | ||||
|           ref.current!.click(); | ||||
|         }}> | ||||
|         上传文件夹 | ||||
|       </Button> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -1,20 +1,25 @@ | ||||
| import { create } from 'zustand'; | ||||
| import { query } from '@/modules'; | ||||
| import { message } from 'antd'; | ||||
| import { isObjectNull } from '@/utils/is-null'; | ||||
|  | ||||
| type AppVersionStore = { | ||||
|   showEdit: boolean; | ||||
|   setShowEdit: (showEdit: boolean) => void; | ||||
|   formData: any; | ||||
|   setFormData: (formData: any) => void; | ||||
|   updateByFromData: () => void; | ||||
|   loading: boolean; | ||||
|   setLoading: (loading: boolean) => void; | ||||
|   key: string; | ||||
|   setKey: (key: string) => void; | ||||
|   list: any[]; | ||||
|   getList: () => Promise<void>; | ||||
|   app: any; | ||||
|   getApp: (key: string, force?: boolean) => Promise<void>; | ||||
|   updateData: (data: any) => Promise<void>; | ||||
|   deleteData: (id: string) => Promise<void>; | ||||
|   publishVersion: (data: any) => Promise<void>; | ||||
| }; | ||||
| export const useAppVersionStore = create<AppVersionStore>((set, get) => { | ||||
|   return { | ||||
| @@ -22,6 +27,16 @@ export const useAppVersionStore = create<AppVersionStore>((set, get) => { | ||||
|     setShowEdit: (showEdit) => set({ showEdit }), | ||||
|     formData: {}, | ||||
|     setFormData: (formData) => set({ formData }), | ||||
|     updateByFromData: () => { | ||||
|       const { formData, list } = get(); | ||||
|       const data = list.map((item) => { | ||||
|         if (item.id === formData.id) { | ||||
|           return formData; | ||||
|         } | ||||
|         return item; | ||||
|       }); | ||||
|       set({ list: data }); | ||||
|     }, | ||||
|     loading: false, | ||||
|     setLoading: (loading) => set({ loading }), | ||||
|     key: '', | ||||
| @@ -38,6 +53,7 @@ export const useAppVersionStore = create<AppVersionStore>((set, get) => { | ||||
|           key, | ||||
|         }, | ||||
|       }); | ||||
|       get().getApp(key); | ||||
|       set({ loading: false }); | ||||
|       if (res.code === 200) { | ||||
|         set({ list: res.data }); | ||||
| @@ -45,6 +61,25 @@ export const useAppVersionStore = create<AppVersionStore>((set, get) => { | ||||
|         message.error(res.message || 'Request failed'); | ||||
|       } | ||||
|     }, | ||||
|     app: {}, | ||||
|     getApp: async (key, force) => { | ||||
|       const { app } = get(); | ||||
|       if (!force && !isObjectNull(app)) { | ||||
|         return; | ||||
|       } | ||||
|       const res = await query.post({ | ||||
|         path: 'user-app', | ||||
|         key: 'get', | ||||
|         data: { | ||||
|           key, | ||||
|         }, | ||||
|       }); | ||||
|       if (res.code === 200) { | ||||
|         set({ app: res.data }); | ||||
|       } else { | ||||
|         message.error(res.message || 'Request failed'); | ||||
|       } | ||||
|     }, | ||||
|     updateData: async (data) => { | ||||
|       const { getList } = get(); | ||||
|       const res = await query.post({ | ||||
| @@ -74,5 +109,22 @@ export const useAppVersionStore = create<AppVersionStore>((set, get) => { | ||||
|         message.error(res.message || 'Request failed'); | ||||
|       } | ||||
|     }, | ||||
|     publishVersion: async (data) => { | ||||
|       const { getList } = get(); | ||||
|       const loaded = message.loading('Publishing...', 0); | ||||
|       const res = await query.post({ | ||||
|         path: 'app', | ||||
|         key: 'publish', | ||||
|         data, | ||||
|       }); | ||||
|       loaded(); | ||||
|       if (res.code === 200) { | ||||
|         message.success('Success'); | ||||
|         // getList(); | ||||
|         get().getApp(get().key, true); | ||||
|       } else { | ||||
|         message.error(res.message || 'Request failed'); | ||||
|       } | ||||
|     }, | ||||
|   }; | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user