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> | <!doctype html> | ||||||
| <html lang="en"> | <html lang="en"> | ||||||
|   <head> |  | ||||||
|     <meta charset="UTF-8" /> | <head> | ||||||
|     <link rel="icon" type="image/svg+xml" href="/vite.svg" /> |   <meta charset="UTF-8" /> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |   <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||||||
|     <title>Vite + React + TS</title> |   <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|   </head> |   <title>Mark</title> | ||||||
|   <body> |   <style> | ||||||
|     <div id="root"></div> |     html, | ||||||
|     <script type="module" src="/src/main.tsx"></script> |     body { | ||||||
|   </body> |       margin: 0; | ||||||
| </html> |       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, |   "private": true, | ||||||
|   "version": "0.0.1", |   "version": "0.0.1", | ||||||
|   "type": "module", |   "type": "module", | ||||||
| @@ -9,14 +9,17 @@ | |||||||
|     "build": "vite build", |     "build": "vite build", | ||||||
|     "lint": "eslint .", |     "lint": "eslint .", | ||||||
|     "preview": "vite preview", |     "preview": "vite preview", | ||||||
|     "pub": "envision deploy ./dist -k vite-react -v 0.0.1", |     "pub": "envision deploy ./dist -k mark -v 0.0.1", | ||||||
|     "ev": "npm run build && npm run deploy", |     "ev": "npm run build && npm run pub", | ||||||
|     "dev:lib": "turbo dev" |     "dev:lib": "turbo dev" | ||||||
|   }, |   }, | ||||||
|   "author": "abearxiong <xiongxiao@xiongxiao.me>", |   "author": "abearxiong <xiongxiao@xiongxiao.me>", | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "@kevisual/query-mark": "workspace:*", | ||||||
|     "@kevisual/router": "0.0.9", |     "@kevisual/router": "0.0.9", | ||||||
|  |     "@kevisual/store": "workspace:*", | ||||||
|  |     "@types/lodash-es": "^4.17.12", | ||||||
|     "clsx": "^2.1.1", |     "clsx": "^2.1.1", | ||||||
|     "dayjs": "^1.11.13", |     "dayjs": "^1.11.13", | ||||||
|     "lodash-es": "^4.17.21", |     "lodash-es": "^4.17.21", | ||||||
| @@ -24,20 +27,21 @@ | |||||||
|     "nanoid": "^5.1.5", |     "nanoid": "^5.1.5", | ||||||
|     "react": "^19.0.0", |     "react": "^19.0.0", | ||||||
|     "react-dom": "^19.0.0", |     "react-dom": "^19.0.0", | ||||||
|  |     "react-hook-form": "^7.54.2", | ||||||
|     "react-toastify": "^11.0.5", |     "react-toastify": "^11.0.5", | ||||||
|     "zustand": "^5.0.3" |     "zustand": "^5.0.3" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@kevisual/query": "0.0.15", |     "@kevisual/query": "0.0.15", | ||||||
|     "@kevisual/types": "^0.0.6", |     "@kevisual/types": "^0.0.6", | ||||||
|     "@tailwindcss/vite": "^4.0.16", |     "@tailwindcss/vite": "^4.0.17", | ||||||
|     "@types/node": "^22.13.13", |     "@types/node": "^22.13.14", | ||||||
|     "@types/react": "^19.0.12", |     "@types/react": "^19.0.12", | ||||||
|     "@types/react-dom": "^19.0.4", |     "@types/react-dom": "^19.0.4", | ||||||
|     "@vitejs/plugin-react": "^4.3.4", |     "@vitejs/plugin-react": "^4.3.4", | ||||||
|     "tailwindcss": "^4.0.16", |     "tailwindcss": "^4.0.17", | ||||||
|     "typescript": "^5.8.2", |     "typescript": "^5.8.2", | ||||||
|     "vite": "^6.2.3" |     "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'; | 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'; | import { basename } from '../modules/basename'; | ||||||
| console.log('basename', basename); | console.log('basename', basename); | ||||||
|  | import { App as MarkApp } from '../manager/Manager'; | ||||||
| export const App = () => { | 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 || '/'; | 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/ | // https://vitejs.dev/config/ | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   plugins: [react(), tailwindcss()], |   plugins: [react(), tailwindcss()], | ||||||
| @@ -38,13 +49,7 @@ export default defineConfig({ | |||||||
|         rewriteWsOrigin: true, |         rewriteWsOrigin: true, | ||||||
|         rewrite: (path) => path.replace(/^\/api/, '/api'), |         rewrite: (path) => path.replace(/^\/api/, '/api'), | ||||||
|       }, |       }, | ||||||
|       '/api/router': { |       ...proxy, | ||||||
|         target: 'ws://localhost:3000', |  | ||||||
|         changeOrigin: true, |  | ||||||
|         ws: true, |  | ||||||
|         rewriteWsOrigin: true, |  | ||||||
|         rewrite: (path) => path.replace(/^\/api/, '/api'), |  | ||||||
|       }, |  | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user