generated from template/vite-react-template
	Compare commits
	
		
			1 Commits
		
	
	
		
			9129dffa4c
			...
			508ec96029
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 508ec96029 | 
							
								
								
									
										39
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								index.html
									
									
									
									
									
								
							| @@ -1,13 +1,30 @@ | ||||
| <!doctype html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Vite + React + TS</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="root"></div> | ||||
|     <script type="module" src="/src/main.tsx"></script> | ||||
|   </body> | ||||
| </html> | ||||
|  | ||||
| <head> | ||||
|   <meta charset="UTF-8" /> | ||||
|   <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|   <title>Mark</title> | ||||
|   <style> | ||||
|     html, | ||||
|     body { | ||||
|       margin: 0; | ||||
|       padding: 0; | ||||
|       width: 100%; | ||||
|       height: 100%; | ||||
|     } | ||||
|  | ||||
|     #root { | ||||
|       width: 100%; | ||||
|       height: 100%; | ||||
|     } | ||||
|   </style> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|   <div id="root"></div> | ||||
|   <script type="module" src="/src/main.tsx"></script> | ||||
| </body> | ||||
|  | ||||
| </html> | ||||
							
								
								
									
										18
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,5 +1,5 @@ | ||||
| { | ||||
|   "name": "vite-react", | ||||
|   "name": "@kevisual/mark", | ||||
|   "private": true, | ||||
|   "version": "0.0.1", | ||||
|   "type": "module", | ||||
| @@ -9,14 +9,17 @@ | ||||
|     "build": "vite build", | ||||
|     "lint": "eslint .", | ||||
|     "preview": "vite preview", | ||||
|     "pub": "envision deploy ./dist -k vite-react -v 0.0.1", | ||||
|     "ev": "npm run build && npm run deploy", | ||||
|     "pub": "envision deploy ./dist -k mark -v 0.0.1", | ||||
|     "ev": "npm run build && npm run pub", | ||||
|     "dev:lib": "turbo dev" | ||||
|   }, | ||||
|   "author": "abearxiong <xiongxiao@xiongxiao.me>", | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "@kevisual/query-mark": "workspace:*", | ||||
|     "@kevisual/router": "0.0.9", | ||||
|     "@kevisual/store": "workspace:*", | ||||
|     "@types/lodash-es": "^4.17.12", | ||||
|     "clsx": "^2.1.1", | ||||
|     "dayjs": "^1.11.13", | ||||
|     "lodash-es": "^4.17.21", | ||||
| @@ -24,20 +27,21 @@ | ||||
|     "nanoid": "^5.1.5", | ||||
|     "react": "^19.0.0", | ||||
|     "react-dom": "^19.0.0", | ||||
|     "react-hook-form": "^7.54.2", | ||||
|     "react-toastify": "^11.0.5", | ||||
|     "zustand": "^5.0.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@kevisual/query": "0.0.15", | ||||
|     "@kevisual/types": "^0.0.6", | ||||
|     "@tailwindcss/vite": "^4.0.16", | ||||
|     "@types/node": "^22.13.13", | ||||
|     "@tailwindcss/vite": "^4.0.17", | ||||
|     "@types/node": "^22.13.14", | ||||
|     "@types/react": "^19.0.12", | ||||
|     "@types/react-dom": "^19.0.4", | ||||
|     "@vitejs/plugin-react": "^4.3.4", | ||||
|     "tailwindcss": "^4.0.16", | ||||
|     "tailwindcss": "^4.0.17", | ||||
|     "typescript": "^5.8.2", | ||||
|     "vite": "^6.2.3" | ||||
|   }, | ||||
|   "packageManager": "pnpm@10.6.5" | ||||
|   "packageManager": "pnpm@10.7.0" | ||||
| } | ||||
							
								
								
									
										13
									
								
								public/locales/en/translation.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								public/locales/en/translation.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|   "markType": "Type", | ||||
|   "summary": "Summary", | ||||
|   "tags": "Tags", | ||||
|   "description": "Description", | ||||
|   "link": "Link", | ||||
|   "createdAt": "Created At", | ||||
|   "updatedAt": "Updated At", | ||||
|   "title": "Title", | ||||
|   "thumbnail": "Thumbnail", | ||||
|   "save": "Save", | ||||
|   "editMarkSuccess": "Edit Mark Success" | ||||
| } | ||||
							
								
								
									
										13
									
								
								public/locales/zh/translation.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								public/locales/zh/translation.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|   "markType": "类型", | ||||
|   "summary": "摘要", | ||||
|   "tags": "标签", | ||||
|   "description": "描述", | ||||
|   "link": "链接", | ||||
|   "createdAt": "创建时间", | ||||
|   "updatedAt": "更新时间", | ||||
|   "title": "标题", | ||||
|   "thumbnail": "缩略图", | ||||
|   "save": "保存", | ||||
|   "editMarkSuccess": "编辑成功" | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/MarkProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/MarkProvider.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import { initI18n, I18NextProvider } from '@kevisual/components/translate/index.tsx'; | ||||
| import { ToastContainer } from 'react-toastify'; | ||||
| import { basename } from './modules/basename'; | ||||
| type Props = { | ||||
|   children: React.ReactNode; | ||||
|   basename?: string; | ||||
| }; | ||||
| // initI18n(''); | ||||
| export const MarkProvider = (props: Props) => { | ||||
|   return ( | ||||
|     <I18NextProvider basename={props.basename || basename} noUse={false}> | ||||
|       <ToastContainer /> | ||||
|       {props.children} | ||||
|     </I18NextProvider> | ||||
|   ); | ||||
| }; | ||||
| @@ -1 +1,2 @@ | ||||
| @import "tailwindcss"; | ||||
| @import 'tailwindcss'; | ||||
| @import '@kevisual/components/theme/wind-theme.css'; | ||||
|   | ||||
| @@ -3,4 +3,9 @@ import { App } from './pages/App.tsx'; | ||||
|  | ||||
| import './index.css'; | ||||
|  | ||||
| createRoot(document.getElementById('root')!).render(<App />); | ||||
| import { MarkProvider } from './MarkProvider.tsx'; | ||||
| createRoot(document.getElementById('root')!).render( | ||||
|   <MarkProvider> | ||||
|     <App /> | ||||
|   </MarkProvider>, | ||||
| ); | ||||
|   | ||||
							
								
								
									
										317
									
								
								src/manager/Manager.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								src/manager/Manager.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,317 @@ | ||||
| import { useManagerStore } from './store'; | ||||
| import { useEffect, useMemo, useState } from 'react'; | ||||
| import { useShallow } from 'zustand/shallow'; | ||||
| import { ManagerProvider } from './Provider'; | ||||
| import { ChevronDown, ChevronLeft, Edit, Plus, Search, Trash, Menu as MenuIcon, MenuSquare } from 'lucide-react'; | ||||
| import dayjs from 'dayjs'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { EditMark as EditMarkComponent } from './edit/Edit'; | ||||
| import { toast } from 'react-toastify'; | ||||
| import clsx from 'clsx'; | ||||
| import { Controller, useForm } from 'react-hook-form'; | ||||
| import { Button, TextField, InputAdornment, IconButton, Menu, MenuItem } from '@mui/material'; | ||||
| import { MarkType } from '@kevisual/query-mark'; | ||||
| type ManagerProps = { | ||||
|   showSearch?: boolean; | ||||
|   showAdd?: boolean; | ||||
|   onClick?: (data?: any) => void; | ||||
|   markType?: MarkType; | ||||
| }; | ||||
| export const Manager = (props: ManagerProps) => { | ||||
|   const { showSearch = true, showAdd = false, onClick } = props; | ||||
|  | ||||
|   const { control } = useForm({ defaultValues: { search: '' } }); | ||||
|   const { list, init, setCurrentMarkId, currentMarkId, deleteMark, getMark, setMarkData, pagination, setPagination, getList, search, setSearch } = | ||||
|     useManagerStore( | ||||
|       useShallow((state) => { | ||||
|         return { | ||||
|           list: state.list, | ||||
|           init: state.init, | ||||
|           currentMarkId: state.currentMarkId, | ||||
|           setCurrentMarkId: state.setCurrentMarkId, | ||||
|           deleteMark: state.deleteMark, | ||||
|           getMark: state.getMark, | ||||
|           setMarkData: state.setMarkData, | ||||
|           pagination: state.pagination, | ||||
|           setPagination: state.setPagination, | ||||
|           search: state.search, | ||||
|           setSearch: state.setSearch, | ||||
|           getList: state.getList, | ||||
|         }; | ||||
|       }), | ||||
|     ); | ||||
|   const { t } = useTranslation(); | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const open = Boolean(anchorEl); | ||||
|  | ||||
|   const handleClick = (event: React.MouseEvent<HTMLElement>) => { | ||||
|     setAnchorEl(event.currentTarget); | ||||
|   }; | ||||
|  | ||||
|   const handleClose = () => { | ||||
|     setAnchorEl(null); | ||||
|   }; | ||||
|   const handleMenuItemClick = (option: string) => { | ||||
|     handleClose(); | ||||
|     console.log('option', option); | ||||
|     init(option as any); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const url = new URL(window.location.href); | ||||
|     let markType = url.searchParams.get('markType') || ''; | ||||
|     if (!markType && props.markType) { | ||||
|       markType = props.markType; | ||||
|     } | ||||
|     init((markType as any) || 'md'); | ||||
|   }, []); | ||||
|   useEffect(() => { | ||||
|     if (search) { | ||||
|       getList(); | ||||
|     } else if (pagination.current > 1) { | ||||
|       getList(); | ||||
|     } | ||||
|   }, [pagination.current, search]); | ||||
|   const onEditMark = async (markId: string) => { | ||||
|     setCurrentMarkId(markId); | ||||
|     const res = await getMark(markId); | ||||
|     console.log('mark', res); | ||||
|     if (res.code === 200) { | ||||
|       setMarkData(res.data!); | ||||
|     } | ||||
|   }; | ||||
|   const onDeleteMark = async (markId: string) => { | ||||
|     const res = await deleteMark(markId); | ||||
|     if (res.code === 200) { | ||||
|       toast.success(t('deleteMarkSuccess')); | ||||
|     } | ||||
|   }; | ||||
|   console.log('list', list.length, pagination.total); | ||||
|   return ( | ||||
|     <div className='w-full h-full p-4  relative'> | ||||
|       <div className='flex  px-4 mb-4 justify-between items-center absolute top-0 left-0 h-[56px] w-full'> | ||||
|         <div className='flex ml-12 items-center space-x-2 '> | ||||
|           <Controller | ||||
|             name='search' | ||||
|             control={control} | ||||
|             render={({ field }) => ( | ||||
|               <TextField | ||||
|                 {...field} | ||||
|                 variant='outlined' | ||||
|                 margin='normal' | ||||
|                 sx={{ | ||||
|                   display: showSearch ? 'block' : 'none', | ||||
|                 }} | ||||
|                 size='small' | ||||
|                 slotProps={{ | ||||
|                   input: { | ||||
|                     endAdornment: ( | ||||
|                       <InputAdornment position='end'> | ||||
|                         <Search className='w-4 h-4' onClick={() => setSearch(field.value)} /> | ||||
|                       </InputAdornment> | ||||
|                     ), | ||||
|                     onKeyDown: (event) => { | ||||
|                       if (event.key === 'Enter') { | ||||
|                         setSearch(field.value); | ||||
|                       } | ||||
|                     }, | ||||
|                   }, | ||||
|                 }} | ||||
|               /> | ||||
|             )} | ||||
|           /> | ||||
|         </div> | ||||
|         <div className={'flex items-center space-x-2'}> | ||||
|           <IconButton onClick={handleClick}> | ||||
|             <MenuIcon className='w-4 h-4' /> | ||||
|           </IconButton> | ||||
|           <Menu | ||||
|             anchorEl={anchorEl} | ||||
|             open={open} | ||||
|             onClose={handleClose} | ||||
|             onClick={(e) => { | ||||
|               console.log('e', e); | ||||
|             }}> | ||||
|             {['md', 'mdx', 'wallnote', 'excalidraw'].map((option) => ( | ||||
|               <MenuItem | ||||
|                 key={option} | ||||
|                 value={option} | ||||
|                 onClick={() => { | ||||
|                   handleMenuItemClick(option); | ||||
|                 }}> | ||||
|                 {option} | ||||
|               </MenuItem> | ||||
|             ))} | ||||
|           </Menu> | ||||
|           <button | ||||
|             className={clsx( | ||||
|               'text-blue-500 cursor-pointer hover:underline flex items-center p-2 rounded-md hover:bg-blue-100 transition duration-200', | ||||
|               showAdd ? '' : 'hidden', | ||||
|             )}> | ||||
|             <Plus | ||||
|               className={clsx('w-4 h-4 ', currentMarkId ? 'rotate-12' : 'rotate-0')} | ||||
|               onClick={() => { | ||||
|                 setCurrentMarkId(''); | ||||
|  | ||||
|                 setMarkData({ | ||||
|                   id: '', | ||||
|                   title: '', | ||||
|                   description: '', | ||||
|                   markType: 'md' as any, | ||||
|                   summary: '', | ||||
|                   tags: [], | ||||
|                   link: '', | ||||
|                 }); | ||||
|               }} | ||||
|             /> | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className='mt-[56px] overflow-auto scrollbar' style={{ height: 'calc(100% - 56px)' }}> | ||||
|         {list.map((item, index) => { | ||||
|           const isCurrent = item.id === currentMarkId; | ||||
|           return ( | ||||
|             <div | ||||
|               key={item.id} | ||||
|               className={`border rounded-lg p-4 mb-4 shadow-md bg-white border-gray-200 ${isCurrent ? 'border-blue-500' : ''}`} | ||||
|               onClick={() => { | ||||
|                 onClick?.(item); | ||||
|               }}> | ||||
|               <div className='flex justify-between items-center'> | ||||
|                 <div className={`text-lg font-bold truncate cursor-pointer ${isCurrent ? 'text-blue-500' : ''}`}>{item.title}</div> | ||||
|                 <div className='flex space-x-2'> | ||||
|                   <button | ||||
|                     className='text-blue-500 cursor-pointer hover:underline flex items-center p-2 rounded-md hover:bg-blue-100 transition duration-200' | ||||
|                     onClick={(e) => { | ||||
|                       e.stopPropagation(); | ||||
|                       onEditMark(item.id); | ||||
|                     }}> | ||||
|                     <Edit className='w-4 h-4 ' /> | ||||
|                   </button> | ||||
|                   <button | ||||
|                     className='text-red-500 cursor-pointer hover:underline flex items-center p-2 rounded-md hover:bg-red-100 transition duration-200' | ||||
|                     onClick={(e) => { | ||||
|                       e.stopPropagation(); | ||||
|                       onDeleteMark(item.id); | ||||
|                     }}> | ||||
|                     <Trash className='w-4 h-4 ' /> | ||||
|                   </button> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div className='text-sm text-gray-600'> | ||||
|                 {t('markType')}: {item.markType} | ||||
|               </div> | ||||
|               <div className='text-sm text-gray-600'> | ||||
|                 {t('summary')}: {item.summary} | ||||
|               </div> | ||||
|               <div className='text-sm text-gray-600'> | ||||
|                 {t('tags')}: {item.tags?.join?.(', ')} | ||||
|               </div> | ||||
|               <div className='text-sm text-gray-600 hidden sm:block'> | ||||
|                 {t('description')}: {item.description} | ||||
|               </div> | ||||
|               <div | ||||
|                 className='text-sm text-gray-600 hidden sm:block truncate' | ||||
|                 onClick={() => { | ||||
|                   window.open(item.link, '_blank'); | ||||
|                 }}> | ||||
|                 {t('link')}: {item.link} | ||||
|               </div> | ||||
|               <div className='text-sm text-gray-600 hidden sm:block'> | ||||
|                 {t('createdAt')}: {dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')} | ||||
|               </div> | ||||
|               <div className='text-sm text-gray-600 hidden sm:block'> | ||||
|                 {t('updatedAt')}: {dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss')} | ||||
|               </div> | ||||
|             </div> | ||||
|           ); | ||||
|         })} | ||||
|         <div className='flex justify-center items-center'> | ||||
|           {list.length < pagination.total && ( | ||||
|             <button | ||||
|               className='text-blue-500 cursor-pointer hover:underline flex items-center p-2 rounded-md hover:bg-blue-100 transition duration-200' | ||||
|               onClick={() => { | ||||
|                 setPagination({ ...pagination, current: pagination.current + 1 }); | ||||
|               }}> | ||||
|               <ChevronDown className='w-4 h-4 ' /> | ||||
|             </button> | ||||
|           )} | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const EditMark = () => { | ||||
|   const { markData } = useManagerStore( | ||||
|     useShallow((state) => { | ||||
|       return { | ||||
|         markData: state.markData, | ||||
|       }; | ||||
|     }), | ||||
|   ); | ||||
|   const mark = markData; | ||||
|   if (!mark) { | ||||
|     return null; | ||||
|   } | ||||
|   if (mark) { | ||||
|     return <EditMarkComponent />; | ||||
|   } | ||||
|   return <div className='w-full h-full'></div>; | ||||
| }; | ||||
| export const LayoutMain = (props: { children?: React.ReactNode }) => { | ||||
|   const [openMenu, setOpenMenu] = useState(false); | ||||
|   return ( | ||||
|     <div className='w-full h-full flex'> | ||||
|       <div className='absolute top-4 left-4 z-10'> | ||||
|         <Button | ||||
|           variant='contained' | ||||
|           color={openMenu ? 'info' : 'primary'} | ||||
|           sx={{ | ||||
|             minWidth: '0px', | ||||
|             padding: '8px', | ||||
|           }} | ||||
|           onClick={() => { | ||||
|             setOpenMenu(!openMenu); | ||||
|           }}> | ||||
|           <MenuSquare className='w-4 h-4' /> | ||||
|         </Button> | ||||
|       </div> | ||||
|       <div className={clsx('h-full w-full sm:w-1/3', openMenu ? '' : 'hidden')}>{props.children}</div> | ||||
|       <div className={clsx('h-full hidden sm:block sm:w-2/3', openMenu ? '' : 'hidden')}> | ||||
|         <EditMark /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| export type AppProps = { | ||||
|   /** | ||||
|    * 标记类型, wallnote md excalidraw | ||||
|    */ | ||||
|   markType?: MarkType; | ||||
|   /** | ||||
|    * 是否显示搜索框 | ||||
|    */ | ||||
|   showSearch?: boolean; | ||||
|   /** | ||||
|    * 是否显示添加按钮 | ||||
|    */ | ||||
|   showAdd?: boolean; | ||||
|   /** | ||||
|    * 点击事件 | ||||
|    */ | ||||
|   onClick?: (data?: any) => void; | ||||
|   /** | ||||
|    * 管理器id, 存储到store的id | ||||
|    */ | ||||
|   managerId?: string; | ||||
| }; | ||||
| export const App = (props: AppProps) => { | ||||
|   return ( | ||||
|     <ManagerProvider id={props.managerId}> | ||||
|       <LayoutMain> | ||||
|         <Manager markType={props.markType} showSearch={props.showSearch} showAdd={props.showAdd} onClick={props.onClick} /> | ||||
|       </LayoutMain> | ||||
|     </ManagerProvider> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										9
									
								
								src/manager/Provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/manager/Provider.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import { StoreContextProvider } from '@kevisual/store/react'; | ||||
| import { createManagerStore } from './store/index'; | ||||
| export const ManagerProvider = ({ children, id }: { children: React.ReactNode; id?: string }) => { | ||||
|   return ( | ||||
|     <StoreContextProvider id={id || 'mark-manager'} stateCreator={createManagerStore}> | ||||
|       {children} | ||||
|     </StoreContextProvider> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										99
									
								
								src/manager/edit/Edit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/manager/edit/Edit.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| import { useForm, Controller } from 'react-hook-form'; | ||||
| import { TextField, Button, Box, MenuItem, Autocomplete, FormControlLabel } from '@mui/material'; | ||||
| import { useManagerStore } from '../store'; | ||||
| import { useShallow } from 'zustand/shallow'; | ||||
| import { useEffect, useState } from 'react'; | ||||
| import { Mark } from '@kevisual/query-mark'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { pick } from 'lodash-es'; | ||||
| import { toast } from 'react-toastify'; | ||||
| import { TagsInput } from '@kevisual/components/select/TagsInput.tsx'; | ||||
| export const EditMark = () => { | ||||
|   const { control, handleSubmit, reset } = useForm(); | ||||
|   const { updateMark, markData, setCurrentMarkId, setMarkData } = useManagerStore( | ||||
|     useShallow((state) => { | ||||
|       return { | ||||
|         updateMark: state.updateMark, | ||||
|         markData: state.markData, | ||||
|         setCurrentMarkId: state.setCurrentMarkId, | ||||
|         currentMarkId: state.currentMarkId, | ||||
|         setMarkData: state.setMarkData, | ||||
|       }; | ||||
|     }), | ||||
|   ); | ||||
|   // const [mark, setMark] = useState<Mark | undefined>(markData); | ||||
|   const mark = pick(markData, ['id', 'title', 'description', 'markType', 'summary', 'tags', 'link', 'thumbnail']); | ||||
|   useEffect(() => { | ||||
|     reset(mark); | ||||
|     console.log('markData', markData); | ||||
|   }, [markData?.id]); | ||||
|   const onSubmit = async (data: any) => { | ||||
|     const res = await updateMark({ ...mark, ...data }); | ||||
|     if (res.code === 200) { | ||||
|       toast.success(t('editMarkSuccess')); | ||||
|     } | ||||
|  | ||||
|     // setCurrentMarkId(''); | ||||
|     // setMarkData(undefined); | ||||
|   }; | ||||
|   const { t } = useTranslation(); | ||||
|   return ( | ||||
|     <Box component='form' sx={{ px: 2, py: 1 }} onSubmit={handleSubmit(onSubmit)} noValidate autoComplete='off' className='w-full h-full overflow-auto'> | ||||
|       <Controller | ||||
|         name='title' | ||||
|         control={control} | ||||
|         defaultValue={mark?.title || ''} | ||||
|         render={({ field }) => <TextField {...field} label={t('title')} variant='outlined' fullWidth margin='normal' />} | ||||
|       /> | ||||
|       <Controller | ||||
|         name='description' | ||||
|         control={control} | ||||
|         defaultValue={mark?.description || ''} | ||||
|         render={({ field }) => <TextField {...field} label={t('description')} variant='outlined' fullWidth margin='normal' multiline />} | ||||
|       /> | ||||
|       <Controller | ||||
|         name='markType' | ||||
|         control={control} | ||||
|         defaultValue={mark?.markType || ''} | ||||
|         render={({ field }) => ( | ||||
|           <Autocomplete | ||||
|             {...field} | ||||
|             options={['md', 'mdx', 'wallnote', 'excalidraw']} | ||||
|             freeSolo | ||||
|             renderInput={(params) => <TextField {...params} label={t('markType')} variant='outlined' fullWidth margin='normal' />} | ||||
|             onChange={(_, value) => field.onChange(value)} | ||||
|           /> | ||||
|         )} | ||||
|       /> | ||||
|       <Controller | ||||
|         name='summary' | ||||
|         control={control} | ||||
|         defaultValue={mark?.summary || ''} | ||||
|         render={({ field }) => <TextField {...field} label={t('summary')} variant='outlined' fullWidth margin='normal' multiline />} | ||||
|       /> | ||||
|       <Controller | ||||
|         name='tags' | ||||
|         control={control} | ||||
|         defaultValue={mark?.tags || ''} | ||||
|         render={({ field }) => { | ||||
|           return <TagsInput {...field} label={t('tags')} showLabel={true} />; | ||||
|         }} | ||||
|       /> | ||||
|       <Controller | ||||
|         name='link' | ||||
|         control={control} | ||||
|         defaultValue={mark?.link || ''} | ||||
|         render={({ field }) => <TextField {...field} label={t('link')} variant='outlined' fullWidth margin='normal' />} | ||||
|       /> | ||||
|       <Controller | ||||
|         name='thumbnail' | ||||
|         control={control} | ||||
|         defaultValue={mark?.thumbnail || ''} | ||||
|         render={({ field }) => <TextField {...field} label={t('thumbnail')} variant='outlined' fullWidth margin='normal' />} | ||||
|       /> | ||||
|       <Button type='submit' variant='contained' color='primary'> | ||||
|         {t('save')} | ||||
|       </Button> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										133
									
								
								src/manager/store/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/manager/store/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| import { StateCreator, StoreManager } from '@kevisual/store'; | ||||
| import { useContextKey } from '@kevisual/store/context'; | ||||
| // import { StateCreator, StoreApi, UseBoundStore } from 'zustand'; | ||||
| import { query as queryClient } from '../../modules/query'; | ||||
| import { Result } from '@kevisual/query/query'; | ||||
| import { QueryMark, Mark, MarkType } from '@kevisual/query-mark'; | ||||
| import { useStore, BoundStore } from '@kevisual/store/react'; | ||||
| import { uniqBy } from 'lodash-es'; | ||||
| export const store = useContextKey('store', () => { | ||||
|   return new StoreManager(); | ||||
| }); | ||||
|  | ||||
| type ManagerStore = { | ||||
|   /** 当前选中的Mark */ | ||||
|   currentMarkId: string; | ||||
|   setCurrentMarkId: (markId: string) => void; | ||||
|   markData: Mark | undefined; | ||||
|   setMarkData: (mark?: Partial<Mark>) => void; | ||||
|   /** 获取Mark列表 */ | ||||
|   getList: () => Promise<any>; | ||||
|   getMarkFromList: (markId: string) => Mark | undefined; | ||||
|   updateMark: (mark: Mark) => Promise<any>; | ||||
|   getMark: (markId: string) => Promise<Result<Mark>>; | ||||
|   deleteMark: (markId: string) => Promise<any>; | ||||
|   /** Mark列表 */ | ||||
|   list: Mark[]; | ||||
|   setList: (list: Mark[]) => void; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     pageSize: number; | ||||
|     total: number; | ||||
|   }; | ||||
|   setPagination: (pagination: { current: number; pageSize: number; total: number }) => void; | ||||
|   /** 搜索 */ | ||||
|   search: string; | ||||
|   setSearch: (search: string) => void; | ||||
|   /** 初始化 */ | ||||
|   init: (markType: MarkType) => Promise<void>; | ||||
|   queryMark: QueryMark; | ||||
|   markType: MarkType; | ||||
| }; | ||||
| export const createManagerStore: StateCreator<ManagerStore, [], [], any> = (set, get, store) => { | ||||
|   return { | ||||
|     currentMarkId: '', | ||||
|     setCurrentMarkId: (markId: string) => set(() => ({ currentMarkId: markId })), | ||||
|     getList: async () => { | ||||
|       const queryMark = get().queryMark; | ||||
|       const { search, pagination } = get(); | ||||
|       const res = await queryMark.getMarkList({ page: pagination.current, pageSize: pagination.pageSize, search }); | ||||
|       const oldList = get().list; | ||||
|       if (res.code === 200) { | ||||
|         const { pagination, list } = res.data || {}; | ||||
|         const newList = [...oldList, ...list]; | ||||
|         const uniqueList = uniqBy(newList, 'id'); | ||||
|         set(() => ({ list: uniqueList })); | ||||
|         set(() => ({ pagination: { current: pagination.current, pageSize: pagination.pageSize, total: pagination.total } })); | ||||
|       } | ||||
|     }, | ||||
|     getMarkFromList: (markId: string) => { | ||||
|       return get().list.find((item) => item.id === markId); | ||||
|     }, | ||||
|     updateMark: async (mark: Mark) => { | ||||
|       const queryMark = get().queryMark; | ||||
|       const res = await queryMark.updateMark(mark.id, mark); | ||||
|       if (res.code === 200) { | ||||
|         set((state) => { | ||||
|           const oldList = state.list; | ||||
|           const resMark = res.data!; | ||||
|           const newList = oldList.map((item) => (item.id === mark.id ? mark : item)); | ||||
|           if (!mark.id) { | ||||
|             newList.unshift(resMark); | ||||
|           } | ||||
|           return { | ||||
|             list: newList, | ||||
|           }; | ||||
|         }); | ||||
|       } | ||||
|       return res; | ||||
|     }, | ||||
|     getMark: async (markId: string) => { | ||||
|       const queryMark = get().queryMark; | ||||
|       const res = await queryMark.getMark(markId); | ||||
|       return res; | ||||
|     }, | ||||
|     list: [], | ||||
|     setList: (list: any[]) => set(() => ({ list })), | ||||
|     init: async (markType: MarkType = 'wallnote') => { | ||||
|       // await get().getList(); | ||||
|       console.log('init', set, get); | ||||
|       const queryMark = new QueryMark({ | ||||
|         query: queryClient as any, | ||||
|         markType, | ||||
|       }); | ||||
|       const url = new URL(window.location.href); | ||||
|       const pageSize = url.searchParams.get('pageSize') || '10'; | ||||
|       set({ queryMark, markType, list: [], pagination: { current: 1, pageSize: parseInt(pageSize), total: 0 }, currentMarkId: '', markData: undefined }); | ||||
|       setTimeout(async () => { | ||||
|         console.log('get', get); | ||||
|         get().getList(); | ||||
|       }, 1000); | ||||
|     }, | ||||
|     deleteMark: async (markId: string) => { | ||||
|       const queryMark = get().queryMark; | ||||
|       const res = await queryMark.deleteMark(markId); | ||||
|       const currentMarkId = get().currentMarkId; | ||||
|       if (res.code === 200) { | ||||
|         // get().getList(); | ||||
|         set((state) => ({ | ||||
|           list: state.list.filter((item) => item.id !== markId), | ||||
|         })); | ||||
|         if (currentMarkId === markId) { | ||||
|           set(() => ({ currentMarkId: '', markData: undefined })); | ||||
|         } | ||||
|       } | ||||
|       return res; | ||||
|     }, | ||||
|     queryMark: undefined, | ||||
|     markType: 'simple', | ||||
|     markData: undefined, | ||||
|     setMarkData: (mark: Mark) => set(() => ({ markData: mark })), | ||||
|     pagination: { | ||||
|       current: 1, | ||||
|       pageSize: 10, | ||||
|       total: 0, | ||||
|     }, | ||||
|     setPagination: (pagination: { current: number; pageSize: number; total: number }) => set(() => ({ pagination })), | ||||
|     /** 搜索 */ | ||||
|     search: '', | ||||
|     setSearch: (search: string) => set(() => ({ search, list: [], pagination: { current: 1, pageSize: 10, total: 0 } })), | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export const useManagerStore = useStore as BoundStore<ManagerStore>; | ||||
| @@ -1,5 +1,10 @@ | ||||
| import { basename } from '../modules/basename'; | ||||
| console.log('basename', basename); | ||||
| import { App as MarkApp } from '../manager/Manager'; | ||||
| export const App = () => { | ||||
|   return <div className='bg-slate-200 w-full h-full border'></div>; | ||||
|   return ( | ||||
|     <div className=' w-full h-full overflow-hidden'> | ||||
|       <MarkApp /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -10,6 +10,17 @@ const isDev = process.env.NODE_ENV === 'development'; | ||||
|  | ||||
| const basename = isDev ? '/' : pkgs?.basename || '/'; | ||||
|  | ||||
| let proxy: any = {}; | ||||
| if (isDev) { | ||||
|   proxy = { | ||||
|     '/api': { | ||||
|       target: 'https://kevisual.xiongxiao.me', | ||||
|       changeOrigin: true, | ||||
|       ws: true, | ||||
|       rewriteWsOrigin: true, | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| // https://vitejs.dev/config/ | ||||
| export default defineConfig({ | ||||
|   plugins: [react(), tailwindcss()], | ||||
| @@ -38,13 +49,7 @@ export default defineConfig({ | ||||
|         rewriteWsOrigin: true, | ||||
|         rewrite: (path) => path.replace(/^\/api/, '/api'), | ||||
|       }, | ||||
|       '/api/router': { | ||||
|         target: 'ws://localhost:3000', | ||||
|         changeOrigin: true, | ||||
|         ws: true, | ||||
|         rewriteWsOrigin: true, | ||||
|         rewrite: (path) => path.replace(/^\/api/, '/api'), | ||||
|       }, | ||||
|       ...proxy, | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user